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,
|
"imageSize": 100,
|
||||||
"commit": false,
|
"commit": false,
|
||||||
"contributors": [
|
"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",
|
"login": "Codelica",
|
||||||
"name": "James",
|
"name": "James",
|
||||||
|
@ -273,18 +297,6 @@
|
||||||
"code"
|
"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",
|
"login": "Ansem93",
|
||||||
"name": "Ansem93",
|
"name": "Ansem93",
|
||||||
|
@ -508,16 +520,6 @@
|
||||||
"doc"
|
"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",
|
"login": "arnested",
|
||||||
"name": "Arne Jørgensen",
|
"name": "Arne Jørgensen",
|
||||||
|
@ -841,6 +843,12 @@
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"login": "andriibratanin",
|
||||||
|
"name": "Andrii Bratanin",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/20169213?v=4",
|
||||||
|
"profile": "https://github.com/andriibratanin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"login": "IAmTamal",
|
"login": "IAmTamal",
|
||||||
"name": "Tamal Das ",
|
"name": "Tamal Das ",
|
||||||
|
@ -849,6 +857,25 @@
|
||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"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,
|
"contributorsPerLine": 7,
|
||||||
|
@ -857,5 +884,6 @@
|
||||||
"repoType": "github",
|
"repoType": "github",
|
||||||
"repoHost": "https://github.com",
|
"repoHost": "https://github.com",
|
||||||
"commitConvention": "none",
|
"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:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
|
@ -44,7 +44,7 @@ jobs:
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# 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).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
@ -69,4 +69,4 @@ jobs:
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- 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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
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
|
- name: Setup python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.10'
|
||||||
cache: 'pip'
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.18.x
|
go-version: 1.20.x
|
||||||
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
||||||
with:
|
with:
|
||||||
version: "2022.1.1"
|
version: "2023.1.6"
|
||||||
install-go: "false" # StaticCheck uses go v1.17 which does not support `any`
|
install-go: "false" # StaticCheck uses go v1.17 which does not support `any`
|
||||||
test:
|
test:
|
||||||
name: Test
|
name: Test
|
||||||
|
@ -29,7 +29,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go-version:
|
go-version:
|
||||||
- 1.18.x
|
- 1.20.x
|
||||||
platform:
|
platform:
|
||||||
- macos-latest
|
- macos-latest
|
||||||
- windows-latest
|
- windows-latest
|
||||||
|
@ -37,13 +37,13 @@ jobs:
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.18.x
|
go-version: 1.20.x
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
go test -v -coverprofile coverage.out -covermode atomic ./...
|
go test -v -coverprofile coverage.out -covermode atomic ./...
|
||||||
|
@ -56,15 +56,15 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.18.x
|
go-version: 1.20.x
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: goreleaser/goreleaser-action@8f67e590f2d095516493f017008adc464e63adb1 #v3
|
uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3
|
||||||
with:
|
with:
|
||||||
version: v0.155.0
|
version: v0.155.0
|
||||||
args: --snapshot --skip-publish --debug
|
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:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
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
|
- name: Build
|
||||||
run: ./build.sh
|
run: ./build.sh
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.18
|
go-version: 1.20.x
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -v -coverprofile coverage.out -covermode atomic ./...
|
run: go test -v -coverprofile coverage.out -covermode atomic ./...
|
||||||
- name: Publish coverage
|
- name: Publish coverage
|
||||||
|
@ -37,7 +39,7 @@ jobs:
|
||||||
- test
|
- test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Publish to Docker Hub
|
- name: Publish to Docker Hub
|
||||||
uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14
|
uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14
|
||||||
with:
|
with:
|
||||||
|
|
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
|
@ -2,9 +2,7 @@ name: Release (Production)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
release:
|
push:
|
||||||
types:
|
|
||||||
- created
|
|
||||||
tags:
|
tags:
|
||||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
- '**/v[0-9]+.[0-9]+.[0-9]+'
|
- '**/v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
@ -15,13 +13,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.18.x
|
go-version: 1.20.x
|
||||||
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
||||||
with:
|
with:
|
||||||
version: "2022.1.1"
|
version: "2022.1.1"
|
||||||
|
@ -32,7 +30,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version:
|
go-version:
|
||||||
- 1.18.x
|
- 1.20.x
|
||||||
platform:
|
platform:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
- macos-latest
|
- macos-latest
|
||||||
|
@ -40,13 +38,13 @@ jobs:
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.18.x
|
go-version: 1.20.x
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
go test ./... -coverprofile coverage.out
|
go test ./... -coverprofile coverage.out
|
||||||
|
@ -59,29 +57,29 @@ jobs:
|
||||||
- lint
|
- lint
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
TAG: ${{ github.event.release.tag_name }}
|
TAG: ${{ github.ref_name }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.18.x
|
go-version: 1.20.x
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2
|
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2
|
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.BOT_USERNAME }}
|
username: ${{ secrets.BOT_USERNAME }}
|
||||||
password: ${{ secrets.BOT_GHCR_PAT }}
|
password: ${{ secrets.BOT_GHCR_PAT }}
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: goreleaser/goreleaser-action@8f67e590f2d095516493f017008adc464e63adb1 #v3
|
uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3
|
||||||
with:
|
with:
|
||||||
version: v0.155.0
|
version: v0.155.0
|
||||||
args: --debug
|
args: --debug
|
||||||
|
@ -93,7 +91,7 @@ jobs:
|
||||||
echo '{"experimental": "enabled"}' > ~/.docker/config.json
|
echo '{"experimental": "enabled"}' > ~/.docker/config.json
|
||||||
- name: Create manifest for version
|
- name: Create manifest for version
|
||||||
run: |
|
run: |
|
||||||
export DH_TAG=$(echo $TAG | sed 's/^v*//')
|
export DH_TAG=$(git tag --points-at HEAD | sed 's/^v*//')
|
||||||
docker manifest create \
|
docker manifest create \
|
||||||
containrrr/watchtower:$DH_TAG \
|
containrrr/watchtower:$DH_TAG \
|
||||||
containrrr/watchtower:amd64-$DH_TAG \
|
containrrr/watchtower:amd64-$DH_TAG \
|
||||||
|
@ -191,7 +189,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Pull new module version
|
- 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
|
/site
|
||||||
coverage.out
|
coverage.out
|
||||||
*.coverprofile
|
*.coverprofile
|
||||||
|
|
||||||
|
docs/assets/wasm_exec.js
|
||||||
|
docs/assets/*.wasm
|
192
README.md
192
README.md
|
@ -31,6 +31,8 @@ $ docker run --detach \
|
||||||
containrrr/watchtower
|
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
|
## Documentation
|
||||||
The full documentation is available at https://containrrr.dev/watchtower.
|
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>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<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" 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"><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="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"><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="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://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://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/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" 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"><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://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://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/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>
|
||||||
<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" 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"><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://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"><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/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/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="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="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" 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"><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="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://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="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>
|
||||||
<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" 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"><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="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"><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="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="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="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="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" 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"><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="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="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://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>
|
||||||
<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" 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"><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://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"><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/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://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://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://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" 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"><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://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://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://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>
|
||||||
<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" 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"><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" 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"><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://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="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/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/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://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://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" 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"><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://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>
|
||||||
<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" 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"><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="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="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/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="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="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://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/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://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" 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"><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/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>
|
||||||
<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" 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"><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://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://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://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://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="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="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/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="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" 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"><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/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>
|
||||||
<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" 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"><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/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/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/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/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/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/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://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://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" 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"><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://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>
|
||||||
<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" 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"><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://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" 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"><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="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" 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"><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://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://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>
|
||||||
<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" 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"><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/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" 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"><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/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" 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"><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://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://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>
|
||||||
<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" 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"><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="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" 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"><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/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" 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"><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/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/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>
|
||||||
<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" 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"><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://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" 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"><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/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" 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"><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://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://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>
|
||||||
<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" 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"><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="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" 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"><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://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" 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"><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://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://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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
|
||||||
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- markdownlint-restore -->
|
<!-- markdownlint-restore -->
|
||||||
|
|
82
cmd/root.go
82
cmd/root.go
|
@ -1,6 +1,7 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -28,17 +29,20 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
client container.Client
|
client container.Client
|
||||||
scheduleSpec string
|
scheduleSpec string
|
||||||
cleanup bool
|
cleanup bool
|
||||||
noRestart bool
|
noRestart bool
|
||||||
monitorOnly bool
|
noPull bool
|
||||||
enableLabel bool
|
monitorOnly bool
|
||||||
notifier t.Notifier
|
enableLabel bool
|
||||||
timeout time.Duration
|
disableContainers []string
|
||||||
lifecycleHooks bool
|
notifier t.Notifier
|
||||||
rollingRestart bool
|
timeout time.Duration
|
||||||
scope string
|
lifecycleHooks bool
|
||||||
|
rollingRestart bool
|
||||||
|
scope string
|
||||||
|
labelPrecedence bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = NewRootCommand()
|
var rootCmd = NewRootCommand()
|
||||||
|
@ -77,23 +81,8 @@ func Execute() {
|
||||||
func PreRun(cmd *cobra.Command, _ []string) {
|
func PreRun(cmd *cobra.Command, _ []string) {
|
||||||
f := cmd.PersistentFlags()
|
f := cmd.PersistentFlags()
|
||||||
flags.ProcessFlagAliases(f)
|
flags.ProcessFlagAliases(f)
|
||||||
|
if err := flags.SetupLogging(f); err != nil {
|
||||||
if enabled, _ := f.GetBool("no-color"); enabled {
|
log.Fatalf("Failed to initialize logging: %s", err.Error())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleSpec, _ = f.GetString("schedule")
|
scheduleSpec, _ = f.GetString("schedule")
|
||||||
|
@ -106,9 +95,11 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
enableLabel, _ = f.GetBool("label-enable")
|
enableLabel, _ = f.GetBool("label-enable")
|
||||||
|
disableContainers, _ = f.GetStringSlice("disable-containers")
|
||||||
lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
|
lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
|
||||||
rollingRestart, _ = f.GetBool("rolling-restart")
|
rollingRestart, _ = f.GetBool("rolling-restart")
|
||||||
scope, _ = f.GetString("scope")
|
scope, _ = f.GetString("scope")
|
||||||
|
labelPrecedence, _ = f.GetBool("label-take-precedence")
|
||||||
|
|
||||||
if scope != "" {
|
if scope != "" {
|
||||||
log.Debugf(`Using scope %q`, scope)
|
log.Debugf(`Using scope %q`, scope)
|
||||||
|
@ -120,7 +111,7 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
noPull, _ := f.GetBool("no-pull")
|
noPull, _ = f.GetBool("no-pull")
|
||||||
includeStopped, _ := f.GetBool("include-stopped")
|
includeStopped, _ := f.GetBool("include-stopped")
|
||||||
includeRestarting, _ := f.GetBool("include-restarting")
|
includeRestarting, _ := f.GetBool("include-restarting")
|
||||||
reviveStopped, _ := f.GetBool("revive-stopped")
|
reviveStopped, _ := f.GetBool("revive-stopped")
|
||||||
|
@ -132,7 +123,6 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
client = container.NewClient(container.ClientOptions{
|
client = container.NewClient(container.ClientOptions{
|
||||||
PullImages: !noPull,
|
|
||||||
IncludeStopped: includeStopped,
|
IncludeStopped: includeStopped,
|
||||||
ReviveStopped: reviveStopped,
|
ReviveStopped: reviveStopped,
|
||||||
RemoveVolumes: removeVolumes,
|
RemoveVolumes: removeVolumes,
|
||||||
|
@ -146,12 +136,22 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
||||||
|
|
||||||
// Run is the main execution flow of the command
|
// Run is the main execution flow of the command
|
||||||
func Run(c *cobra.Command, names []string) {
|
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")
|
runOnce, _ := c.PersistentFlags().GetBool("run-once")
|
||||||
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
|
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
|
||||||
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")
|
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")
|
||||||
unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls")
|
unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls")
|
||||||
apiToken, _ := c.PersistentFlags().GetString("http-api-token")
|
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 {
|
if rollingRestart && monitorOnly {
|
||||||
log.Fatal("Rolling restarts is not compatible with the global monitor only flag")
|
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)
|
metrics.RegisterScan(metric)
|
||||||
}, updateLock)
|
}, updateLock)
|
||||||
httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle)
|
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.
|
// we need to trigger the startup messages manually.
|
||||||
if !unblockHTTPAPI {
|
if !unblockHTTPAPI {
|
||||||
writeStartupMessage(c, time.Time{}, filterDesc)
|
writeStartupMessage(c, time.Time{}, filterDesc)
|
||||||
|
@ -199,7 +199,7 @@ func Run(c *cobra.Command, names []string) {
|
||||||
httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle)
|
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)
|
log.Error("failed to start API", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,13 +359,15 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string,
|
||||||
func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
|
func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
|
||||||
notifier.StartNotification()
|
notifier.StartNotification()
|
||||||
updateParams := t.UpdateParams{
|
updateParams := t.UpdateParams{
|
||||||
Filter: filter,
|
Filter: filter,
|
||||||
Cleanup: cleanup,
|
Cleanup: cleanup,
|
||||||
NoRestart: noRestart,
|
NoRestart: noRestart,
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
MonitorOnly: monitorOnly,
|
MonitorOnly: monitorOnly,
|
||||||
LifecycleHooks: lifecycleHooks,
|
LifecycleHooks: lifecycleHooks,
|
||||||
RollingRestart: rollingRestart,
|
RollingRestart: rollingRestart,
|
||||||
|
LabelPrecedence: labelPrecedence,
|
||||||
|
NoPull: noPull,
|
||||||
}
|
}
|
||||||
result, err := actions.Update(client, updateParams)
|
result, err := actions.Update(client, updateParams)
|
||||||
if err != nil {
|
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 \
|
RUN apk add --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
@ -17,4 +17,7 @@ COPY --from=alpine \
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
COPY watchtower /
|
COPY watchtower /
|
||||||
|
|
||||||
|
HEALTHCHECK CMD [ "/watchtower", "--health-check"]
|
||||||
|
|
||||||
ENTRYPOINT ["/watchtower"]
|
ENTRYPOINT ["/watchtower"]
|
||||||
|
|
|
@ -7,6 +7,13 @@ FROM golang:alpine as builder
|
||||||
# use version (for example "v0.3.3") or "main"
|
# use version (for example "v0.3.3") or "main"
|
||||||
ARG WATCHTOWER_VERSION=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 \
|
RUN apk add --no-cache \
|
||||||
alpine-sdk \
|
alpine-sdk \
|
||||||
ca-certificates \
|
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 /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
COPY --from=builder /watchtower/watchtower /watchtower
|
COPY --from=builder /watchtower/watchtower /watchtower
|
||||||
|
|
||||||
|
HEALTHCHECK CMD [ "/watchtower", "--health-check"]
|
||||||
|
|
||||||
ENTRYPOINT ["/watchtower"]
|
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 /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
COPY --from=builder /go/watchtower/watchtower /watchtower
|
COPY --from=builder /go/watchtower/watchtower /watchtower
|
||||||
|
|
||||||
|
HEALTHCHECK CMD [ "/watchtower", "--health-check"]
|
||||||
|
|
||||||
ENTRYPOINT ["/watchtower"]
|
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.
|
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
|
## Help
|
||||||
Shows documentation about the supported flags.
|
Shows documentation about the supported flags.
|
||||||
|
|
||||||
|
@ -58,8 +85,8 @@ Environment Variable: WATCHTOWER_CLEANUP
|
||||||
Default: false
|
Default: false
|
||||||
```
|
```
|
||||||
|
|
||||||
## Remove attached volumes
|
## Remove anonymous 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.
|
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
|
```text
|
||||||
Argument: --remove-volumes
|
Argument: --remove-volumes
|
||||||
|
@ -107,6 +134,17 @@ Environment Variable: WATCHTOWER_LOG_LEVEL
|
||||||
Default: info
|
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
|
## ANSI colors
|
||||||
Disable ANSI color escape codes in log output.
|
Disable ANSI color escape codes in log output.
|
||||||
|
|
||||||
|
@ -151,7 +189,7 @@ Environment Variable: WATCHTOWER_INCLUDE_RESTARTING
|
||||||
Will also include created and exited containers.
|
Will also include created and exited containers.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Argument: --include-stopped
|
Argument: --include-stopped, -S
|
||||||
Environment Variable: WATCHTOWER_INCLUDE_STOPPED
|
Environment Variable: WATCHTOWER_INCLUDE_STOPPED
|
||||||
Type: Boolean
|
Type: Boolean
|
||||||
Default: false
|
Default: false
|
||||||
|
@ -178,7 +216,7 @@ Environment Variable: WATCHTOWER_POLL_INTERVAL
|
||||||
```
|
```
|
||||||
|
|
||||||
## Filter by enable label
|
## 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
|
```text
|
||||||
Argument: --label-enable
|
Argument: --label-enable
|
||||||
|
@ -188,10 +226,23 @@ Environment Variable: WATCHTOWER_LABEL_ENABLE
|
||||||
```
|
```
|
||||||
|
|
||||||
## Filter by disable label
|
## 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
|
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.
|
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
|
## Without updating containers
|
||||||
Will only monitor for new images, send notifications and invoke
|
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
|
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.
|
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
|
## Without restarting containers
|
||||||
Do not restart containers after updating. This option can be useful when the start of the 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.
|
is managed by an external system such as systemd.
|
||||||
|
@ -234,6 +298,11 @@ Environment Variable: WATCHTOWER_NO_PULL
|
||||||
Default: false
|
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
|
## Without sending a startup message
|
||||||
Do not send a message after watchtower started. Otherwise there will be an info-level notification.
|
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.
|
Run an update attempt against a container name list one time immediately and exit.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Argument: --run-once
|
Argument: --run-once, -R
|
||||||
Environment Variable: WATCHTOWER_RUN_ONCE
|
Environment Variable: WATCHTOWER_RUN_ONCE
|
||||||
Type: Boolean
|
Type: Boolean
|
||||||
Default: false
|
Default: false
|
||||||
|
@ -267,6 +336,7 @@ Environment Variable: WATCHTOWER_HTTP_API_UPDATE
|
||||||
|
|
||||||
## HTTP API Token
|
## HTTP API Token
|
||||||
Sets an authentication token to HTTP API requests.
|
Sets an authentication token to HTTP API requests.
|
||||||
|
Can also reference a file, in which case the contents of the file are used.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Argument: --http-api-token
|
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.
|
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).
|
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
|
```text
|
||||||
Argument: --scope
|
Argument: --scope
|
||||||
Environment Variable: WATCHTOWER_SCOPE
|
Environment Variable: WATCHTOWER_SCOPE
|
||||||
|
@ -361,3 +436,32 @@ Environment Variable: WATCHTOWER_WARN_ON_HEAD_FAILURE
|
||||||
Possible values: always, auto, never
|
Possible values: always, auto, never
|
||||||
Default: auto
|
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).
|
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:
|
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 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;
|
- 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
|
```bash
|
||||||
curl -H "Authorization: Bearer mytoken" localhost:8080/v1/update
|
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
|
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.
|
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.
|
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`.
|
as well as creating a port mapping for your container for port `8080`.
|
||||||
|
|
||||||
The metrics API endpoint is `/v1/metrics`.
|
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-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-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.
|
- 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-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-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:
|
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.
|
- `--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
|
learn more about the different service URLs you can use. You can define multiple services by space separating the
|
||||||
URLs. (See example below)
|
URLs. (See example below)
|
||||||
|
|
||||||
|
@ -56,6 +57,10 @@ outputs timestamp and log level.
|
||||||
custom format.
|
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.
|
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:
|
Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -110,7 +115,7 @@ Example using a custom report template that always sends a session report after
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name watchtower \
|
--name watchtower \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-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_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \
|
||||||
-e WATCHTOWER_NOTIFICATION_TEMPLATE="
|
-e WATCHTOWER_NOTIFICATION_TEMPLATE="
|
||||||
{{- if .Report -}}
|
{{- if .Report -}}
|
||||||
|
@ -130,7 +135,7 @@ Example using a custom report template that always sends a session report after
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
|
{{range .Entries -}}{{.Message}}{{\"\n\"}}{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
" \
|
" \
|
||||||
containrrr/watchtower
|
containrrr/watchtower
|
||||||
|
|
|
@ -23,19 +23,29 @@ password `auth` string:
|
||||||
```
|
```
|
||||||
|
|
||||||
`<REGISTRY_NAME>` needs to be replaced by the name of your private registry
|
`<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"
|
!!! info "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`.
|
To access private repositories on Docker Hub,
|
||||||
So instead of
|
`<REGISTRY_NAME>` should be `https://index.docker.io/v1/`.
|
||||||
```
|
In this special case, the registry domain does not have to be specified
|
||||||
docker run -d myuser/myimage
|
in `docker run` or `docker-compose`. Like Docker, Watchtower will use the
|
||||||
```
|
Docker Hub registry and its credentials when no registry domain is specified.
|
||||||
you would run it as
|
|
||||||
```
|
|
||||||
docker run -d index.docker.io/myuser/myimage
|
|
||||||
```
|
|
||||||
|
|
||||||
|
<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:
|
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"
|
version: "3.4"
|
||||||
services:
|
services:
|
||||||
watchtower:
|
watchtower:
|
||||||
image: index.docker.io/containrrr/watchtower:latest
|
image: containrrr/watchtower:latest
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- <PATH_TO_HOME_DIR>/.docker/config.json:/config.json
|
- <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):
|
1. Create the Dockerfile (contents below):
|
||||||
```Dockerfile
|
```Dockerfile
|
||||||
FROM golang:1.16
|
FROM golang:1.20
|
||||||
|
|
||||||
ENV GO111MODULE off
|
ENV GO111MODULE off
|
||||||
ENV CGO_ENABLED 0
|
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.
|
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;
|
- 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;
|
- 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:
|
For example, in a Docker Compose config file:
|
||||||
|
|
||||||
|
@ -12,16 +13,29 @@ For example, in a Docker Compose config file:
|
||||||
version: '3'
|
version: '3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app-monitored-by-watchtower:
|
app-with-scope:
|
||||||
image: myapps/monitored-by-watchtower
|
image: myapps/monitored-by-watchtower
|
||||||
labels:
|
labels: [ "com.centurylinklabs.watchtower.scope=myscope" ]
|
||||||
- "com.centurylinklabs.watchtower.scope=myscope"
|
|
||||||
|
|
||||||
watchtower:
|
scoped-watchtower:
|
||||||
image: containrrr/watchtower
|
image: containrrr/watchtower
|
||||||
volumes:
|
volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ]
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
command: --interval 30 --scope myscope
|
command: --interval 30 --scope myscope
|
||||||
labels:
|
labels: [ "com.centurylinklabs.watchtower.scope=myscope" ]
|
||||||
- "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
|
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
|
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
|
from a private repo on the GitHub Registry and monitors it with watchtower. Note the command argument changing the interval
|
||||||
30s rather than the default 24 hours.
|
to 30s rather than the default 24 hours.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
cavo:
|
cavo:
|
||||||
image: index.docker.io/<org>/<image>:<tag>
|
image: ghcr.io/<org>/<image>:<tag>
|
||||||
ports:
|
ports:
|
||||||
- "443:3443"
|
- "443:3443"
|
||||||
- "80:3080"
|
- "80:3080"
|
||||||
|
|
77
go.mod
77
go.mod
|
@ -1,65 +1,72 @@
|
||||||
module github.com/containrrr/watchtower
|
module github.com/containrrr/watchtower
|
||||||
|
|
||||||
go 1.18
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/containrrr/shoutrrr v0.7.1
|
github.com/containrrr/shoutrrr v0.8.0
|
||||||
github.com/docker/cli v20.10.23+incompatible
|
github.com/distribution/reference v0.5.0
|
||||||
github.com/docker/distribution v2.8.1+incompatible
|
github.com/docker/cli v24.0.7+incompatible
|
||||||
github.com/docker/docker v20.10.23+incompatible
|
github.com/docker/docker v24.0.7+incompatible
|
||||||
github.com/docker/go-connections v0.4.0
|
github.com/docker/go-connections v0.4.0
|
||||||
github.com/onsi/ginkgo v1.16.5
|
github.com/onsi/ginkgo v1.16.5
|
||||||
github.com/onsi/gomega v1.25.0
|
github.com/onsi/gomega v1.30.0
|
||||||
github.com/prometheus/client_golang v1.14.0
|
github.com/prometheus/client_golang v1.18.0
|
||||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
github.com/robfig/cron v1.2.0
|
||||||
github.com/sirupsen/logrus v1.9.0
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/cobra v1.6.1
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.15.0
|
github.com/spf13/viper v1.18.2
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.4
|
||||||
golang.org/x/net v0.5.0
|
golang.org/x/net v0.19.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
github.com/Microsoft/go-winio v0.4.17 // indirect
|
github.com/Microsoft/go-winio v0.4.17 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // 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/docker-credential-helpers v0.6.1 // indirect
|
||||||
github.com/docker/go-units v0.4.0 // indirect
|
github.com/docker/go-units v0.4.0 // indirect
|
||||||
github.com/fatih/color v1.13.0 // indirect
|
github.com/fatih/color v1.15.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.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/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
|
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
|
||||||
github.com/nxadm/tail v1.4.8 // indirect
|
github.com/nxadm/tail v1.4.8 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.2 // 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/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.37.0 // indirect
|
github.com/prometheus/common v0.45.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/spf13/afero v1.9.3 // indirect
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
github.com/spf13/cast v1.5.0 // indirect
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.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/stretchr/objx v0.5.0 // indirect
|
||||||
github.com/subosito/gotenv v1.4.2 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
golang.org/x/sys v0.4.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
golang.org/x/text v0.6.0
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/time v0.1.0 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // 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/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
package actions_test
|
package actions_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/internal/actions"
|
"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/containrrr/watchtower/internal/actions/mocks"
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
|
@ -37,7 +38,7 @@ var _ = Describe("the actions package", func() {
|
||||||
It("should not do anything", func() {
|
It("should not do anything", func() {
|
||||||
client := CreateMockClient(
|
client := CreateMockClient(
|
||||||
&TestData{
|
&TestData{
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
CreateMockContainer(
|
CreateMockContainer(
|
||||||
"test-container",
|
"test-container",
|
||||||
"test-container",
|
"test-container",
|
||||||
|
@ -59,7 +60,7 @@ var _ = Describe("the actions package", func() {
|
||||||
client = CreateMockClient(
|
client = CreateMockClient(
|
||||||
&TestData{
|
&TestData{
|
||||||
NameOfContainerToKeep: "test-container-02",
|
NameOfContainerToKeep: "test-container-02",
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
CreateMockContainer(
|
CreateMockContainer(
|
||||||
"test-container-01",
|
"test-container-01",
|
||||||
"test-container-01",
|
"test-container-01",
|
||||||
|
@ -89,7 +90,7 @@ var _ = Describe("the actions package", func() {
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
client = CreateMockClient(
|
client = CreateMockClient(
|
||||||
&TestData{
|
&TestData{
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
CreateMockContainer(
|
CreateMockContainer(
|
||||||
"test-container-01",
|
"test-container-01",
|
||||||
"test-container-01",
|
"test-container-01",
|
||||||
|
|
|
@ -2,16 +2,15 @@ package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/containrrr/watchtower/pkg/types"
|
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/containrrr/watchtower/pkg/container"
|
||||||
"github.com/containrrr/watchtower/pkg/filters"
|
"github.com/containrrr/watchtower/pkg/filters"
|
||||||
"github.com/containrrr/watchtower/pkg/sorter"
|
"github.com/containrrr/watchtower/pkg/sorter"
|
||||||
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/pkg/container"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CheckForSanity makes sure everything is sane before starting
|
// 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
|
// will stop and remove all but the most recently started container. This behaviour can be bypassed
|
||||||
// if a scope UID is defined.
|
// if a scope UID is defined.
|
||||||
func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool, scope string) error {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -55,7 +58,7 @@ func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool,
|
||||||
return cleanupExcessWatchtowers(containers, client, cleanup)
|
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
|
var stopErrors int
|
||||||
|
|
||||||
sort.Sort(sorter.ByCreated(containers))
|
sort.Sort(sorter.ByCreated(containers))
|
||||||
|
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/pkg/container"
|
|
||||||
|
|
||||||
t "github.com/containrrr/watchtower/pkg/types"
|
t "github.com/containrrr/watchtower/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,7 +19,7 @@ type MockClient struct {
|
||||||
type TestData struct {
|
type TestData struct {
|
||||||
TriedToRemoveImageCount int
|
TriedToRemoveImageCount int
|
||||||
NameOfContainerToKeep string
|
NameOfContainerToKeep string
|
||||||
Containers []container.Container
|
Containers []t.Container
|
||||||
Staleness map[string]bool
|
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
|
// 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
|
return client.TestData.Containers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopContainer is a mock method
|
// 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 {
|
if c.Name() == client.TestData.NameOfContainerToKeep {
|
||||||
return errors.New("tried to stop the instance we want to keep")
|
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
|
// 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
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenameContainer is a mock method
|
// RenameContainer is a mock method
|
||||||
func (client MockClient) RenameContainer(_ container.Container, _ string) error {
|
func (client MockClient) RenameContainer(_ t.Container, _ string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +67,7 @@ func (client MockClient) RemoveImageByID(_ t.ImageID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContainer is a mock method
|
// 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
|
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
|
// 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()]
|
stale, found := client.TestData.Staleness[cont.Name()]
|
||||||
if !found {
|
if !found {
|
||||||
stale = true
|
stale = true
|
||||||
|
@ -97,6 +95,6 @@ func (client MockClient) IsContainerStale(cont container.Container) (bool, t.Ima
|
||||||
}
|
}
|
||||||
|
|
||||||
// WarnOnHeadPullFailed is always true for the mock client
|
// WarnOnHeadPullFailed is always true for the mock client
|
||||||
func (client MockClient) WarnOnHeadPullFailed(_ container.Container) bool {
|
func (client MockClient) WarnOnHeadPullFailed(_ t.Container) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateMockContainer creates a container substitute valid for testing
|
// 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{
|
content := types.ContainerJSON{
|
||||||
ContainerJSONBase: &types.ContainerJSONBase{
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -31,7 +31,7 @@ func CreateMockContainer(id string, name string, image string, created time.Time
|
||||||
ExposedPorts: map[nat.Port]struct{}{},
|
ExposedPorts: map[nat.Port]struct{}{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return *container.NewContainer(
|
return container.NewContainer(
|
||||||
&content,
|
&content,
|
||||||
CreateMockImageInfo(image),
|
CreateMockImageInfo(image),
|
||||||
)
|
)
|
||||||
|
@ -48,12 +48,12 @@ func CreateMockImageInfo(image string) *types.ImageInspect {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateMockContainerWithImageInfo should only be used for testing
|
// 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)
|
return CreateMockContainerWithImageInfoP(id, name, image, created, &imageInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateMockContainerWithImageInfoP should only be used for testing
|
// 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{
|
content := types.ContainerJSON{
|
||||||
ContainerJSONBase: &types.ContainerJSONBase{
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -66,21 +66,21 @@ func CreateMockContainerWithImageInfoP(id string, name string, image string, cre
|
||||||
Labels: make(map[string]string),
|
Labels: make(map[string]string),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return *container.NewContainer(
|
return container.NewContainer(
|
||||||
&content,
|
&content,
|
||||||
imageInfo,
|
imageInfo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateMockContainerWithDigest should only be used for testing
|
// 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 := CreateMockContainer(id, name, image, created)
|
||||||
c.ImageInfo().RepoDigests = []string{digest}
|
c.ImageInfo().RepoDigests = []string{digest}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateMockContainerWithConfig creates a container substitute valid for testing
|
// 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{
|
content := types.ContainerJSON{
|
||||||
ContainerJSONBase: &types.ContainerJSONBase{
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -97,14 +97,14 @@ func CreateMockContainerWithConfig(id string, name string, image string, running
|
||||||
},
|
},
|
||||||
Config: config,
|
Config: config,
|
||||||
}
|
}
|
||||||
return *container.NewContainer(
|
return container.NewContainer(
|
||||||
&content,
|
&content,
|
||||||
CreateMockImageInfo(image),
|
CreateMockImageInfo(image),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateContainerForProgress creates a container substitute for tracking session/update progress
|
// 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)
|
indexStr := strconv.Itoa(idPrefix + index)
|
||||||
mockID := indexStr + strings.Repeat("0", 61-len(indexStr))
|
mockID := indexStr + strings.Repeat("0", 61-len(indexStr))
|
||||||
contID := "c79" + mockID
|
contID := "c79" + mockID
|
||||||
|
@ -120,7 +120,7 @@ func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (con
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateMockContainerWithLinks should only be used for testing
|
// 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{
|
content := types.ContainerJSON{
|
||||||
ContainerJSONBase: &types.ContainerJSONBase{
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
@ -136,7 +136,7 @@ func CreateMockContainerWithLinks(id string, name string, image string, created
|
||||||
Labels: make(map[string]string),
|
Labels: make(map[string]string),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return *container.NewContainer(
|
return container.NewContainer(
|
||||||
&content,
|
&content,
|
||||||
imageInfo,
|
imageInfo,
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,6 @@ package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/internal/util"
|
"github.com/containrrr/watchtower/internal/util"
|
||||||
"github.com/containrrr/watchtower/pkg/container"
|
"github.com/containrrr/watchtower/pkg/container"
|
||||||
|
@ -34,8 +33,8 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
||||||
staleCheckFailed := 0
|
staleCheckFailed := 0
|
||||||
|
|
||||||
for i, targetContainer := range containers {
|
for i, targetContainer := range containers {
|
||||||
stale, newestImage, err := client.IsContainerStale(targetContainer)
|
stale, newestImage, err := client.IsContainerStale(targetContainer, params)
|
||||||
shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly()
|
shouldUpdate := stale && !params.NoRestart && !targetContainer.IsMonitorOnly(params)
|
||||||
if err == nil && shouldUpdate {
|
if err == nil && shouldUpdate {
|
||||||
// Check to make sure we have all the necessary information for recreating the container
|
// Check to make sure we have all the necessary information for recreating the container
|
||||||
err = targetContainer.VerifyConfiguration()
|
err = targetContainer.VerifyConfiguration()
|
||||||
|
@ -58,7 +57,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
||||||
} else {
|
} else {
|
||||||
progress.AddScanned(targetContainer, newestImage)
|
progress.AddScanned(targetContainer, newestImage)
|
||||||
}
|
}
|
||||||
containers[i].Stale = stale
|
containers[i].SetStale(stale)
|
||||||
|
|
||||||
if stale {
|
if stale {
|
||||||
staleCount++
|
staleCount++
|
||||||
|
@ -72,13 +71,11 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
||||||
|
|
||||||
UpdateImplicitRestart(containers)
|
UpdateImplicitRestart(containers)
|
||||||
|
|
||||||
var containersToUpdate []container.Container
|
var containersToUpdate []types.Container
|
||||||
if !params.MonitorOnly {
|
for _, c := range containers {
|
||||||
for _, c := range containers {
|
if !c.IsMonitorOnly(params) {
|
||||||
if !c.IsMonitorOnly() {
|
containersToUpdate = append(containersToUpdate, c)
|
||||||
containersToUpdate = append(containersToUpdate, c)
|
progress.MarkForUpdate(c.ID())
|
||||||
progress.MarkForUpdate(c.ID())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +94,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
||||||
return progress.Report(), nil
|
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))
|
cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
|
||||||
failed := make(map[types.ContainerID]error, len(containers))
|
failed := make(map[types.ContainerID]error, len(containers))
|
||||||
|
|
||||||
|
@ -109,7 +106,7 @@ func performRollingRestart(containers []container.Container, client container.Cl
|
||||||
} else {
|
} else {
|
||||||
if err := restartStaleContainer(containers[i], client, params); err != nil {
|
if err := restartStaleContainer(containers[i], client, params); err != nil {
|
||||||
failed[containers[i].ID()] = err
|
failed[containers[i].ID()] = err
|
||||||
} else if containers[i].Stale {
|
} else if containers[i].IsStale() {
|
||||||
// Only add (previously) stale containers' images to cleanup
|
// Only add (previously) stale containers' images to cleanup
|
||||||
cleanupImageIDs[containers[i].ImageID()] = true
|
cleanupImageIDs[containers[i].ImageID()] = true
|
||||||
}
|
}
|
||||||
|
@ -123,7 +120,7 @@ func performRollingRestart(containers []container.Container, client container.Cl
|
||||||
return failed
|
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))
|
failed = make(map[types.ContainerID]error, len(containers))
|
||||||
stopped = make(map[types.ImageID]bool, len(containers))
|
stopped = make(map[types.ImageID]bool, len(containers))
|
||||||
for i := len(containers) - 1; i >= 0; i-- {
|
for i := len(containers) - 1; i >= 0; i-- {
|
||||||
|
@ -138,7 +135,7 @@ func stopContainersInReversedOrder(containers []container.Container, client cont
|
||||||
return
|
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() {
|
if container.IsWatchtower() {
|
||||||
log.Debugf("This is the watchtower container %s", container.Name())
|
log.Debugf("This is the watchtower container %s", container.Name())
|
||||||
return nil
|
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
|
// 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 {
|
if err := container.VerifyConfiguration(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -175,7 +172,7 @@ func stopStaleContainer(container container.Container, client container.Client,
|
||||||
return nil
|
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))
|
cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
|
||||||
failed := make(map[types.ContainerID]error, 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 stoppedImages[c.SafeImageID()] {
|
||||||
if err := restartStaleContainer(c, client, params); err != nil {
|
if err := restartStaleContainer(c, client, params); err != nil {
|
||||||
failed[c.ID()] = err
|
failed[c.ID()] = err
|
||||||
} else if c.Stale {
|
} else if c.IsStale() {
|
||||||
// Only add (previously) stale containers' images to cleanup
|
// Only add (previously) stale containers' images to cleanup
|
||||||
cleanupImageIDs[c.ImageID()] = true
|
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
|
// 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
|
// 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
|
// 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
|
// UpdateImplicitRestart iterates through the passed containers, setting the
|
||||||
// `LinkedToRestarting` flag if any of it's linked containers are marked for restart
|
// `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 {
|
for ci, c := range containers {
|
||||||
if c.ToRestart() {
|
if c.ToRestart() {
|
||||||
|
@ -250,7 +247,7 @@ func UpdateImplicitRestart(containers []container.Container) {
|
||||||
"linked": c.Name(),
|
"linked": c.Name(),
|
||||||
}).Debug("container is linked to restarting")
|
}).Debug("container is linked to restarting")
|
||||||
// NOTE: To mutate the array, the `c` variable cannot be used as it's a copy
|
// 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
|
// linkedContainerMarkedForRestart returns the name of the first link that matches a
|
||||||
// container marked for restart
|
// container marked for restart
|
||||||
func linkedContainerMarkedForRestart(links []string, containers []container.Container) string {
|
func linkedContainerMarkedForRestart(links []string, containers []types.Container) string {
|
||||||
for _, linkName := range links {
|
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 {
|
for _, candidate := range containers {
|
||||||
if candidate.Name() == linkName && candidate.ToRestart() {
|
if candidate.Name() == linkName && candidate.ToRestart() {
|
||||||
return linkName
|
return linkName
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/internal/actions"
|
"github.com/containrrr/watchtower/internal/actions"
|
||||||
"github.com/containrrr/watchtower/pkg/container"
|
|
||||||
"github.com/containrrr/watchtower/pkg/types"
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
dockerTypes "github.com/docker/docker/api/types"
|
dockerTypes "github.com/docker/docker/api/types"
|
||||||
dockerContainer "github.com/docker/docker/api/types/container"
|
dockerContainer "github.com/docker/docker/api/types/container"
|
||||||
|
@ -18,7 +17,7 @@ import (
|
||||||
func getCommonTestData(keepContainer string) *TestData {
|
func getCommonTestData(keepContainer string) *TestData {
|
||||||
return &TestData{
|
return &TestData{
|
||||||
NameOfContainerToKeep: keepContainer,
|
NameOfContainerToKeep: keepContainer,
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
CreateMockContainer(
|
CreateMockContainer(
|
||||||
"test-container-01",
|
"test-container-01",
|
||||||
"test-container-01",
|
"test-container-01",
|
||||||
|
@ -59,7 +58,7 @@ func getLinkedTestData(withImageInfo bool) *TestData {
|
||||||
|
|
||||||
return &TestData{
|
return &TestData{
|
||||||
Staleness: map[string]bool{linkingContainer.Name(): false},
|
Staleness: map[string]bool{linkingContainer.Name(): false},
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
staleContainer,
|
staleContainer,
|
||||||
linkingContainer,
|
linkingContainer,
|
||||||
},
|
},
|
||||||
|
@ -130,7 +129,7 @@ var _ = Describe("the update action", func() {
|
||||||
client := CreateMockClient(
|
client := CreateMockClient(
|
||||||
&TestData{
|
&TestData{
|
||||||
NameOfContainerToKeep: "test-container-02",
|
NameOfContainerToKeep: "test-container-02",
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
CreateMockContainer(
|
CreateMockContainer(
|
||||||
"test-container-01",
|
"test-container-01",
|
||||||
"test-container-01",
|
"test-container-01",
|
||||||
|
@ -163,7 +162,7 @@ var _ = Describe("the update action", func() {
|
||||||
It("should not update any containers", func() {
|
It("should not update any containers", func() {
|
||||||
client := CreateMockClient(
|
client := CreateMockClient(
|
||||||
&TestData{
|
&TestData{
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
CreateMockContainer(
|
CreateMockContainer(
|
||||||
"test-container-01",
|
"test-container-01",
|
||||||
"test-container-01",
|
"test-container-01",
|
||||||
|
@ -179,12 +178,84 @@ var _ = Describe("the update action", func() {
|
||||||
false,
|
false,
|
||||||
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(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
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() {
|
When("watchtower has been instructed to run lifecycle hooks", func() {
|
||||||
|
@ -194,7 +265,7 @@ var _ = Describe("the update action", func() {
|
||||||
client := CreateMockClient(
|
client := CreateMockClient(
|
||||||
&TestData{
|
&TestData{
|
||||||
//NameOfContainerToKeep: "test-container-02",
|
//NameOfContainerToKeep: "test-container-02",
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
CreateMockContainerWithConfig(
|
CreateMockContainerWithConfig(
|
||||||
"test-container-02",
|
"test-container-02",
|
||||||
"test-container-02",
|
"test-container-02",
|
||||||
|
@ -227,7 +298,7 @@ var _ = Describe("the update action", func() {
|
||||||
client := CreateMockClient(
|
client := CreateMockClient(
|
||||||
&TestData{
|
&TestData{
|
||||||
//NameOfContainerToKeep: "test-container-02",
|
//NameOfContainerToKeep: "test-container-02",
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
CreateMockContainerWithConfig(
|
CreateMockContainerWithConfig(
|
||||||
"test-container-02",
|
"test-container-02",
|
||||||
"test-container-02",
|
"test-container-02",
|
||||||
|
@ -259,7 +330,7 @@ var _ = Describe("the update action", func() {
|
||||||
client := CreateMockClient(
|
client := CreateMockClient(
|
||||||
&TestData{
|
&TestData{
|
||||||
//NameOfContainerToKeep: "test-container-02",
|
//NameOfContainerToKeep: "test-container-02",
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
CreateMockContainerWithConfig(
|
CreateMockContainerWithConfig(
|
||||||
"test-container-02",
|
"test-container-02",
|
||||||
"test-container-02",
|
"test-container-02",
|
||||||
|
@ -300,7 +371,7 @@ var _ = Describe("the update action", func() {
|
||||||
ExposedPorts: map[nat.Port]struct{}{},
|
ExposedPorts: map[nat.Port]struct{}{},
|
||||||
})
|
})
|
||||||
|
|
||||||
provider.Stale = true
|
provider.SetStale(true)
|
||||||
|
|
||||||
consumer := CreateMockContainerWithConfig(
|
consumer := CreateMockContainerWithConfig(
|
||||||
"test-container-consumer",
|
"test-container-consumer",
|
||||||
|
@ -316,7 +387,7 @@ var _ = Describe("the update action", func() {
|
||||||
ExposedPorts: map[nat.Port]struct{}{},
|
ExposedPorts: map[nat.Port]struct{}{},
|
||||||
})
|
})
|
||||||
|
|
||||||
containers := []container.Container{
|
containers := []types.Container{
|
||||||
provider,
|
provider,
|
||||||
consumer,
|
consumer,
|
||||||
}
|
}
|
||||||
|
@ -338,7 +409,7 @@ var _ = Describe("the update action", func() {
|
||||||
client := CreateMockClient(
|
client := CreateMockClient(
|
||||||
&TestData{
|
&TestData{
|
||||||
//NameOfContainerToKeep: "test-container-02",
|
//NameOfContainerToKeep: "test-container-02",
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
CreateMockContainerWithConfig(
|
CreateMockContainerWithConfig(
|
||||||
"test-container-02",
|
"test-container-02",
|
||||||
"test-container-02",
|
"test-container-02",
|
||||||
|
@ -370,7 +441,7 @@ var _ = Describe("the update action", func() {
|
||||||
client := CreateMockClient(
|
client := CreateMockClient(
|
||||||
&TestData{
|
&TestData{
|
||||||
//NameOfContainerToKeep: "test-container-02",
|
//NameOfContainerToKeep: "test-container-02",
|
||||||
Containers: []container.Container{
|
Containers: []types.Container{
|
||||||
CreateMockContainerWithConfig(
|
CreateMockContainerWithConfig(
|
||||||
"test-container-02",
|
"test-container-02",
|
||||||
"test-container-02",
|
"test-container-02",
|
||||||
|
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -24,9 +24,9 @@ var defaultInterval = int((time.Hour * 24).Seconds())
|
||||||
// RegisterDockerFlags that are used directly by the docker api client
|
// RegisterDockerFlags that are used directly by the docker api client
|
||||||
func RegisterDockerFlags(rootCmd *cobra.Command) {
|
func RegisterDockerFlags(rootCmd *cobra.Command) {
|
||||||
flags := rootCmd.PersistentFlags()
|
flags := rootCmd.PersistentFlags()
|
||||||
flags.StringP("host", "H", viper.GetString("DOCKER_HOST"), "daemon socket to connect to")
|
flags.StringP("host", "H", envString("DOCKER_HOST"), "daemon socket to connect to")
|
||||||
flags.BoolP("tlsverify", "v", viper.GetBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote")
|
flags.BoolP("tlsverify", "v", envBool("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("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
|
// RegisterSystemFlags that are used by watchtower to modify the program flow
|
||||||
|
@ -35,132 +35,145 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
|
||||||
flags.IntP(
|
flags.IntP(
|
||||||
"interval",
|
"interval",
|
||||||
"i",
|
"i",
|
||||||
viper.GetInt("WATCHTOWER_POLL_INTERVAL"),
|
envInt("WATCHTOWER_POLL_INTERVAL"),
|
||||||
"Poll interval (in seconds)")
|
"Poll interval (in seconds)")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"schedule",
|
"schedule",
|
||||||
"s",
|
"s",
|
||||||
viper.GetString("WATCHTOWER_SCHEDULE"),
|
envString("WATCHTOWER_SCHEDULE"),
|
||||||
"The cron expression which defines when to update")
|
"The cron expression which defines when to update")
|
||||||
|
|
||||||
flags.DurationP(
|
flags.DurationP(
|
||||||
"stop-timeout",
|
"stop-timeout",
|
||||||
"t",
|
"t",
|
||||||
viper.GetDuration("WATCHTOWER_TIMEOUT"),
|
envDuration("WATCHTOWER_TIMEOUT"),
|
||||||
"Timeout before a container is forcefully stopped")
|
"Timeout before a container is forcefully stopped")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"no-pull",
|
"no-pull",
|
||||||
"",
|
"",
|
||||||
viper.GetBool("WATCHTOWER_NO_PULL"),
|
envBool("WATCHTOWER_NO_PULL"),
|
||||||
"Do not pull any new images")
|
"Do not pull any new images")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"no-restart",
|
"no-restart",
|
||||||
"",
|
"",
|
||||||
viper.GetBool("WATCHTOWER_NO_RESTART"),
|
envBool("WATCHTOWER_NO_RESTART"),
|
||||||
"Do not restart any containers")
|
"Do not restart any containers")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"no-startup-message",
|
"no-startup-message",
|
||||||
"",
|
"",
|
||||||
viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"),
|
envBool("WATCHTOWER_NO_STARTUP_MESSAGE"),
|
||||||
"Prevents watchtower from sending a startup message")
|
"Prevents watchtower from sending a startup message")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"cleanup",
|
"cleanup",
|
||||||
"c",
|
"c",
|
||||||
viper.GetBool("WATCHTOWER_CLEANUP"),
|
envBool("WATCHTOWER_CLEANUP"),
|
||||||
"Remove previously used images after updating")
|
"Remove previously used images after updating")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"remove-volumes",
|
"remove-volumes",
|
||||||
"",
|
"",
|
||||||
viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"),
|
envBool("WATCHTOWER_REMOVE_VOLUMES"),
|
||||||
"Remove attached volumes before updating")
|
"Remove attached volumes before updating")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"label-enable",
|
"label-enable",
|
||||||
"e",
|
"e",
|
||||||
viper.GetBool("WATCHTOWER_LABEL_ENABLE"),
|
envBool("WATCHTOWER_LABEL_ENABLE"),
|
||||||
"Watch containers where the com.centurylinklabs.watchtower.enable label is true")
|
"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(
|
flags.BoolP(
|
||||||
"debug",
|
"debug",
|
||||||
"d",
|
"d",
|
||||||
viper.GetBool("WATCHTOWER_DEBUG"),
|
envBool("WATCHTOWER_DEBUG"),
|
||||||
"Enable debug mode with verbose logging")
|
"Enable debug mode with verbose logging")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"trace",
|
"trace",
|
||||||
"",
|
"",
|
||||||
viper.GetBool("WATCHTOWER_TRACE"),
|
envBool("WATCHTOWER_TRACE"),
|
||||||
"Enable trace mode with very verbose logging - caution, exposes credentials")
|
"Enable trace mode with very verbose logging - caution, exposes credentials")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"monitor-only",
|
"monitor-only",
|
||||||
"m",
|
"m",
|
||||||
viper.GetBool("WATCHTOWER_MONITOR_ONLY"),
|
envBool("WATCHTOWER_MONITOR_ONLY"),
|
||||||
"Will only monitor for new images, not update the containers")
|
"Will only monitor for new images, not update the containers")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"run-once",
|
"run-once",
|
||||||
"R",
|
"R",
|
||||||
viper.GetBool("WATCHTOWER_RUN_ONCE"),
|
envBool("WATCHTOWER_RUN_ONCE"),
|
||||||
"Run once now and exit")
|
"Run once now and exit")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"include-restarting",
|
"include-restarting",
|
||||||
"",
|
"",
|
||||||
viper.GetBool("WATCHTOWER_INCLUDE_RESTARTING"),
|
envBool("WATCHTOWER_INCLUDE_RESTARTING"),
|
||||||
"Will also include restarting containers")
|
"Will also include restarting containers")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"include-stopped",
|
"include-stopped",
|
||||||
"S",
|
"S",
|
||||||
viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"),
|
envBool("WATCHTOWER_INCLUDE_STOPPED"),
|
||||||
"Will also include created and exited containers")
|
"Will also include created and exited containers")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"revive-stopped",
|
"revive-stopped",
|
||||||
"",
|
"",
|
||||||
viper.GetBool("WATCHTOWER_REVIVE_STOPPED"),
|
envBool("WATCHTOWER_REVIVE_STOPPED"),
|
||||||
"Will also start stopped containers that were updated, if include-stopped is active")
|
"Will also start stopped containers that were updated, if include-stopped is active")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"enable-lifecycle-hooks",
|
"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")
|
"Enable the execution of commands triggered by pre- and post-update lifecycle hooks")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"rolling-restart",
|
"rolling-restart",
|
||||||
"",
|
"",
|
||||||
viper.GetBool("WATCHTOWER_ROLLING_RESTART"),
|
envBool("WATCHTOWER_ROLLING_RESTART"),
|
||||||
"Restart containers one at a time")
|
"Restart containers one at a time")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"http-api-update",
|
"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")
|
"Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request")
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"http-api-metrics",
|
"http-api-metrics",
|
||||||
"",
|
"",
|
||||||
viper.GetBool("WATCHTOWER_HTTP_API_METRICS"),
|
envBool("WATCHTOWER_HTTP_API_METRICS"),
|
||||||
"Runs Watchtower with the Prometheus metrics API enabled")
|
"Runs Watchtower with the Prometheus metrics API enabled")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"http-api-token",
|
"http-api-token",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_HTTP_API_TOKEN"),
|
envString("WATCHTOWER_HTTP_API_TOKEN"),
|
||||||
"Sets an authentication token to HTTP API requests.")
|
"Sets an authentication token to HTTP API requests.")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"http-api-periodic-polls",
|
"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")
|
"Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled")
|
||||||
|
|
||||||
// https://no-color.org/
|
// https://no-color.org/
|
||||||
|
@ -173,19 +186,31 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"scope",
|
"scope",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_SCOPE"),
|
envString("WATCHTOWER_SCOPE"),
|
||||||
"Defines a monitoring scope for the Watchtower instance.")
|
"Defines a monitoring scope for the Watchtower instance.")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"porcelain",
|
"porcelain",
|
||||||
"P",
|
"P",
|
||||||
viper.GetString("WATCHTOWER_PORCELAIN"),
|
envString("WATCHTOWER_PORCELAIN"),
|
||||||
`Write session results to stdout using a stable versioned format. Supported values: "v1"`)
|
`Write session results to stdout using a stable versioned format. Supported values: "v1"`)
|
||||||
|
|
||||||
flags.String(
|
flags.String(
|
||||||
"log-level",
|
"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")
|
"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
|
// RegisterNotificationFlags that are used by watchtower to send notifications
|
||||||
|
@ -195,177 +220,202 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) {
|
||||||
flags.StringSliceP(
|
flags.StringSliceP(
|
||||||
"notifications",
|
"notifications",
|
||||||
"n",
|
"n",
|
||||||
viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"),
|
envStringSlice("WATCHTOWER_NOTIFICATIONS"),
|
||||||
" Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)")
|
" Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)")
|
||||||
|
|
||||||
flags.String(
|
flags.String(
|
||||||
"notifications-level",
|
"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")
|
"The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug")
|
||||||
|
|
||||||
flags.IntP(
|
flags.IntP(
|
||||||
"notifications-delay",
|
"notifications-delay",
|
||||||
"",
|
"",
|
||||||
viper.GetInt("WATCHTOWER_NOTIFICATIONS_DELAY"),
|
envInt("WATCHTOWER_NOTIFICATIONS_DELAY"),
|
||||||
"Delay before sending notifications, expressed in seconds")
|
"Delay before sending notifications, expressed in seconds")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notifications-hostname",
|
"notifications-hostname",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"),
|
envString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"),
|
||||||
"Custom hostname for notification titles")
|
"Custom hostname for notification titles")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-email-from",
|
"notification-email-from",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"),
|
envString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"),
|
||||||
"Address to send notification emails from")
|
"Address to send notification emails from")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-email-to",
|
"notification-email-to",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"),
|
envString("WATCHTOWER_NOTIFICATION_EMAIL_TO"),
|
||||||
"Address to send notification emails to")
|
"Address to send notification emails to")
|
||||||
|
|
||||||
flags.IntP(
|
flags.IntP(
|
||||||
"notification-email-delay",
|
"notification-email-delay",
|
||||||
"",
|
"",
|
||||||
viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"),
|
envInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"),
|
||||||
"Delay before sending notifications, expressed in seconds")
|
"Delay before sending notifications, expressed in seconds")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-email-server",
|
"notification-email-server",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"),
|
envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"),
|
||||||
"SMTP server to send notification emails through")
|
"SMTP server to send notification emails through")
|
||||||
|
|
||||||
flags.IntP(
|
flags.IntP(
|
||||||
"notification-email-server-port",
|
"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")
|
"SMTP server port to send notification emails through")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"notification-email-server-tls-skip-verify",
|
"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.
|
`Controls whether watchtower verifies the SMTP server's certificate chain and host name.
|
||||||
Should only be used for testing.`)
|
Should only be used for testing.`)
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-email-server-user",
|
"notification-email-server-user",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"),
|
envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"),
|
||||||
"SMTP server user for sending notifications")
|
"SMTP server user for sending notifications")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-email-server-password",
|
"notification-email-server-password",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"),
|
envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"),
|
||||||
"SMTP server password for sending notifications")
|
"SMTP server password for sending notifications")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-email-subjecttag",
|
"notification-email-subjecttag",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"),
|
envString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"),
|
||||||
"Subject prefix tag for notifications via mail")
|
"Subject prefix tag for notifications via mail")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-slack-hook-url",
|
"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")
|
"The Slack Hook URL to send notifications to")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-slack-identifier",
|
"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")
|
"A string which will be used to identify the messages coming from this watchtower instance")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-slack-channel",
|
"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")
|
"A string which overrides the webhook's default channel. Example: #my-custom-channel")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-slack-icon-emoji",
|
"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")
|
"An emoji code string to use in place of the default icon")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-slack-icon-url",
|
"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")
|
"An icon image URL string to use in place of the default icon")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-msteams-hook",
|
"notification-msteams-hook",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"),
|
envString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"),
|
||||||
"The MSTeams WebHook URL to send notifications to")
|
"The MSTeams WebHook URL to send notifications to")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"notification-msteams-data",
|
"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")
|
"The MSTeams notifier will try to extract log entry fields as MSTeams message facts")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-gotify-url",
|
"notification-gotify-url",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"),
|
envString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"),
|
||||||
"The Gotify URL to send notifications to")
|
"The Gotify URL to send notifications to")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-gotify-token",
|
"notification-gotify-token",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"),
|
envString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"),
|
||||||
"The Gotify Application required to query the Gotify API")
|
"The Gotify Application required to query the Gotify API")
|
||||||
|
|
||||||
flags.BoolP(
|
flags.BoolP(
|
||||||
"notification-gotify-tls-skip-verify",
|
"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.
|
`Controls whether watchtower verifies the Gotify server's certificate chain and host name.
|
||||||
Should only be used for testing.`)
|
Should only be used for testing.`)
|
||||||
|
|
||||||
flags.String(
|
flags.String(
|
||||||
"notification-template",
|
"notification-template",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"),
|
envString("WATCHTOWER_NOTIFICATION_TEMPLATE"),
|
||||||
"The shoutrrr text/template for the messages")
|
"The shoutrrr text/template for the messages")
|
||||||
|
|
||||||
flags.StringArray(
|
flags.StringArray(
|
||||||
"notification-url",
|
"notification-url",
|
||||||
viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"),
|
envStringSlice("WATCHTOWER_NOTIFICATION_URL"),
|
||||||
"The shoutrrr URL to send notifications to")
|
"The shoutrrr URL to send notifications to")
|
||||||
|
|
||||||
flags.Bool("notification-report",
|
flags.Bool("notification-report",
|
||||||
viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"),
|
envBool("WATCHTOWER_NOTIFICATION_REPORT"),
|
||||||
"Use the session report as the notification template data")
|
"Use the session report as the notification template data")
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"notification-title-tag",
|
"notification-title-tag",
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_NOTIFICATION_TITLE_TAG"),
|
envString("WATCHTOWER_NOTIFICATION_TITLE_TAG"),
|
||||||
"Title prefix tag for notifications")
|
"Title prefix tag for notifications")
|
||||||
|
|
||||||
flags.Bool("notification-skip-title",
|
flags.Bool("notification-skip-title",
|
||||||
viper.GetBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"),
|
envBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"),
|
||||||
"Do not pass the title param to notifications")
|
"Do not pass the title param to notifications")
|
||||||
|
|
||||||
flags.String(
|
flags.String(
|
||||||
"warn-on-head-failure",
|
"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")
|
"When to warn about HEAD pull requests failing. Possible values: always, auto or never")
|
||||||
|
|
||||||
flags.Bool(
|
flags.Bool(
|
||||||
"notification-log-stdout",
|
"notification-log-stdout",
|
||||||
viper.GetBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"),
|
envBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"),
|
||||||
"Write notification logs to stdout instead of logging (to stderr)")
|
"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
|
// SetDefaults provides default values for environment variables
|
||||||
func SetDefaults() {
|
func SetDefaults() {
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
|
@ -379,6 +429,7 @@ func SetDefaults() {
|
||||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "")
|
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "")
|
||||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower")
|
viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower")
|
||||||
viper.SetDefault("WATCHTOWER_LOG_LEVEL", "info")
|
viper.SetDefault("WATCHTOWER_LOG_LEVEL", "info")
|
||||||
|
viper.SetDefault("WATCHTOWER_LOG_FORMAT", "auto")
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnvConfig translates the command-line options into environment variables
|
// EnvConfig translates the command-line options into environment variables
|
||||||
|
@ -467,14 +518,17 @@ func GetSecretsFromFiles(rootCmd *cobra.Command) {
|
||||||
"notification-msteams-hook",
|
"notification-msteams-hook",
|
||||||
"notification-gotify-token",
|
"notification-gotify-token",
|
||||||
"notification-url",
|
"notification-url",
|
||||||
|
"http-api-token",
|
||||||
}
|
}
|
||||||
for _, secret := range secrets {
|
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.
|
// 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)
|
flag := flags.Lookup(secret)
|
||||||
if sliceValue, ok := flag.Value.(pflag.SliceValue); ok {
|
if sliceValue, ok := flag.Value.(pflag.SliceValue); ok {
|
||||||
oldValues := sliceValue.GetSlice()
|
oldValues := sliceValue.GetSlice()
|
||||||
|
@ -483,7 +537,7 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) {
|
||||||
if value != "" && isFile(value) {
|
if value != "" && isFile(value) {
|
||||||
file, err := os.Open(value)
|
file, err := os.Open(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
scanner := bufio.NewScanner(file)
|
scanner := bufio.NewScanner(file)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
|
@ -493,25 +547,26 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) {
|
||||||
}
|
}
|
||||||
values = append(values, line)
|
values = append(values, line)
|
||||||
}
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
values = append(values, value)
|
values = append(values, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sliceValue.Replace(values)
|
return sliceValue.Replace(values)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
value := flag.Value.String()
|
value := flag.Value.String()
|
||||||
if value != "" && isFile(value) {
|
if value != "" && isFile(value) {
|
||||||
file, err := ioutil.ReadFile(value)
|
content, err := os.ReadFile(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return err
|
||||||
}
|
|
||||||
err = flags.Set(secret, strings.TrimSpace(string(file)))
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
}
|
||||||
|
return flags.Set(secret, strings.TrimSpace(string(content)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isFile(s string) bool {
|
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
|
// update schedule flag to match interval if it's set, or to the default if none of them are
|
||||||
if intervalChanged || !scheduleChanged {
|
if intervalChanged || !scheduleChanged {
|
||||||
interval, _ := flags.GetInt(`interval`)
|
interval, _ := flags.GetInt(`interval`)
|
||||||
flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
|
_ = flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
|
||||||
}
|
}
|
||||||
|
|
||||||
if flagIsEnabled(flags, `debug`) {
|
if flagIsEnabled(flags, `debug`) {
|
||||||
flags.Set(`log-level`, `debug`)
|
_ = flags.Set(`log-level`, `debug`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if flagIsEnabled(flags, `trace`) {
|
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 {
|
func flagIsEnabled(flags *pflag.FlagSet, name string) bool {
|
||||||
value, err := flags.GetBool(name)
|
value, err := flags.GetBool(name)
|
||||||
if err != nil {
|
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 {
|
if flagValues, ok := flag.Value.(pflag.SliceValue); ok {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
flagValues.Append(value)
|
_ = flagValues.Append(value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf(`the value for flag %q is not a slice value`, name)
|
return fmt.Errorf(`the value for flag %q is not a slice value`, name)
|
||||||
|
|
|
@ -1,20 +1,22 @@
|
||||||
package flags
|
package flags
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEnvConfig_Defaults(t *testing.T) {
|
func TestEnvConfig_Defaults(t *testing.T) {
|
||||||
// Unset testing environments own variables, since those are not what is under test
|
// Unset testing environments own variables, since those are not what is under test
|
||||||
os.Unsetenv("DOCKER_TLS_VERIFY")
|
_ = os.Unsetenv("DOCKER_TLS_VERIFY")
|
||||||
os.Unsetenv("DOCKER_HOST")
|
_ = os.Unsetenv("DOCKER_HOST")
|
||||||
|
|
||||||
cmd := new(cobra.Command)
|
cmd := new(cobra.Command)
|
||||||
SetDefaults()
|
SetDefaults()
|
||||||
|
@ -48,10 +50,7 @@ func TestEnvConfig_Custom(t *testing.T) {
|
||||||
|
|
||||||
func TestGetSecretsFromFilesWithString(t *testing.T) {
|
func TestGetSecretsFromFilesWithString(t *testing.T) {
|
||||||
value := "supersecretstring"
|
value := "supersecretstring"
|
||||||
|
t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
|
||||||
err := os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD")
|
|
||||||
|
|
||||||
testGetSecretsFromFiles(t, "notification-email-server-password", value)
|
testGetSecretsFromFiles(t, "notification-email-server-password", value)
|
||||||
}
|
}
|
||||||
|
@ -60,18 +59,15 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) {
|
||||||
value := "megasecretstring"
|
value := "megasecretstring"
|
||||||
|
|
||||||
// Create the temporary file which will contain a secret.
|
// 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)
|
require.NoError(t, err)
|
||||||
defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
|
|
||||||
|
|
||||||
// Write the secret to the temporary file.
|
// Write the secret to the temporary file.
|
||||||
secret := []byte(value)
|
_, err = file.Write([]byte(value))
|
||||||
_, err = file.Write(secret)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, file.Close())
|
||||||
|
|
||||||
err = os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
|
t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
|
||||||
require.NoError(t, err)
|
|
||||||
defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD")
|
|
||||||
|
|
||||||
testGetSecretsFromFiles(t, "notification-email-server-password", value)
|
testGetSecretsFromFiles(t, "notification-email-server-password", value)
|
||||||
}
|
}
|
||||||
|
@ -80,16 +76,15 @@ func TestGetSliceSecretsFromFiles(t *testing.T) {
|
||||||
values := []string{"entry2", "", "entry3"}
|
values := []string{"entry2", "", "entry3"}
|
||||||
|
|
||||||
// Create the temporary file which will contain a secret.
|
// 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)
|
require.NoError(t, err)
|
||||||
defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
|
|
||||||
|
|
||||||
// Write the secret to the temporary file.
|
// Write the secret to the temporary file.
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
_, err = file.WriteString("\n" + value)
|
_, err = file.WriteString("\n" + value)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
file.Close()
|
require.NoError(t, file.Close())
|
||||||
|
|
||||||
testGetSecretsFromFiles(t, "notification-url", `[entry1,entry2,entry3]`,
|
testGetSecretsFromFiles(t, "notification-url", `[entry1,entry2,entry3]`,
|
||||||
`--notification-url`, "entry1",
|
`--notification-url`, "entry1",
|
||||||
|
@ -99,6 +94,7 @@ func TestGetSliceSecretsFromFiles(t *testing.T) {
|
||||||
func testGetSecretsFromFiles(t *testing.T, flagName string, expected string, args ...string) {
|
func testGetSecretsFromFiles(t *testing.T, flagName string, expected string, args ...string) {
|
||||||
cmd := new(cobra.Command)
|
cmd := new(cobra.Command)
|
||||||
SetDefaults()
|
SetDefaults()
|
||||||
|
RegisterSystemFlags(cmd)
|
||||||
RegisterNotificationFlags(cmd)
|
RegisterNotificationFlags(cmd)
|
||||||
require.NoError(t, cmd.ParseFlags(args))
|
require.NoError(t, cmd.ParseFlags(args))
|
||||||
GetSecretsFromFiles(cmd)
|
GetSecretsFromFiles(cmd)
|
||||||
|
@ -166,9 +162,7 @@ func TestProcessFlagAliases(t *testing.T) {
|
||||||
|
|
||||||
func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
|
func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
|
||||||
cmd := new(cobra.Command)
|
cmd := new(cobra.Command)
|
||||||
err := os.Setenv("WATCHTOWER_DEBUG", `true`)
|
t.Setenv("WATCHTOWER_DEBUG", `true`)
|
||||||
require.NoError(t, err)
|
|
||||||
defer os.Unsetenv("WATCHTOWER_DEBUG")
|
|
||||||
|
|
||||||
SetDefaults()
|
SetDefaults()
|
||||||
RegisterDockerFlags(cmd)
|
RegisterDockerFlags(cmd)
|
||||||
|
@ -183,6 +177,57 @@ func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
|
||||||
assert.Equal(t, `debug`, logLevel)
|
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) {
|
func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
|
||||||
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
|
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
|
||||||
cmd := new(cobra.Command)
|
cmd := new(cobra.Command)
|
||||||
|
@ -202,9 +247,7 @@ func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
|
||||||
func TestProcessFlagAliasesScheduleFromEnvironment(t *testing.T) {
|
func TestProcessFlagAliasesScheduleFromEnvironment(t *testing.T) {
|
||||||
cmd := new(cobra.Command)
|
cmd := new(cobra.Command)
|
||||||
|
|
||||||
err := os.Setenv("WATCHTOWER_SCHEDULE", `@hourly`)
|
t.Setenv("WATCHTOWER_SCHEDULE", `@hourly`)
|
||||||
require.NoError(t, err)
|
|
||||||
defer os.Unsetenv("WATCHTOWER_SCHEDULE")
|
|
||||||
|
|
||||||
SetDefaults()
|
SetDefaults()
|
||||||
RegisterDockerFlags(cmd)
|
RegisterDockerFlags(cmd)
|
||||||
|
@ -234,3 +277,63 @@ func TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) {
|
||||||
ProcessFlagAliases(flags)
|
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
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/assert"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSliceEqual_True(t *testing.T) {
|
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, "b": x, "c": x}, m1)
|
||||||
assert.Equal(t, map[string]struct{}{"a": x, "c": x}, m2)
|
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'
|
- 'Stop signals': 'stop-signals.md'
|
||||||
- 'Lifecycle hooks': 'lifecycle-hooks.md'
|
- 'Lifecycle hooks': 'lifecycle-hooks.md'
|
||||||
- 'Running multiple instances': 'running-multiple-instances.md'
|
- 'Running multiple instances': 'running-multiple-instances.md'
|
||||||
|
- 'HTTP API Mode': 'http-api-mode.md'
|
||||||
- 'Metrics': 'metrics.md'
|
- 'Metrics': 'metrics.md'
|
||||||
plugins:
|
plugins:
|
||||||
- search
|
- search
|
||||||
|
|
BIN
oryxBuildBinary
Executable file
BIN
oryxBuildBinary
Executable file
Binary file not shown.
|
@ -2,18 +2,18 @@ package metrics_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/pkg/api"
|
"github.com/containrrr/watchtower/pkg/api"
|
||||||
metricsAPI "github.com/containrrr/watchtower/pkg/api/metrics"
|
metricsAPI "github.com/containrrr/watchtower/pkg/api/metrics"
|
||||||
"github.com/containrrr/watchtower/pkg/metrics"
|
"github.com/containrrr/watchtower/pkg/metrics"
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo"
|
|
||||||
. "github.com/onsi/gomega"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -36,7 +36,7 @@ func getWithToken(handler http.Handler) map[string]string {
|
||||||
handler.ServeHTTP(respWriter, req)
|
handler.ServeHTTP(respWriter, req)
|
||||||
|
|
||||||
res := respWriter.Result()
|
res := respWriter.Result()
|
||||||
body, _ := ioutil.ReadAll(res.Body)
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
|
||||||
for _, line := range strings.Split(string(body), "\n") {
|
for _, line := range strings.Split(string(body), "\n") {
|
||||||
if len(line) < 1 || line[0] == '#' {
|
if len(line) < 1 || line[0] == '#' {
|
||||||
|
|
|
@ -3,14 +3,10 @@ package container
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
@ -18,6 +14,10 @@ import (
|
||||||
sdkClient "github.com/docker/docker/client"
|
sdkClient "github.com/docker/docker/client"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/context"
|
"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"
|
const defaultStopSignal = "SIGTERM"
|
||||||
|
@ -25,23 +25,23 @@ const defaultStopSignal = "SIGTERM"
|
||||||
// A Client is the interface through which watchtower interacts with the
|
// A Client is the interface through which watchtower interacts with the
|
||||||
// Docker API.
|
// Docker API.
|
||||||
type Client interface {
|
type Client interface {
|
||||||
ListContainers(t.Filter) ([]Container, error)
|
ListContainers(t.Filter) ([]t.Container, error)
|
||||||
GetContainer(containerID t.ContainerID) (Container, error)
|
GetContainer(containerID t.ContainerID) (t.Container, error)
|
||||||
StopContainer(Container, time.Duration) error
|
StopContainer(t.Container, time.Duration) error
|
||||||
StartContainer(Container) (t.ContainerID, error)
|
StartContainer(t.Container) (t.ContainerID, error)
|
||||||
RenameContainer(Container, string) error
|
RenameContainer(t.Container, string) error
|
||||||
IsContainerStale(Container) (stale bool, latestImage t.ImageID, err 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)
|
ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error)
|
||||||
RemoveImageByID(t.ImageID) 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
|
// NewClient returns a new Client instance which can be used to interact with
|
||||||
// the Docker API.
|
// the Docker API.
|
||||||
// The client reads its configuration from the following environment variables:
|
// The client reads its configuration from the following environment variables:
|
||||||
// * DOCKER_HOST the docker-engine host to send api requests to
|
// - DOCKER_HOST the docker-engine host to send api requests to
|
||||||
// * DOCKER_TLS_VERIFY whether to verify tls certificates
|
// - DOCKER_TLS_VERIFY whether to verify tls certificates
|
||||||
// * DOCKER_API_VERSION the minimum docker api version to work with
|
// - DOCKER_API_VERSION the minimum docker api version to work with
|
||||||
func NewClient(opts ClientOptions) Client {
|
func NewClient(opts ClientOptions) Client {
|
||||||
cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv)
|
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
|
// ClientOptions contains the options for how the docker client wrapper should behave
|
||||||
type ClientOptions struct {
|
type ClientOptions struct {
|
||||||
PullImages bool
|
|
||||||
RemoveVolumes bool
|
RemoveVolumes bool
|
||||||
IncludeStopped bool
|
IncludeStopped bool
|
||||||
ReviveStopped bool
|
ReviveStopped bool
|
||||||
|
@ -82,7 +81,7 @@ type dockerClient struct {
|
||||||
ClientOptions
|
ClientOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client dockerClient) WarnOnHeadPullFailed(container Container) bool {
|
func (client dockerClient) WarnOnHeadPullFailed(container t.Container) bool {
|
||||||
if client.WarnOnHeadFailed == WarnAlways {
|
if client.WarnOnHeadFailed == WarnAlways {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -93,8 +92,8 @@ func (client dockerClient) WarnOnHeadPullFailed(container Container) bool {
|
||||||
return registry.WarnOnAPIConsumption(container)
|
return registry.WarnOnAPIConsumption(container)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) {
|
func (client dockerClient) ListContainers(fn t.Filter) ([]t.Container, error) {
|
||||||
cs := []Container{}
|
cs := []t.Container{}
|
||||||
bg := context.Background()
|
bg := context.Background()
|
||||||
|
|
||||||
if client.IncludeStopped && client.IncludeRestarting {
|
if client.IncludeStopped && client.IncludeRestarting {
|
||||||
|
@ -149,24 +148,40 @@ func (client dockerClient) createListFilter() filters.Args {
|
||||||
return filterArgs
|
return filterArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client dockerClient) GetContainer(containerID t.ContainerID) (Container, error) {
|
func (client dockerClient) GetContainer(containerID t.ContainerID) (t.Container, error) {
|
||||||
bg := context.Background()
|
bg := context.Background()
|
||||||
|
|
||||||
containerInfo, err := client.api.ContainerInspect(bg, string(containerID))
|
containerInfo, err := client.api.ContainerInspect(bg, string(containerID))
|
||||||
if err != nil {
|
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)
|
imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Failed to retrieve container image info: %v", err)
|
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()
|
bg := context.Background()
|
||||||
signal := c.StopSignal()
|
signal := c.StopSignal()
|
||||||
if signal == "" {
|
if signal == "" {
|
||||||
|
@ -186,7 +201,7 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
|
||||||
// TODO: This should probably be checked.
|
// TODO: This should probably be checked.
|
||||||
_ = client.waitForStopOrTimeout(c, timeout)
|
_ = client.waitForStopOrTimeout(c, timeout)
|
||||||
|
|
||||||
if c.containerInfo.HostConfig.AutoRemove {
|
if c.ContainerInfo().HostConfig.AutoRemove {
|
||||||
log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", shortID)
|
log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", shortID)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("Removing container %s", shortID)
|
log.Debugf("Removing container %s", shortID)
|
||||||
|
@ -208,11 +223,34 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
|
||||||
return nil
|
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()
|
bg := context.Background()
|
||||||
config := c.runtimeConfig()
|
config := c.GetCreateConfig()
|
||||||
hostConfig := c.hostConfig()
|
hostConfig := c.GetCreateHostConfig()
|
||||||
networkConfig := &network.NetworkingConfig{EndpointsConfig: c.containerInfo.NetworkSettings.Networks}
|
networkConfig := client.GetNetworkConfig(c)
|
||||||
|
|
||||||
// simpleNetworkConfig is a networkConfig with only 1 network.
|
// simpleNetworkConfig is a networkConfig with only 1 network.
|
||||||
// see: https://github.com/docker/docker/issues/29265
|
// see: https://github.com/docker/docker/issues/29265
|
||||||
simpleNetworkConfig := func() *network.NetworkingConfig {
|
simpleNetworkConfig := func() *network.NetworkingConfig {
|
||||||
|
@ -228,6 +266,7 @@ func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) {
|
||||||
name := c.Name()
|
name := c.Name()
|
||||||
|
|
||||||
log.Infof("Creating %s", name)
|
log.Infof("Creating %s", name)
|
||||||
|
|
||||||
createdContainer, err := client.api.ContainerCreate(bg, config, hostConfig, simpleNetworkConfig, nil, name)
|
createdContainer, err := client.api.ContainerCreate(bg, config, hostConfig, simpleNetworkConfig, nil, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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()
|
name := c.Name()
|
||||||
|
|
||||||
log.Debugf("Starting container %s (%s)", name, t.ContainerID(creation.ID).ShortID())
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client dockerClient) RenameContainer(c Container, newName string) error {
|
func (client dockerClient) RenameContainer(c t.Container, newName string) error {
|
||||||
bg := context.Background()
|
bg := context.Background()
|
||||||
log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID().ShortID(), newName)
|
log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID().ShortID(), newName)
|
||||||
return client.api.ContainerRename(bg, string(c.ID()), 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()
|
ctx := context.Background()
|
||||||
|
|
||||||
if !client.PullImages {
|
if container.IsNoPull(params) {
|
||||||
log.Debugf("Skipping image pull.")
|
log.Debugf("Skipping image pull.")
|
||||||
} else if err := client.PullImage(ctx, container); err != nil {
|
} else if err := client.PullImage(ctx, container); err != nil {
|
||||||
return false, container.SafeImageID(), err
|
return false, container.SafeImageID(), err
|
||||||
|
@ -289,8 +328,8 @@ func (client dockerClient) IsContainerStale(container Container) (stale bool, la
|
||||||
return client.HasNewImage(ctx, container)
|
return client.HasNewImage(ctx, container)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client dockerClient) HasNewImage(ctx context.Context, container Container) (hasNew bool, latestImage t.ImageID, err error) {
|
func (client dockerClient) HasNewImage(ctx context.Context, container t.Container) (hasNew bool, latestImage t.ImageID, err error) {
|
||||||
currentImageID := t.ImageID(container.containerInfo.ContainerJSONBase.Image)
|
currentImageID := t.ImageID(container.ContainerInfo().ContainerJSONBase.Image)
|
||||||
imageName := container.ImageName()
|
imageName := container.ImageName()
|
||||||
|
|
||||||
newImageInfo, _, err := client.api.ImageInspectWithRaw(ctx, 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
|
// 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
|
// 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()
|
containerName := container.Name()
|
||||||
imageName := container.ImageName()
|
imageName := container.ImageName()
|
||||||
|
|
||||||
|
@ -359,7 +398,7 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e
|
||||||
|
|
||||||
defer response.Close()
|
defer response.Close()
|
||||||
// the pull request will be aborted prematurely unless the response is read
|
// 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)
|
log.Error(err)
|
||||||
return 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 {
|
func (client dockerClient) RemoveImageByID(id t.ImageID) error {
|
||||||
log.Infof("Removing image %s", id.ShortID())
|
log.Infof("Removing image %s", id.ShortID())
|
||||||
|
|
||||||
_, err := client.api.ImageRemove(
|
items, err := client.api.ImageRemove(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
string(id),
|
string(id),
|
||||||
types.ImageRemoveOptions{
|
types.ImageRemoveOptions{
|
||||||
Force: true,
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -478,7 +538,7 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e
|
||||||
return false, nil
|
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()
|
bg := context.Background()
|
||||||
timeout := time.After(waitTime)
|
timeout := time.After(waitTime)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/containrrr/watchtower/internal/util"
|
||||||
"github.com/containrrr/watchtower/pkg/container/mocks"
|
"github.com/containrrr/watchtower/pkg/container/mocks"
|
||||||
"github.com/containrrr/watchtower/pkg/filters"
|
"github.com/containrrr/watchtower/pkg/filters"
|
||||||
t "github.com/containrrr/watchtower/pkg/types"
|
t "github.com/containrrr/watchtower/pkg/types"
|
||||||
|
@ -10,6 +12,7 @@ import (
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/backend"
|
"github.com/docker/docker/api/types/backend"
|
||||||
cli "github.com/docker/docker/client"
|
cli "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/onsi/gomega/gbytes"
|
"github.com/onsi/gomega/gbytes"
|
||||||
"github.com/onsi/gomega/ghttp"
|
"github.com/onsi/gomega/ghttp"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -35,8 +38,8 @@ var _ = Describe("the client", func() {
|
||||||
mockServer.Close()
|
mockServer.Close()
|
||||||
})
|
})
|
||||||
Describe("WarnOnHeadPullFailed", func() {
|
Describe("WarnOnHeadPullFailed", func() {
|
||||||
containerUnknown := *MockContainer(WithImageName("unknown.repo/prefix/imagename:latest"))
|
containerUnknown := MockContainer(WithImageName("unknown.repo/prefix/imagename:latest"))
|
||||||
containerKnown := *MockContainer(WithImageName("docker.io/prefix/imagename:latest"))
|
containerKnown := MockContainer(WithImageName("docker.io/prefix/imagename:latest"))
|
||||||
|
|
||||||
When(`warn on head failure is set to "always"`, func() {
|
When(`warn on head failure is set to "always"`, func() {
|
||||||
c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}}
|
c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}}
|
||||||
|
@ -66,16 +69,17 @@ var _ = Describe("the client", func() {
|
||||||
When("the image consist of a pinned hash", func() {
|
When("the image consist of a pinned hash", func() {
|
||||||
It("should gracefully fail with a useful message", func() {
|
It("should gracefully fail with a useful message", func() {
|
||||||
c := dockerClient{}
|
c := dockerClient{}
|
||||||
pinnedContainer := *MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b"))
|
pinnedContainer := MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b"))
|
||||||
c.PullImage(context.Background(), pinnedContainer)
|
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("removing a running container", func() {
|
||||||
When("the container still exist after stopping", func() {
|
When("the container still exist after stopping", func() {
|
||||||
It("should attempt to remove the container", func() {
|
It("should attempt to remove the container", func() {
|
||||||
container := *MockContainer(WithContainerState(types.ContainerState{Running: true}))
|
container := MockContainer(WithContainerState(types.ContainerState{Running: true}))
|
||||||
containerStopped := *MockContainer(WithContainerState(types.ContainerState{Running: false}))
|
containerStopped := MockContainer(WithContainerState(types.ContainerState{Running: false}))
|
||||||
|
|
||||||
cid := container.ContainerInfo().ID
|
cid := container.ContainerInfo().ID
|
||||||
mockServer.AppendHandlers(
|
mockServer.AppendHandlers(
|
||||||
|
@ -90,7 +94,7 @@ var _ = Describe("the client", func() {
|
||||||
})
|
})
|
||||||
When("the container does not exist after stopping", func() {
|
When("the container does not exist after stopping", func() {
|
||||||
It("should not cause an error", 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
|
cid := container.ContainerInfo().ID
|
||||||
mockServer.AppendHandlers(
|
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("listing containers", func() {
|
||||||
When("no filter is provided", func() {
|
When("no filter is provided", func() {
|
||||||
It("should return all available containers", func() {
|
It("should return all available containers", func() {
|
||||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
||||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
|
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
|
||||||
client := dockerClient{
|
client := dockerClient{
|
||||||
api: docker,
|
api: docker,
|
||||||
ClientOptions: ClientOptions{PullImages: false},
|
ClientOptions: ClientOptions{},
|
||||||
}
|
}
|
||||||
containers, err := client.ListContainers(filters.NoFilter)
|
containers, err := client.ListContainers(filters.NoFilter)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
@ -120,11 +155,11 @@ var _ = Describe("the client", func() {
|
||||||
When("a filter matching nothing", func() {
|
When("a filter matching nothing", func() {
|
||||||
It("should return an empty array", func() {
|
It("should return an empty array", func() {
|
||||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
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)
|
filter := filters.FilterByNames([]string{"lollercoaster"}, filters.NoFilter)
|
||||||
client := dockerClient{
|
client := dockerClient{
|
||||||
api: docker,
|
api: docker,
|
||||||
ClientOptions: ClientOptions{PullImages: false},
|
ClientOptions: ClientOptions{},
|
||||||
}
|
}
|
||||||
containers, err := client.ListContainers(filter)
|
containers, err := client.ListContainers(filter)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
@ -134,10 +169,10 @@ var _ = Describe("the client", func() {
|
||||||
When("a watchtower filter is provided", func() {
|
When("a watchtower filter is provided", func() {
|
||||||
It("should return only the watchtower container", func() {
|
It("should return only the watchtower container", func() {
|
||||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
||||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
|
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
|
||||||
client := dockerClient{
|
client := dockerClient{
|
||||||
api: docker,
|
api: docker,
|
||||||
ClientOptions: ClientOptions{PullImages: false},
|
ClientOptions: ClientOptions{},
|
||||||
}
|
}
|
||||||
containers, err := client.ListContainers(filters.WatchtowerContainersFilter)
|
containers, err := client.ListContainers(filters.WatchtowerContainersFilter)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
@ -147,10 +182,10 @@ var _ = Describe("the client", func() {
|
||||||
When(`include stopped is enabled`, func() {
|
When(`include stopped is enabled`, func() {
|
||||||
It("should return both stopped and running containers", func() {
|
It("should return both stopped and running containers", func() {
|
||||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running", "exited", "created"))
|
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{
|
client := dockerClient{
|
||||||
api: docker,
|
api: docker,
|
||||||
ClientOptions: ClientOptions{PullImages: false, IncludeStopped: true},
|
ClientOptions: ClientOptions{IncludeStopped: true},
|
||||||
}
|
}
|
||||||
containers, err := client.ListContainers(filters.NoFilter)
|
containers, err := client.ListContainers(filters.NoFilter)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
@ -160,10 +195,10 @@ var _ = Describe("the client", func() {
|
||||||
When(`include restarting is enabled`, func() {
|
When(`include restarting is enabled`, func() {
|
||||||
It("should return both restarting and running containers", func() {
|
It("should return both restarting and running containers", func() {
|
||||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running", "restarting"))
|
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{
|
client := dockerClient{
|
||||||
api: docker,
|
api: docker,
|
||||||
ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: true},
|
ClientOptions: ClientOptions{IncludeRestarting: true},
|
||||||
}
|
}
|
||||||
containers, err := client.ListContainers(filters.NoFilter)
|
containers, err := client.ListContainers(filters.NoFilter)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
@ -173,30 +208,58 @@ var _ = Describe("the client", func() {
|
||||||
When(`include restarting is disabled`, func() {
|
When(`include restarting is disabled`, func() {
|
||||||
It("should not return restarting containers", func() {
|
It("should not return restarting containers", func() {
|
||||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
||||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
|
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
|
||||||
client := dockerClient{
|
client := dockerClient{
|
||||||
api: docker,
|
api: docker,
|
||||||
ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: false},
|
ClientOptions: ClientOptions{IncludeRestarting: false},
|
||||||
}
|
}
|
||||||
containers, err := client.ListContainers(filters.NoFilter)
|
containers, err := client.ListContainers(filters.NoFilter)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(containers).NotTo(ContainElement(havingRestartingState(true)))
|
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() {
|
Describe(`ExecuteCommand`, func() {
|
||||||
When(`logging`, func() {
|
When(`logging`, func() {
|
||||||
It("should include container id field", func() {
|
It("should include container id field", func() {
|
||||||
client := dockerClient{
|
client := dockerClient{
|
||||||
api: docker,
|
api: docker,
|
||||||
ClientOptions: ClientOptions{PullImages: false},
|
ClientOptions: ClientOptions{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture logrus output in buffer
|
// Capture logrus output in buffer
|
||||||
logbuf := gbytes.NewBuffer()
|
resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
|
||||||
origOut := logrus.StandardLogger().Out
|
defer resetLogrus()
|
||||||
defer logrus.SetOutput(origOut)
|
|
||||||
logrus.SetOutput(logbuf)
|
|
||||||
|
|
||||||
user := ""
|
user := ""
|
||||||
containerID := t.ContainerID("ex-cont-id")
|
containerID := t.ContainerID("ex-cont-id")
|
||||||
|
@ -253,26 +316,62 @@ 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
|
// Gomega matcher helpers
|
||||||
|
|
||||||
func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher {
|
func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher {
|
||||||
return WithTransform(containerImageName, matcher)
|
return WithTransform(containerImageName, matcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
func containerImageName(container Container) string {
|
func containerImageName(container t.Container) string {
|
||||||
return container.ImageName()
|
return container.ImageName()
|
||||||
}
|
}
|
||||||
|
|
||||||
func havingRestartingState(expected bool) gt.GomegaMatcher {
|
func havingRestartingState(expected bool) gt.GomegaMatcher {
|
||||||
return WithTransform(func(container Container) bool {
|
return WithTransform(func(container t.Container) bool {
|
||||||
return container.containerInfo.State.Restarting
|
return container.ContainerInfo().State.Restarting
|
||||||
}, Equal(expected))
|
}, Equal(expected))
|
||||||
}
|
}
|
||||||
|
|
||||||
func havingRunningState(expected bool) gt.GomegaMatcher {
|
func havingRunningState(expected bool) gt.GomegaMatcher {
|
||||||
return WithTransform(func(container Container) bool {
|
return WithTransform(func(container t.Container) bool {
|
||||||
return container.containerInfo.State.Running
|
return container.ContainerInfo().State.Running
|
||||||
}, Equal(expected))
|
}, Equal(expected))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,14 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/internal/util"
|
"github.com/containrrr/watchtower/internal/util"
|
||||||
wt "github.com/containrrr/watchtower/pkg/types"
|
wt "github.com/containrrr/watchtower/pkg/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
dockercontainer "github.com/docker/docker/api/types/container"
|
dockercontainer "github.com/docker/docker/api/types/container"
|
||||||
|
@ -32,6 +34,26 @@ type Container struct {
|
||||||
imageInfo *types.ImageInspect
|
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
|
// ContainerInfo fetches JSON info for the container
|
||||||
func (c Container) ContainerInfo() *types.ContainerJSON {
|
func (c Container) ContainerInfo() *types.ContainerJSON {
|
||||||
return c.containerInfo
|
return c.containerInfo
|
||||||
|
@ -109,20 +131,31 @@ func (c Container) Enabled() (bool, bool) {
|
||||||
return parsedBool, true
|
return parsedBool, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsMonitorOnly returns the value of the monitor-only label. If the label
|
// IsMonitorOnly returns whether the container should only be monitored based on values of
|
||||||
// is not set then false is returned.
|
// the monitor-only label, the monitor-only argument and the label-take-precedence argument.
|
||||||
func (c Container) IsMonitorOnly() bool {
|
func (c Container) IsMonitorOnly(params wt.UpdateParams) bool {
|
||||||
rawBool, ok := c.getLabelValue(monitorOnlyLabel)
|
return c.getContainerOrGlobalBool(params.MonitorOnly, monitorOnlyLabel, params.LabelPrecedence)
|
||||||
if !ok {
|
}
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedBool, err := strconv.ParseBool(rawBool)
|
// IsNoPull returns whether the image should be pulled based on values of
|
||||||
if err != nil {
|
// the no-pull label, the no-pull argument and the label-take-precedence argument.
|
||||||
return false
|
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
|
// 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)
|
dependsOnLabelValue := c.getLabelValueOrEmpty(dependsOnLabel)
|
||||||
|
|
||||||
if dependsOnLabelValue != "" {
|
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
|
return links
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,6 +193,13 @@ func (c Container) Links() []string {
|
||||||
name := strings.Split(link, ":")[0]
|
name := strings.Split(link, ":")[0]
|
||||||
links = append(links, name)
|
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
|
return links
|
||||||
|
@ -217,18 +264,23 @@ func (c Container) StopSignal() string {
|
||||||
return c.getLabelValueOrEmpty(signalLabel)
|
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
|
// 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,
|
// 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
|
// the ContainerConfig that comes back from the Inspect call merges the default
|
||||||
// configuration (the stuff specified in the metadata for the image itself)
|
// configuration (the stuff specified in the metadata for the image itself)
|
||||||
// with the overridden configuration (the stuff that you might specify as part
|
// 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
|
// 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
|
// default options. To do this we have to compare the ContainerConfig for the
|
||||||
// running container with the ContainerConfig from the image that container was
|
// running container with the ContainerConfig from the image that container was
|
||||||
// started from. This function returns a ContainerConfig which contains just
|
// started from. This function returns a ContainerConfig which contains just
|
||||||
// the options overridden at runtime.
|
// the options overridden at runtime.
|
||||||
func (c Container) runtimeConfig() *dockercontainer.Config {
|
func (c Container) GetCreateConfig() *dockercontainer.Config {
|
||||||
config := c.containerInfo.Config
|
config := c.containerInfo.Config
|
||||||
hostConfig := c.containerInfo.HostConfig
|
hostConfig := c.containerInfo.HostConfig
|
||||||
imageConfig := c.imageInfo.Config
|
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.Env = util.SliceSubtract(config.Env, imageConfig.Env)
|
||||||
|
|
||||||
config.Labels = util.StringMapSubtract(config.Labels, imageConfig.Labels)
|
config.Labels = util.StringMapSubtract(config.Labels, imageConfig.Labels)
|
||||||
|
@ -272,9 +347,9 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any links in the HostConfig need to be re-written before they can be
|
// GetCreateHostConfig returns the container's current HostConfig with any links
|
||||||
// re-submitted to the Docker create API.
|
// re-written so that they can be re-submitted to the Docker create API.
|
||||||
func (c Container) hostConfig() *dockercontainer.HostConfig {
|
func (c Container) GetCreateHostConfig() *dockercontainer.HostConfig {
|
||||||
hostConfig := c.containerInfo.HostConfig
|
hostConfig := c.containerInfo.HostConfig
|
||||||
|
|
||||||
for i, link := range hostConfig.Links {
|
for i, link := range hostConfig.Links {
|
||||||
|
|
|
@ -21,7 +21,8 @@ func MockContainer(updates ...MockContainerUpdate) *Container {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
image := types.ImageInspect{
|
image := types.ImageInspect{
|
||||||
ID: "image_id",
|
ID: "image_id",
|
||||||
|
Config: &dockerContainer.Config{},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, update := range updates {
|
for _, update := range updates {
|
||||||
|
@ -64,3 +65,15 @@ func WithContainerState(state types.ContainerState) MockContainerUpdate {
|
||||||
cnt.State = &state
|
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
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
|
dc "github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "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() {
|
When("asked for metadata", func() {
|
||||||
var c *Container
|
var c *Container
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
|
@ -178,14 +267,21 @@ var _ = Describe("the container", func() {
|
||||||
"com.centurylinklabs.watchtower.depends-on": "postgres",
|
"com.centurylinklabs.watchtower.depends-on": "postgres",
|
||||||
}))
|
}))
|
||||||
links := c.Links()
|
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() {
|
It("should fetch depending containers if there are many", func() {
|
||||||
c = MockContainer(WithLabels(map[string]string{
|
c = MockContainer(WithLabels(map[string]string{
|
||||||
"com.centurylinklabs.watchtower.depends-on": "postgres,redis",
|
"com.centurylinklabs.watchtower.depends-on": "postgres,redis",
|
||||||
}))
|
}))
|
||||||
links := c.Links()
|
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() {
|
It("should fetch depending containers if label is blank", func() {
|
||||||
c = MockContainer(WithLabels(map[string]string{
|
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() {
|
When("there is a pre or post update timeout", func() {
|
||||||
It("should return minute values", func() {
|
It("should return minute values", func() {
|
||||||
c = MockContainer(WithLabels(map[string]string{
|
c = MockContainer(WithLabels(map[string]string{
|
||||||
|
|
|
@ -5,3 +5,4 @@ import "errors"
|
||||||
var errorNoImageInfo = errors.New("no available image info")
|
var errorNoImageInfo = errors.New("no available image info")
|
||||||
var errorNoContainerInfo = errors.New("no available container info")
|
var errorNoContainerInfo = errors.New("no available container info")
|
||||||
var errorInvalidConfig = errors.New("container configuration missing or invalid")
|
var errorInvalidConfig = errors.New("container configuration missing or invalid")
|
||||||
|
var errorLabelNotFound = errors.New("label was not found in container")
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
watchtowerLabel = "com.centurylinklabs.watchtower"
|
watchtowerLabel = "com.centurylinklabs.watchtower"
|
||||||
signalLabel = "com.centurylinklabs.watchtower.stop-signal"
|
signalLabel = "com.centurylinklabs.watchtower.stop-signal"
|
||||||
enableLabel = "com.centurylinklabs.watchtower.enable"
|
enableLabel = "com.centurylinklabs.watchtower.enable"
|
||||||
monitorOnlyLabel = "com.centurylinklabs.watchtower.monitor-only"
|
monitorOnlyLabel = "com.centurylinklabs.watchtower.monitor-only"
|
||||||
dependsOnLabel = "com.centurylinklabs.watchtower.depends-on"
|
noPullLabel = "com.centurylinklabs.watchtower.no-pull"
|
||||||
zodiacLabel = "com.centurylinklabs.zodiac.original-image"
|
dependsOnLabel = "com.centurylinklabs.watchtower.depends-on"
|
||||||
scope = "com.centurylinklabs.watchtower.scope"
|
zodiacLabel = "com.centurylinklabs.zodiac.original-image"
|
||||||
preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check"
|
scope = "com.centurylinklabs.watchtower.scope"
|
||||||
postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check"
|
preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check"
|
||||||
preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update"
|
postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check"
|
||||||
postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update"
|
preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update"
|
||||||
preUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout"
|
postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update"
|
||||||
|
preUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout"
|
||||||
postUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.post-update-timeout"
|
postUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.post-update-timeout"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,3 +57,11 @@ func (c Container) getLabelValue(label string) (string, bool) {
|
||||||
val, ok := c.containerInfo.Config.Labels[label]
|
val, ok := c.containerInfo.Config.Labels[label]
|
||||||
return val, ok
|
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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"github.com/onsi/ginkgo"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
t "github.com/containrrr/watchtower/pkg/types"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
@ -16,10 +20,9 @@ import (
|
||||||
|
|
||||||
func getMockJSONFile(relPath string) ([]byte, error) {
|
func getMockJSONFile(relPath string) ([]byte, error) {
|
||||||
absPath, _ := filepath.Abs(relPath)
|
absPath, _ := filepath.Abs(relPath)
|
||||||
buf, err := ioutil.ReadFile(absPath)
|
buf, err := os.ReadFile(absPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// logrus.WithError(err).WithField("file", absPath).Error(err)
|
return nil, fmt.Errorf("mock JSON file %q not found: %e", absPath, err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return buf, nil
|
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
|
// GetContainerHandlers returns the handlers serving lookups for the supplied container mock files
|
||||||
func GetContainerHandlers(containerFiles ...string) []http.HandlerFunc {
|
func GetContainerHandlers(containerRefs ...*ContainerRef) []http.HandlerFunc {
|
||||||
handlers := make([]http.HandlerFunc, 0, len(containerFiles)*2)
|
handlers := make([]http.HandlerFunc, 0, len(containerRefs)*3)
|
||||||
for _, file := range containerFiles {
|
for _, containerRef := range containerRefs {
|
||||||
handlers = append(handlers, getContainerFileHandler(file))
|
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
|
// Also append the image request since that will be called for every container
|
||||||
if file == "running" {
|
handlers = append(handlers, getImageHandler(containerRef.image.id,
|
||||||
// The "running" container is the only one using image02
|
RespondWithJSONFile(containerRef.image.getFileName(), http.StatusOK),
|
||||||
handlers = append(handlers, getImageFileHandler(1))
|
))
|
||||||
} else {
|
|
||||||
handlers = append(handlers, getImageFileHandler(0))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return handlers
|
return handlers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,24 +70,90 @@ func createFilterArgs(statuses []string) filters.Args {
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
var containerFileIds = map[string]string{
|
var defaultImage = imageRef{
|
||||||
"stopped": "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65",
|
// watchtower
|
||||||
"watchtower": "3d88e0e3543281c747d88b27e246578b65ae8964ba86c7cd7522cf84e0978134",
|
id: t.ImageID("sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa"),
|
||||||
"running": "b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008",
|
file: "default",
|
||||||
"restarting": "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b67",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageIds = []string{
|
var Watchtower = ContainerRef{
|
||||||
"sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa",
|
name: "watchtower",
|
||||||
"sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd",
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
func getContainerFileHandler(file string) http.HandlerFunc {
|
var netSupplierOK = ContainerRef{
|
||||||
id, ok := containerFileIds[file]
|
id: "25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2",
|
||||||
failTestUnless(ok)
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
return getContainerHandler(
|
return getContainerHandler(
|
||||||
id,
|
string(cr.id),
|
||||||
RespondWithJSONFile(fmt.Sprintf("./mocks/data/container_%v.json", file), http.StatusOK),
|
RespondWithJSONFile(containerFile, http.StatusOK),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +175,7 @@ func GetContainerHandler(containerID string, containerInfo *types.ContainerJSON)
|
||||||
|
|
||||||
// GetImageHandler mocks the GET images/{id}/json endpoint
|
// GetImageHandler mocks the GET images/{id}/json endpoint
|
||||||
func GetImageHandler(imageInfo *types.ImageInspect) http.HandlerFunc {
|
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
|
// 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()
|
bytes, err := filterArgs.MarshalJSON()
|
||||||
O.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred())
|
O.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred())
|
||||||
query := url.Values{
|
query := url.Values{
|
||||||
"limit": []string{"0"},
|
|
||||||
"filters": []string{string(bytes)},
|
"filters": []string{string(bytes)},
|
||||||
}
|
}
|
||||||
return ghttp.CombineHandlers(
|
return ghttp.CombineHandlers(
|
||||||
|
@ -138,23 +209,13 @@ func respondWithFilteredContainers(filters filters.Args) http.HandlerFunc {
|
||||||
return ghttp.RespondWithJSONEncoded(http.StatusOK, filteredContainers)
|
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(
|
return ghttp.CombineHandlers(
|
||||||
ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%s/json", imageId)),
|
ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%s/json", imageId)),
|
||||||
responseHandler,
|
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
|
// KillContainerHandler mocks the POST containers/{id}/kill endpoint
|
||||||
func KillContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
|
func KillContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
|
||||||
responseHandler := noContentStatusResponse
|
responseHandler := noContentStatusResponse
|
||||||
|
@ -180,7 +241,7 @@ func RemoveContainerHandler(containerID string, found FoundStatus) http.HandlerF
|
||||||
}
|
}
|
||||||
|
|
||||||
func containerNotFoundResponse(containerID string) http.HandlerFunc {
|
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)
|
var noContentStatusResponse = ghttp.RespondWith(http.StatusNoContent, nil)
|
||||||
|
@ -191,3 +252,29 @@ const (
|
||||||
Found FoundStatus = true
|
Found FoundStatus = true
|
||||||
Missing FoundStatus = false
|
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
|
// NoFilter will not filter out any containers
|
||||||
func NoFilter(t.FilterableContainer) bool { return true }
|
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 {
|
func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
|
||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
return baseFilter
|
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
|
// FilterByEnableLabel returns all containers that have the enabled label set
|
||||||
func FilterByEnableLabel(baseFilter t.Filter) t.Filter {
|
func FilterByEnableLabel(baseFilter t.Filter) t.Filter {
|
||||||
return func(c t.FilterableContainer) bool {
|
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
|
// FilterByScope returns all containers that belongs to a specific scope
|
||||||
func FilterByScope(scope string, baseFilter t.Filter) t.Filter {
|
func FilterByScope(scope string, baseFilter t.Filter) t.Filter {
|
||||||
if scope == "" {
|
|
||||||
return baseFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(c t.FilterableContainer) bool {
|
return func(c t.FilterableContainer) bool {
|
||||||
containerScope, ok := c.Scope()
|
containerScope, containerHasScope := c.Scope()
|
||||||
if ok && containerScope == scope {
|
|
||||||
|
if !containerHasScope || containerScope == "" {
|
||||||
|
containerScope = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
if containerScope == scope {
|
||||||
return baseFilter(c)
|
return baseFilter(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,10 +120,11 @@ func FilterByImage(images []string, baseFilter t.Filter) t.Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildFilter creates the needed filter of containers
|
// 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{}
|
sb := strings.Builder{}
|
||||||
filter := NoFilter
|
filter := NoFilter
|
||||||
filter = FilterByNames(names, filter)
|
filter = FilterByNames(names, filter)
|
||||||
|
filter = FilterByDisableNames(disableNames, filter)
|
||||||
|
|
||||||
if len(names) > 0 {
|
if len(names) > 0 {
|
||||||
sb.WriteString("which name matches \"")
|
sb.WriteString("which name matches \"")
|
||||||
|
@ -118,6 +136,16 @@ func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, stri
|
||||||
}
|
}
|
||||||
sb.WriteString(`", `)
|
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 enableLabel {
|
||||||
// If label filtering is enabled, containers should only be considered
|
// 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)
|
filter = FilterByEnableLabel(filter)
|
||||||
sb.WriteString("using enable label, ")
|
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 a scope has been defined, containers should only be considered
|
||||||
// if the scope is specifically set.
|
// if the scope is specifically set.
|
||||||
filter = FilterByScope(scope, filter)
|
filter = FilterByScope(scope, filter)
|
||||||
|
|
|
@ -111,6 +111,53 @@ func TestFilterByScope(t *testing.T) {
|
||||||
container.AssertExpectations(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) {
|
func TestFilterByDisabledLabel(t *testing.T) {
|
||||||
filter := FilterByDisabledLabel(NoFilter)
|
filter := FilterByDisabledLabel(NoFilter)
|
||||||
assert.NotNil(t, filter)
|
assert.NotNil(t, filter)
|
||||||
|
@ -171,7 +218,7 @@ func TestFilterByImage(t *testing.T) {
|
||||||
func TestBuildFilter(t *testing.T) {
|
func TestBuildFilter(t *testing.T) {
|
||||||
names := []string{"test", "valid"}
|
names := []string{"test", "valid"}
|
||||||
|
|
||||||
filter, desc := BuildFilter(names, false, "")
|
filter, desc := BuildFilter(names, []string{}, false, "")
|
||||||
assert.Contains(t, desc, "test")
|
assert.Contains(t, desc, "test")
|
||||||
assert.Contains(t, desc, "or")
|
assert.Contains(t, desc, "or")
|
||||||
assert.Contains(t, desc, "valid")
|
assert.Contains(t, desc, "valid")
|
||||||
|
@ -210,7 +257,7 @@ func TestBuildFilterEnableLabel(t *testing.T) {
|
||||||
var names []string
|
var names []string
|
||||||
names = append(names, "test")
|
names = append(names, "test")
|
||||||
|
|
||||||
filter, desc := BuildFilter(names, true, "")
|
filter, desc := BuildFilter(names, []string{}, true, "")
|
||||||
assert.Contains(t, desc, "using enable label")
|
assert.Contains(t, desc, "using enable label")
|
||||||
|
|
||||||
container := new(mocks.FilterableContainer)
|
container := new(mocks.FilterableContainer)
|
||||||
|
@ -235,3 +282,52 @@ func TestBuildFilterEnableLabel(t *testing.T) {
|
||||||
assert.False(t, filter(container))
|
assert.False(t, filter(container))
|
||||||
container.AssertExpectations(t)
|
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.
|
// 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())
|
clog := log.WithField("container", container.Name())
|
||||||
command := container.GetLifecyclePreCheckCommand()
|
command := container.GetLifecyclePreCheckCommand()
|
||||||
if len(command) == 0 {
|
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.
|
// 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())
|
clog := log.WithField("container", container.Name())
|
||||||
command := container.GetLifecyclePostCheckCommand()
|
command := container.GetLifecyclePostCheckCommand()
|
||||||
if len(command) == 0 {
|
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.
|
// 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()
|
timeout := container.PreUpdateTimeout()
|
||||||
command := container.GetLifecyclePreUpdateCommand()
|
command := container.GetLifecyclePreUpdateCommand()
|
||||||
clog := log.WithField("container", container.Name())
|
clog := log.WithField("container", container.Name())
|
||||||
|
|
|
@ -59,13 +59,3 @@ func marshalReports(reports []t.ContainerReport) []jsonMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ json.Marshaler = &Data{}
|
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"
|
||||||
"github.com/containrrr/shoutrrr/pkg/types"
|
"github.com/containrrr/shoutrrr/pkg/types"
|
||||||
|
"github.com/containrrr/watchtower/pkg/notifications/templates"
|
||||||
t "github.com/containrrr/watchtower/pkg/types"
|
t "github.com/containrrr/watchtower/pkg/types"
|
||||||
log "github.com/sirupsen/logrus"
|
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
|
// LocalLog is a logrus logger that does not send entries as notifications
|
||||||
|
@ -61,7 +60,7 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
|
||||||
return names
|
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 {
|
func (n *shoutrrrTypeNotifier) GetURLs() []string {
|
||||||
return n.Urls
|
return n.Urls
|
||||||
}
|
}
|
||||||
|
@ -74,7 +73,7 @@ func (n *shoutrrrTypeNotifier) AddLogHook() {
|
||||||
n.receiving = true
|
n.receiving = true
|
||||||
log.AddHook(n)
|
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)
|
go sendNotifications(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +109,7 @@ func createNotifier(urls []string, level log.Level, tplString string, legacy boo
|
||||||
legacyTemplate: legacy,
|
legacyTemplate: legacy,
|
||||||
data: data,
|
data: data,
|
||||||
params: params,
|
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) {
|
func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, err error) {
|
||||||
funcs := template.FuncMap{
|
|
||||||
"ToUpper": strings.ToUpper,
|
tplBase := template.New("").Funcs(templates.Funcs)
|
||||||
"ToLower": strings.ToLower,
|
|
||||||
"ToJSON": toJSON,
|
|
||||||
"Title": cases.Title(language.AmericanEnglish).String,
|
|
||||||
}
|
|
||||||
tplBase := template.New("").Funcs(funcs)
|
|
||||||
|
|
||||||
if builtin, found := commonTemplates[tplString]; found {
|
if builtin, found := commonTemplates[tplString]; found {
|
||||||
log.WithField(`template`, tplString).Debug(`Using common template`)
|
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"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
||||||
"github.com/containrrr/watchtower/pkg/types"
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
"github.com/docker/distribution/reference"
|
ref "github.com/distribution/reference"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,13 +20,13 @@ const ChallengeHeader = "WWW-Authenticate"
|
||||||
|
|
||||||
// GetToken fetches a token for the registry hosting the provided image
|
// GetToken fetches a token for the registry hosting the provided image
|
||||||
func GetToken(container types.Container, registryAuth string) (string, error) {
|
func GetToken(container types.Container, registryAuth string) (string, error) {
|
||||||
var err error
|
normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName())
|
||||||
var URL url.URL
|
if err != nil {
|
||||||
|
|
||||||
if URL, err = GetChallengeURL(container.ImageName()); err != nil {
|
|
||||||
return "", err
|
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
|
var req *http.Request
|
||||||
if req, err = GetChallengeRequest(URL); err != nil {
|
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
|
return fmt.Sprintf("Basic %s", registryAuth), nil
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(challenge, "bearer") {
|
if strings.HasPrefix(challenge, "bearer") {
|
||||||
return GetBearerHeader(challenge, container.ImageName(), registryAuth)
|
return GetBearerHeader(challenge, normalizedRef, registryAuth)
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", errors.New("unsupported challenge type from registry")
|
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
|
// 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{}
|
client := http.Client{}
|
||||||
if strings.Contains(img, ":") {
|
authURL, err := GetAuthURL(challenge, imageRef)
|
||||||
img = strings.Split(img, ":")[0]
|
|
||||||
}
|
|
||||||
authURL, err := GetAuthURL(challenge, img)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -91,7 +88,8 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string,
|
||||||
|
|
||||||
if registryAuth != "" {
|
if registryAuth != "" {
|
||||||
logrus.Debug("Credentials found.")
|
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))
|
r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth))
|
||||||
} else {
|
} else {
|
||||||
logrus.Debug("No credentials found.")
|
logrus.Debug("No credentials found.")
|
||||||
|
@ -102,7 +100,7 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string,
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
body, _ := ioutil.ReadAll(authResponse.Body)
|
body, _ := io.ReadAll(authResponse.Body)
|
||||||
tokenResponse := &types.TokenResponse{}
|
tokenResponse := &types.TokenResponse{}
|
||||||
|
|
||||||
err = json.Unmarshal(body, 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
|
// 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)
|
loweredChallenge := strings.ToLower(challenge)
|
||||||
raw := strings.TrimPrefix(loweredChallenge, "bearer")
|
raw := strings.TrimPrefix(loweredChallenge, "bearer")
|
||||||
|
|
||||||
|
@ -123,10 +121,9 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) {
|
||||||
|
|
||||||
for _, pair := range pairs {
|
for _, pair := range pairs {
|
||||||
trimmed := strings.Trim(pair, " ")
|
trimmed := strings.Trim(pair, " ")
|
||||||
kv := strings.Split(trimmed, "=")
|
if key, val, ok := strings.Cut(trimmed, "="); ok {
|
||||||
key := kv[0]
|
values[key] = strings.Trim(val, `"`)
|
||||||
val := strings.Trim(kv[1], "\"")
|
}
|
||||||
values[key] = val
|
|
||||||
}
|
}
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"realm": values["realm"],
|
"realm": values["realm"],
|
||||||
|
@ -141,53 +138,25 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) {
|
||||||
q := authURL.Query()
|
q := authURL.Query()
|
||||||
q.Add("service", values["service"])
|
q.Add("service", values["service"])
|
||||||
|
|
||||||
scopeImage := GetScopeFromImageName(img, values["service"])
|
scopeImage := ref.Path(imageRef)
|
||||||
|
|
||||||
scope := fmt.Sprintf("repository:%s:pull", scopeImage)
|
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)
|
q.Add("scope", scope)
|
||||||
|
|
||||||
authURL.RawQuery = q.Encode()
|
authURL.RawQuery = q.Encode()
|
||||||
return authURL, nil
|
return authURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetScopeFromImageName normalizes an image name for use as scope during auth and head requests
|
// GetChallengeURL returns the URL to check auth requirements
|
||||||
func GetScopeFromImageName(img, svc string) string {
|
// for access to a given image
|
||||||
parts := strings.Split(img, "/")
|
func GetChallengeURL(imageRef ref.Named) url.URL {
|
||||||
|
host, _ := helpers.GetRegistryAddress(imageRef.Name())
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
URL := url.URL{
|
URL := url.URL{
|
||||||
Scheme: "https",
|
Scheme: "https",
|
||||||
Host: host,
|
Host: host,
|
||||||
Path: "/v2/",
|
Path: "/v2/",
|
||||||
}
|
}
|
||||||
return URL, nil
|
return URL
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,17 @@ package auth_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/containrrr/watchtower/internal/actions/mocks"
|
|
||||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/containrrr/watchtower/internal/actions/mocks"
|
||||||
|
"github.com/containrrr/watchtower/pkg/registry/auth"
|
||||||
|
|
||||||
wtTypes "github.com/containrrr/watchtower/pkg/types"
|
wtTypes "github.com/containrrr/watchtower/pkg/types"
|
||||||
|
ref "github.com/distribution/reference"
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
|
@ -51,7 +54,7 @@ var _ = Describe("the auth module", func() {
|
||||||
mockCreated,
|
mockCreated,
|
||||||
mockDigest)
|
mockDigest)
|
||||||
|
|
||||||
When("getting an auth url", func() {
|
Describe("GetToken", func() {
|
||||||
It("should parse the token from the response",
|
It("should parse the token from the response",
|
||||||
SkipIfCredentialsEmpty(GHCRCredentials, func() {
|
SkipIfCredentialsEmpty(GHCRCredentials, func() {
|
||||||
creds := fmt.Sprintf("%s:%s", GHCRCredentials.Username, GHCRCredentials.Password)
|
creds := fmt.Sprintf("%s:%s", GHCRCredentials.Username, GHCRCredentials.Password)
|
||||||
|
@ -60,61 +63,100 @@ var _ = Describe("the auth module", func() {
|
||||||
Expect(token).NotTo(Equal(""))
|
Expect(token).NotTo(Equal(""))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("GetAuthURL", func() {
|
||||||
It("should create a valid auth url object based on the challenge header supplied", 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{
|
expected := &url.URL{
|
||||||
Host: "ghcr.io",
|
Host: "ghcr.io",
|
||||||
Scheme: "https",
|
Scheme: "https",
|
||||||
Path: "/token",
|
Path: "/token",
|
||||||
RawQuery: "scope=repository%3Acontainrrr%2Fwatchtower%3Apull&service=ghcr.io",
|
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(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"`
|
When("given an invalid challenge header", func() {
|
||||||
res, err := auth.GetAuthURL(input, "containrrr/watchtower")
|
It("should return an error", func() {
|
||||||
Expect(err).To(HaveOccurred())
|
challenge := `bearer realm="https://ghcr.io/token"`
|
||||||
Expect(res).To(BeNil())
|
imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
URL, err := auth.GetAuthURL(challenge, imageRef)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(URL).To(BeNil())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("deriving the auth scope from an image name", func() {
|
||||||
|
It("should prepend official dockerhub images with \"library/\"", func() {
|
||||||
|
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(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(getScopeFromImageAuthURL("piksel/containrrr/watchtower")).To(Equal("piksel/containrrr/watchtower"))
|
||||||
|
Expect(getScopeFromImageAuthURL("ghcr.io/piksel/containrrr/watchtower")).To(Equal("piksel/containrrr/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())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
When("getting a challenge url", func() {
|
|
||||||
|
Describe("GetChallengeURL", func() {
|
||||||
It("should create a valid challenge url object based on the image ref supplied", 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/"}
|
expected := url.URL{Host: "ghcr.io", Scheme: "https", Path: "/v2/"}
|
||||||
Expect(auth.GetChallengeURL("ghcr.io/containrrr/watchtower:latest")).To(Equal(expected))
|
imageRef, _ := ref.ParseNormalizedNamed("ghcr.io/containrrr/watchtower:latest")
|
||||||
|
Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected))
|
||||||
})
|
})
|
||||||
It("should assume dockerhub if the image ref is not fully qualified", func() {
|
It("should assume Docker Hub for image refs with no explicit registry", func() {
|
||||||
expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"}
|
expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"}
|
||||||
Expect(auth.GetChallengeURL("containrrr/watchtower:latest")).To(Equal(expected))
|
imageRef, _ := ref.ParseNormalizedNamed("containrrr/watchtower:latest")
|
||||||
|
Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected))
|
||||||
})
|
})
|
||||||
It("should convert legacy dockerhub hostnames to index.docker.io", func() {
|
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/"}
|
expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"}
|
||||||
Expect(auth.GetChallengeURL("docker.io/containrrr/watchtower:latest")).To(Equal(expected))
|
imageRef, _ := ref.ParseNormalizedNamed("docker.io/containrrr/watchtower:latest")
|
||||||
Expect(auth.GetChallengeURL("registry-1.docker.io/containrrr/watchtower:latest")).To(Equal(expected))
|
Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected))
|
||||||
})
|
|
||||||
})
|
|
||||||
When("getting 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"))
|
|
||||||
|
|
||||||
})
|
|
||||||
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"))
|
|
||||||
})
|
|
||||||
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"))
|
|
||||||
})
|
|
||||||
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"))
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/internal/meta"
|
"github.com/containrrr/watchtower/internal/meta"
|
||||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
"github.com/containrrr/watchtower/pkg/registry/auth"
|
||||||
"github.com/containrrr/watchtower/pkg/registry/manifest"
|
"github.com/containrrr/watchtower/pkg/registry/manifest"
|
||||||
"github.com/containrrr/watchtower/pkg/types"
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContentDigestHeader is the key for the key-value pair containing the digest header
|
// 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, _ := http.NewRequest("HEAD", url, nil)
|
||||||
req.Header.Set("User-Agent", meta.UserAgent)
|
req.Header.Set("User-Agent", meta.UserAgent)
|
||||||
|
|
||||||
if token != "" {
|
if token == "" {
|
||||||
logrus.WithField("token", token).Trace("Setting request token")
|
|
||||||
} else {
|
|
||||||
return "", errors.New("could not fetch 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("Authorization", token)
|
||||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
||||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v2+json")
|
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v2+json")
|
||||||
|
|
|
@ -1,36 +1,28 @@
|
||||||
package helpers
|
package helpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"github.com/distribution/reference"
|
||||||
url2 "net/url"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConvertToHostname strips a url from everything but the hostname part
|
// domains for Docker Hub, the default registry
|
||||||
func ConvertToHostname(url string) (string, string, error) {
|
const (
|
||||||
urlWithSchema := fmt.Sprintf("x://%s", url)
|
DefaultRegistryDomain = "docker.io"
|
||||||
u, err := url2.Parse(urlWithSchema)
|
DefaultRegistryHost = "index.docker.io"
|
||||||
if err != nil {
|
LegacyDefaultRegistryDomain = "index.docker.io"
|
||||||
return "", "", err
|
)
|
||||||
}
|
|
||||||
hostName := u.Hostname()
|
|
||||||
port := u.Port()
|
|
||||||
|
|
||||||
return hostName, port, err
|
// GetRegistryAddress parses an image name
|
||||||
}
|
// and returns the address of the specified registry
|
||||||
|
func GetRegistryAddress(imageRef string) (string, error) {
|
||||||
// NormalizeRegistry makes sure variations of DockerHubs registry
|
normalizedRef, err := reference.ParseNormalizedNamed(imageRef)
|
||||||
func NormalizeRegistry(registry string) (string, error) {
|
|
||||||
hostName, port, err := ConvertToHostname(registry)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if hostName == "registry-1.docker.io" || hostName == "docker.io" {
|
address := reference.Domain(normalizedRef)
|
||||||
hostName = "index.docker.io"
|
|
||||||
}
|
|
||||||
|
|
||||||
if port != "" {
|
if address == DefaultRegistryDomain {
|
||||||
return fmt.Sprintf("%s:%s", hostName, port), nil
|
address = DefaultRegistryHost
|
||||||
}
|
}
|
||||||
return hostName, nil
|
return address, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package helpers
|
package helpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHelpers(t *testing.T) {
|
func TestHelpers(t *testing.T) {
|
||||||
|
@ -12,20 +13,25 @@ func TestHelpers(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = Describe("the helpers", func() {
|
var _ = Describe("the helpers", func() {
|
||||||
|
Describe("GetRegistryAddress", func() {
|
||||||
When("converting an url to a hostname", func() {
|
It("should return error if passed empty string", func() {
|
||||||
It("should return docker.io given docker.io/containrrr/watchtower:latest", func() {
|
_, err := GetRegistryAddress("")
|
||||||
host, port, err := ConvertToHostname("docker.io/containrrr/watchtower:latest")
|
Expect(err).To(HaveOccurred())
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
Expect(host).To(Equal("docker.io"))
|
|
||||||
Expect(port).To(BeEmpty())
|
|
||||||
})
|
})
|
||||||
})
|
It("should return index.docker.io for image refs with no explicit registry", func() {
|
||||||
When("normalizing the registry information", func() {
|
Expect(GetRegistryAddress("watchtower")).To(Equal("index.docker.io"))
|
||||||
It("should return index.docker.io given docker.io", func() {
|
Expect(GetRegistryAddress("containrrr/watchtower")).To(Equal("index.docker.io"))
|
||||||
out, err := NormalizeRegistry("docker.io/containrrr/watchtower:latest")
|
})
|
||||||
Expect(err).NotTo(HaveOccurred())
|
It("should return index.docker.io for image refs with docker.io domain", func() {
|
||||||
Expect(out).To(Equal("index.docker.io"))
|
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
|
package manifest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
url2 "net/url"
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
||||||
"github.com/containrrr/watchtower/pkg/types"
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
ref "github.com/docker/distribution/reference"
|
ref "github.com/distribution/reference"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
url2 "net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// BuildManifestURL from raw image data
|
// BuildManifestURL from raw image data
|
||||||
func BuildManifestURL(container types.Container) (string, error) {
|
func BuildManifestURL(container types.Container) (string, error) {
|
||||||
|
normalizedRef, err := ref.ParseDockerRef(container.ImageName())
|
||||||
normalizedName, err := ref.ParseNormalizedNamed(container.ImageName())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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())
|
host, _ := helpers.GetRegistryAddress(normalizedTaggedRef.Name())
|
||||||
img, tag := ExtractImageAndTag(strings.TrimPrefix(container.ImageName(), host+"/"))
|
img, tag := ref.Path(normalizedTaggedRef), normalizedTaggedRef.Tag()
|
||||||
|
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"image": img,
|
"image": img,
|
||||||
"tag": tag,
|
"tag": tag,
|
||||||
"normalized": normalizedName,
|
"normalized": normalizedTaggedRef.Name(),
|
||||||
"host": host,
|
"host": host,
|
||||||
}).Debug("Parsing image ref")
|
}).Debug("Parsing image ref")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
img = auth.GetScopeFromImageName(img, host)
|
|
||||||
|
|
||||||
if !strings.Contains(img, "/") {
|
|
||||||
img = "library/" + img
|
|
||||||
}
|
|
||||||
url := url2.URL{
|
url := url2.URL{
|
||||||
Scheme: "https",
|
Scheme: "https",
|
||||||
Host: host,
|
Host: host,
|
||||||
|
@ -44,24 +43,3 @@ func BuildManifestURL(container types.Container) (string, error) {
|
||||||
}
|
}
|
||||||
return url.String(), nil
|
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
|
package manifest_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/containrrr/watchtower/internal/actions/mocks"
|
"github.com/containrrr/watchtower/internal/actions/mocks"
|
||||||
"github.com/containrrr/watchtower/pkg/registry/manifest"
|
"github.com/containrrr/watchtower/pkg/registry/manifest"
|
||||||
apiTypes "github.com/docker/docker/api/types"
|
apiTypes "github.com/docker/docker/api/types"
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestManifest(t *testing.T) {
|
func TestManifest(t *testing.T) {
|
||||||
|
@ -16,60 +17,58 @@ func TestManifest(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = Describe("the manifest module", func() {
|
var _ = Describe("the manifest module", func() {
|
||||||
mockId := "mock-id"
|
Describe("BuildManifestURL", func() {
|
||||||
mockName := "mock-container"
|
|
||||||
mockCreated := time.Now()
|
|
||||||
|
|
||||||
When("building a manifest url", func() {
|
|
||||||
It("should return a valid url given a fully qualified image", func() {
|
It("should return a valid url given a fully qualified image", func() {
|
||||||
expected := "https://ghcr.io/v2/containrrr/watchtower/manifests/latest"
|
imageRef := "ghcr.io/containrrr/watchtower:mytag"
|
||||||
imageInfo := apiTypes.ImageInspect{
|
expected := "https://ghcr.io/v2/containrrr/watchtower/manifests/mytag"
|
||||||
RepoTags: []string{
|
|
||||||
"ghcr.io/k6io/operator:latest",
|
URL, err := buildMockContainerManifestURL(imageRef)
|
||||||
},
|
|
||||||
}
|
|
||||||
mock := mocks.CreateMockContainerWithImageInfo(mockId, mockName, "ghcr.io/containrrr/watchtower:latest", mockCreated, imageInfo)
|
|
||||||
res, err := manifest.BuildManifestURL(mock)
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(res).To(Equal(expected))
|
Expect(URL).To(Equal(expected))
|
||||||
})
|
})
|
||||||
It("should assume dockerhub for non-qualified images", func() {
|
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"
|
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)
|
URL, err := buildMockContainerManifestURL(imageRef)
|
||||||
res, err := manifest.BuildManifestURL(mock)
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(res).To(Equal(expected))
|
Expect(URL).To(Equal(expected))
|
||||||
})
|
})
|
||||||
It("should assume latest for images that lack an explicit tag", func() {
|
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"
|
expected := "https://index.docker.io/v2/containrrr/watchtower/manifests/latest"
|
||||||
imageInfo := apiTypes.ImageInspect{
|
|
||||||
|
|
||||||
RepoTags: []string{
|
URL, err := buildMockContainerManifestURL(imageRef)
|
||||||
"containrrr/watchtower",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mock := mocks.CreateMockContainerWithImageInfo(mockId, mockName, "containrrr/watchtower", mockCreated, imageInfo)
|
|
||||||
|
|
||||||
res, err := manifest.BuildManifestURL(mock)
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(res).To(Equal(expected))
|
Expect(URL).To(Equal(expected))
|
||||||
})
|
})
|
||||||
It("should combine the tag name and digest pinning into one digest, given multiple colons", func() {
|
It("should not prepend library/ for single-part container names in registries other than Docker Hub", func() {
|
||||||
in := "containrrr/watchtower:latest@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7"
|
imageRef := "docker-registry.domain/imagename:latest"
|
||||||
image, tag := "containrrr/watchtower", "latest@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7"
|
expected := "https://docker-registry.domain/v2/imagename/manifests/latest"
|
||||||
|
|
||||||
imageOut, tagOut := manifest.ExtractImageAndTag(in)
|
URL, err := buildMockContainerManifestURL(imageRef)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(imageOut).To(Equal(image))
|
Expect(URL).To(Equal(expected))
|
||||||
Expect(tagOut).To(Equal(tag))
|
})
|
||||||
|
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)
|
||||||
|
|
||||||
|
return manifest.BuildManifestURL(mock)
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package registry
|
||||||
import (
|
import (
|
||||||
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
||||||
watchtowerTypes "github.com/containrrr/watchtower/pkg/types"
|
watchtowerTypes "github.com/containrrr/watchtower/pkg/types"
|
||||||
ref "github.com/docker/distribution/reference"
|
ref "github.com/distribution/reference"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -19,7 +19,9 @@ func GetPullOptions(imageName string) (types.ImagePullOptions, error) {
|
||||||
if auth == "" {
|
if auth == "" {
|
||||||
return types.ImagePullOptions{}, nil
|
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{
|
return types.ImagePullOptions{
|
||||||
RegistryAuth: auth,
|
RegistryAuth: auth,
|
||||||
|
@ -41,17 +43,17 @@ func DefaultAuthHandler() (string, error) {
|
||||||
// Will return false if behavior for container is unknown.
|
// Will return false if behavior for container is unknown.
|
||||||
func WarnOnAPIConsumption(container watchtowerTypes.Container) bool {
|
func WarnOnAPIConsumption(container watchtowerTypes.Container) bool {
|
||||||
|
|
||||||
normalizedName, err := ref.ParseNormalizedNamed(container.ImageName())
|
normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
containerHost, err := helpers.NormalizeRegistry(normalizedName.String())
|
containerHost, err := helpers.GetRegistryAddress(normalizedRef.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if containerHost == "index.docker.io" || containerHost == "ghcr.io" {
|
if containerHost == helpers.DefaultRegistryHost || containerHost == "ghcr.io" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,11 +23,9 @@ var _ = Describe("Registry", func() {
|
||||||
})
|
})
|
||||||
When("Given a container with an image explicitly from dockerhub", func() {
|
When("Given a container with an image explicitly from dockerhub", func() {
|
||||||
It("should want to warn", 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("index.docker.io/docker:latest")).To(BeTrue())
|
||||||
Expect(testContainerWithImage("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() {
|
When("Given a container with an image from some other registry", func() {
|
||||||
It("should not want to warn", func() {
|
It("should not want to warn", func() {
|
||||||
|
|
|
@ -5,13 +5,12 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
||||||
cliconfig "github.com/docker/cli/cli/config"
|
cliconfig "github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/docker/cli/cli/config/credentials"
|
"github.com/docker/cli/cli/config/credentials"
|
||||||
"github.com/docker/cli/cli/config/types"
|
"github.com/docker/cli/cli/config/types"
|
||||||
"github.com/docker/distribution/reference"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,7 +18,7 @@ import (
|
||||||
// loaded from environment variables or docker config
|
// loaded from environment variables or docker config
|
||||||
// as available in that order
|
// as available in that order
|
||||||
func EncodedAuth(ref string) (string, error) {
|
func EncodedAuth(ref string) (string, error) {
|
||||||
auth, err := EncodedEnvAuth(ref)
|
auth, err := EncodedEnvAuth()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
auth, err = EncodedConfigAuth(ref)
|
auth, err = EncodedConfigAuth(ref)
|
||||||
}
|
}
|
||||||
|
@ -29,7 +28,7 @@ func EncodedAuth(ref string) (string, error) {
|
||||||
// EncodedEnvAuth returns an encoded auth config for the given registry
|
// EncodedEnvAuth returns an encoded auth config for the given registry
|
||||||
// loaded from environment variables
|
// loaded from environment variables
|
||||||
// Returns an error if authentication environment variables have not been set
|
// 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")
|
username := os.Getenv("REPO_USER")
|
||||||
password := os.Getenv("REPO_PASS")
|
password := os.Getenv("REPO_PASS")
|
||||||
if username != "" && password != "" {
|
if username != "" && password != "" {
|
||||||
|
@ -37,8 +36,11 @@ func EncodedEnvAuth(ref string) (string, error) {
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: password,
|
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 EncodeAuth(auth)
|
||||||
}
|
}
|
||||||
return "", errors.New("registry auth environment variables (REPO_USER, REPO_PASS) not set")
|
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
|
// loaded from the docker config
|
||||||
// Returns an empty string if credentials cannot be found for the referenced server
|
// Returns an empty string if credentials cannot be found for the referenced server
|
||||||
// The docker config must be mounted on the container
|
// The docker config must be mounted on the container
|
||||||
func EncodedConfigAuth(ref string) (string, error) {
|
func EncodedConfigAuth(imageRef string) (string, error) {
|
||||||
server, err := ParseServerAddress(ref)
|
server, err := helpers.GetRegistryAddress(imageRef)
|
||||||
if err != nil {
|
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
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir := os.Getenv("DOCKER_CONFIG")
|
configDir := os.Getenv("DOCKER_CONFIG")
|
||||||
if configDir == "" {
|
if configDir == "" {
|
||||||
configDir = "/"
|
configDir = "/"
|
||||||
}
|
}
|
||||||
configFile, err := cliconfig.Load(configDir)
|
configFile, err := cliconfig.Load(configDir)
|
||||||
if err != nil {
|
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
|
return "", err
|
||||||
}
|
}
|
||||||
credStore := CredentialsStore(*configFile)
|
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)
|
log.WithField("config_file", configFile.Filename).Debugf("No credentials for %s found", server)
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, ref, configFile.Filename)
|
log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, server, configFile.Filename)
|
||||||
log.Tracef("Using auth password %s", auth.Password)
|
// CREDENTIAL: Uncomment to log docker config password
|
||||||
|
// log.Tracef("Using auth password %s", auth.Password)
|
||||||
return EncodeAuth(auth)
|
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
|
// CredentialsStore returns a new credentials store based
|
||||||
// on the settings provided in the configuration file.
|
// on the settings provided in the configuration file.
|
||||||
func CredentialsStore(configFile configfile.ConfigFile) credentials.Store {
|
func CredentialsStore(configFile configfile.ConfigFile) credentials.Store {
|
||||||
|
|
|
@ -1,65 +1,49 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("Testing with Ginkgo", func() {
|
var _ = Describe("Registry credential helpers", func() {
|
||||||
It("encoded env auth_ should return an error if repo envs are unset", func() {
|
Describe("EncodedAuth", func() {
|
||||||
_ = os.Unsetenv("REPO_USER")
|
It("should return repo credentials from env when set", func() {
|
||||||
_ = os.Unsetenv("REPO_PASS")
|
var err error
|
||||||
|
expected := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0="
|
||||||
|
|
||||||
_, err := EncodedEnvAuth("")
|
err = os.Setenv("REPO_USER", "containrrr-user")
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
err = os.Setenv("REPO_PASS", "containrrr-pass")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
config, err := EncodedEnvAuth()
|
||||||
|
Expect(config).To(Equal(expected))
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
It("encoded env auth_ should return auth hash if repo envs are set", func() {
|
|
||||||
var err error
|
|
||||||
expectedHash := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0="
|
|
||||||
|
|
||||||
err = os.Setenv("REPO_USER", "containrrr-user")
|
Describe("EncodedEnvAuth", func() {
|
||||||
Expect(err).NotTo(HaveOccurred())
|
It("should return an error if repo envs are unset", func() {
|
||||||
|
_ = os.Unsetenv("REPO_USER")
|
||||||
|
_ = os.Unsetenv("REPO_PASS")
|
||||||
|
|
||||||
err = os.Setenv("REPO_PASS", "containrrr-pass")
|
_, err := EncodedEnvAuth()
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).To(HaveOccurred())
|
||||||
|
})
|
||||||
config, err := EncodedEnvAuth("")
|
|
||||||
Expect(config).To(Equal(expectedHash))
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
})
|
})
|
||||||
It("encoded config auth_ should return an error if file is not present", func() {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
err = os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail")
|
Describe("EncodedConfigAuth", func() {
|
||||||
Expect(err).NotTo(HaveOccurred())
|
It("should return an error if file is not present", func() {
|
||||||
|
var err error
|
||||||
|
|
||||||
_, err = EncodedConfigAuth("")
|
err = os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail")
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
})
|
_, 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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/containrrr/watchtower/pkg/container"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ByCreated allows a list of Container structs to be sorted by the container's
|
// ByCreated allows a list of Container structs to be sorted by the container's
|
||||||
// created date.
|
// created date.
|
||||||
type ByCreated []container.Container
|
type ByCreated []types.Container
|
||||||
|
|
||||||
func (c ByCreated) Len() int { return len(c) }
|
func (c ByCreated) Len() int { return len(c) }
|
||||||
func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
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
|
// 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
|
// of their dependencies. This sort order ensures that linked containers can
|
||||||
// be started in the correct order.
|
// be started in the correct order.
|
||||||
func SortByDependencies(containers []container.Container) ([]container.Container, error) {
|
func SortByDependencies(containers []types.Container) ([]types.Container, error) {
|
||||||
sorter := dependencySorter{}
|
sorter := dependencySorter{}
|
||||||
return sorter.Sort(containers)
|
return sorter.Sort(containers)
|
||||||
}
|
}
|
||||||
|
|
||||||
type dependencySorter struct {
|
type dependencySorter struct {
|
||||||
unvisited []container.Container
|
unvisited []types.Container
|
||||||
marked map[string]bool
|
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.unvisited = containers
|
||||||
ds.marked = map[string]bool{}
|
ds.marked = map[string]bool{}
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ func (ds *dependencySorter) Sort(containers []container.Container) ([]container.
|
||||||
return ds.sorted, nil
|
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 {
|
if _, ok := ds.marked[c.Name()]; ok {
|
||||||
return fmt.Errorf("circular reference to %s", c.Name())
|
return fmt.Errorf("circular reference to %s", c.Name())
|
||||||
|
@ -84,7 +85,7 @@ func (ds *dependencySorter) visit(c container.Container) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *dependencySorter) findUnvisited(name string) *container.Container {
|
func (ds *dependencySorter) findUnvisited(name string) *types.Container {
|
||||||
for _, c := range ds.unvisited {
|
for _, c := range ds.unvisited {
|
||||||
if c.Name() == name {
|
if c.Name() == name {
|
||||||
return &c
|
return &c
|
||||||
|
@ -94,7 +95,7 @@ func (ds *dependencySorter) findUnvisited(name string) *container.Container {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *dependencySorter) removeUnvisited(c container.Container) {
|
func (ds *dependencySorter) removeUnvisited(c types.Container) {
|
||||||
var idx int
|
var idx int
|
||||||
for i := range ds.unvisited {
|
for i := range ds.unvisited {
|
||||||
if ds.unvisited[i].Name() == c.Name() {
|
if ds.unvisited[i].Name() == c.Name() {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
dc "github.com/docker/docker/api/types/container"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ImageID is a hash string representing a container image
|
// ImageID is a hash string representing a container image
|
||||||
|
@ -50,7 +52,7 @@ type Container interface {
|
||||||
SafeImageID() ImageID
|
SafeImageID() ImageID
|
||||||
ImageName() string
|
ImageName() string
|
||||||
Enabled() (bool, bool)
|
Enabled() (bool, bool)
|
||||||
IsMonitorOnly() bool
|
IsMonitorOnly(UpdateParams) bool
|
||||||
Scope() (string, bool)
|
Scope() (string, bool)
|
||||||
Links() []string
|
Links() []string
|
||||||
ToRestart() bool
|
ToRestart() bool
|
||||||
|
@ -62,4 +64,15 @@ type Container interface {
|
||||||
GetLifecyclePostCheckCommand() string
|
GetLifecyclePostCheckCommand() string
|
||||||
GetLifecyclePreUpdateCommand() string
|
GetLifecyclePreUpdateCommand() string
|
||||||
GetLifecyclePostUpdateCommand() 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,13 @@ import (
|
||||||
|
|
||||||
// UpdateParams contains all different options available to alter the behavior of the Update func
|
// UpdateParams contains all different options available to alter the behavior of the Update func
|
||||||
type UpdateParams struct {
|
type UpdateParams struct {
|
||||||
Filter Filter
|
Filter Filter
|
||||||
Cleanup bool
|
Cleanup bool
|
||||||
NoRestart bool
|
NoRestart bool
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
MonitorOnly bool
|
MonitorOnly bool
|
||||||
LifecycleHooks bool
|
NoPull bool
|
||||||
RollingRestart 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