mirror of
https://github.com/containrrr/watchtower.git
synced 2025-09-22 05:40:50 +02:00
Compare commits
128 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
76f9cea516 | ||
![]() |
7ba3049ef9 | ||
![]() |
af3ad21740 | ||
![]() |
0a14f3aa9c | ||
![]() |
588ba43f39 | ||
![]() |
9411b374b2 | ||
![]() |
2d7735fea6 | ||
![]() |
6b57003a53 | ||
![]() |
01fd38b3a9 | ||
![]() |
de1304085a | ||
![]() |
7fbdd2f49b | ||
![]() |
69d9e7f297 | ||
![]() |
2576ba3715 | ||
![]() |
a031189cd0 | ||
![]() |
2dc0122873 | ||
![]() |
1b898dbc70 | ||
![]() |
cfc00eef0a | ||
![]() |
a047c7f9ff | ||
![]() |
14a468d380 | ||
![]() |
e6fef8d7b5 | ||
![]() |
458d66147a | ||
![]() |
097df11000 | ||
![]() |
48539c4faf | ||
![]() |
7fb04d0411 | ||
![]() |
887569f453 | ||
![]() |
bd9d7309d1 | ||
![]() |
9aecd33f24 | ||
![]() |
3d1ed2381b | ||
![]() |
2b56ee1f94 | ||
![]() |
c4d493881d | ||
![]() |
dd54055143 | ||
![]() |
40b8c77100 | ||
![]() |
72e437f173 | ||
![]() |
8aa41b8101 | ||
![]() |
0a30955818 | ||
![]() |
284066a39d | ||
![]() |
1754dd185d | ||
![]() |
2abaa47fd3 | ||
![]() |
623f4e67fb | ||
![]() |
9180e9558e | ||
![]() |
9b28fbc24d | ||
![]() |
d1f58c538a | ||
![]() |
8e3bde7e0b | ||
![]() |
79ebad0e19 | ||
![]() |
897b1714d0 | ||
![]() |
650acde015 | ||
![]() |
1d5a8d9a4c | ||
![]() |
7648e9c98a | ||
![]() |
6685fef287 | ||
![]() |
856fd25b60 | ||
![]() |
da3988363f | ||
![]() |
11423d1aae | ||
![]() |
a56b9bdb7c | ||
![]() |
e8affe3fef | ||
![]() |
36391b0ae7 | ||
![]() |
cd458fa526 | ||
![]() |
b801b63881 | ||
![]() |
2e643ed7da | ||
![]() |
0d2eba1f9f | ||
![]() |
02da45d3f8 | ||
![]() |
9f60766692 | ||
![]() |
139f67270b | ||
![]() |
32204a7c2d | ||
![]() |
30280e38b4 | ||
![]() |
7948242260 | ||
![]() |
be113d7798 | ||
![]() |
dca45f50cb | ||
![]() |
bba9b2b100 | ||
![]() |
a5d7f23d2e | ||
![]() |
5eb00cc7e5 | ||
![]() |
7dc8d9f5b0 | ||
![]() |
dfe4346ab3 | ||
![]() |
32988aa9bc | ||
![]() |
1b3a5d7921 | ||
![]() |
787ce72ffd | ||
![]() |
244e3ce737 | ||
![]() |
243b217dad | ||
![]() |
c7c1aee20b | ||
![]() |
170c79d7e4 | ||
![]() |
a7ca7832ff | ||
![]() |
ad644fc756 | ||
![]() |
b0acc8f626 | ||
![]() |
4495445648 | ||
![]() |
118069162f | ||
![]() |
6c9dd5012e | ||
![]() |
1fed2a87a6 | ||
![]() |
4a5c03823e | ||
![]() |
89da17a23f | ||
![]() |
9806c95331 | ||
![]() |
bac02e74af | ||
![]() |
0a74e509fb | ||
![]() |
021e9f3320 | ||
![]() |
43a296aee3 | ||
![]() |
e271286799 | ||
![]() |
080292661e | ||
![]() |
c7499e8b34 | ||
![]() |
b27bd05130 | ||
![]() |
47dcb4925b | ||
![]() |
9957fffbf4 | ||
![]() |
44436ebda8 | ||
![]() |
311bb93986 | ||
![]() |
aec7762386 | ||
![]() |
0a5bd54fb7 | ||
![]() |
dd1ec09668 | ||
![]() |
25fdb40312 | ||
![]() |
aa50d12389 | ||
![]() |
a0fe4a4694 | ||
![]() |
cfcbcac8b0 | ||
![]() |
4d661bf63b | ||
![]() |
9d6b008b4b | ||
![]() |
a34c02f0b4 | ||
![]() |
46f24d232b | ||
![]() |
288ed1129c | ||
![]() |
3f091f1c05 | ||
![]() |
d0617aa11c | ||
![]() |
dfcaf07b95 | ||
![]() |
035572f0bd | ||
![]() |
81d23760c8 | ||
![]() |
c6c01c80e9 | ||
![]() |
df1b86bc29 | ||
![]() |
9470bf81c5 | ||
![]() |
bbbe04119c | ||
![]() |
6ace7bd0dd | ||
![]() |
ee8f293f47 | ||
![]() |
bde1383cd9 | ||
![]() |
7de73560f1 | ||
![]() |
b94741dbec | ||
![]() |
98d0c47391 |
86 changed files with 4350 additions and 2031 deletions
|
@ -5,6 +5,30 @@
|
|||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"contributors": [
|
||||
{
|
||||
"login": "piksel",
|
||||
"name": "nils måsén",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/807383?v=4",
|
||||
"profile": "https://piksel.se",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"maintenance",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "simskij",
|
||||
"name": "Simon Aronsson",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1596025?v=4",
|
||||
"profile": "http://simme.dev",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"maintenance",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Codelica",
|
||||
"name": "James",
|
||||
|
@ -273,18 +297,6 @@
|
|||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "simskij",
|
||||
"name": "Simon Aronsson",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1596025?v=4",
|
||||
"profile": "http://simme.dev",
|
||||
"contributions": [
|
||||
"code",
|
||||
"maintenance",
|
||||
"review",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Ansem93",
|
||||
"name": "Ansem93",
|
||||
|
@ -508,16 +520,6 @@
|
|||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "piksel",
|
||||
"name": "nils måsén",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/807383?v=4",
|
||||
"profile": "https://piksel.se",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "arnested",
|
||||
"name": "Arne Jørgensen",
|
||||
|
@ -841,6 +843,12 @@
|
|||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "andriibratanin",
|
||||
"name": "Andrii Bratanin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20169213?v=4",
|
||||
"profile": "https://github.com/andriibratanin"
|
||||
},
|
||||
{
|
||||
"login": "IAmTamal",
|
||||
"name": "Tamal Das ",
|
||||
|
@ -849,6 +857,25 @@
|
|||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "testwill",
|
||||
"name": "guangwu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8717479?v=4",
|
||||
"profile": "https://github.com/testwill",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nothub",
|
||||
"name": "Florian Hübner",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/48992448?v=4",
|
||||
"profile": "http://hub.lol",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
@ -857,5 +884,6 @@
|
|||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"commitConvention": "none",
|
||||
"skipCi": true
|
||||
"skipCi": true,
|
||||
"commitType": "docs"
|
||||
}
|
||||
|
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -31,7 +31,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
@ -44,7 +44,7 @@ jobs:
|
|||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
|
@ -55,7 +55,7 @@ jobs:
|
|||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
@ -69,4 +69,4 @@ jobs:
|
|||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
|
12
.github/workflows/dependabot-approve.yml
vendored
Normal file
12
.github/workflows/dependabot-approve.yml
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
name: Auto approve dependabot PRs
|
||||
|
||||
on: pull_request_target
|
||||
|
||||
jobs:
|
||||
auto-approve:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
steps:
|
||||
- uses: hmarr/auto-approve-action@v3
|
10
.github/workflows/publish-docs.yml
vendored
10
.github/workflows/publish-docs.yml
vendored
|
@ -14,11 +14,17 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
- name: Build tplprev
|
||||
run: scripts/build-tplprev.sh
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: 'pip'
|
||||
|
|
24
.github/workflows/pull-request.yml
vendored
24
.github/workflows/pull-request.yml
vendored
|
@ -12,16 +12,16 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.20.x
|
||||
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
||||
with:
|
||||
version: "2022.1.1"
|
||||
version: "2023.1.6"
|
||||
install-go: "false" # StaticCheck uses go v1.17 which does not support `any`
|
||||
test:
|
||||
name: Test
|
||||
|
@ -29,7 +29,7 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
go-version:
|
||||
- 1.18.x
|
||||
- 1.20.x
|
||||
platform:
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
|
@ -37,13 +37,13 @@ jobs:
|
|||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.20.x
|
||||
- name: Run tests
|
||||
run: |
|
||||
go test -v -coverprofile coverage.out -covermode atomic ./...
|
||||
|
@ -56,15 +56,15 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.20.x
|
||||
- name: Build
|
||||
uses: goreleaser/goreleaser-action@8f67e590f2d095516493f017008adc464e63adb1 #v3
|
||||
uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3
|
||||
with:
|
||||
version: v0.155.0
|
||||
args: --snapshot --skip-publish --debug
|
||||
|
|
18
.github/workflows/release-dev.yaml
vendored
18
.github/workflows/release-dev.yaml
vendored
|
@ -10,21 +10,23 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
- name: Build
|
||||
run: ./build.sh
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.20.x
|
||||
- name: Test
|
||||
run: go test -v -coverprofile coverage.out -covermode atomic ./...
|
||||
- name: Publish coverage
|
||||
|
@ -37,7 +39,7 @@ jobs:
|
|||
- test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Publish to Docker Hub
|
||||
uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14
|
||||
with:
|
||||
|
|
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
|
@ -2,9 +2,7 @@ name: Release (Production)
|
|||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- '**/v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
@ -15,13 +13,13 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.20.x
|
||||
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
||||
with:
|
||||
version: "2022.1.1"
|
||||
|
@ -32,7 +30,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- 1.18.x
|
||||
- 1.20.x
|
||||
platform:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
|
@ -40,13 +38,13 @@ jobs:
|
|||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.20.x
|
||||
- name: Run tests
|
||||
run: |
|
||||
go test ./... -coverprofile coverage.out
|
||||
|
@ -59,29 +57,29 @@ jobs:
|
|||
- lint
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
go-version: 1.20.x
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2
|
||||
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2
|
||||
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2
|
||||
with:
|
||||
username: ${{ secrets.BOT_USERNAME }}
|
||||
password: ${{ secrets.BOT_GHCR_PAT }}
|
||||
registry: ghcr.io
|
||||
- name: Build
|
||||
uses: goreleaser/goreleaser-action@8f67e590f2d095516493f017008adc464e63adb1 #v3
|
||||
uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3
|
||||
with:
|
||||
version: v0.155.0
|
||||
args: --debug
|
||||
|
@ -93,7 +91,7 @@ jobs:
|
|||
echo '{"experimental": "enabled"}' > ~/.docker/config.json
|
||||
- name: Create manifest for version
|
||||
run: |
|
||||
export DH_TAG=$(echo $TAG | sed 's/^v*//')
|
||||
export DH_TAG=$(git tag --points-at HEAD | sed 's/^v*//')
|
||||
docker manifest create \
|
||||
containrrr/watchtower:$DH_TAG \
|
||||
containrrr/watchtower:amd64-$DH_TAG \
|
||||
|
@ -191,7 +189,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Pull new module version
|
||||
uses: andrewslotin/go-proxy-pull-action@bfc19ec6536e1638181b2ad6a03e16c7ccfb122f #master@2022-10-14
|
||||
uses: andrewslotin/go-proxy-pull-action@50fea06a976087614babb9508e5c528b464f4645 #master@2022-10-14
|
||||
|
||||
|
||||
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -8,3 +8,6 @@ dist
|
|||
/site
|
||||
coverage.out
|
||||
*.coverprofile
|
||||
|
||||
docs/assets/wasm_exec.js
|
||||
docs/assets/*.wasm
|
192
README.md
192
README.md
|
@ -31,6 +31,8 @@ $ docker run --detach \
|
|||
containrrr/watchtower
|
||||
```
|
||||
|
||||
Watchtower is intended to be used in homelabs, media centers, local dev environments, and similar. We do **not** recommend using Watchtower in a commercial or production environment. If that is you, you should be looking into using Kubernetes. If that feels like too big a step for you, please look into solutions like [MicroK8s](https://microk8s.io/) and [k3s](https://k3s.io/) that take away a lot of the toil of running a Kubernetes cluster.
|
||||
|
||||
## Documentation
|
||||
The full documentation is available at https://containrrr.dev/watchtower.
|
||||
|
||||
|
@ -44,126 +46,128 @@ 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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Documentation">📖</a> <a href="#maintenance-piksel" title="Maintenance">🚧</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Apiksel" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><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="https://github.com/containrrr/watchtower/commits?author=simskij" title="Documentation">📖</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></td>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
</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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
</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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
</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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
</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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
</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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
</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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
</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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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="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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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>
|
||||
<td align="center" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><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" valign="top" width="14.28%"><a href="https://github.com/testwill"><img src="https://avatars.githubusercontent.com/u/8717479?v=4?s=100" width="100px;" alt="guangwu"/><br /><sub><b>guangwu</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=testwill" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://hub.lol"><img src="https://avatars.githubusercontent.com/u/48992448?v=4?s=100" width="100px;" alt="Florian Hübner"/><br /><sub><b>Florian Hübner</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=nothub" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=nothub" 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 -->
|
||||
|
|
46
cmd/root.go
46
cmd/root.go
|
@ -1,6 +1,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -32,13 +33,16 @@ var (
|
|||
scheduleSpec string
|
||||
cleanup bool
|
||||
noRestart bool
|
||||
noPull bool
|
||||
monitorOnly bool
|
||||
enableLabel bool
|
||||
disableContainers []string
|
||||
notifier t.Notifier
|
||||
timeout time.Duration
|
||||
lifecycleHooks bool
|
||||
rollingRestart bool
|
||||
scope string
|
||||
labelPrecedence bool
|
||||
)
|
||||
|
||||
var rootCmd = NewRootCommand()
|
||||
|
@ -77,23 +81,8 @@ func Execute() {
|
|||
func PreRun(cmd *cobra.Command, _ []string) {
|
||||
f := cmd.PersistentFlags()
|
||||
flags.ProcessFlagAliases(f)
|
||||
|
||||
if enabled, _ := f.GetBool("no-color"); enabled {
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: true,
|
||||
})
|
||||
} else {
|
||||
// enable logrus built-in support for https://bixense.com/clicolors/
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
EnvironmentOverrideColors: true,
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
if err := flags.SetupLogging(f); err != nil {
|
||||
log.Fatalf("Failed to initialize logging: %s", err.Error())
|
||||
}
|
||||
|
||||
scheduleSpec, _ = f.GetString("schedule")
|
||||
|
@ -106,9 +95,11 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
}
|
||||
|
||||
enableLabel, _ = f.GetBool("label-enable")
|
||||
disableContainers, _ = f.GetStringSlice("disable-containers")
|
||||
lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
|
||||
rollingRestart, _ = f.GetBool("rolling-restart")
|
||||
scope, _ = f.GetString("scope")
|
||||
labelPrecedence, _ = f.GetBool("label-take-precedence")
|
||||
|
||||
if scope != "" {
|
||||
log.Debugf(`Using scope %q`, scope)
|
||||
|
@ -120,7 +111,7 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
noPull, _ := f.GetBool("no-pull")
|
||||
noPull, _ = f.GetBool("no-pull")
|
||||
includeStopped, _ := f.GetBool("include-stopped")
|
||||
includeRestarting, _ := f.GetBool("include-restarting")
|
||||
reviveStopped, _ := f.GetBool("revive-stopped")
|
||||
|
@ -132,7 +123,6 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
}
|
||||
|
||||
client = container.NewClient(container.ClientOptions{
|
||||
PullImages: !noPull,
|
||||
IncludeStopped: includeStopped,
|
||||
ReviveStopped: reviveStopped,
|
||||
RemoveVolumes: removeVolumes,
|
||||
|
@ -146,12 +136,22 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
|
||||
// Run is the main execution flow of the command
|
||||
func Run(c *cobra.Command, names []string) {
|
||||
filter, filterDesc := filters.BuildFilter(names, enableLabel, scope)
|
||||
filter, filterDesc := filters.BuildFilter(names, disableContainers, enableLabel, scope)
|
||||
runOnce, _ := c.PersistentFlags().GetBool("run-once")
|
||||
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
|
||||
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")
|
||||
unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls")
|
||||
apiToken, _ := c.PersistentFlags().GetString("http-api-token")
|
||||
healthCheck, _ := c.PersistentFlags().GetBool("health-check")
|
||||
|
||||
if healthCheck {
|
||||
// health check should not have pid 1
|
||||
if os.Getpid() == 1 {
|
||||
time.Sleep(1 * time.Second)
|
||||
log.Fatal("The health check flag should never be passed to the main watchtower container process")
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if rollingRestart && monitorOnly {
|
||||
log.Fatal("Rolling restarts is not compatible with the global monitor only flag")
|
||||
|
@ -187,7 +187,7 @@ func Run(c *cobra.Command, names []string) {
|
|||
metrics.RegisterScan(metric)
|
||||
}, updateLock)
|
||||
httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle)
|
||||
// If polling isn't enabled the scheduler is never started and
|
||||
// If polling isn't enabled the scheduler is never started, and
|
||||
// we need to trigger the startup messages manually.
|
||||
if !unblockHTTPAPI {
|
||||
writeStartupMessage(c, time.Time{}, filterDesc)
|
||||
|
@ -199,7 +199,7 @@ func Run(c *cobra.Command, names []string) {
|
|||
httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle)
|
||||
}
|
||||
|
||||
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && err != http.ErrServerClosed {
|
||||
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Error("failed to start API", err)
|
||||
}
|
||||
|
||||
|
@ -366,6 +366,8 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
|
|||
MonitorOnly: monitorOnly,
|
||||
LifecycleHooks: lifecycleHooks,
|
||||
RollingRestart: rollingRestart,
|
||||
LabelPrecedence: labelPrecedence,
|
||||
NoPull: noPull,
|
||||
}
|
||||
result, err := actions.Update(client, updateParams)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM --platform=$BUILDPLATFORM alpine:3.17.1 as alpine
|
||||
FROM --platform=$BUILDPLATFORM alpine:3.19.0 as alpine
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
|
@ -17,4 +17,7 @@ COPY --from=alpine \
|
|||
EXPOSE 8080
|
||||
|
||||
COPY watchtower /
|
||||
|
||||
HEALTHCHECK CMD [ "/watchtower", "--health-check"]
|
||||
|
||||
ENTRYPOINT ["/watchtower"]
|
||||
|
|
|
@ -7,6 +7,13 @@ FROM golang:alpine as builder
|
|||
# use version (for example "v0.3.3") or "main"
|
||||
ARG WATCHTOWER_VERSION=main
|
||||
|
||||
# Pre download required modules to avoid redownloading at each build thanks to docker layer caching.
|
||||
# Copying go.mod and go.sum ensure to invalid the layer/build cache if there is a change in module requirement
|
||||
WORKDIR /watchtower
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN go mod download
|
||||
|
||||
RUN apk add --no-cache \
|
||||
alpine-sdk \
|
||||
ca-certificates \
|
||||
|
@ -35,4 +42,6 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifi
|
|||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
COPY --from=builder /watchtower/watchtower /watchtower
|
||||
|
||||
HEALTHCHECK CMD [ "/watchtower", "--health-check"]
|
||||
|
||||
ENTRYPOINT ["/watchtower"]
|
||||
|
|
|
@ -35,4 +35,6 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifi
|
|||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
COPY --from=builder /go/watchtower/watchtower /watchtower
|
||||
|
||||
HEALTHCHECK CMD [ "/watchtower", "--health-check"]
|
||||
|
||||
ENTRYPOINT ["/watchtower"]
|
||||
|
|
17
dockerfiles/container-networking/docker-compose.yml
Normal file
17
dockerfiles/container-networking/docker-compose.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
services:
|
||||
producer:
|
||||
image: qmcgaw/gluetun:v3.35.0
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
- VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
|
||||
- OPENVPN_USER=${OPENVPN_USER}
|
||||
- OPENVPN_PASSWORD=${OPENVPN_PASSWORD}
|
||||
- SERVER_COUNTRIES=${SERVER_COUNTRIES}
|
||||
consumer:
|
||||
depends_on:
|
||||
- producer
|
||||
image: nginx:1.25.1
|
||||
network_mode: "service:producer"
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.depends-on=/wt-contnet-producer-1"
|
|
@ -27,6 +27,33 @@ In the example above, watchtower will execute an upgrade attempt on the containe
|
|||
|
||||
When no arguments are specified, watchtower will monitor all running containers.
|
||||
|
||||
## Secrets/Files
|
||||
|
||||
Some arguments can also reference a file, in which case the contents of the file are used as the value.
|
||||
This can be used to avoid putting secrets in the configuration file or command line.
|
||||
|
||||
The following arguments are currently supported (including their corresponding `WATCHTOWER_` environment variables):
|
||||
- `notification-url`
|
||||
- `notification-email-server-password`
|
||||
- `notification-slack-hook-url`
|
||||
- `notification-msteams-hook`
|
||||
- `notification-gotify-token`
|
||||
- `http-api-token`
|
||||
|
||||
### Example docker-compose usage
|
||||
```yaml
|
||||
secrets:
|
||||
access_token:
|
||||
file: access_token
|
||||
|
||||
services:
|
||||
watchtower:
|
||||
secrets:
|
||||
- access_token
|
||||
environment:
|
||||
- WATCHTOWER_HTTP_API_TOKEN=/run/secrets/access_token
|
||||
```
|
||||
|
||||
## Help
|
||||
Shows documentation about the supported flags.
|
||||
|
||||
|
@ -58,8 +85,8 @@ Environment Variable: WATCHTOWER_CLEANUP
|
|||
Default: false
|
||||
```
|
||||
|
||||
## Remove attached volumes
|
||||
Removes attached volumes after updating. When this flag is specified, watchtower will remove all attached volumes from the container before restarting with a new image. Use this option to force new volumes to be populated as containers are updated.
|
||||
## Remove anonymous volumes
|
||||
Removes anonymous volumes after updating. When this flag is specified, watchtower will remove all anonymous volumes from the container before restarting with a new image. Named volumes will not be removed!
|
||||
|
||||
```text
|
||||
Argument: --remove-volumes
|
||||
|
@ -107,6 +134,17 @@ Environment Variable: WATCHTOWER_LOG_LEVEL
|
|||
Default: info
|
||||
```
|
||||
|
||||
## Logging format
|
||||
|
||||
Sets what logging format to use for console output.
|
||||
|
||||
```text
|
||||
Argument: --log-format, -l
|
||||
Environment Variable: WATCHTOWER_LOG_FORMAT
|
||||
Possible values: Auto, LogFmt, Pretty or JSON
|
||||
Default: Auto
|
||||
```
|
||||
|
||||
## ANSI colors
|
||||
Disable ANSI color escape codes in log output.
|
||||
|
||||
|
@ -151,7 +189,7 @@ Environment Variable: WATCHTOWER_INCLUDE_RESTARTING
|
|||
Will also include created and exited containers.
|
||||
|
||||
```text
|
||||
Argument: --include-stopped
|
||||
Argument: --include-stopped, -S
|
||||
Environment Variable: WATCHTOWER_INCLUDE_STOPPED
|
||||
Type: Boolean
|
||||
Default: false
|
||||
|
@ -178,7 +216,7 @@ Environment Variable: WATCHTOWER_POLL_INTERVAL
|
|||
```
|
||||
|
||||
## Filter by enable label
|
||||
Update containers that have a `com.centurylinklabs.watchtower.enable` label set to true.
|
||||
Monitor and update containers that have a `com.centurylinklabs.watchtower.enable` label set to true.
|
||||
|
||||
```text
|
||||
Argument: --label-enable
|
||||
|
@ -188,10 +226,23 @@ Environment Variable: WATCHTOWER_LABEL_ENABLE
|
|||
```
|
||||
|
||||
## Filter by disable label
|
||||
__Do not__ update containers that have `com.centurylinklabs.watchtower.enable` label set to false and
|
||||
__Do not__ Monitor and update containers that have `com.centurylinklabs.watchtower.enable` label set to false and
|
||||
no `--label-enable` argument is passed. Note that only one or the other (targeting by enable label) can be
|
||||
used at the same time to target containers.
|
||||
|
||||
## Filter by disabling specific container names
|
||||
Monitor and update containers whose names are not in a given set of names.
|
||||
|
||||
This can be used to exclude specific containers, when setting labels is not an option.
|
||||
The listed containers will be excluded even if they have the enable filter set to true.
|
||||
|
||||
```text
|
||||
Argument: --disable-containers, -x
|
||||
Environment Variable: WATCHTOWER_DISABLE_CONTAINERS
|
||||
Type: Comma- or space-separated string list
|
||||
Default: ""
|
||||
```
|
||||
|
||||
## Without updating containers
|
||||
Will only monitor for new images, send notifications and invoke
|
||||
the [pre-check/post-check hooks](https://containrrr.dev/watchtower/lifecycle-hooks/), but will __not__ update the
|
||||
|
@ -211,6 +262,19 @@ Environment Variable: WATCHTOWER_MONITOR_ONLY
|
|||
|
||||
Note that monitor-only can also be specified on a per-container basis with the `com.centurylinklabs.watchtower.monitor-only` label set on those containers.
|
||||
|
||||
See [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set
|
||||
|
||||
## With label taking precedence over arguments
|
||||
|
||||
By default, arguments will take precedence over labels. This means that if you set `WATCHTOWER_MONITOR_ONLY` to true or use `--monitor-only`, a container with `com.centurylinklabs.watchtower.monitor-only` set to false will not be updated. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will also be updated. This also apply to the no pull option. if you set `WATCHTOWER_NO_PULL` to true or use `--no-pull`, a container with `com.centurylinklabs.watchtower.no-pull` set to false will not pull the new image. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will pull image
|
||||
|
||||
```text
|
||||
Argument: --label-take-precedence
|
||||
Environment Variable: WATCHTOWER_LABEL_TAKE_PRECEDENCE
|
||||
Type: Boolean
|
||||
Default: false
|
||||
```
|
||||
|
||||
## Without restarting containers
|
||||
Do not restart containers after updating. This option can be useful when the start of the containers
|
||||
is managed by an external system such as systemd.
|
||||
|
@ -234,6 +298,11 @@ Environment Variable: WATCHTOWER_NO_PULL
|
|||
Default: false
|
||||
```
|
||||
|
||||
Note that no-pull can also be specified on a per-container basis with the
|
||||
`com.centurylinklabs.watchtower.no-pull` label set on those containers.
|
||||
|
||||
See [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set
|
||||
|
||||
## Without sending a startup message
|
||||
Do not send a message after watchtower started. Otherwise there will be an info-level notification.
|
||||
|
||||
|
@ -248,7 +317,7 @@ Environment Variable: WATCHTOWER_NO_STARTUP_MESSAGE
|
|||
Run an update attempt against a container name list one time immediately and exit.
|
||||
|
||||
```text
|
||||
Argument: --run-once
|
||||
Argument: --run-once, -R
|
||||
Environment Variable: WATCHTOWER_RUN_ONCE
|
||||
Type: Boolean
|
||||
Default: false
|
||||
|
@ -267,6 +336,7 @@ Environment Variable: WATCHTOWER_HTTP_API_UPDATE
|
|||
|
||||
## HTTP API Token
|
||||
Sets an authentication token to HTTP API requests.
|
||||
Can also reference a file, in which case the contents of the file are used.
|
||||
|
||||
```text
|
||||
Argument: --http-api-token
|
||||
|
@ -289,6 +359,11 @@ Environment Variable: WATCHTOWER_HTTP_API_PERIODIC_POLLS
|
|||
Update containers that have a `com.centurylinklabs.watchtower.scope` label set with the same value as the given argument.
|
||||
This enables [running multiple instances](https://containrrr.dev/watchtower/running-multiple-instances).
|
||||
|
||||
!!! note "Filter by lack of scope"
|
||||
If you want other instances of watchtower to ignore the scoped containers, set this argument to `none`.
|
||||
When omitted, watchtower will update all containers regardless of scope.
|
||||
|
||||
|
||||
```text
|
||||
Argument: --scope
|
||||
Environment Variable: WATCHTOWER_SCOPE
|
||||
|
@ -361,3 +436,32 @@ Environment Variable: WATCHTOWER_WARN_ON_HEAD_FAILURE
|
|||
Possible values: always, auto, never
|
||||
Default: auto
|
||||
```
|
||||
|
||||
## Health check
|
||||
|
||||
Returns a success exit code to enable usage with docker `HEALTHCHECK`. This check is naive and only returns checks whether there is another process running inside the container, as it is the only known form of failure state for watchtowers container.
|
||||
|
||||
!!! note "Only for HEALTHCHECK use"
|
||||
Never put this on the main container executable command line as it is only meant to be run from docker HEALTHCHECK.
|
||||
|
||||
```text
|
||||
Argument: --health-check
|
||||
```
|
||||
|
||||
## Programatic Output (porcelain)
|
||||
|
||||
Writes the session results to STDOUT using a stable, machine-readable format (indicated by the argument VERSION).
|
||||
|
||||
Alias for:
|
||||
|
||||
```text
|
||||
--notification-url logger://
|
||||
--notification-log-stdout
|
||||
--notification-report
|
||||
--notification-template porcelain.VERSION.summary-no-log
|
||||
|
||||
Argument: --porcelain, -P
|
||||
Environment Variable: WATCHTOWER_PORCELAIN
|
||||
Possible values: v1
|
||||
Default: -
|
||||
```
|
||||
|
|
|
@ -58,6 +58,7 @@ If instead you want to [only include containers with the enable label](https://c
|
|||
If you wish to create a monitoring scope, you will need to [run multiple instances and set a scope for each of them](https://containrrr.github.io/watchtower/running-multiple-instances).
|
||||
|
||||
Watchtower filters running containers by testing them against each configured criteria. A container is monitored if all criteria are met. For example:
|
||||
|
||||
- If a container's name is on the monitoring name list (not empty `--name` argument) but it is not enabled (_centurylinklabs.watchtower.enable=false_), it won't be monitored;
|
||||
- If a container's name is not on the monitoring name list (not empty `--name` argument), even if it is enabled (_centurylinklabs.watchtower.enable=true_ and `--label-enable` flag is set), it won't be monitored;
|
||||
|
||||
|
|
|
@ -35,3 +35,11 @@ Notice that there is an environment variable named WATCHTOWER_HTTP_API_TOKEN. To
|
|||
```bash
|
||||
curl -H "Authorization: Bearer mytoken" localhost:8080/v1/update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
In order to update only certain images, the image names can be provided as URL query parameters. The following `curl` command would trigger an update for the images `foo/bar` and `foo/baz`:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer mytoken" localhost:8080/v1/update?image=foo/bar,foo/baz
|
||||
```
|
||||
|
|
|
@ -11,5 +11,5 @@ CONTAINER ID IMAGE STATUS PORTS
|
|||
6cc4d2a9d1a5 containrrr/watchtower Up 15 minutes watchtower
|
||||
```
|
||||
|
||||
Every few minutes watchtower will pull the latest _centurylink/wetty-cli_ image and compare it to the one that was used to run the "wetty" container. If it sees that the image has changed it will stop/remove the "wetty" container and then restart it using the new image and the same `docker run` options that were used to start the container initially (in this case, that would include the `-p 8080:3000` port mapping).
|
||||
Every day watchtower will pull the latest _centurylink/wetty-cli_ image and compare it to the one that was used to run the "wetty" container. If it sees that the image has changed it will stop/remove the "wetty" container and then restart it using the new image and the same `docker run` options that were used to start the container initially (in this case, that would include the `-p 8080:3000` port mapping).
|
||||
|
||||
|
|
|
@ -2,4 +2,6 @@ Watchtower will detect if there are links between any of the running containers
|
|||
|
||||
For example, imagine you were running a _mysql_ container and a _wordpress_ container which had been linked to the _mysql_ container. If watchtower were to detect that the _mysql_ container required an update, it would first shut down the linked _wordpress_ container followed by the _mysql_ container. When restarting the containers it would handle _mysql_ first and then _wordpress_ to ensure that the link continued to work.
|
||||
|
||||
If you want to override existing links you can use special `com.centurylinklabs.watchtower.depends-on` label with dependent container names, separated by a comma.
|
||||
If you want to override existing links, or if you are not using links, you can use special `com.centurylinklabs.watchtower.depends-on` label with dependent container names, separated by a comma.
|
||||
|
||||
When you have a depending container that is using `network_mode: service:container` then watchtower will treat that container as an implicit link.
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Metrics can be used to track how Watchtower behaves over time.
|
||||
|
||||
To use this feature, you have to set an [API token](arguments.md#http-api-token) and [enable the metrics API](arguments.md#http-api-metrics),
|
||||
To use this feature, you have to set an [API token](arguments.md#http_api_token) and [enable the metrics API](arguments.md#http_api_metrics),
|
||||
as well as creating a port mapping for your container for port `8080`.
|
||||
|
||||
The metrics API endpoint is `/v1/metrics`.
|
||||
|
|
|
@ -18,19 +18,20 @@ system, [logrus](http://github.com/sirupsen/logrus).
|
|||
|
||||
- `--notifications-level` (env. `WATCHTOWER_NOTIFICATIONS_LEVEL`): Controls the log level which is used for the notifications. If omitted, the default log level is `info`. Possible values are: `panic`, `fatal`, `error`, `warn`, `info`, `debug` or `trace`.
|
||||
- `--notifications-hostname` (env. `WATCHTOWER_NOTIFICATIONS_HOSTNAME`): Custom hostname specified in subject/title. Useful to override the operating system hostname.
|
||||
- `--notifications-delay` (env. `WATCHTOWER_NOTIFICATION_DELAY`): Delay before sending notifications expressed in seconds.
|
||||
- `--notifications-delay` (env. `WATCHTOWER_NOTIFICATIONS_DELAY`): Delay before sending notifications expressed in seconds.
|
||||
- Watchtower will post a notification every time it is started. This behavior [can be changed](https://containrrr.github.io/watchtower/arguments/#without_sending_a_startup_message) with an argument.
|
||||
- `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.
|
||||
- `--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.
|
||||
- `--notification-log-stdout` (env. `WATCHTOWER_NOTIFICATION_LOG_STDOUT`): Enable output from `logger://` shoutrrr service to stdout.
|
||||
|
||||
## [shoutrrr](https://github.com/containrrr/shoutrrr) notifications
|
||||
## [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
|
||||
Go to [containrrr.dev/shoutrrr/v0.8/services/overview](https://containrrr.dev/shoutrrr/v0.8/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)
|
||||
|
||||
|
@ -56,6 +57,10 @@ outputs timestamp and log level.
|
|||
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.
|
||||
|
||||
!!! note "Skipping notifications"
|
||||
To skip sending notifications that do not contain any information, you can wrap your template with `{{if .}}` and `{{end}}`.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
|
@ -110,7 +115,7 @@ Example using a custom report template that always sends a session report after
|
|||
docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WATCHTOWER_NOTIFICATION_REPORT="true"
|
||||
-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 -}}
|
||||
|
@ -130,7 +135,7 @@ Example using a custom report template that always sends a session report after
|
|||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
|
||||
{{range .Entries -}}{{.Message}}{{\"\n\"}}{{- end -}}
|
||||
{{- end -}}
|
||||
" \
|
||||
containrrr/watchtower
|
||||
|
|
|
@ -23,19 +23,29 @@ password `auth` string:
|
|||
```
|
||||
|
||||
`<REGISTRY_NAME>` needs to be replaced by the name of your private registry
|
||||
(e.g., `my-private-registry.example.org`)
|
||||
(e.g., `my-private-registry.example.org`).
|
||||
|
||||
!!! important "Using private images on docker hub"
|
||||
When using private images on docker hub, the containers beeing watched needs to use the full image name, including the repository prefix `index.docker.io`.
|
||||
So instead of
|
||||
```
|
||||
docker run -d myuser/myimage
|
||||
```
|
||||
you would run it as
|
||||
```
|
||||
docker run -d index.docker.io/myuser/myimage
|
||||
```
|
||||
!!! info "Using private images on Docker Hub"
|
||||
To access private repositories on Docker Hub,
|
||||
`<REGISTRY_NAME>` should be `https://index.docker.io/v1/`.
|
||||
In this special case, the registry domain does not have to be specified
|
||||
in `docker run` or `docker-compose`. Like Docker, Watchtower will use the
|
||||
Docker Hub registry and its credentials when no registry domain is specified.
|
||||
|
||||
<sub>Watchtower will recognize credentials with `<REGISTRY_NAME>` `index.docker.io`,
|
||||
but the Docker CLI will not.</sub>
|
||||
|
||||
!!! important "Using a private registry on a local host"
|
||||
To use a private registry hosted locally, make sure to correctly specify the registry host
|
||||
in both `config.json` and the `docker run` command or `docker-compose` file.
|
||||
Valid hosts are `localhost[:PORT]`, `HOST:PORT`,
|
||||
or any multi-part `domain.name` or IP-address with or without a port.
|
||||
|
||||
Examples:
|
||||
* `localhost` -> `localhost/myimage`
|
||||
* `127.0.0.1` -> `127.0.0.1/myimage:mytag`
|
||||
* `host.domain` -> `host.domain/myorganization/myimage`
|
||||
* `other-lan-host:80` -> `other-lan-host:80/imagename:latest`
|
||||
|
||||
The required `auth` string can be generated as follows:
|
||||
|
||||
|
@ -75,7 +85,7 @@ When creating the watchtower container via docker-compose, use the following lin
|
|||
version: "3.4"
|
||||
services:
|
||||
watchtower:
|
||||
image: index.docker.io/containrrr/watchtower:latest
|
||||
image: containrrr/watchtower:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- <PATH_TO_HOME_DIR>/.docker/config.json:/config.json
|
||||
|
@ -114,7 +124,7 @@ in a volume that may be mounted onto your watchtower container.
|
|||
|
||||
1. Create the Dockerfile (contents below):
|
||||
```Dockerfile
|
||||
FROM golang:1.16
|
||||
FROM golang:1.20
|
||||
|
||||
ENV GO111MODULE off
|
||||
ENV CGO_ENABLED 0
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
By default, Watchtower will clean up other instances and won't allow multiple instances running on the same Docker host or swarm. It is possible to override this behavior by defining a [scope](https://containrrr.github.io/watchtower/arguments/#filter_by_scope) to each running instance.
|
||||
|
||||
Notice that:
|
||||
!!! note
|
||||
- Multiple instances can't run with the same scope;
|
||||
- An instance without a scope will clean up other running instances, even if they have a defined scope;
|
||||
- Supplying `none` as the scope will treat `com.centurylinklabs.watchtower.scope=none`, `com.centurylinklabs.watchtower.scope=` and the lack of a `com.centurylinklabs.watchtower.scope` label as the scope `none`. This effectly enables you to run both scoped and unscoped watchtower instances on the same machine.
|
||||
|
||||
To define an instance monitoring scope, use the `--scope` argument or the `WATCHTOWER_SCOPE` environment variable on startup and set the _com.centurylinklabs.watchtower.scope_ label with the same value for the containers you want to include in this instance's scope (including the instance itself).
|
||||
To define an instance monitoring scope, use the `--scope` argument or the `WATCHTOWER_SCOPE` environment variable on startup and set the `com.centurylinklabs.watchtower.scope` label with the same value for the containers you want to include in this instance's scope (including the instance itself).
|
||||
|
||||
For example, in a Docker Compose config file:
|
||||
|
||||
|
@ -12,16 +13,29 @@ For example, in a Docker Compose config file:
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
app-monitored-by-watchtower:
|
||||
app-with-scope:
|
||||
image: myapps/monitored-by-watchtower
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.scope=myscope"
|
||||
labels: [ "com.centurylinklabs.watchtower.scope=myscope" ]
|
||||
|
||||
watchtower:
|
||||
scoped-watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ]
|
||||
command: --interval 30 --scope myscope
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.scope=myscope"
|
||||
labels: [ "com.centurylinklabs.watchtower.scope=myscope" ]
|
||||
|
||||
unscoped-app-a:
|
||||
image: myapps/app-a
|
||||
|
||||
unscoped-app-b:
|
||||
image: myapps/app-b
|
||||
labels: [ "com.centurylinklabs.watchtower.scope=none" ]
|
||||
|
||||
unscoped-app-c:
|
||||
image: myapps/app-b
|
||||
labels: [ "com.centurylinklabs.watchtower.scope=" ]
|
||||
|
||||
unscoped-watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ]
|
||||
command: --interval 30 --scope none
|
||||
```
|
||||
|
|
251
docs/template-preview.md
Normal file
251
docs/template-preview.md
Normal file
|
@ -0,0 +1,251 @@
|
|||
<style>
|
||||
#tplprev {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 1rem;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
margin-right: -13.3rem
|
||||
}
|
||||
#tplprev textarea {
|
||||
box-decoration-break: slice;
|
||||
overflow: auto;
|
||||
padding: 0.77em 1.18em;
|
||||
scrollbar-color: var(--md-default-fg-color--lighter) transparent;
|
||||
scrollbar-width: thin;
|
||||
touch-action: auto;
|
||||
word-break: normal;
|
||||
height: 420px;
|
||||
flex: 1;
|
||||
}
|
||||
#tplprev .controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
column-gap: 0.5rem
|
||||
}
|
||||
#tplprev textarea, #tplprev input {
|
||||
background-color: var(--md-code-bg-color);
|
||||
border-width: 0;
|
||||
border-radius: 0.1rem;
|
||||
color: var(--md-code-fg-color);
|
||||
font-feature-settings: "kern";
|
||||
font-family: var(--md-code-font-family);
|
||||
}
|
||||
.numfield {
|
||||
font-size: .7rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#tplprev button {
|
||||
border-radius: 0.1rem;
|
||||
color: var(--md-primary-bg-color);
|
||||
background-color: var(--md-primary-fg-color);
|
||||
flex:1;
|
||||
min-width: 12ch;
|
||||
padding: 0.5rem
|
||||
}
|
||||
#tplprev button:hover {
|
||||
background-color: var(--md-accent-fg-color);
|
||||
}
|
||||
#tplprev input[type="number"] { width: 5ch; flex: 1; font-size: 1rem; }
|
||||
#tplprev fieldset {
|
||||
margin-top: -0.5rem;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
#tplprev .template-wrapper {
|
||||
display: flex;
|
||||
flex:1;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
#tplprev .result-wrapper {
|
||||
flex: 1;
|
||||
display: flex
|
||||
}
|
||||
#result {
|
||||
font-size: 0.7rem;
|
||||
background-color: var(--md-code-bg-color);
|
||||
scrollbar-color: var(--md-default-fg-color--lighter) transparent;
|
||||
scrollbar-width: thin;
|
||||
touch-action: auto;
|
||||
overflow: auto;
|
||||
padding: 0.77em 1.18em;
|
||||
margin:0;
|
||||
height: 540px;
|
||||
flex:1;
|
||||
width:100%
|
||||
}
|
||||
#result b {color: var(--md-code-hl-special-color)}
|
||||
#result i {color: var(--md-code-hl-keyword-color)}
|
||||
#tplprev .loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
background: var(--md-code-bg-color);
|
||||
margin-top: 0
|
||||
}
|
||||
</style>
|
||||
<script src="../assets/wasm_exec.js"></script>
|
||||
<script>
|
||||
let wasmLoaded = false;
|
||||
const updatePreview = () => {
|
||||
if (!wasmLoaded) return;
|
||||
const form = document.querySelector('#tplprev');
|
||||
const input = form.template.value;
|
||||
console.log('Input: %o', input);
|
||||
const arrFromCount = (key) => Array.from(Array(form[key]?.valueAsNumber ?? 0), () => key);
|
||||
const states = form.report.value === "yes" ? [
|
||||
...arrFromCount("skipped"),
|
||||
...arrFromCount("scanned"),
|
||||
...arrFromCount("updated"),
|
||||
...arrFromCount("failed" ),
|
||||
...arrFromCount("fresh" ),
|
||||
...arrFromCount("stale" ),
|
||||
] : [];
|
||||
console.log("States: %o", states);
|
||||
const levels = form.log.value === "yes" ? [
|
||||
...arrFromCount("error"),
|
||||
...arrFromCount("warning"),
|
||||
...arrFromCount("info"),
|
||||
...arrFromCount("debug"),
|
||||
] : [];
|
||||
console.log("Levels: %o", levels);
|
||||
const output = WATCHTOWER.tplprev(input, states, levels);
|
||||
console.log('Output: \n%o', output);
|
||||
if (output.startsWith('Error: ')) {
|
||||
document.querySelector('#result').innerHTML = `<b>Error</b>: ${output.substring(7)}`;
|
||||
} else if (output.length) {
|
||||
document.querySelector('#result').innerText = output;
|
||||
} else {
|
||||
document.querySelector('#result').innerHTML = '<i>empty (would not be sent as a notification)</i>';
|
||||
}
|
||||
}
|
||||
const formSubmitted = (e) => {
|
||||
//e.preventDefault();
|
||||
//updatePreview();
|
||||
}
|
||||
let debounce;
|
||||
const inputUpdated = () => {
|
||||
if(debounce) clearTimeout(debounce);
|
||||
debounce = setTimeout(() => updatePreview(), 400);
|
||||
}
|
||||
const formChanged = (e) => {
|
||||
console.log('form changed: %o', e);
|
||||
const targetToggle = e.target.dataset['toggle'];
|
||||
if (targetToggle) {
|
||||
e.target.form[targetToggle].value = e.target.checked ? "yes" : "no";
|
||||
}
|
||||
updatePreview()
|
||||
}
|
||||
const go = new Go();
|
||||
WebAssembly.instantiateStreaming(fetch("../assets/tplprev.wasm"), go.importObject).then((result) => {
|
||||
go.run(result.instance);
|
||||
document.querySelector('#tplprev .loading').style.display = "none";
|
||||
wasmLoaded = true;
|
||||
updatePreview();
|
||||
});
|
||||
</script>
|
||||
<form id="tplprev" onchange="formChanged(event)" onsubmit="formSubmitted(event)">
|
||||
<pre class="loading">loading wasm...</pre>
|
||||
<div class="template-wrapper">
|
||||
<textarea name="template" type="text" onkeyup="inputUpdated()">{{- 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 -}}
|
||||
{{- if (and .Entries .Report) }}
|
||||
|
||||
Logs:
|
||||
{{ end -}}
|
||||
{{range .Entries -}}{{.Time.Format "2006-01-02T15:04:05Z07:00"}} [{{.Level}}] {{.Message}}{{"\n"}}{{- end -}}</textarea>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<fieldset>
|
||||
<input type="hidden" name="report" value="yes" />
|
||||
<legend><label><input type="checkbox" data-toggle="report" checked /> Container report</label></legend>
|
||||
<label class="numfield">
|
||||
Skipped:
|
||||
<input type="number" name="skipped" value="3" />
|
||||
</label>
|
||||
<label class="numfield">
|
||||
Scanned:
|
||||
<input type="number" name="scanned" value="3" />
|
||||
</label>
|
||||
<label class="numfield">
|
||||
Updated:
|
||||
<input type="number" name="updated" value="3" />
|
||||
</label>
|
||||
<label class="numfield">
|
||||
Failed:
|
||||
<input type="number" name="failed" value="3" />
|
||||
</label>
|
||||
<label class="numfield">
|
||||
Fresh:
|
||||
<input type="number" name="fresh" value="3" />
|
||||
</label>
|
||||
<label class="numfield">
|
||||
Stale:
|
||||
<input type="number" name="stale" value="3" />
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<input type="hidden" name="log" value="yes" />
|
||||
<legend><label><input type="checkbox" data-toggle="log" checked /> Log entries</label></legend>
|
||||
<label class="numfield">
|
||||
Error:
|
||||
<input type="number" name="error" value="1" />
|
||||
</label>
|
||||
<label class="numfield">
|
||||
Warning:
|
||||
<input type="number" name="warning" value="2" />
|
||||
</label>
|
||||
<label class="numfield">
|
||||
Info:
|
||||
<input type="number" name="info" value="3" />
|
||||
</label>
|
||||
<label class="numfield">
|
||||
Debug:
|
||||
<input type="number" name="debug" value="4" />
|
||||
</label>
|
||||
</fieldset>
|
||||
<button type="submit">Update preview</button>
|
||||
</div>
|
||||
<div style="result-wrapper">
|
||||
<pre id="result"></pre>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
const loadQueryVals = () => {
|
||||
const form = document.querySelector('#tplprev');
|
||||
const params = new URLSearchParams(location.search);
|
||||
for(const [key, value] of params){
|
||||
form[key].value = value;
|
||||
const toggleInput = form.querySelector(`[data-toggle="${key}"]`);
|
||||
if (toggleInput) {
|
||||
toggleInput.checked = value === "yes";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", loadQueryVals());
|
||||
} else {
|
||||
loadQueryVals();
|
||||
}
|
||||
</script>
|
|
@ -48,14 +48,14 @@ docker run -d \
|
|||
|
||||
If you mount the config file as described above, be sure to also prepend the URL for the registry when starting up your
|
||||
watched image (you can omit the https://). Here is a complete docker-compose.yml file that starts up a docker container
|
||||
from a private repo at Docker Hub and monitors it with watchtower. Note the command argument changing the interval to
|
||||
30s rather than the default 24 hours.
|
||||
from a private repo on the GitHub Registry and monitors it with watchtower. Note the command argument changing the interval
|
||||
to 30s rather than the default 24 hours.
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
cavo:
|
||||
image: index.docker.io/<org>/<image>:<tag>
|
||||
image: ghcr.io/<org>/<image>:<tag>
|
||||
ports:
|
||||
- "443:3443"
|
||||
- "80:3080"
|
||||
|
|
77
go.mod
77
go.mod
|
@ -1,65 +1,72 @@
|
|||
module github.com/containrrr/watchtower
|
||||
|
||||
go 1.18
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
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.23+incompatible
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/distribution/reference v0.5.0
|
||||
github.com/docker/cli v24.0.7+incompatible
|
||||
github.com/docker/docker v24.0.7+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
github.com/onsi/gomega v1.25.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.6.1
|
||||
github.com/onsi/gomega v1.30.0
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/robfig/cron v1.2.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/net v0.5.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/net v0.19.0
|
||||
)
|
||||
|
||||
require github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.17 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
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.6.0 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.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.9 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // 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/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
|
||||
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/v2 v2.0.6 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.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.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // 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
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package actions_test
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/actions"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
|
||||
. "github.com/containrrr/watchtower/internal/actions/mocks"
|
||||
. "github.com/onsi/ginkgo"
|
||||
|
@ -37,7 +38,7 @@ var _ = Describe("the actions package", func() {
|
|||
It("should not do anything", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container",
|
||||
"test-container",
|
||||
|
@ -59,7 +60,7 @@ var _ = Describe("the actions package", func() {
|
|||
client = CreateMockClient(
|
||||
&TestData{
|
||||
NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
|
@ -89,7 +90,7 @@ var _ = Describe("the actions package", func() {
|
|||
BeforeEach(func() {
|
||||
client = CreateMockClient(
|
||||
&TestData{
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
|
|
|
@ -2,16 +2,15 @@ package actions
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/filters"
|
||||
"github.com/containrrr/watchtower/pkg/sorter"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
)
|
||||
|
||||
// CheckForSanity makes sure everything is sane before starting
|
||||
|
@ -40,7 +39,11 @@ func CheckForSanity(client container.Client, filter types.Filter, rollingRestart
|
|||
// will stop and remove all but the most recently started container. This behaviour can be bypassed
|
||||
// if a scope UID is defined.
|
||||
func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool, scope string) error {
|
||||
containers, err := client.ListContainers(filters.FilterByScope(scope, filters.WatchtowerContainersFilter))
|
||||
filter := filters.WatchtowerContainersFilter
|
||||
if scope != "" {
|
||||
filter = filters.FilterByScope(scope, filter)
|
||||
}
|
||||
containers, err := client.ListContainers(filter)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -55,7 +58,7 @@ func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool,
|
|||
return cleanupExcessWatchtowers(containers, client, cleanup)
|
||||
}
|
||||
|
||||
func cleanupExcessWatchtowers(containers []container.Container, client container.Client, cleanup bool) error {
|
||||
func cleanupExcessWatchtowers(containers []types.Container, client container.Client, cleanup bool) error {
|
||||
var stopErrors int
|
||||
|
||||
sort.Sort(sorter.ByCreated(containers))
|
||||
|
|
|
@ -5,8 +5,6 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
|
@ -21,7 +19,7 @@ type MockClient struct {
|
|||
type TestData struct {
|
||||
TriedToRemoveImageCount int
|
||||
NameOfContainerToKeep string
|
||||
Containers []container.Container
|
||||
Containers []t.Container
|
||||
Staleness map[string]bool
|
||||
}
|
||||
|
||||
|
@ -40,12 +38,12 @@ func CreateMockClient(data *TestData, pullImages bool, removeVolumes bool) MockC
|
|||
}
|
||||
|
||||
// ListContainers is a mock method returning the provided container testdata
|
||||
func (client MockClient) ListContainers(_ t.Filter) ([]container.Container, error) {
|
||||
func (client MockClient) ListContainers(_ t.Filter) ([]t.Container, error) {
|
||||
return client.TestData.Containers, nil
|
||||
}
|
||||
|
||||
// StopContainer is a mock method
|
||||
func (client MockClient) StopContainer(c container.Container, _ time.Duration) error {
|
||||
func (client MockClient) StopContainer(c t.Container, _ time.Duration) error {
|
||||
if c.Name() == client.TestData.NameOfContainerToKeep {
|
||||
return errors.New("tried to stop the instance we want to keep")
|
||||
}
|
||||
|
@ -53,12 +51,12 @@ func (client MockClient) StopContainer(c container.Container, _ time.Duration) e
|
|||
}
|
||||
|
||||
// StartContainer is a mock method
|
||||
func (client MockClient) StartContainer(_ container.Container) (t.ContainerID, error) {
|
||||
func (client MockClient) StartContainer(_ t.Container) (t.ContainerID, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// RenameContainer is a mock method
|
||||
func (client MockClient) RenameContainer(_ container.Container, _ string) error {
|
||||
func (client MockClient) RenameContainer(_ t.Container, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -69,7 +67,7 @@ func (client MockClient) RemoveImageByID(_ t.ImageID) error {
|
|||
}
|
||||
|
||||
// GetContainer is a mock method
|
||||
func (client MockClient) GetContainer(_ t.ContainerID) (container.Container, error) {
|
||||
func (client MockClient) GetContainer(_ t.ContainerID) (t.Container, error) {
|
||||
return client.TestData.Containers[0], nil
|
||||
}
|
||||
|
||||
|
@ -88,7 +86,7 @@ func (client MockClient) ExecuteCommand(_ t.ContainerID, command string, _ int)
|
|||
}
|
||||
|
||||
// IsContainerStale is true if not explicitly stated in TestData for the mock client
|
||||
func (client MockClient) IsContainerStale(cont container.Container) (bool, t.ImageID, error) {
|
||||
func (client MockClient) IsContainerStale(cont t.Container, params t.UpdateParams) (bool, t.ImageID, error) {
|
||||
stale, found := client.TestData.Staleness[cont.Name()]
|
||||
if !found {
|
||||
stale = true
|
||||
|
@ -97,6 +95,6 @@ func (client MockClient) IsContainerStale(cont container.Container) (bool, t.Ima
|
|||
}
|
||||
|
||||
// WarnOnHeadPullFailed is always true for the mock client
|
||||
func (client MockClient) WarnOnHeadPullFailed(_ container.Container) bool {
|
||||
func (client MockClient) WarnOnHeadPullFailed(_ t.Container) bool {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
)
|
||||
|
||||
// CreateMockContainer creates a container substitute valid for testing
|
||||
func CreateMockContainer(id string, name string, image string, created time.Time) container.Container {
|
||||
func CreateMockContainer(id string, name string, image string, created time.Time) wt.Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: id,
|
||||
|
@ -31,7 +31,7 @@ func CreateMockContainer(id string, name string, image string, created time.Time
|
|||
ExposedPorts: map[nat.Port]struct{}{},
|
||||
},
|
||||
}
|
||||
return *container.NewContainer(
|
||||
return container.NewContainer(
|
||||
&content,
|
||||
CreateMockImageInfo(image),
|
||||
)
|
||||
|
@ -48,12 +48,12 @@ func CreateMockImageInfo(image string) *types.ImageInspect {
|
|||
}
|
||||
|
||||
// CreateMockContainerWithImageInfo should only be used for testing
|
||||
func CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) container.Container {
|
||||
func CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) wt.Container {
|
||||
return CreateMockContainerWithImageInfoP(id, name, image, created, &imageInfo)
|
||||
}
|
||||
|
||||
// CreateMockContainerWithImageInfoP should only be used for testing
|
||||
func CreateMockContainerWithImageInfoP(id string, name string, image string, created time.Time, imageInfo *types.ImageInspect) container.Container {
|
||||
func CreateMockContainerWithImageInfoP(id string, name string, image string, created time.Time, imageInfo *types.ImageInspect) wt.Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: id,
|
||||
|
@ -66,21 +66,21 @@ func CreateMockContainerWithImageInfoP(id string, name string, image string, cre
|
|||
Labels: make(map[string]string),
|
||||
},
|
||||
}
|
||||
return *container.NewContainer(
|
||||
return container.NewContainer(
|
||||
&content,
|
||||
imageInfo,
|
||||
)
|
||||
}
|
||||
|
||||
// CreateMockContainerWithDigest should only be used for testing
|
||||
func CreateMockContainerWithDigest(id string, name string, image string, created time.Time, digest string) container.Container {
|
||||
func CreateMockContainerWithDigest(id string, name string, image string, created time.Time, digest string) wt.Container {
|
||||
c := CreateMockContainer(id, name, image, created)
|
||||
c.ImageInfo().RepoDigests = []string{digest}
|
||||
return c
|
||||
}
|
||||
|
||||
// CreateMockContainerWithConfig creates a container substitute valid for testing
|
||||
func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) container.Container {
|
||||
func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) wt.Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: id,
|
||||
|
@ -97,14 +97,14 @@ func CreateMockContainerWithConfig(id string, name string, image string, running
|
|||
},
|
||||
Config: config,
|
||||
}
|
||||
return *container.NewContainer(
|
||||
return container.NewContainer(
|
||||
&content,
|
||||
CreateMockImageInfo(image),
|
||||
)
|
||||
}
|
||||
|
||||
// CreateContainerForProgress creates a container substitute for tracking session/update progress
|
||||
func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (container.Container, wt.ImageID) {
|
||||
func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (wt.Container, wt.ImageID) {
|
||||
indexStr := strconv.Itoa(idPrefix + index)
|
||||
mockID := indexStr + strings.Repeat("0", 61-len(indexStr))
|
||||
contID := "c79" + mockID
|
||||
|
@ -120,7 +120,7 @@ func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (con
|
|||
}
|
||||
|
||||
// CreateMockContainerWithLinks should only be used for testing
|
||||
func CreateMockContainerWithLinks(id string, name string, image string, created time.Time, links []string, imageInfo *types.ImageInspect) container.Container {
|
||||
func CreateMockContainerWithLinks(id string, name string, image string, created time.Time, links []string, imageInfo *types.ImageInspect) wt.Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: id,
|
||||
|
@ -136,7 +136,7 @@ func CreateMockContainerWithLinks(id string, name string, image string, created
|
|||
Labels: make(map[string]string),
|
||||
},
|
||||
}
|
||||
return *container.NewContainer(
|
||||
return container.NewContainer(
|
||||
&content,
|
||||
imageInfo,
|
||||
)
|
||||
|
|
|
@ -2,7 +2,6 @@ package actions
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/util"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
|
@ -34,8 +33,8 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
|||
staleCheckFailed := 0
|
||||
|
||||
for i, targetContainer := range containers {
|
||||
stale, newestImage, err := client.IsContainerStale(targetContainer)
|
||||
shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly()
|
||||
stale, newestImage, err := client.IsContainerStale(targetContainer, params)
|
||||
shouldUpdate := stale && !params.NoRestart && !targetContainer.IsMonitorOnly(params)
|
||||
if err == nil && shouldUpdate {
|
||||
// Check to make sure we have all the necessary information for recreating the container
|
||||
err = targetContainer.VerifyConfiguration()
|
||||
|
@ -58,7 +57,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
|||
} else {
|
||||
progress.AddScanned(targetContainer, newestImage)
|
||||
}
|
||||
containers[i].Stale = stale
|
||||
containers[i].SetStale(stale)
|
||||
|
||||
if stale {
|
||||
staleCount++
|
||||
|
@ -72,15 +71,13 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
|||
|
||||
UpdateImplicitRestart(containers)
|
||||
|
||||
var containersToUpdate []container.Container
|
||||
if !params.MonitorOnly {
|
||||
var containersToUpdate []types.Container
|
||||
for _, c := range containers {
|
||||
if !c.IsMonitorOnly() {
|
||||
if !c.IsMonitorOnly(params) {
|
||||
containersToUpdate = append(containersToUpdate, c)
|
||||
progress.MarkForUpdate(c.ID())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if params.RollingRestart {
|
||||
progress.UpdateFailed(performRollingRestart(containersToUpdate, client, params))
|
||||
|
@ -97,7 +94,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
|||
return progress.Report(), nil
|
||||
}
|
||||
|
||||
func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {
|
||||
func performRollingRestart(containers []types.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {
|
||||
cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
|
||||
failed := make(map[types.ContainerID]error, len(containers))
|
||||
|
||||
|
@ -109,7 +106,7 @@ func performRollingRestart(containers []container.Container, client container.Cl
|
|||
} else {
|
||||
if err := restartStaleContainer(containers[i], client, params); err != nil {
|
||||
failed[containers[i].ID()] = err
|
||||
} else if containers[i].Stale {
|
||||
} else if containers[i].IsStale() {
|
||||
// Only add (previously) stale containers' images to cleanup
|
||||
cleanupImageIDs[containers[i].ImageID()] = true
|
||||
}
|
||||
|
@ -123,7 +120,7 @@ func performRollingRestart(containers []container.Container, client container.Cl
|
|||
return failed
|
||||
}
|
||||
|
||||
func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) {
|
||||
func stopContainersInReversedOrder(containers []types.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) {
|
||||
failed = make(map[types.ContainerID]error, len(containers))
|
||||
stopped = make(map[types.ImageID]bool, len(containers))
|
||||
for i := len(containers) - 1; i >= 0; i-- {
|
||||
|
@ -138,7 +135,7 @@ func stopContainersInReversedOrder(containers []container.Container, client cont
|
|||
return
|
||||
}
|
||||
|
||||
func stopStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error {
|
||||
func stopStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error {
|
||||
if container.IsWatchtower() {
|
||||
log.Debugf("This is the watchtower container %s", container.Name())
|
||||
return nil
|
||||
|
@ -149,7 +146,7 @@ func stopStaleContainer(container container.Container, client container.Client,
|
|||
}
|
||||
|
||||
// Perform an additional check here to prevent us from stopping a linked container we cannot restart
|
||||
if container.LinkedToRestarting {
|
||||
if container.IsLinkedToRestarting() {
|
||||
if err := container.VerifyConfiguration(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -175,7 +172,7 @@ func stopStaleContainer(container container.Container, client container.Client,
|
|||
return nil
|
||||
}
|
||||
|
||||
func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error {
|
||||
func restartContainersInSortedOrder(containers []types.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error {
|
||||
cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
|
||||
failed := make(map[types.ContainerID]error, len(containers))
|
||||
|
||||
|
@ -186,7 +183,7 @@ func restartContainersInSortedOrder(containers []container.Container, client con
|
|||
if stoppedImages[c.SafeImageID()] {
|
||||
if err := restartStaleContainer(c, client, params); err != nil {
|
||||
failed[c.ID()] = err
|
||||
} else if c.Stale {
|
||||
} else if c.IsStale() {
|
||||
// Only add (previously) stale containers' images to cleanup
|
||||
cleanupImageIDs[c.ImageID()] = true
|
||||
}
|
||||
|
@ -211,7 +208,7 @@ func cleanupImages(client container.Client, imageIDs map[types.ImageID]bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func restartStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error {
|
||||
func restartStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error {
|
||||
// Since we can't shutdown a watchtower container immediately, we need to
|
||||
// start the new one while the old one is still running. This prevents us
|
||||
// from re-using the same container name so we first rename the current
|
||||
|
@ -236,7 +233,7 @@ func restartStaleContainer(container container.Container, client container.Clien
|
|||
|
||||
// UpdateImplicitRestart iterates through the passed containers, setting the
|
||||
// `LinkedToRestarting` flag if any of it's linked containers are marked for restart
|
||||
func UpdateImplicitRestart(containers []container.Container) {
|
||||
func UpdateImplicitRestart(containers []types.Container) {
|
||||
|
||||
for ci, c := range containers {
|
||||
if c.ToRestart() {
|
||||
|
@ -250,7 +247,7 @@ func UpdateImplicitRestart(containers []container.Container) {
|
|||
"linked": c.Name(),
|
||||
}).Debug("container is linked to restarting")
|
||||
// NOTE: To mutate the array, the `c` variable cannot be used as it's a copy
|
||||
containers[ci].LinkedToRestarting = true
|
||||
containers[ci].SetLinkedToRestarting(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -258,12 +255,8 @@ func UpdateImplicitRestart(containers []container.Container) {
|
|||
|
||||
// linkedContainerMarkedForRestart returns the name of the first link that matches a
|
||||
// container marked for restart
|
||||
func linkedContainerMarkedForRestart(links []string, containers []container.Container) string {
|
||||
func linkedContainerMarkedForRestart(links []string, containers []types.Container) string {
|
||||
for _, linkName := range links {
|
||||
// Since the container names need to start with '/', let's prepend it if it's missing
|
||||
if !strings.HasPrefix(linkName, "/") {
|
||||
linkName = "/" + linkName
|
||||
}
|
||||
for _, candidate := range containers {
|
||||
if candidate.Name() == linkName && candidate.ToRestart() {
|
||||
return linkName
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/actions"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
dockerContainer "github.com/docker/docker/api/types/container"
|
||||
|
@ -18,7 +17,7 @@ import (
|
|||
func getCommonTestData(keepContainer string) *TestData {
|
||||
return &TestData{
|
||||
NameOfContainerToKeep: keepContainer,
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
|
@ -59,7 +58,7 @@ func getLinkedTestData(withImageInfo bool) *TestData {
|
|||
|
||||
return &TestData{
|
||||
Staleness: map[string]bool{linkingContainer.Name(): false},
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
staleContainer,
|
||||
linkingContainer,
|
||||
},
|
||||
|
@ -130,7 +129,7 @@ var _ = Describe("the update action", func() {
|
|||
client := CreateMockClient(
|
||||
&TestData{
|
||||
NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
|
@ -163,7 +162,7 @@ var _ = Describe("the update action", func() {
|
|||
It("should not update any containers", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
|
@ -179,13 +178,85 @@ var _ = Describe("the update action", func() {
|
|||
false,
|
||||
false,
|
||||
)
|
||||
_, err := actions.Update(client, types.UpdateParams{MonitorOnly: true})
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||
})
|
||||
When("watchtower has been instructed to have label take precedence", func() {
|
||||
It("it should update containers when monitor only is set to false", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"fake-image2:latest",
|
||||
false,
|
||||
false,
|
||||
time.Now(),
|
||||
&dockerContainer.Config{
|
||||
Labels: map[string]string{
|
||||
"com.centurylinklabs.watchtower.monitor-only": "false",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||
})
|
||||
It("it should update not containers when monitor only is set to true", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"fake-image2:latest",
|
||||
false,
|
||||
false,
|
||||
time.Now(),
|
||||
&dockerContainer.Config{
|
||||
Labels: map[string]string{
|
||||
"com.centurylinklabs.watchtower.monitor-only": "true",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||
})
|
||||
It("it should update not containers when monitor only is not set", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
"fake-image:latest",
|
||||
time.Now()),
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("watchtower has been instructed to run lifecycle hooks", func() {
|
||||
|
||||
|
@ -194,7 +265,7 @@ var _ = Describe("the update action", func() {
|
|||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
|
@ -227,7 +298,7 @@ var _ = Describe("the update action", func() {
|
|||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
|
@ -259,7 +330,7 @@ var _ = Describe("the update action", func() {
|
|||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
|
@ -300,7 +371,7 @@ var _ = Describe("the update action", func() {
|
|||
ExposedPorts: map[nat.Port]struct{}{},
|
||||
})
|
||||
|
||||
provider.Stale = true
|
||||
provider.SetStale(true)
|
||||
|
||||
consumer := CreateMockContainerWithConfig(
|
||||
"test-container-consumer",
|
||||
|
@ -316,7 +387,7 @@ var _ = Describe("the update action", func() {
|
|||
ExposedPorts: map[nat.Port]struct{}{},
|
||||
})
|
||||
|
||||
containers := []container.Container{
|
||||
containers := []types.Container{
|
||||
provider,
|
||||
consumer,
|
||||
}
|
||||
|
@ -338,7 +409,7 @@ var _ = Describe("the update action", func() {
|
|||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
|
@ -370,7 +441,7 @@ var _ = Describe("the update action", func() {
|
|||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -24,9 +24,9 @@ var defaultInterval = int((time.Hour * 24).Seconds())
|
|||
// RegisterDockerFlags that are used directly by the docker api client
|
||||
func RegisterDockerFlags(rootCmd *cobra.Command) {
|
||||
flags := rootCmd.PersistentFlags()
|
||||
flags.StringP("host", "H", viper.GetString("DOCKER_HOST"), "daemon socket to connect to")
|
||||
flags.BoolP("tlsverify", "v", viper.GetBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote")
|
||||
flags.StringP("api-version", "a", viper.GetString("DOCKER_API_VERSION"), "api version to use by docker client")
|
||||
flags.StringP("host", "H", envString("DOCKER_HOST"), "daemon socket to connect to")
|
||||
flags.BoolP("tlsverify", "v", envBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote")
|
||||
flags.StringP("api-version", "a", envString("DOCKER_API_VERSION"), "api version to use by docker client")
|
||||
}
|
||||
|
||||
// RegisterSystemFlags that are used by watchtower to modify the program flow
|
||||
|
@ -35,132 +35,145 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
|
|||
flags.IntP(
|
||||
"interval",
|
||||
"i",
|
||||
viper.GetInt("WATCHTOWER_POLL_INTERVAL"),
|
||||
envInt("WATCHTOWER_POLL_INTERVAL"),
|
||||
"Poll interval (in seconds)")
|
||||
|
||||
flags.StringP(
|
||||
"schedule",
|
||||
"s",
|
||||
viper.GetString("WATCHTOWER_SCHEDULE"),
|
||||
envString("WATCHTOWER_SCHEDULE"),
|
||||
"The cron expression which defines when to update")
|
||||
|
||||
flags.DurationP(
|
||||
"stop-timeout",
|
||||
"t",
|
||||
viper.GetDuration("WATCHTOWER_TIMEOUT"),
|
||||
envDuration("WATCHTOWER_TIMEOUT"),
|
||||
"Timeout before a container is forcefully stopped")
|
||||
|
||||
flags.BoolP(
|
||||
"no-pull",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NO_PULL"),
|
||||
envBool("WATCHTOWER_NO_PULL"),
|
||||
"Do not pull any new images")
|
||||
|
||||
flags.BoolP(
|
||||
"no-restart",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NO_RESTART"),
|
||||
envBool("WATCHTOWER_NO_RESTART"),
|
||||
"Do not restart any containers")
|
||||
|
||||
flags.BoolP(
|
||||
"no-startup-message",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"),
|
||||
envBool("WATCHTOWER_NO_STARTUP_MESSAGE"),
|
||||
"Prevents watchtower from sending a startup message")
|
||||
|
||||
flags.BoolP(
|
||||
"cleanup",
|
||||
"c",
|
||||
viper.GetBool("WATCHTOWER_CLEANUP"),
|
||||
envBool("WATCHTOWER_CLEANUP"),
|
||||
"Remove previously used images after updating")
|
||||
|
||||
flags.BoolP(
|
||||
"remove-volumes",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"),
|
||||
envBool("WATCHTOWER_REMOVE_VOLUMES"),
|
||||
"Remove attached volumes before updating")
|
||||
|
||||
flags.BoolP(
|
||||
"label-enable",
|
||||
"e",
|
||||
viper.GetBool("WATCHTOWER_LABEL_ENABLE"),
|
||||
envBool("WATCHTOWER_LABEL_ENABLE"),
|
||||
"Watch containers where the com.centurylinklabs.watchtower.enable label is true")
|
||||
|
||||
flags.StringSliceP(
|
||||
"disable-containers",
|
||||
"x",
|
||||
// Due to issue spf13/viper#380, can't use viper.GetStringSlice:
|
||||
regexp.MustCompile("[, ]+").Split(envString("WATCHTOWER_DISABLE_CONTAINERS"), -1),
|
||||
"Comma-separated list of containers to explicitly exclude from watching.")
|
||||
|
||||
flags.StringP(
|
||||
"log-format",
|
||||
"l",
|
||||
viper.GetString("WATCHTOWER_LOG_FORMAT"),
|
||||
"Sets what logging format to use for console output. Possible values: Auto, LogFmt, Pretty, JSON")
|
||||
|
||||
flags.BoolP(
|
||||
"debug",
|
||||
"d",
|
||||
viper.GetBool("WATCHTOWER_DEBUG"),
|
||||
envBool("WATCHTOWER_DEBUG"),
|
||||
"Enable debug mode with verbose logging")
|
||||
|
||||
flags.BoolP(
|
||||
"trace",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_TRACE"),
|
||||
envBool("WATCHTOWER_TRACE"),
|
||||
"Enable trace mode with very verbose logging - caution, exposes credentials")
|
||||
|
||||
flags.BoolP(
|
||||
"monitor-only",
|
||||
"m",
|
||||
viper.GetBool("WATCHTOWER_MONITOR_ONLY"),
|
||||
envBool("WATCHTOWER_MONITOR_ONLY"),
|
||||
"Will only monitor for new images, not update the containers")
|
||||
|
||||
flags.BoolP(
|
||||
"run-once",
|
||||
"R",
|
||||
viper.GetBool("WATCHTOWER_RUN_ONCE"),
|
||||
envBool("WATCHTOWER_RUN_ONCE"),
|
||||
"Run once now and exit")
|
||||
|
||||
flags.BoolP(
|
||||
"include-restarting",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_INCLUDE_RESTARTING"),
|
||||
envBool("WATCHTOWER_INCLUDE_RESTARTING"),
|
||||
"Will also include restarting containers")
|
||||
|
||||
flags.BoolP(
|
||||
"include-stopped",
|
||||
"S",
|
||||
viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"),
|
||||
envBool("WATCHTOWER_INCLUDE_STOPPED"),
|
||||
"Will also include created and exited containers")
|
||||
|
||||
flags.BoolP(
|
||||
"revive-stopped",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_REVIVE_STOPPED"),
|
||||
envBool("WATCHTOWER_REVIVE_STOPPED"),
|
||||
"Will also start stopped containers that were updated, if include-stopped is active")
|
||||
|
||||
flags.BoolP(
|
||||
"enable-lifecycle-hooks",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"),
|
||||
envBool("WATCHTOWER_LIFECYCLE_HOOKS"),
|
||||
"Enable the execution of commands triggered by pre- and post-update lifecycle hooks")
|
||||
|
||||
flags.BoolP(
|
||||
"rolling-restart",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_ROLLING_RESTART"),
|
||||
envBool("WATCHTOWER_ROLLING_RESTART"),
|
||||
"Restart containers one at a time")
|
||||
|
||||
flags.BoolP(
|
||||
"http-api-update",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_HTTP_API_UPDATE"),
|
||||
envBool("WATCHTOWER_HTTP_API_UPDATE"),
|
||||
"Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request")
|
||||
flags.BoolP(
|
||||
"http-api-metrics",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_HTTP_API_METRICS"),
|
||||
envBool("WATCHTOWER_HTTP_API_METRICS"),
|
||||
"Runs Watchtower with the Prometheus metrics API enabled")
|
||||
|
||||
flags.StringP(
|
||||
"http-api-token",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_HTTP_API_TOKEN"),
|
||||
envString("WATCHTOWER_HTTP_API_TOKEN"),
|
||||
"Sets an authentication token to HTTP API requests.")
|
||||
|
||||
flags.BoolP(
|
||||
"http-api-periodic-polls",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"),
|
||||
envBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"),
|
||||
"Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled")
|
||||
|
||||
// https://no-color.org/
|
||||
|
@ -173,19 +186,31 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
|
|||
flags.StringP(
|
||||
"scope",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_SCOPE"),
|
||||
envString("WATCHTOWER_SCOPE"),
|
||||
"Defines a monitoring scope for the Watchtower instance.")
|
||||
|
||||
flags.StringP(
|
||||
"porcelain",
|
||||
"P",
|
||||
viper.GetString("WATCHTOWER_PORCELAIN"),
|
||||
envString("WATCHTOWER_PORCELAIN"),
|
||||
`Write session results to stdout using a stable versioned format. Supported values: "v1"`)
|
||||
|
||||
flags.String(
|
||||
"log-level",
|
||||
viper.GetString("WATCHTOWER_LOG_LEVEL"),
|
||||
envString("WATCHTOWER_LOG_LEVEL"),
|
||||
"The maximum log level that will be written to STDERR. Possible values: panic, fatal, error, warn, info, debug or trace")
|
||||
|
||||
flags.BoolP(
|
||||
"health-check",
|
||||
"",
|
||||
false,
|
||||
"Do health check and exit")
|
||||
|
||||
flags.BoolP(
|
||||
"label-take-precedence",
|
||||
"",
|
||||
envBool("WATCHTOWER_LABEL_TAKE_PRECEDENCE"),
|
||||
"Label applied to containers take precedence over arguments")
|
||||
}
|
||||
|
||||
// RegisterNotificationFlags that are used by watchtower to send notifications
|
||||
|
@ -195,177 +220,202 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) {
|
|||
flags.StringSliceP(
|
||||
"notifications",
|
||||
"n",
|
||||
viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"),
|
||||
envStringSlice("WATCHTOWER_NOTIFICATIONS"),
|
||||
" Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)")
|
||||
|
||||
flags.String(
|
||||
"notifications-level",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"),
|
||||
envString("WATCHTOWER_NOTIFICATIONS_LEVEL"),
|
||||
"The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug")
|
||||
|
||||
flags.IntP(
|
||||
"notifications-delay",
|
||||
"",
|
||||
viper.GetInt("WATCHTOWER_NOTIFICATIONS_DELAY"),
|
||||
envInt("WATCHTOWER_NOTIFICATIONS_DELAY"),
|
||||
"Delay before sending notifications, expressed in seconds")
|
||||
|
||||
flags.StringP(
|
||||
"notifications-hostname",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"),
|
||||
envString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"),
|
||||
"Custom hostname for notification titles")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-from",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"),
|
||||
"Address to send notification emails from")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-to",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_TO"),
|
||||
"Address to send notification emails to")
|
||||
|
||||
flags.IntP(
|
||||
"notification-email-delay",
|
||||
"",
|
||||
viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"),
|
||||
envInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"),
|
||||
"Delay before sending notifications, expressed in seconds")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-server",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"),
|
||||
"SMTP server to send notification emails through")
|
||||
|
||||
flags.IntP(
|
||||
"notification-email-server-port",
|
||||
"",
|
||||
viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"),
|
||||
envInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"),
|
||||
"SMTP server port to send notification emails through")
|
||||
|
||||
flags.BoolP(
|
||||
"notification-email-server-tls-skip-verify",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"),
|
||||
envBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"),
|
||||
`Controls whether watchtower verifies the SMTP server's certificate chain and host name.
|
||||
Should only be used for testing.`)
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-server-user",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"),
|
||||
"SMTP server user for sending notifications")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-server-password",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"),
|
||||
"SMTP server password for sending notifications")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-subjecttag",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"),
|
||||
"Subject prefix tag for notifications via mail")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-hook-url",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"),
|
||||
envString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"),
|
||||
"The Slack Hook URL to send notifications to")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-identifier",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"),
|
||||
envString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"),
|
||||
"A string which will be used to identify the messages coming from this watchtower instance")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-channel",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"),
|
||||
envString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"),
|
||||
"A string which overrides the webhook's default channel. Example: #my-custom-channel")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-icon-emoji",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"),
|
||||
envString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"),
|
||||
"An emoji code string to use in place of the default icon")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-icon-url",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"),
|
||||
envString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"),
|
||||
"An icon image URL string to use in place of the default icon")
|
||||
|
||||
flags.StringP(
|
||||
"notification-msteams-hook",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"),
|
||||
envString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"),
|
||||
"The MSTeams WebHook URL to send notifications to")
|
||||
|
||||
flags.BoolP(
|
||||
"notification-msteams-data",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"),
|
||||
envBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"),
|
||||
"The MSTeams notifier will try to extract log entry fields as MSTeams message facts")
|
||||
|
||||
flags.StringP(
|
||||
"notification-gotify-url",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"),
|
||||
envString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"),
|
||||
"The Gotify URL to send notifications to")
|
||||
|
||||
flags.StringP(
|
||||
"notification-gotify-token",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"),
|
||||
envString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"),
|
||||
"The Gotify Application required to query the Gotify API")
|
||||
|
||||
flags.BoolP(
|
||||
"notification-gotify-tls-skip-verify",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"),
|
||||
envBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"),
|
||||
`Controls whether watchtower verifies the Gotify server's certificate chain and host name.
|
||||
Should only be used for testing.`)
|
||||
|
||||
flags.String(
|
||||
"notification-template",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"),
|
||||
envString("WATCHTOWER_NOTIFICATION_TEMPLATE"),
|
||||
"The shoutrrr text/template for the messages")
|
||||
|
||||
flags.StringArray(
|
||||
"notification-url",
|
||||
viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"),
|
||||
envStringSlice("WATCHTOWER_NOTIFICATION_URL"),
|
||||
"The shoutrrr URL to send notifications to")
|
||||
|
||||
flags.Bool("notification-report",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"),
|
||||
envBool("WATCHTOWER_NOTIFICATION_REPORT"),
|
||||
"Use the session report as the notification template data")
|
||||
|
||||
flags.StringP(
|
||||
"notification-title-tag",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_TITLE_TAG"),
|
||||
envString("WATCHTOWER_NOTIFICATION_TITLE_TAG"),
|
||||
"Title prefix tag for notifications")
|
||||
|
||||
flags.Bool("notification-skip-title",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"),
|
||||
envBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"),
|
||||
"Do not pass the title param to notifications")
|
||||
|
||||
flags.String(
|
||||
"warn-on-head-failure",
|
||||
viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),
|
||||
envString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),
|
||||
"When to warn about HEAD pull requests failing. Possible values: always, auto or never")
|
||||
|
||||
flags.Bool(
|
||||
"notification-log-stdout",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"),
|
||||
envBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"),
|
||||
"Write notification logs to stdout instead of logging (to stderr)")
|
||||
}
|
||||
|
||||
func envString(key string) string {
|
||||
viper.MustBindEnv(key)
|
||||
return viper.GetString(key)
|
||||
}
|
||||
|
||||
func envStringSlice(key string) []string {
|
||||
viper.MustBindEnv(key)
|
||||
return viper.GetStringSlice(key)
|
||||
}
|
||||
|
||||
func envInt(key string) int {
|
||||
viper.MustBindEnv(key)
|
||||
return viper.GetInt(key)
|
||||
}
|
||||
|
||||
func envBool(key string) bool {
|
||||
viper.MustBindEnv(key)
|
||||
return viper.GetBool(key)
|
||||
}
|
||||
|
||||
func envDuration(key string) time.Duration {
|
||||
viper.MustBindEnv(key)
|
||||
return viper.GetDuration(key)
|
||||
}
|
||||
|
||||
// SetDefaults provides default values for environment variables
|
||||
func SetDefaults() {
|
||||
viper.AutomaticEnv()
|
||||
|
@ -379,6 +429,7 @@ func SetDefaults() {
|
|||
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "")
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower")
|
||||
viper.SetDefault("WATCHTOWER_LOG_LEVEL", "info")
|
||||
viper.SetDefault("WATCHTOWER_LOG_FORMAT", "auto")
|
||||
}
|
||||
|
||||
// EnvConfig translates the command-line options into environment variables
|
||||
|
@ -467,14 +518,17 @@ func GetSecretsFromFiles(rootCmd *cobra.Command) {
|
|||
"notification-msteams-hook",
|
||||
"notification-gotify-token",
|
||||
"notification-url",
|
||||
"http-api-token",
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
getSecretFromFile(flags, secret)
|
||||
if err := getSecretFromFile(flags, secret); err != nil {
|
||||
log.Fatalf("failed to get secret from flag %v: %s", secret, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getSecretFromFile will check if the flag contains a reference to a file; if it does, replaces the value of the flag with the contents of the file.
|
||||
func getSecretFromFile(flags *pflag.FlagSet, secret string) {
|
||||
func getSecretFromFile(flags *pflag.FlagSet, secret string) error {
|
||||
flag := flags.Lookup(secret)
|
||||
if sliceValue, ok := flag.Value.(pflag.SliceValue); ok {
|
||||
oldValues := sliceValue.GetSlice()
|
||||
|
@ -483,7 +537,7 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) {
|
|||
if value != "" && isFile(value) {
|
||||
file, err := os.Open(value)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
|
@ -493,25 +547,26 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) {
|
|||
}
|
||||
values = append(values, line)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
sliceValue.Replace(values)
|
||||
return
|
||||
return sliceValue.Replace(values)
|
||||
}
|
||||
|
||||
value := flag.Value.String()
|
||||
if value != "" && isFile(value) {
|
||||
file, err := ioutil.ReadFile(value)
|
||||
content, err := os.ReadFile(value)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = flags.Set(secret, strings.TrimSpace(string(file)))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
return flags.Set(secret, strings.TrimSpace(string(content)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isFile(s string) bool {
|
||||
|
@ -564,19 +619,59 @@ func ProcessFlagAliases(flags *pflag.FlagSet) {
|
|||
// update schedule flag to match interval if it's set, or to the default if none of them are
|
||||
if intervalChanged || !scheduleChanged {
|
||||
interval, _ := flags.GetInt(`interval`)
|
||||
flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
|
||||
_ = flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
|
||||
}
|
||||
|
||||
if flagIsEnabled(flags, `debug`) {
|
||||
flags.Set(`log-level`, `debug`)
|
||||
_ = flags.Set(`log-level`, `debug`)
|
||||
}
|
||||
|
||||
if flagIsEnabled(flags, `trace`) {
|
||||
flags.Set(`log-level`, `trace`)
|
||||
_ = flags.Set(`log-level`, `trace`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// SetupLogging reads only the flags that is needed to set up logging and applies them to the global logger
|
||||
func SetupLogging(f *pflag.FlagSet) error {
|
||||
logFormat, _ := f.GetString(`log-format`)
|
||||
noColor, _ := f.GetBool("no-color")
|
||||
|
||||
switch strings.ToLower(logFormat) {
|
||||
case "auto":
|
||||
// This will either use the "pretty" or "logfmt" format, based on whether the standard out is connected to a TTY
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: noColor,
|
||||
// enable logrus built-in support for https://bixense.com/clicolors/
|
||||
EnvironmentOverrideColors: true,
|
||||
})
|
||||
case "json":
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
case "logfmt":
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: true,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
case "pretty":
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
// "Pretty" format combined with `--no-color` will only change the timestamp to the time since start
|
||||
ForceColors: !noColor,
|
||||
FullTimestamp: false,
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("invalid log format: %s", logFormat)
|
||||
}
|
||||
|
||||
rawLogLevel, _ := f.GetString(`log-level`)
|
||||
if logLevel, err := log.ParseLevel(rawLogLevel); err != nil {
|
||||
return fmt.Errorf("invalid log level: %e", err)
|
||||
} else {
|
||||
log.SetLevel(logLevel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func flagIsEnabled(flags *pflag.FlagSet, name string) bool {
|
||||
value, err := flags.GetBool(name)
|
||||
if err != nil {
|
||||
|
@ -593,7 +688,7 @@ func appendFlagValue(flags *pflag.FlagSet, name string, values ...string) error
|
|||
|
||||
if flagValues, ok := flag.Value.(pflag.SliceValue); ok {
|
||||
for _, value := range values {
|
||||
flagValues.Append(value)
|
||||
_ = flagValues.Append(value)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf(`the value for flag %q is not a slice value`, name)
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
package flags
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEnvConfig_Defaults(t *testing.T) {
|
||||
// Unset testing environments own variables, since those are not what is under test
|
||||
os.Unsetenv("DOCKER_TLS_VERIFY")
|
||||
os.Unsetenv("DOCKER_HOST")
|
||||
_ = os.Unsetenv("DOCKER_TLS_VERIFY")
|
||||
_ = os.Unsetenv("DOCKER_HOST")
|
||||
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
|
@ -48,10 +50,7 @@ func TestEnvConfig_Custom(t *testing.T) {
|
|||
|
||||
func TestGetSecretsFromFilesWithString(t *testing.T) {
|
||||
value := "supersecretstring"
|
||||
|
||||
err := os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
|
||||
require.NoError(t, err)
|
||||
defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD")
|
||||
t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
|
||||
|
||||
testGetSecretsFromFiles(t, "notification-email-server-password", value)
|
||||
}
|
||||
|
@ -60,18 +59,15 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) {
|
|||
value := "megasecretstring"
|
||||
|
||||
// Create the temporary file which will contain a secret.
|
||||
file, err := ioutil.TempFile(os.TempDir(), "watchtower-")
|
||||
file, err := os.CreateTemp(t.TempDir(), "watchtower-")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
|
||||
|
||||
// Write the secret to the temporary file.
|
||||
secret := []byte(value)
|
||||
_, err = file.Write(secret)
|
||||
_, err = file.Write([]byte(value))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, file.Close())
|
||||
|
||||
err = os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
|
||||
require.NoError(t, err)
|
||||
defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD")
|
||||
t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
|
||||
|
||||
testGetSecretsFromFiles(t, "notification-email-server-password", value)
|
||||
}
|
||||
|
@ -80,16 +76,15 @@ func TestGetSliceSecretsFromFiles(t *testing.T) {
|
|||
values := []string{"entry2", "", "entry3"}
|
||||
|
||||
// Create the temporary file which will contain a secret.
|
||||
file, err := ioutil.TempFile(os.TempDir(), "watchtower-")
|
||||
file, err := os.CreateTemp(t.TempDir(), "watchtower-")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
|
||||
|
||||
// Write the secret to the temporary file.
|
||||
for _, value := range values {
|
||||
_, err = file.WriteString("\n" + value)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
file.Close()
|
||||
require.NoError(t, file.Close())
|
||||
|
||||
testGetSecretsFromFiles(t, "notification-url", `[entry1,entry2,entry3]`,
|
||||
`--notification-url`, "entry1",
|
||||
|
@ -99,6 +94,7 @@ func TestGetSliceSecretsFromFiles(t *testing.T) {
|
|||
func testGetSecretsFromFiles(t *testing.T, flagName string, expected string, args ...string) {
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
RegisterSystemFlags(cmd)
|
||||
RegisterNotificationFlags(cmd)
|
||||
require.NoError(t, cmd.ParseFlags(args))
|
||||
GetSecretsFromFiles(cmd)
|
||||
|
@ -166,9 +162,7 @@ func TestProcessFlagAliases(t *testing.T) {
|
|||
|
||||
func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
err := os.Setenv("WATCHTOWER_DEBUG", `true`)
|
||||
require.NoError(t, err)
|
||||
defer os.Unsetenv("WATCHTOWER_DEBUG")
|
||||
t.Setenv("WATCHTOWER_DEBUG", `true`)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
|
@ -183,6 +177,57 @@ func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
|
|||
assert.Equal(t, `debug`, logLevel)
|
||||
}
|
||||
|
||||
func TestLogFormatFlag(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
|
||||
// Ensure the default value is Auto
|
||||
require.NoError(t, cmd.ParseFlags([]string{}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)
|
||||
|
||||
// Test JSON format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `JSON`}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
assert.IsType(t, &logrus.JSONFormatter{}, logrus.StandardLogger().Formatter)
|
||||
|
||||
// Test Pretty format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `pretty`}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)
|
||||
textFormatter, ok := (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)
|
||||
assert.True(t, ok)
|
||||
assert.True(t, textFormatter.ForceColors)
|
||||
assert.False(t, textFormatter.FullTimestamp)
|
||||
|
||||
// Test LogFmt format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `logfmt`}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
textFormatter, ok = (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)
|
||||
assert.True(t, ok)
|
||||
assert.True(t, textFormatter.DisableColors)
|
||||
assert.True(t, textFormatter.FullTimestamp)
|
||||
|
||||
// Test invalid format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `cowsay`}))
|
||||
require.Error(t, SetupLogging(cmd.Flags()))
|
||||
}
|
||||
|
||||
func TestLogLevelFlag(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
|
||||
// Test invalid format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-level`, `gossip`}))
|
||||
require.Error(t, SetupLogging(cmd.Flags()))
|
||||
}
|
||||
|
||||
func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
|
||||
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
|
||||
cmd := new(cobra.Command)
|
||||
|
@ -202,9 +247,7 @@ func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
|
|||
func TestProcessFlagAliasesScheduleFromEnvironment(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
|
||||
err := os.Setenv("WATCHTOWER_SCHEDULE", `@hourly`)
|
||||
require.NoError(t, err)
|
||||
defer os.Unsetenv("WATCHTOWER_SCHEDULE")
|
||||
t.Setenv("WATCHTOWER_SCHEDULE", `@hourly`)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
|
@ -234,3 +277,63 @@ func TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) {
|
|||
ProcessFlagAliases(flags)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFlagsArePrecentInDocumentation(t *testing.T) {
|
||||
|
||||
// Legacy notifcations are ignored, since they are (soft) deprecated
|
||||
ignoredEnvs := map[string]string{
|
||||
"WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI": "legacy",
|
||||
"WATCHTOWER_NOTIFICATION_SLACK_ICON_URL": "legacy",
|
||||
}
|
||||
|
||||
ignoredFlags := map[string]string{
|
||||
"notification-gotify-url": "legacy",
|
||||
"notification-slack-icon-emoji": "legacy",
|
||||
"notification-slack-icon-url": "legacy",
|
||||
}
|
||||
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
RegisterNotificationFlags(cmd)
|
||||
|
||||
flags := cmd.PersistentFlags()
|
||||
|
||||
docFiles := []string{
|
||||
"../../docs/arguments.md",
|
||||
"../../docs/lifecycle-hooks.md",
|
||||
"../../docs/notifications.md",
|
||||
}
|
||||
allDocs := ""
|
||||
for _, f := range docFiles {
|
||||
bytes, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not load docs file %q: %v", f, err)
|
||||
}
|
||||
allDocs += string(bytes)
|
||||
}
|
||||
|
||||
flags.VisitAll(func(f *pflag.Flag) {
|
||||
if !strings.Contains(allDocs, "--"+f.Name) {
|
||||
if _, found := ignoredFlags[f.Name]; !found {
|
||||
t.Logf("Docs does not mention flag long name %q", f.Name)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
if !strings.Contains(allDocs, "-"+f.Shorthand) {
|
||||
t.Logf("Docs does not mention flag shorthand %q (%q)", f.Shorthand, f.Name)
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
for _, key := range viper.AllKeys() {
|
||||
envKey := strings.ToUpper(key)
|
||||
if !strings.Contains(allDocs, envKey) {
|
||||
if _, found := ignoredEnvs[envKey]; !found {
|
||||
t.Logf("Docs does not mention environment variable %q", envKey)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
24
internal/util/rand_sha256.go
Normal file
24
internal/util/rand_sha256.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GenerateRandomSHA256 generates a random 64 character SHA 256 hash string
|
||||
func GenerateRandomSHA256() string {
|
||||
return GenerateRandomPrefixedSHA256()[7:]
|
||||
}
|
||||
|
||||
// GenerateRandomPrefixedSHA256 generates a random 64 character SHA 256 hash string, prefixed with `sha256:`
|
||||
func GenerateRandomPrefixedSHA256() string {
|
||||
hash := make([]byte, 32)
|
||||
_, _ = rand.Read(hash)
|
||||
sb := bytes.NewBufferString("sha256:")
|
||||
sb.Grow(64)
|
||||
for _, h := range hash {
|
||||
_, _ = fmt.Fprintf(sb, "%02x", h)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSliceEqual_True(t *testing.T) {
|
||||
|
@ -62,3 +64,15 @@ func TestStructMapSubtract(t *testing.T) {
|
|||
assert.Equal(t, map[string]struct{}{"a": x, "b": x, "c": x}, m1)
|
||||
assert.Equal(t, map[string]struct{}{"a": x, "c": x}, m2)
|
||||
}
|
||||
|
||||
// GenerateRandomSHA256 generates a random 64 character SHA 256 hash string
|
||||
func TestGenerateRandomSHA256(t *testing.T) {
|
||||
res := GenerateRandomSHA256()
|
||||
assert.Len(t, res, 64)
|
||||
assert.NotContains(t, res, "sha256:")
|
||||
}
|
||||
|
||||
func TestGenerateRandomPrefixedSHA256(t *testing.T) {
|
||||
res := GenerateRandomPrefixedSHA256()
|
||||
assert.Regexp(t, regexp.MustCompile("sha256:[0-9|a-f]{64}"), res)
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ nav:
|
|||
- 'Stop signals': 'stop-signals.md'
|
||||
- 'Lifecycle hooks': 'lifecycle-hooks.md'
|
||||
- 'Running multiple instances': 'running-multiple-instances.md'
|
||||
- 'HTTP API Mode': 'http-api-mode.md'
|
||||
- 'Metrics': 'metrics.md'
|
||||
plugins:
|
||||
- search
|
||||
|
|
BIN
oryxBuildBinary
Executable file
BIN
oryxBuildBinary
Executable file
Binary file not shown.
|
@ -2,18 +2,18 @@ package metrics_test
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/api"
|
||||
metricsAPI "github.com/containrrr/watchtower/pkg/api/metrics"
|
||||
"github.com/containrrr/watchtower/pkg/metrics"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -36,7 +36,7 @@ func getWithToken(handler http.Handler) map[string]string {
|
|||
handler.ServeHTTP(respWriter, req)
|
||||
|
||||
res := respWriter.Result()
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
||||
for _, line := range strings.Split(string(body), "\n") {
|
||||
if len(line) < 1 || line[0] == '#' {
|
||||
|
|
|
@ -3,14 +3,10 @@ package container
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/registry"
|
||||
"github.com/containrrr/watchtower/pkg/registry/digest"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
|
@ -18,6 +14,10 @@ import (
|
|||
sdkClient "github.com/docker/docker/client"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/registry"
|
||||
"github.com/containrrr/watchtower/pkg/registry/digest"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
const defaultStopSignal = "SIGTERM"
|
||||
|
@ -25,23 +25,23 @@ const defaultStopSignal = "SIGTERM"
|
|||
// A Client is the interface through which watchtower interacts with the
|
||||
// Docker API.
|
||||
type Client interface {
|
||||
ListContainers(t.Filter) ([]Container, error)
|
||||
GetContainer(containerID t.ContainerID) (Container, error)
|
||||
StopContainer(Container, time.Duration) error
|
||||
StartContainer(Container) (t.ContainerID, error)
|
||||
RenameContainer(Container, string) error
|
||||
IsContainerStale(Container) (stale bool, latestImage t.ImageID, err error)
|
||||
ListContainers(t.Filter) ([]t.Container, error)
|
||||
GetContainer(containerID t.ContainerID) (t.Container, error)
|
||||
StopContainer(t.Container, time.Duration) error
|
||||
StartContainer(t.Container) (t.ContainerID, error)
|
||||
RenameContainer(t.Container, string) error
|
||||
IsContainerStale(t.Container, t.UpdateParams) (stale bool, latestImage t.ImageID, err error)
|
||||
ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error)
|
||||
RemoveImageByID(t.ImageID) error
|
||||
WarnOnHeadPullFailed(container Container) bool
|
||||
WarnOnHeadPullFailed(container t.Container) bool
|
||||
}
|
||||
|
||||
// NewClient returns a new Client instance which can be used to interact with
|
||||
// the Docker API.
|
||||
// The client reads its configuration from the following environment variables:
|
||||
// * DOCKER_HOST the docker-engine host to send api requests to
|
||||
// * DOCKER_TLS_VERIFY whether to verify tls certificates
|
||||
// * DOCKER_API_VERSION the minimum docker api version to work with
|
||||
// - DOCKER_HOST the docker-engine host to send api requests to
|
||||
// - DOCKER_TLS_VERIFY whether to verify tls certificates
|
||||
// - DOCKER_API_VERSION the minimum docker api version to work with
|
||||
func NewClient(opts ClientOptions) Client {
|
||||
cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv)
|
||||
|
||||
|
@ -57,7 +57,6 @@ func NewClient(opts ClientOptions) Client {
|
|||
|
||||
// ClientOptions contains the options for how the docker client wrapper should behave
|
||||
type ClientOptions struct {
|
||||
PullImages bool
|
||||
RemoveVolumes bool
|
||||
IncludeStopped bool
|
||||
ReviveStopped bool
|
||||
|
@ -82,7 +81,7 @@ type dockerClient struct {
|
|||
ClientOptions
|
||||
}
|
||||
|
||||
func (client dockerClient) WarnOnHeadPullFailed(container Container) bool {
|
||||
func (client dockerClient) WarnOnHeadPullFailed(container t.Container) bool {
|
||||
if client.WarnOnHeadFailed == WarnAlways {
|
||||
return true
|
||||
}
|
||||
|
@ -93,8 +92,8 @@ func (client dockerClient) WarnOnHeadPullFailed(container Container) bool {
|
|||
return registry.WarnOnAPIConsumption(container)
|
||||
}
|
||||
|
||||
func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) {
|
||||
cs := []Container{}
|
||||
func (client dockerClient) ListContainers(fn t.Filter) ([]t.Container, error) {
|
||||
cs := []t.Container{}
|
||||
bg := context.Background()
|
||||
|
||||
if client.IncludeStopped && client.IncludeRestarting {
|
||||
|
@ -149,24 +148,40 @@ func (client dockerClient) createListFilter() filters.Args {
|
|||
return filterArgs
|
||||
}
|
||||
|
||||
func (client dockerClient) GetContainer(containerID t.ContainerID) (Container, error) {
|
||||
func (client dockerClient) GetContainer(containerID t.ContainerID) (t.Container, error) {
|
||||
bg := context.Background()
|
||||
|
||||
containerInfo, err := client.api.ContainerInspect(bg, string(containerID))
|
||||
if err != nil {
|
||||
return Container{}, err
|
||||
return &Container{}, err
|
||||
}
|
||||
|
||||
netType, netContainerId, found := strings.Cut(string(containerInfo.HostConfig.NetworkMode), ":")
|
||||
if found && netType == "container" {
|
||||
parentContainer, err := client.api.ContainerInspect(bg, netContainerId)
|
||||
if err != nil {
|
||||
log.WithFields(map[string]interface{}{
|
||||
"container": containerInfo.Name,
|
||||
"error": err,
|
||||
"network-container": netContainerId,
|
||||
}).Warnf("Unable to resolve network container: %v", err)
|
||||
|
||||
} else {
|
||||
// Replace the container ID with a container name to allow it to reference the re-created network container
|
||||
containerInfo.HostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf("container:%s", parentContainer.Name))
|
||||
}
|
||||
}
|
||||
|
||||
imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to retrieve container image info: %v", err)
|
||||
return Container{containerInfo: &containerInfo, imageInfo: nil}, nil
|
||||
return &Container{containerInfo: &containerInfo, imageInfo: nil}, nil
|
||||
}
|
||||
|
||||
return Container{containerInfo: &containerInfo, imageInfo: &imageInfo}, nil
|
||||
return &Container{containerInfo: &containerInfo, imageInfo: &imageInfo}, nil
|
||||
}
|
||||
|
||||
func (client dockerClient) StopContainer(c Container, timeout time.Duration) error {
|
||||
func (client dockerClient) StopContainer(c t.Container, timeout time.Duration) error {
|
||||
bg := context.Background()
|
||||
signal := c.StopSignal()
|
||||
if signal == "" {
|
||||
|
@ -186,7 +201,7 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
|
|||
// TODO: This should probably be checked.
|
||||
_ = client.waitForStopOrTimeout(c, timeout)
|
||||
|
||||
if c.containerInfo.HostConfig.AutoRemove {
|
||||
if c.ContainerInfo().HostConfig.AutoRemove {
|
||||
log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", shortID)
|
||||
} else {
|
||||
log.Debugf("Removing container %s", shortID)
|
||||
|
@ -208,11 +223,34 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
|
|||
return nil
|
||||
}
|
||||
|
||||
func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) {
|
||||
func (client dockerClient) GetNetworkConfig(c t.Container) *network.NetworkingConfig {
|
||||
config := &network.NetworkingConfig{
|
||||
EndpointsConfig: c.ContainerInfo().NetworkSettings.Networks,
|
||||
}
|
||||
|
||||
for _, ep := range config.EndpointsConfig {
|
||||
aliases := make([]string, 0, len(ep.Aliases))
|
||||
cidAlias := c.ID().ShortID()
|
||||
|
||||
// Remove the old container ID alias from the network aliases, as it would accumulate across updates otherwise
|
||||
for _, alias := range ep.Aliases {
|
||||
if alias == cidAlias {
|
||||
continue
|
||||
}
|
||||
aliases = append(aliases, alias)
|
||||
}
|
||||
|
||||
ep.Aliases = aliases
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func (client dockerClient) StartContainer(c t.Container) (t.ContainerID, error) {
|
||||
bg := context.Background()
|
||||
config := c.runtimeConfig()
|
||||
hostConfig := c.hostConfig()
|
||||
networkConfig := &network.NetworkingConfig{EndpointsConfig: c.containerInfo.NetworkSettings.Networks}
|
||||
config := c.GetCreateConfig()
|
||||
hostConfig := c.GetCreateHostConfig()
|
||||
networkConfig := client.GetNetworkConfig(c)
|
||||
|
||||
// simpleNetworkConfig is a networkConfig with only 1 network.
|
||||
// see: https://github.com/docker/docker/issues/29265
|
||||
simpleNetworkConfig := func() *network.NetworkingConfig {
|
||||
|
@ -228,6 +266,7 @@ func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) {
|
|||
name := c.Name()
|
||||
|
||||
log.Infof("Creating %s", name)
|
||||
|
||||
createdContainer, err := client.api.ContainerCreate(bg, config, hostConfig, simpleNetworkConfig, nil, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -260,7 +299,7 @@ func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) {
|
|||
|
||||
}
|
||||
|
||||
func (client dockerClient) doStartContainer(bg context.Context, c Container, creation container.ContainerCreateCreatedBody) error {
|
||||
func (client dockerClient) doStartContainer(bg context.Context, c t.Container, creation container.CreateResponse) error {
|
||||
name := c.Name()
|
||||
|
||||
log.Debugf("Starting container %s (%s)", name, t.ContainerID(creation.ID).ShortID())
|
||||
|
@ -271,16 +310,16 @@ func (client dockerClient) doStartContainer(bg context.Context, c Container, cre
|
|||
return nil
|
||||
}
|
||||
|
||||
func (client dockerClient) RenameContainer(c Container, newName string) error {
|
||||
func (client dockerClient) RenameContainer(c t.Container, newName string) error {
|
||||
bg := context.Background()
|
||||
log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID().ShortID(), newName)
|
||||
return client.api.ContainerRename(bg, string(c.ID()), newName)
|
||||
}
|
||||
|
||||
func (client dockerClient) IsContainerStale(container Container) (stale bool, latestImage t.ImageID, err error) {
|
||||
func (client dockerClient) IsContainerStale(container t.Container, params t.UpdateParams) (stale bool, latestImage t.ImageID, err error) {
|
||||
ctx := context.Background()
|
||||
|
||||
if !client.PullImages {
|
||||
if container.IsNoPull(params) {
|
||||
log.Debugf("Skipping image pull.")
|
||||
} else if err := client.PullImage(ctx, container); err != nil {
|
||||
return false, container.SafeImageID(), err
|
||||
|
@ -289,8 +328,8 @@ func (client dockerClient) IsContainerStale(container Container) (stale bool, la
|
|||
return client.HasNewImage(ctx, container)
|
||||
}
|
||||
|
||||
func (client dockerClient) HasNewImage(ctx context.Context, container Container) (hasNew bool, latestImage t.ImageID, err error) {
|
||||
currentImageID := t.ImageID(container.containerInfo.ContainerJSONBase.Image)
|
||||
func (client dockerClient) HasNewImage(ctx context.Context, container t.Container) (hasNew bool, latestImage t.ImageID, err error) {
|
||||
currentImageID := t.ImageID(container.ContainerInfo().ContainerJSONBase.Image)
|
||||
imageName := container.ImageName()
|
||||
|
||||
newImageInfo, _, err := client.api.ImageInspectWithRaw(ctx, imageName)
|
||||
|
@ -310,7 +349,7 @@ func (client dockerClient) HasNewImage(ctx context.Context, container Container)
|
|||
|
||||
// PullImage pulls the latest image for the supplied container, optionally skipping if it's digest can be confirmed
|
||||
// to match the one that the registry reports via a HEAD request
|
||||
func (client dockerClient) PullImage(ctx context.Context, container Container) error {
|
||||
func (client dockerClient) PullImage(ctx context.Context, container t.Container) error {
|
||||
containerName := container.Name()
|
||||
imageName := container.ImageName()
|
||||
|
||||
|
@ -359,7 +398,7 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e
|
|||
|
||||
defer response.Close()
|
||||
// the pull request will be aborted prematurely unless the response is read
|
||||
if _, err = ioutil.ReadAll(response); err != nil {
|
||||
if _, err = io.ReadAll(response); err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
@ -369,13 +408,34 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e
|
|||
func (client dockerClient) RemoveImageByID(id t.ImageID) error {
|
||||
log.Infof("Removing image %s", id.ShortID())
|
||||
|
||||
_, err := client.api.ImageRemove(
|
||||
items, err := client.api.ImageRemove(
|
||||
context.Background(),
|
||||
string(id),
|
||||
types.ImageRemoveOptions{
|
||||
Force: true,
|
||||
})
|
||||
|
||||
if log.IsLevelEnabled(log.DebugLevel) {
|
||||
deleted := strings.Builder{}
|
||||
untagged := strings.Builder{}
|
||||
for _, item := range items {
|
||||
if item.Deleted != "" {
|
||||
if deleted.Len() > 0 {
|
||||
deleted.WriteString(`, `)
|
||||
}
|
||||
deleted.WriteString(t.ImageID(item.Deleted).ShortID())
|
||||
}
|
||||
if item.Untagged != "" {
|
||||
if untagged.Len() > 0 {
|
||||
untagged.WriteString(`, `)
|
||||
}
|
||||
untagged.WriteString(t.ImageID(item.Untagged).ShortID())
|
||||
}
|
||||
}
|
||||
fields := log.Fields{`deleted`: deleted.String(), `untagged`: untagged.String()}
|
||||
log.WithFields(fields).Debug("Image removal completed")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -478,7 +538,7 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (client dockerClient) waitForStopOrTimeout(c Container, waitTime time.Duration) error {
|
||||
func (client dockerClient) waitForStopOrTimeout(c t.Container, waitTime time.Duration) error {
|
||||
bg := context.Background()
|
||||
timeout := time.After(waitTime)
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/util"
|
||||
"github.com/containrrr/watchtower/pkg/container/mocks"
|
||||
"github.com/containrrr/watchtower/pkg/filters"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
|
@ -10,6 +12,7 @@ import (
|
|||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
cli "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/onsi/gomega/ghttp"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -35,8 +38,8 @@ var _ = Describe("the client", func() {
|
|||
mockServer.Close()
|
||||
})
|
||||
Describe("WarnOnHeadPullFailed", func() {
|
||||
containerUnknown := *MockContainer(WithImageName("unknown.repo/prefix/imagename:latest"))
|
||||
containerKnown := *MockContainer(WithImageName("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}}
|
||||
|
@ -66,16 +69,17 @@ 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 := *MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b"))
|
||||
c.PullImage(context.Background(), pinnedContainer)
|
||||
pinnedContainer := MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b"))
|
||||
err := c.PullImage(context.Background(), pinnedContainer)
|
||||
Expect(err).To(MatchError(`container uses a pinned image, and cannot be updated by watchtower`))
|
||||
})
|
||||
})
|
||||
})
|
||||
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}))
|
||||
container := MockContainer(WithContainerState(types.ContainerState{Running: true}))
|
||||
containerStopped := MockContainer(WithContainerState(types.ContainerState{Running: false}))
|
||||
|
||||
cid := container.ContainerInfo().ID
|
||||
mockServer.AppendHandlers(
|
||||
|
@ -90,7 +94,7 @@ var _ = Describe("the client", func() {
|
|||
})
|
||||
When("the container does not exist after stopping", func() {
|
||||
It("should not cause an error", func() {
|
||||
container := *MockContainer(WithContainerState(types.ContainerState{Running: true}))
|
||||
container := MockContainer(WithContainerState(types.ContainerState{Running: true}))
|
||||
|
||||
cid := container.ContainerInfo().ID
|
||||
mockServer.AppendHandlers(
|
||||
|
@ -103,14 +107,45 @@ var _ = Describe("the client", func() {
|
|||
})
|
||||
})
|
||||
})
|
||||
When("removing a image", func() {
|
||||
When("debug logging is enabled", func() {
|
||||
It("should log removed and untagged images", func() {
|
||||
imageA := util.GenerateRandomSHA256()
|
||||
imageAParent := util.GenerateRandomSHA256()
|
||||
images := map[string][]string{imageA: {imageAParent}}
|
||||
mockServer.AppendHandlers(mocks.RemoveImageHandler(images))
|
||||
c := dockerClient{api: docker}
|
||||
|
||||
resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
|
||||
defer resetLogrus()
|
||||
|
||||
Expect(c.RemoveImageByID(t.ImageID(imageA))).To(Succeed())
|
||||
|
||||
shortA := t.ImageID(imageA).ShortID()
|
||||
shortAParent := t.ImageID(imageAParent).ShortID()
|
||||
|
||||
Eventually(logbuf).Should(gbytes.Say(`deleted="%v, %v" untagged="?%v"?`, shortA, shortAParent, shortA))
|
||||
})
|
||||
})
|
||||
When("image is not found", func() {
|
||||
It("should return an error", func() {
|
||||
image := util.GenerateRandomSHA256()
|
||||
mockServer.AppendHandlers(mocks.RemoveImageHandler(nil))
|
||||
c := dockerClient{api: docker}
|
||||
|
||||
err := c.RemoveImageByID(t.ImageID(image))
|
||||
Expect(errdefs.IsNotFound(err)).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
When("listing containers", func() {
|
||||
When("no filter is provided", func() {
|
||||
It("should return all available containers", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{PullImages: false},
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
containers, err := client.ListContainers(filters.NoFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
@ -120,11 +155,11 @@ var _ = Describe("the client", func() {
|
|||
When("a filter matching nothing", func() {
|
||||
It("should return an empty array", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
|
||||
filter := filters.FilterByNames([]string{"lollercoaster"}, filters.NoFilter)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{PullImages: false},
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
containers, err := client.ListContainers(filter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
@ -134,10 +169,10 @@ var _ = Describe("the client", func() {
|
|||
When("a watchtower filter is provided", func() {
|
||||
It("should return only the watchtower container", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{PullImages: false},
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
containers, err := client.ListContainers(filters.WatchtowerContainersFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
@ -147,10 +182,10 @@ var _ = Describe("the client", func() {
|
|||
When(`include stopped is enabled`, func() {
|
||||
It("should return both stopped and running containers", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running", "exited", "created"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("stopped", "watchtower", "running")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Stopped, &mocks.Watchtower, &mocks.Running)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{PullImages: false, IncludeStopped: true},
|
||||
ClientOptions: ClientOptions{IncludeStopped: true},
|
||||
}
|
||||
containers, err := client.ListContainers(filters.NoFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
@ -160,10 +195,10 @@ var _ = Describe("the client", func() {
|
|||
When(`include restarting is enabled`, func() {
|
||||
It("should return both restarting and running containers", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running", "restarting"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running", "restarting")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running, &mocks.Restarting)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: true},
|
||||
ClientOptions: ClientOptions{IncludeRestarting: true},
|
||||
}
|
||||
containers, err := client.ListContainers(filters.NoFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
@ -173,30 +208,58 @@ var _ = Describe("the client", func() {
|
|||
When(`include restarting is disabled`, func() {
|
||||
It("should not return restarting containers", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: false},
|
||||
ClientOptions: ClientOptions{IncludeRestarting: false},
|
||||
}
|
||||
containers, err := client.ListContainers(filters.NoFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(containers).NotTo(ContainElement(havingRestartingState(true)))
|
||||
})
|
||||
})
|
||||
When(`a container uses container network mode`, func() {
|
||||
When(`the network container can be resolved`, func() {
|
||||
It("should return the container name instead of the ID", func() {
|
||||
consumerContainerRef := mocks.NetConsumerOK
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
container, err := client.GetContainer(consumerContainerRef.ContainerID())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
networkMode := container.ContainerInfo().HostConfig.NetworkMode
|
||||
Expect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierContainerName))
|
||||
})
|
||||
})
|
||||
When(`the network container cannot be resolved`, func() {
|
||||
It("should still return the container ID", func() {
|
||||
consumerContainerRef := mocks.NetConsumerInvalidSupplier
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
container, err := client.GetContainer(consumerContainerRef.ContainerID())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
networkMode := container.ContainerInfo().HostConfig.NetworkMode
|
||||
Expect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierNotFoundID))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Describe(`ExecuteCommand`, func() {
|
||||
When(`logging`, func() {
|
||||
It("should include container id field", func() {
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{PullImages: false},
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
|
||||
// Capture logrus output in buffer
|
||||
logbuf := gbytes.NewBuffer()
|
||||
origOut := logrus.StandardLogger().Out
|
||||
defer logrus.SetOutput(origOut)
|
||||
logrus.SetOutput(logbuf)
|
||||
resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
|
||||
defer resetLogrus()
|
||||
|
||||
user := ""
|
||||
containerID := t.ContainerID("ex-cont-id")
|
||||
|
@ -253,7 +316,43 @@ var _ = Describe("the client", func() {
|
|||
})
|
||||
})
|
||||
})
|
||||
Describe(`GetNetworkConfig`, func() {
|
||||
When(`providing a container with network aliases`, func() {
|
||||
It(`should omit the container ID alias`, func() {
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{IncludeRestarting: false},
|
||||
}
|
||||
container := MockContainer(WithImageName("docker.io/prefix/imagename:latest"))
|
||||
|
||||
aliases := []string{"One", "Two", container.ID().ShortID(), "Four"}
|
||||
endpoints := map[string]*network.EndpointSettings{
|
||||
`test`: {Aliases: aliases},
|
||||
}
|
||||
container.containerInfo.NetworkSettings = &types.NetworkSettings{Networks: endpoints}
|
||||
Expect(container.ContainerInfo().NetworkSettings.Networks[`test`].Aliases).To(Equal(aliases))
|
||||
Expect(client.GetNetworkConfig(container).EndpointsConfig[`test`].Aliases).To(Equal([]string{"One", "Two", "Four"}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Capture logrus output in buffer
|
||||
func captureLogrus(level logrus.Level) (func(), *gbytes.Buffer) {
|
||||
|
||||
logbuf := gbytes.NewBuffer()
|
||||
|
||||
origOut := logrus.StandardLogger().Out
|
||||
logrus.SetOutput(logbuf)
|
||||
|
||||
origLev := logrus.StandardLogger().Level
|
||||
logrus.SetLevel(level)
|
||||
|
||||
return func() {
|
||||
logrus.SetOutput(origOut)
|
||||
logrus.SetLevel(origLev)
|
||||
}, logbuf
|
||||
}
|
||||
|
||||
// Gomega matcher helpers
|
||||
|
||||
|
@ -261,18 +360,18 @@ func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher {
|
|||
return WithTransform(containerImageName, matcher)
|
||||
}
|
||||
|
||||
func containerImageName(container Container) string {
|
||||
func containerImageName(container t.Container) string {
|
||||
return container.ImageName()
|
||||
}
|
||||
|
||||
func havingRestartingState(expected bool) gt.GomegaMatcher {
|
||||
return WithTransform(func(container Container) bool {
|
||||
return container.containerInfo.State.Restarting
|
||||
return WithTransform(func(container t.Container) bool {
|
||||
return container.ContainerInfo().State.Restarting
|
||||
}, Equal(expected))
|
||||
}
|
||||
|
||||
func havingRunningState(expected bool) gt.GomegaMatcher {
|
||||
return WithTransform(func(container Container) bool {
|
||||
return container.containerInfo.State.Running
|
||||
return WithTransform(func(container t.Container) bool {
|
||||
return container.ContainerInfo().State.Running
|
||||
}, Equal(expected))
|
||||
}
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/util"
|
||||
wt "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockercontainer "github.com/docker/docker/api/types/container"
|
||||
|
@ -32,6 +34,26 @@ type Container struct {
|
|||
imageInfo *types.ImageInspect
|
||||
}
|
||||
|
||||
// IsLinkedToRestarting returns the current value of the LinkedToRestarting field for the container
|
||||
func (c *Container) IsLinkedToRestarting() bool {
|
||||
return c.LinkedToRestarting
|
||||
}
|
||||
|
||||
// IsStale returns the current value of the Stale field for the container
|
||||
func (c *Container) IsStale() bool {
|
||||
return c.Stale
|
||||
}
|
||||
|
||||
// SetLinkedToRestarting sets the LinkedToRestarting field for the container
|
||||
func (c *Container) SetLinkedToRestarting(value bool) {
|
||||
c.LinkedToRestarting = value
|
||||
}
|
||||
|
||||
// SetStale implements sets the Stale field for the container
|
||||
func (c *Container) SetStale(value bool) {
|
||||
c.Stale = value
|
||||
}
|
||||
|
||||
// ContainerInfo fetches JSON info for the container
|
||||
func (c Container) ContainerInfo() *types.ContainerJSON {
|
||||
return c.containerInfo
|
||||
|
@ -109,20 +131,31 @@ func (c Container) Enabled() (bool, bool) {
|
|||
return parsedBool, true
|
||||
}
|
||||
|
||||
// IsMonitorOnly returns the value of the monitor-only label. If the label
|
||||
// is not set then false is returned.
|
||||
func (c Container) IsMonitorOnly() bool {
|
||||
rawBool, ok := c.getLabelValue(monitorOnlyLabel)
|
||||
if !ok {
|
||||
return false
|
||||
// IsMonitorOnly returns whether the container should only be monitored based on values of
|
||||
// the monitor-only label, the monitor-only argument and the label-take-precedence argument.
|
||||
func (c Container) IsMonitorOnly(params wt.UpdateParams) bool {
|
||||
return c.getContainerOrGlobalBool(params.MonitorOnly, monitorOnlyLabel, params.LabelPrecedence)
|
||||
}
|
||||
|
||||
parsedBool, err := strconv.ParseBool(rawBool)
|
||||
if err != nil {
|
||||
return false
|
||||
// IsNoPull returns whether the image should be pulled based on values of
|
||||
// the no-pull label, the no-pull argument and the label-take-precedence argument.
|
||||
func (c Container) IsNoPull(params wt.UpdateParams) bool {
|
||||
return c.getContainerOrGlobalBool(params.NoPull, noPullLabel, params.LabelPrecedence)
|
||||
}
|
||||
|
||||
return parsedBool
|
||||
func (c Container) getContainerOrGlobalBool(globalVal bool, label string, contPrecedence bool) bool {
|
||||
if contVal, err := c.getBoolLabelValue(label); err != nil {
|
||||
if !errors.Is(err, errorLabelNotFound) {
|
||||
logrus.WithField("error", err).WithField("label", label).Warn("Failed to parse label value")
|
||||
}
|
||||
return globalVal
|
||||
} else {
|
||||
if contPrecedence {
|
||||
return contVal
|
||||
} else {
|
||||
return contVal || globalVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scope returns the value of the scope UID label and if the label
|
||||
|
@ -144,7 +177,14 @@ func (c Container) Links() []string {
|
|||
dependsOnLabelValue := c.getLabelValueOrEmpty(dependsOnLabel)
|
||||
|
||||
if dependsOnLabelValue != "" {
|
||||
links := strings.Split(dependsOnLabelValue, ",")
|
||||
for _, link := range strings.Split(dependsOnLabelValue, ",") {
|
||||
// Since the container names need to start with '/', let's prepend it if it's missing
|
||||
if !strings.HasPrefix(link, "/") {
|
||||
link = "/" + link
|
||||
}
|
||||
links = append(links, link)
|
||||
}
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
|
@ -153,6 +193,13 @@ func (c Container) Links() []string {
|
|||
name := strings.Split(link, ":")[0]
|
||||
links = append(links, name)
|
||||
}
|
||||
|
||||
// If the container uses another container for networking, it can be considered an implicit link
|
||||
// since the container would stop working if the network supplier were to be recreated
|
||||
networkMode := c.containerInfo.HostConfig.NetworkMode
|
||||
if networkMode.IsContainer() {
|
||||
links = append(links, networkMode.ConnectedContainer())
|
||||
}
|
||||
}
|
||||
|
||||
return links
|
||||
|
@ -217,18 +264,23 @@ func (c Container) StopSignal() string {
|
|||
return c.getLabelValueOrEmpty(signalLabel)
|
||||
}
|
||||
|
||||
// GetCreateConfig returns the container's current Config converted into a format
|
||||
// that can be re-submitted to the Docker create API.
|
||||
//
|
||||
// Ideally, we'd just be able to take the ContainerConfig from the old container
|
||||
// and use it as the starting point for creating the new container; however,
|
||||
// the ContainerConfig that comes back from the Inspect call merges the default
|
||||
// configuration (the stuff specified in the metadata for the image itself)
|
||||
// with the overridden configuration (the stuff that you might specify as part
|
||||
// of the "docker run"). In order to avoid unintentionally overriding the
|
||||
// of the "docker run").
|
||||
//
|
||||
// In order to avoid unintentionally overriding the
|
||||
// defaults in the new image we need to separate the override options from the
|
||||
// default options. To do this we have to compare the ContainerConfig for the
|
||||
// running container with the ContainerConfig from the image that container was
|
||||
// started from. This function returns a ContainerConfig which contains just
|
||||
// the options overridden at runtime.
|
||||
func (c Container) runtimeConfig() *dockercontainer.Config {
|
||||
func (c Container) GetCreateConfig() *dockercontainer.Config {
|
||||
config := c.containerInfo.Config
|
||||
hostConfig := c.containerInfo.HostConfig
|
||||
imageConfig := c.imageInfo.Config
|
||||
|
@ -252,6 +304,29 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
|
|||
}
|
||||
}
|
||||
|
||||
// Clear HEALTHCHECK configuration (if default)
|
||||
if config.Healthcheck != nil && imageConfig.Healthcheck != nil {
|
||||
if util.SliceEqual(config.Healthcheck.Test, imageConfig.Healthcheck.Test) {
|
||||
config.Healthcheck.Test = nil
|
||||
}
|
||||
|
||||
if config.Healthcheck.Retries == imageConfig.Healthcheck.Retries {
|
||||
config.Healthcheck.Retries = 0
|
||||
}
|
||||
|
||||
if config.Healthcheck.Interval == imageConfig.Healthcheck.Interval {
|
||||
config.Healthcheck.Interval = 0
|
||||
}
|
||||
|
||||
if config.Healthcheck.Timeout == imageConfig.Healthcheck.Timeout {
|
||||
config.Healthcheck.Timeout = 0
|
||||
}
|
||||
|
||||
if config.Healthcheck.StartPeriod == imageConfig.Healthcheck.StartPeriod {
|
||||
config.Healthcheck.StartPeriod = 0
|
||||
}
|
||||
}
|
||||
|
||||
config.Env = util.SliceSubtract(config.Env, imageConfig.Env)
|
||||
|
||||
config.Labels = util.StringMapSubtract(config.Labels, imageConfig.Labels)
|
||||
|
@ -272,9 +347,9 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
|
|||
return config
|
||||
}
|
||||
|
||||
// Any links in the HostConfig need to be re-written before they can be
|
||||
// re-submitted to the Docker create API.
|
||||
func (c Container) hostConfig() *dockercontainer.HostConfig {
|
||||
// GetCreateHostConfig returns the container's current HostConfig with any links
|
||||
// re-written so that they can be re-submitted to the Docker create API.
|
||||
func (c Container) GetCreateHostConfig() *dockercontainer.HostConfig {
|
||||
hostConfig := c.containerInfo.HostConfig
|
||||
|
||||
for i, link := range hostConfig.Links {
|
||||
|
|
|
@ -22,6 +22,7 @@ func MockContainer(updates ...MockContainerUpdate) *Container {
|
|||
}
|
||||
image := types.ImageInspect{
|
||||
ID: "image_id",
|
||||
Config: &dockerContainer.Config{},
|
||||
}
|
||||
|
||||
for _, update := range updates {
|
||||
|
@ -64,3 +65,15 @@ func WithContainerState(state types.ContainerState) MockContainerUpdate {
|
|||
cnt.State = &state
|
||||
}
|
||||
}
|
||||
|
||||
func WithHealthcheck(healthConfig dockerContainer.HealthConfig) MockContainerUpdate {
|
||||
return func(cnt *types.ContainerJSON, img *types.ImageInspect) {
|
||||
cnt.Config.Healthcheck = &healthConfig
|
||||
}
|
||||
}
|
||||
|
||||
func WithImageHealthcheck(healthConfig dockerContainer.HealthConfig) MockContainerUpdate {
|
||||
return func(cnt *types.ContainerJSON, img *types.ImageInspect) {
|
||||
img.Config.Healthcheck = &healthConfig
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
dc "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
@ -66,6 +68,93 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
})
|
||||
})
|
||||
Describe("GetCreateConfig", func() {
|
||||
When("container healthcheck config is equal to image config", func() {
|
||||
It("should return empty healthcheck values", func() {
|
||||
c := MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
}), WithImageHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))
|
||||
|
||||
c = MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
Timeout: 30,
|
||||
}), WithImageHealthcheck(dc.HealthConfig{
|
||||
Timeout: 30,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))
|
||||
|
||||
c = MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
StartPeriod: 30,
|
||||
}), WithImageHealthcheck(dc.HealthConfig{
|
||||
StartPeriod: 30,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))
|
||||
|
||||
c = MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
Retries: 30,
|
||||
}), WithImageHealthcheck(dc.HealthConfig{
|
||||
Retries: 30,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))
|
||||
})
|
||||
})
|
||||
When("container healthcheck config is different to image config", func() {
|
||||
It("should return the container healthcheck values", func() {
|
||||
c := MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
Interval: 30,
|
||||
Timeout: 30,
|
||||
StartPeriod: 10,
|
||||
Retries: 2,
|
||||
}), WithImageHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "10s"},
|
||||
Interval: 10,
|
||||
Timeout: 60,
|
||||
StartPeriod: 30,
|
||||
Retries: 10,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
Interval: 30,
|
||||
Timeout: 30,
|
||||
StartPeriod: 10,
|
||||
Retries: 2,
|
||||
}))
|
||||
})
|
||||
})
|
||||
When("container healthcheck config is empty", func() {
|
||||
It("should not panic", func() {
|
||||
c := MockContainer(WithImageHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "10s"},
|
||||
Interval: 10,
|
||||
Timeout: 60,
|
||||
StartPeriod: 30,
|
||||
Retries: 10,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(BeNil())
|
||||
})
|
||||
})
|
||||
When("container image healthcheck config is empty", func() {
|
||||
It("should not panic", func() {
|
||||
c := MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
Interval: 30,
|
||||
Timeout: 30,
|
||||
StartPeriod: 10,
|
||||
Retries: 2,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
Interval: 30,
|
||||
Timeout: 30,
|
||||
StartPeriod: 10,
|
||||
Retries: 2,
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
When("asked for metadata", func() {
|
||||
var c *Container
|
||||
BeforeEach(func() {
|
||||
|
@ -178,14 +267,21 @@ var _ = Describe("the container", func() {
|
|||
"com.centurylinklabs.watchtower.depends-on": "postgres",
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("postgres"), HaveLen(1)))
|
||||
Expect(links).To(SatisfyAll(ContainElement("/postgres"), HaveLen(1)))
|
||||
})
|
||||
It("should fetch depending containers if there are many", func() {
|
||||
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)))
|
||||
Expect(links).To(SatisfyAll(ContainElement("/postgres"), ContainElement("/redis"), HaveLen(2)))
|
||||
})
|
||||
It("should only add slashes to names when they are missing", func() {
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.depends-on": "/postgres,redis",
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("/postgres"), ContainElement("/redis")))
|
||||
})
|
||||
It("should fetch depending containers if label is blank", func() {
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
|
@ -207,6 +303,77 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
})
|
||||
|
||||
When("checking no-pull label", func() {
|
||||
When("no-pull argument is not set", func() {
|
||||
When("no-pull label is true", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "true",
|
||||
}))
|
||||
It("should return true", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(true))
|
||||
})
|
||||
})
|
||||
When("no-pull label is false", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "false",
|
||||
}))
|
||||
It("should return false", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))
|
||||
})
|
||||
})
|
||||
When("no-pull label is set to an invalid value", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "maybe",
|
||||
}))
|
||||
It("should return false", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))
|
||||
})
|
||||
})
|
||||
When("no-pull label is unset", func() {
|
||||
c = MockContainer(WithLabels(map[string]string{}))
|
||||
It("should return false", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))
|
||||
})
|
||||
})
|
||||
})
|
||||
When("no-pull argument is set to true", func() {
|
||||
When("no-pull label is true", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "true",
|
||||
}))
|
||||
It("should return true", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{NoPull: true})).To(Equal(true))
|
||||
})
|
||||
})
|
||||
When("no-pull label is false", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "false",
|
||||
}))
|
||||
It("should return true", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{NoPull: true})).To(Equal(true))
|
||||
})
|
||||
})
|
||||
When("label-take-precedence argument is set to true", func() {
|
||||
When("no-pull label is true", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "true",
|
||||
}))
|
||||
It("should return true", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{LabelPrecedence: true, NoPull: true})).To(Equal(true))
|
||||
})
|
||||
})
|
||||
When("no-pull label is false", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "false",
|
||||
}))
|
||||
It("should return false", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{LabelPrecedence: true, NoPull: true})).To(Equal(false))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("there is a pre or post update timeout", func() {
|
||||
It("should return minute values", func() {
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
|
|
|
@ -5,3 +5,4 @@ import "errors"
|
|||
var errorNoImageInfo = errors.New("no available image info")
|
||||
var errorNoContainerInfo = errors.New("no available container info")
|
||||
var errorInvalidConfig = errors.New("container configuration missing or invalid")
|
||||
var errorLabelNotFound = errors.New("label was not found in container")
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package container
|
||||
|
||||
import "strconv"
|
||||
|
||||
const (
|
||||
watchtowerLabel = "com.centurylinklabs.watchtower"
|
||||
signalLabel = "com.centurylinklabs.watchtower.stop-signal"
|
||||
enableLabel = "com.centurylinklabs.watchtower.enable"
|
||||
monitorOnlyLabel = "com.centurylinklabs.watchtower.monitor-only"
|
||||
noPullLabel = "com.centurylinklabs.watchtower.no-pull"
|
||||
dependsOnLabel = "com.centurylinklabs.watchtower.depends-on"
|
||||
zodiacLabel = "com.centurylinklabs.zodiac.original-image"
|
||||
scope = "com.centurylinklabs.watchtower.scope"
|
||||
|
@ -54,3 +57,11 @@ func (c Container) getLabelValue(label string) (string, bool) {
|
|||
val, ok := c.containerInfo.Config.Labels[label]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (c Container) getBoolLabelValue(label string) (bool, error) {
|
||||
if strVal, ok := c.containerInfo.Config.Labels[label]; ok {
|
||||
value, err := strconv.ParseBool(strVal)
|
||||
return value, err
|
||||
}
|
||||
return false, errorLabelNotFound
|
||||
}
|
||||
|
|
|
@ -3,10 +3,14 @@ package mocks
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"github.com/onsi/ginkgo"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
|
@ -16,10 +20,9 @@ import (
|
|||
|
||||
func getMockJSONFile(relPath string) ([]byte, error) {
|
||||
absPath, _ := filepath.Abs(relPath)
|
||||
buf, err := ioutil.ReadFile(absPath)
|
||||
buf, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
// logrus.WithError(err).WithField("file", absPath).Error(err)
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("mock JSON file %q not found: %e", absPath, err)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
@ -40,19 +43,22 @@ func respondWithJSONFile(relPath string, statusCode int, optionalHeader ...http.
|
|||
}
|
||||
|
||||
// GetContainerHandlers returns the handlers serving lookups for the supplied container mock files
|
||||
func GetContainerHandlers(containerFiles ...string) []http.HandlerFunc {
|
||||
handlers := make([]http.HandlerFunc, 0, len(containerFiles)*2)
|
||||
for _, file := range containerFiles {
|
||||
handlers = append(handlers, getContainerFileHandler(file))
|
||||
func GetContainerHandlers(containerRefs ...*ContainerRef) []http.HandlerFunc {
|
||||
handlers := make([]http.HandlerFunc, 0, len(containerRefs)*3)
|
||||
for _, containerRef := range containerRefs {
|
||||
handlers = append(handlers, getContainerFileHandler(containerRef))
|
||||
|
||||
// Also append any containers that the container references, if any
|
||||
for _, ref := range containerRef.references {
|
||||
handlers = append(handlers, getContainerFileHandler(ref))
|
||||
}
|
||||
|
||||
// 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, getImageFileHandler(1))
|
||||
} else {
|
||||
handlers = append(handlers, getImageFileHandler(0))
|
||||
}
|
||||
handlers = append(handlers, getImageHandler(containerRef.image.id,
|
||||
RespondWithJSONFile(containerRef.image.getFileName(), http.StatusOK),
|
||||
))
|
||||
}
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
|
@ -64,24 +70,90 @@ func createFilterArgs(statuses []string) filters.Args {
|
|||
return args
|
||||
}
|
||||
|
||||
var containerFileIds = map[string]string{
|
||||
"stopped": "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65",
|
||||
"watchtower": "3d88e0e3543281c747d88b27e246578b65ae8964ba86c7cd7522cf84e0978134",
|
||||
"running": "b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008",
|
||||
"restarting": "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b67",
|
||||
var defaultImage = imageRef{
|
||||
// watchtower
|
||||
id: t.ImageID("sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa"),
|
||||
file: "default",
|
||||
}
|
||||
|
||||
var imageIds = []string{
|
||||
"sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa",
|
||||
"sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd",
|
||||
var Watchtower = ContainerRef{
|
||||
name: "watchtower",
|
||||
id: "3d88e0e3543281c747d88b27e246578b65ae8964ba86c7cd7522cf84e0978134",
|
||||
image: &defaultImage,
|
||||
}
|
||||
var Stopped = ContainerRef{
|
||||
name: "stopped",
|
||||
id: "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65",
|
||||
image: &defaultImage,
|
||||
}
|
||||
var Running = ContainerRef{
|
||||
name: "running",
|
||||
id: "b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008",
|
||||
image: &imageRef{
|
||||
// portainer
|
||||
id: t.ImageID("sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd"),
|
||||
file: "running",
|
||||
},
|
||||
}
|
||||
var Restarting = ContainerRef{
|
||||
name: "restarting",
|
||||
id: "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b67",
|
||||
image: &defaultImage,
|
||||
}
|
||||
|
||||
var netSupplierOK = ContainerRef{
|
||||
id: "25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2",
|
||||
name: "net_supplier",
|
||||
image: &imageRef{
|
||||
// gluetun
|
||||
id: t.ImageID("sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51"),
|
||||
file: "net_producer",
|
||||
},
|
||||
}
|
||||
var netSupplierNotFound = ContainerRef{
|
||||
id: NetSupplierNotFoundID,
|
||||
name: netSupplierOK.name,
|
||||
isMissing: true,
|
||||
}
|
||||
|
||||
// NetConsumerOK is used for testing `container` networking mode
|
||||
// returns a container that consumes an existing supplier container
|
||||
var NetConsumerOK = ContainerRef{
|
||||
id: "1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6",
|
||||
name: "net_consumer",
|
||||
image: &imageRef{
|
||||
id: t.ImageID("sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8"), // nginx
|
||||
file: "net_consumer",
|
||||
},
|
||||
references: []*ContainerRef{&netSupplierOK},
|
||||
}
|
||||
|
||||
// NetConsumerInvalidSupplier is used for testing `container` networking mode
|
||||
// returns a container that references a supplying container that does not exist
|
||||
var NetConsumerInvalidSupplier = ContainerRef{
|
||||
id: NetConsumerOK.id,
|
||||
name: "net_consumer-missing_supplier",
|
||||
image: NetConsumerOK.image,
|
||||
references: []*ContainerRef{&netSupplierNotFound},
|
||||
}
|
||||
|
||||
const NetSupplierNotFoundID = "badc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc"
|
||||
const NetSupplierContainerName = "/wt-contnet-producer-1"
|
||||
|
||||
func getContainerFileHandler(cr *ContainerRef) http.HandlerFunc {
|
||||
|
||||
if cr.isMissing {
|
||||
return containerNotFoundResponse(string(cr.id))
|
||||
}
|
||||
|
||||
containerFile, err := cr.getContainerFile()
|
||||
if err != nil {
|
||||
ginkgo.Fail(fmt.Sprintf("Failed to get container mock file: %v", err))
|
||||
}
|
||||
|
||||
func getContainerFileHandler(file string) http.HandlerFunc {
|
||||
id, ok := containerFileIds[file]
|
||||
failTestUnless(ok)
|
||||
return getContainerHandler(
|
||||
id,
|
||||
RespondWithJSONFile(fmt.Sprintf("./mocks/data/container_%v.json", file), http.StatusOK),
|
||||
string(cr.id),
|
||||
RespondWithJSONFile(containerFile, http.StatusOK),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -103,7 +175,7 @@ func GetContainerHandler(containerID string, containerInfo *types.ContainerJSON)
|
|||
|
||||
// GetImageHandler mocks the GET images/{id}/json endpoint
|
||||
func GetImageHandler(imageInfo *types.ImageInspect) http.HandlerFunc {
|
||||
return getImageHandler(imageInfo.ID, ghttp.RespondWithJSONEncoded(http.StatusOK, imageInfo))
|
||||
return getImageHandler(t.ImageID(imageInfo.ID), ghttp.RespondWithJSONEncoded(http.StatusOK, imageInfo))
|
||||
}
|
||||
|
||||
// ListContainersHandler mocks the GET containers/json endpoint, filtering the returned containers based on statuses
|
||||
|
@ -112,7 +184,6 @@ func ListContainersHandler(statuses ...string) http.HandlerFunc {
|
|||
bytes, err := filterArgs.MarshalJSON()
|
||||
O.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred())
|
||||
query := url.Values{
|
||||
"limit": []string{"0"},
|
||||
"filters": []string{string(bytes)},
|
||||
}
|
||||
return ghttp.CombineHandlers(
|
||||
|
@ -138,23 +209,13 @@ func respondWithFilteredContainers(filters filters.Args) http.HandlerFunc {
|
|||
return ghttp.RespondWithJSONEncoded(http.StatusOK, filteredContainers)
|
||||
}
|
||||
|
||||
func getImageHandler(imageId string, responseHandler http.HandlerFunc) http.HandlerFunc {
|
||||
func getImageHandler(imageId t.ImageID, responseHandler http.HandlerFunc) http.HandlerFunc {
|
||||
return ghttp.CombineHandlers(
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -180,7 +241,7 @@ func RemoveContainerHandler(containerID string, found FoundStatus) http.HandlerF
|
|||
}
|
||||
|
||||
func containerNotFoundResponse(containerID string) http.HandlerFunc {
|
||||
return ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{message: "No such container: " + containerID})
|
||||
return ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{message: "No such container: " + string(containerID)})
|
||||
}
|
||||
|
||||
var noContentStatusResponse = ghttp.RespondWith(http.StatusNoContent, nil)
|
||||
|
@ -191,3 +252,29 @@ const (
|
|||
Found FoundStatus = true
|
||||
Missing FoundStatus = false
|
||||
)
|
||||
|
||||
// RemoveImageHandler mocks the DELETE images/ID endpoint, simulating removal of the given imagesWithParents
|
||||
func RemoveImageHandler(imagesWithParents map[string][]string) http.HandlerFunc {
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("DELETE", O.MatchRegexp("/images/.*")),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(r.URL.Path, `/`)
|
||||
image := parts[len(parts)-1]
|
||||
|
||||
if parents, found := imagesWithParents[image]; found {
|
||||
items := []types.ImageDeleteResponseItem{
|
||||
{Untagged: image},
|
||||
{Deleted: image},
|
||||
}
|
||||
for _, parent := range parents {
|
||||
items = append(items, types.ImageDeleteResponseItem{Deleted: parent})
|
||||
}
|
||||
ghttp.RespondWithJSONEncoded(http.StatusOK, items)(w, r)
|
||||
} else {
|
||||
ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{
|
||||
message: "Something went wrong.",
|
||||
})(w, r)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
42
pkg/container/mocks/container_ref.go
Normal file
42
pkg/container/mocks/container_ref.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
type imageRef struct {
|
||||
id t.ImageID
|
||||
file string
|
||||
}
|
||||
|
||||
func (ir *imageRef) getFileName() string {
|
||||
return fmt.Sprintf("./mocks/data/image_%v.json", ir.file)
|
||||
}
|
||||
|
||||
type ContainerRef struct {
|
||||
name string
|
||||
id t.ContainerID
|
||||
image *imageRef
|
||||
file string
|
||||
references []*ContainerRef
|
||||
isMissing bool
|
||||
}
|
||||
|
||||
func (cr *ContainerRef) getContainerFile() (containerFile string, err error) {
|
||||
file := cr.file
|
||||
if file == "" {
|
||||
file = cr.name
|
||||
}
|
||||
|
||||
containerFile = fmt.Sprintf("./mocks/data/container_%v.json", file)
|
||||
_, err = os.Stat(containerFile)
|
||||
|
||||
return containerFile, err
|
||||
}
|
||||
|
||||
func (cr *ContainerRef) ContainerID() t.ContainerID {
|
||||
return cr.id
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
{
|
||||
"Id": "1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6",
|
||||
"Created": "2023-07-25T14:55:14.69155887Z",
|
||||
"Path": "/docker-entrypoint.sh",
|
||||
"Args": [
|
||||
"nginx",
|
||||
"-g",
|
||||
"daemon off;"
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 3743,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2023-07-25T14:55:15.299654437Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"Image": "sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6-json.log",
|
||||
"Name": "/wt-contnet-consumer-1",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": null,
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "container:badc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "host",
|
||||
"Dns": null,
|
||||
"DnsOptions": null,
|
||||
"DnsSearch": null,
|
||||
"ExtraHosts": [],
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 0,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": null,
|
||||
"BlkioDeviceReadBps": null,
|
||||
"BlkioDeviceWriteBps": null,
|
||||
"BlkioDeviceReadIOps": null,
|
||||
"BlkioDeviceWriteIOps": null,
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": null,
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": false,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": null,
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"LowerDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2-init/diff:/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/diff:/var/lib/docker/overlay2/09785ba17f27c783ef8b44f369f9aac0ca936000b57abf22b3c54d1e6eb8e27b/diff:/var/lib/docker/overlay2/6f8acd64ae44fd4d14bcb90c105eceba46854aa3985b5b6b317bcc5692cfc286/diff:/var/lib/docker/overlay2/73d41c15edb21c5f12cf53e313f48b5da55283aafc77d35b7bc662241879d7e7/diff:/var/lib/docker/overlay2/d97b55f3d966ae031492369a98e9e00d2bd31e520290fe2034e0a2b1ed77c91e/diff:/var/lib/docker/overlay2/053e9ca65c6b64cb9d98a812ff7488c7e77938b4fb8e0c4d2ad7f8ec235f0f20/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [],
|
||||
"Config": {
|
||||
"Hostname": "25e75393800b",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": true,
|
||||
"AttachStderr": true,
|
||||
"ExposedPorts": {
|
||||
"80/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"NGINX_VERSION=1.23.3",
|
||||
"NJS_VERSION=0.7.9",
|
||||
"PKG_RELEASE=1~bullseye"
|
||||
],
|
||||
"Cmd": [
|
||||
"nginx",
|
||||
"-g",
|
||||
"daemon off;"
|
||||
],
|
||||
"Image": "nginx",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/docker-entrypoint.sh"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"com.docker.compose.config-hash": "8bb0e1c8c61f6d495840ba9133ebfb1e4ffda3e1adb701a011b03951848bb9fa",
|
||||
"com.docker.compose.container-number": "1",
|
||||
"com.docker.compose.depends_on": "producer:service_started:false",
|
||||
"com.docker.compose.image": "sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8",
|
||||
"com.docker.compose.oneoff": "False",
|
||||
"com.docker.compose.project": "wt-contnet",
|
||||
"com.docker.compose.project.config_files": "/tmp/wt-contnet/docker-compose.yaml",
|
||||
"com.docker.compose.project.working_dir": "/tmp/wt-contnet",
|
||||
"com.docker.compose.replace": "07bb70608f96f577aa02b9f317500e23e691c94eb099f6fb52301dfb031d0668",
|
||||
"com.docker.compose.service": "consumer",
|
||||
"com.docker.compose.version": "2.19.1",
|
||||
"desktop.docker.io/wsl-distro": "Ubuntu",
|
||||
"maintainer": "NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e"
|
||||
},
|
||||
"StopSignal": "SIGQUIT"
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "",
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"Ports": {},
|
||||
"SandboxKey": "",
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "",
|
||||
"Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "",
|
||||
"IPPrefixLen": 0,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "",
|
||||
"Networks": {}
|
||||
}
|
||||
}
|
205
pkg/container/mocks/data/container_net_consumer.json
Normal file
205
pkg/container/mocks/data/container_net_consumer.json
Normal file
|
@ -0,0 +1,205 @@
|
|||
{
|
||||
"Id": "1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6",
|
||||
"Created": "2023-07-25T14:55:14.69155887Z",
|
||||
"Path": "/docker-entrypoint.sh",
|
||||
"Args": [
|
||||
"nginx",
|
||||
"-g",
|
||||
"daemon off;"
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 3743,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2023-07-25T14:55:15.299654437Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"Image": "sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6-json.log",
|
||||
"Name": "/wt-contnet-consumer-1",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": null,
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "container:25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"CapAdd": null,
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "host",
|
||||
"Dns": null,
|
||||
"DnsOptions": null,
|
||||
"DnsSearch": null,
|
||||
"ExtraHosts": [],
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 0,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": null,
|
||||
"BlkioDeviceReadBps": null,
|
||||
"BlkioDeviceWriteBps": null,
|
||||
"BlkioDeviceReadIOps": null,
|
||||
"BlkioDeviceWriteIOps": null,
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": null,
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": false,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": null,
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"LowerDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2-init/diff:/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/diff:/var/lib/docker/overlay2/09785ba17f27c783ef8b44f369f9aac0ca936000b57abf22b3c54d1e6eb8e27b/diff:/var/lib/docker/overlay2/6f8acd64ae44fd4d14bcb90c105eceba46854aa3985b5b6b317bcc5692cfc286/diff:/var/lib/docker/overlay2/73d41c15edb21c5f12cf53e313f48b5da55283aafc77d35b7bc662241879d7e7/diff:/var/lib/docker/overlay2/d97b55f3d966ae031492369a98e9e00d2bd31e520290fe2034e0a2b1ed77c91e/diff:/var/lib/docker/overlay2/053e9ca65c6b64cb9d98a812ff7488c7e77938b4fb8e0c4d2ad7f8ec235f0f20/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [],
|
||||
"Config": {
|
||||
"Hostname": "25e75393800b",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": true,
|
||||
"AttachStderr": true,
|
||||
"ExposedPorts": {
|
||||
"80/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"NGINX_VERSION=1.23.3",
|
||||
"NJS_VERSION=0.7.9",
|
||||
"PKG_RELEASE=1~bullseye"
|
||||
],
|
||||
"Cmd": [
|
||||
"nginx",
|
||||
"-g",
|
||||
"daemon off;"
|
||||
],
|
||||
"Image": "nginx",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/docker-entrypoint.sh"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"com.docker.compose.config-hash": "8bb0e1c8c61f6d495840ba9133ebfb1e4ffda3e1adb701a011b03951848bb9fa",
|
||||
"com.docker.compose.container-number": "1",
|
||||
"com.docker.compose.depends_on": "producer:service_started:false",
|
||||
"com.docker.compose.image": "sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8",
|
||||
"com.docker.compose.oneoff": "False",
|
||||
"com.docker.compose.project": "wt-contnet",
|
||||
"com.docker.compose.project.config_files": "/tmp/wt-contnet/docker-compose.yaml",
|
||||
"com.docker.compose.project.working_dir": "/tmp/wt-contnet",
|
||||
"com.docker.compose.replace": "07bb70608f96f577aa02b9f317500e23e691c94eb099f6fb52301dfb031d0668",
|
||||
"com.docker.compose.service": "consumer",
|
||||
"com.docker.compose.version": "2.19.1",
|
||||
"desktop.docker.io/wsl-distro": "Ubuntu",
|
||||
"maintainer": "NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e"
|
||||
},
|
||||
"StopSignal": "SIGQUIT"
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "",
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"Ports": {},
|
||||
"SandboxKey": "",
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "",
|
||||
"Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "",
|
||||
"IPPrefixLen": 0,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "",
|
||||
"Networks": {}
|
||||
}
|
||||
}
|
380
pkg/container/mocks/data/container_net_supplier.json
Normal file
380
pkg/container/mocks/data/container_net_supplier.json
Normal file
|
@ -0,0 +1,380 @@
|
|||
{
|
||||
"Id": "25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2",
|
||||
"Created": "2023-07-25T14:55:14.595662628Z",
|
||||
"Path": "/gluetun-entrypoint",
|
||||
"Args": [],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Paused": false,
|
||||
"Restarting": false,
|
||||
"OOMKilled": false,
|
||||
"Dead": false,
|
||||
"Pid": 3648,
|
||||
"ExitCode": 0,
|
||||
"Error": "",
|
||||
"StartedAt": "2023-07-25T14:55:15.193430103Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z",
|
||||
"Health": {
|
||||
"Status": "healthy",
|
||||
"FailingStreak": 0,
|
||||
"Log": [
|
||||
{
|
||||
"Start": "2023-07-25T15:00:32.078491228Z",
|
||||
"End": "2023-07-25T15:00:32.194554876Z",
|
||||
"ExitCode": 0,
|
||||
"Output": ""
|
||||
},
|
||||
{
|
||||
"Start": "2023-07-25T15:00:37.199245496Z",
|
||||
"End": "2023-07-25T15:00:37.294845687Z",
|
||||
"ExitCode": 0,
|
||||
"Output": ""
|
||||
},
|
||||
{
|
||||
"Start": "2023-07-25T15:00:42.299676089Z",
|
||||
"End": "2023-07-25T15:00:42.384213818Z",
|
||||
"ExitCode": 0,
|
||||
"Output": ""
|
||||
},
|
||||
{
|
||||
"Start": "2023-07-25T15:00:47.389142447Z",
|
||||
"End": "2023-07-25T15:00:47.514483294Z",
|
||||
"ExitCode": 0,
|
||||
"Output": ""
|
||||
},
|
||||
{
|
||||
"Start": "2023-07-25T15:00:52.518770886Z",
|
||||
"End": "2023-07-25T15:00:52.644288742Z",
|
||||
"ExitCode": 0,
|
||||
"Output": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Image": "sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51",
|
||||
"ResolvConfPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/resolv.conf",
|
||||
"HostnamePath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hostname",
|
||||
"HostsPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hosts",
|
||||
"LogPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2-json.log",
|
||||
"Name": "/wt-contnet-producer-1",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"MountLabel": "",
|
||||
"ProcessLabel": "",
|
||||
"AppArmorProfile": "",
|
||||
"ExecIDs": null,
|
||||
"HostConfig": {
|
||||
"Binds": null,
|
||||
"ContainerIDFile": "",
|
||||
"LogConfig": {
|
||||
"Type": "json-file",
|
||||
"Config": {}
|
||||
},
|
||||
"NetworkMode": "wt-contnet_default",
|
||||
"PortBindings": {},
|
||||
"RestartPolicy": {
|
||||
"Name": "",
|
||||
"MaximumRetryCount": 0
|
||||
},
|
||||
"AutoRemove": false,
|
||||
"VolumeDriver": "",
|
||||
"VolumesFrom": null,
|
||||
"ConsoleSize": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"CapAdd": [
|
||||
"NET_ADMIN"
|
||||
],
|
||||
"CapDrop": null,
|
||||
"CgroupnsMode": "host",
|
||||
"Dns": null,
|
||||
"DnsOptions": null,
|
||||
"DnsSearch": null,
|
||||
"ExtraHosts": [],
|
||||
"GroupAdd": null,
|
||||
"IpcMode": "private",
|
||||
"Cgroup": "",
|
||||
"Links": null,
|
||||
"OomScoreAdj": 0,
|
||||
"PidMode": "",
|
||||
"Privileged": false,
|
||||
"PublishAllPorts": false,
|
||||
"ReadonlyRootfs": false,
|
||||
"SecurityOpt": null,
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"ShmSize": 67108864,
|
||||
"Runtime": "runc",
|
||||
"Isolation": "",
|
||||
"CpuShares": 0,
|
||||
"Memory": 0,
|
||||
"NanoCpus": 0,
|
||||
"CgroupParent": "",
|
||||
"BlkioWeight": 0,
|
||||
"BlkioWeightDevice": null,
|
||||
"BlkioDeviceReadBps": null,
|
||||
"BlkioDeviceWriteBps": null,
|
||||
"BlkioDeviceReadIOps": null,
|
||||
"BlkioDeviceWriteIOps": null,
|
||||
"CpuPeriod": 0,
|
||||
"CpuQuota": 0,
|
||||
"CpuRealtimePeriod": 0,
|
||||
"CpuRealtimeRuntime": 0,
|
||||
"CpusetCpus": "",
|
||||
"CpusetMems": "",
|
||||
"Devices": null,
|
||||
"DeviceCgroupRules": null,
|
||||
"DeviceRequests": null,
|
||||
"MemoryReservation": 0,
|
||||
"MemorySwap": 0,
|
||||
"MemorySwappiness": null,
|
||||
"OomKillDisable": false,
|
||||
"PidsLimit": null,
|
||||
"Ulimits": null,
|
||||
"CpuCount": 0,
|
||||
"CpuPercent": 0,
|
||||
"IOMaximumIOps": 0,
|
||||
"IOMaximumBandwidth": 0,
|
||||
"MaskedPaths": [
|
||||
"/proc/asound",
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/proc/scsi",
|
||||
"/sys/firmware"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
},
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"LowerDir": "/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2-init/diff:/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/diff:/var/lib/docker/overlay2/a20c9490a23ee8af51898892d9bf32258d44e0e07f3799475be8e8f273a50f73/diff:/var/lib/docker/overlay2/d4c97f367c37c6ada9de57f438a3e19cc714be2a54a6f582a03de9e42d88b344/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"Mounts": [],
|
||||
"Config": {
|
||||
"Hostname": "25e75393800b",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": true,
|
||||
"AttachStderr": true,
|
||||
"ExposedPorts": {
|
||||
"8000/tcp": {},
|
||||
"8388/tcp": {},
|
||||
"8388/udp": {},
|
||||
"8888/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"OPENVPN_PASSWORD=",
|
||||
"SERVER_COUNTRIES=Sweden",
|
||||
"VPN_SERVICE_PROVIDER=nordvpn",
|
||||
"OPENVPN_USER=",
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"VPN_TYPE=openvpn",
|
||||
"VPN_ENDPOINT_IP=",
|
||||
"VPN_ENDPOINT_PORT=",
|
||||
"VPN_INTERFACE=tun0",
|
||||
"OPENVPN_PROTOCOL=udp",
|
||||
"OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user",
|
||||
"OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password",
|
||||
"OPENVPN_VERSION=2.5",
|
||||
"OPENVPN_VERBOSITY=1",
|
||||
"OPENVPN_FLAGS=",
|
||||
"OPENVPN_CIPHERS=",
|
||||
"OPENVPN_AUTH=",
|
||||
"OPENVPN_PROCESS_USER=root",
|
||||
"OPENVPN_CUSTOM_CONFIG=",
|
||||
"WIREGUARD_PRIVATE_KEY=",
|
||||
"WIREGUARD_PRESHARED_KEY=",
|
||||
"WIREGUARD_PUBLIC_KEY=",
|
||||
"WIREGUARD_ALLOWED_IPS=",
|
||||
"WIREGUARD_ADDRESSES=",
|
||||
"WIREGUARD_MTU=1400",
|
||||
"WIREGUARD_IMPLEMENTATION=auto",
|
||||
"SERVER_REGIONS=",
|
||||
"SERVER_CITIES=",
|
||||
"SERVER_HOSTNAMES=",
|
||||
"ISP=",
|
||||
"OWNED_ONLY=no",
|
||||
"PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET=",
|
||||
"VPN_PORT_FORWARDING=off",
|
||||
"VPN_PORT_FORWARDING_PROVIDER=",
|
||||
"VPN_PORT_FORWARDING_STATUS_FILE=/tmp/gluetun/forwarded_port",
|
||||
"OPENVPN_CERT=",
|
||||
"OPENVPN_KEY=",
|
||||
"OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt",
|
||||
"OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey",
|
||||
"OPENVPN_ENCRYPTED_KEY=",
|
||||
"OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key",
|
||||
"OPENVPN_KEY_PASSPHRASE=",
|
||||
"OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase",
|
||||
"SERVER_NUMBER=",
|
||||
"SERVER_NAMES=",
|
||||
"FREE_ONLY=",
|
||||
"MULTIHOP_ONLY=",
|
||||
"PREMIUM_ONLY=",
|
||||
"FIREWALL=on",
|
||||
"FIREWALL_VPN_INPUT_PORTS=",
|
||||
"FIREWALL_INPUT_PORTS=",
|
||||
"FIREWALL_OUTBOUND_SUBNETS=",
|
||||
"FIREWALL_DEBUG=off",
|
||||
"LOG_LEVEL=info",
|
||||
"HEALTH_SERVER_ADDRESS=127.0.0.1:9999",
|
||||
"HEALTH_TARGET_ADDRESS=cloudflare.com:443",
|
||||
"HEALTH_SUCCESS_WAIT_DURATION=5s",
|
||||
"HEALTH_VPN_DURATION_INITIAL=6s",
|
||||
"HEALTH_VPN_DURATION_ADDITION=5s",
|
||||
"DOT=on",
|
||||
"DOT_PROVIDERS=cloudflare",
|
||||
"DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112",
|
||||
"DOT_VERBOSITY=1",
|
||||
"DOT_VERBOSITY_DETAILS=0",
|
||||
"DOT_VALIDATION_LOGLEVEL=0",
|
||||
"DOT_CACHING=on",
|
||||
"DOT_IPV6=off",
|
||||
"BLOCK_MALICIOUS=on",
|
||||
"BLOCK_SURVEILLANCE=off",
|
||||
"BLOCK_ADS=off",
|
||||
"UNBLOCK=",
|
||||
"DNS_UPDATE_PERIOD=24h",
|
||||
"DNS_ADDRESS=127.0.0.1",
|
||||
"DNS_KEEP_NAMESERVER=off",
|
||||
"HTTPPROXY=",
|
||||
"HTTPPROXY_LOG=off",
|
||||
"HTTPPROXY_LISTENING_ADDRESS=:8888",
|
||||
"HTTPPROXY_STEALTH=off",
|
||||
"HTTPPROXY_USER=",
|
||||
"HTTPPROXY_PASSWORD=",
|
||||
"HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user",
|
||||
"HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password",
|
||||
"SHADOWSOCKS=off",
|
||||
"SHADOWSOCKS_LOG=off",
|
||||
"SHADOWSOCKS_LISTENING_ADDRESS=:8388",
|
||||
"SHADOWSOCKS_PASSWORD=",
|
||||
"SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password",
|
||||
"SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305",
|
||||
"HTTP_CONTROL_SERVER_LOG=on",
|
||||
"HTTP_CONTROL_SERVER_ADDRESS=:8000",
|
||||
"UPDATER_PERIOD=0",
|
||||
"UPDATER_MIN_RATIO=0.8",
|
||||
"UPDATER_VPN_SERVICE_PROVIDERS=",
|
||||
"PUBLICIP_FILE=/tmp/gluetun/ip",
|
||||
"PUBLICIP_PERIOD=12h",
|
||||
"PPROF_ENABLED=no",
|
||||
"PPROF_BLOCK_PROFILE_RATE=0",
|
||||
"PPROF_MUTEX_PROFILE_RATE=0",
|
||||
"PPROF_HTTP_SERVER_ADDRESS=:6060",
|
||||
"VERSION_INFORMATION=on",
|
||||
"TZ=",
|
||||
"PUID=",
|
||||
"PGID="
|
||||
],
|
||||
"Cmd": null,
|
||||
"Healthcheck": {
|
||||
"Test": [
|
||||
"CMD-SHELL",
|
||||
"/gluetun-entrypoint healthcheck"
|
||||
],
|
||||
"Interval": 5000000000,
|
||||
"Timeout": 5000000000,
|
||||
"StartPeriod": 10000000000,
|
||||
"Retries": 1
|
||||
},
|
||||
"Image": "qmcgaw/gluetun",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/gluetun-entrypoint"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"com.docker.compose.config-hash": "6dc7dc42a86edb47039de3650a9cb9bdcf4866c113b8f9d797722c9dfd20428b",
|
||||
"com.docker.compose.container-number": "1",
|
||||
"com.docker.compose.depends_on": "",
|
||||
"com.docker.compose.image": "sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51",
|
||||
"com.docker.compose.oneoff": "False",
|
||||
"com.docker.compose.project": "wt-contnet",
|
||||
"com.docker.compose.project.config_files": "/tmp/wt-contnet/docker-compose.yaml",
|
||||
"com.docker.compose.project.working_dir": "/tmp/wt-contnet",
|
||||
"com.docker.compose.replace": "9bd1ce000be81819fc915aa60a1674c7573b59a26ac4643ecf427a5732b9785f",
|
||||
"com.docker.compose.service": "producer",
|
||||
"com.docker.compose.version": "2.19.1",
|
||||
"desktop.docker.io/wsl-distro": "Ubuntu",
|
||||
"org.opencontainers.image.authors": "quentin.mcgaw@gmail.com",
|
||||
"org.opencontainers.image.created": "2023-07-22T16:07:05.641Z",
|
||||
"org.opencontainers.image.description": "VPN client in a thin Docker container for multiple VPN providers, written in Go, and using OpenVPN or Wireguard, DNS over TLS, with a few proxy servers built-in.",
|
||||
"org.opencontainers.image.documentation": "https://github.com/qdm12/gluetun",
|
||||
"org.opencontainers.image.licenses": "MIT",
|
||||
"org.opencontainers.image.revision": "eecfb3952f202c0de3867d88e96d80c6b0f48359",
|
||||
"org.opencontainers.image.source": "https://github.com/qdm12/gluetun",
|
||||
"org.opencontainers.image.title": "gluetun",
|
||||
"org.opencontainers.image.url": "https://github.com/qdm12/gluetun",
|
||||
"org.opencontainers.image.version": "latest"
|
||||
}
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "34a321b64bb1b15f994dfccff0e235f881504f240c2028876ff6683962eaa10e",
|
||||
"HairpinMode": false,
|
||||
"LinkLocalIPv6Address": "",
|
||||
"LinkLocalIPv6PrefixLen": 0,
|
||||
"Ports": {
|
||||
"8000/tcp": null,
|
||||
"8388/tcp": null,
|
||||
"8388/udp": null,
|
||||
"8888/tcp": null
|
||||
},
|
||||
"SandboxKey": "/var/run/docker/netns/34a321b64bb1",
|
||||
"SecondaryIPAddresses": null,
|
||||
"SecondaryIPv6Addresses": null,
|
||||
"EndpointID": "",
|
||||
"Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"IPAddress": "",
|
||||
"IPPrefixLen": 0,
|
||||
"IPv6Gateway": "",
|
||||
"MacAddress": "",
|
||||
"Networks": {
|
||||
"wt-contnet_default": {
|
||||
"IPAMConfig": null,
|
||||
"Links": null,
|
||||
"Aliases": [
|
||||
"wt-contnet-producer-1",
|
||||
"producer",
|
||||
"25e75393800b"
|
||||
],
|
||||
"NetworkID": "f0f652a79efc54bcad52aafb4cbcc3b5dce1acaf11b172d8678d25f665faf63d",
|
||||
"EndpointID": "2429c2b5d08db6c986bbd419a52ca4dd352715d80c5aeae04742efb84b0356fc",
|
||||
"Gateway": "172.19.0.1",
|
||||
"IPAddress": "172.19.0.2",
|
||||
"IPPrefixLen": 16,
|
||||
"IPv6Gateway": "",
|
||||
"GlobalIPv6Address": "",
|
||||
"GlobalIPv6PrefixLen": 0,
|
||||
"MacAddress": "02:42:ac:13:00:02",
|
||||
"DriverOpts": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
115
pkg/container/mocks/data/image_net_consumer.json
Normal file
115
pkg/container/mocks/data/image_net_consumer.json
Normal file
|
@ -0,0 +1,115 @@
|
|||
{
|
||||
"Id": "sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8",
|
||||
"RepoTags": [
|
||||
"nginx:latest"
|
||||
],
|
||||
"RepoDigests": [
|
||||
"nginx@sha256:aa0afebbb3cfa473099a62c4b32e9b3fb73ed23f2a75a65ce1d4b4f55a5c2ef2"
|
||||
],
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"Created": "2023-03-01T18:43:12.914398123Z",
|
||||
"Container": "71a4c9a59d252d7c54812429bfe5df477e54e91ebfff1939ae39ecdf055d445c",
|
||||
"ContainerConfig": {
|
||||
"Hostname": "71a4c9a59d25",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"ExposedPorts": {
|
||||
"80/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"NGINX_VERSION=1.23.3",
|
||||
"NJS_VERSION=0.7.9",
|
||||
"PKG_RELEASE=1~bullseye"
|
||||
],
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"#(nop) ",
|
||||
"CMD [\"nginx\" \"-g\" \"daemon off;\"]"
|
||||
],
|
||||
"Image": "sha256:6716b8a33f73b21e193bb63424ea1105eaaa6a8237fefe75570bea18c87a1711",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/docker-entrypoint.sh"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
|
||||
},
|
||||
"StopSignal": "SIGQUIT"
|
||||
},
|
||||
"DockerVersion": "20.10.23",
|
||||
"Author": "",
|
||||
"Config": {
|
||||
"Hostname": "",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"ExposedPorts": {
|
||||
"80/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"NGINX_VERSION=1.23.3",
|
||||
"NJS_VERSION=0.7.9",
|
||||
"PKG_RELEASE=1~bullseye"
|
||||
],
|
||||
"Cmd": [
|
||||
"nginx",
|
||||
"-g",
|
||||
"daemon off;"
|
||||
],
|
||||
"Image": "sha256:6716b8a33f73b21e193bb63424ea1105eaaa6a8237fefe75570bea18c87a1711",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/docker-entrypoint.sh"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
|
||||
},
|
||||
"StopSignal": "SIGQUIT"
|
||||
},
|
||||
"Architecture": "amd64",
|
||||
"Os": "linux",
|
||||
"Size": 141838643,
|
||||
"VirtualSize": 141838643,
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"LowerDir": "/var/lib/docker/overlay2/09785ba17f27c783ef8b44f369f9aac0ca936000b57abf22b3c54d1e6eb8e27b/diff:/var/lib/docker/overlay2/6f8acd64ae44fd4d14bcb90c105eceba46854aa3985b5b6b317bcc5692cfc286/diff:/var/lib/docker/overlay2/73d41c15edb21c5f12cf53e313f48b5da55283aafc77d35b7bc662241879d7e7/diff:/var/lib/docker/overlay2/d97b55f3d966ae031492369a98e9e00d2bd31e520290fe2034e0a2b1ed77c91e/diff:/var/lib/docker/overlay2/053e9ca65c6b64cb9d98a812ff7488c7e77938b4fb8e0c4d2ad7f8ec235f0f20/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"RootFS": {
|
||||
"Type": "layers",
|
||||
"Layers": [
|
||||
"sha256:650abce4b096b06ac8bec2046d821d66d801af34f1f1d4c5e272ad030c7873db",
|
||||
"sha256:4dc5cd799a08ff49a603870c8378ea93083bfc2a4176f56e5531997e94c195d0",
|
||||
"sha256:e161c82b34d21179db1f546c1cd84153d28a17d865ccaf2dedeb06a903fec12c",
|
||||
"sha256:83ba6d8ffb8c2974174c02d3ba549e7e0656ebb1bc075a6b6ee89b6c609c6a71",
|
||||
"sha256:d8466e142d8710abf5b495ebb536478f7e19d9d03b151b5d5bd09df4cfb49248",
|
||||
"sha256:101af4ba983b04be266217ecee414e88b23e394f62e9801c7c1bdb37cb37bcaa"
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"LastTagTime": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
210
pkg/container/mocks/data/image_net_producer.json
Normal file
210
pkg/container/mocks/data/image_net_producer.json
Normal file
|
@ -0,0 +1,210 @@
|
|||
{
|
||||
"Id": "sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51",
|
||||
"RepoTags": [
|
||||
"qmcgaw/gluetun:latest"
|
||||
],
|
||||
"RepoDigests": [
|
||||
"qmcgaw/gluetun@sha256:cd532bf4ef88a348a915c6dc62a9867a2eca89aa70559b0b4a1ea15cc0e595d1"
|
||||
],
|
||||
"Parent": "",
|
||||
"Comment": "buildkit.dockerfile.v0",
|
||||
"Created": "2023-07-22T16:10:29.457146856Z",
|
||||
"Container": "",
|
||||
"ContainerConfig": {
|
||||
"Hostname": "",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": null,
|
||||
"Cmd": null,
|
||||
"Image": "",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": null,
|
||||
"Labels": null
|
||||
},
|
||||
"DockerVersion": "",
|
||||
"Author": "",
|
||||
"Config": {
|
||||
"Hostname": "",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"ExposedPorts": {
|
||||
"8000/tcp": {},
|
||||
"8388/tcp": {},
|
||||
"8388/udp": {},
|
||||
"8888/tcp": {}
|
||||
},
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"VPN_SERVICE_PROVIDER=pia",
|
||||
"VPN_TYPE=openvpn",
|
||||
"VPN_ENDPOINT_IP=",
|
||||
"VPN_ENDPOINT_PORT=",
|
||||
"VPN_INTERFACE=tun0",
|
||||
"OPENVPN_PROTOCOL=udp",
|
||||
"OPENVPN_USER=",
|
||||
"OPENVPN_PASSWORD=",
|
||||
"OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user",
|
||||
"OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password",
|
||||
"OPENVPN_VERSION=2.5",
|
||||
"OPENVPN_VERBOSITY=1",
|
||||
"OPENVPN_FLAGS=",
|
||||
"OPENVPN_CIPHERS=",
|
||||
"OPENVPN_AUTH=",
|
||||
"OPENVPN_PROCESS_USER=root",
|
||||
"OPENVPN_CUSTOM_CONFIG=",
|
||||
"WIREGUARD_PRIVATE_KEY=",
|
||||
"WIREGUARD_PRESHARED_KEY=",
|
||||
"WIREGUARD_PUBLIC_KEY=",
|
||||
"WIREGUARD_ALLOWED_IPS=",
|
||||
"WIREGUARD_ADDRESSES=",
|
||||
"WIREGUARD_MTU=1400",
|
||||
"WIREGUARD_IMPLEMENTATION=auto",
|
||||
"SERVER_REGIONS=",
|
||||
"SERVER_COUNTRIES=",
|
||||
"SERVER_CITIES=",
|
||||
"SERVER_HOSTNAMES=",
|
||||
"ISP=",
|
||||
"OWNED_ONLY=no",
|
||||
"PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET=",
|
||||
"VPN_PORT_FORWARDING=off",
|
||||
"VPN_PORT_FORWARDING_PROVIDER=",
|
||||
"VPN_PORT_FORWARDING_STATUS_FILE=/tmp/gluetun/forwarded_port",
|
||||
"OPENVPN_CERT=",
|
||||
"OPENVPN_KEY=",
|
||||
"OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt",
|
||||
"OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey",
|
||||
"OPENVPN_ENCRYPTED_KEY=",
|
||||
"OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key",
|
||||
"OPENVPN_KEY_PASSPHRASE=",
|
||||
"OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase",
|
||||
"SERVER_NUMBER=",
|
||||
"SERVER_NAMES=",
|
||||
"FREE_ONLY=",
|
||||
"MULTIHOP_ONLY=",
|
||||
"PREMIUM_ONLY=",
|
||||
"FIREWALL=on",
|
||||
"FIREWALL_VPN_INPUT_PORTS=",
|
||||
"FIREWALL_INPUT_PORTS=",
|
||||
"FIREWALL_OUTBOUND_SUBNETS=",
|
||||
"FIREWALL_DEBUG=off",
|
||||
"LOG_LEVEL=info",
|
||||
"HEALTH_SERVER_ADDRESS=127.0.0.1:9999",
|
||||
"HEALTH_TARGET_ADDRESS=cloudflare.com:443",
|
||||
"HEALTH_SUCCESS_WAIT_DURATION=5s",
|
||||
"HEALTH_VPN_DURATION_INITIAL=6s",
|
||||
"HEALTH_VPN_DURATION_ADDITION=5s",
|
||||
"DOT=on",
|
||||
"DOT_PROVIDERS=cloudflare",
|
||||
"DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112",
|
||||
"DOT_VERBOSITY=1",
|
||||
"DOT_VERBOSITY_DETAILS=0",
|
||||
"DOT_VALIDATION_LOGLEVEL=0",
|
||||
"DOT_CACHING=on",
|
||||
"DOT_IPV6=off",
|
||||
"BLOCK_MALICIOUS=on",
|
||||
"BLOCK_SURVEILLANCE=off",
|
||||
"BLOCK_ADS=off",
|
||||
"UNBLOCK=",
|
||||
"DNS_UPDATE_PERIOD=24h",
|
||||
"DNS_ADDRESS=127.0.0.1",
|
||||
"DNS_KEEP_NAMESERVER=off",
|
||||
"HTTPPROXY=",
|
||||
"HTTPPROXY_LOG=off",
|
||||
"HTTPPROXY_LISTENING_ADDRESS=:8888",
|
||||
"HTTPPROXY_STEALTH=off",
|
||||
"HTTPPROXY_USER=",
|
||||
"HTTPPROXY_PASSWORD=",
|
||||
"HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user",
|
||||
"HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password",
|
||||
"SHADOWSOCKS=off",
|
||||
"SHADOWSOCKS_LOG=off",
|
||||
"SHADOWSOCKS_LISTENING_ADDRESS=:8388",
|
||||
"SHADOWSOCKS_PASSWORD=",
|
||||
"SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password",
|
||||
"SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305",
|
||||
"HTTP_CONTROL_SERVER_LOG=on",
|
||||
"HTTP_CONTROL_SERVER_ADDRESS=:8000",
|
||||
"UPDATER_PERIOD=0",
|
||||
"UPDATER_MIN_RATIO=0.8",
|
||||
"UPDATER_VPN_SERVICE_PROVIDERS=",
|
||||
"PUBLICIP_FILE=/tmp/gluetun/ip",
|
||||
"PUBLICIP_PERIOD=12h",
|
||||
"PPROF_ENABLED=no",
|
||||
"PPROF_BLOCK_PROFILE_RATE=0",
|
||||
"PPROF_MUTEX_PROFILE_RATE=0",
|
||||
"PPROF_HTTP_SERVER_ADDRESS=:6060",
|
||||
"VERSION_INFORMATION=on",
|
||||
"TZ=",
|
||||
"PUID=",
|
||||
"PGID="
|
||||
],
|
||||
"Cmd": null,
|
||||
"Healthcheck": {
|
||||
"Test": [
|
||||
"CMD-SHELL",
|
||||
"/gluetun-entrypoint healthcheck"
|
||||
],
|
||||
"Interval": 5000000000,
|
||||
"Timeout": 5000000000,
|
||||
"StartPeriod": 10000000000,
|
||||
"Retries": 1
|
||||
},
|
||||
"Image": "",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": [
|
||||
"/gluetun-entrypoint"
|
||||
],
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
"org.opencontainers.image.authors": "quentin.mcgaw@gmail.com",
|
||||
"org.opencontainers.image.created": "2023-07-22T16:07:05.641Z",
|
||||
"org.opencontainers.image.description": "VPN client in a thin Docker container for multiple VPN providers, written in Go, and using OpenVPN or Wireguard, DNS over TLS, with a few proxy servers built-in.",
|
||||
"org.opencontainers.image.documentation": "https://github.com/qdm12/gluetun",
|
||||
"org.opencontainers.image.licenses": "MIT",
|
||||
"org.opencontainers.image.revision": "eecfb3952f202c0de3867d88e96d80c6b0f48359",
|
||||
"org.opencontainers.image.source": "https://github.com/qdm12/gluetun",
|
||||
"org.opencontainers.image.title": "gluetun",
|
||||
"org.opencontainers.image.url": "https://github.com/qdm12/gluetun",
|
||||
"org.opencontainers.image.version": "latest"
|
||||
}
|
||||
},
|
||||
"Architecture": "amd64",
|
||||
"Os": "linux",
|
||||
"Size": 42602255,
|
||||
"VirtualSize": 42602255,
|
||||
"GraphDriver": {
|
||||
"Data": {
|
||||
"LowerDir": "/var/lib/docker/overlay2/a20c9490a23ee8af51898892d9bf32258d44e0e07f3799475be8e8f273a50f73/diff:/var/lib/docker/overlay2/d4c97f367c37c6ada9de57f438a3e19cc714be2a54a6f582a03de9e42d88b344/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/work"
|
||||
},
|
||||
"Name": "overlay2"
|
||||
},
|
||||
"RootFS": {
|
||||
"Type": "layers",
|
||||
"Layers": [
|
||||
"sha256:78a822fe2a2d2c84f3de4a403188c45f623017d6a4521d23047c9fbb0801794c",
|
||||
"sha256:122dbeefc08382d88b3fe57ad81c1e2428af5b81c172d112723a33e2a20fe880",
|
||||
"sha256:3d215e55b88a99dcd7cf4349618326ab129771e12fdf6c6ef5cbb71a265dbb6c"
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"LastTagTime": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatch
|
|||
// NoFilter will not filter out any containers
|
||||
func NoFilter(t.FilterableContainer) bool { return true }
|
||||
|
||||
// FilterByNames returns all containers that match the specified name
|
||||
// FilterByNames returns all containers that match one of the specified names
|
||||
func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
|
||||
if len(names) == 0 {
|
||||
return baseFilter
|
||||
|
@ -41,6 +41,22 @@ func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
|
|||
}
|
||||
}
|
||||
|
||||
// FilterByDisableNames returns all containers that don't match any of the specified names
|
||||
func FilterByDisableNames(disableNames []string, baseFilter t.Filter) t.Filter {
|
||||
if len(disableNames) == 0 {
|
||||
return baseFilter
|
||||
}
|
||||
|
||||
return func(c t.FilterableContainer) bool {
|
||||
for _, name := range disableNames {
|
||||
if name == c.Name() || name == c.Name()[1:] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return baseFilter(c)
|
||||
}
|
||||
}
|
||||
|
||||
// FilterByEnableLabel returns all containers that have the enabled label set
|
||||
func FilterByEnableLabel(baseFilter t.Filter) t.Filter {
|
||||
return func(c t.FilterableContainer) bool {
|
||||
|
@ -70,13 +86,14 @@ func FilterByDisabledLabel(baseFilter t.Filter) t.Filter {
|
|||
|
||||
// FilterByScope returns all containers that belongs to a specific scope
|
||||
func FilterByScope(scope string, baseFilter t.Filter) t.Filter {
|
||||
if scope == "" {
|
||||
return baseFilter
|
||||
return func(c t.FilterableContainer) bool {
|
||||
containerScope, containerHasScope := c.Scope()
|
||||
|
||||
if !containerHasScope || containerScope == "" {
|
||||
containerScope = "none"
|
||||
}
|
||||
|
||||
return func(c t.FilterableContainer) bool {
|
||||
containerScope, ok := c.Scope()
|
||||
if ok && containerScope == scope {
|
||||
if containerScope == scope {
|
||||
return baseFilter(c)
|
||||
}
|
||||
|
||||
|
@ -103,10 +120,11 @@ func FilterByImage(images []string, baseFilter t.Filter) t.Filter {
|
|||
}
|
||||
|
||||
// BuildFilter creates the needed filter of containers
|
||||
func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, string) {
|
||||
func BuildFilter(names []string, disableNames []string, enableLabel bool, scope string) (t.Filter, string) {
|
||||
sb := strings.Builder{}
|
||||
filter := NoFilter
|
||||
filter = FilterByNames(names, filter)
|
||||
filter = FilterByDisableNames(disableNames, filter)
|
||||
|
||||
if len(names) > 0 {
|
||||
sb.WriteString("which name matches \"")
|
||||
|
@ -118,6 +136,16 @@ func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, stri
|
|||
}
|
||||
sb.WriteString(`", `)
|
||||
}
|
||||
if len(disableNames) > 0 {
|
||||
sb.WriteString("not named one of \"")
|
||||
for i, n := range disableNames {
|
||||
sb.WriteString(n)
|
||||
if i < len(disableNames)-1 {
|
||||
sb.WriteString(`" or "`)
|
||||
}
|
||||
}
|
||||
sb.WriteString(`", `)
|
||||
}
|
||||
|
||||
if enableLabel {
|
||||
// If label filtering is enabled, containers should only be considered
|
||||
|
@ -125,7 +153,13 @@ func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, stri
|
|||
filter = FilterByEnableLabel(filter)
|
||||
sb.WriteString("using enable label, ")
|
||||
}
|
||||
if scope != "" {
|
||||
|
||||
if scope == "none" {
|
||||
// If a scope has explicitly defined as "none", containers should only be considered
|
||||
// if they do not have a scope defined, or if it's explicitly set to "none".
|
||||
filter = FilterByScope(scope, filter)
|
||||
sb.WriteString(`without a scope, "`)
|
||||
} else if scope != "" {
|
||||
// If a scope has been defined, containers should only be considered
|
||||
// if the scope is specifically set.
|
||||
filter = FilterByScope(scope, filter)
|
||||
|
|
|
@ -111,6 +111,53 @@ func TestFilterByScope(t *testing.T) {
|
|||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestFilterByNoneScope(t *testing.T) {
|
||||
scope := "none"
|
||||
|
||||
filter := FilterByScope(scope, NoFilter)
|
||||
assert.NotNil(t, filter)
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("Scope").Return("anyscope", true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Scope").Return("", false)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Scope").Return("", true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Scope").Return("none", true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestBuildFilterNoneScope(t *testing.T) {
|
||||
filter, desc := BuildFilter(nil, nil, false, "none")
|
||||
|
||||
assert.Contains(t, desc, "without a scope")
|
||||
|
||||
scoped := new(mocks.FilterableContainer)
|
||||
scoped.On("Enabled").Return(false, false)
|
||||
scoped.On("Scope").Return("anyscope", true)
|
||||
|
||||
unscoped := new(mocks.FilterableContainer)
|
||||
unscoped.On("Enabled").Return(false, false)
|
||||
unscoped.On("Scope").Return("", false)
|
||||
|
||||
assert.False(t, filter(scoped))
|
||||
assert.True(t, filter(unscoped))
|
||||
|
||||
scoped.AssertExpectations(t)
|
||||
unscoped.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestFilterByDisabledLabel(t *testing.T) {
|
||||
filter := FilterByDisabledLabel(NoFilter)
|
||||
assert.NotNil(t, filter)
|
||||
|
@ -171,7 +218,7 @@ func TestFilterByImage(t *testing.T) {
|
|||
func TestBuildFilter(t *testing.T) {
|
||||
names := []string{"test", "valid"}
|
||||
|
||||
filter, desc := BuildFilter(names, false, "")
|
||||
filter, desc := BuildFilter(names, []string{}, false, "")
|
||||
assert.Contains(t, desc, "test")
|
||||
assert.Contains(t, desc, "or")
|
||||
assert.Contains(t, desc, "valid")
|
||||
|
@ -210,7 +257,7 @@ func TestBuildFilterEnableLabel(t *testing.T) {
|
|||
var names []string
|
||||
names = append(names, "test")
|
||||
|
||||
filter, desc := BuildFilter(names, true, "")
|
||||
filter, desc := BuildFilter(names, []string{}, true, "")
|
||||
assert.Contains(t, desc, "using enable label")
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
|
@ -235,3 +282,52 @@ func TestBuildFilterEnableLabel(t *testing.T) {
|
|||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestBuildFilterDisableContainer(t *testing.T) {
|
||||
filter, desc := BuildFilter([]string{}, []string{"excluded", "notfound"}, false, "")
|
||||
assert.Contains(t, desc, "not named")
|
||||
assert.Contains(t, desc, "excluded")
|
||||
assert.Contains(t, desc, "or")
|
||||
assert.Contains(t, desc, "notfound")
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("Another")
|
||||
container.On("Enabled").Return(false, false)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("AnotherOne")
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("test")
|
||||
container.On("Enabled").Return(false, false)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("excluded")
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("excludedAsSubstring")
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("notfound")
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Enabled").Return(false, true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ func ExecutePostChecks(client container.Client, params types.UpdateParams) {
|
|||
}
|
||||
|
||||
// ExecutePreCheckCommand tries to run the pre-check lifecycle hook for a single container.
|
||||
func ExecutePreCheckCommand(client container.Client, container container.Container) {
|
||||
func ExecutePreCheckCommand(client container.Client, container types.Container) {
|
||||
clog := log.WithField("container", container.Name())
|
||||
command := container.GetLifecyclePreCheckCommand()
|
||||
if len(command) == 0 {
|
||||
|
@ -45,7 +45,7 @@ func ExecutePreCheckCommand(client container.Client, container container.Contain
|
|||
}
|
||||
|
||||
// ExecutePostCheckCommand tries to run the post-check lifecycle hook for a single container.
|
||||
func ExecutePostCheckCommand(client container.Client, container container.Container) {
|
||||
func ExecutePostCheckCommand(client container.Client, container types.Container) {
|
||||
clog := log.WithField("container", container.Name())
|
||||
command := container.GetLifecyclePostCheckCommand()
|
||||
if len(command) == 0 {
|
||||
|
@ -61,7 +61,7 @@ func ExecutePostCheckCommand(client container.Client, container container.Contai
|
|||
}
|
||||
|
||||
// ExecutePreUpdateCommand tries to run the pre-update lifecycle hook for a single container.
|
||||
func ExecutePreUpdateCommand(client container.Client, container container.Container) (SkipUpdate bool, err error) {
|
||||
func ExecutePreUpdateCommand(client container.Client, container types.Container) (SkipUpdate bool, err error) {
|
||||
timeout := container.PreUpdateTimeout()
|
||||
command := container.GetLifecyclePreUpdateCommand()
|
||||
clog := log.WithField("container", container.Name())
|
||||
|
|
|
@ -59,13 +59,3 @@ func marshalReports(reports []t.ContainerReport) []jsonMap {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
143
pkg/notifications/preview/data/data.go
Normal file
143
pkg/notifications/preview/data/data.go
Normal file
|
@ -0,0 +1,143 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
type previewData struct {
|
||||
rand *rand.Rand
|
||||
lastTime time.Time
|
||||
report *report
|
||||
containerCount int
|
||||
Entries []*logEntry
|
||||
StaticData staticData
|
||||
}
|
||||
|
||||
type staticData struct {
|
||||
Title string
|
||||
Host string
|
||||
}
|
||||
|
||||
// New initializes a new preview data struct
|
||||
func New() *previewData {
|
||||
return &previewData{
|
||||
rand: rand.New(rand.NewSource(1)),
|
||||
lastTime: time.Now().Add(-30 * time.Minute),
|
||||
report: nil,
|
||||
containerCount: 0,
|
||||
Entries: []*logEntry{},
|
||||
StaticData: staticData{
|
||||
Title: "Title",
|
||||
Host: "Host",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddFromState adds a container status entry to the report with the given state
|
||||
func (pb *previewData) AddFromState(state State) {
|
||||
cid := types.ContainerID(pb.generateID())
|
||||
old := types.ImageID(pb.generateID())
|
||||
new := types.ImageID(pb.generateID())
|
||||
name := pb.generateName()
|
||||
image := pb.generateImageName(name)
|
||||
var err error
|
||||
if state == FailedState {
|
||||
err = errors.New(pb.randomEntry(errorMessages))
|
||||
} else if state == SkippedState {
|
||||
err = errors.New(pb.randomEntry(skippedMessages))
|
||||
}
|
||||
pb.addContainer(containerStatus{
|
||||
containerID: cid,
|
||||
oldImage: old,
|
||||
newImage: new,
|
||||
containerName: name,
|
||||
imageName: image,
|
||||
error: err,
|
||||
state: state,
|
||||
})
|
||||
}
|
||||
|
||||
func (pb *previewData) addContainer(c containerStatus) {
|
||||
if pb.report == nil {
|
||||
pb.report = &report{}
|
||||
}
|
||||
switch c.state {
|
||||
case ScannedState:
|
||||
pb.report.scanned = append(pb.report.scanned, &c)
|
||||
case UpdatedState:
|
||||
pb.report.updated = append(pb.report.updated, &c)
|
||||
case FailedState:
|
||||
pb.report.failed = append(pb.report.failed, &c)
|
||||
case SkippedState:
|
||||
pb.report.skipped = append(pb.report.skipped, &c)
|
||||
case StaleState:
|
||||
pb.report.stale = append(pb.report.stale, &c)
|
||||
case FreshState:
|
||||
pb.report.fresh = append(pb.report.fresh, &c)
|
||||
default:
|
||||
return
|
||||
}
|
||||
pb.containerCount += 1
|
||||
}
|
||||
|
||||
// AddLogEntry adds a preview log entry of the given level
|
||||
func (pd *previewData) AddLogEntry(level LogLevel) {
|
||||
var msg string
|
||||
switch level {
|
||||
case FatalLevel:
|
||||
fallthrough
|
||||
case ErrorLevel:
|
||||
fallthrough
|
||||
case WarnLevel:
|
||||
msg = pd.randomEntry(logErrors)
|
||||
default:
|
||||
msg = pd.randomEntry(logMessages)
|
||||
}
|
||||
pd.Entries = append(pd.Entries, &logEntry{
|
||||
Message: msg,
|
||||
Data: map[string]any{},
|
||||
Time: pd.generateTime(),
|
||||
Level: level,
|
||||
})
|
||||
}
|
||||
|
||||
// Report returns a preview report
|
||||
func (pb *previewData) Report() types.Report {
|
||||
return pb.report
|
||||
}
|
||||
|
||||
func (pb *previewData) generateID() string {
|
||||
buf := make([]byte, 32)
|
||||
_, _ = pb.rand.Read(buf)
|
||||
return hex.EncodeToString(buf)
|
||||
}
|
||||
|
||||
func (pb *previewData) generateTime() time.Time {
|
||||
pb.lastTime = pb.lastTime.Add(time.Duration(pb.rand.Intn(30)) * time.Second)
|
||||
return pb.lastTime
|
||||
}
|
||||
|
||||
func (pb *previewData) randomEntry(arr []string) string {
|
||||
return arr[pb.rand.Intn(len(arr))]
|
||||
}
|
||||
|
||||
func (pb *previewData) generateName() string {
|
||||
index := pb.containerCount
|
||||
if index <= len(containerNames) {
|
||||
return "/" + containerNames[index]
|
||||
}
|
||||
suffix := index / len(containerNames)
|
||||
index %= len(containerNames)
|
||||
return "/" + containerNames[index] + strconv.FormatInt(int64(suffix), 10)
|
||||
}
|
||||
|
||||
func (pb *previewData) generateImageName(name string) string {
|
||||
index := pb.containerCount % len(organizationNames)
|
||||
return organizationNames[index] + name + ":latest"
|
||||
}
|
56
pkg/notifications/preview/data/logs.go
Normal file
56
pkg/notifications/preview/data/logs.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type logEntry struct {
|
||||
Message string
|
||||
Data map[string]any
|
||||
Time time.Time
|
||||
Level LogLevel
|
||||
}
|
||||
|
||||
// LogLevel is the analog of logrus.Level
|
||||
type LogLevel string
|
||||
|
||||
const (
|
||||
TraceLevel LogLevel = "trace"
|
||||
DebugLevel LogLevel = "debug"
|
||||
InfoLevel LogLevel = "info"
|
||||
WarnLevel LogLevel = "warning"
|
||||
ErrorLevel LogLevel = "error"
|
||||
FatalLevel LogLevel = "fatal"
|
||||
PanicLevel LogLevel = "panic"
|
||||
)
|
||||
|
||||
// LevelsFromString parses a string of level characters and returns a slice of the corresponding log levels
|
||||
func LevelsFromString(str string) []LogLevel {
|
||||
levels := make([]LogLevel, 0, len(str))
|
||||
for _, c := range str {
|
||||
switch c {
|
||||
case 'p':
|
||||
levels = append(levels, PanicLevel)
|
||||
case 'f':
|
||||
levels = append(levels, FatalLevel)
|
||||
case 'e':
|
||||
levels = append(levels, ErrorLevel)
|
||||
case 'w':
|
||||
levels = append(levels, WarnLevel)
|
||||
case 'i':
|
||||
levels = append(levels, InfoLevel)
|
||||
case 'd':
|
||||
levels = append(levels, DebugLevel)
|
||||
case 't':
|
||||
levels = append(levels, TraceLevel)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
return levels
|
||||
}
|
||||
|
||||
// String returns the log level as a string
|
||||
func (level LogLevel) String() string {
|
||||
return string(level)
|
||||
}
|
178
pkg/notifications/preview/data/preview_strings.go
Normal file
178
pkg/notifications/preview/data/preview_strings.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
package data
|
||||
|
||||
var containerNames = []string{
|
||||
"cyberscribe",
|
||||
"datamatrix",
|
||||
"nexasync",
|
||||
"quantumquill",
|
||||
"aerosphere",
|
||||
"virtuos",
|
||||
"fusionflow",
|
||||
"neuralink",
|
||||
"pixelpulse",
|
||||
"synthwave",
|
||||
"codecraft",
|
||||
"zapzone",
|
||||
"robologic",
|
||||
"dreamstream",
|
||||
"infinisync",
|
||||
"megamesh",
|
||||
"novalink",
|
||||
"xenogenius",
|
||||
"ecosim",
|
||||
"innovault",
|
||||
"techtracer",
|
||||
"fusionforge",
|
||||
"quantumquest",
|
||||
"neuronest",
|
||||
"codefusion",
|
||||
"datadyno",
|
||||
"pixelpioneer",
|
||||
"vortexvision",
|
||||
"cybercraft",
|
||||
"synthsphere",
|
||||
"infinitescript",
|
||||
"roborhythm",
|
||||
"dreamengine",
|
||||
"aquasync",
|
||||
"geniusgrid",
|
||||
"megamind",
|
||||
"novasync-pro",
|
||||
"xenonwave",
|
||||
"ecologic",
|
||||
"innoscan",
|
||||
}
|
||||
|
||||
var organizationNames = []string{
|
||||
"techwave",
|
||||
"codecrafters",
|
||||
"innotechlabs",
|
||||
"fusionsoft",
|
||||
"cyberpulse",
|
||||
"quantumscribe",
|
||||
"datadynamo",
|
||||
"neuralink",
|
||||
"pixelpro",
|
||||
"synthwizards",
|
||||
"virtucorplabs",
|
||||
"robologic",
|
||||
"dreamstream",
|
||||
"novanest",
|
||||
"megamind",
|
||||
"xenonwave",
|
||||
"ecologic",
|
||||
"innosync",
|
||||
"techgenius",
|
||||
"nexasoft",
|
||||
"codewave",
|
||||
"zapzone",
|
||||
"techsphere",
|
||||
"aquatech",
|
||||
"quantumcraft",
|
||||
"neuronest",
|
||||
"datafusion",
|
||||
"pixelpioneer",
|
||||
"synthsphere",
|
||||
"infinitescribe",
|
||||
"roborhythm",
|
||||
"dreamengine",
|
||||
"vortexvision",
|
||||
"geniusgrid",
|
||||
"megamesh",
|
||||
"novasync",
|
||||
"xenogeniuslabs",
|
||||
"ecosim",
|
||||
"innovault",
|
||||
}
|
||||
|
||||
var errorMessages = []string{
|
||||
"Error 404: Resource not found",
|
||||
"Critical Error: System meltdown imminent",
|
||||
"Error 500: Internal server error",
|
||||
"Invalid input: Please check your data",
|
||||
"Access denied: Unauthorized access detected",
|
||||
"Network connection lost: Please check your connection",
|
||||
"Error 403: Forbidden access",
|
||||
"Fatal error: System crash imminent",
|
||||
"File not found: Check the file path",
|
||||
"Invalid credentials: Authentication failed",
|
||||
"Error 502: Bad Gateway",
|
||||
"Database connection failed: Please try again later",
|
||||
"Security breach detected: Take immediate action",
|
||||
"Error 400: Bad request",
|
||||
"Out of memory: Close unnecessary applications",
|
||||
"Invalid configuration: Check your settings",
|
||||
"Error 503: Service unavailable",
|
||||
"File is read-only: Cannot modify",
|
||||
"Data corruption detected: Backup your data",
|
||||
"Error 401: Unauthorized",
|
||||
"Disk space full: Free up disk space",
|
||||
"Connection timeout: Retry your request",
|
||||
"Error 504: Gateway timeout",
|
||||
"File access denied: Permission denied",
|
||||
"Unexpected error: Please contact support",
|
||||
"Error 429: Too many requests",
|
||||
"Invalid URL: Check the URL format",
|
||||
"Database query failed: Try again later",
|
||||
"Error 408: Request timeout",
|
||||
"File is in use: Close the file and try again",
|
||||
"Invalid parameter: Check your input",
|
||||
"Error 502: Proxy error",
|
||||
"Database connection lost: Reconnect and try again",
|
||||
"File size exceeds limit: Reduce the file size",
|
||||
"Error 503: Overloaded server",
|
||||
"Operation aborted: Try again",
|
||||
"Invalid API key: Check your API key",
|
||||
"Error 507: Insufficient storage",
|
||||
"Database deadlock: Retry your transaction",
|
||||
"Error 405: Method not allowed",
|
||||
"File format not supported: Choose a different format",
|
||||
"Unknown error: Contact system administrator",
|
||||
}
|
||||
|
||||
var skippedMessages = []string{
|
||||
"Fear of introducing new bugs",
|
||||
"Don't have time for the update process",
|
||||
"Current version works fine for my needs",
|
||||
"Concerns about compatibility with other software",
|
||||
"Limited bandwidth for downloading updates",
|
||||
"Worries about losing custom settings or configurations",
|
||||
"Lack of trust in the software developer's updates",
|
||||
"Dislike changes to the user interface",
|
||||
"Avoiding potential subscription fees",
|
||||
"Suspicion of hidden data collection in updates",
|
||||
"Apprehension about changes in privacy policies",
|
||||
"Prefer the older version's features or design",
|
||||
"Worry about software becoming more resource-intensive",
|
||||
"Avoiding potential changes in licensing terms",
|
||||
"Waiting for initial bugs to be resolved in the update",
|
||||
"Concerns about update breaking third-party plugins or extensions",
|
||||
"Belief that the software is already secure enough",
|
||||
"Don't want to relearn how to use the software",
|
||||
"Fear of losing access to older file formats",
|
||||
"Avoiding the hassle of having to update multiple devices",
|
||||
}
|
||||
|
||||
var logMessages = []string{
|
||||
"Checking for available updates...",
|
||||
"Downloading update package...",
|
||||
"Verifying update integrity...",
|
||||
"Preparing to install update...",
|
||||
"Backing up existing configuration...",
|
||||
"Installing update...",
|
||||
"Update installation complete.",
|
||||
"Applying configuration settings...",
|
||||
"Cleaning up temporary files...",
|
||||
"Update successful! Software is now up-to-date.",
|
||||
"Restarting the application...",
|
||||
"Restart complete. Enjoy the latest features!",
|
||||
"Update rollback complete. Your software remains at the previous version.",
|
||||
}
|
||||
|
||||
var logErrors = []string{
|
||||
"Unable to check for updates. Please check your internet connection.",
|
||||
"Update package download failed. Try again later.",
|
||||
"Update verification failed. Please contact support.",
|
||||
"Update installation failed. Rolling back to the previous version...",
|
||||
"Your configuration settings may have been reset to defaults.",
|
||||
}
|
110
pkg/notifications/preview/data/report.go
Normal file
110
pkg/notifications/preview/data/report.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
// State is the outcome of a container in a session report
|
||||
type State string
|
||||
|
||||
const (
|
||||
ScannedState State = "scanned"
|
||||
UpdatedState State = "updated"
|
||||
FailedState State = "failed"
|
||||
SkippedState State = "skipped"
|
||||
StaleState State = "stale"
|
||||
FreshState State = "fresh"
|
||||
)
|
||||
|
||||
// StatesFromString parses a string of state characters and returns a slice of the corresponding report states
|
||||
func StatesFromString(str string) []State {
|
||||
states := make([]State, 0, len(str))
|
||||
for _, c := range str {
|
||||
switch c {
|
||||
case 'c':
|
||||
states = append(states, ScannedState)
|
||||
case 'u':
|
||||
states = append(states, UpdatedState)
|
||||
case 'e':
|
||||
states = append(states, FailedState)
|
||||
case 'k':
|
||||
states = append(states, SkippedState)
|
||||
case 't':
|
||||
states = append(states, StaleState)
|
||||
case 'f':
|
||||
states = append(states, FreshState)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
type report struct {
|
||||
scanned []types.ContainerReport
|
||||
updated []types.ContainerReport
|
||||
failed []types.ContainerReport
|
||||
skipped []types.ContainerReport
|
||||
stale []types.ContainerReport
|
||||
fresh []types.ContainerReport
|
||||
}
|
||||
|
||||
func (r *report) Scanned() []types.ContainerReport {
|
||||
return r.scanned
|
||||
}
|
||||
func (r *report) Updated() []types.ContainerReport {
|
||||
return r.updated
|
||||
}
|
||||
func (r *report) Failed() []types.ContainerReport {
|
||||
return r.failed
|
||||
}
|
||||
func (r *report) Skipped() []types.ContainerReport {
|
||||
return r.skipped
|
||||
}
|
||||
func (r *report) Stale() []types.ContainerReport {
|
||||
return r.stale
|
||||
}
|
||||
func (r *report) Fresh() []types.ContainerReport {
|
||||
return r.fresh
|
||||
}
|
||||
|
||||
func (r *report) All() []types.ContainerReport {
|
||||
allLen := len(r.scanned) + len(r.updated) + len(r.failed) + len(r.skipped) + len(r.stale) + len(r.fresh)
|
||||
all := make([]types.ContainerReport, 0, allLen)
|
||||
|
||||
presentIds := map[types.ContainerID][]string{}
|
||||
|
||||
appendUnique := func(reports []types.ContainerReport) {
|
||||
for _, cr := range reports {
|
||||
if _, found := presentIds[cr.ID()]; found {
|
||||
continue
|
||||
}
|
||||
all = append(all, cr)
|
||||
presentIds[cr.ID()] = nil
|
||||
}
|
||||
}
|
||||
|
||||
appendUnique(r.updated)
|
||||
appendUnique(r.failed)
|
||||
appendUnique(r.skipped)
|
||||
appendUnique(r.stale)
|
||||
appendUnique(r.fresh)
|
||||
appendUnique(r.scanned)
|
||||
|
||||
sort.Sort(sortableContainers(all))
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
type sortableContainers []types.ContainerReport
|
||||
|
||||
// Len implements sort.Interface.Len
|
||||
func (s sortableContainers) Len() int { return len(s) }
|
||||
|
||||
// Less implements sort.Interface.Less
|
||||
func (s sortableContainers) Less(i, j int) bool { return s[i].ID() < s[j].ID() }
|
||||
|
||||
// Swap implements sort.Interface.Swap
|
||||
func (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
44
pkg/notifications/preview/data/status.go
Normal file
44
pkg/notifications/preview/data/status.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package data
|
||||
|
||||
import wt "github.com/containrrr/watchtower/pkg/types"
|
||||
|
||||
type containerStatus struct {
|
||||
containerID wt.ContainerID
|
||||
oldImage wt.ImageID
|
||||
newImage wt.ImageID
|
||||
containerName string
|
||||
imageName string
|
||||
error
|
||||
state State
|
||||
}
|
||||
|
||||
func (u *containerStatus) ID() wt.ContainerID {
|
||||
return u.containerID
|
||||
}
|
||||
|
||||
func (u *containerStatus) Name() string {
|
||||
return u.containerName
|
||||
}
|
||||
|
||||
func (u *containerStatus) CurrentImageID() wt.ImageID {
|
||||
return u.oldImage
|
||||
}
|
||||
|
||||
func (u *containerStatus) LatestImageID() wt.ImageID {
|
||||
return u.newImage
|
||||
}
|
||||
|
||||
func (u *containerStatus) ImageName() string {
|
||||
return u.imageName
|
||||
}
|
||||
|
||||
func (u *containerStatus) Error() string {
|
||||
if u.error == nil {
|
||||
return ""
|
||||
}
|
||||
return u.error.Error()
|
||||
}
|
||||
|
||||
func (u *containerStatus) State() string {
|
||||
return string(u.state)
|
||||
}
|
36
pkg/notifications/preview/tplprev.go
Normal file
36
pkg/notifications/preview/tplprev.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package preview
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/notifications/preview/data"
|
||||
"github.com/containrrr/watchtower/pkg/notifications/templates"
|
||||
)
|
||||
|
||||
func Render(input string, states []data.State, loglevels []data.LogLevel) (string, error) {
|
||||
|
||||
data := data.New()
|
||||
|
||||
tpl, err := template.New("").Funcs(templates.Funcs).Parse(input)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse %v", err)
|
||||
}
|
||||
|
||||
for _, state := range states {
|
||||
data.AddFromState(state)
|
||||
}
|
||||
|
||||
for _, level := range loglevels {
|
||||
data.AddLogEntry(level)
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
err = tpl.Execute(&buf, data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to execute template: %v", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
|
@ -10,10 +10,9 @@ import (
|
|||
|
||||
"github.com/containrrr/shoutrrr"
|
||||
"github.com/containrrr/shoutrrr/pkg/types"
|
||||
"github.com/containrrr/watchtower/pkg/notifications/templates"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// LocalLog is a logrus logger that does not send entries as notifications
|
||||
|
@ -61,7 +60,7 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
|
|||
return names
|
||||
}
|
||||
|
||||
// GetNames returns a list of URLs for notification services that has been added
|
||||
// GetURLs returns a list of URLs for notification services that has been added
|
||||
func (n *shoutrrrTypeNotifier) GetURLs() []string {
|
||||
return n.Urls
|
||||
}
|
||||
|
@ -74,7 +73,7 @@ func (n *shoutrrrTypeNotifier) AddLogHook() {
|
|||
n.receiving = true
|
||||
log.AddHook(n)
|
||||
|
||||
// Do the sending in a separate goroutine so we don't block the main process.
|
||||
// Do the sending in a separate goroutine, so we don't block the main process.
|
||||
go sendNotifications(n)
|
||||
}
|
||||
|
||||
|
@ -110,6 +109,7 @@ func createNotifier(urls []string, level log.Level, tplString string, legacy boo
|
|||
legacyTemplate: legacy,
|
||||
data: data,
|
||||
params: params,
|
||||
delay: delay,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,13 +207,8 @@ func (n *shoutrrrTypeNotifier) Fire(entry *log.Entry) error {
|
|||
}
|
||||
|
||||
func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, err error) {
|
||||
funcs := template.FuncMap{
|
||||
"ToUpper": strings.ToUpper,
|
||||
"ToLower": strings.ToLower,
|
||||
"ToJSON": toJSON,
|
||||
"Title": cases.Title(language.AmericanEnglish).String,
|
||||
}
|
||||
tplBase := template.New("").Funcs(funcs)
|
||||
|
||||
tplBase := template.New("").Funcs(templates.Funcs)
|
||||
|
||||
if builtin, found := commonTemplates[tplString]; found {
|
||||
log.WithField(`template`, tplString).Debug(`Using common template`)
|
||||
|
|
27
pkg/notifications/templates/funcs.go
Normal file
27
pkg/notifications/templates/funcs.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package templates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var Funcs = template.FuncMap{
|
||||
"ToUpper": strings.ToUpper,
|
||||
"ToLower": strings.ToLower,
|
||||
"ToJSON": toJSON,
|
||||
"Title": cases.Title(language.AmericanEnglish).String,
|
||||
}
|
||||
|
||||
func toJSON(v interface{}) string {
|
||||
var bytes []byte
|
||||
var err error
|
||||
if bytes, err = json.MarshalIndent(v, "", " "); err != nil {
|
||||
return fmt.Sprintf("failed to marshal JSON in notification template: %v", err)
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
|
@ -4,14 +4,14 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
ref "github.com/distribution/reference"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -20,13 +20,13 @@ const ChallengeHeader = "WWW-Authenticate"
|
|||
|
||||
// GetToken fetches a token for the registry hosting the provided image
|
||||
func GetToken(container types.Container, registryAuth string) (string, error) {
|
||||
var err error
|
||||
var URL url.URL
|
||||
|
||||
if URL, err = GetChallengeURL(container.ImageName()); err != nil {
|
||||
normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
logrus.WithField("URL", URL.String()).Debug("Building challenge URL")
|
||||
|
||||
URL := GetChallengeURL(normalizedRef)
|
||||
logrus.WithField("URL", URL.String()).Debug("Built challenge URL")
|
||||
|
||||
var req *http.Request
|
||||
if req, err = GetChallengeRequest(URL); err != nil {
|
||||
|
@ -55,7 +55,7 @@ func GetToken(container types.Container, registryAuth string) (string, error) {
|
|||
return fmt.Sprintf("Basic %s", registryAuth), nil
|
||||
}
|
||||
if strings.HasPrefix(challenge, "bearer") {
|
||||
return GetBearerHeader(challenge, container.ImageName(), registryAuth)
|
||||
return GetBearerHeader(challenge, normalizedRef, registryAuth)
|
||||
}
|
||||
|
||||
return "", errors.New("unsupported challenge type from registry")
|
||||
|
@ -73,12 +73,9 @@ func GetChallengeRequest(URL url.URL) (*http.Request, error) {
|
|||
}
|
||||
|
||||
// GetBearerHeader tries to fetch a bearer token from the registry based on the challenge instructions
|
||||
func GetBearerHeader(challenge string, img string, registryAuth string) (string, error) {
|
||||
func GetBearerHeader(challenge string, imageRef ref.Named, registryAuth string) (string, error) {
|
||||
client := http.Client{}
|
||||
if strings.Contains(img, ":") {
|
||||
img = strings.Split(img, ":")[0]
|
||||
}
|
||||
authURL, err := GetAuthURL(challenge, img)
|
||||
authURL, err := GetAuthURL(challenge, imageRef)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -91,7 +88,8 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string,
|
|||
|
||||
if registryAuth != "" {
|
||||
logrus.Debug("Credentials found.")
|
||||
logrus.Tracef("Credentials: %v", registryAuth)
|
||||
// CREDENTIAL: Uncomment to log registry credentials
|
||||
// logrus.Tracef("Credentials: %v", registryAuth)
|
||||
r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth))
|
||||
} else {
|
||||
logrus.Debug("No credentials found.")
|
||||
|
@ -102,7 +100,7 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string,
|
|||
return "", err
|
||||
}
|
||||
|
||||
body, _ := ioutil.ReadAll(authResponse.Body)
|
||||
body, _ := io.ReadAll(authResponse.Body)
|
||||
tokenResponse := &types.TokenResponse{}
|
||||
|
||||
err = json.Unmarshal(body, tokenResponse)
|
||||
|
@ -114,7 +112,7 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string,
|
|||
}
|
||||
|
||||
// GetAuthURL from the instructions in the challenge
|
||||
func GetAuthURL(challenge string, img string) (*url.URL, error) {
|
||||
func GetAuthURL(challenge string, imageRef ref.Named) (*url.URL, error) {
|
||||
loweredChallenge := strings.ToLower(challenge)
|
||||
raw := strings.TrimPrefix(loweredChallenge, "bearer")
|
||||
|
||||
|
@ -123,10 +121,9 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) {
|
|||
|
||||
for _, pair := range pairs {
|
||||
trimmed := strings.Trim(pair, " ")
|
||||
kv := strings.Split(trimmed, "=")
|
||||
key := kv[0]
|
||||
val := strings.Trim(kv[1], "\"")
|
||||
values[key] = val
|
||||
if key, val, ok := strings.Cut(trimmed, "="); ok {
|
||||
values[key] = strings.Trim(val, `"`)
|
||||
}
|
||||
}
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"realm": values["realm"],
|
||||
|
@ -141,53 +138,25 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) {
|
|||
q := authURL.Query()
|
||||
q.Add("service", values["service"])
|
||||
|
||||
scopeImage := GetScopeFromImageName(img, values["service"])
|
||||
scopeImage := ref.Path(imageRef)
|
||||
|
||||
scope := fmt.Sprintf("repository:%s:pull", scopeImage)
|
||||
logrus.WithFields(logrus.Fields{"scope": scope, "image": img}).Debug("Setting scope for auth token")
|
||||
logrus.WithFields(logrus.Fields{"scope": scope, "image": imageRef.Name()}).Debug("Setting scope for auth token")
|
||||
q.Add("scope", scope)
|
||||
|
||||
authURL.RawQuery = q.Encode()
|
||||
return authURL, nil
|
||||
}
|
||||
|
||||
// GetScopeFromImageName normalizes an image name for use as scope during auth and head requests
|
||||
func GetScopeFromImageName(img, svc string) string {
|
||||
parts := strings.Split(img, "/")
|
||||
|
||||
if len(parts) > 2 {
|
||||
if strings.Contains(svc, "docker.io") {
|
||||
return fmt.Sprintf("%s/%s", parts[1], strings.Join(parts[2:], "/"))
|
||||
}
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
if len(parts) == 2 {
|
||||
if strings.Contains(parts[0], "docker.io") {
|
||||
return fmt.Sprintf("library/%s", parts[1])
|
||||
}
|
||||
return strings.Replace(img, svc+"/", "", 1)
|
||||
}
|
||||
|
||||
if strings.Contains(svc, "docker.io") {
|
||||
return fmt.Sprintf("library/%s", parts[0])
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// GetChallengeURL creates a URL object based on the image info
|
||||
func GetChallengeURL(img string) (url.URL, error) {
|
||||
|
||||
normalizedNamed, _ := reference.ParseNormalizedNamed(img)
|
||||
host, err := helpers.NormalizeRegistry(normalizedNamed.String())
|
||||
if err != nil {
|
||||
return url.URL{}, err
|
||||
}
|
||||
// GetChallengeURL returns the URL to check auth requirements
|
||||
// for access to a given image
|
||||
func GetChallengeURL(imageRef ref.Named) url.URL {
|
||||
host, _ := helpers.GetRegistryAddress(imageRef.Name())
|
||||
|
||||
URL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: host,
|
||||
Path: "/v2/",
|
||||
}
|
||||
return URL, nil
|
||||
return URL
|
||||
}
|
||||
|
|
|
@ -2,14 +2,17 @@ package auth_test
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/containrrr/watchtower/internal/actions/mocks"
|
||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/actions/mocks"
|
||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
||||
|
||||
wtTypes "github.com/containrrr/watchtower/pkg/types"
|
||||
ref "github.com/distribution/reference"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
@ -51,7 +54,7 @@ var _ = Describe("the auth module", func() {
|
|||
mockCreated,
|
||||
mockDigest)
|
||||
|
||||
When("getting an auth url", func() {
|
||||
Describe("GetToken", func() {
|
||||
It("should parse the token from the response",
|
||||
SkipIfCredentialsEmpty(GHCRCredentials, func() {
|
||||
creds := fmt.Sprintf("%s:%s", GHCRCredentials.Username, GHCRCredentials.Password)
|
||||
|
@ -60,61 +63,100 @@ var _ = Describe("the auth module", func() {
|
|||
Expect(token).NotTo(Equal(""))
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
Describe("GetAuthURL", func() {
|
||||
It("should create a valid auth url object based on the challenge header supplied", func() {
|
||||
input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"`
|
||||
challenge := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"`
|
||||
imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
expected := &url.URL{
|
||||
Host: "ghcr.io",
|
||||
Scheme: "https",
|
||||
Path: "/token",
|
||||
RawQuery: "scope=repository%3Acontainrrr%2Fwatchtower%3Apull&service=ghcr.io",
|
||||
}
|
||||
res, err := auth.GetAuthURL(input, "containrrr/watchtower")
|
||||
|
||||
URL, err := auth.GetAuthURL(challenge, imageRef)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal(expected))
|
||||
Expect(URL).To(Equal(expected))
|
||||
})
|
||||
It("should create a valid auth url object based on the challenge header supplied", func() {
|
||||
input := `bearer realm="https://ghcr.io/token"`
|
||||
res, err := auth.GetAuthURL(input, "containrrr/watchtower")
|
||||
|
||||
When("given an invalid challenge header", func() {
|
||||
It("should return an error", func() {
|
||||
challenge := `bearer realm="https://ghcr.io/token"`
|
||||
imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
URL, err := auth.GetAuthURL(challenge, imageRef)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(res).To(BeNil())
|
||||
Expect(URL).To(BeNil())
|
||||
})
|
||||
})
|
||||
When("getting a challenge url", func() {
|
||||
It("should create a valid challenge url object based on the image ref supplied", func() {
|
||||
expected := url.URL{Host: "ghcr.io", Scheme: "https", Path: "/v2/"}
|
||||
Expect(auth.GetChallengeURL("ghcr.io/containrrr/watchtower:latest")).To(Equal(expected))
|
||||
})
|
||||
It("should assume dockerhub if the image ref is not fully qualified", func() {
|
||||
expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"}
|
||||
Expect(auth.GetChallengeURL("containrrr/watchtower:latest")).To(Equal(expected))
|
||||
})
|
||||
It("should convert legacy dockerhub hostnames to index.docker.io", func() {
|
||||
expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"}
|
||||
Expect(auth.GetChallengeURL("docker.io/containrrr/watchtower:latest")).To(Equal(expected))
|
||||
Expect(auth.GetChallengeURL("registry-1.docker.io/containrrr/watchtower:latest")).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
When("getting the auth scope from an image name", func() {
|
||||
|
||||
When("deriving the auth scope from an image name", func() {
|
||||
It("should prepend official dockerhub images with \"library/\"", func() {
|
||||
Expect(auth.GetScopeFromImageName("docker.io/registry", "index.docker.io")).To(Equal("library/registry"))
|
||||
Expect(auth.GetScopeFromImageName("docker.io/registry", "docker.io")).To(Equal("library/registry"))
|
||||
|
||||
Expect(auth.GetScopeFromImageName("registry", "index.docker.io")).To(Equal("library/registry"))
|
||||
Expect(auth.GetScopeFromImageName("watchtower", "registry-1.docker.io")).To(Equal("library/watchtower"))
|
||||
|
||||
Expect(getScopeFromImageAuthURL("registry")).To(Equal("library/registry"))
|
||||
Expect(getScopeFromImageAuthURL("docker.io/registry")).To(Equal("library/registry"))
|
||||
Expect(getScopeFromImageAuthURL("index.docker.io/registry")).To(Equal("library/registry"))
|
||||
})
|
||||
It("should not include vanity hosts\"", func() {
|
||||
Expect(auth.GetScopeFromImageName("docker.io/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower"))
|
||||
Expect(auth.GetScopeFromImageName("index.docker.io/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower"))
|
||||
Expect(getScopeFromImageAuthURL("docker.io/containrrr/watchtower")).To(Equal("containrrr/watchtower"))
|
||||
Expect(getScopeFromImageAuthURL("index.docker.io/containrrr/watchtower")).To(Equal("containrrr/watchtower"))
|
||||
})
|
||||
It("should not destroy three segment image names\"", func() {
|
||||
Expect(auth.GetScopeFromImageName("piksel/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower"))
|
||||
Expect(auth.GetScopeFromImageName("piksel/containrrr/watchtower", "ghcr.io")).To(Equal("piksel/containrrr/watchtower"))
|
||||
Expect(getScopeFromImageAuthURL("piksel/containrrr/watchtower")).To(Equal("piksel/containrrr/watchtower"))
|
||||
Expect(getScopeFromImageAuthURL("ghcr.io/piksel/containrrr/watchtower")).To(Equal("piksel/containrrr/watchtower"))
|
||||
})
|
||||
It("should not add \"library/\" for one segment image names if they're not on dockerhub", func() {
|
||||
Expect(auth.GetScopeFromImageName("ghcr.io/watchtower", "ghcr.io")).To(Equal("watchtower"))
|
||||
Expect(auth.GetScopeFromImageName("watchtower", "ghcr.io")).To(Equal("watchtower"))
|
||||
It("should not prepend library/ to image names if they're not on dockerhub", func() {
|
||||
Expect(getScopeFromImageAuthURL("ghcr.io/watchtower")).To(Equal("watchtower"))
|
||||
Expect(getScopeFromImageAuthURL("ghcr.io/containrrr/watchtower")).To(Equal("containrrr/watchtower"))
|
||||
})
|
||||
})
|
||||
It("should not crash when an empty field is received", func() {
|
||||
input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",`
|
||||
imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
res, err := auth.GetAuthURL(input, imageRef)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).NotTo(BeNil())
|
||||
})
|
||||
It("should not crash when a field without a value is received", func() {
|
||||
input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",valuelesskey`
|
||||
imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
res, err := auth.GetAuthURL(input, imageRef)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).NotTo(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetChallengeURL", func() {
|
||||
It("should create a valid challenge url object based on the image ref supplied", func() {
|
||||
expected := url.URL{Host: "ghcr.io", Scheme: "https", Path: "/v2/"}
|
||||
imageRef, _ := ref.ParseNormalizedNamed("ghcr.io/containrrr/watchtower:latest")
|
||||
Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected))
|
||||
})
|
||||
It("should assume Docker Hub for image refs with no explicit registry", func() {
|
||||
expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"}
|
||||
imageRef, _ := ref.ParseNormalizedNamed("containrrr/watchtower:latest")
|
||||
Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected))
|
||||
})
|
||||
It("should use index.docker.io if the image ref specifies docker.io", func() {
|
||||
expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"}
|
||||
imageRef, _ := ref.ParseNormalizedNamed("docker.io/containrrr/watchtower:latest")
|
||||
Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var scopeImageRegexp = MatchRegexp("^repository:[a-z0-9]+(/[a-z0-9]+)*:pull$")
|
||||
|
||||
func getScopeFromImageAuthURL(imageName string) string {
|
||||
normalizedRef, _ := ref.ParseNormalizedNamed(imageName)
|
||||
challenge := `bearer realm="https://dummy.host/token",service="dummy.host",scope="repository:user/image:pull"`
|
||||
URL, _ := auth.GetAuthURL(challenge, normalizedRef)
|
||||
|
||||
scope := URL.Query().Get("scope")
|
||||
Expect(scopeImageRegexp.Match(scope)).To(BeTrue())
|
||||
return strings.Replace(scope[11:], ":pull", "", 1)
|
||||
}
|
||||
|
|
|
@ -6,15 +6,16 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/meta"
|
||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
||||
"github.com/containrrr/watchtower/pkg/registry/manifest"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ContentDigestHeader is the key for the key-value pair containing the digest header
|
||||
|
@ -93,12 +94,13 @@ func GetDigest(url string, token string) (string, error) {
|
|||
req, _ := http.NewRequest("HEAD", url, nil)
|
||||
req.Header.Set("User-Agent", meta.UserAgent)
|
||||
|
||||
if token != "" {
|
||||
logrus.WithField("token", token).Trace("Setting request token")
|
||||
} else {
|
||||
if token == "" {
|
||||
return "", errors.New("could not fetch token")
|
||||
}
|
||||
|
||||
// CREDENTIAL: Uncomment to log the request token
|
||||
// logrus.WithField("token", token).Trace("Setting request token")
|
||||
|
||||
req.Header.Add("Authorization", token)
|
||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v2+json")
|
||||
|
|
|
@ -1,36 +1,28 @@
|
|||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
url2 "net/url"
|
||||
"github.com/distribution/reference"
|
||||
)
|
||||
|
||||
// ConvertToHostname strips a url from everything but the hostname part
|
||||
func ConvertToHostname(url string) (string, string, error) {
|
||||
urlWithSchema := fmt.Sprintf("x://%s", url)
|
||||
u, err := url2.Parse(urlWithSchema)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
hostName := u.Hostname()
|
||||
port := u.Port()
|
||||
// domains for Docker Hub, the default registry
|
||||
const (
|
||||
DefaultRegistryDomain = "docker.io"
|
||||
DefaultRegistryHost = "index.docker.io"
|
||||
LegacyDefaultRegistryDomain = "index.docker.io"
|
||||
)
|
||||
|
||||
return hostName, port, err
|
||||
}
|
||||
|
||||
// NormalizeRegistry makes sure variations of DockerHubs registry
|
||||
func NormalizeRegistry(registry string) (string, error) {
|
||||
hostName, port, err := ConvertToHostname(registry)
|
||||
// GetRegistryAddress parses an image name
|
||||
// and returns the address of the specified registry
|
||||
func GetRegistryAddress(imageRef string) (string, error) {
|
||||
normalizedRef, err := reference.ParseNormalizedNamed(imageRef)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if hostName == "registry-1.docker.io" || hostName == "docker.io" {
|
||||
hostName = "index.docker.io"
|
||||
}
|
||||
address := reference.Domain(normalizedRef)
|
||||
|
||||
if port != "" {
|
||||
return fmt.Sprintf("%s:%s", hostName, port), nil
|
||||
if address == DefaultRegistryDomain {
|
||||
address = DefaultRegistryHost
|
||||
}
|
||||
return hostName, nil
|
||||
return address, nil
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package helpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHelpers(t *testing.T) {
|
||||
|
@ -12,20 +13,25 @@ func TestHelpers(t *testing.T) {
|
|||
}
|
||||
|
||||
var _ = Describe("the helpers", func() {
|
||||
|
||||
When("converting an url to a hostname", func() {
|
||||
It("should return docker.io given docker.io/containrrr/watchtower:latest", func() {
|
||||
host, port, err := ConvertToHostname("docker.io/containrrr/watchtower:latest")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(host).To(Equal("docker.io"))
|
||||
Expect(port).To(BeEmpty())
|
||||
Describe("GetRegistryAddress", func() {
|
||||
It("should return error if passed empty string", func() {
|
||||
_, err := GetRegistryAddress("")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
It("should return index.docker.io for image refs with no explicit registry", func() {
|
||||
Expect(GetRegistryAddress("watchtower")).To(Equal("index.docker.io"))
|
||||
Expect(GetRegistryAddress("containrrr/watchtower")).To(Equal("index.docker.io"))
|
||||
})
|
||||
When("normalizing the registry information", func() {
|
||||
It("should return index.docker.io given docker.io", func() {
|
||||
out, err := NormalizeRegistry("docker.io/containrrr/watchtower:latest")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(out).To(Equal("index.docker.io"))
|
||||
It("should return index.docker.io for image refs with docker.io domain", func() {
|
||||
Expect(GetRegistryAddress("docker.io/watchtower")).To(Equal("index.docker.io"))
|
||||
Expect(GetRegistryAddress("docker.io/containrrr/watchtower")).To(Equal("index.docker.io"))
|
||||
})
|
||||
It("should return the host if passed an image name containing a local host", func() {
|
||||
Expect(GetRegistryAddress("henk:80/watchtower")).To(Equal("henk:80"))
|
||||
Expect(GetRegistryAddress("localhost/watchtower")).To(Equal("localhost"))
|
||||
})
|
||||
It("should return the server address if passed a fully qualified image name", func() {
|
||||
Expect(GetRegistryAddress("github.com/containrrr/config")).To(Equal("github.com"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,42 +1,41 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
||||
url2 "net/url"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
ref "github.com/docker/distribution/reference"
|
||||
ref "github.com/distribution/reference"
|
||||
"github.com/sirupsen/logrus"
|
||||
url2 "net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BuildManifestURL from raw image data
|
||||
func BuildManifestURL(container types.Container) (string, error) {
|
||||
|
||||
normalizedName, err := ref.ParseNormalizedNamed(container.ImageName())
|
||||
normalizedRef, err := ref.ParseDockerRef(container.ImageName())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
normalizedTaggedRef, isTagged := normalizedRef.(ref.NamedTagged)
|
||||
if !isTagged {
|
||||
return "", errors.New("Parsed container image ref has no tag: " + normalizedRef.String())
|
||||
}
|
||||
|
||||
host, err := helpers.NormalizeRegistry(normalizedName.String())
|
||||
img, tag := ExtractImageAndTag(strings.TrimPrefix(container.ImageName(), host+"/"))
|
||||
host, _ := helpers.GetRegistryAddress(normalizedTaggedRef.Name())
|
||||
img, tag := ref.Path(normalizedTaggedRef), normalizedTaggedRef.Tag()
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"image": img,
|
||||
"tag": tag,
|
||||
"normalized": normalizedName,
|
||||
"normalized": normalizedTaggedRef.Name(),
|
||||
"host": host,
|
||||
}).Debug("Parsing image ref")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
img = auth.GetScopeFromImageName(img, host)
|
||||
|
||||
if !strings.Contains(img, "/") {
|
||||
img = "library/" + img
|
||||
}
|
||||
url := url2.URL{
|
||||
Scheme: "https",
|
||||
Host: host,
|
||||
|
@ -44,24 +43,3 @@ func BuildManifestURL(container types.Container) (string, error) {
|
|||
}
|
||||
return url.String(), nil
|
||||
}
|
||||
|
||||
// ExtractImageAndTag from a concatenated string
|
||||
func ExtractImageAndTag(imageName string) (string, string) {
|
||||
var img string
|
||||
var tag string
|
||||
|
||||
if strings.Contains(imageName, ":") {
|
||||
parts := strings.Split(imageName, ":")
|
||||
if len(parts) > 2 {
|
||||
img = parts[0]
|
||||
tag = strings.Join(parts[1:], ":")
|
||||
} else {
|
||||
img = parts[0]
|
||||
tag = parts[1]
|
||||
}
|
||||
} else {
|
||||
img = imageName
|
||||
tag = "latest"
|
||||
}
|
||||
return img, tag
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package manifest_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/actions/mocks"
|
||||
"github.com/containrrr/watchtower/pkg/registry/manifest"
|
||||
apiTypes "github.com/docker/docker/api/types"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestManifest(t *testing.T) {
|
||||
|
@ -16,60 +17,58 @@ func TestManifest(t *testing.T) {
|
|||
}
|
||||
|
||||
var _ = Describe("the manifest module", func() {
|
||||
mockId := "mock-id"
|
||||
Describe("BuildManifestURL", func() {
|
||||
It("should return a valid url given a fully qualified image", func() {
|
||||
imageRef := "ghcr.io/containrrr/watchtower:mytag"
|
||||
expected := "https://ghcr.io/v2/containrrr/watchtower/manifests/mytag"
|
||||
|
||||
URL, err := buildMockContainerManifestURL(imageRef)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(URL).To(Equal(expected))
|
||||
})
|
||||
It("should assume Docker Hub for image refs with no explicit registry", func() {
|
||||
imageRef := "containrrr/watchtower:latest"
|
||||
expected := "https://index.docker.io/v2/containrrr/watchtower/manifests/latest"
|
||||
|
||||
URL, err := buildMockContainerManifestURL(imageRef)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(URL).To(Equal(expected))
|
||||
})
|
||||
It("should assume latest for image refs with no explicit tag", func() {
|
||||
imageRef := "containrrr/watchtower"
|
||||
expected := "https://index.docker.io/v2/containrrr/watchtower/manifests/latest"
|
||||
|
||||
URL, err := buildMockContainerManifestURL(imageRef)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(URL).To(Equal(expected))
|
||||
})
|
||||
It("should not prepend library/ for single-part container names in registries other than Docker Hub", func() {
|
||||
imageRef := "docker-registry.domain/imagename:latest"
|
||||
expected := "https://docker-registry.domain/v2/imagename/manifests/latest"
|
||||
|
||||
URL, err := buildMockContainerManifestURL(imageRef)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(URL).To(Equal(expected))
|
||||
})
|
||||
It("should throw an error on pinned images", func() {
|
||||
imageRef := "docker-registry.domain/imagename@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7"
|
||||
URL, err := buildMockContainerManifestURL(imageRef)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(URL).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func buildMockContainerManifestURL(imageRef string) (string, error) {
|
||||
imageInfo := apiTypes.ImageInspect{
|
||||
RepoTags: []string{
|
||||
imageRef,
|
||||
},
|
||||
}
|
||||
mockID := "mock-id"
|
||||
mockName := "mock-container"
|
||||
mockCreated := time.Now()
|
||||
mock := mocks.CreateMockContainerWithImageInfo(mockID, mockName, imageRef, mockCreated, imageInfo)
|
||||
|
||||
When("building a manifest url", func() {
|
||||
It("should return a valid url given a fully qualified image", func() {
|
||||
expected := "https://ghcr.io/v2/containrrr/watchtower/manifests/latest"
|
||||
imageInfo := apiTypes.ImageInspect{
|
||||
RepoTags: []string{
|
||||
"ghcr.io/k6io/operator:latest",
|
||||
},
|
||||
return manifest.BuildManifestURL(mock)
|
||||
}
|
||||
mock := mocks.CreateMockContainerWithImageInfo(mockId, mockName, "ghcr.io/containrrr/watchtower:latest", mockCreated, imageInfo)
|
||||
res, err := manifest.BuildManifestURL(mock)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal(expected))
|
||||
})
|
||||
It("should assume dockerhub for non-qualified images", func() {
|
||||
expected := "https://index.docker.io/v2/containrrr/watchtower/manifests/latest"
|
||||
imageInfo := apiTypes.ImageInspect{
|
||||
RepoTags: []string{
|
||||
"containrrr/watchtower:latest",
|
||||
},
|
||||
}
|
||||
|
||||
mock := mocks.CreateMockContainerWithImageInfo(mockId, mockName, "containrrr/watchtower:latest", mockCreated, imageInfo)
|
||||
res, err := manifest.BuildManifestURL(mock)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal(expected))
|
||||
})
|
||||
It("should assume latest for images that lack an explicit tag", func() {
|
||||
expected := "https://index.docker.io/v2/containrrr/watchtower/manifests/latest"
|
||||
imageInfo := apiTypes.ImageInspect{
|
||||
|
||||
RepoTags: []string{
|
||||
"containrrr/watchtower",
|
||||
},
|
||||
}
|
||||
|
||||
mock := mocks.CreateMockContainerWithImageInfo(mockId, mockName, "containrrr/watchtower", mockCreated, imageInfo)
|
||||
|
||||
res, err := manifest.BuildManifestURL(mock)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal(expected))
|
||||
})
|
||||
It("should combine the tag name and digest pinning into one digest, given multiple colons", func() {
|
||||
in := "containrrr/watchtower:latest@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7"
|
||||
image, tag := "containrrr/watchtower", "latest@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7"
|
||||
|
||||
imageOut, tagOut := manifest.ExtractImageAndTag(in)
|
||||
|
||||
Expect(imageOut).To(Equal(image))
|
||||
Expect(tagOut).To(Equal(tag))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@ package registry
|
|||
import (
|
||||
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
||||
watchtowerTypes "github.com/containrrr/watchtower/pkg/types"
|
||||
ref "github.com/docker/distribution/reference"
|
||||
ref "github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -19,7 +19,9 @@ func GetPullOptions(imageName string) (types.ImagePullOptions, error) {
|
|||
if auth == "" {
|
||||
return types.ImagePullOptions{}, nil
|
||||
}
|
||||
log.Tracef("Got auth value: %s", auth)
|
||||
|
||||
// CREDENTIAL: Uncomment to log docker config auth
|
||||
// log.Tracef("Got auth value: %s", auth)
|
||||
|
||||
return types.ImagePullOptions{
|
||||
RegistryAuth: auth,
|
||||
|
@ -41,17 +43,17 @@ func DefaultAuthHandler() (string, error) {
|
|||
// Will return false if behavior for container is unknown.
|
||||
func WarnOnAPIConsumption(container watchtowerTypes.Container) bool {
|
||||
|
||||
normalizedName, err := ref.ParseNormalizedNamed(container.ImageName())
|
||||
normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName())
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
containerHost, err := helpers.NormalizeRegistry(normalizedName.String())
|
||||
containerHost, err := helpers.GetRegistryAddress(normalizedRef.Name())
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if containerHost == "index.docker.io" || containerHost == "ghcr.io" {
|
||||
if containerHost == helpers.DefaultRegistryHost || containerHost == "ghcr.io" {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -23,11 +23,9 @@ var _ = Describe("Registry", func() {
|
|||
})
|
||||
When("Given a container with an image explicitly from dockerhub", func() {
|
||||
It("should want to warn", func() {
|
||||
Expect(testContainerWithImage("registry-1.docker.io/docker:latest")).To(BeTrue())
|
||||
Expect(testContainerWithImage("index.docker.io/docker:latest")).To(BeTrue())
|
||||
Expect(testContainerWithImage("docker.io/docker:latest")).To(BeTrue())
|
||||
})
|
||||
|
||||
})
|
||||
When("Given a container with an image from some other registry", func() {
|
||||
It("should not want to warn", func() {
|
||||
|
|
|
@ -5,13 +5,12 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -19,7 +18,7 @@ import (
|
|||
// loaded from environment variables or docker config
|
||||
// as available in that order
|
||||
func EncodedAuth(ref string) (string, error) {
|
||||
auth, err := EncodedEnvAuth(ref)
|
||||
auth, err := EncodedEnvAuth()
|
||||
if err != nil {
|
||||
auth, err = EncodedConfigAuth(ref)
|
||||
}
|
||||
|
@ -29,7 +28,7 @@ func EncodedAuth(ref string) (string, error) {
|
|||
// EncodedEnvAuth returns an encoded auth config for the given registry
|
||||
// loaded from environment variables
|
||||
// Returns an error if authentication environment variables have not been set
|
||||
func EncodedEnvAuth(ref string) (string, error) {
|
||||
func EncodedEnvAuth() (string, error) {
|
||||
username := os.Getenv("REPO_USER")
|
||||
password := os.Getenv("REPO_PASS")
|
||||
if username != "" && password != "" {
|
||||
|
@ -37,8 +36,11 @@ func EncodedEnvAuth(ref string) (string, error) {
|
|||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
log.Debugf("Loaded auth credentials for user %s on registry %s", auth.Username, ref)
|
||||
log.Tracef("Using auth password %s", auth.Password)
|
||||
|
||||
log.Debugf("Loaded auth credentials for registry user %s from environment", auth.Username)
|
||||
// CREDENTIAL: Uncomment to log REPO_PASS environment variable
|
||||
// log.Tracef("Using auth password %s", auth.Password)
|
||||
|
||||
return EncodeAuth(auth)
|
||||
}
|
||||
return "", errors.New("registry auth environment variables (REPO_USER, REPO_PASS) not set")
|
||||
|
@ -48,19 +50,20 @@ func EncodedEnvAuth(ref string) (string, error) {
|
|||
// loaded from the docker config
|
||||
// Returns an empty string if credentials cannot be found for the referenced server
|
||||
// The docker config must be mounted on the container
|
||||
func EncodedConfigAuth(ref string) (string, error) {
|
||||
server, err := ParseServerAddress(ref)
|
||||
func EncodedConfigAuth(imageRef string) (string, error) {
|
||||
server, err := helpers.GetRegistryAddress(imageRef)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to parse the image ref %s", err)
|
||||
log.Errorf("Could not get registry from image ref %s", imageRef)
|
||||
return "", err
|
||||
}
|
||||
|
||||
configDir := os.Getenv("DOCKER_CONFIG")
|
||||
if configDir == "" {
|
||||
configDir = "/"
|
||||
}
|
||||
configFile, err := cliconfig.Load(configDir)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to find default config file %s", err)
|
||||
log.Errorf("Unable to find default config file: %s", err)
|
||||
return "", err
|
||||
}
|
||||
credStore := CredentialsStore(*configFile)
|
||||
|
@ -70,23 +73,12 @@ func EncodedConfigAuth(ref string) (string, error) {
|
|||
log.WithField("config_file", configFile.Filename).Debugf("No credentials for %s found", server)
|
||||
return "", nil
|
||||
}
|
||||
log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, ref, configFile.Filename)
|
||||
log.Tracef("Using auth password %s", auth.Password)
|
||||
log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, server, configFile.Filename)
|
||||
// CREDENTIAL: Uncomment to log docker config password
|
||||
// log.Tracef("Using auth password %s", auth.Password)
|
||||
return EncodeAuth(auth)
|
||||
}
|
||||
|
||||
// ParseServerAddress extracts the server part from a container image ref
|
||||
func ParseServerAddress(ref string) (string, error) {
|
||||
|
||||
parsedRef, err := reference.Parse(ref)
|
||||
if err != nil {
|
||||
return ref, err
|
||||
}
|
||||
|
||||
parts := strings.Split(parsedRef.String(), "/")
|
||||
return parts[0], nil
|
||||
}
|
||||
|
||||
// CredentialsStore returns a new credentials store based
|
||||
// on the settings provided in the configuration file.
|
||||
func CredentialsStore(configFile configfile.ConfigFile) credentials.Store {
|
||||
|
|
|
@ -1,22 +1,17 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"os"
|
||||
)
|
||||
|
||||
var _ = Describe("Testing with Ginkgo", func() {
|
||||
It("encoded env auth_ should return an error if repo envs are unset", func() {
|
||||
_ = os.Unsetenv("REPO_USER")
|
||||
_ = os.Unsetenv("REPO_PASS")
|
||||
|
||||
_, err := EncodedEnvAuth("")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
It("encoded env auth_ should return auth hash if repo envs are set", func() {
|
||||
var _ = Describe("Registry credential helpers", func() {
|
||||
Describe("EncodedAuth", func() {
|
||||
It("should return repo credentials from env when set", func() {
|
||||
var err error
|
||||
expectedHash := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0="
|
||||
expected := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0="
|
||||
|
||||
err = os.Setenv("REPO_USER", "containrrr-user")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
@ -24,11 +19,24 @@ var _ = Describe("Testing with Ginkgo", func() {
|
|||
err = os.Setenv("REPO_PASS", "containrrr-pass")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
config, err := EncodedEnvAuth("")
|
||||
Expect(config).To(Equal(expectedHash))
|
||||
config, err := EncodedEnvAuth()
|
||||
Expect(config).To(Equal(expected))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
It("encoded config auth_ should return an error if file is not present", func() {
|
||||
})
|
||||
|
||||
Describe("EncodedEnvAuth", func() {
|
||||
It("should return an error if repo envs are unset", func() {
|
||||
_ = os.Unsetenv("REPO_USER")
|
||||
_ = os.Unsetenv("REPO_PASS")
|
||||
|
||||
_, err := EncodedEnvAuth()
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("EncodedConfigAuth", func() {
|
||||
It("should return an error if file is not present", func() {
|
||||
var err error
|
||||
|
||||
err = os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail")
|
||||
|
@ -36,30 +44,6 @@ var _ = Describe("Testing with Ginkgo", func() {
|
|||
|
||||
_, err = EncodedConfigAuth("")
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
})
|
||||
/*
|
||||
* TODO:
|
||||
* This part only confirms that it still works in the same way as it did
|
||||
* with the old version of the docker api client sdk. I'd say that
|
||||
* ParseServerAddress likely needs to be elaborated a bit to default to
|
||||
* dockerhub in case no server address was provided.
|
||||
*
|
||||
* ++ @simskij, 2019-04-04
|
||||
*/
|
||||
It("parse server address_ should return error if passed empty string", func() {
|
||||
|
||||
_, err := ParseServerAddress("")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
It("parse server address_ should return the organization part if passed an image name missing server name", func() {
|
||||
|
||||
val, _ := ParseServerAddress("containrrr/config")
|
||||
Expect(val).To(Equal("containrrr"))
|
||||
})
|
||||
It("parse server address_ should return the server name if passed a fully qualified image name", func() {
|
||||
|
||||
val, _ := ParseServerAddress("github.com/containrrrr/config")
|
||||
Expect(val).To(Equal("github.com"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -2,13 +2,14 @@ package sorter
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
// ByCreated allows a list of Container structs to be sorted by the container's
|
||||
// created date.
|
||||
type ByCreated []container.Container
|
||||
type ByCreated []types.Container
|
||||
|
||||
func (c ByCreated) Len() int { return len(c) }
|
||||
func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
||||
|
@ -34,18 +35,18 @@ func (c ByCreated) Less(i, j int) bool {
|
|||
// the front of the list while containers with links will be sorted after all
|
||||
// of their dependencies. This sort order ensures that linked containers can
|
||||
// be started in the correct order.
|
||||
func SortByDependencies(containers []container.Container) ([]container.Container, error) {
|
||||
func SortByDependencies(containers []types.Container) ([]types.Container, error) {
|
||||
sorter := dependencySorter{}
|
||||
return sorter.Sort(containers)
|
||||
}
|
||||
|
||||
type dependencySorter struct {
|
||||
unvisited []container.Container
|
||||
unvisited []types.Container
|
||||
marked map[string]bool
|
||||
sorted []container.Container
|
||||
sorted []types.Container
|
||||
}
|
||||
|
||||
func (ds *dependencySorter) Sort(containers []container.Container) ([]container.Container, error) {
|
||||
func (ds *dependencySorter) Sort(containers []types.Container) ([]types.Container, error) {
|
||||
ds.unvisited = containers
|
||||
ds.marked = map[string]bool{}
|
||||
|
||||
|
@ -58,7 +59,7 @@ func (ds *dependencySorter) Sort(containers []container.Container) ([]container.
|
|||
return ds.sorted, nil
|
||||
}
|
||||
|
||||
func (ds *dependencySorter) visit(c container.Container) error {
|
||||
func (ds *dependencySorter) visit(c types.Container) error {
|
||||
|
||||
if _, ok := ds.marked[c.Name()]; ok {
|
||||
return fmt.Errorf("circular reference to %s", c.Name())
|
||||
|
@ -84,7 +85,7 @@ func (ds *dependencySorter) visit(c container.Container) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ds *dependencySorter) findUnvisited(name string) *container.Container {
|
||||
func (ds *dependencySorter) findUnvisited(name string) *types.Container {
|
||||
for _, c := range ds.unvisited {
|
||||
if c.Name() == name {
|
||||
return &c
|
||||
|
@ -94,7 +95,7 @@ func (ds *dependencySorter) findUnvisited(name string) *container.Container {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (ds *dependencySorter) removeUnvisited(c container.Container) {
|
||||
func (ds *dependencySorter) removeUnvisited(c types.Container) {
|
||||
var idx int
|
||||
for i := range ds.unvisited {
|
||||
if ds.unvisited[i].Name() == c.Name() {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dc "github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
// ImageID is a hash string representing a container image
|
||||
|
@ -50,7 +52,7 @@ type Container interface {
|
|||
SafeImageID() ImageID
|
||||
ImageName() string
|
||||
Enabled() (bool, bool)
|
||||
IsMonitorOnly() bool
|
||||
IsMonitorOnly(UpdateParams) bool
|
||||
Scope() (string, bool)
|
||||
Links() []string
|
||||
ToRestart() bool
|
||||
|
@ -62,4 +64,15 @@ type Container interface {
|
|||
GetLifecyclePostCheckCommand() string
|
||||
GetLifecyclePreUpdateCommand() string
|
||||
GetLifecyclePostUpdateCommand() string
|
||||
VerifyConfiguration() error
|
||||
SetStale(bool)
|
||||
IsStale() bool
|
||||
IsNoPull(UpdateParams) bool
|
||||
SetLinkedToRestarting(bool)
|
||||
IsLinkedToRestarting() bool
|
||||
PreUpdateTimeout() int
|
||||
PostUpdateTimeout() int
|
||||
IsRestarting() bool
|
||||
GetCreateConfig() *dc.Config
|
||||
GetCreateHostConfig() *dc.HostConfig
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ type UpdateParams struct {
|
|||
NoRestart bool
|
||||
Timeout time.Duration
|
||||
MonitorOnly bool
|
||||
NoPull bool
|
||||
LifecycleHooks bool
|
||||
RollingRestart bool
|
||||
LabelPrecedence bool
|
||||
}
|
||||
|
|
7
scripts/build-tplprev.sh
Executable file
7
scripts/build-tplprev.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd $(git rev-parse --show-toplevel)
|
||||
|
||||
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./docs/assets/
|
||||
|
||||
GOARCH=wasm GOOS=js go build -o ./docs/assets/tplprev.wasm ./tplprev
|
42
scripts/contnet-tests.sh
Executable file
42
scripts/contnet-tests.sh
Executable file
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
function exit_env_err() {
|
||||
>&2 echo "Required environment variable not set: $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -z "$VPN_SERVICE_PROVIDER" ]; then exit_env_err "VPN_SERVICE_PROVIDER"; fi
|
||||
if [ -z "$OPENVPN_USER" ]; then exit_env_err "OPENVPN_USER"; fi
|
||||
if [ -z "$OPENVPN_PASSWORD" ]; then exit_env_err "OPENVPN_PASSWORD"; fi
|
||||
# if [ -z "$SERVER_COUNTRIES" ]; then exit_env_err "SERVER_COUNTRIES"; fi
|
||||
|
||||
|
||||
export SERVER_COUNTRIES=${SERVER_COUNTRIES:"Sweden"}
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
COMPOSE_FILE="$REPO_ROOT/dockerfiles/container-networking/docker-compose.yml"
|
||||
DEFAULT_WATCHTOWER="$REPO_ROOT/watchtower"
|
||||
WATCHTOWER="$*"
|
||||
WATCHTOWER=${WATCHTOWER:-$DEFAULT_WATCHTOWER}
|
||||
echo "repo root path is $REPO_ROOT"
|
||||
echo "watchtower path is $WATCHTOWER"
|
||||
echo "compose file path is $COMPOSE_FILE"
|
||||
|
||||
echo; echo "=== Forcing network container producer update..."
|
||||
|
||||
echo "Pull previous version of gluetun..."
|
||||
docker pull qmcgaw/gluetun:v3.34.3
|
||||
echo "Fake new version of gluetun by retagging v3.34.4 as v3.35.0..."
|
||||
docker tag qmcgaw/gluetun:v3.34.3 qmcgaw/gluetun:v3.35.0
|
||||
|
||||
echo; echo "=== Creating containers..."
|
||||
|
||||
docker compose -p "wt-contnet" -f "$COMPOSE_FILE" up -d
|
||||
|
||||
echo; echo "=== Running watchtower"
|
||||
$WATCHTOWER --run-once
|
||||
|
||||
echo; echo "=== Removing containers..."
|
||||
|
||||
docker compose -p "wt-contnet" -f "$COMPOSE_FILE" down
|
49
tplprev/main.go
Normal file
49
tplprev/main.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
//go:build !wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/meta"
|
||||
"github.com/containrrr/watchtower/pkg/notifications/preview"
|
||||
"github.com/containrrr/watchtower/pkg/notifications/preview/data"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Fprintf(os.Stderr, "watchtower/tplprev %v\n\n", meta.Version)
|
||||
|
||||
var states string
|
||||
var entries string
|
||||
|
||||
flag.StringVar(&states, "states", "cccuuueeekkktttfff", "sCanned, Updated, failEd, sKipped, sTale, Fresh")
|
||||
flag.StringVar(&entries, "entries", "ewwiiidddd", "Fatal,Error,Warn,Info,Debug,Trace")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if len(flag.Args()) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "Missing required argument TEMPLATE")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
input, err := os.ReadFile(flag.Arg(0))
|
||||
if err != nil {
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := preview.Render(string(input), data.StatesFromString(states), data.LevelsFromString(entries))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(result)
|
||||
}
|
62
tplprev/main_wasm.go
Normal file
62
tplprev/main_wasm.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
//go:build wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/meta"
|
||||
"github.com/containrrr/watchtower/pkg/notifications/preview"
|
||||
"github.com/containrrr/watchtower/pkg/notifications/preview/data"
|
||||
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("watchtower/tplprev v" + meta.Version)
|
||||
|
||||
js.Global().Set("WATCHTOWER", js.ValueOf(map[string]any{
|
||||
"tplprev": js.FuncOf(jsTplPrev),
|
||||
}))
|
||||
<-make(chan bool)
|
||||
|
||||
}
|
||||
|
||||
func jsTplPrev(this js.Value, args []js.Value) any {
|
||||
|
||||
if len(args) < 3 {
|
||||
return "Requires 3 arguments passed"
|
||||
}
|
||||
|
||||
input := args[0].String()
|
||||
|
||||
statesArg := args[1]
|
||||
var states []data.State
|
||||
|
||||
if statesArg.Type() == js.TypeString {
|
||||
states = data.StatesFromString(statesArg.String())
|
||||
} else {
|
||||
for i := 0; i < statesArg.Length(); i++ {
|
||||
state := data.State(statesArg.Index(i).String())
|
||||
states = append(states, state)
|
||||
}
|
||||
}
|
||||
|
||||
levelsArg := args[2]
|
||||
var levels []data.LogLevel
|
||||
|
||||
if levelsArg.Type() == js.TypeString {
|
||||
levels = data.LevelsFromString(statesArg.String())
|
||||
} else {
|
||||
for i := 0; i < levelsArg.Length(); i++ {
|
||||
level := data.LogLevel(levelsArg.Index(i).String())
|
||||
levels = append(levels, level)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := preview.Render(input, states, levels)
|
||||
if err != nil {
|
||||
return "Error: " + err.Error()
|
||||
}
|
||||
return result
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue