mirror of
https://github.com/containrrr/watchtower.git
synced 2025-09-22 05:40:50 +02:00
Compare commits
229 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 | ||
![]() |
3d1bf8ab72 | ||
![]() |
264046d5f9 | ||
![]() |
547d033460 | ||
![]() |
8464e0dece | ||
![]() |
0168cd8a40 | ||
![]() |
ff564ef806 | ||
![]() |
b62f8d7738 | ||
![]() |
14b235a542 | ||
![]() |
87c5695e84 | ||
![]() |
c16ac967c5 | ||
![]() |
36b3a64d86 | ||
![]() |
8b5eb9cb9c | ||
![]() |
84fb391d58 | ||
![]() |
fe5077881b | ||
![]() |
8449d9c34f | ||
![]() |
2faf4c4cc1 | ||
![]() |
dcb38c68d8 | ||
![]() |
d2cfefb08a | ||
![]() |
e060e5e38a | ||
![]() |
3a59664c0e | ||
![]() |
b71eb2dec7 | ||
![]() |
3190ce2df1 | ||
![]() |
a4d00bfd75 | ||
![]() |
d5d711bd14 | ||
![]() |
d744c34886 | ||
![]() |
987f2bb5fa | ||
![]() |
e8df2b2ddf | ||
![]() |
1e6b09550b | ||
![]() |
403c600b99 | ||
![]() |
ae8c36f04f | ||
![]() |
5134e159e2 | ||
![]() |
216d8df0e7 | ||
![]() |
5ddca29f05 | ||
![]() |
d1e6fa885f | ||
![]() |
cb555f539d | ||
![]() |
2102a056de | ||
![]() |
ef44ae9ba7 | ||
![]() |
398d2713d6 | ||
![]() |
425684c761 | ||
![]() |
c3f9d778b0 | ||
![]() |
b855675b9f | ||
![]() |
9ea5d3b5e8 | ||
![]() |
9a2f9c48c7 | ||
![]() |
0a0998f83c | ||
![]() |
a19887546b | ||
![]() |
faa94c4fd1 | ||
![]() |
fc401dae75 | ||
![]() |
1fc24f691e | ||
![]() |
4c4c27574c | ||
![]() |
57e14ac34c | ||
![]() |
b50e76580e | ||
![]() |
9cee9e02d0 | ||
![]() |
c1fb00a567 | ||
![]() |
0fddbfb7ed | ||
![]() |
230312fb50 | ||
![]() |
1f0fc7209b | ||
![]() |
cbbdbb7ad6 | ||
![]() |
626bd547e9 | ||
![]() |
6c215cc4f8 | ||
![]() |
f047d75dcc | ||
![]() |
e04a107694 | ||
![]() |
964879d228 | ||
![]() |
ab7f8233bb | ||
![]() |
a15c0e3440 | ||
![]() |
ccc7878179 | ||
![]() |
1745704163 | ||
![]() |
77b8f82678 | ||
![]() |
146576818c | ||
![]() |
7f808d8b1c | ||
![]() |
cf307db473 | ||
![]() |
063e7247b9 | ||
![]() |
6b2ef10ab2 | ||
![]() |
d860016335 | ||
![]() |
e02227fed0 | ||
![]() |
54f19df491 | ||
![]() |
e316f9eaae | ||
![]() |
3849b48468 | ||
![]() |
27e936c16d | ||
![]() |
7900471f88 | ||
![]() |
a429c373ff | ||
![]() |
36d3569c4a | ||
![]() |
1569445e6a | ||
![]() |
bd2adf6e5f | ||
![]() |
489356aa42 | ||
![]() |
08831f798c | ||
![]() |
5f8565570c | ||
![]() |
ae33e77de5 | ||
![]() |
2aa01da608 | ||
![]() |
30f36b3ca2 | ||
![]() |
739f328ee5 | ||
![]() |
33b8a9822c | ||
![]() |
e983beb52a | ||
![]() |
de40b0ce11 | ||
![]() |
3e4b26957d | ||
![]() |
56368a7207 | ||
![]() |
2f4d58776d | ||
![]() |
e9c83af533 | ||
![]() |
d12ce7ce79 | ||
![]() |
a5c60a9fe6 | ||
![]() |
f79e4b5435 | ||
![]() |
25a9639368 |
121 changed files with 6776 additions and 2540 deletions
|
@ -5,6 +5,30 @@
|
|||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"contributors": [
|
||||
{
|
||||
"login": "piksel",
|
||||
"name": "nils måsén",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/807383?v=4",
|
||||
"profile": "https://piksel.se",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"maintenance",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "simskij",
|
||||
"name": "Simon Aronsson",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1596025?v=4",
|
||||
"profile": "http://simme.dev",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc",
|
||||
"maintenance",
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Codelica",
|
||||
"name": "James",
|
||||
|
@ -273,18 +297,6 @@
|
|||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "simskij",
|
||||
"name": "Simon Aronsson",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1596025?v=4",
|
||||
"profile": "http://simme.dev",
|
||||
"contributions": [
|
||||
"code",
|
||||
"maintenance",
|
||||
"review",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Ansem93",
|
||||
"name": "Ansem93",
|
||||
|
@ -508,16 +520,6 @@
|
|||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "piksel",
|
||||
"name": "nils måsén",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/807383?v=4",
|
||||
"profile": "https://piksel.se",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "arnested",
|
||||
"name": "Arne Jørgensen",
|
||||
|
@ -693,7 +695,8 @@
|
|||
"profile": "https://github.com/ksurl",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
"code",
|
||||
"infra"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -812,6 +815,67 @@
|
|||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Foxite",
|
||||
"name": "Dirk Kok",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20421657?v=4",
|
||||
"profile": "https://ko-fi.com/foxite",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "EDIflyer",
|
||||
"name": "EDIflyer",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/13610277?v=4",
|
||||
"profile": "https://github.com/EDIflyer",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jauderho",
|
||||
"name": "Jauder Ho",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/13562?v=4",
|
||||
"profile": "https://github.com/jauderho",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "andriibratanin",
|
||||
"name": "Andrii Bratanin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20169213?v=4",
|
||||
"profile": "https://github.com/andriibratanin"
|
||||
},
|
||||
{
|
||||
"login": "IAmTamal",
|
||||
"name": "Tamal Das ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/72851613?v=4",
|
||||
"profile": "https://tamal.vercel.app/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "testwill",
|
||||
"name": "guangwu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8717479?v=4",
|
||||
"profile": "https://github.com/testwill",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nothub",
|
||||
"name": "Florian Hübner",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/48992448?v=4",
|
||||
"profile": "http://hub.lol",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
@ -820,5 +884,6 @@
|
|||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"commitConvention": "none",
|
||||
"skipCi": true
|
||||
"skipCi": true,
|
||||
"commitType": "docs"
|
||||
}
|
||||
|
|
14
.editorconfig
Normal file
14
.editorconfig
Normal file
|
@ -0,0 +1,14 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
|
||||
[*.css]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[{go.mod,go.sum,*.go}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
@ -1,2 +0,0 @@
|
|||
custom: https://www.amazon.com/hz/wishlist/ls/F94JJV822VX6
|
||||
github: simskij
|
71
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
71
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
|
@ -0,0 +1,71 @@
|
|||
name: 🐛 Bug report
|
||||
description: Create a report to help us improve
|
||||
labels: ["Priority: Medium, Status: Available, Type: Bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Before submitting your issue, please make sure you're using the containrrr/watchtower:latest image. If not, switch to this image prior to posting your report. Other forks, or the old `v2tec` image are **not** supported.
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
value: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: Please add screenshots if applicable
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: We would want to know the following things
|
||||
value: |
|
||||
- Platform
|
||||
- Architecture
|
||||
- Docker Version
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Your logs
|
||||
description: Paste the logs from running watchtower with the `--debug` option.
|
||||
render: text
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
53
.github/ISSUE_TEMPLATE/bug_report.md
vendored
53
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,53 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'Priority: Medium, Status: Available, Type: Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!--
|
||||
Before submitting your issue, please make sure you're using the containrrr/watchtower:latest image.
|
||||
If not, switch to this image prior to posting your report. Other forks, or the old `v2tec` image are **not** supported.
|
||||
-->
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!--
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Screenshots**
|
||||
<!--
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
-->
|
||||
**Environment**
|
||||
<!--
|
||||
We want to know:
|
||||
- Platform
|
||||
- Architecture
|
||||
- Docker version
|
||||
-->
|
||||
|
||||
<details>
|
||||
<summary><b> Logs from running watchtower with the <code>--debug</code> option </b></summary>
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
**Additional context**
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
-->
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'Priority: Low, Status: Available, Type: Enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
name: 💡 Feature request
|
||||
description: Have a new idea/feature ? Please suggest!
|
||||
labels: ["Priority: Low, Status: Available, Type: Enhancement"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: extrainfo
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
|
21
.github/dependabot.yml
vendored
Normal file
21
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "docker" # See documentation for possible values
|
||||
directory: "/dockerfiles" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -31,7 +31,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
@ -44,7 +44,7 @@ jobs:
|
|||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
|
@ -55,7 +55,7 @@ jobs:
|
|||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
@ -69,4 +69,4 @@ jobs:
|
|||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
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
|
7
.github/workflows/greetings.yml
vendored
7
.github/workflows/greetings.yml
vendored
|
@ -1,6 +1,11 @@
|
|||
name: Greetings
|
||||
|
||||
on: [issues]
|
||||
on:
|
||||
# Runs in the context of the target (containrrr/watchtower) repository, and as such has access to GITHUB_TOKEN
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
|
|
20
.github/workflows/publish-docs.yml
vendored
20
.github/workflows/publish-docs.yml
vendored
|
@ -14,14 +14,24 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
- name: Build tplprev
|
||||
run: scripts/build-tplprev.sh
|
||||
- name: Setup python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: |
|
||||
docs-requirements.txt
|
||||
- name: Install mkdocs
|
||||
run: |
|
||||
pip install \
|
||||
mkdocs \
|
||||
mkdocs-material \
|
||||
md-toc
|
||||
pip install -r docs-requirements.txt
|
||||
- name: Generate docs
|
||||
run: mkdocs gh-deploy --strict
|
34
.github/workflows/pull-request.yml
vendored
34
.github/workflows/pull-request.yml
vendored
|
@ -12,26 +12,24 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
- name: Install linter
|
||||
run: |
|
||||
go get -u golang.org/x/lint/golint
|
||||
- name: Lint files
|
||||
run: |
|
||||
golint -set_exit_status ./...
|
||||
go-version: 1.20.x
|
||||
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
||||
with:
|
||||
version: "2023.1.6"
|
||||
install-go: "false" # StaticCheck uses go v1.17 which does not support `any`
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version:
|
||||
- 1.15.x
|
||||
- 1.20.x
|
||||
platform:
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
|
@ -39,18 +37,18 @@ jobs:
|
|||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
go-version: 1.20.x
|
||||
- name: Run tests
|
||||
run: |
|
||||
go test -v -coverprofile coverage.out -covermode atomic ./...
|
||||
- name: Publish coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
build:
|
||||
|
@ -58,15 +56,15 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
go-version: 1.20.x
|
||||
- name: Build
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3
|
||||
with:
|
||||
version: v0.155.0
|
||||
args: --snapshot --skip-publish --debug
|
||||
|
|
24
.github/workflows/release-dev.yaml
vendored
24
.github/workflows/release-dev.yaml
vendored
|
@ -10,25 +10,27 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
go-version: 1.15
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
- name: Build
|
||||
run: ./build.sh
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.20.x
|
||||
- name: Test
|
||||
run: go test -v -coverprofile coverage.out -covermode atomic ./...
|
||||
- name: Publish coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
publish:
|
||||
|
@ -37,9 +39,9 @@ jobs:
|
|||
- test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Publish to Docker Hub
|
||||
uses: jerray/publish-docker-action@master
|
||||
uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
@ -47,7 +49,7 @@ jobs:
|
|||
repository: containrrr/watchtower
|
||||
tags: latest-dev
|
||||
- name: Publish to GHCR
|
||||
uses: jerray/publish-docker-action@master
|
||||
uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14
|
||||
with:
|
||||
username: ${{ secrets.BOT_USERNAME }}
|
||||
password: ${{ secrets.BOT_GHCR_PAT }}
|
||||
|
|
46
.github/workflows/release.yml
vendored
46
.github/workflows/release.yml
vendored
|
@ -2,9 +2,7 @@ name: Release (Production)
|
|||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- '**/v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
@ -15,26 +13,24 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
- name: Install linter
|
||||
run: |
|
||||
go get -u golang.org/x/lint/golint
|
||||
- name: Lint files
|
||||
run: |
|
||||
golint -set_exit_status ./...
|
||||
go-version: 1.20.x
|
||||
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
||||
with:
|
||||
version: "2022.1.1"
|
||||
install-go: "false" # StaticCheck uses go v1.17 which does not support `any`
|
||||
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- 1.15.x
|
||||
- 1.20.x
|
||||
platform:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
|
@ -42,13 +38,13 @@ jobs:
|
|||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
go-version: 1.20.x
|
||||
- name: Run tests
|
||||
run: |
|
||||
go test ./... -coverprofile coverage.out
|
||||
|
@ -61,29 +57,29 @@ jobs:
|
|||
- lint
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
go-version: 1.20.x
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2
|
||||
with:
|
||||
username: ${{ secrets.BOT_USERNAME }}
|
||||
password: ${{ secrets.BOT_GHCR_PAT }}
|
||||
registry: ghcr.io
|
||||
- name: Build
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3
|
||||
with:
|
||||
version: v0.155.0
|
||||
args: --debug
|
||||
|
@ -95,7 +91,7 @@ jobs:
|
|||
echo '{"experimental": "enabled"}' > ~/.docker/config.json
|
||||
- name: Create manifest for version
|
||||
run: |
|
||||
export DH_TAG=$(echo $TAG | sed 's/^v*//')
|
||||
export DH_TAG=$(git tag --points-at HEAD | sed 's/^v*//')
|
||||
docker manifest create \
|
||||
containrrr/watchtower:$DH_TAG \
|
||||
containrrr/watchtower:amd64-$DH_TAG \
|
||||
|
@ -193,7 +189,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Pull new module version
|
||||
uses: andrewslotin/go-proxy-pull-action@master
|
||||
uses: andrewslotin/go-proxy-pull-action@50fea06a976087614babb9508e5c528b464f4645 #master@2022-10-14
|
||||
|
||||
|
||||
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -8,3 +8,6 @@ dist
|
|||
/site
|
||||
coverage.out
|
||||
*.coverprofile
|
||||
|
||||
docs/assets/wasm_exec.js
|
||||
docs/assets/*.wasm
|
239
README.md
239
README.md
|
@ -31,6 +31,8 @@ $ docker run --detach \
|
|||
containrrr/watchtower
|
||||
```
|
||||
|
||||
Watchtower is intended to be used in homelabs, media centers, local dev environments, and similar. We do **not** recommend using Watchtower in a commercial or production environment. If that is you, you should be looking into using Kubernetes. If that feels like too big a step for you, please look into solutions like [MicroK8s](https://microk8s.io/) and [k3s](https://k3s.io/) that take away a lot of the toil of running a Kubernetes cluster.
|
||||
|
||||
## Documentation
|
||||
The full documentation is available at https://containrrr.dev/watchtower.
|
||||
|
||||
|
@ -42,119 +44,130 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="http://codelica.com"><img src="https://avatars3.githubusercontent.com/u/386101?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Codelica" title="Tests">⚠️</a> <a href="#ideas-Codelica" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://kopfkrieg.org"><img src="https://avatars2.githubusercontent.com/u/5047813?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Florian</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3AKopfKrieg" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=KopfKrieg" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/bdehamer"><img src="https://avatars1.githubusercontent.com/u/398027?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Brian DeHamer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bdehamer" title="Code">💻</a> <a href="#maintenance-bdehamer" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://github.com/rosscado"><img src="https://avatars1.githubusercontent.com/u/16578183?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ross Cadogan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rosscado" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/stffabi"><img src="https://avatars0.githubusercontent.com/u/9464631?v=4?s=100" width="100px;" alt=""/><br /><sub><b>stffabi</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=stffabi" title="Code">💻</a> <a href="#maintenance-stffabi" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://github.com/ATCUSA"><img src="https://avatars3.githubusercontent.com/u/3581228?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Austin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ATCUSA" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://labs.ctl.io"><img src="https://avatars2.githubusercontent.com/u/6181487?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David Gardner</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Adavidgardner11" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=davidgardner11" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/dolanor"><img src="https://avatars3.githubusercontent.com/u/928722?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tanguy ⧓ Herrmann</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dolanor" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/rdamazio"><img src="https://avatars3.githubusercontent.com/u/997641?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rodrigo Damazio Bovendorp</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.taisun.io/"><img src="https://avatars3.githubusercontent.com/u/1852688?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ryan Kuba</b></sub></a><br /><a href="#infra-thelamer" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://github.com/cnrmck"><img src="https://avatars2.githubusercontent.com/u/22061955?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cnrmck</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cnrmck" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://harrywalter.co.uk"><img src="https://avatars3.githubusercontent.com/u/338588?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Harry Walter</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=haswalt" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://projectsperanza.com"><img src="https://avatars3.githubusercontent.com/u/74515?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Robotex</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Robotex" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://geraldpape.io"><img src="https://avatars0.githubusercontent.com/u/1494211?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gerald Pape</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ubergesundheit" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/fomk"><img src="https://avatars0.githubusercontent.com/u/17636183?v=4?s=100" width="100px;" alt=""/><br /><sub><b>fomk</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=fomk" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/svengo"><img src="https://avatars3.githubusercontent.com/u/2502366?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sven Gottwald</b></sub></a><br /><a href="#infra-svengo" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center"><a href="https://liberapay.com/techknowlogick/"><img src="https://avatars1.githubusercontent.com/u/164197?v=4?s=100" width="100px;" alt=""/><br /><sub><b>techknowlogick</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=techknowlogick" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://log.c5t.org/about/"><img src="https://avatars1.githubusercontent.com/u/1449568?v=4?s=100" width="100px;" alt=""/><br /><sub><b>waja</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=waja" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://scottalbertson.com"><img src="https://avatars2.githubusercontent.com/u/154463?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Scott Albertson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=salbertson" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/huddlesj"><img src="https://avatars1.githubusercontent.com/u/11966535?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jason Huddleston</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=huddlesj" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://npstr.space/"><img src="https://avatars3.githubusercontent.com/u/6048348?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Napster</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=napstr" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/darknode"><img src="https://avatars1.githubusercontent.com/u/809429?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maxim</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://schmitt.cat"><img src="https://avatars0.githubusercontent.com/u/17984549?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Max Schmitt</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mxschmitt" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/cron410"><img src="https://avatars1.githubusercontent.com/u/3082899?v=4?s=100" width="100px;" alt=""/><br /><sub><b>cron410</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cron410" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Cardoso222"><img src="https://avatars3.githubusercontent.com/u/7026517?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Paulo Henrique</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Cardoso222" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://coded.io"><img src="https://avatars0.githubusercontent.com/u/107097?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kaleb Elwert</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=belak" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/wmbutler"><img src="https://avatars1.githubusercontent.com/u/1254810?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Bill Butler</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=wmbutler" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.mariotacke.io"><img src="https://avatars2.githubusercontent.com/u/4942019?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mario Tacke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mariotacke" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://markwoodbridge.com"><img src="https://avatars2.githubusercontent.com/u/1101318?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mark Woodbridge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mrw34" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://simme.dev"><img src="https://avatars0.githubusercontent.com/u/1596025?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Simon Aronsson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Code">💻</a> <a href="#maintenance-simskij" title="Maintenance">🚧</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Asimskij" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Ansem93"><img src="https://avatars3.githubusercontent.com/u/6626218?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ansem93</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Ansem93" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/lukapeschke"><img src="https://avatars1.githubusercontent.com/u/17085536?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Luka Peschke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/zoispag"><img src="https://avatars0.githubusercontent.com/u/21138205?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Zois Pagoulatos</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zoispag" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Azoispag" title="Reviewed Pull Requests">👀</a> <a href="#maintenance-zoispag" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://alexandre.menif.name"><img src="https://avatars0.githubusercontent.com/u/16152103?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexandre Menif</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=alexandremenif" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/chugunov"><img src="https://avatars1.githubusercontent.com/u/4140479?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrey</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chugunov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://noplanman.ch"><img src="https://avatars3.githubusercontent.com/u/9423417?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Armando Lüscher</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=noplanman" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/rjbudke"><img src="https://avatars2.githubusercontent.com/u/273485?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ryan Budke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rjbudke" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://kaloyan.raev.name"><img src="https://avatars2.githubusercontent.com/u/468091?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Kaloyan Raev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/sixth"><img src="https://avatars3.githubusercontent.com/u/11591445?v=4?s=100" width="100px;" alt=""/><br /><sub><b>sixth</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixth" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://foosel.net"><img src="https://avatars0.githubusercontent.com/u/83657?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gina Häußge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=foosel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/8ear"><img src="https://avatars0.githubusercontent.com/u/10329648?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Max H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=8ear" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://pjknkda.github.io"><img src="https://avatars0.githubusercontent.com/u/4986524?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jungkook Park</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pjknkda" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://achfrag.net"><img src="https://avatars1.githubusercontent.com/u/5753622?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Jan Kristof Nidzwetzki</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jnidzwetzki" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.lukaselsner.de"><img src="https://avatars0.githubusercontent.com/u/1413542?v=4?s=100" width="100px;" alt=""/><br /><sub><b>lukas</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mindrunner" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://codingcoffee.dev"><img src="https://avatars3.githubusercontent.com/u/13611153?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ameya Shenoy</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=codingCoffee" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/raymondelooff"><img src="https://avatars0.githubusercontent.com/u/9716806?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Raymon de Looff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=raymondelooff" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://codemonkeylabs.com"><img src="https://avatars2.githubusercontent.com/u/704034?v=4?s=100" width="100px;" alt=""/><br /><sub><b>John Clayton</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jsclayton" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Germs2004"><img src="https://avatars2.githubusercontent.com/u/5519340?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Germs2004</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Germs2004" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/lukwil"><img src="https://avatars1.githubusercontent.com/u/30203234?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Lukas Willburger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukwil" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/auanasgheps"><img src="https://avatars2.githubusercontent.com/u/20586878?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Oliver Cervera</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=auanasgheps" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/victorcmoura"><img src="https://avatars1.githubusercontent.com/u/26290053?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Victor Moura</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/mbrandau"><img src="https://avatars3.githubusercontent.com/u/12972798?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maximilian Brandau</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/aneisch"><img src="https://avatars1.githubusercontent.com/u/6991461?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrew</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=aneisch" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/sixcorners"><img src="https://avatars0.githubusercontent.com/u/585501?v=4?s=100" width="100px;" alt=""/><br /><sub><b>sixcorners</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixcorners" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://piksel.se"><img src="https://avatars2.githubusercontent.com/u/807383?v=4?s=100" width="100px;" alt=""/><br /><sub><b>nils måsén</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://arnested.dk"><img src="https://avatars2.githubusercontent.com/u/190005?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Arne Jørgensen</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=arnested" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Aarnested" title="Reviewed Pull Requests">👀</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/patski123"><img src="https://avatars1.githubusercontent.com/u/19295295?v=4?s=100" width="100px;" alt=""/><br /><sub><b>PatSki123</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patski123" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://rubyroidlabs.com/"><img src="https://avatars2.githubusercontent.com/u/624999?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Valentine Zavadsky</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/bopoh24"><img src="https://avatars2.githubusercontent.com/u/4086631?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Alexander Voronin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bopoh24" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/issues?q=author%3Abopoh24" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="http://www.teqneers.de"><img src="https://avatars0.githubusercontent.com/u/788989?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Oliver Mueller</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ogmueller" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/tammert"><img src="https://avatars0.githubusercontent.com/u/8885250?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sebastiaan Tammer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tammert" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Miosame"><img src="https://avatars1.githubusercontent.com/u/8201077?v=4?s=100" width="100px;" alt=""/><br /><sub><b>miosame</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=miosame" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://mtz.gr"><img src="https://avatars3.githubusercontent.com/u/590246?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrew Metzger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/issues?q=author%3Aandrewjmetzger" title="Bug reports">🐛</a> <a href="#example-andrewjmetzger" title="Examples">💡</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/pgrimaud"><img src="https://avatars1.githubusercontent.com/u/1866496?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pgrimaud" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/mattdoran"><img src="https://avatars0.githubusercontent.com/u/577779?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Matt Doran</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mattdoran" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/MihailITPlace"><img src="https://avatars2.githubusercontent.com/u/28401551?v=4?s=100" width="100px;" alt=""/><br /><sub><b>MihailITPlace</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MihailITPlace" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/bugficks"><img src="https://avatars1.githubusercontent.com/u/2992895?v=4?s=100" width="100px;" alt=""/><br /><sub><b>bugficks</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/MichaelSp"><img src="https://avatars0.githubusercontent.com/u/448282?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Michael</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MichaelSp" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/jokay"><img src="https://avatars0.githubusercontent.com/u/18613935?v=4?s=100" width="100px;" alt=""/><br /><sub><b>D. Domig</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jokay" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://maxwells-daemon.io"><img src="https://avatars1.githubusercontent.com/u/260084?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ben Osheroff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=osheroff" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/dhet"><img src="https://avatars3.githubusercontent.com/u/2668621?v=4?s=100" width="100px;" alt=""/><br /><sub><b>David H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dhet" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.gridgeo.com"><img src="https://avatars1.githubusercontent.com/u/671887?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Chander Ganesan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chander" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/yrien30"><img src="https://avatars1.githubusercontent.com/u/26816162?v=4?s=100" width="100px;" alt=""/><br /><sub><b>yrien30</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=yrien30" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ksurl"><img src="https://avatars1.githubusercontent.com/u/1371562?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ksurl</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/rg9400"><img src="https://avatars2.githubusercontent.com/u/39887349?v=4?s=100" width="100px;" alt=""/><br /><sub><b>rg9400</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rg9400" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/tkalus"><img src="https://avatars2.githubusercontent.com/u/287181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Turtle Kalus</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tkalus" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/SrihariThalla"><img src="https://avatars1.githubusercontent.com/u/7479937?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Srihari Thalla</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=SrihariThalla" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://nymous.io"><img src="https://avatars1.githubusercontent.com/u/4216559?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Thomas Gaudin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=nymous" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://indigo.re/"><img src="https://avatars.githubusercontent.com/u/2804645?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hydrargyrum</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hydrargyrum" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://reinout.vanrees.org"><img src="https://avatars.githubusercontent.com/u/121433?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Reinout van Rees</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=reinout" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/DasSkelett"><img src="https://avatars.githubusercontent.com/u/28812678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>DasSkelett</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=DasSkelett" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/zenjabba"><img src="https://avatars.githubusercontent.com/u/679864?v=4?s=100" width="100px;" alt=""/><br /><sub><b>zenjabba</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zenjabba" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://quan.io"><img src="https://avatars.githubusercontent.com/u/3526705?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dan Quan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=djquan" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/modem7"><img src="https://avatars.githubusercontent.com/u/4349962?v=4?s=100" width="100px;" alt=""/><br /><sub><b>modem7</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=modem7" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/hypnoglow"><img src="https://avatars.githubusercontent.com/u/4853075?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Igor Zibarev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hypnoglow" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/patricegautier"><img src="https://avatars.githubusercontent.com/u/38435239?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Patrice</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patricegautier" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://jamesw.link/me"><img src="https://avatars.githubusercontent.com/u/8067792?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James White</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jamesmacwhite" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://piksel.se"><img src="https://avatars2.githubusercontent.com/u/807383?v=4?s=100" width="100px;" alt="nils måsén"/><br /><sub><b>nils måsén</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Documentation">📖</a> <a href="#maintenance-piksel" title="Maintenance">🚧</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Apiksel" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://simme.dev"><img src="https://avatars0.githubusercontent.com/u/1596025?v=4?s=100" width="100px;" alt="Simon Aronsson"/><br /><sub><b>Simon Aronsson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Documentation">📖</a> <a href="#maintenance-simskij" title="Maintenance">🚧</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Asimskij" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://codelica.com"><img src="https://avatars3.githubusercontent.com/u/386101?v=4?s=100" width="100px;" alt="James"/><br /><sub><b>James</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Codelica" title="Tests">⚠️</a> <a href="#ideas-Codelica" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://kopfkrieg.org"><img src="https://avatars2.githubusercontent.com/u/5047813?v=4?s=100" width="100px;" alt="Florian"/><br /><sub><b>Florian</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3AKopfKrieg" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=KopfKrieg" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bdehamer"><img src="https://avatars1.githubusercontent.com/u/398027?v=4?s=100" width="100px;" alt="Brian DeHamer"/><br /><sub><b>Brian DeHamer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bdehamer" title="Code">💻</a> <a href="#maintenance-bdehamer" title="Maintenance">🚧</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rosscado"><img src="https://avatars1.githubusercontent.com/u/16578183?v=4?s=100" width="100px;" alt="Ross Cadogan"/><br /><sub><b>Ross Cadogan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rosscado" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/stffabi"><img src="https://avatars0.githubusercontent.com/u/9464631?v=4?s=100" width="100px;" alt="stffabi"/><br /><sub><b>stffabi</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=stffabi" title="Code">💻</a> <a href="#maintenance-stffabi" title="Maintenance">🚧</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ATCUSA"><img src="https://avatars3.githubusercontent.com/u/3581228?v=4?s=100" width="100px;" alt="Austin"/><br /><sub><b>Austin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ATCUSA" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://labs.ctl.io"><img src="https://avatars2.githubusercontent.com/u/6181487?v=4?s=100" width="100px;" alt="David Gardner"/><br /><sub><b>David Gardner</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Adavidgardner11" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/containrrr/watchtower/commits?author=davidgardner11" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dolanor"><img src="https://avatars3.githubusercontent.com/u/928722?v=4?s=100" width="100px;" alt="Tanguy ⧓ Herrmann"/><br /><sub><b>Tanguy ⧓ Herrmann</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dolanor" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rdamazio"><img src="https://avatars3.githubusercontent.com/u/997641?v=4?s=100" width="100px;" alt="Rodrigo Damazio Bovendorp"/><br /><sub><b>Rodrigo Damazio Bovendorp</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=rdamazio" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.taisun.io/"><img src="https://avatars3.githubusercontent.com/u/1852688?v=4?s=100" width="100px;" alt="Ryan Kuba"/><br /><sub><b>Ryan Kuba</b></sub></a><br /><a href="#infra-thelamer" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cnrmck"><img src="https://avatars2.githubusercontent.com/u/22061955?v=4?s=100" width="100px;" alt="cnrmck"/><br /><sub><b>cnrmck</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cnrmck" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://harrywalter.co.uk"><img src="https://avatars3.githubusercontent.com/u/338588?v=4?s=100" width="100px;" alt="Harry Walter"/><br /><sub><b>Harry Walter</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=haswalt" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://projectsperanza.com"><img src="https://avatars3.githubusercontent.com/u/74515?v=4?s=100" width="100px;" alt="Robotex"/><br /><sub><b>Robotex</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Robotex" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://geraldpape.io"><img src="https://avatars0.githubusercontent.com/u/1494211?v=4?s=100" width="100px;" alt="Gerald Pape"/><br /><sub><b>Gerald Pape</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ubergesundheit" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fomk"><img src="https://avatars0.githubusercontent.com/u/17636183?v=4?s=100" width="100px;" alt="fomk"/><br /><sub><b>fomk</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=fomk" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/svengo"><img src="https://avatars3.githubusercontent.com/u/2502366?v=4?s=100" width="100px;" alt="Sven Gottwald"/><br /><sub><b>Sven Gottwald</b></sub></a><br /><a href="#infra-svengo" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://liberapay.com/techknowlogick/"><img src="https://avatars1.githubusercontent.com/u/164197?v=4?s=100" width="100px;" alt="techknowlogick"/><br /><sub><b>techknowlogick</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=techknowlogick" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://log.c5t.org/about/"><img src="https://avatars1.githubusercontent.com/u/1449568?v=4?s=100" width="100px;" alt="waja"/><br /><sub><b>waja</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=waja" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://scottalbertson.com"><img src="https://avatars2.githubusercontent.com/u/154463?v=4?s=100" width="100px;" alt="Scott Albertson"/><br /><sub><b>Scott Albertson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=salbertson" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/huddlesj"><img src="https://avatars1.githubusercontent.com/u/11966535?v=4?s=100" width="100px;" alt="Jason Huddleston"/><br /><sub><b>Jason Huddleston</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=huddlesj" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://npstr.space/"><img src="https://avatars3.githubusercontent.com/u/6048348?v=4?s=100" width="100px;" alt="Napster"/><br /><sub><b>Napster</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=napstr" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/darknode"><img src="https://avatars1.githubusercontent.com/u/809429?v=4?s=100" width="100px;" alt="Maxim"/><br /><sub><b>Maxim</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=darknode" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://schmitt.cat"><img src="https://avatars0.githubusercontent.com/u/17984549?v=4?s=100" width="100px;" alt="Max Schmitt"/><br /><sub><b>Max Schmitt</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mxschmitt" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/cron410"><img src="https://avatars1.githubusercontent.com/u/3082899?v=4?s=100" width="100px;" alt="cron410"/><br /><sub><b>cron410</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=cron410" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Cardoso222"><img src="https://avatars3.githubusercontent.com/u/7026517?v=4?s=100" width="100px;" alt="Paulo Henrique"/><br /><sub><b>Paulo Henrique</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Cardoso222" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://coded.io"><img src="https://avatars0.githubusercontent.com/u/107097?v=4?s=100" width="100px;" alt="Kaleb Elwert"/><br /><sub><b>Kaleb Elwert</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=belak" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wmbutler"><img src="https://avatars1.githubusercontent.com/u/1254810?v=4?s=100" width="100px;" alt="Bill Butler"/><br /><sub><b>Bill Butler</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=wmbutler" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.mariotacke.io"><img src="https://avatars2.githubusercontent.com/u/4942019?v=4?s=100" width="100px;" alt="Mario Tacke"/><br /><sub><b>Mario Tacke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mariotacke" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://markwoodbridge.com"><img src="https://avatars2.githubusercontent.com/u/1101318?v=4?s=100" width="100px;" alt="Mark Woodbridge"/><br /><sub><b>Mark Woodbridge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mrw34" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ansem93"><img src="https://avatars3.githubusercontent.com/u/6626218?v=4?s=100" width="100px;" alt="Ansem93"/><br /><sub><b>Ansem93</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Ansem93" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lukapeschke"><img src="https://avatars1.githubusercontent.com/u/17085536?v=4?s=100" width="100px;" alt="Luka Peschke"/><br /><sub><b>Luka Peschke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zoispag"><img src="https://avatars0.githubusercontent.com/u/21138205?v=4?s=100" width="100px;" alt="Zois Pagoulatos"/><br /><sub><b>Zois Pagoulatos</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zoispag" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Azoispag" title="Reviewed Pull Requests">👀</a> <a href="#maintenance-zoispag" title="Maintenance">🚧</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://alexandre.menif.name"><img src="https://avatars0.githubusercontent.com/u/16152103?v=4?s=100" width="100px;" alt="Alexandre Menif"/><br /><sub><b>Alexandre Menif</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=alexandremenif" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chugunov"><img src="https://avatars1.githubusercontent.com/u/4140479?v=4?s=100" width="100px;" alt="Andrey"/><br /><sub><b>Andrey</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chugunov" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://noplanman.ch"><img src="https://avatars3.githubusercontent.com/u/9423417?v=4?s=100" width="100px;" alt="Armando Lüscher"/><br /><sub><b>Armando Lüscher</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=noplanman" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rjbudke"><img src="https://avatars2.githubusercontent.com/u/273485?v=4?s=100" width="100px;" alt="Ryan Budke"/><br /><sub><b>Ryan Budke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rjbudke" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://kaloyan.raev.name"><img src="https://avatars2.githubusercontent.com/u/468091?v=4?s=100" width="100px;" alt="Kaloyan Raev"/><br /><sub><b>Kaloyan Raev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sixth"><img src="https://avatars3.githubusercontent.com/u/11591445?v=4?s=100" width="100px;" alt="sixth"/><br /><sub><b>sixth</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixth" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://foosel.net"><img src="https://avatars0.githubusercontent.com/u/83657?v=4?s=100" width="100px;" alt="Gina Häußge"/><br /><sub><b>Gina Häußge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=foosel" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/8ear"><img src="https://avatars0.githubusercontent.com/u/10329648?v=4?s=100" width="100px;" alt="Max H."/><br /><sub><b>Max H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=8ear" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://pjknkda.github.io"><img src="https://avatars0.githubusercontent.com/u/4986524?v=4?s=100" width="100px;" alt="Jungkook Park"/><br /><sub><b>Jungkook Park</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pjknkda" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://achfrag.net"><img src="https://avatars1.githubusercontent.com/u/5753622?v=4?s=100" width="100px;" alt="Jan Kristof Nidzwetzki"/><br /><sub><b>Jan Kristof Nidzwetzki</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jnidzwetzki" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.lukaselsner.de"><img src="https://avatars0.githubusercontent.com/u/1413542?v=4?s=100" width="100px;" alt="lukas"/><br /><sub><b>lukas</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mindrunner" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://codingcoffee.dev"><img src="https://avatars3.githubusercontent.com/u/13611153?v=4?s=100" width="100px;" alt="Ameya Shenoy"/><br /><sub><b>Ameya Shenoy</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=codingCoffee" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/raymondelooff"><img src="https://avatars0.githubusercontent.com/u/9716806?v=4?s=100" width="100px;" alt="Raymon de Looff"/><br /><sub><b>Raymon de Looff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=raymondelooff" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://codemonkeylabs.com"><img src="https://avatars2.githubusercontent.com/u/704034?v=4?s=100" width="100px;" alt="John Clayton"/><br /><sub><b>John Clayton</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jsclayton" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Germs2004"><img src="https://avatars2.githubusercontent.com/u/5519340?v=4?s=100" width="100px;" alt="Germs2004"/><br /><sub><b>Germs2004</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Germs2004" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lukwil"><img src="https://avatars1.githubusercontent.com/u/30203234?v=4?s=100" width="100px;" alt="Lukas Willburger"/><br /><sub><b>Lukas Willburger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukwil" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/auanasgheps"><img src="https://avatars2.githubusercontent.com/u/20586878?v=4?s=100" width="100px;" alt="Oliver Cervera"/><br /><sub><b>Oliver Cervera</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=auanasgheps" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/victorcmoura"><img src="https://avatars1.githubusercontent.com/u/26290053?v=4?s=100" width="100px;" alt="Victor Moura"/><br /><sub><b>Victor Moura</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=victorcmoura" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mbrandau"><img src="https://avatars3.githubusercontent.com/u/12972798?v=4?s=100" width="100px;" alt="Maximilian Brandau"/><br /><sub><b>Maximilian Brandau</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aneisch"><img src="https://avatars1.githubusercontent.com/u/6991461?v=4?s=100" width="100px;" alt="Andrew"/><br /><sub><b>Andrew</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=aneisch" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sixcorners"><img src="https://avatars0.githubusercontent.com/u/585501?v=4?s=100" width="100px;" alt="sixcorners"/><br /><sub><b>sixcorners</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixcorners" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://arnested.dk"><img src="https://avatars2.githubusercontent.com/u/190005?v=4?s=100" width="100px;" alt="Arne Jørgensen"/><br /><sub><b>Arne Jørgensen</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=arnested" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Aarnested" title="Reviewed Pull Requests">👀</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/patski123"><img src="https://avatars1.githubusercontent.com/u/19295295?v=4?s=100" width="100px;" alt="PatSki123"/><br /><sub><b>PatSki123</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patski123" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://rubyroidlabs.com/"><img src="https://avatars2.githubusercontent.com/u/624999?v=4?s=100" width="100px;" alt="Valentine Zavadsky"/><br /><sub><b>Valentine Zavadsky</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=Saicheg" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bopoh24"><img src="https://avatars2.githubusercontent.com/u/4086631?v=4?s=100" width="100px;" alt="Alexander Voronin"/><br /><sub><b>Alexander Voronin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bopoh24" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/issues?q=author%3Abopoh24" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.teqneers.de"><img src="https://avatars0.githubusercontent.com/u/788989?v=4?s=100" width="100px;" alt="Oliver Mueller"/><br /><sub><b>Oliver Mueller</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ogmueller" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tammert"><img src="https://avatars0.githubusercontent.com/u/8885250?v=4?s=100" width="100px;" alt="Sebastiaan Tammer"/><br /><sub><b>Sebastiaan Tammer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tammert" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Miosame"><img src="https://avatars1.githubusercontent.com/u/8201077?v=4?s=100" width="100px;" alt="miosame"/><br /><sub><b>miosame</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=miosame" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://mtz.gr"><img src="https://avatars3.githubusercontent.com/u/590246?v=4?s=100" width="100px;" alt="Andrew Metzger"/><br /><sub><b>Andrew Metzger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/issues?q=author%3Aandrewjmetzger" title="Bug reports">🐛</a> <a href="#example-andrewjmetzger" title="Examples">💡</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pgrimaud"><img src="https://avatars1.githubusercontent.com/u/1866496?v=4?s=100" width="100px;" alt="Pierre Grimaud"/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pgrimaud" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mattdoran"><img src="https://avatars0.githubusercontent.com/u/577779?v=4?s=100" width="100px;" alt="Matt Doran"/><br /><sub><b>Matt Doran</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mattdoran" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MihailITPlace"><img src="https://avatars2.githubusercontent.com/u/28401551?v=4?s=100" width="100px;" alt="MihailITPlace"/><br /><sub><b>MihailITPlace</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MihailITPlace" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bugficks"><img src="https://avatars1.githubusercontent.com/u/2992895?v=4?s=100" width="100px;" alt="bugficks"/><br /><sub><b>bugficks</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=bugficks" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MichaelSp"><img src="https://avatars0.githubusercontent.com/u/448282?v=4?s=100" width="100px;" alt="Michael"/><br /><sub><b>Michael</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=MichaelSp" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jokay"><img src="https://avatars0.githubusercontent.com/u/18613935?v=4?s=100" width="100px;" alt="D. Domig"/><br /><sub><b>D. Domig</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jokay" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://maxwells-daemon.io"><img src="https://avatars1.githubusercontent.com/u/260084?v=4?s=100" width="100px;" alt="Ben Osheroff"/><br /><sub><b>Ben Osheroff</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=osheroff" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dhet"><img src="https://avatars3.githubusercontent.com/u/2668621?v=4?s=100" width="100px;" alt="David H."/><br /><sub><b>David H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=dhet" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.gridgeo.com"><img src="https://avatars1.githubusercontent.com/u/671887?v=4?s=100" width="100px;" alt="Chander Ganesan"/><br /><sub><b>Chander Ganesan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chander" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/yrien30"><img src="https://avatars1.githubusercontent.com/u/26816162?v=4?s=100" width="100px;" alt="yrien30"/><br /><sub><b>yrien30</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=yrien30" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ksurl"><img src="https://avatars1.githubusercontent.com/u/1371562?v=4?s=100" width="100px;" alt="ksurl"/><br /><sub><b>ksurl</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=ksurl" title="Code">💻</a> <a href="#infra-ksurl" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rg9400"><img src="https://avatars2.githubusercontent.com/u/39887349?v=4?s=100" width="100px;" alt="rg9400"/><br /><sub><b>rg9400</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rg9400" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tkalus"><img src="https://avatars2.githubusercontent.com/u/287181?v=4?s=100" width="100px;" alt="Turtle Kalus"/><br /><sub><b>Turtle Kalus</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tkalus" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SrihariThalla"><img src="https://avatars1.githubusercontent.com/u/7479937?v=4?s=100" width="100px;" alt="Srihari Thalla"/><br /><sub><b>Srihari Thalla</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=SrihariThalla" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://nymous.io"><img src="https://avatars1.githubusercontent.com/u/4216559?v=4?s=100" width="100px;" alt="Thomas Gaudin"/><br /><sub><b>Thomas Gaudin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=nymous" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://indigo.re/"><img src="https://avatars.githubusercontent.com/u/2804645?v=4?s=100" width="100px;" alt="hydrargyrum"/><br /><sub><b>hydrargyrum</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hydrargyrum" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://reinout.vanrees.org"><img src="https://avatars.githubusercontent.com/u/121433?v=4?s=100" width="100px;" alt="Reinout van Rees"/><br /><sub><b>Reinout van Rees</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=reinout" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DasSkelett"><img src="https://avatars.githubusercontent.com/u/28812678?v=4?s=100" width="100px;" alt="DasSkelett"/><br /><sub><b>DasSkelett</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=DasSkelett" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zenjabba"><img src="https://avatars.githubusercontent.com/u/679864?v=4?s=100" width="100px;" alt="zenjabba"/><br /><sub><b>zenjabba</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zenjabba" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://quan.io"><img src="https://avatars.githubusercontent.com/u/3526705?v=4?s=100" width="100px;" alt="Dan Quan"/><br /><sub><b>Dan Quan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=djquan" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/modem7"><img src="https://avatars.githubusercontent.com/u/4349962?v=4?s=100" width="100px;" alt="modem7"/><br /><sub><b>modem7</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=modem7" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hypnoglow"><img src="https://avatars.githubusercontent.com/u/4853075?v=4?s=100" width="100px;" alt="Igor Zibarev"/><br /><sub><b>Igor Zibarev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hypnoglow" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/patricegautier"><img src="https://avatars.githubusercontent.com/u/38435239?v=4?s=100" width="100px;" alt="Patrice"/><br /><sub><b>Patrice</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=patricegautier" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://jamesw.link/me"><img src="https://avatars.githubusercontent.com/u/8067792?v=4?s=100" width="100px;" alt="James White"/><br /><sub><b>James White</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jamesmacwhite" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://ko-fi.com/foxite"><img src="https://avatars.githubusercontent.com/u/20421657?v=4?s=100" width="100px;" alt="Dirk Kok"/><br /><sub><b>Dirk Kok</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Foxite" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EDIflyer"><img src="https://avatars.githubusercontent.com/u/13610277?v=4?s=100" width="100px;" alt="EDIflyer"/><br /><sub><b>EDIflyer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=EDIflyer" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jauderho"><img src="https://avatars.githubusercontent.com/u/13562?v=4?s=100" width="100px;" alt="Jauder Ho"/><br /><sub><b>Jauder Ho</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jauderho" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://tamal.vercel.app/"><img src="https://avatars.githubusercontent.com/u/72851613?v=4?s=100" width="100px;" alt="Tamal Das "/><br /><sub><b>Tamal Das </b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=IAmTamal" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/testwill"><img src="https://avatars.githubusercontent.com/u/8717479?v=4?s=100" width="100px;" alt="guangwu"/><br /><sub><b>guangwu</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=testwill" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://hub.lol"><img src="https://avatars.githubusercontent.com/u/48992448?v=4?s=100" width="100px;" alt="Florian Hübner"/><br /><sub><b>Florian Hübner</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=nothub" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=nothub" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/andriibratanin"><img src="https://avatars.githubusercontent.com/u/20169213?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Andrii Bratanin</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=andriibratanin" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
|
|
6
build.sh
6
build.sh
|
@ -1,5 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
BINFILE=watchtower
|
||||
if [ -n "$MSYSTEM" ]; then
|
||||
BINFILE=watchtower.exe
|
||||
fi
|
||||
VERSION=$(git describe --tags)
|
||||
echo "Building $VERSION..."
|
||||
go build -o watchtower -ldflags "-X github.com/containrrr/watchtower/internal/meta.Version=$VERSION"
|
||||
go build -o $BINFILE -ldflags "-X github.com/containrrr/watchtower/internal/meta.Version=$VERSION"
|
||||
|
|
111
cmd/notify-upgrade.go
Normal file
111
cmd/notify-upgrade.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
// Package cmd contains the watchtower (sub-)commands
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/flags"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/notifications"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var notifyUpgradeCommand = NewNotifyUpgradeCommand()
|
||||
|
||||
// NewNotifyUpgradeCommand creates the notify upgrade command for watchtower
|
||||
func NewNotifyUpgradeCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "notify-upgrade",
|
||||
Short: "Upgrade legacy notification configuration to shoutrrr URLs",
|
||||
Run: runNotifyUpgrade,
|
||||
}
|
||||
}
|
||||
|
||||
func runNotifyUpgrade(cmd *cobra.Command, args []string) {
|
||||
if err := runNotifyUpgradeE(cmd, args); err != nil {
|
||||
logf("Notification upgrade failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runNotifyUpgradeE(cmd *cobra.Command, _ []string) error {
|
||||
f := cmd.Flags()
|
||||
flags.ProcessFlagAliases(f)
|
||||
|
||||
notifier = notifications.NewNotifier(cmd)
|
||||
urls := notifier.GetURLs()
|
||||
|
||||
logf("Found notification configurations for: %v", strings.Join(notifier.GetNames(), ", "))
|
||||
|
||||
outFile, err := os.CreateTemp("/", "watchtower-notif-urls-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %v", err)
|
||||
}
|
||||
logf("Writing notification URLs to %v", outFile.Name())
|
||||
logf("")
|
||||
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString("WATCHTOWER_NOTIFICATION_URL=")
|
||||
|
||||
for i, u := range urls {
|
||||
if i != 0 {
|
||||
sb.WriteRune(' ')
|
||||
}
|
||||
sb.WriteString(u)
|
||||
}
|
||||
|
||||
_, err = fmt.Fprint(outFile, sb.String())
|
||||
tryOrLog(err, "Failed to write to output file")
|
||||
|
||||
tryOrLog(outFile.Sync(), "Failed to sync output file")
|
||||
tryOrLog(outFile.Close(), "Failed to close output file")
|
||||
|
||||
containerID := "<CONTAINER>"
|
||||
cid, err := container.GetRunningContainerID()
|
||||
tryOrLog(err, "Failed to get running container ID")
|
||||
if cid != "" {
|
||||
containerID = cid.ShortID()
|
||||
}
|
||||
logf("To get the environment file, use:")
|
||||
logf("cp %v:%v ./watchtower-notifications.env", containerID, outFile.Name())
|
||||
logf("")
|
||||
logf("Note: This file will be removed in 5 minutes or when this container is stopped!")
|
||||
|
||||
signalChannel := make(chan os.Signal, 1)
|
||||
time.AfterFunc(5*time.Minute, func() {
|
||||
signalChannel <- syscall.SIGALRM
|
||||
})
|
||||
|
||||
signal.Notify(signalChannel, os.Interrupt)
|
||||
signal.Notify(signalChannel, syscall.SIGTERM)
|
||||
|
||||
switch <-signalChannel {
|
||||
case syscall.SIGALRM:
|
||||
logf("Timed out!")
|
||||
case os.Interrupt, syscall.SIGTERM:
|
||||
logf("Stopping...")
|
||||
default:
|
||||
}
|
||||
|
||||
if err := os.Remove(outFile.Name()); err != nil {
|
||||
logf("Failed to remove file, it may still be present in the container image! Error: %v", err)
|
||||
} else {
|
||||
logf("Environment file has been removed.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func tryOrLog(err error, message string) {
|
||||
if err != nil {
|
||||
logf("%v: %v\n", message, err)
|
||||
}
|
||||
}
|
||||
|
||||
func logf(format string, v ...interface{}) {
|
||||
fmt.Fprintln(os.Stderr, fmt.Sprintf(format, v...))
|
||||
}
|
132
cmd/root.go
132
cmd/root.go
|
@ -1,7 +1,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/containrrr/watchtower/internal/meta"
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -11,12 +11,12 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
apiMetrics "github.com/containrrr/watchtower/pkg/api/metrics"
|
||||
"github.com/containrrr/watchtower/pkg/api/update"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/actions"
|
||||
"github.com/containrrr/watchtower/internal/flags"
|
||||
"github.com/containrrr/watchtower/internal/meta"
|
||||
"github.com/containrrr/watchtower/pkg/api"
|
||||
apiMetrics "github.com/containrrr/watchtower/pkg/api/metrics"
|
||||
"github.com/containrrr/watchtower/pkg/api/update"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/filters"
|
||||
"github.com/containrrr/watchtower/pkg/metrics"
|
||||
|
@ -29,18 +29,20 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
client container.Client
|
||||
scheduleSpec string
|
||||
cleanup bool
|
||||
noRestart bool
|
||||
monitorOnly bool
|
||||
enableLabel bool
|
||||
notifier t.Notifier
|
||||
timeout time.Duration
|
||||
lifecycleHooks bool
|
||||
rollingRestart bool
|
||||
scope string
|
||||
// Set on build using ldflags
|
||||
client container.Client
|
||||
scheduleSpec string
|
||||
cleanup bool
|
||||
noRestart bool
|
||||
noPull bool
|
||||
monitorOnly bool
|
||||
enableLabel bool
|
||||
disableContainers []string
|
||||
notifier t.Notifier
|
||||
timeout time.Duration
|
||||
lifecycleHooks bool
|
||||
rollingRestart bool
|
||||
scope string
|
||||
labelPrecedence bool
|
||||
)
|
||||
|
||||
var rootCmd = NewRootCommand()
|
||||
|
@ -56,6 +58,7 @@ func NewRootCommand() *cobra.Command {
|
|||
`,
|
||||
Run: Run,
|
||||
PreRun: PreRun,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,6 +71,7 @@ func init() {
|
|||
|
||||
// Execute the root func and exit in case of errors
|
||||
func Execute() {
|
||||
rootCmd.AddCommand(notifyUpgradeCommand)
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -76,37 +80,12 @@ func Execute() {
|
|||
// PreRun is a lifecycle hook that runs before the command is executed.
|
||||
func PreRun(cmd *cobra.Command, _ []string) {
|
||||
f := cmd.PersistentFlags()
|
||||
|
||||
if enabled, _ := f.GetBool("no-color"); enabled {
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: true,
|
||||
})
|
||||
} else {
|
||||
// enable logrus built-in support for https://bixense.com/clicolors/
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
EnvironmentOverrideColors: true,
|
||||
})
|
||||
flags.ProcessFlagAliases(f)
|
||||
if err := flags.SetupLogging(f); err != nil {
|
||||
log.Fatalf("Failed to initialize logging: %s", err.Error())
|
||||
}
|
||||
|
||||
if enabled, _ := f.GetBool("debug"); enabled {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
if enabled, _ := f.GetBool("trace"); enabled {
|
||||
log.SetLevel(log.TraceLevel)
|
||||
}
|
||||
|
||||
pollingSet := f.Changed("interval")
|
||||
schedule, _ := f.GetString("schedule")
|
||||
cronLen := len(schedule)
|
||||
|
||||
if pollingSet && cronLen > 0 {
|
||||
log.Fatal("Only schedule or interval can be defined, not both.")
|
||||
} else if cronLen > 0 {
|
||||
scheduleSpec, _ = f.GetString("schedule")
|
||||
} else {
|
||||
interval, _ := f.GetInt("interval")
|
||||
scheduleSpec = "@every " + strconv.Itoa(interval) + "s"
|
||||
}
|
||||
scheduleSpec, _ = f.GetString("schedule")
|
||||
|
||||
flags.GetSecretsFromFiles(cmd)
|
||||
cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd)
|
||||
|
@ -116,11 +95,15 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
}
|
||||
|
||||
enableLabel, _ = f.GetBool("label-enable")
|
||||
disableContainers, _ = f.GetStringSlice("disable-containers")
|
||||
lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
|
||||
rollingRestart, _ = f.GetBool("rolling-restart")
|
||||
scope, _ = f.GetString("scope")
|
||||
labelPrecedence, _ = f.GetBool("label-take-precedence")
|
||||
|
||||
log.Debug(scope)
|
||||
if scope != "" {
|
||||
log.Debugf(`Using scope %q`, scope)
|
||||
}
|
||||
|
||||
// configure environment vars for client
|
||||
err := flags.EnvConfig(cmd)
|
||||
|
@ -128,7 +111,7 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
noPull, _ := f.GetBool("no-pull")
|
||||
noPull, _ = f.GetBool("no-pull")
|
||||
includeStopped, _ := f.GetBool("include-stopped")
|
||||
includeRestarting, _ := f.GetBool("include-restarting")
|
||||
reviveStopped, _ := f.GetBool("revive-stopped")
|
||||
|
@ -139,26 +122,36 @@ func PreRun(cmd *cobra.Command, _ []string) {
|
|||
log.Warn("Using `WATCHTOWER_NO_PULL` and `WATCHTOWER_MONITOR_ONLY` simultaneously might lead to no action being taken at all. If this is intentional, you may safely ignore this message.")
|
||||
}
|
||||
|
||||
client = container.NewClient(
|
||||
!noPull,
|
||||
includeStopped,
|
||||
reviveStopped,
|
||||
removeVolumes,
|
||||
includeRestarting,
|
||||
warnOnHeadPullFailed,
|
||||
)
|
||||
client = container.NewClient(container.ClientOptions{
|
||||
IncludeStopped: includeStopped,
|
||||
ReviveStopped: reviveStopped,
|
||||
RemoveVolumes: removeVolumes,
|
||||
IncludeRestarting: includeRestarting,
|
||||
WarnOnHeadFailed: container.WarningStrategy(warnOnHeadPullFailed),
|
||||
})
|
||||
|
||||
notifier = notifications.NewNotifier(cmd)
|
||||
notifier.AddLogHook()
|
||||
}
|
||||
|
||||
// Run is the main execution flow of the command
|
||||
func Run(c *cobra.Command, names []string) {
|
||||
filter, filterDesc := filters.BuildFilter(names, enableLabel, scope)
|
||||
filter, filterDesc := filters.BuildFilter(names, disableContainers, enableLabel, scope)
|
||||
runOnce, _ := c.PersistentFlags().GetBool("run-once")
|
||||
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
|
||||
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")
|
||||
unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls")
|
||||
apiToken, _ := c.PersistentFlags().GetString("http-api-token")
|
||||
healthCheck, _ := c.PersistentFlags().GetBool("health-check")
|
||||
|
||||
if healthCheck {
|
||||
// health check should not have pid 1
|
||||
if os.Getpid() == 1 {
|
||||
time.Sleep(1 * time.Second)
|
||||
log.Fatal("The health check flag should never be passed to the main watchtower container process")
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if rollingRestart && monitorOnly {
|
||||
log.Fatal("Rolling restarts is not compatible with the global monitor only flag")
|
||||
|
@ -189,9 +182,12 @@ func Run(c *cobra.Command, names []string) {
|
|||
httpAPI := api.New(apiToken)
|
||||
|
||||
if enableUpdateAPI {
|
||||
updateHandler := update.New(func() { runUpdatesWithNotifications(filter) }, updateLock)
|
||||
updateHandler := update.New(func(images []string) {
|
||||
metric := runUpdatesWithNotifications(filters.FilterByImage(images, filter))
|
||||
metrics.RegisterScan(metric)
|
||||
}, updateLock)
|
||||
httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle)
|
||||
// If polling isn't enabled the scheduler is never started and
|
||||
// If polling isn't enabled the scheduler is never started, and
|
||||
// we need to trigger the startup messages manually.
|
||||
if !unblockHTTPAPI {
|
||||
writeStartupMessage(c, time.Time{}, filterDesc)
|
||||
|
@ -203,7 +199,7 @@ func Run(c *cobra.Command, names []string) {
|
|||
httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle)
|
||||
}
|
||||
|
||||
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && err != http.ErrServerClosed {
|
||||
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Error("failed to start API", err)
|
||||
}
|
||||
|
||||
|
@ -293,7 +289,7 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {
|
|||
startupLog.Info("Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST"))
|
||||
startupLog.Info("Note that the first check will be performed in " + until)
|
||||
} else if runOnce, _ := c.PersistentFlags().GetBool("run-once"); runOnce {
|
||||
startupLog.Info("Running a one time update.")
|
||||
startupLog.Info("Running a one time update.")
|
||||
} else {
|
||||
startupLog.Info("Periodic runs are not enabled.")
|
||||
}
|
||||
|
@ -363,13 +359,15 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string,
|
|||
func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
|
||||
notifier.StartNotification()
|
||||
updateParams := t.UpdateParams{
|
||||
Filter: filter,
|
||||
Cleanup: cleanup,
|
||||
NoRestart: noRestart,
|
||||
Timeout: timeout,
|
||||
MonitorOnly: monitorOnly,
|
||||
LifecycleHooks: lifecycleHooks,
|
||||
RollingRestart: rollingRestart,
|
||||
Filter: filter,
|
||||
Cleanup: cleanup,
|
||||
NoRestart: noRestart,
|
||||
Timeout: timeout,
|
||||
MonitorOnly: monitorOnly,
|
||||
LifecycleHooks: lifecycleHooks,
|
||||
RollingRestart: rollingRestart,
|
||||
LabelPrecedence: labelPrecedence,
|
||||
NoPull: noPull,
|
||||
}
|
||||
result, err := actions.Update(client, updateParams)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM --platform=$BUILDPLATFORM alpine:3.15 as alpine
|
||||
FROM --platform=$BUILDPLATFORM alpine:3.19.0 as alpine
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
|
@ -17,4 +17,7 @@ COPY --from=alpine \
|
|||
EXPOSE 8080
|
||||
|
||||
COPY watchtower /
|
||||
|
||||
HEALTHCHECK CMD [ "/watchtower", "--health-check"]
|
||||
|
||||
ENTRYPOINT ["/watchtower"]
|
||||
|
|
|
@ -7,6 +7,13 @@ FROM golang:alpine as builder
|
|||
# use version (for example "v0.3.3") or "main"
|
||||
ARG WATCHTOWER_VERSION=main
|
||||
|
||||
# Pre download required modules to avoid redownloading at each build thanks to docker layer caching.
|
||||
# Copying go.mod and go.sum ensure to invalid the layer/build cache if there is a change in module requirement
|
||||
WORKDIR /watchtower
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN go mod download
|
||||
|
||||
RUN apk add --no-cache \
|
||||
alpine-sdk \
|
||||
ca-certificates \
|
||||
|
@ -35,4 +42,6 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifi
|
|||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
COPY --from=builder /watchtower/watchtower /watchtower
|
||||
|
||||
HEALTHCHECK CMD [ "/watchtower", "--health-check"]
|
||||
|
||||
ENTRYPOINT ["/watchtower"]
|
||||
|
|
|
@ -35,4 +35,6 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifi
|
|||
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
COPY --from=builder /go/watchtower/watchtower /watchtower
|
||||
|
||||
HEALTHCHECK CMD [ "/watchtower", "--health-check"]
|
||||
|
||||
ENTRYPOINT ["/watchtower"]
|
||||
|
|
17
dockerfiles/container-networking/docker-compose.yml
Normal file
17
dockerfiles/container-networking/docker-compose.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
services:
|
||||
producer:
|
||||
image: qmcgaw/gluetun:v3.35.0
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
- VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
|
||||
- OPENVPN_USER=${OPENVPN_USER}
|
||||
- OPENVPN_PASSWORD=${OPENVPN_PASSWORD}
|
||||
- SERVER_COUNTRIES=${SERVER_COUNTRIES}
|
||||
consumer:
|
||||
depends_on:
|
||||
- producer
|
||||
image: nginx:1.25.1
|
||||
network_mode: "service:producer"
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.depends-on=/wt-contnet-producer-1"
|
3
docs-requirements.txt
Normal file
3
docs-requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
mkdocs
|
||||
mkdocs-material
|
||||
md-toc
|
|
@ -27,6 +27,33 @@ In the example above, watchtower will execute an upgrade attempt on the containe
|
|||
|
||||
When no arguments are specified, watchtower will monitor all running containers.
|
||||
|
||||
## Secrets/Files
|
||||
|
||||
Some arguments can also reference a file, in which case the contents of the file are used as the value.
|
||||
This can be used to avoid putting secrets in the configuration file or command line.
|
||||
|
||||
The following arguments are currently supported (including their corresponding `WATCHTOWER_` environment variables):
|
||||
- `notification-url`
|
||||
- `notification-email-server-password`
|
||||
- `notification-slack-hook-url`
|
||||
- `notification-msteams-hook`
|
||||
- `notification-gotify-token`
|
||||
- `http-api-token`
|
||||
|
||||
### Example docker-compose usage
|
||||
```yaml
|
||||
secrets:
|
||||
access_token:
|
||||
file: access_token
|
||||
|
||||
services:
|
||||
watchtower:
|
||||
secrets:
|
||||
- access_token
|
||||
environment:
|
||||
- WATCHTOWER_HTTP_API_TOKEN=/run/secrets/access_token
|
||||
```
|
||||
|
||||
## Help
|
||||
Shows documentation about the supported flags.
|
||||
|
||||
|
@ -58,8 +85,8 @@ Environment Variable: WATCHTOWER_CLEANUP
|
|||
Default: false
|
||||
```
|
||||
|
||||
## Remove attached volumes
|
||||
Removes attached volumes after updating. When this flag is specified, watchtower will remove all attached volumes from the container before restarting with a new image. Use this option to force new volumes to be populated as containers are updated.
|
||||
## Remove anonymous volumes
|
||||
Removes anonymous volumes after updating. When this flag is specified, watchtower will remove all anonymous volumes from the container before restarting with a new image. Named volumes will not be removed!
|
||||
|
||||
```text
|
||||
Argument: --remove-volumes
|
||||
|
@ -71,6 +98,10 @@ Environment Variable: WATCHTOWER_REMOVE_VOLUMES
|
|||
## Debug
|
||||
Enable debug mode with verbose logging.
|
||||
|
||||
!!! note "Notes"
|
||||
Alias for `--log-level debug`. See [Maximum log level](#maximum-log-level).
|
||||
Does _not_ take an argument when used as an argument. Using `--debug true` will **not** work.
|
||||
|
||||
```text
|
||||
Argument: --debug, -d
|
||||
Environment Variable: WATCHTOWER_DEBUG
|
||||
|
@ -81,6 +112,10 @@ Environment Variable: WATCHTOWER_DEBUG
|
|||
## Trace
|
||||
Enable trace mode with very verbose logging. Caution: exposes credentials!
|
||||
|
||||
!!! note "Notes"
|
||||
Alias for `--log-level trace`. See [Maximum log level](#maximum-log-level).
|
||||
Does _not_ take an argument when used as an argument. Using `--trace true` will **not** work.
|
||||
|
||||
```text
|
||||
Argument: --trace
|
||||
Environment Variable: WATCHTOWER_TRACE
|
||||
|
@ -88,6 +123,28 @@ Environment Variable: WATCHTOWER_TRACE
|
|||
Default: false
|
||||
```
|
||||
|
||||
## Maximum log level
|
||||
|
||||
The maximum log level that will be written to STDERR (shown in `docker log` when used in a container).
|
||||
|
||||
```text
|
||||
Argument: --log-level
|
||||
Environment Variable: WATCHTOWER_LOG_LEVEL
|
||||
Possible values: panic, fatal, error, warn, info, debug or trace
|
||||
Default: info
|
||||
```
|
||||
|
||||
## Logging format
|
||||
|
||||
Sets what logging format to use for console output.
|
||||
|
||||
```text
|
||||
Argument: --log-format, -l
|
||||
Environment Variable: WATCHTOWER_LOG_FORMAT
|
||||
Possible values: Auto, LogFmt, Pretty or JSON
|
||||
Default: Auto
|
||||
```
|
||||
|
||||
## ANSI colors
|
||||
Disable ANSI color escape codes in log output.
|
||||
|
||||
|
@ -132,7 +189,7 @@ Environment Variable: WATCHTOWER_INCLUDE_RESTARTING
|
|||
Will also include created and exited containers.
|
||||
|
||||
```text
|
||||
Argument: --include-stopped
|
||||
Argument: --include-stopped, -S
|
||||
Environment Variable: WATCHTOWER_INCLUDE_STOPPED
|
||||
Type: Boolean
|
||||
Default: false
|
||||
|
@ -159,7 +216,7 @@ Environment Variable: WATCHTOWER_POLL_INTERVAL
|
|||
```
|
||||
|
||||
## Filter by enable label
|
||||
Update containers that have a `com.centurylinklabs.watchtower.enable` label set to true.
|
||||
Monitor and update containers that have a `com.centurylinklabs.watchtower.enable` label set to true.
|
||||
|
||||
```text
|
||||
Argument: --label-enable
|
||||
|
@ -169,10 +226,23 @@ Environment Variable: WATCHTOWER_LABEL_ENABLE
|
|||
```
|
||||
|
||||
## Filter by disable label
|
||||
__Do not__ update containers that have `com.centurylinklabs.watchtower.enable` label set to false and
|
||||
__Do not__ Monitor and update containers that have `com.centurylinklabs.watchtower.enable` label set to false and
|
||||
no `--label-enable` argument is passed. Note that only one or the other (targeting by enable label) can be
|
||||
used at the same time to target containers.
|
||||
|
||||
## Filter by disabling specific container names
|
||||
Monitor and update containers whose names are not in a given set of names.
|
||||
|
||||
This can be used to exclude specific containers, when setting labels is not an option.
|
||||
The listed containers will be excluded even if they have the enable filter set to true.
|
||||
|
||||
```text
|
||||
Argument: --disable-containers, -x
|
||||
Environment Variable: WATCHTOWER_DISABLE_CONTAINERS
|
||||
Type: Comma- or space-separated string list
|
||||
Default: ""
|
||||
```
|
||||
|
||||
## Without updating containers
|
||||
Will only monitor for new images, send notifications and invoke
|
||||
the [pre-check/post-check hooks](https://containrrr.dev/watchtower/lifecycle-hooks/), but will __not__ update the
|
||||
|
@ -192,6 +262,19 @@ Environment Variable: WATCHTOWER_MONITOR_ONLY
|
|||
|
||||
Note that monitor-only can also be specified on a per-container basis with the `com.centurylinklabs.watchtower.monitor-only` label set on those containers.
|
||||
|
||||
See [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set
|
||||
|
||||
## With label taking precedence over arguments
|
||||
|
||||
By default, arguments will take precedence over labels. This means that if you set `WATCHTOWER_MONITOR_ONLY` to true or use `--monitor-only`, a container with `com.centurylinklabs.watchtower.monitor-only` set to false will not be updated. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will also be updated. This also apply to the no pull option. if you set `WATCHTOWER_NO_PULL` to true or use `--no-pull`, a container with `com.centurylinklabs.watchtower.no-pull` set to false will not pull the new image. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will pull image
|
||||
|
||||
```text
|
||||
Argument: --label-take-precedence
|
||||
Environment Variable: WATCHTOWER_LABEL_TAKE_PRECEDENCE
|
||||
Type: Boolean
|
||||
Default: false
|
||||
```
|
||||
|
||||
## Without restarting containers
|
||||
Do not restart containers after updating. This option can be useful when the start of the containers
|
||||
is managed by an external system such as systemd.
|
||||
|
@ -215,6 +298,11 @@ Environment Variable: WATCHTOWER_NO_PULL
|
|||
Default: false
|
||||
```
|
||||
|
||||
Note that no-pull can also be specified on a per-container basis with the
|
||||
`com.centurylinklabs.watchtower.no-pull` label set on those containers.
|
||||
|
||||
See [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set
|
||||
|
||||
## Without sending a startup message
|
||||
Do not send a message after watchtower started. Otherwise there will be an info-level notification.
|
||||
|
||||
|
@ -229,7 +317,7 @@ Environment Variable: WATCHTOWER_NO_STARTUP_MESSAGE
|
|||
Run an update attempt against a container name list one time immediately and exit.
|
||||
|
||||
```text
|
||||
Argument: --run-once
|
||||
Argument: --run-once, -R
|
||||
Environment Variable: WATCHTOWER_RUN_ONCE
|
||||
Type: Boolean
|
||||
Default: false
|
||||
|
@ -248,6 +336,7 @@ Environment Variable: WATCHTOWER_HTTP_API_UPDATE
|
|||
|
||||
## HTTP API Token
|
||||
Sets an authentication token to HTTP API requests.
|
||||
Can also reference a file, in which case the contents of the file are used.
|
||||
|
||||
```text
|
||||
Argument: --http-api-token
|
||||
|
@ -270,6 +359,11 @@ Environment Variable: WATCHTOWER_HTTP_API_PERIODIC_POLLS
|
|||
Update containers that have a `com.centurylinklabs.watchtower.scope` label set with the same value as the given argument.
|
||||
This enables [running multiple instances](https://containrrr.dev/watchtower/running-multiple-instances).
|
||||
|
||||
!!! note "Filter by lack of scope"
|
||||
If you want other instances of watchtower to ignore the scoped containers, set this argument to `none`.
|
||||
When omitted, watchtower will update all containers regardless of scope.
|
||||
|
||||
|
||||
```text
|
||||
Argument: --scope
|
||||
Environment Variable: WATCHTOWER_SCOPE
|
||||
|
@ -342,3 +436,32 @@ Environment Variable: WATCHTOWER_WARN_ON_HEAD_FAILURE
|
|||
Possible values: always, auto, never
|
||||
Default: auto
|
||||
```
|
||||
|
||||
## Health check
|
||||
|
||||
Returns a success exit code to enable usage with docker `HEALTHCHECK`. This check is naive and only returns checks whether there is another process running inside the container, as it is the only known form of failure state for watchtowers container.
|
||||
|
||||
!!! note "Only for HEALTHCHECK use"
|
||||
Never put this on the main container executable command line as it is only meant to be run from docker HEALTHCHECK.
|
||||
|
||||
```text
|
||||
Argument: --health-check
|
||||
```
|
||||
|
||||
## Programatic Output (porcelain)
|
||||
|
||||
Writes the session results to STDOUT using a stable, machine-readable format (indicated by the argument VERSION).
|
||||
|
||||
Alias for:
|
||||
|
||||
```text
|
||||
--notification-url logger://
|
||||
--notification-log-stdout
|
||||
--notification-report
|
||||
--notification-template porcelain.VERSION.summary-no-log
|
||||
|
||||
Argument: --porcelain, -P
|
||||
Environment Variable: WATCHTOWER_PORCELAIN
|
||||
Possible values: v1
|
||||
Default: -
|
||||
```
|
||||
|
|
|
@ -7,33 +7,58 @@ There are two options:
|
|||
|
||||
## Full Exclude
|
||||
|
||||
If you need to exclude some containers, set the _com.centurylinklabs.watchtower.enable_ label to `false`.
|
||||
If you need to exclude some containers, set the _com.centurylinklabs.watchtower.enable_ label to `false`. For clarity this should be set **on the container(s)** you wish to be ignored, this is not set on watchtower.
|
||||
|
||||
```docker
|
||||
LABEL com.centurylinklabs.watchtower.enable="false"
|
||||
```
|
||||
=== "dockerfile"
|
||||
|
||||
Or, it can be specified as part of the `docker run` command line:
|
||||
```docker
|
||||
LABEL com.centurylinklabs.watchtower.enable="false"
|
||||
```
|
||||
=== "docker run"
|
||||
|
||||
```bash
|
||||
docker run -d --label=com.centurylinklabs.watchtower.enable=false someimage
|
||||
```
|
||||
```bash
|
||||
docker run -d --label=com.centurylinklabs.watchtower.enable=false someimage
|
||||
```
|
||||
|
||||
If you need to [include only containers with the enable label](https://containrrr.github.io/watchtower/arguments/#filter_by_enable_label), pass the `--label-enable` flag or the `WATCHTOWER_LABEL_ENABLE` environment variable on startup and set the _com.centurylinklabs.watchtower.enable_ label with a value of `true` for the containers you want to watch.
|
||||
=== "docker-compose"
|
||||
|
||||
```docker
|
||||
LABEL com.centurylinklabs.watchtower.enable="true"
|
||||
```
|
||||
``` yaml
|
||||
version: "3"
|
||||
services:
|
||||
someimage:
|
||||
container_name: someimage
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
```
|
||||
|
||||
Or, it can be specified as part of the `docker run` command line:
|
||||
If instead you want to [only include containers with the enable label](https://containrrr.github.io/watchtower/arguments/#filter_by_enable_label), pass the `--label-enable` flag or the `WATCHTOWER_LABEL_ENABLE` environment variable on startup for watchtower and set the _com.centurylinklabs.watchtower.enable_ label with a value of `true` on the containers you want to watch.
|
||||
|
||||
```bash
|
||||
docker run -d --label=com.centurylinklabs.watchtower.enable=true someimage
|
||||
```
|
||||
=== "dockerfile"
|
||||
|
||||
```docker
|
||||
LABEL com.centurylinklabs.watchtower.enable="true"
|
||||
```
|
||||
=== "docker run"
|
||||
|
||||
```bash
|
||||
docker run -d --label=com.centurylinklabs.watchtower.enable=true someimage
|
||||
```
|
||||
|
||||
=== "docker-compose"
|
||||
|
||||
``` yaml
|
||||
version: "3"
|
||||
services:
|
||||
someimage:
|
||||
container_name: someimage
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
```
|
||||
|
||||
If you wish to create a monitoring scope, you will need to [run multiple instances and set a scope for each of them](https://containrrr.github.io/watchtower/running-multiple-instances).
|
||||
|
||||
Watchtower filters running containers by testing them against each configured criteria. A container is monitored if all criteria are met. For example:
|
||||
|
||||
- If a container's name is on the monitoring name list (not empty `--name` argument) but it is not enabled (_centurylinklabs.watchtower.enable=false_), it won't be monitored;
|
||||
- If a container's name is not on the monitoring name list (not empty `--name` argument), even if it is enabled (_centurylinklabs.watchtower.enable=true_ and `--label-enable` flag is set), it won't be monitored;
|
||||
|
||||
|
|
|
@ -35,3 +35,11 @@ Notice that there is an environment variable named WATCHTOWER_HTTP_API_TOKEN. To
|
|||
```bash
|
||||
curl -H "Authorization: Bearer mytoken" localhost:8080/v1/update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
In order to update only certain images, the image names can be provided as URL query parameters. The following `curl` command would trigger an update for the images `foo/bar` and `foo/baz`:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer mytoken" localhost:8080/v1/update?image=foo/bar,foo/baz
|
||||
```
|
||||
|
|
|
@ -11,5 +11,5 @@ CONTAINER ID IMAGE STATUS PORTS
|
|||
6cc4d2a9d1a5 containrrr/watchtower Up 15 minutes watchtower
|
||||
```
|
||||
|
||||
Every few minutes watchtower will pull the latest _centurylink/wetty-cli_ image and compare it to the one that was used to run the "wetty" container. If it sees that the image has changed it will stop/remove the "wetty" container and then restart it using the new image and the same `docker run` options that were used to start the container initially (in this case, that would include the `-p 8080:3000` port mapping).
|
||||
Every day watchtower will pull the latest _centurylink/wetty-cli_ image and compare it to the one that was used to run the "wetty" container. If it sees that the image has changed it will stop/remove the "wetty" container and then restart it using the new image and the same `docker run` options that were used to start the container initially (in this case, that would include the `-p 8080:3000` port mapping).
|
||||
|
||||
|
|
|
@ -2,4 +2,6 @@ Watchtower will detect if there are links between any of the running containers
|
|||
|
||||
For example, imagine you were running a _mysql_ container and a _wordpress_ container which had been linked to the _mysql_ container. If watchtower were to detect that the _mysql_ container required an update, it would first shut down the linked _wordpress_ container followed by the _mysql_ container. When restarting the containers it would handle _mysql_ first and then _wordpress_ to ensure that the link continued to work.
|
||||
|
||||
If you want to override existing links you can use special `com.centurylinklabs.watchtower.depends-on` label with dependent container names, separated by a comma.
|
||||
If you want to override existing links, or if you are not using links, you can use special `com.centurylinklabs.watchtower.depends-on` label with dependent container names, separated by a comma.
|
||||
|
||||
When you have a depending container that is using `network_mode: service:container` then watchtower will treat that container as an implicit link.
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Metrics can be used to track how Watchtower behaves over time.
|
||||
|
||||
To use this feature, you have to set an [API token](arguments.md#http-api-token) and [enable the metrics API](arguments.md#http-api-metrics),
|
||||
To use this feature, you have to set an [API token](arguments.md#http_api_token) and [enable the metrics API](arguments.md#http_api_metrics),
|
||||
as well as creating a port mapping for your container for port `8080`.
|
||||
|
||||
The metrics API endpoint is `/v1/metrics`.
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
# Notifications
|
||||
|
||||
Watchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging
|
||||
system, [logrus](http://github.com/sirupsen/logrus). The types of notifications to send are set by passing a
|
||||
comma-separated list of values to the `--notifications` option
|
||||
(or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values:
|
||||
|
||||
- `email` to send notifications via e-mail
|
||||
- `slack` to send notifications through a Slack webhook
|
||||
- `msteams` to send notifications via MSTeams webhook
|
||||
- `gotify` to send notifications via Gotify
|
||||
- `shoutrrr` to send notifications via [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr)
|
||||
system, [logrus](http://github.com/sirupsen/logrus).
|
||||
|
||||
!!! note "Using multiple notifications with environment variables"
|
||||
There is currently a bug in Viper (https://github.com/spf13/viper/issues/380), which prevents comma-separated slices to
|
||||
|
@ -26,9 +18,231 @@ comma-separated list of values to the `--notifications` option
|
|||
|
||||
- `--notifications-level` (env. `WATCHTOWER_NOTIFICATIONS_LEVEL`): Controls the log level which is used for the notifications. If omitted, the default log level is `info`. Possible values are: `panic`, `fatal`, `error`, `warn`, `info`, `debug` or `trace`.
|
||||
- `--notifications-hostname` (env. `WATCHTOWER_NOTIFICATIONS_HOSTNAME`): Custom hostname specified in subject/title. Useful to override the operating system hostname.
|
||||
- `--notifications-delay` (env. `WATCHTOWER_NOTIFICATIONS_DELAY`): Delay before sending notifications expressed in seconds.
|
||||
- Watchtower will post a notification every time it is started. This behavior [can be changed](https://containrrr.github.io/watchtower/arguments/#without_sending_a_startup_message) with an argument.
|
||||
- `--notification-title-tag` (env. `WATCHTOWER_NOTIFICATION_TITLE_TAG`): Prefix to include in the title. Useful when running multiple watchtowers.
|
||||
- `--notification-skip-title` (env. `WATCHTOWER_NOTIFICATION_SKIP_TITLE`): Do not pass the title param to notifications. This will not pass a dynamic title override to notification services. If no title is configured for the service, it will remove the title all together.
|
||||
- `--notification-log-stdout` (env. `WATCHTOWER_NOTIFICATION_LOG_STDOUT`): Enable output from `logger://` shoutrrr service to stdout.
|
||||
|
||||
## Available services
|
||||
## [Shoutrrr](https://github.com/containrrr/shoutrrr) notifications
|
||||
|
||||
To send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set:
|
||||
|
||||
- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. This option can also reference a file, in which case the contents of the file are used.
|
||||
|
||||
|
||||
Go to [containrrr.dev/shoutrrr/v0.8/services/overview](https://containrrr.dev/shoutrrr/v0.8/services/overview) to
|
||||
learn more about the different service URLs you can use. You can define multiple services by space separating the
|
||||
URLs. (See example below)
|
||||
|
||||
You can customize the message posted by setting a template.
|
||||
|
||||
- `--notification-template` (env. `WATCHTOWER_NOTIFICATION_TEMPLATE`): The template used for the message.
|
||||
|
||||
The template is a Go [template](https://golang.org/pkg/text/template/) that either format a list
|
||||
of [log entries](https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry) or a `notification.Data` struct.
|
||||
|
||||
Simple templates are used unless the `notification-report` flag is specified:
|
||||
|
||||
- `--notification-report` (env. `WATCHTOWER_NOTIFICATION_REPORT`): Use the session report as the notification template data.
|
||||
|
||||
## Simple templates
|
||||
|
||||
The default value if not set is `{{range .}}{{.Message}}{{println}}{{end}}`. The example below uses a template that also
|
||||
outputs timestamp and log level.
|
||||
|
||||
!!! tip "Custom date format"
|
||||
If you want to adjust the date/time format it must show how the
|
||||
[reference time](https://golang.org/pkg/time/#pkg-constants) (_Mon Jan 2 15:04:05 MST 2006_) would be displayed in your
|
||||
custom format.
|
||||
i.e., The day of the year has to be 1, the month has to be 2 (february), the hour 3 (or 15 for 24h time) etc.
|
||||
|
||||
!!! note "Skipping notifications"
|
||||
To skip sending notifications that do not contain any information, you can wrap your template with `{{if .}}` and `{{end}}`.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \
|
||||
-e WATCHTOWER_NOTIFICATION_TEMPLATE="{{range .}}{{.Time.Format \"2006-01-02 15:04:05\"}} ({{.Level}}): {{.Message}}{{println}}{{end}}" \
|
||||
containrrr/watchtower
|
||||
```
|
||||
|
||||
## Report templates
|
||||
|
||||
The default template for report notifications are the following:
|
||||
```go
|
||||
{{- if .Report -}}
|
||||
{{- with .Report -}}
|
||||
{{- if ( or .Updated .Failed ) -}}
|
||||
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
|
||||
{{- range .Updated}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
|
||||
{{- end -}}
|
||||
{{- range .Fresh}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}
|
||||
{{- end -}}
|
||||
{{- range .Skipped}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- range .Failed}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
|
||||
{{- end -}}
|
||||
```
|
||||
|
||||
It will be used to send a summary of every session if there are any containers that were updated or which failed to update.
|
||||
|
||||
!!! note "Skipping notifications"
|
||||
Whenever the result of applying the template results in an empty string, no notifications will
|
||||
be sent. This is by default used to limit the notifications to only be sent when there something noteworthy occurred.
|
||||
|
||||
You can replace `{{- if ( or .Updated .Failed ) -}}` with any logic you want to decide when to send the notifications.
|
||||
|
||||
Example using a custom report template that always sends a session report after each run:
|
||||
|
||||
=== "docker run"
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WATCHTOWER_NOTIFICATION_REPORT="true" \
|
||||
-e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \
|
||||
-e WATCHTOWER_NOTIFICATION_TEMPLATE="
|
||||
{{- if .Report -}}
|
||||
{{- with .Report -}}
|
||||
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
|
||||
{{- range .Updated}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
|
||||
{{- end -}}
|
||||
{{- range .Fresh}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}
|
||||
{{- end -}}
|
||||
{{- range .Skipped}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- range .Failed}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{range .Entries -}}{{.Message}}{{\"\n\"}}{{- end -}}
|
||||
{{- end -}}
|
||||
" \
|
||||
containrrr/watchtower
|
||||
```
|
||||
|
||||
=== "docker-compose"
|
||||
|
||||
``` yaml
|
||||
version: "3"
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
env:
|
||||
WATCHTOWER_NOTIFICATION_REPORT: "true"
|
||||
WATCHTOWER_NOTIFICATION_URL: >
|
||||
discord://token@channel
|
||||
slack://watchtower@token-a/token-b/token-c
|
||||
WATCHTOWER_NOTIFICATION_TEMPLATE: |
|
||||
{{- if .Report -}}
|
||||
{{- with .Report -}}
|
||||
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
|
||||
{{- range .Updated}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
|
||||
{{- end -}}
|
||||
{{- range .Fresh}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}
|
||||
{{- end -}}
|
||||
{{- range .Skipped}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- range .Failed}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
|
||||
{{- end -}}
|
||||
```
|
||||
|
||||
## Legacy notifications
|
||||
|
||||
For backwards compatibility, the notifications can also be configured using legacy notification options. These will automatically be converted to shoutrrr URLs when used.
|
||||
The types of notifications to send are set by passing a comma-separated list of values to the `--notifications` option
|
||||
(or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values:
|
||||
|
||||
- `email` to send notifications via e-mail
|
||||
- `slack` to send notifications through a Slack webhook
|
||||
- `msteams` to send notifications via MSTeams webhook
|
||||
- `gotify` to send notifications via Gotify
|
||||
|
||||
### `notify-upgrade`
|
||||
If watchtower is started with `notify-upgrade` as it's first argument, it will generate a .env file with your current legacy notification options converted to shoutrrr URLs.
|
||||
|
||||
=== "docker run"
|
||||
|
||||
```bash
|
||||
$ docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WATCHTOWER_NOTIFICATIONS=slack \
|
||||
-e WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL="https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy" \
|
||||
containrrr/watchtower \
|
||||
notify-upgrade
|
||||
```
|
||||
|
||||
=== "docker-compose.yml"
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
env:
|
||||
WATCHTOWER_NOTIFICATIONS: slack
|
||||
WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL: https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy
|
||||
command: notify-upgrade
|
||||
```
|
||||
|
||||
|
||||
You can then copy this file from the container (a message with the full command to do so will be logged) and use it with your current setup:
|
||||
|
||||
=== "docker run"
|
||||
|
||||
```bash
|
||||
$ docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--env-file watchtower-notifications.env \
|
||||
containrrr/watchtower
|
||||
```
|
||||
|
||||
=== "docker-compose.yml"
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
env_file:
|
||||
- watchtower-notifications.env
|
||||
```
|
||||
|
||||
### Email
|
||||
|
||||
|
@ -42,7 +256,7 @@ To receive notifications by email, the following command-line options, or their
|
|||
- `--notification-email-server-user` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER`): The username to authenticate with the SMTP server with.
|
||||
- `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with. Can also reference a file, in which case the contents of the file are used.
|
||||
- `--notification-email-delay` (env. `WATCHTOWER_NOTIFICATION_EMAIL_DELAY`): Delay before sending notifications expressed in seconds.
|
||||
- `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers.
|
||||
- `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers. **NOTE:** This will affect all notification types.
|
||||
|
||||
Example:
|
||||
|
||||
|
@ -174,40 +388,3 @@ docker run -d \
|
|||
|
||||
If you want to disable TLS verification for the Gotify instance, you can use either `-e WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY=true` or `--notification-gotify-tls-skip-verify`.
|
||||
|
||||
### [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr)
|
||||
|
||||
To send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set:
|
||||
|
||||
- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used.
|
||||
|
||||
Go to [containrrr.dev/shoutrrr/v0.5/services/overview](https://containrrr.dev/shoutrrr/v0.5/services/overview) to
|
||||
learn more about the different service URLs you can use. You can define multiple services by space separating the
|
||||
URLs. (See example below)
|
||||
|
||||
You can customize the message posted by setting a template.
|
||||
|
||||
- `--notification-template` (env. `WATCHTOWER_NOTIFICATION_TEMPLATE`): The template used for the message.
|
||||
|
||||
The template is a Go [template](https://golang.org/pkg/text/template/) and that format a list
|
||||
of [log entries](https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry).
|
||||
|
||||
The default value if not set is `{{range .}}{{.Message}}{{println}}{{end}}`. The example below uses a template that also
|
||||
outputs timestamp and log level.
|
||||
|
||||
!!! tip "Custom date format"
|
||||
If you want to adjust the date/time format it must show how the
|
||||
[reference time](https://golang.org/pkg/time/#pkg-constants) (_Mon Jan 2 15:04:05 MST 2006_) would be displayed in your
|
||||
custom format.
|
||||
i.e., The day of the year has to be 1, the month has to be 2 (february), the hour 3 (or 15 for 24h time) etc.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name watchtower \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WATCHTOWER_NOTIFICATIONS=shoutrrr \
|
||||
-e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \
|
||||
-e WATCHTOWER_NOTIFICATION_TEMPLATE="{{range .}}{{.Time.Format \"2006-01-02 15:04:05\"}} ({{.Level}}): {{.Message}}{{println}}{{end}}" \
|
||||
containrrr/watchtower
|
||||
```
|
||||
|
|
|
@ -23,19 +23,29 @@ password `auth` string:
|
|||
```
|
||||
|
||||
`<REGISTRY_NAME>` needs to be replaced by the name of your private registry
|
||||
(e.g., `my-private-registry.example.org`)
|
||||
(e.g., `my-private-registry.example.org`).
|
||||
|
||||
!!! important "Using private images on docker hub"
|
||||
When using private images on docker hub, the containers beeing watched needs to use the full image name, including the repository prefix `index.docker.io`.
|
||||
So instead of
|
||||
```
|
||||
docker run -d myuser/myimage
|
||||
```
|
||||
you would run it as
|
||||
```
|
||||
docker run -d index.docker.io/myuser/myimage
|
||||
```
|
||||
!!! info "Using private images on Docker Hub"
|
||||
To access private repositories on Docker Hub,
|
||||
`<REGISTRY_NAME>` should be `https://index.docker.io/v1/`.
|
||||
In this special case, the registry domain does not have to be specified
|
||||
in `docker run` or `docker-compose`. Like Docker, Watchtower will use the
|
||||
Docker Hub registry and its credentials when no registry domain is specified.
|
||||
|
||||
<sub>Watchtower will recognize credentials with `<REGISTRY_NAME>` `index.docker.io`,
|
||||
but the Docker CLI will not.</sub>
|
||||
|
||||
!!! important "Using a private registry on a local host"
|
||||
To use a private registry hosted locally, make sure to correctly specify the registry host
|
||||
in both `config.json` and the `docker run` command or `docker-compose` file.
|
||||
Valid hosts are `localhost[:PORT]`, `HOST:PORT`,
|
||||
or any multi-part `domain.name` or IP-address with or without a port.
|
||||
|
||||
Examples:
|
||||
* `localhost` -> `localhost/myimage`
|
||||
* `127.0.0.1` -> `127.0.0.1/myimage:mytag`
|
||||
* `host.domain` -> `host.domain/myorganization/myimage`
|
||||
* `other-lan-host:80` -> `other-lan-host:80/imagename:latest`
|
||||
|
||||
The required `auth` string can be generated as follows:
|
||||
|
||||
|
@ -75,7 +85,7 @@ When creating the watchtower container via docker-compose, use the following lin
|
|||
version: "3.4"
|
||||
services:
|
||||
watchtower:
|
||||
image: index.docker.io/containrrr/watchtower:latest
|
||||
image: containrrr/watchtower:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- <PATH_TO_HOME_DIR>/.docker/config.json:/config.json
|
||||
|
@ -114,7 +124,7 @@ in a volume that may be mounted onto your watchtower container.
|
|||
|
||||
1. Create the Dockerfile (contents below):
|
||||
```Dockerfile
|
||||
FROM golang:1.16
|
||||
FROM golang:1.20
|
||||
|
||||
ENV GO111MODULE off
|
||||
ENV CGO_ENABLED 0
|
||||
|
@ -145,7 +155,7 @@ in a volume that may be mounted onto your watchtower container.
|
|||
```
|
||||
|
||||
3. Create a configuration file for docker, and store it in $HOME/.docker/config.json (replace the <AWS_ACCOUNT_ID>
|
||||
placeholders with your AWS Account ID):
|
||||
placeholders with your AWS Account ID and <AWS_ECR_REGION> with your AWS ECR Region):
|
||||
```json
|
||||
{
|
||||
"credsStore" : "ecr-login",
|
||||
|
@ -153,10 +163,10 @@ in a volume that may be mounted onto your watchtower container.
|
|||
"User-Agent" : "Docker-Client/19.03.1 (XXXXXX)"
|
||||
},
|
||||
"auths" : {
|
||||
"<AWS_ACCOUNT_ID>.dkr.ecr.us-west-1.amazonaws.com" : {}
|
||||
"<AWS_ACCOUNT_ID>.dkr.ecr.<AWS_ECR_REGION>.amazonaws.com" : {}
|
||||
},
|
||||
"credHelpers": {
|
||||
"<AWS_ACCOUNT_ID>.dkr.ecr.us-west-1.amazonaws.com" : "ecr-login"
|
||||
"<AWS_ACCOUNT_ID>.dkr.ecr.<AWS_ECR_REGION>.amazonaws.com" : "ecr-login"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
By default, Watchtower will clean up other instances and won't allow multiple instances running on the same Docker host or swarm. It is possible to override this behavior by defining a [scope](https://containrrr.github.io/watchtower/arguments/#filter_by_scope) to each running instance.
|
||||
|
||||
Notice that:
|
||||
- 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;
|
||||
!!! note
|
||||
- Multiple instances can't run with the same scope;
|
||||
- An instance without a scope will clean up other running instances, even if they have a defined scope;
|
||||
- Supplying `none` as the scope will treat `com.centurylinklabs.watchtower.scope=none`, `com.centurylinklabs.watchtower.scope=` and the lack of a `com.centurylinklabs.watchtower.scope` label as the scope `none`. This effectly enables you to run both scoped and unscoped watchtower instances on the same machine.
|
||||
|
||||
To define an instance monitoring scope, use the `--scope` argument or the `WATCHTOWER_SCOPE` environment variable on startup and set the _com.centurylinklabs.watchtower.scope_ label with the same value for the containers you want to include in this instance's scope (including the instance itself).
|
||||
To define an instance monitoring scope, use the `--scope` argument or the `WATCHTOWER_SCOPE` environment variable on startup and set the `com.centurylinklabs.watchtower.scope` label with the same value for the containers you want to include in this instance's scope (including the instance itself).
|
||||
|
||||
For example, in a Docker Compose config file:
|
||||
|
||||
|
@ -12,16 +13,29 @@ For example, in a Docker Compose config file:
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
app-monitored-by-watchtower:
|
||||
app-with-scope:
|
||||
image: myapps/monitored-by-watchtower
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.scope=myscope"
|
||||
labels: [ "com.centurylinklabs.watchtower.scope=myscope" ]
|
||||
|
||||
watchtower:
|
||||
scoped-watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ]
|
||||
command: --interval 30 --scope myscope
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.scope=myscope"
|
||||
labels: [ "com.centurylinklabs.watchtower.scope=myscope" ]
|
||||
|
||||
unscoped-app-a:
|
||||
image: myapps/app-a
|
||||
|
||||
unscoped-app-b:
|
||||
image: myapps/app-b
|
||||
labels: [ "com.centurylinklabs.watchtower.scope=none" ]
|
||||
|
||||
unscoped-app-c:
|
||||
image: myapps/app-b
|
||||
labels: [ "com.centurylinklabs.watchtower.scope=" ]
|
||||
|
||||
unscoped-watchtower:
|
||||
image: containrrr/watchtower
|
||||
volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ]
|
||||
command: --interval 30 --scope none
|
||||
```
|
||||
|
|
|
@ -1,16 +1,87 @@
|
|||
[data-md-color-scheme="containrrr"] {
|
||||
--md-primary-fg-color: #406170;
|
||||
--md-primary-fg-color--light:#acbfc7;
|
||||
--md-primary-fg-color--dark: #003343;
|
||||
--md-accent-fg-color: #003343;
|
||||
--md-accent-fg-color--transparent: #00334310;
|
||||
/* Primary and accent */
|
||||
--md-primary-fg-color: #406170;
|
||||
--md-primary-fg-color--light:#acbfc7;
|
||||
--md-primary-fg-color--dark: #003343;
|
||||
--md-accent-fg-color: #003343;
|
||||
--md-accent-fg-color--transparent: #00334310;
|
||||
|
||||
/* Typeset overrides */
|
||||
--md-typeset-a-color: var(--md-primary-fg-color);
|
||||
}
|
||||
|
||||
[data-md-color-scheme="containrrr-dark"] {
|
||||
--md-hue: 199;
|
||||
|
||||
/* Primary and accent */
|
||||
--md-primary-fg-color: hsl(199deg 27% 35% / 100%);
|
||||
--md-primary-fg-color--link: hsl(199deg 45% 65% / 100%);
|
||||
--md-primary-fg-color--light: hsl(198deg 19% 73% / 100%);
|
||||
--md-primary-fg-color--dark: hsl(194deg 100% 13% / 100%);
|
||||
--md-accent-fg-color: hsl(194deg 45% 50% / 100%);
|
||||
--md-accent-fg-color--transparent: hsl(194deg 45% 50% / 6.3%);
|
||||
|
||||
/* Default */
|
||||
--md-default-fg-color: hsl(var(--md-hue) 75% 95% / 100%);
|
||||
--md-default-fg-color--light: hsl(var(--md-hue) 75% 90% / 62%);
|
||||
--md-default-fg-color--lighter: hsl(var(--md-hue) 75% 90% / 32%);
|
||||
--md-default-fg-color--lightest: hsl(var(--md-hue) 75% 90% / 12%);
|
||||
--md-default-bg-color: hsl(var(--md-hue) 15% 21% / 100%);
|
||||
--md-default-bg-color--light: hsl(var(--md-hue) 15% 21% / 54%);
|
||||
--md-default-bg-color--lighter: hsl(var(--md-hue) 15% 21% / 26%);
|
||||
--md-default-bg-color--lightest: hsl(var(--md-hue) 15% 21% / 7%);
|
||||
|
||||
/* Code */
|
||||
--md-code-fg-color: hsl(var(--md-hue) 18% 86% / 100%);
|
||||
--md-code-bg-color: hsl(var(--md-hue) 15% 15% / 100%);
|
||||
--md-code-hl-color: hsl(218deg 100% 63% / 15%);
|
||||
--md-code-hl-number-color: hsl(346deg 74% 63% / 100%);
|
||||
--md-code-hl-special-color: hsl(320deg 83% 66% / 100%);
|
||||
--md-code-hl-function-color: hsl(271deg 57% 65% / 100%);
|
||||
--md-code-hl-constant-color: hsl(230deg 62% 70% / 100%);
|
||||
--md-code-hl-keyword-color: hsl(199deg 33% 64% / 100%);
|
||||
--md-code-hl-string-color: hsl( 50deg 34% 74% / 100%);
|
||||
--md-code-hl-name-color: var(--md-code-fg-color);
|
||||
--md-code-hl-operator-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-punctuation-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-comment-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-generic-color: var(--md-default-fg-color--light);
|
||||
--md-code-hl-variable-color: hsl(241deg 22% 60% / 100%);
|
||||
|
||||
/* Typeset */
|
||||
--md-typeset-color: var(--md-default-fg-color);
|
||||
--md-typeset-a-color: var(--md-primary-fg-color--link);
|
||||
--md-typeset-mark-color: hsl(218deg 100% 63% / 30%);
|
||||
--md-typeset-kbd-color: hsl(var(--md-hue) 15% 94% / 12%);
|
||||
--md-typeset-kbd-accent-color: hsl(var(--md-hue) 15% 94% / 20%);
|
||||
--md-typeset-kbd-border-color: hsl(var(--md-hue) 15% 14% / 100%);
|
||||
--md-typeset-table-color: hsl(var(--md-hue) 75% 95% / 12%);
|
||||
|
||||
/* Admonition */
|
||||
--md-admonition-fg-color: var(--md-default-fg-color);
|
||||
--md-admonition-bg-color: var(--md-default-bg-color);
|
||||
|
||||
/* Footer */
|
||||
--md-footer-bg-color: hsl(var(--md-hue) 15% 12% / 87%);
|
||||
--md-footer-bg-color--dark: hsl(var(--md-hue) 15% 10% / 100%);
|
||||
|
||||
/* Shadows */
|
||||
--md-shadow-z1:
|
||||
0 0.2rem 0.50rem rgba(0 0 0 20%),
|
||||
0 0 0.05rem rgba(0 0 0 10%);
|
||||
--md-shadow-z2:
|
||||
0 0.2rem 0.50rem rgba(0 0 0 30%),
|
||||
0 0 0.05rem rgba(0 0 0 25%);
|
||||
--md-shadow-z3:
|
||||
0 0.2rem 0.50rem rgba(0 0 0 40%),
|
||||
0 0 0.05rem rgba(0 0 0 35%);
|
||||
}
|
||||
|
||||
.md-header-nav__button.md-logo {
|
||||
padding: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.md-header-nav__button.md-logo img {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
}
|
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>
|
|
@ -27,12 +27,12 @@ docker run -d \
|
|||
|
||||
Also check out [this Stack Overflow answer](https://stackoverflow.com/a/30494145/7872793) for more options on how to pass environment variables.
|
||||
|
||||
Mounting the host's docker config file:
|
||||
Alternatively if you 2FA authentication setup on Docker Hub then passing username and password will be insufficient. Instead you can run `docker login` to store your credentials in `$HOME/.docker/config.json` and then mount this config file to make it available to the Watchtower container:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name watchtower \
|
||||
-v /home/<user>/.docker/config.json:/config.json \
|
||||
-v $HOME/.docker/config.json:/config.json \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
containrrr/watchtower container_to_watch --debug
|
||||
```
|
||||
|
@ -48,14 +48,14 @@ docker run -d \
|
|||
|
||||
If you mount the config file as described above, be sure to also prepend the URL for the registry when starting up your
|
||||
watched image (you can omit the https://). Here is a complete docker-compose.yml file that starts up a docker container
|
||||
from a private repo at Docker Hub and monitors it with watchtower. Note the command argument changing the interval to
|
||||
30s rather than the default 24 hours.
|
||||
from a private repo on the GitHub Registry and monitors it with watchtower. Note the command argument changing the interval
|
||||
to 30s rather than the default 24 hours.
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
cavo:
|
||||
image: index.docker.io/<org>/<image>:<tag>
|
||||
image: ghcr.io/<org>/<image>:<tag>
|
||||
ports:
|
||||
- "443:3443"
|
||||
- "80:3080"
|
||||
|
|
89
go.mod
89
go.mod
|
@ -1,31 +1,74 @@
|
|||
module github.com/containrrr/watchtower
|
||||
|
||||
go 1.12
|
||||
|
||||
// Use non-vulnerable runc (until github.com/containerd/containerd v1.6.0 is stable)
|
||||
replace github.com/opencontainers/runc => github.com/opencontainers/runc v1.0.3
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/containerd/containerd v1.5.9 // indirect
|
||||
github.com/containrrr/shoutrrr v0.5.2
|
||||
github.com/docker/cli v20.10.8+incompatible
|
||||
github.com/docker/distribution v2.7.1+incompatible
|
||||
github.com/docker/docker v20.10.8+incompatible
|
||||
github.com/docker/docker-credential-helpers v0.6.1 // indirect
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/distribution/reference v0.5.0
|
||||
github.com/docker/cli v24.0.7+incompatible
|
||||
github.com/docker/docker v24.0.7+incompatible
|
||||
github.com/docker/go-connections v0.4.0
|
||||
github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22 // indirect
|
||||
github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
github.com/onsi/gomega v1.30.0
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/robfig/cron v1.2.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/net v0.19.0
|
||||
)
|
||||
|
||||
require github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.17 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.1 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect
|
||||
github.com/onsi/ginkgo v1.14.2
|
||||
github.com/onsi/gomega v1.10.3
|
||||
github.com/prometheus/client_golang v1.7.1
|
||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.6.3
|
||||
github.com/stretchr/testify v1.6.1
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
|
||||
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
)
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
"properties": [
|
||||
{
|
||||
"id": "displayName",
|
||||
"value": "Faled"
|
||||
"value": "Failed"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package actions_test
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/actions"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
|
||||
. "github.com/containrrr/watchtower/internal/actions/mocks"
|
||||
. "github.com/onsi/ginkgo"
|
||||
|
@ -37,7 +38,7 @@ var _ = Describe("the actions package", func() {
|
|||
It("should not do anything", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container",
|
||||
"test-container",
|
||||
|
@ -59,7 +60,7 @@ var _ = Describe("the actions package", func() {
|
|||
client = CreateMockClient(
|
||||
&TestData{
|
||||
NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
|
@ -89,7 +90,7 @@ var _ = Describe("the actions package", func() {
|
|||
BeforeEach(func() {
|
||||
client = CreateMockClient(
|
||||
&TestData{
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
|
|
|
@ -2,16 +2,15 @@ package actions
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/filters"
|
||||
"github.com/containrrr/watchtower/pkg/sorter"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
)
|
||||
|
||||
// CheckForSanity makes sure everything is sane before starting
|
||||
|
@ -40,7 +39,11 @@ func CheckForSanity(client container.Client, filter types.Filter, rollingRestart
|
|||
// will stop and remove all but the most recently started container. This behaviour can be bypassed
|
||||
// if a scope UID is defined.
|
||||
func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool, scope string) error {
|
||||
containers, err := client.ListContainers(filters.FilterByScope(scope, filters.WatchtowerContainersFilter))
|
||||
filter := filters.WatchtowerContainersFilter
|
||||
if scope != "" {
|
||||
filter = filters.FilterByScope(scope, filter)
|
||||
}
|
||||
containers, err := client.ListContainers(filter)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -55,7 +58,7 @@ func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool,
|
|||
return cleanupExcessWatchtowers(containers, client, cleanup)
|
||||
}
|
||||
|
||||
func cleanupExcessWatchtowers(containers []container.Container, client container.Client, cleanup bool) error {
|
||||
func cleanupExcessWatchtowers(containers []types.Container, client container.Client, cleanup bool) error {
|
||||
var stopErrors int
|
||||
|
||||
sort.Sort(sorter.ByCreated(containers))
|
||||
|
|
|
@ -3,7 +3,6 @@ package mocks
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"time"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
|
@ -20,7 +19,8 @@ type MockClient struct {
|
|||
type TestData struct {
|
||||
TriedToRemoveImageCount int
|
||||
NameOfContainerToKeep string
|
||||
Containers []container.Container
|
||||
Containers []t.Container
|
||||
Staleness map[string]bool
|
||||
}
|
||||
|
||||
// TriedToRemoveImage is a test helper function to check whether RemoveImageByID has been called
|
||||
|
@ -38,12 +38,12 @@ func CreateMockClient(data *TestData, pullImages bool, removeVolumes bool) MockC
|
|||
}
|
||||
|
||||
// ListContainers is a mock method returning the provided container testdata
|
||||
func (client MockClient) ListContainers(_ t.Filter) ([]container.Container, error) {
|
||||
func (client MockClient) ListContainers(_ t.Filter) ([]t.Container, error) {
|
||||
return client.TestData.Containers, nil
|
||||
}
|
||||
|
||||
// StopContainer is a mock method
|
||||
func (client MockClient) StopContainer(c container.Container, _ time.Duration) error {
|
||||
func (client MockClient) StopContainer(c t.Container, _ time.Duration) error {
|
||||
if c.Name() == client.TestData.NameOfContainerToKeep {
|
||||
return errors.New("tried to stop the instance we want to keep")
|
||||
}
|
||||
|
@ -51,12 +51,12 @@ func (client MockClient) StopContainer(c container.Container, _ time.Duration) e
|
|||
}
|
||||
|
||||
// StartContainer is a mock method
|
||||
func (client MockClient) StartContainer(_ container.Container) (t.ContainerID, error) {
|
||||
func (client MockClient) StartContainer(_ t.Container) (t.ContainerID, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// RenameContainer is a mock method
|
||||
func (client MockClient) RenameContainer(_ container.Container, _ string) error {
|
||||
func (client MockClient) RenameContainer(_ t.Container, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ func (client MockClient) RemoveImageByID(_ t.ImageID) error {
|
|||
}
|
||||
|
||||
// GetContainer is a mock method
|
||||
func (client MockClient) GetContainer(_ t.ContainerID) (container.Container, error) {
|
||||
func (client MockClient) GetContainer(_ t.ContainerID) (t.Container, error) {
|
||||
return client.TestData.Containers[0], nil
|
||||
}
|
||||
|
||||
|
@ -85,12 +85,16 @@ func (client MockClient) ExecuteCommand(_ t.ContainerID, command string, _ int)
|
|||
}
|
||||
}
|
||||
|
||||
// IsContainerStale is always true for the mock client
|
||||
func (client MockClient) IsContainerStale(_ container.Container) (bool, t.ImageID, error) {
|
||||
return true, "", nil
|
||||
// IsContainerStale is true if not explicitly stated in TestData for the mock client
|
||||
func (client MockClient) IsContainerStale(cont t.Container, params t.UpdateParams) (bool, t.ImageID, error) {
|
||||
stale, found := client.TestData.Staleness[cont.Name()]
|
||||
if !found {
|
||||
stale = true
|
||||
}
|
||||
return stale, "", nil
|
||||
}
|
||||
|
||||
// WarnOnHeadPullFailed is always true for the mock client
|
||||
func (client MockClient) WarnOnHeadPullFailed(_ container.Container) bool {
|
||||
func (client MockClient) WarnOnHeadPullFailed(_ t.Container) bool {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -2,18 +2,19 @@ package mocks
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
wt "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerContainer "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CreateMockContainer creates a container substitute valid for testing
|
||||
func CreateMockContainer(id string, name string, image string, created time.Time) container.Container {
|
||||
func CreateMockContainer(id string, name string, image string, created time.Time) wt.Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: id,
|
||||
|
@ -30,24 +31,29 @@ func CreateMockContainer(id string, name string, image string, created time.Time
|
|||
ExposedPorts: map[nat.Port]struct{}{},
|
||||
},
|
||||
}
|
||||
return *container.NewContainer(
|
||||
return container.NewContainer(
|
||||
&content,
|
||||
&types.ImageInspect{
|
||||
ID: image,
|
||||
RepoDigests: []string{
|
||||
image,
|
||||
},
|
||||
},
|
||||
CreateMockImageInfo(image),
|
||||
)
|
||||
}
|
||||
|
||||
// CreateMockImageInfo returns a mock image info struct based on the passed image
|
||||
func CreateMockImageInfo(image string) *types.ImageInspect {
|
||||
return &types.ImageInspect{
|
||||
ID: image,
|
||||
RepoDigests: []string{
|
||||
image,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateMockContainerWithImageInfo should only be used for testing
|
||||
func CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) container.Container {
|
||||
func CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) wt.Container {
|
||||
return CreateMockContainerWithImageInfoP(id, name, image, created, &imageInfo)
|
||||
}
|
||||
|
||||
// CreateMockContainerWithImageInfoP should only be used for testing
|
||||
func CreateMockContainerWithImageInfoP(id string, name string, image string, created time.Time, imageInfo *types.ImageInspect) container.Container {
|
||||
func CreateMockContainerWithImageInfoP(id string, name string, image string, created time.Time, imageInfo *types.ImageInspect) wt.Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: id,
|
||||
|
@ -60,21 +66,21 @@ func CreateMockContainerWithImageInfoP(id string, name string, image string, cre
|
|||
Labels: make(map[string]string),
|
||||
},
|
||||
}
|
||||
return *container.NewContainer(
|
||||
return container.NewContainer(
|
||||
&content,
|
||||
imageInfo,
|
||||
)
|
||||
}
|
||||
|
||||
// CreateMockContainerWithDigest should only be used for testing
|
||||
func CreateMockContainerWithDigest(id string, name string, image string, created time.Time, digest string) container.Container {
|
||||
func CreateMockContainerWithDigest(id string, name string, image string, created time.Time, digest string) wt.Container {
|
||||
c := CreateMockContainer(id, name, image, created)
|
||||
c.ImageInfo().RepoDigests = []string{digest}
|
||||
return c
|
||||
}
|
||||
|
||||
// CreateMockContainerWithConfig creates a container substitute valid for testing
|
||||
func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) container.Container {
|
||||
func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) wt.Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: id,
|
||||
|
@ -91,16 +97,14 @@ func CreateMockContainerWithConfig(id string, name string, image string, running
|
|||
},
|
||||
Config: config,
|
||||
}
|
||||
return *container.NewContainer(
|
||||
return container.NewContainer(
|
||||
&content,
|
||||
&types.ImageInspect{
|
||||
ID: image,
|
||||
},
|
||||
CreateMockImageInfo(image),
|
||||
)
|
||||
}
|
||||
|
||||
// CreateContainerForProgress creates a container substitute for tracking session/update progress
|
||||
func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (container.Container, wt.ImageID) {
|
||||
func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (wt.Container, wt.ImageID) {
|
||||
indexStr := strconv.Itoa(idPrefix + index)
|
||||
mockID := indexStr + strings.Repeat("0", 61-len(indexStr))
|
||||
contID := "c79" + mockID
|
||||
|
@ -114,3 +118,26 @@ func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (con
|
|||
c := CreateMockContainerWithConfig(contID, contName, oldImgID, true, false, time.Now(), config)
|
||||
return c, wt.ImageID(newImgID)
|
||||
}
|
||||
|
||||
// CreateMockContainerWithLinks should only be used for testing
|
||||
func CreateMockContainerWithLinks(id string, name string, image string, created time.Time, links []string, imageInfo *types.ImageInspect) wt.Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: id,
|
||||
Image: image,
|
||||
Name: name,
|
||||
Created: created.String(),
|
||||
HostConfig: &dockerContainer.HostConfig{
|
||||
Links: links,
|
||||
},
|
||||
},
|
||||
Config: &dockerContainer.Config{
|
||||
Image: image,
|
||||
Labels: make(map[string]string),
|
||||
},
|
||||
}
|
||||
return container.NewContainer(
|
||||
&content,
|
||||
imageInfo,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package mocks
|
|||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/session"
|
||||
wt "github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
@ -21,16 +22,13 @@ func CreateMockProgressReport(states ...session.State) wt.Report {
|
|||
case session.SkippedState:
|
||||
c, _ := CreateContainerForProgress(index, 41, "skip%d")
|
||||
progress.AddSkipped(c, errors.New("unpossible"))
|
||||
break
|
||||
case session.FreshState:
|
||||
c, _ := CreateContainerForProgress(index, 31, "frsh%d")
|
||||
progress.AddScanned(c, c.ImageID())
|
||||
break
|
||||
case session.UpdatedState:
|
||||
c, newImage := CreateContainerForProgress(index, 11, "updt%d")
|
||||
progress.AddScanned(c, newImage)
|
||||
progress.MarkForUpdate(c.ID())
|
||||
break
|
||||
case session.FailedState:
|
||||
c, newImage := CreateContainerForProgress(index, 21, "fail%d")
|
||||
progress.AddScanned(c, newImage)
|
||||
|
|
|
@ -2,6 +2,7 @@ package actions
|
|||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/util"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/lifecycle"
|
||||
|
@ -9,7 +10,6 @@ import (
|
|||
"github.com/containrrr/watchtower/pkg/sorter"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Update looks at the running Docker containers to see if any of the images
|
||||
|
@ -33,8 +33,8 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
|||
staleCheckFailed := 0
|
||||
|
||||
for i, targetContainer := range containers {
|
||||
stale, newestImage, err := client.IsContainerStale(targetContainer)
|
||||
shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly()
|
||||
stale, newestImage, err := client.IsContainerStale(targetContainer, params)
|
||||
shouldUpdate := stale && !params.NoRestart && !targetContainer.IsMonitorOnly(params)
|
||||
if err == nil && shouldUpdate {
|
||||
// Check to make sure we have all the necessary information for recreating the container
|
||||
err = targetContainer.VerifyConfiguration()
|
||||
|
@ -57,7 +57,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
|||
} else {
|
||||
progress.AddScanned(targetContainer, newestImage)
|
||||
}
|
||||
containers[i].Stale = stale
|
||||
containers[i].SetStale(stale)
|
||||
|
||||
if stale {
|
||||
staleCount++
|
||||
|
@ -71,13 +71,11 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
|||
|
||||
UpdateImplicitRestart(containers)
|
||||
|
||||
var containersToUpdate []container.Container
|
||||
if !params.MonitorOnly {
|
||||
for _, c := range containers {
|
||||
if !c.IsMonitorOnly() {
|
||||
containersToUpdate = append(containersToUpdate, c)
|
||||
progress.MarkForUpdate(c.ID())
|
||||
}
|
||||
var containersToUpdate []types.Container
|
||||
for _, c := range containers {
|
||||
if !c.IsMonitorOnly(params) {
|
||||
containersToUpdate = append(containersToUpdate, c)
|
||||
progress.MarkForUpdate(c.ID())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +94,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
|
|||
return progress.Report(), nil
|
||||
}
|
||||
|
||||
func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {
|
||||
func performRollingRestart(containers []types.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {
|
||||
cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
|
||||
failed := make(map[types.ContainerID]error, len(containers))
|
||||
|
||||
|
@ -108,8 +106,10 @@ func performRollingRestart(containers []container.Container, client container.Cl
|
|||
} else {
|
||||
if err := restartStaleContainer(containers[i], client, params); err != nil {
|
||||
failed[containers[i].ID()] = err
|
||||
} else if containers[i].IsStale() {
|
||||
// Only add (previously) stale containers' images to cleanup
|
||||
cleanupImageIDs[containers[i].ImageID()] = true
|
||||
}
|
||||
cleanupImageIDs[containers[i].ImageID()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -120,21 +120,22 @@ func performRollingRestart(containers []container.Container, client container.Cl
|
|||
return failed
|
||||
}
|
||||
|
||||
func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) {
|
||||
func stopContainersInReversedOrder(containers []types.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) {
|
||||
failed = make(map[types.ContainerID]error, len(containers))
|
||||
stopped = make(map[types.ImageID]bool, len(containers))
|
||||
for i := len(containers) - 1; i >= 0; i-- {
|
||||
if err := stopStaleContainer(containers[i], client, params); err != nil {
|
||||
failed[containers[i].ID()] = err
|
||||
} else {
|
||||
stopped[containers[i].ImageID()] = true
|
||||
// NOTE: If a container is restarted due to a dependency this might be empty
|
||||
stopped[containers[i].SafeImageID()] = true
|
||||
}
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func stopStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error {
|
||||
func stopStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error {
|
||||
if container.IsWatchtower() {
|
||||
log.Debugf("This is the watchtower container %s", container.Name())
|
||||
return nil
|
||||
|
@ -143,6 +144,14 @@ func stopStaleContainer(container container.Container, client container.Client,
|
|||
if !container.ToRestart() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Perform an additional check here to prevent us from stopping a linked container we cannot restart
|
||||
if container.IsLinkedToRestarting() {
|
||||
if err := container.VerifyConfiguration(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if params.LifecycleHooks {
|
||||
skipUpdate, err := lifecycle.ExecutePreUpdateCommand(client, container)
|
||||
if err != nil {
|
||||
|
@ -163,7 +172,7 @@ func stopStaleContainer(container container.Container, client container.Client,
|
|||
return nil
|
||||
}
|
||||
|
||||
func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error {
|
||||
func restartContainersInSortedOrder(containers []types.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error {
|
||||
cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
|
||||
failed := make(map[types.ContainerID]error, len(containers))
|
||||
|
||||
|
@ -171,11 +180,13 @@ func restartContainersInSortedOrder(containers []container.Container, client con
|
|||
if !c.ToRestart() {
|
||||
continue
|
||||
}
|
||||
if stoppedImages[c.ImageID()] {
|
||||
if stoppedImages[c.SafeImageID()] {
|
||||
if err := restartStaleContainer(c, client, params); err != nil {
|
||||
failed[c.ID()] = err
|
||||
} else if c.IsStale() {
|
||||
// Only add (previously) stale containers' images to cleanup
|
||||
cleanupImageIDs[c.ImageID()] = true
|
||||
}
|
||||
cleanupImageIDs[c.ImageID()] = true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,13 +199,16 @@ func restartContainersInSortedOrder(containers []container.Container, client con
|
|||
|
||||
func cleanupImages(client container.Client, imageIDs map[types.ImageID]bool) {
|
||||
for imageID := range imageIDs {
|
||||
if imageID == "" {
|
||||
continue
|
||||
}
|
||||
if err := client.RemoveImageByID(imageID); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restartStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error {
|
||||
func restartStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error {
|
||||
// Since we can't shutdown a watchtower container immediately, we need to
|
||||
// start the new one while the old one is still running. This prevents us
|
||||
// from re-using the same container name so we first rename the current
|
||||
|
@ -219,7 +233,7 @@ func restartStaleContainer(container container.Container, client container.Clien
|
|||
|
||||
// UpdateImplicitRestart iterates through the passed containers, setting the
|
||||
// `LinkedToRestarting` flag if any of it's linked containers are marked for restart
|
||||
func UpdateImplicitRestart(containers []container.Container) {
|
||||
func UpdateImplicitRestart(containers []types.Container) {
|
||||
|
||||
for ci, c := range containers {
|
||||
if c.ToRestart() {
|
||||
|
@ -233,7 +247,7 @@ func UpdateImplicitRestart(containers []container.Container) {
|
|||
"linked": c.Name(),
|
||||
}).Debug("container is linked to restarting")
|
||||
// NOTE: To mutate the array, the `c` variable cannot be used as it's a copy
|
||||
containers[ci].LinkedToRestarting = true
|
||||
containers[ci].SetLinkedToRestarting(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -241,12 +255,8 @@ func UpdateImplicitRestart(containers []container.Container) {
|
|||
|
||||
// linkedContainerMarkedForRestart returns the name of the first link that matches a
|
||||
// container marked for restart
|
||||
func linkedContainerMarkedForRestart(links []string, containers []container.Container) string {
|
||||
func linkedContainerMarkedForRestart(links []string, containers []types.Container) string {
|
||||
for _, linkName := range links {
|
||||
// Since the container names need to start with '/', let's prepend it if it's missing
|
||||
if !strings.HasPrefix(linkName, "/") {
|
||||
linkName = "/" + linkName
|
||||
}
|
||||
for _, candidate := range containers {
|
||||
if candidate.Name() == linkName && candidate.ToRestart() {
|
||||
return linkName
|
||||
|
|
|
@ -1,55 +1,75 @@
|
|||
package actions_test
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/actions"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
dockerTypes "github.com/docker/docker/api/types"
|
||||
dockerContainer "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"time"
|
||||
|
||||
. "github.com/containrrr/watchtower/internal/actions/mocks"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func getCommonTestData(keepContainer string) *TestData {
|
||||
return &TestData{
|
||||
NameOfContainerToKeep: keepContainer,
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
"fake-image:latest",
|
||||
time.Now().AddDate(0, 0, -1)),
|
||||
CreateMockContainer(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"fake-image:latest",
|
||||
time.Now()),
|
||||
CreateMockContainer(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"fake-image:latest",
|
||||
time.Now()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getLinkedTestData(withImageInfo bool) *TestData {
|
||||
staleContainer := CreateMockContainer(
|
||||
"test-container-01",
|
||||
"/test-container-01",
|
||||
"fake-image1:latest",
|
||||
time.Now().AddDate(0, 0, -1))
|
||||
|
||||
var imageInfo *dockerTypes.ImageInspect
|
||||
if withImageInfo {
|
||||
imageInfo = CreateMockImageInfo("test-container-02")
|
||||
}
|
||||
linkingContainer := CreateMockContainerWithLinks(
|
||||
"test-container-02",
|
||||
"/test-container-02",
|
||||
"fake-image2:latest",
|
||||
time.Now(),
|
||||
[]string{staleContainer.Name()},
|
||||
imageInfo)
|
||||
|
||||
return &TestData{
|
||||
Staleness: map[string]bool{linkingContainer.Name(): false},
|
||||
Containers: []types.Container{
|
||||
staleContainer,
|
||||
linkingContainer,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var _ = Describe("the update action", func() {
|
||||
var client MockClient
|
||||
|
||||
When("watchtower has been instructed to clean up", func() {
|
||||
BeforeEach(func() {
|
||||
pullImages := false
|
||||
removeVolumes := false
|
||||
//goland:noinspection GoBoolExpressions
|
||||
client = CreateMockClient(
|
||||
&TestData{
|
||||
NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
"fake-image:latest",
|
||||
time.Now().AddDate(0, 0, -1)),
|
||||
CreateMockContainer(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"fake-image:latest",
|
||||
time.Now()),
|
||||
CreateMockContainer(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"fake-image:latest",
|
||||
time.Now()),
|
||||
},
|
||||
},
|
||||
pullImages,
|
||||
removeVolumes,
|
||||
)
|
||||
})
|
||||
|
||||
When("there are multiple containers using the same image", func() {
|
||||
It("should only try to remove the image once", func() {
|
||||
|
||||
client := CreateMockClient(getCommonTestData(""), false, false)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||
|
@ -57,8 +77,9 @@ var _ = Describe("the update action", func() {
|
|||
})
|
||||
When("there are multiple containers using different images", func() {
|
||||
It("should try to remove each of them", func() {
|
||||
client.TestData.Containers = append(
|
||||
client.TestData.Containers,
|
||||
testData := getCommonTestData("")
|
||||
testData.Containers = append(
|
||||
testData.Containers,
|
||||
CreateMockContainer(
|
||||
"unique-test-container",
|
||||
"unique-test-container",
|
||||
|
@ -66,28 +87,49 @@ var _ = Describe("the update action", func() {
|
|||
time.Now(),
|
||||
),
|
||||
)
|
||||
client := CreateMockClient(testData, false, false)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(2))
|
||||
})
|
||||
})
|
||||
When("there are linked containers being updated", func() {
|
||||
It("should not try to remove their images", func() {
|
||||
client := CreateMockClient(getLinkedTestData(true), false, false)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||
})
|
||||
})
|
||||
When("performing a rolling restart update", func() {
|
||||
It("should try to remove the image once", func() {
|
||||
|
||||
client := CreateMockClient(getCommonTestData(""), false, false)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, RollingRestart: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||
})
|
||||
})
|
||||
When("updating a linked container with missing image info", func() {
|
||||
It("should gracefully fail", func() {
|
||||
client := CreateMockClient(getLinkedTestData(false), false, false)
|
||||
|
||||
report, err := actions.Update(client, types.UpdateParams{})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// Note: Linked containers that were skipped for recreation is not counted in Failed
|
||||
// If this happens, an error is emitted to the logs, so a notification should still be sent.
|
||||
Expect(report.Updated()).To(HaveLen(1))
|
||||
Expect(report.Fresh()).To(HaveLen(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("watchtower has been instructed to monitor only", func() {
|
||||
When("certain containers are set to monitor only", func() {
|
||||
BeforeEach(func() {
|
||||
client = CreateMockClient(
|
||||
It("should not update those containers", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
|
@ -110,9 +152,6 @@ var _ = Describe("the update action", func() {
|
|||
false,
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
It("should not update those containers", func() {
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||
|
@ -120,10 +159,10 @@ var _ = Describe("the update action", func() {
|
|||
})
|
||||
|
||||
When("monitor only is set globally", func() {
|
||||
BeforeEach(func() {
|
||||
client = CreateMockClient(
|
||||
It("should not update any containers", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
|
@ -139,25 +178,94 @@ var _ = Describe("the update action", func() {
|
|||
false,
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
It("should not update any containers", func() {
|
||||
_, err := actions.Update(client, types.UpdateParams{MonitorOnly: true})
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||
})
|
||||
})
|
||||
When("watchtower has been instructed to have label take precedence", func() {
|
||||
It("it should update containers when monitor only is set to false", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"fake-image2:latest",
|
||||
false,
|
||||
false,
|
||||
time.Now(),
|
||||
&dockerContainer.Config{
|
||||
Labels: map[string]string{
|
||||
"com.centurylinklabs.watchtower.monitor-only": "false",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||
})
|
||||
It("it should update not containers when monitor only is set to true", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
"fake-image2:latest",
|
||||
false,
|
||||
false,
|
||||
time.Now(),
|
||||
&dockerContainer.Config{
|
||||
Labels: map[string]string{
|
||||
"com.centurylinklabs.watchtower.monitor-only": "true",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||
})
|
||||
It("it should update not containers when monitor only is not set", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainer(
|
||||
"test-container-01",
|
||||
"test-container-01",
|
||||
"fake-image:latest",
|
||||
time.Now()),
|
||||
},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("watchtower has been instructed to run lifecycle hooks", func() {
|
||||
|
||||
When("prupddate script returns 1", func() {
|
||||
BeforeEach(func() {
|
||||
client = CreateMockClient(
|
||||
When("pre-update script returns 1", func() {
|
||||
It("should not update those containers", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
|
@ -177,9 +285,7 @@ var _ = Describe("the update action", func() {
|
|||
false,
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
It("should not update those containers", func() {
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||
|
@ -188,11 +294,11 @@ var _ = Describe("the update action", func() {
|
|||
})
|
||||
|
||||
When("prupddate script returns 75", func() {
|
||||
BeforeEach(func() {
|
||||
client = CreateMockClient(
|
||||
It("should not update those containers", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
|
@ -212,9 +318,6 @@ var _ = Describe("the update action", func() {
|
|||
false,
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
It("should not update those containers", func() {
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
|
||||
|
@ -223,11 +326,11 @@ var _ = Describe("the update action", func() {
|
|||
})
|
||||
|
||||
When("prupddate script returns 0", func() {
|
||||
BeforeEach(func() {
|
||||
client = CreateMockClient(
|
||||
It("should update those containers", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
|
@ -247,9 +350,6 @@ var _ = Describe("the update action", func() {
|
|||
false,
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
It("should update those containers", func() {
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||
|
@ -271,7 +371,7 @@ var _ = Describe("the update action", func() {
|
|||
ExposedPorts: map[nat.Port]struct{}{},
|
||||
})
|
||||
|
||||
provider.Stale = true
|
||||
provider.SetStale(true)
|
||||
|
||||
consumer := CreateMockContainerWithConfig(
|
||||
"test-container-consumer",
|
||||
|
@ -287,7 +387,7 @@ var _ = Describe("the update action", func() {
|
|||
ExposedPorts: map[nat.Port]struct{}{},
|
||||
})
|
||||
|
||||
containers := []container.Container{
|
||||
containers := []types.Container{
|
||||
provider,
|
||||
consumer,
|
||||
}
|
||||
|
@ -305,11 +405,11 @@ var _ = Describe("the update action", func() {
|
|||
})
|
||||
|
||||
When("container is not running", func() {
|
||||
BeforeEach(func() {
|
||||
client = CreateMockClient(
|
||||
It("skip running preupdate", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
|
@ -329,9 +429,6 @@ var _ = Describe("the update action", func() {
|
|||
false,
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
It("skip running preupdate", func() {
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||
|
@ -340,11 +437,11 @@ var _ = Describe("the update action", func() {
|
|||
})
|
||||
|
||||
When("container is restarting", func() {
|
||||
BeforeEach(func() {
|
||||
client = CreateMockClient(
|
||||
It("skip running preupdate", func() {
|
||||
client := CreateMockClient(
|
||||
&TestData{
|
||||
//NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
Containers: []types.Container{
|
||||
CreateMockContainerWithConfig(
|
||||
"test-container-02",
|
||||
"test-container-02",
|
||||
|
@ -364,9 +461,6 @@ var _ = Describe("the update action", func() {
|
|||
false,
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
It("skip running preupdate", func() {
|
||||
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package flags
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -16,12 +19,14 @@ import (
|
|||
// use watchtower
|
||||
const DockerAPIMinVersion string = "1.25"
|
||||
|
||||
var defaultInterval = int((time.Hour * 24).Seconds())
|
||||
|
||||
// RegisterDockerFlags that are used directly by the docker api client
|
||||
func RegisterDockerFlags(rootCmd *cobra.Command) {
|
||||
flags := rootCmd.PersistentFlags()
|
||||
flags.StringP("host", "H", viper.GetString("DOCKER_HOST"), "daemon socket to connect to")
|
||||
flags.BoolP("tlsverify", "v", viper.GetBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote")
|
||||
flags.StringP("api-version", "a", viper.GetString("DOCKER_API_VERSION"), "api version to use by docker client")
|
||||
flags.StringP("host", "H", envString("DOCKER_HOST"), "daemon socket to connect to")
|
||||
flags.BoolP("tlsverify", "v", envBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote")
|
||||
flags.StringP("api-version", "a", envString("DOCKER_API_VERSION"), "api version to use by docker client")
|
||||
}
|
||||
|
||||
// RegisterSystemFlags that are used by watchtower to modify the program flow
|
||||
|
@ -30,143 +35,182 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
|
|||
flags.IntP(
|
||||
"interval",
|
||||
"i",
|
||||
viper.GetInt("WATCHTOWER_POLL_INTERVAL"),
|
||||
envInt("WATCHTOWER_POLL_INTERVAL"),
|
||||
"Poll interval (in seconds)")
|
||||
|
||||
flags.StringP(
|
||||
"schedule",
|
||||
"s",
|
||||
viper.GetString("WATCHTOWER_SCHEDULE"),
|
||||
envString("WATCHTOWER_SCHEDULE"),
|
||||
"The cron expression which defines when to update")
|
||||
|
||||
flags.DurationP(
|
||||
"stop-timeout",
|
||||
"t",
|
||||
viper.GetDuration("WATCHTOWER_TIMEOUT"),
|
||||
envDuration("WATCHTOWER_TIMEOUT"),
|
||||
"Timeout before a container is forcefully stopped")
|
||||
|
||||
flags.BoolP(
|
||||
"no-pull",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NO_PULL"),
|
||||
envBool("WATCHTOWER_NO_PULL"),
|
||||
"Do not pull any new images")
|
||||
|
||||
flags.BoolP(
|
||||
"no-restart",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NO_RESTART"),
|
||||
envBool("WATCHTOWER_NO_RESTART"),
|
||||
"Do not restart any containers")
|
||||
|
||||
flags.BoolP(
|
||||
"no-startup-message",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"),
|
||||
envBool("WATCHTOWER_NO_STARTUP_MESSAGE"),
|
||||
"Prevents watchtower from sending a startup message")
|
||||
|
||||
flags.BoolP(
|
||||
"cleanup",
|
||||
"c",
|
||||
viper.GetBool("WATCHTOWER_CLEANUP"),
|
||||
envBool("WATCHTOWER_CLEANUP"),
|
||||
"Remove previously used images after updating")
|
||||
|
||||
flags.BoolP(
|
||||
"remove-volumes",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"),
|
||||
envBool("WATCHTOWER_REMOVE_VOLUMES"),
|
||||
"Remove attached volumes before updating")
|
||||
|
||||
flags.BoolP(
|
||||
"label-enable",
|
||||
"e",
|
||||
viper.GetBool("WATCHTOWER_LABEL_ENABLE"),
|
||||
envBool("WATCHTOWER_LABEL_ENABLE"),
|
||||
"Watch containers where the com.centurylinklabs.watchtower.enable label is true")
|
||||
|
||||
flags.StringSliceP(
|
||||
"disable-containers",
|
||||
"x",
|
||||
// Due to issue spf13/viper#380, can't use viper.GetStringSlice:
|
||||
regexp.MustCompile("[, ]+").Split(envString("WATCHTOWER_DISABLE_CONTAINERS"), -1),
|
||||
"Comma-separated list of containers to explicitly exclude from watching.")
|
||||
|
||||
flags.StringP(
|
||||
"log-format",
|
||||
"l",
|
||||
viper.GetString("WATCHTOWER_LOG_FORMAT"),
|
||||
"Sets what logging format to use for console output. Possible values: Auto, LogFmt, Pretty, JSON")
|
||||
|
||||
flags.BoolP(
|
||||
"debug",
|
||||
"d",
|
||||
viper.GetBool("WATCHTOWER_DEBUG"),
|
||||
envBool("WATCHTOWER_DEBUG"),
|
||||
"Enable debug mode with verbose logging")
|
||||
|
||||
flags.BoolP(
|
||||
"trace",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_TRACE"),
|
||||
envBool("WATCHTOWER_TRACE"),
|
||||
"Enable trace mode with very verbose logging - caution, exposes credentials")
|
||||
|
||||
flags.BoolP(
|
||||
"monitor-only",
|
||||
"m",
|
||||
viper.GetBool("WATCHTOWER_MONITOR_ONLY"),
|
||||
envBool("WATCHTOWER_MONITOR_ONLY"),
|
||||
"Will only monitor for new images, not update the containers")
|
||||
|
||||
flags.BoolP(
|
||||
"run-once",
|
||||
"R",
|
||||
viper.GetBool("WATCHTOWER_RUN_ONCE"),
|
||||
envBool("WATCHTOWER_RUN_ONCE"),
|
||||
"Run once now and exit")
|
||||
|
||||
flags.BoolP(
|
||||
"include-restarting",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_INCLUDE_RESTARTING"),
|
||||
envBool("WATCHTOWER_INCLUDE_RESTARTING"),
|
||||
"Will also include restarting containers")
|
||||
|
||||
flags.BoolP(
|
||||
"include-stopped",
|
||||
"S",
|
||||
viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"),
|
||||
envBool("WATCHTOWER_INCLUDE_STOPPED"),
|
||||
"Will also include created and exited containers")
|
||||
|
||||
flags.BoolP(
|
||||
"revive-stopped",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_REVIVE_STOPPED"),
|
||||
envBool("WATCHTOWER_REVIVE_STOPPED"),
|
||||
"Will also start stopped containers that were updated, if include-stopped is active")
|
||||
|
||||
flags.BoolP(
|
||||
"enable-lifecycle-hooks",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"),
|
||||
envBool("WATCHTOWER_LIFECYCLE_HOOKS"),
|
||||
"Enable the execution of commands triggered by pre- and post-update lifecycle hooks")
|
||||
|
||||
flags.BoolP(
|
||||
"rolling-restart",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_ROLLING_RESTART"),
|
||||
envBool("WATCHTOWER_ROLLING_RESTART"),
|
||||
"Restart containers one at a time")
|
||||
|
||||
flags.BoolP(
|
||||
"http-api-update",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_HTTP_API_UPDATE"),
|
||||
envBool("WATCHTOWER_HTTP_API_UPDATE"),
|
||||
"Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request")
|
||||
flags.BoolP(
|
||||
"http-api-metrics",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_HTTP_API_METRICS"),
|
||||
envBool("WATCHTOWER_HTTP_API_METRICS"),
|
||||
"Runs Watchtower with the Prometheus metrics API enabled")
|
||||
|
||||
flags.StringP(
|
||||
"http-api-token",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_HTTP_API_TOKEN"),
|
||||
envString("WATCHTOWER_HTTP_API_TOKEN"),
|
||||
"Sets an authentication token to HTTP API requests.")
|
||||
|
||||
flags.BoolP(
|
||||
"http-api-periodic-polls",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"),
|
||||
envBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"),
|
||||
"Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled")
|
||||
|
||||
// https://no-color.org/
|
||||
flags.BoolP(
|
||||
"no-color",
|
||||
"",
|
||||
viper.IsSet("NO_COLOR"),
|
||||
"Disable ANSI color escape codes in log output")
|
||||
|
||||
flags.StringP(
|
||||
"scope",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_SCOPE"),
|
||||
envString("WATCHTOWER_SCOPE"),
|
||||
"Defines a monitoring scope for the Watchtower instance.")
|
||||
|
||||
flags.StringP(
|
||||
"porcelain",
|
||||
"P",
|
||||
envString("WATCHTOWER_PORCELAIN"),
|
||||
`Write session results to stdout using a stable versioned format. Supported values: "v1"`)
|
||||
|
||||
flags.String(
|
||||
"log-level",
|
||||
envString("WATCHTOWER_LOG_LEVEL"),
|
||||
"The maximum log level that will be written to STDERR. Possible values: panic, fatal, error, warn, info, debug or trace")
|
||||
|
||||
flags.BoolP(
|
||||
"health-check",
|
||||
"",
|
||||
false,
|
||||
"Do health check and exit")
|
||||
|
||||
flags.BoolP(
|
||||
"label-take-precedence",
|
||||
"",
|
||||
envBool("WATCHTOWER_LABEL_TAKE_PRECEDENCE"),
|
||||
"Label applied to containers take precedence over arguments")
|
||||
}
|
||||
|
||||
// RegisterNotificationFlags that are used by watchtower to send notifications
|
||||
|
@ -176,170 +220,216 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) {
|
|||
flags.StringSliceP(
|
||||
"notifications",
|
||||
"n",
|
||||
viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"),
|
||||
envStringSlice("WATCHTOWER_NOTIFICATIONS"),
|
||||
" Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)")
|
||||
|
||||
flags.String(
|
||||
"notifications-level",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"),
|
||||
envString("WATCHTOWER_NOTIFICATIONS_LEVEL"),
|
||||
"The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug")
|
||||
|
||||
flags.IntP(
|
||||
"notifications-delay",
|
||||
"",
|
||||
envInt("WATCHTOWER_NOTIFICATIONS_DELAY"),
|
||||
"Delay before sending notifications, expressed in seconds")
|
||||
|
||||
flags.StringP(
|
||||
"notifications-hostname",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"),
|
||||
envString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"),
|
||||
"Custom hostname for notification titles")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-from",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"),
|
||||
"Address to send notification emails from")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-to",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_TO"),
|
||||
"Address to send notification emails to")
|
||||
|
||||
flags.IntP(
|
||||
"notification-email-delay",
|
||||
"",
|
||||
viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"),
|
||||
envInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"),
|
||||
"Delay before sending notifications, expressed in seconds")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-server",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"),
|
||||
"SMTP server to send notification emails through")
|
||||
|
||||
flags.IntP(
|
||||
"notification-email-server-port",
|
||||
"",
|
||||
viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"),
|
||||
envInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"),
|
||||
"SMTP server port to send notification emails through")
|
||||
|
||||
flags.BoolP(
|
||||
"notification-email-server-tls-skip-verify",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"),
|
||||
envBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"),
|
||||
`Controls whether watchtower verifies the SMTP server's certificate chain and host name.
|
||||
Should only be used for testing.`)
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-server-user",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"),
|
||||
"SMTP server user for sending notifications")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-server-password",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"),
|
||||
"SMTP server password for sending notifications")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-subjecttag",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"),
|
||||
envString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"),
|
||||
"Subject prefix tag for notifications via mail")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-hook-url",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"),
|
||||
envString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"),
|
||||
"The Slack Hook URL to send notifications to")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-identifier",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"),
|
||||
envString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"),
|
||||
"A string which will be used to identify the messages coming from this watchtower instance")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-channel",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"),
|
||||
envString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"),
|
||||
"A string which overrides the webhook's default channel. Example: #my-custom-channel")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-icon-emoji",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"),
|
||||
envString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"),
|
||||
"An emoji code string to use in place of the default icon")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-icon-url",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"),
|
||||
envString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"),
|
||||
"An icon image URL string to use in place of the default icon")
|
||||
|
||||
flags.StringP(
|
||||
"notification-msteams-hook",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"),
|
||||
envString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"),
|
||||
"The MSTeams WebHook URL to send notifications to")
|
||||
|
||||
flags.BoolP(
|
||||
"notification-msteams-data",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"),
|
||||
envBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"),
|
||||
"The MSTeams notifier will try to extract log entry fields as MSTeams message facts")
|
||||
|
||||
flags.StringP(
|
||||
"notification-gotify-url",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"),
|
||||
envString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"),
|
||||
"The Gotify URL to send notifications to")
|
||||
|
||||
flags.StringP(
|
||||
"notification-gotify-token",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"),
|
||||
envString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"),
|
||||
"The Gotify Application required to query the Gotify API")
|
||||
|
||||
flags.BoolP(
|
||||
"notification-gotify-tls-skip-verify",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"),
|
||||
envBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"),
|
||||
`Controls whether watchtower verifies the Gotify server's certificate chain and host name.
|
||||
Should only be used for testing.`)
|
||||
|
||||
flags.String(
|
||||
"notification-template",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"),
|
||||
envString("WATCHTOWER_NOTIFICATION_TEMPLATE"),
|
||||
"The shoutrrr text/template for the messages")
|
||||
|
||||
flags.StringArray(
|
||||
"notification-url",
|
||||
viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"),
|
||||
envStringSlice("WATCHTOWER_NOTIFICATION_URL"),
|
||||
"The shoutrrr URL to send notifications to")
|
||||
|
||||
flags.Bool("notification-report",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"),
|
||||
envBool("WATCHTOWER_NOTIFICATION_REPORT"),
|
||||
"Use the session report as the notification template data")
|
||||
|
||||
flags.StringP(
|
||||
"notification-title-tag",
|
||||
"",
|
||||
envString("WATCHTOWER_NOTIFICATION_TITLE_TAG"),
|
||||
"Title prefix tag for notifications")
|
||||
|
||||
flags.Bool("notification-skip-title",
|
||||
envBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"),
|
||||
"Do not pass the title param to notifications")
|
||||
|
||||
flags.String(
|
||||
"warn-on-head-failure",
|
||||
viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),
|
||||
envString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),
|
||||
"When to warn about HEAD pull requests failing. Possible values: always, auto or never")
|
||||
|
||||
flags.Bool(
|
||||
"notification-log-stdout",
|
||||
envBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"),
|
||||
"Write notification logs to stdout instead of logging (to stderr)")
|
||||
}
|
||||
|
||||
func envString(key string) string {
|
||||
viper.MustBindEnv(key)
|
||||
return viper.GetString(key)
|
||||
}
|
||||
|
||||
func envStringSlice(key string) []string {
|
||||
viper.MustBindEnv(key)
|
||||
return viper.GetStringSlice(key)
|
||||
}
|
||||
|
||||
func envInt(key string) int {
|
||||
viper.MustBindEnv(key)
|
||||
return viper.GetInt(key)
|
||||
}
|
||||
|
||||
func envBool(key string) bool {
|
||||
viper.MustBindEnv(key)
|
||||
return viper.GetBool(key)
|
||||
}
|
||||
|
||||
func envDuration(key string) time.Duration {
|
||||
viper.MustBindEnv(key)
|
||||
return viper.GetDuration(key)
|
||||
}
|
||||
|
||||
// SetDefaults provides default values for environment variables
|
||||
func SetDefaults() {
|
||||
day := (time.Hour * 24).Seconds()
|
||||
viper.AutomaticEnv()
|
||||
viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock")
|
||||
viper.SetDefault("DOCKER_API_VERSION", DockerAPIMinVersion)
|
||||
viper.SetDefault("WATCHTOWER_POLL_INTERVAL", day)
|
||||
viper.SetDefault("WATCHTOWER_POLL_INTERVAL", defaultInterval)
|
||||
viper.SetDefault("WATCHTOWER_TIMEOUT", time.Second*10)
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATIONS", []string{})
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATIONS_LEVEL", "info")
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT", 25)
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "")
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower")
|
||||
viper.SetDefault("WATCHTOWER_LOG_LEVEL", "info")
|
||||
viper.SetDefault("WATCHTOWER_LOG_FORMAT", "auto")
|
||||
}
|
||||
|
||||
// EnvConfig translates the command-line options into environment variables
|
||||
|
@ -427,34 +517,191 @@ func GetSecretsFromFiles(rootCmd *cobra.Command) {
|
|||
"notification-slack-hook-url",
|
||||
"notification-msteams-hook",
|
||||
"notification-gotify-token",
|
||||
"notification-url",
|
||||
"http-api-token",
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
getSecretFromFile(flags, secret)
|
||||
if err := getSecretFromFile(flags, secret); err != nil {
|
||||
log.Fatalf("failed to get secret from flag %v: %s", secret, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getSecretFromFile will check if the flag contains a reference to a file; if it does, replaces the value of the flag with the contents of the file.
|
||||
func getSecretFromFile(flags *pflag.FlagSet, secret string) {
|
||||
value, err := flags.GetString(secret)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
func getSecretFromFile(flags *pflag.FlagSet, secret string) error {
|
||||
flag := flags.Lookup(secret)
|
||||
if sliceValue, ok := flag.Value.(pflag.SliceValue); ok {
|
||||
oldValues := sliceValue.GetSlice()
|
||||
values := make([]string, 0, len(oldValues))
|
||||
for _, value := range oldValues {
|
||||
if value != "" && isFile(value) {
|
||||
file, err := os.Open(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
values = append(values, line)
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
return sliceValue.Replace(values)
|
||||
}
|
||||
|
||||
value := flag.Value.String()
|
||||
if value != "" && isFile(value) {
|
||||
file, err := ioutil.ReadFile(value)
|
||||
content, err := os.ReadFile(value)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = flags.Set(secret, strings.TrimSpace(string(file)))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
return flags.Set(secret, strings.TrimSpace(string(content)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isFile(s string) bool {
|
||||
_, err := os.Stat(s)
|
||||
if os.IsNotExist(err) {
|
||||
firstColon := strings.IndexRune(s, ':')
|
||||
if firstColon != 1 && firstColon != -1 {
|
||||
// If the string contains a ':', but it's not the second character, it's probably not a file
|
||||
// and will cause a fatal error on windows if stat'ed
|
||||
// This still allows for paths that start with 'c:\' etc.
|
||||
return false
|
||||
}
|
||||
return true
|
||||
_, err := os.Stat(s)
|
||||
return !errors.Is(err, os.ErrNotExist)
|
||||
}
|
||||
|
||||
// ProcessFlagAliases updates the value of flags that are being set by helper flags
|
||||
func ProcessFlagAliases(flags *pflag.FlagSet) {
|
||||
|
||||
porcelain, err := flags.GetString(`porcelain`)
|
||||
if err != nil {
|
||||
log.Fatalf(`Failed to get flag: %v`, err)
|
||||
}
|
||||
if porcelain != "" {
|
||||
if porcelain != "v1" {
|
||||
log.Fatalf(`Unknown porcelain version %q. Supported values: "v1"`, porcelain)
|
||||
}
|
||||
if err = appendFlagValue(flags, `notification-url`, `logger://`); err != nil {
|
||||
log.Errorf(`Failed to set flag: %v`, err)
|
||||
}
|
||||
setFlagIfDefault(flags, `notification-log-stdout`, `true`)
|
||||
setFlagIfDefault(flags, `notification-report`, `true`)
|
||||
tpl := fmt.Sprintf(`porcelain.%s.summary-no-log`, porcelain)
|
||||
setFlagIfDefault(flags, `notification-template`, tpl)
|
||||
}
|
||||
|
||||
scheduleChanged := flags.Changed(`schedule`)
|
||||
intervalChanged := flags.Changed(`interval`)
|
||||
// FIXME: snakeswap
|
||||
// due to how viper is integrated by swapping the defaults for the flags, we need this hack:
|
||||
if val, _ := flags.GetString(`schedule`); val != `` {
|
||||
scheduleChanged = true
|
||||
}
|
||||
if val, _ := flags.GetInt(`interval`); val != defaultInterval {
|
||||
intervalChanged = true
|
||||
}
|
||||
|
||||
if intervalChanged && scheduleChanged {
|
||||
log.Fatal(`Only schedule or interval can be defined, not both.`)
|
||||
}
|
||||
|
||||
// update schedule flag to match interval if it's set, or to the default if none of them are
|
||||
if intervalChanged || !scheduleChanged {
|
||||
interval, _ := flags.GetInt(`interval`)
|
||||
_ = flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
|
||||
}
|
||||
|
||||
if flagIsEnabled(flags, `debug`) {
|
||||
_ = flags.Set(`log-level`, `debug`)
|
||||
}
|
||||
|
||||
if flagIsEnabled(flags, `trace`) {
|
||||
_ = flags.Set(`log-level`, `trace`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// SetupLogging reads only the flags that is needed to set up logging and applies them to the global logger
|
||||
func SetupLogging(f *pflag.FlagSet) error {
|
||||
logFormat, _ := f.GetString(`log-format`)
|
||||
noColor, _ := f.GetBool("no-color")
|
||||
|
||||
switch strings.ToLower(logFormat) {
|
||||
case "auto":
|
||||
// This will either use the "pretty" or "logfmt" format, based on whether the standard out is connected to a TTY
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: noColor,
|
||||
// enable logrus built-in support for https://bixense.com/clicolors/
|
||||
EnvironmentOverrideColors: true,
|
||||
})
|
||||
case "json":
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
case "logfmt":
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
DisableColors: true,
|
||||
FullTimestamp: true,
|
||||
})
|
||||
case "pretty":
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
// "Pretty" format combined with `--no-color` will only change the timestamp to the time since start
|
||||
ForceColors: !noColor,
|
||||
FullTimestamp: false,
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("invalid log format: %s", logFormat)
|
||||
}
|
||||
|
||||
rawLogLevel, _ := f.GetString(`log-level`)
|
||||
if logLevel, err := log.ParseLevel(rawLogLevel); err != nil {
|
||||
return fmt.Errorf("invalid log level: %e", err)
|
||||
} else {
|
||||
log.SetLevel(logLevel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func flagIsEnabled(flags *pflag.FlagSet, name string) bool {
|
||||
value, err := flags.GetBool(name)
|
||||
if err != nil {
|
||||
log.Fatalf(`The flag %q is not defined`, name)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func appendFlagValue(flags *pflag.FlagSet, name string, values ...string) error {
|
||||
flag := flags.Lookup(name)
|
||||
if flag == nil {
|
||||
return fmt.Errorf(`invalid flag name %q`, name)
|
||||
}
|
||||
|
||||
if flagValues, ok := flag.Value.(pflag.SliceValue); ok {
|
||||
for _, value := range values {
|
||||
_ = flagValues.Append(value)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf(`the value for flag %q is not a slice value`, name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setFlagIfDefault(flags *pflag.FlagSet, name string, value string) {
|
||||
if flags.Changed(name) {
|
||||
return
|
||||
}
|
||||
if err := flags.Set(name, value); err != nil {
|
||||
log.Errorf(`Failed to set flag: %v`, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
package flags
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEnvConfig_Defaults(t *testing.T) {
|
||||
// Unset testing environments own variables, since those are not what is under test
|
||||
_ = os.Unsetenv("DOCKER_TLS_VERIFY")
|
||||
_ = os.Unsetenv("DOCKER_HOST")
|
||||
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
|
@ -43,9 +50,7 @@ func TestEnvConfig_Custom(t *testing.T) {
|
|||
|
||||
func TestGetSecretsFromFilesWithString(t *testing.T) {
|
||||
value := "supersecretstring"
|
||||
|
||||
err := os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
|
||||
require.NoError(t, err)
|
||||
t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
|
||||
|
||||
testGetSecretsFromFiles(t, "notification-email-server-password", value)
|
||||
}
|
||||
|
@ -54,28 +59,48 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) {
|
|||
value := "megasecretstring"
|
||||
|
||||
// Create the temporary file which will contain a secret.
|
||||
file, err := ioutil.TempFile(os.TempDir(), "watchtower-")
|
||||
file, err := os.CreateTemp(t.TempDir(), "watchtower-")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
|
||||
|
||||
// Write the secret to the temporary file.
|
||||
secret := []byte(value)
|
||||
_, err = file.Write(secret)
|
||||
_, err = file.Write([]byte(value))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, file.Close())
|
||||
|
||||
err = os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
|
||||
require.NoError(t, err)
|
||||
t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
|
||||
|
||||
testGetSecretsFromFiles(t, "notification-email-server-password", value)
|
||||
}
|
||||
|
||||
func testGetSecretsFromFiles(t *testing.T, flagName string, expected string) {
|
||||
func TestGetSliceSecretsFromFiles(t *testing.T) {
|
||||
values := []string{"entry2", "", "entry3"}
|
||||
|
||||
// Create the temporary file which will contain a secret.
|
||||
file, err := os.CreateTemp(t.TempDir(), "watchtower-")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Write the secret to the temporary file.
|
||||
for _, value := range values {
|
||||
_, err = file.WriteString("\n" + value)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
require.NoError(t, file.Close())
|
||||
|
||||
testGetSecretsFromFiles(t, "notification-url", `[entry1,entry2,entry3]`,
|
||||
`--notification-url`, "entry1",
|
||||
`--notification-url`, file.Name())
|
||||
}
|
||||
|
||||
func testGetSecretsFromFiles(t *testing.T, flagName string, expected string, args ...string) {
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
RegisterSystemFlags(cmd)
|
||||
RegisterNotificationFlags(cmd)
|
||||
require.NoError(t, cmd.ParseFlags(args))
|
||||
GetSecretsFromFiles(cmd)
|
||||
value, err := cmd.PersistentFlags().GetString(flagName)
|
||||
require.NoError(t, err)
|
||||
flag := cmd.PersistentFlags().Lookup(flagName)
|
||||
require.NotNil(t, flag)
|
||||
value := flag.Value.String()
|
||||
|
||||
assert.Equal(t, expected, value)
|
||||
}
|
||||
|
@ -94,3 +119,221 @@ func TestHTTPAPIPeriodicPollsFlag(t *testing.T) {
|
|||
|
||||
assert.Equal(t, true, periodicPolls)
|
||||
}
|
||||
|
||||
func TestIsFile(t *testing.T) {
|
||||
assert.False(t, isFile("https://google.com"), "an URL should never be considered a file")
|
||||
assert.True(t, isFile(os.Args[0]), "the currently running binary path should always be considered a file")
|
||||
}
|
||||
|
||||
func TestProcessFlagAliases(t *testing.T) {
|
||||
logrus.StandardLogger().ExitFunc = func(_ int) { t.FailNow() }
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
RegisterNotificationFlags(cmd)
|
||||
|
||||
require.NoError(t, cmd.ParseFlags([]string{
|
||||
`--porcelain`, `v1`,
|
||||
`--interval`, `10`,
|
||||
`--trace`,
|
||||
}))
|
||||
flags := cmd.Flags()
|
||||
ProcessFlagAliases(flags)
|
||||
|
||||
urls, _ := flags.GetStringArray(`notification-url`)
|
||||
assert.Contains(t, urls, `logger://`)
|
||||
|
||||
logStdout, _ := flags.GetBool(`notification-log-stdout`)
|
||||
assert.True(t, logStdout)
|
||||
|
||||
report, _ := flags.GetBool(`notification-report`)
|
||||
assert.True(t, report)
|
||||
|
||||
template, _ := flags.GetString(`notification-template`)
|
||||
assert.Equal(t, `porcelain.v1.summary-no-log`, template)
|
||||
|
||||
sched, _ := flags.GetString(`schedule`)
|
||||
assert.Equal(t, `@every 10s`, sched)
|
||||
|
||||
logLevel, _ := flags.GetString(`log-level`)
|
||||
assert.Equal(t, `trace`, logLevel)
|
||||
}
|
||||
|
||||
func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
t.Setenv("WATCHTOWER_DEBUG", `true`)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
RegisterNotificationFlags(cmd)
|
||||
|
||||
require.NoError(t, cmd.ParseFlags([]string{}))
|
||||
flags := cmd.Flags()
|
||||
ProcessFlagAliases(flags)
|
||||
|
||||
logLevel, _ := flags.GetString(`log-level`)
|
||||
assert.Equal(t, `debug`, logLevel)
|
||||
}
|
||||
|
||||
func TestLogFormatFlag(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
|
||||
// Ensure the default value is Auto
|
||||
require.NoError(t, cmd.ParseFlags([]string{}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)
|
||||
|
||||
// Test JSON format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `JSON`}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
assert.IsType(t, &logrus.JSONFormatter{}, logrus.StandardLogger().Formatter)
|
||||
|
||||
// Test Pretty format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `pretty`}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)
|
||||
textFormatter, ok := (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)
|
||||
assert.True(t, ok)
|
||||
assert.True(t, textFormatter.ForceColors)
|
||||
assert.False(t, textFormatter.FullTimestamp)
|
||||
|
||||
// Test LogFmt format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `logfmt`}))
|
||||
require.NoError(t, SetupLogging(cmd.Flags()))
|
||||
textFormatter, ok = (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)
|
||||
assert.True(t, ok)
|
||||
assert.True(t, textFormatter.DisableColors)
|
||||
assert.True(t, textFormatter.FullTimestamp)
|
||||
|
||||
// Test invalid format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `cowsay`}))
|
||||
require.Error(t, SetupLogging(cmd.Flags()))
|
||||
}
|
||||
|
||||
func TestLogLevelFlag(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
|
||||
// Test invalid format
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--log-level`, `gossip`}))
|
||||
require.Error(t, SetupLogging(cmd.Flags()))
|
||||
}
|
||||
|
||||
func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
|
||||
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
RegisterNotificationFlags(cmd)
|
||||
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--schedule`, `@hourly`, `--interval`, `10`}))
|
||||
flags := cmd.Flags()
|
||||
|
||||
assert.PanicsWithValue(t, `FATAL`, func() {
|
||||
ProcessFlagAliases(flags)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessFlagAliasesScheduleFromEnvironment(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
|
||||
t.Setenv("WATCHTOWER_SCHEDULE", `@hourly`)
|
||||
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
RegisterNotificationFlags(cmd)
|
||||
|
||||
require.NoError(t, cmd.ParseFlags([]string{}))
|
||||
flags := cmd.Flags()
|
||||
ProcessFlagAliases(flags)
|
||||
|
||||
sched, _ := flags.GetString(`schedule`)
|
||||
assert.Equal(t, `@hourly`, sched)
|
||||
}
|
||||
|
||||
func TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) {
|
||||
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
RegisterNotificationFlags(cmd)
|
||||
|
||||
require.NoError(t, cmd.ParseFlags([]string{`--porcelain`, `cowboy`}))
|
||||
flags := cmd.Flags()
|
||||
|
||||
assert.PanicsWithValue(t, `FATAL`, func() {
|
||||
ProcessFlagAliases(flags)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFlagsArePrecentInDocumentation(t *testing.T) {
|
||||
|
||||
// Legacy notifcations are ignored, since they are (soft) deprecated
|
||||
ignoredEnvs := map[string]string{
|
||||
"WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI": "legacy",
|
||||
"WATCHTOWER_NOTIFICATION_SLACK_ICON_URL": "legacy",
|
||||
}
|
||||
|
||||
ignoredFlags := map[string]string{
|
||||
"notification-gotify-url": "legacy",
|
||||
"notification-slack-icon-emoji": "legacy",
|
||||
"notification-slack-icon-url": "legacy",
|
||||
}
|
||||
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
RegisterSystemFlags(cmd)
|
||||
RegisterNotificationFlags(cmd)
|
||||
|
||||
flags := cmd.PersistentFlags()
|
||||
|
||||
docFiles := []string{
|
||||
"../../docs/arguments.md",
|
||||
"../../docs/lifecycle-hooks.md",
|
||||
"../../docs/notifications.md",
|
||||
}
|
||||
allDocs := ""
|
||||
for _, f := range docFiles {
|
||||
bytes, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not load docs file %q: %v", f, err)
|
||||
}
|
||||
allDocs += string(bytes)
|
||||
}
|
||||
|
||||
flags.VisitAll(func(f *pflag.Flag) {
|
||||
if !strings.Contains(allDocs, "--"+f.Name) {
|
||||
if _, found := ignoredFlags[f.Name]; !found {
|
||||
t.Logf("Docs does not mention flag long name %q", f.Name)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
if !strings.Contains(allDocs, "-"+f.Shorthand) {
|
||||
t.Logf("Docs does not mention flag shorthand %q (%q)", f.Shorthand, f.Name)
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
for _, key := range viper.AllKeys() {
|
||||
envKey := strings.ToUpper(key)
|
||||
if !strings.Contains(allDocs, envKey) {
|
||||
if _, found := ignoredEnvs[envKey]; !found {
|
||||
t.Logf("Docs does not mention environment variable %q", envKey)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
24
internal/util/rand_sha256.go
Normal file
24
internal/util/rand_sha256.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GenerateRandomSHA256 generates a random 64 character SHA 256 hash string
|
||||
func GenerateRandomSHA256() string {
|
||||
return GenerateRandomPrefixedSHA256()[7:]
|
||||
}
|
||||
|
||||
// GenerateRandomPrefixedSHA256 generates a random 64 character SHA 256 hash string, prefixed with `sha256:`
|
||||
func GenerateRandomPrefixedSHA256() string {
|
||||
hash := make([]byte, 32)
|
||||
_, _ = rand.Read(hash)
|
||||
sb := bytes.NewBufferString("sha256:")
|
||||
sb.Grow(64)
|
||||
for _, h := range hash {
|
||||
_, _ = fmt.Fprintf(sb, "%02x", h)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSliceEqual_True(t *testing.T) {
|
||||
|
@ -62,3 +64,15 @@ func TestStructMapSubtract(t *testing.T) {
|
|||
assert.Equal(t, map[string]struct{}{"a": x, "b": x, "c": x}, m1)
|
||||
assert.Equal(t, map[string]struct{}{"a": x, "c": x}, m2)
|
||||
}
|
||||
|
||||
// GenerateRandomSHA256 generates a random 64 character SHA 256 hash string
|
||||
func TestGenerateRandomSHA256(t *testing.T) {
|
||||
res := GenerateRandomSHA256()
|
||||
assert.Len(t, res, 64)
|
||||
assert.NotContains(t, res, "sha256:")
|
||||
}
|
||||
|
||||
func TestGenerateRandomPrefixedSHA256(t *testing.T) {
|
||||
res := GenerateRandomPrefixedSHA256()
|
||||
assert.Regexp(t, regexp.MustCompile("sha256:[0-9|a-f]{64}"), res)
|
||||
}
|
||||
|
|
12
mkdocs.yml
12
mkdocs.yml
|
@ -5,7 +5,16 @@ edit_uri: edit/main/docs/
|
|||
theme:
|
||||
name: 'material'
|
||||
palette:
|
||||
scheme: containrrr
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: containrrr
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: Switch to dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: containrrr-dark
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
logo: images/logo-450px.png
|
||||
favicon: images/favicon.ico
|
||||
extra_css:
|
||||
|
@ -39,6 +48,7 @@ nav:
|
|||
- 'Stop signals': 'stop-signals.md'
|
||||
- 'Lifecycle hooks': 'lifecycle-hooks.md'
|
||||
- 'Running multiple instances': 'running-multiple-instances.md'
|
||||
- 'HTTP API Mode': 'http-api-mode.md'
|
||||
- 'Metrics': 'metrics.md'
|
||||
plugins:
|
||||
- search
|
||||
|
|
BIN
oryxBuildBinary
Executable file
BIN
oryxBuildBinary
Executable file
Binary file not shown.
|
@ -2,18 +2,18 @@ package metrics_test
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/api"
|
||||
metricsAPI "github.com/containrrr/watchtower/pkg/api/metrics"
|
||||
"github.com/containrrr/watchtower/pkg/metrics"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -36,7 +36,7 @@ func getWithToken(handler http.Handler) map[string]string {
|
|||
handler.ServeHTTP(respWriter, req)
|
||||
|
||||
res := respWriter.Result()
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
||||
for _, line := range strings.Split(string(body), "\n") {
|
||||
if len(line) < 1 || line[0] == '#' {
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -13,7 +14,7 @@ var (
|
|||
)
|
||||
|
||||
// New is a factory function creating a new Handler instance
|
||||
func New(updateFn func(), updateLock chan bool) *Handler {
|
||||
func New(updateFn func(images []string), updateLock chan bool) *Handler {
|
||||
if updateLock != nil {
|
||||
lock = updateLock
|
||||
} else {
|
||||
|
@ -29,7 +30,7 @@ func New(updateFn func(), updateLock chan bool) *Handler {
|
|||
|
||||
// Handler is an API handler used for triggering container update scans
|
||||
type Handler struct {
|
||||
fn func()
|
||||
fn func(images []string)
|
||||
Path string
|
||||
}
|
||||
|
||||
|
@ -43,12 +44,29 @@ func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case chanValue := <-lock:
|
||||
var images []string
|
||||
imageQueries, found := r.URL.Query()["image"]
|
||||
if found {
|
||||
for _, image := range imageQueries {
|
||||
images = append(images, strings.Split(image, ",")...)
|
||||
}
|
||||
|
||||
} else {
|
||||
images = nil
|
||||
}
|
||||
|
||||
if len(images) > 0 {
|
||||
chanValue := <-lock
|
||||
defer func() { lock <- chanValue }()
|
||||
handle.fn()
|
||||
default:
|
||||
log.Debug("Skipped. Another update already running.")
|
||||
handle.fn(images)
|
||||
} else {
|
||||
select {
|
||||
case chanValue := <-lock:
|
||||
defer func() { lock <- chanValue }()
|
||||
handle.fn(images)
|
||||
default:
|
||||
log.Debug("Skipped. Another update already running.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
29
pkg/container/cgroup_id.go
Normal file
29
pkg/container/cgroup_id.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
var dockerContainerPattern = regexp.MustCompile(`[0-9]+:.*:/docker/([a-f|0-9]{64})`)
|
||||
|
||||
// GetRunningContainerID tries to resolve the current container ID from the current process cgroup information
|
||||
func GetRunningContainerID() (cid types.ContainerID, err error) {
|
||||
file, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", os.Getpid()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return getRunningContainerIDFromString(string(file)), nil
|
||||
}
|
||||
|
||||
func getRunningContainerIDFromString(s string) types.ContainerID {
|
||||
matches := dockerContainerPattern.FindStringSubmatch(s)
|
||||
if len(matches) < 2 {
|
||||
return ""
|
||||
}
|
||||
return types.ContainerID(matches[1])
|
||||
}
|
40
pkg/container/cgroup_id_test.go
Normal file
40
pkg/container/cgroup_id_test.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("GetRunningContainerID", func() {
|
||||
When("a matching container ID is found", func() {
|
||||
It("should return that container ID", func() {
|
||||
cid := getRunningContainerIDFromString(`
|
||||
15:name=systemd:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
14:misc:/
|
||||
13:rdma:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
12:pids:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
11:hugetlb:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
10:net_prio:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
9:perf_event:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
8:net_cls:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
7:freezer:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
6:devices:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
5:blkio:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
4:cpuacct:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
3:cpu:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
2:cpuset:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
1:memory:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
0::/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377
|
||||
`)
|
||||
Expect(cid).To(BeEquivalentTo(`991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377`))
|
||||
})
|
||||
})
|
||||
When("no matching container ID could be found", func() {
|
||||
It("should return that container ID", func() {
|
||||
cid := getRunningContainerIDFromString(`14:misc:/`)
|
||||
Expect(cid).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
//
|
|
@ -3,14 +3,10 @@ package container
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/registry"
|
||||
"github.com/containrrr/watchtower/pkg/registry/digest"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
|
@ -18,6 +14,10 @@ import (
|
|||
sdkClient "github.com/docker/docker/client"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/registry"
|
||||
"github.com/containrrr/watchtower/pkg/registry/digest"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
const defaultStopSignal = "SIGTERM"
|
||||
|
@ -25,24 +25,24 @@ const defaultStopSignal = "SIGTERM"
|
|||
// A Client is the interface through which watchtower interacts with the
|
||||
// Docker API.
|
||||
type Client interface {
|
||||
ListContainers(t.Filter) ([]Container, error)
|
||||
GetContainer(containerID t.ContainerID) (Container, error)
|
||||
StopContainer(Container, time.Duration) error
|
||||
StartContainer(Container) (t.ContainerID, error)
|
||||
RenameContainer(Container, string) error
|
||||
IsContainerStale(Container) (stale bool, latestImage t.ImageID, err error)
|
||||
ListContainers(t.Filter) ([]t.Container, error)
|
||||
GetContainer(containerID t.ContainerID) (t.Container, error)
|
||||
StopContainer(t.Container, time.Duration) error
|
||||
StartContainer(t.Container) (t.ContainerID, error)
|
||||
RenameContainer(t.Container, string) error
|
||||
IsContainerStale(t.Container, t.UpdateParams) (stale bool, latestImage t.ImageID, err error)
|
||||
ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error)
|
||||
RemoveImageByID(t.ImageID) error
|
||||
WarnOnHeadPullFailed(container Container) bool
|
||||
WarnOnHeadPullFailed(container t.Container) bool
|
||||
}
|
||||
|
||||
// NewClient returns a new Client instance which can be used to interact with
|
||||
// the Docker API.
|
||||
// The client reads its configuration from the following environment variables:
|
||||
// * DOCKER_HOST the docker-engine host to send api requests to
|
||||
// * DOCKER_TLS_VERIFY whether to verify tls certificates
|
||||
// * DOCKER_API_VERSION the minimum docker api version to work with
|
||||
func NewClient(pullImages, includeStopped, reviveStopped, removeVolumes, includeRestarting bool, warnOnHeadFailed string) Client {
|
||||
// - DOCKER_HOST the docker-engine host to send api requests to
|
||||
// - DOCKER_TLS_VERIFY whether to verify tls certificates
|
||||
// - DOCKER_API_VERSION the minimum docker api version to work with
|
||||
func NewClient(opts ClientOptions) Client {
|
||||
cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv)
|
||||
|
||||
if err != nil {
|
||||
|
@ -50,46 +50,57 @@ func NewClient(pullImages, includeStopped, reviveStopped, removeVolumes, include
|
|||
}
|
||||
|
||||
return dockerClient{
|
||||
api: cli,
|
||||
pullImages: pullImages,
|
||||
removeVolumes: removeVolumes,
|
||||
includeStopped: includeStopped,
|
||||
reviveStopped: reviveStopped,
|
||||
includeRestarting: includeRestarting,
|
||||
warnOnHeadFailed: warnOnHeadFailed,
|
||||
api: cli,
|
||||
ClientOptions: opts,
|
||||
}
|
||||
}
|
||||
|
||||
// ClientOptions contains the options for how the docker client wrapper should behave
|
||||
type ClientOptions struct {
|
||||
RemoveVolumes bool
|
||||
IncludeStopped bool
|
||||
ReviveStopped bool
|
||||
IncludeRestarting bool
|
||||
WarnOnHeadFailed WarningStrategy
|
||||
}
|
||||
|
||||
// WarningStrategy is a value determining when to show warnings
|
||||
type WarningStrategy string
|
||||
|
||||
const (
|
||||
// WarnAlways warns whenever the problem occurs
|
||||
WarnAlways WarningStrategy = "always"
|
||||
// WarnNever never warns when the problem occurs
|
||||
WarnNever WarningStrategy = "never"
|
||||
// WarnAuto skips warning when the problem was expected
|
||||
WarnAuto WarningStrategy = "auto"
|
||||
)
|
||||
|
||||
type dockerClient struct {
|
||||
api sdkClient.CommonAPIClient
|
||||
pullImages bool
|
||||
removeVolumes bool
|
||||
includeStopped bool
|
||||
reviveStopped bool
|
||||
includeRestarting bool
|
||||
warnOnHeadFailed string
|
||||
api sdkClient.CommonAPIClient
|
||||
ClientOptions
|
||||
}
|
||||
|
||||
func (client dockerClient) WarnOnHeadPullFailed(container Container) bool {
|
||||
if client.warnOnHeadFailed == "always" {
|
||||
func (client dockerClient) WarnOnHeadPullFailed(container t.Container) bool {
|
||||
if client.WarnOnHeadFailed == WarnAlways {
|
||||
return true
|
||||
}
|
||||
if client.warnOnHeadFailed == "never" {
|
||||
if client.WarnOnHeadFailed == WarnNever {
|
||||
return false
|
||||
}
|
||||
|
||||
return registry.WarnOnAPIConsumption(container)
|
||||
}
|
||||
|
||||
func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) {
|
||||
cs := []Container{}
|
||||
func (client dockerClient) ListContainers(fn t.Filter) ([]t.Container, error) {
|
||||
cs := []t.Container{}
|
||||
bg := context.Background()
|
||||
|
||||
if client.includeStopped && client.includeRestarting {
|
||||
if client.IncludeStopped && client.IncludeRestarting {
|
||||
log.Debug("Retrieving running, stopped, restarting and exited containers")
|
||||
} else if client.includeStopped {
|
||||
} else if client.IncludeStopped {
|
||||
log.Debug("Retrieving running, stopped and exited containers")
|
||||
} else if client.includeRestarting {
|
||||
} else if client.IncludeRestarting {
|
||||
log.Debug("Retrieving running and restarting containers")
|
||||
} else {
|
||||
log.Debug("Retrieving running containers")
|
||||
|
@ -125,36 +136,52 @@ func (client dockerClient) createListFilter() filters.Args {
|
|||
filterArgs := filters.NewArgs()
|
||||
filterArgs.Add("status", "running")
|
||||
|
||||
if client.includeStopped {
|
||||
if client.IncludeStopped {
|
||||
filterArgs.Add("status", "created")
|
||||
filterArgs.Add("status", "exited")
|
||||
}
|
||||
|
||||
if client.includeRestarting {
|
||||
if client.IncludeRestarting {
|
||||
filterArgs.Add("status", "restarting")
|
||||
}
|
||||
|
||||
return filterArgs
|
||||
}
|
||||
|
||||
func (client dockerClient) GetContainer(containerID t.ContainerID) (Container, error) {
|
||||
func (client dockerClient) GetContainer(containerID t.ContainerID) (t.Container, error) {
|
||||
bg := context.Background()
|
||||
|
||||
containerInfo, err := client.api.ContainerInspect(bg, string(containerID))
|
||||
if err != nil {
|
||||
return Container{}, err
|
||||
return &Container{}, err
|
||||
}
|
||||
|
||||
netType, netContainerId, found := strings.Cut(string(containerInfo.HostConfig.NetworkMode), ":")
|
||||
if found && netType == "container" {
|
||||
parentContainer, err := client.api.ContainerInspect(bg, netContainerId)
|
||||
if err != nil {
|
||||
log.WithFields(map[string]interface{}{
|
||||
"container": containerInfo.Name,
|
||||
"error": err,
|
||||
"network-container": netContainerId,
|
||||
}).Warnf("Unable to resolve network container: %v", err)
|
||||
|
||||
} else {
|
||||
// Replace the container ID with a container name to allow it to reference the re-created network container
|
||||
containerInfo.HostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf("container:%s", parentContainer.Name))
|
||||
}
|
||||
}
|
||||
|
||||
imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to retrieve container image info: %v", err)
|
||||
return Container{containerInfo: &containerInfo, imageInfo: nil}, nil
|
||||
return &Container{containerInfo: &containerInfo, imageInfo: nil}, nil
|
||||
}
|
||||
|
||||
return Container{containerInfo: &containerInfo, imageInfo: &imageInfo}, nil
|
||||
return &Container{containerInfo: &containerInfo, imageInfo: &imageInfo}, nil
|
||||
}
|
||||
|
||||
func (client dockerClient) StopContainer(c Container, timeout time.Duration) error {
|
||||
func (client dockerClient) StopContainer(c t.Container, timeout time.Duration) error {
|
||||
bg := context.Background()
|
||||
signal := c.StopSignal()
|
||||
if signal == "" {
|
||||
|
@ -174,12 +201,16 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
|
|||
// TODO: This should probably be checked.
|
||||
_ = client.waitForStopOrTimeout(c, timeout)
|
||||
|
||||
if c.containerInfo.HostConfig.AutoRemove {
|
||||
if c.ContainerInfo().HostConfig.AutoRemove {
|
||||
log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", shortID)
|
||||
} else {
|
||||
log.Debugf("Removing container %s", shortID)
|
||||
|
||||
if err := client.api.ContainerRemove(bg, idStr, types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.removeVolumes}); err != nil {
|
||||
if err := client.api.ContainerRemove(bg, idStr, types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.RemoveVolumes}); err != nil {
|
||||
if sdkClient.IsErrNotFound(err) {
|
||||
log.Debugf("Container %s not found, skipping removal.", shortID)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -192,11 +223,34 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
|
|||
return nil
|
||||
}
|
||||
|
||||
func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) {
|
||||
func (client dockerClient) GetNetworkConfig(c t.Container) *network.NetworkingConfig {
|
||||
config := &network.NetworkingConfig{
|
||||
EndpointsConfig: c.ContainerInfo().NetworkSettings.Networks,
|
||||
}
|
||||
|
||||
for _, ep := range config.EndpointsConfig {
|
||||
aliases := make([]string, 0, len(ep.Aliases))
|
||||
cidAlias := c.ID().ShortID()
|
||||
|
||||
// Remove the old container ID alias from the network aliases, as it would accumulate across updates otherwise
|
||||
for _, alias := range ep.Aliases {
|
||||
if alias == cidAlias {
|
||||
continue
|
||||
}
|
||||
aliases = append(aliases, alias)
|
||||
}
|
||||
|
||||
ep.Aliases = aliases
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func (client dockerClient) StartContainer(c t.Container) (t.ContainerID, error) {
|
||||
bg := context.Background()
|
||||
config := c.runtimeConfig()
|
||||
hostConfig := c.hostConfig()
|
||||
networkConfig := &network.NetworkingConfig{EndpointsConfig: c.containerInfo.NetworkSettings.Networks}
|
||||
config := c.GetCreateConfig()
|
||||
hostConfig := c.GetCreateHostConfig()
|
||||
networkConfig := client.GetNetworkConfig(c)
|
||||
|
||||
// simpleNetworkConfig is a networkConfig with only 1 network.
|
||||
// see: https://github.com/docker/docker/issues/29265
|
||||
simpleNetworkConfig := func() *network.NetworkingConfig {
|
||||
|
@ -212,6 +266,7 @@ func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) {
|
|||
name := c.Name()
|
||||
|
||||
log.Infof("Creating %s", name)
|
||||
|
||||
createdContainer, err := client.api.ContainerCreate(bg, config, hostConfig, simpleNetworkConfig, nil, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -236,7 +291,7 @@ func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) {
|
|||
}
|
||||
|
||||
createdContainerID := t.ContainerID(createdContainer.ID)
|
||||
if !c.IsRunning() && !client.reviveStopped {
|
||||
if !c.IsRunning() && !client.ReviveStopped {
|
||||
return createdContainerID, nil
|
||||
}
|
||||
|
||||
|
@ -244,7 +299,7 @@ func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) {
|
|||
|
||||
}
|
||||
|
||||
func (client dockerClient) doStartContainer(bg context.Context, c Container, creation container.ContainerCreateCreatedBody) error {
|
||||
func (client dockerClient) doStartContainer(bg context.Context, c t.Container, creation container.CreateResponse) error {
|
||||
name := c.Name()
|
||||
|
||||
log.Debugf("Starting container %s (%s)", name, t.ContainerID(creation.ID).ShortID())
|
||||
|
@ -255,16 +310,16 @@ func (client dockerClient) doStartContainer(bg context.Context, c Container, cre
|
|||
return nil
|
||||
}
|
||||
|
||||
func (client dockerClient) RenameContainer(c Container, newName string) error {
|
||||
func (client dockerClient) RenameContainer(c t.Container, newName string) error {
|
||||
bg := context.Background()
|
||||
log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID().ShortID(), newName)
|
||||
return client.api.ContainerRename(bg, string(c.ID()), newName)
|
||||
}
|
||||
|
||||
func (client dockerClient) IsContainerStale(container Container) (stale bool, latestImage t.ImageID, err error) {
|
||||
func (client dockerClient) IsContainerStale(container t.Container, params t.UpdateParams) (stale bool, latestImage t.ImageID, err error) {
|
||||
ctx := context.Background()
|
||||
|
||||
if !client.pullImages {
|
||||
if container.IsNoPull(params) {
|
||||
log.Debugf("Skipping image pull.")
|
||||
} else if err := client.PullImage(ctx, container); err != nil {
|
||||
return false, container.SafeImageID(), err
|
||||
|
@ -273,8 +328,8 @@ func (client dockerClient) IsContainerStale(container Container) (stale bool, la
|
|||
return client.HasNewImage(ctx, container)
|
||||
}
|
||||
|
||||
func (client dockerClient) HasNewImage(ctx context.Context, container Container) (hasNew bool, latestImage t.ImageID, err error) {
|
||||
currentImageID := t.ImageID(container.containerInfo.ContainerJSONBase.Image)
|
||||
func (client dockerClient) HasNewImage(ctx context.Context, container t.Container) (hasNew bool, latestImage t.ImageID, err error) {
|
||||
currentImageID := t.ImageID(container.ContainerInfo().ContainerJSONBase.Image)
|
||||
imageName := container.ImageName()
|
||||
|
||||
newImageInfo, _, err := client.api.ImageInspectWithRaw(ctx, imageName)
|
||||
|
@ -294,7 +349,7 @@ func (client dockerClient) HasNewImage(ctx context.Context, container Container)
|
|||
|
||||
// PullImage pulls the latest image for the supplied container, optionally skipping if it's digest can be confirmed
|
||||
// to match the one that the registry reports via a HEAD request
|
||||
func (client dockerClient) PullImage(ctx context.Context, container Container) error {
|
||||
func (client dockerClient) PullImage(ctx context.Context, container t.Container) error {
|
||||
containerName := container.Name()
|
||||
imageName := container.ImageName()
|
||||
|
||||
|
@ -303,6 +358,10 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e
|
|||
"container": containerName,
|
||||
}
|
||||
|
||||
if strings.HasPrefix(imageName, "sha256:") {
|
||||
return fmt.Errorf("container uses a pinned image, and cannot be updated by watchtower")
|
||||
}
|
||||
|
||||
log.WithFields(fields).Debugf("Trying to load authentication credentials.")
|
||||
opts, err := registry.GetPullOptions(imageName)
|
||||
if err != nil {
|
||||
|
@ -339,7 +398,7 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e
|
|||
|
||||
defer response.Close()
|
||||
// the pull request will be aborted prematurely unless the response is read
|
||||
if _, err = ioutil.ReadAll(response); err != nil {
|
||||
if _, err = io.ReadAll(response); err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
@ -349,13 +408,34 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e
|
|||
func (client dockerClient) RemoveImageByID(id t.ImageID) error {
|
||||
log.Infof("Removing image %s", id.ShortID())
|
||||
|
||||
_, err := client.api.ImageRemove(
|
||||
items, err := client.api.ImageRemove(
|
||||
context.Background(),
|
||||
string(id),
|
||||
types.ImageRemoveOptions{
|
||||
Force: true,
|
||||
})
|
||||
|
||||
if log.IsLevelEnabled(log.DebugLevel) {
|
||||
deleted := strings.Builder{}
|
||||
untagged := strings.Builder{}
|
||||
for _, item := range items {
|
||||
if item.Deleted != "" {
|
||||
if deleted.Len() > 0 {
|
||||
deleted.WriteString(`, `)
|
||||
}
|
||||
deleted.WriteString(t.ImageID(item.Deleted).ShortID())
|
||||
}
|
||||
if item.Untagged != "" {
|
||||
if untagged.Len() > 0 {
|
||||
untagged.WriteString(`, `)
|
||||
}
|
||||
untagged.WriteString(t.ImageID(item.Untagged).ShortID())
|
||||
}
|
||||
}
|
||||
fields := log.Fields{`deleted`: deleted.String(), `untagged`: untagged.String()}
|
||||
log.WithFields(fields).Debug("Image removal completed")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -438,7 +518,7 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e
|
|||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if execInspect.Running == true {
|
||||
if execInspect.Running {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
@ -451,14 +531,14 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e
|
|||
}
|
||||
|
||||
if execInspect.ExitCode > 0 {
|
||||
return false, fmt.Errorf("Command exited with code %v %s", execInspect.ExitCode, execOutput)
|
||||
return false, fmt.Errorf("command exited with code %v %s", execInspect.ExitCode, execOutput)
|
||||
}
|
||||
break
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (client dockerClient) waitForStopOrTimeout(c Container, waitTime time.Duration) error {
|
||||
func (client dockerClient) waitForStopOrTimeout(c t.Container, waitTime time.Duration) error {
|
||||
bg := context.Background()
|
||||
timeout := time.After(waitTime)
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/util"
|
||||
"github.com/containrrr/watchtower/pkg/container/mocks"
|
||||
"github.com/containrrr/watchtower/pkg/filters"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
|
@ -8,14 +12,16 @@ import (
|
|||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
cli "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/onsi/gomega/ghttp"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
. "github.com/onsi/gomega/types"
|
||||
gt "github.com/onsi/gomega/types"
|
||||
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
|
@ -32,41 +38,114 @@ var _ = Describe("the client", func() {
|
|||
mockServer.Close()
|
||||
})
|
||||
Describe("WarnOnHeadPullFailed", func() {
|
||||
containerUnknown := *mockContainerWithImageName("unknown.repo/prefix/imagename:latest")
|
||||
containerKnown := *mockContainerWithImageName("docker.io/prefix/imagename:latest")
|
||||
containerUnknown := MockContainer(WithImageName("unknown.repo/prefix/imagename:latest"))
|
||||
containerKnown := MockContainer(WithImageName("docker.io/prefix/imagename:latest"))
|
||||
|
||||
When("warn on head failure is set to \"always\"", func() {
|
||||
c := newClientNoAPI(false, false, false, false, false, "always")
|
||||
When(`warn on head failure is set to "always"`, func() {
|
||||
c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}}
|
||||
It("should always return true", func() {
|
||||
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeTrue())
|
||||
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue())
|
||||
})
|
||||
})
|
||||
When("warn on head failure is set to \"auto\"", func() {
|
||||
c := newClientNoAPI(false, false, false, false, false, "auto")
|
||||
It("should always return true", func() {
|
||||
When(`warn on head failure is set to "auto"`, func() {
|
||||
c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAuto}}
|
||||
It("should return false for unknown repos", func() {
|
||||
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
|
||||
})
|
||||
It("should", func() {
|
||||
It("should return true for known repos", func() {
|
||||
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue())
|
||||
})
|
||||
})
|
||||
When("warn on head failure is set to \"never\"", func() {
|
||||
c := newClientNoAPI(false, false, false, false, false, "never")
|
||||
When(`warn on head failure is set to "never"`, func() {
|
||||
c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnNever}}
|
||||
It("should never return true", func() {
|
||||
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
|
||||
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeFalse())
|
||||
})
|
||||
})
|
||||
})
|
||||
When("pulling the latest image", func() {
|
||||
When("the image consist of a pinned hash", func() {
|
||||
It("should gracefully fail with a useful message", func() {
|
||||
c := dockerClient{}
|
||||
pinnedContainer := MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b"))
|
||||
err := c.PullImage(context.Background(), pinnedContainer)
|
||||
Expect(err).To(MatchError(`container uses a pinned image, and cannot be updated by watchtower`))
|
||||
})
|
||||
})
|
||||
})
|
||||
When("removing a running container", func() {
|
||||
When("the container still exist after stopping", func() {
|
||||
It("should attempt to remove the container", func() {
|
||||
container := MockContainer(WithContainerState(types.ContainerState{Running: true}))
|
||||
containerStopped := MockContainer(WithContainerState(types.ContainerState{Running: false}))
|
||||
|
||||
cid := container.ContainerInfo().ID
|
||||
mockServer.AppendHandlers(
|
||||
mocks.KillContainerHandler(cid, mocks.Found),
|
||||
mocks.GetContainerHandler(cid, containerStopped.ContainerInfo()),
|
||||
mocks.RemoveContainerHandler(cid, mocks.Found),
|
||||
mocks.GetContainerHandler(cid, nil),
|
||||
)
|
||||
|
||||
Expect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed())
|
||||
})
|
||||
})
|
||||
When("the container does not exist after stopping", func() {
|
||||
It("should not cause an error", func() {
|
||||
container := MockContainer(WithContainerState(types.ContainerState{Running: true}))
|
||||
|
||||
cid := container.ContainerInfo().ID
|
||||
mockServer.AppendHandlers(
|
||||
mocks.KillContainerHandler(cid, mocks.Found),
|
||||
mocks.GetContainerHandler(cid, nil),
|
||||
mocks.RemoveContainerHandler(cid, mocks.Missing),
|
||||
)
|
||||
|
||||
Expect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
When("removing a image", func() {
|
||||
When("debug logging is enabled", func() {
|
||||
It("should log removed and untagged images", func() {
|
||||
imageA := util.GenerateRandomSHA256()
|
||||
imageAParent := util.GenerateRandomSHA256()
|
||||
images := map[string][]string{imageA: {imageAParent}}
|
||||
mockServer.AppendHandlers(mocks.RemoveImageHandler(images))
|
||||
c := dockerClient{api: docker}
|
||||
|
||||
resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
|
||||
defer resetLogrus()
|
||||
|
||||
Expect(c.RemoveImageByID(t.ImageID(imageA))).To(Succeed())
|
||||
|
||||
shortA := t.ImageID(imageA).ShortID()
|
||||
shortAParent := t.ImageID(imageAParent).ShortID()
|
||||
|
||||
Eventually(logbuf).Should(gbytes.Say(`deleted="%v, %v" untagged="?%v"?`, shortA, shortAParent, shortA))
|
||||
})
|
||||
})
|
||||
When("image is not found", func() {
|
||||
It("should return an error", func() {
|
||||
image := util.GenerateRandomSHA256()
|
||||
mockServer.AppendHandlers(mocks.RemoveImageHandler(nil))
|
||||
c := dockerClient{api: docker}
|
||||
|
||||
err := c.RemoveImageByID(t.ImageID(image))
|
||||
Expect(errdefs.IsNotFound(err)).To(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
When("listing containers", func() {
|
||||
When("no filter is provided", func() {
|
||||
It("should return all available containers", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
pullImages: false,
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
containers, err := client.ListContainers(filters.NoFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
@ -76,11 +155,11 @@ var _ = Describe("the client", func() {
|
|||
When("a filter matching nothing", func() {
|
||||
It("should return an empty array", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
|
||||
filter := filters.FilterByNames([]string{"lollercoaster"}, filters.NoFilter)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
pullImages: false,
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
containers, err := client.ListContainers(filter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
@ -90,10 +169,10 @@ var _ = Describe("the client", func() {
|
|||
When("a watchtower filter is provided", func() {
|
||||
It("should return only the watchtower container", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
pullImages: false,
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
containers, err := client.ListContainers(filters.WatchtowerContainersFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
@ -103,11 +182,10 @@ var _ = Describe("the client", func() {
|
|||
When(`include stopped is enabled`, func() {
|
||||
It("should return both stopped and running containers", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running", "exited", "created"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("stopped", "watchtower", "running")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Stopped, &mocks.Watchtower, &mocks.Running)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
pullImages: false,
|
||||
includeStopped: true,
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{IncludeStopped: true},
|
||||
}
|
||||
containers, err := client.ListContainers(filters.NoFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
@ -117,11 +195,10 @@ var _ = Describe("the client", func() {
|
|||
When(`include restarting is enabled`, func() {
|
||||
It("should return both restarting and running containers", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running", "restarting"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running", "restarting")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running, &mocks.Restarting)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
pullImages: false,
|
||||
includeRestarting: true,
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{IncludeRestarting: true},
|
||||
}
|
||||
containers, err := client.ListContainers(filters.NoFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
@ -131,31 +208,58 @@ var _ = Describe("the client", func() {
|
|||
When(`include restarting is disabled`, func() {
|
||||
It("should not return restarting containers", func() {
|
||||
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
pullImages: false,
|
||||
includeRestarting: false,
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{IncludeRestarting: false},
|
||||
}
|
||||
containers, err := client.ListContainers(filters.NoFilter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(containers).NotTo(ContainElement(havingRestartingState(true)))
|
||||
})
|
||||
})
|
||||
When(`a container uses container network mode`, func() {
|
||||
When(`the network container can be resolved`, func() {
|
||||
It("should return the container name instead of the ID", func() {
|
||||
consumerContainerRef := mocks.NetConsumerOK
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
container, err := client.GetContainer(consumerContainerRef.ContainerID())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
networkMode := container.ContainerInfo().HostConfig.NetworkMode
|
||||
Expect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierContainerName))
|
||||
})
|
||||
})
|
||||
When(`the network container cannot be resolved`, func() {
|
||||
It("should still return the container ID", func() {
|
||||
consumerContainerRef := mocks.NetConsumerInvalidSupplier
|
||||
mockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...)
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
container, err := client.GetContainer(consumerContainerRef.ContainerID())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
networkMode := container.ContainerInfo().HostConfig.NetworkMode
|
||||
Expect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierNotFoundID))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Describe(`ExecuteCommand`, func() {
|
||||
When(`logging`, func() {
|
||||
It("should include container id field", func() {
|
||||
client := dockerClient{
|
||||
api: docker,
|
||||
pullImages: false,
|
||||
api: docker,
|
||||
ClientOptions: ClientOptions{},
|
||||
}
|
||||
|
||||
// Capture logrus output in buffer
|
||||
logbuf := gbytes.NewBuffer()
|
||||
origOut := logrus.StandardLogger().Out
|
||||
defer logrus.SetOutput(origOut)
|
||||
logrus.SetOutput(logbuf)
|
||||
resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
|
||||
defer resetLogrus()
|
||||
|
||||
user := ""
|
||||
containerID := t.ContainerID("ex-cont-id")
|
||||
|
@ -212,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
|
||||
|
||||
func withContainerImageName(matcher GomegaMatcher) GomegaMatcher {
|
||||
func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher {
|
||||
return WithTransform(containerImageName, matcher)
|
||||
}
|
||||
|
||||
func containerImageName(container Container) string {
|
||||
func containerImageName(container t.Container) string {
|
||||
return container.ImageName()
|
||||
}
|
||||
|
||||
func havingRestartingState(expected bool) GomegaMatcher {
|
||||
return WithTransform(func(container Container) bool {
|
||||
return container.containerInfo.State.Restarting
|
||||
func havingRestartingState(expected bool) gt.GomegaMatcher {
|
||||
return WithTransform(func(container t.Container) bool {
|
||||
return container.ContainerInfo().State.Restarting
|
||||
}, Equal(expected))
|
||||
}
|
||||
|
||||
func havingRunningState(expected bool) GomegaMatcher {
|
||||
return WithTransform(func(container Container) bool {
|
||||
return container.containerInfo.State.Running
|
||||
func havingRunningState(expected bool) gt.GomegaMatcher {
|
||||
return WithTransform(func(container t.Container) bool {
|
||||
return container.ContainerInfo().State.Running
|
||||
}, Equal(expected))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
// Package container contains code related to dealing with docker containers
|
||||
package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/util"
|
||||
wt "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockercontainer "github.com/docker/docker/api/types/container"
|
||||
|
@ -31,6 +34,26 @@ type Container struct {
|
|||
imageInfo *types.ImageInspect
|
||||
}
|
||||
|
||||
// IsLinkedToRestarting returns the current value of the LinkedToRestarting field for the container
|
||||
func (c *Container) IsLinkedToRestarting() bool {
|
||||
return c.LinkedToRestarting
|
||||
}
|
||||
|
||||
// IsStale returns the current value of the Stale field for the container
|
||||
func (c *Container) IsStale() bool {
|
||||
return c.Stale
|
||||
}
|
||||
|
||||
// SetLinkedToRestarting sets the LinkedToRestarting field for the container
|
||||
func (c *Container) SetLinkedToRestarting(value bool) {
|
||||
c.LinkedToRestarting = value
|
||||
}
|
||||
|
||||
// SetStale implements sets the Stale field for the container
|
||||
func (c *Container) SetStale(value bool) {
|
||||
c.Stale = value
|
||||
}
|
||||
|
||||
// ContainerInfo fetches JSON info for the container
|
||||
func (c Container) ContainerInfo() *types.ContainerJSON {
|
||||
return c.containerInfo
|
||||
|
@ -108,20 +131,31 @@ func (c Container) Enabled() (bool, bool) {
|
|||
return parsedBool, true
|
||||
}
|
||||
|
||||
// IsMonitorOnly returns the value of the monitor-only label. If the label
|
||||
// is not set then false is returned.
|
||||
func (c Container) IsMonitorOnly() bool {
|
||||
rawBool, ok := c.getLabelValue(monitorOnlyLabel)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// IsMonitorOnly returns whether the container should only be monitored based on values of
|
||||
// the monitor-only label, the monitor-only argument and the label-take-precedence argument.
|
||||
func (c Container) IsMonitorOnly(params wt.UpdateParams) bool {
|
||||
return c.getContainerOrGlobalBool(params.MonitorOnly, monitorOnlyLabel, params.LabelPrecedence)
|
||||
}
|
||||
|
||||
parsedBool, err := strconv.ParseBool(rawBool)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// IsNoPull returns whether the image should be pulled based on values of
|
||||
// the no-pull label, the no-pull argument and the label-take-precedence argument.
|
||||
func (c Container) IsNoPull(params wt.UpdateParams) bool {
|
||||
return c.getContainerOrGlobalBool(params.NoPull, noPullLabel, params.LabelPrecedence)
|
||||
}
|
||||
|
||||
return parsedBool
|
||||
func (c Container) getContainerOrGlobalBool(globalVal bool, label string, contPrecedence bool) bool {
|
||||
if contVal, err := c.getBoolLabelValue(label); err != nil {
|
||||
if !errors.Is(err, errorLabelNotFound) {
|
||||
logrus.WithField("error", err).WithField("label", label).Warn("Failed to parse label value")
|
||||
}
|
||||
return globalVal
|
||||
} else {
|
||||
if contPrecedence {
|
||||
return contVal
|
||||
} else {
|
||||
return contVal || globalVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scope returns the value of the scope UID label and if the label
|
||||
|
@ -143,7 +177,14 @@ func (c Container) Links() []string {
|
|||
dependsOnLabelValue := c.getLabelValueOrEmpty(dependsOnLabel)
|
||||
|
||||
if dependsOnLabelValue != "" {
|
||||
links := strings.Split(dependsOnLabelValue, ",")
|
||||
for _, link := range strings.Split(dependsOnLabelValue, ",") {
|
||||
// Since the container names need to start with '/', let's prepend it if it's missing
|
||||
if !strings.HasPrefix(link, "/") {
|
||||
link = "/" + link
|
||||
}
|
||||
links = append(links, link)
|
||||
}
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
|
@ -152,6 +193,13 @@ func (c Container) Links() []string {
|
|||
name := strings.Split(link, ":")[0]
|
||||
links = append(links, name)
|
||||
}
|
||||
|
||||
// If the container uses another container for networking, it can be considered an implicit link
|
||||
// since the container would stop working if the network supplier were to be recreated
|
||||
networkMode := c.containerInfo.HostConfig.NetworkMode
|
||||
if networkMode.IsContainer() {
|
||||
links = append(links, networkMode.ConnectedContainer())
|
||||
}
|
||||
}
|
||||
|
||||
return links
|
||||
|
@ -216,18 +264,23 @@ func (c Container) StopSignal() string {
|
|||
return c.getLabelValueOrEmpty(signalLabel)
|
||||
}
|
||||
|
||||
// GetCreateConfig returns the container's current Config converted into a format
|
||||
// that can be re-submitted to the Docker create API.
|
||||
//
|
||||
// Ideally, we'd just be able to take the ContainerConfig from the old container
|
||||
// and use it as the starting point for creating the new container; however,
|
||||
// the ContainerConfig that comes back from the Inspect call merges the default
|
||||
// configuration (the stuff specified in the metadata for the image itself)
|
||||
// with the overridden configuration (the stuff that you might specify as part
|
||||
// of the "docker run"). In order to avoid unintentionally overriding the
|
||||
// of the "docker run").
|
||||
//
|
||||
// In order to avoid unintentionally overriding the
|
||||
// defaults in the new image we need to separate the override options from the
|
||||
// default options. To do this we have to compare the ContainerConfig for the
|
||||
// running container with the ContainerConfig from the image that container was
|
||||
// started from. This function returns a ContainerConfig which contains just
|
||||
// the options overridden at runtime.
|
||||
func (c Container) runtimeConfig() *dockercontainer.Config {
|
||||
func (c Container) GetCreateConfig() *dockercontainer.Config {
|
||||
config := c.containerInfo.Config
|
||||
hostConfig := c.containerInfo.HostConfig
|
||||
imageConfig := c.imageInfo.Config
|
||||
|
@ -251,6 +304,29 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
|
|||
}
|
||||
}
|
||||
|
||||
// Clear HEALTHCHECK configuration (if default)
|
||||
if config.Healthcheck != nil && imageConfig.Healthcheck != nil {
|
||||
if util.SliceEqual(config.Healthcheck.Test, imageConfig.Healthcheck.Test) {
|
||||
config.Healthcheck.Test = nil
|
||||
}
|
||||
|
||||
if config.Healthcheck.Retries == imageConfig.Healthcheck.Retries {
|
||||
config.Healthcheck.Retries = 0
|
||||
}
|
||||
|
||||
if config.Healthcheck.Interval == imageConfig.Healthcheck.Interval {
|
||||
config.Healthcheck.Interval = 0
|
||||
}
|
||||
|
||||
if config.Healthcheck.Timeout == imageConfig.Healthcheck.Timeout {
|
||||
config.Healthcheck.Timeout = 0
|
||||
}
|
||||
|
||||
if config.Healthcheck.StartPeriod == imageConfig.Healthcheck.StartPeriod {
|
||||
config.Healthcheck.StartPeriod = 0
|
||||
}
|
||||
}
|
||||
|
||||
config.Env = util.SliceSubtract(config.Env, imageConfig.Env)
|
||||
|
||||
config.Labels = util.StringMapSubtract(config.Labels, imageConfig.Labels)
|
||||
|
@ -271,9 +347,9 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
|
|||
return config
|
||||
}
|
||||
|
||||
// Any links in the HostConfig need to be re-written before they can be
|
||||
// re-submitted to the Docker create API.
|
||||
func (c Container) hostConfig() *dockercontainer.HostConfig {
|
||||
// GetCreateHostConfig returns the container's current HostConfig with any links
|
||||
// re-written so that they can be re-submitted to the Docker create API.
|
||||
func (c Container) GetCreateHostConfig() *dockercontainer.HostConfig {
|
||||
hostConfig := c.containerInfo.HostConfig
|
||||
|
||||
for i, link := range hostConfig.Links {
|
||||
|
|
79
pkg/container/container_mock_test.go
Normal file
79
pkg/container/container_mock_test.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerContainer "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
type MockContainerUpdate func(*types.ContainerJSON, *types.ImageInspect)
|
||||
|
||||
func MockContainer(updates ...MockContainerUpdate) *Container {
|
||||
containerInfo := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: "container_id",
|
||||
Image: "image",
|
||||
Name: "test-containrrr",
|
||||
HostConfig: &dockerContainer.HostConfig{},
|
||||
},
|
||||
Config: &dockerContainer.Config{
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
}
|
||||
image := types.ImageInspect{
|
||||
ID: "image_id",
|
||||
Config: &dockerContainer.Config{},
|
||||
}
|
||||
|
||||
for _, update := range updates {
|
||||
update(&containerInfo, &image)
|
||||
}
|
||||
return NewContainer(&containerInfo, &image)
|
||||
}
|
||||
|
||||
func WithPortBindings(portBindingSources ...string) MockContainerUpdate {
|
||||
return func(c *types.ContainerJSON, i *types.ImageInspect) {
|
||||
portBindings := nat.PortMap{}
|
||||
for _, pbs := range portBindingSources {
|
||||
portBindings[nat.Port(pbs)] = []nat.PortBinding{}
|
||||
}
|
||||
c.HostConfig.PortBindings = portBindings
|
||||
}
|
||||
}
|
||||
|
||||
func WithImageName(name string) MockContainerUpdate {
|
||||
return func(c *types.ContainerJSON, i *types.ImageInspect) {
|
||||
c.Config.Image = name
|
||||
i.RepoTags = append(i.RepoTags, name)
|
||||
}
|
||||
}
|
||||
|
||||
func WithLinks(links []string) MockContainerUpdate {
|
||||
return func(c *types.ContainerJSON, i *types.ImageInspect) {
|
||||
c.HostConfig.Links = links
|
||||
}
|
||||
}
|
||||
|
||||
func WithLabels(labels map[string]string) MockContainerUpdate {
|
||||
return func(c *types.ContainerJSON, i *types.ImageInspect) {
|
||||
c.Config.Labels = labels
|
||||
}
|
||||
}
|
||||
|
||||
func WithContainerState(state types.ContainerState) MockContainerUpdate {
|
||||
return func(cnt *types.ContainerJSON, img *types.ImageInspect) {
|
||||
cnt.State = &state
|
||||
}
|
||||
}
|
||||
|
||||
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,8 +1,8 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
dc "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
@ -12,7 +12,7 @@ var _ = Describe("the container", func() {
|
|||
Describe("VerifyConfiguration", func() {
|
||||
When("verifying a container with no image info", func() {
|
||||
It("should return an error", func() {
|
||||
c := mockContainerWithPortBindings()
|
||||
c := MockContainer(WithPortBindings())
|
||||
c.imageInfo = nil
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).To(Equal(errorNoImageInfo))
|
||||
|
@ -20,7 +20,7 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
When("verifying a container with no container info", func() {
|
||||
It("should return an error", func() {
|
||||
c := mockContainerWithPortBindings()
|
||||
c := MockContainer(WithPortBindings())
|
||||
c.containerInfo = nil
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).To(Equal(errorNoContainerInfo))
|
||||
|
@ -28,7 +28,7 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
When("verifying a container with no config", func() {
|
||||
It("should return an error", func() {
|
||||
c := mockContainerWithPortBindings()
|
||||
c := MockContainer(WithPortBindings())
|
||||
c.containerInfo.Config = nil
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).To(Equal(errorInvalidConfig))
|
||||
|
@ -36,7 +36,7 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
When("verifying a container with no host config", func() {
|
||||
It("should return an error", func() {
|
||||
c := mockContainerWithPortBindings()
|
||||
c := MockContainer(WithPortBindings())
|
||||
c.containerInfo.HostConfig = nil
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).To(Equal(errorInvalidConfig))
|
||||
|
@ -44,14 +44,14 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
When("verifying a container with no port bindings", func() {
|
||||
It("should not return an error", func() {
|
||||
c := mockContainerWithPortBindings()
|
||||
c := MockContainer(WithPortBindings())
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
When("verifying a container with port bindings, but no exposed ports", func() {
|
||||
It("should make the config compatible with updating", func() {
|
||||
c := mockContainerWithPortBindings("80/tcp")
|
||||
c := MockContainer(WithPortBindings("80/tcp"))
|
||||
c.containerInfo.Config.ExposedPorts = nil
|
||||
Expect(c.VerifyConfiguration()).To(Succeed())
|
||||
|
||||
|
@ -61,20 +61,107 @@ var _ = Describe("the container", func() {
|
|||
})
|
||||
When("verifying a container with port bindings and exposed ports is non-nil", func() {
|
||||
It("should return an error", func() {
|
||||
c := mockContainerWithPortBindings("80/tcp")
|
||||
c := MockContainer(WithPortBindings("80/tcp"))
|
||||
c.containerInfo.Config.ExposedPorts = map[nat.Port]struct{}{"80/tcp": {}}
|
||||
err := c.VerifyConfiguration()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
Describe("GetCreateConfig", func() {
|
||||
When("container healthcheck config is equal to image config", func() {
|
||||
It("should return empty healthcheck values", func() {
|
||||
c := MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
}), WithImageHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))
|
||||
|
||||
c = MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
Timeout: 30,
|
||||
}), WithImageHealthcheck(dc.HealthConfig{
|
||||
Timeout: 30,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))
|
||||
|
||||
c = MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
StartPeriod: 30,
|
||||
}), WithImageHealthcheck(dc.HealthConfig{
|
||||
StartPeriod: 30,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))
|
||||
|
||||
c = MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
Retries: 30,
|
||||
}), WithImageHealthcheck(dc.HealthConfig{
|
||||
Retries: 30,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{}))
|
||||
})
|
||||
})
|
||||
When("container healthcheck config is different to image config", func() {
|
||||
It("should return the container healthcheck values", func() {
|
||||
c := MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
Interval: 30,
|
||||
Timeout: 30,
|
||||
StartPeriod: 10,
|
||||
Retries: 2,
|
||||
}), WithImageHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "10s"},
|
||||
Interval: 10,
|
||||
Timeout: 60,
|
||||
StartPeriod: 30,
|
||||
Retries: 10,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
Interval: 30,
|
||||
Timeout: 30,
|
||||
StartPeriod: 10,
|
||||
Retries: 2,
|
||||
}))
|
||||
})
|
||||
})
|
||||
When("container healthcheck config is empty", func() {
|
||||
It("should not panic", func() {
|
||||
c := MockContainer(WithImageHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "10s"},
|
||||
Interval: 10,
|
||||
Timeout: 60,
|
||||
StartPeriod: 30,
|
||||
Retries: 10,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(BeNil())
|
||||
})
|
||||
})
|
||||
When("container image healthcheck config is empty", func() {
|
||||
It("should not panic", func() {
|
||||
c := MockContainer(WithHealthcheck(dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
Interval: 30,
|
||||
Timeout: 30,
|
||||
StartPeriod: 10,
|
||||
Retries: 2,
|
||||
}))
|
||||
Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{
|
||||
Test: []string{"/usr/bin/sleep", "1s"},
|
||||
Interval: 30,
|
||||
Timeout: 30,
|
||||
StartPeriod: 10,
|
||||
Retries: 2,
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
When("asked for metadata", func() {
|
||||
var c *Container
|
||||
BeforeEach(func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.enable": "true",
|
||||
"com.centurylinklabs.watchtower": "true",
|
||||
})
|
||||
}))
|
||||
})
|
||||
It("should return its name on calls to .Name()", func() {
|
||||
name := c.Name()
|
||||
|
@ -91,36 +178,28 @@ var _ = Describe("the container", func() {
|
|||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeTrue())
|
||||
Expect(enabled).NotTo(BeFalse())
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(exists).NotTo(BeFalse())
|
||||
})
|
||||
It("should return false, true if present but not true on calls to .Enabled()", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "false"})
|
||||
c = MockContainer(WithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "false"}))
|
||||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeFalse())
|
||||
Expect(enabled).NotTo(BeTrue())
|
||||
Expect(exists).To(BeTrue())
|
||||
Expect(exists).NotTo(BeFalse())
|
||||
})
|
||||
It("should return false, false if not present on calls to .Enabled()", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"lol": "false"})
|
||||
c = MockContainer(WithLabels(map[string]string{"lol": "false"}))
|
||||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeFalse())
|
||||
Expect(enabled).NotTo(BeTrue())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(exists).NotTo(BeTrue())
|
||||
})
|
||||
It("should return false, false if present but not parsable .Enabled()", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "falsy"})
|
||||
c = MockContainer(WithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "falsy"}))
|
||||
enabled, exists := c.Enabled()
|
||||
|
||||
Expect(enabled).To(BeFalse())
|
||||
Expect(enabled).NotTo(BeTrue())
|
||||
Expect(exists).To(BeFalse())
|
||||
Expect(exists).NotTo(BeTrue())
|
||||
})
|
||||
When("checking if its a watchtower instance", func() {
|
||||
It("should return true if the label is set to true", func() {
|
||||
|
@ -128,31 +207,31 @@ var _ = Describe("the container", func() {
|
|||
Expect(isWatchtower).To(BeTrue())
|
||||
})
|
||||
It("should return false if the label is present but set to false", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower": "false"})
|
||||
c = MockContainer(WithLabels(map[string]string{"com.centurylinklabs.watchtower": "false"}))
|
||||
isWatchtower := c.IsWatchtower()
|
||||
Expect(isWatchtower).To(BeFalse())
|
||||
})
|
||||
It("should return false if the label is not present", func() {
|
||||
c = mockContainerWithLabels(map[string]string{"funny.label": "false"})
|
||||
c = MockContainer(WithLabels(map[string]string{"funny.label": "false"}))
|
||||
isWatchtower := c.IsWatchtower()
|
||||
Expect(isWatchtower).To(BeFalse())
|
||||
})
|
||||
It("should return false if there are no labels", func() {
|
||||
c = mockContainerWithLabels(map[string]string{})
|
||||
c = MockContainer(WithLabels(map[string]string{}))
|
||||
isWatchtower := c.IsWatchtower()
|
||||
Expect(isWatchtower).To(BeFalse())
|
||||
})
|
||||
})
|
||||
When("fetching the custom stop signal", func() {
|
||||
It("should return the signal if its set", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.stop-signal": "SIGKILL",
|
||||
})
|
||||
}))
|
||||
stopSignal := c.StopSignal()
|
||||
Expect(stopSignal).To(Equal("SIGKILL"))
|
||||
})
|
||||
It("should return an empty string if its not set", func() {
|
||||
c = mockContainerWithLabels(map[string]string{})
|
||||
c = MockContainer(WithLabels(map[string]string{}))
|
||||
stopSignal := c.StopSignal()
|
||||
Expect(stopSignal).To(Equal(""))
|
||||
})
|
||||
|
@ -160,22 +239,22 @@ var _ = Describe("the container", func() {
|
|||
When("fetching the image name", func() {
|
||||
When("the zodiac label is present", func() {
|
||||
It("should fetch the image name from it", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.zodiac.original-image": "the-original-image",
|
||||
})
|
||||
}))
|
||||
imageName := c.ImageName()
|
||||
Expect(imageName).To(Equal(imageName))
|
||||
})
|
||||
})
|
||||
It("should return the image name", func() {
|
||||
name := "image-name:3"
|
||||
c = mockContainerWithImageName(name)
|
||||
c = MockContainer(WithImageName(name))
|
||||
imageName := c.ImageName()
|
||||
Expect(imageName).To(Equal(name))
|
||||
})
|
||||
It("should assume latest if no tag is supplied", func() {
|
||||
name := "image-name"
|
||||
c = mockContainerWithImageName(name)
|
||||
c = MockContainer(WithImageName(name))
|
||||
imageName := c.ImageName()
|
||||
Expect(imageName).To(Equal(name + ":latest"))
|
||||
})
|
||||
|
@ -184,113 +263,129 @@ var _ = Describe("the container", func() {
|
|||
When("fetching container links", func() {
|
||||
When("the depends on label is present", func() {
|
||||
It("should fetch depending containers from it", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.depends-on": "postgres",
|
||||
})
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("postgres"), HaveLen(1)))
|
||||
Expect(links).To(SatisfyAll(ContainElement("/postgres"), HaveLen(1)))
|
||||
})
|
||||
It("should fetch depending containers if there are many", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.depends-on": "postgres,redis",
|
||||
})
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("postgres"), ContainElement("redis"), HaveLen(2)))
|
||||
Expect(links).To(SatisfyAll(ContainElement("/postgres"), ContainElement("/redis"), HaveLen(2)))
|
||||
})
|
||||
It("should only add slashes to names when they are missing", func() {
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.depends-on": "/postgres,redis",
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("/postgres"), ContainElement("/redis")))
|
||||
})
|
||||
It("should fetch depending containers if label is blank", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.depends-on": "",
|
||||
})
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(HaveLen(0))
|
||||
})
|
||||
})
|
||||
When("the depends on label is not present", func() {
|
||||
It("should fetch depending containers from host config links", func() {
|
||||
c = mockContainerWithLinks([]string{
|
||||
c = MockContainer(WithLinks([]string{
|
||||
"redis:test-containrrr",
|
||||
"postgres:test-containrrr",
|
||||
})
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("redis"), ContainElement("postgres"), HaveLen(2)))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("checking no-pull label", func() {
|
||||
When("no-pull argument is not set", func() {
|
||||
When("no-pull label is true", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "true",
|
||||
}))
|
||||
It("should return true", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(true))
|
||||
})
|
||||
})
|
||||
When("no-pull label is false", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "false",
|
||||
}))
|
||||
It("should return false", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))
|
||||
})
|
||||
})
|
||||
When("no-pull label is set to an invalid value", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "maybe",
|
||||
}))
|
||||
It("should return false", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))
|
||||
})
|
||||
})
|
||||
When("no-pull label is unset", func() {
|
||||
c = MockContainer(WithLabels(map[string]string{}))
|
||||
It("should return false", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false))
|
||||
})
|
||||
})
|
||||
})
|
||||
When("no-pull argument is set to true", func() {
|
||||
When("no-pull label is true", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "true",
|
||||
}))
|
||||
It("should return true", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{NoPull: true})).To(Equal(true))
|
||||
})
|
||||
})
|
||||
When("no-pull label is false", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "false",
|
||||
}))
|
||||
It("should return true", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{NoPull: true})).To(Equal(true))
|
||||
})
|
||||
})
|
||||
When("label-take-precedence argument is set to true", func() {
|
||||
When("no-pull label is true", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "true",
|
||||
}))
|
||||
It("should return true", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{LabelPrecedence: true, NoPull: true})).To(Equal(true))
|
||||
})
|
||||
})
|
||||
When("no-pull label is false", func() {
|
||||
c := MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.no-pull": "false",
|
||||
}))
|
||||
It("should return false", func() {
|
||||
Expect(c.IsNoPull(types.UpdateParams{LabelPrecedence: true, NoPull: true})).To(Equal(false))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("there is a pre or post update timeout", func() {
|
||||
It("should return minute values", func() {
|
||||
c = mockContainerWithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "3",
|
||||
"com.centurylinklabs.watchtower.lifecycle.post-update-timeout": "5",
|
||||
})
|
||||
preTimeout := c.PreUpdateTimeout()
|
||||
Expect(preTimeout).To(Equal(3))
|
||||
postTimeout := c.PostUpdateTimeout()
|
||||
Expect(postTimeout).To(Equal(5))
|
||||
})
|
||||
})
|
||||
It("should return minute values", func() {
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "3",
|
||||
"com.centurylinklabs.watchtower.lifecycle.post-update-timeout": "5",
|
||||
}))
|
||||
preTimeout := c.PreUpdateTimeout()
|
||||
Expect(preTimeout).To(Equal(3))
|
||||
postTimeout := c.PostUpdateTimeout()
|
||||
Expect(postTimeout).To(Equal(5))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
func mockContainerWithPortBindings(portBindingSources ...string) *Container {
|
||||
mockContainer := mockContainerWithLabels(nil)
|
||||
mockContainer.imageInfo = &types.ImageInspect{}
|
||||
hostConfig := &container.HostConfig{
|
||||
PortBindings: nat.PortMap{},
|
||||
}
|
||||
for _, pbs := range portBindingSources {
|
||||
hostConfig.PortBindings[nat.Port(pbs)] = []nat.PortBinding{}
|
||||
}
|
||||
mockContainer.containerInfo.HostConfig = hostConfig
|
||||
return mockContainer
|
||||
}
|
||||
|
||||
func mockContainerWithImageName(name string) *Container {
|
||||
mockContainer := mockContainerWithLabels(nil)
|
||||
mockContainer.containerInfo.Config.Image = name
|
||||
return mockContainer
|
||||
}
|
||||
|
||||
func mockContainerWithLinks(links []string) *Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: "container_id",
|
||||
Image: "image",
|
||||
Name: "test-containrrr",
|
||||
HostConfig: &container.HostConfig{
|
||||
Links: links,
|
||||
},
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
}
|
||||
return NewContainer(&content, nil)
|
||||
}
|
||||
|
||||
func mockContainerWithLabels(labels map[string]string) *Container {
|
||||
content := types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
ID: "container_id",
|
||||
Image: "image",
|
||||
Name: "test-containrrr",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: labels,
|
||||
},
|
||||
}
|
||||
return NewContainer(&content, nil)
|
||||
}
|
||||
|
||||
func newClientNoAPI(pullImages, includeStopped, reviveStopped, removeVolumes, includeRestarting bool, warnOnHeadFailed string) Client {
|
||||
return dockerClient{
|
||||
api: nil,
|
||||
pullImages: pullImages,
|
||||
removeVolumes: removeVolumes,
|
||||
includeStopped: includeStopped,
|
||||
reviveStopped: reviveStopped,
|
||||
includeRestarting: includeRestarting,
|
||||
warnOnHeadFailed: warnOnHeadFailed,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,5 +4,5 @@ import "errors"
|
|||
|
||||
var errorNoImageInfo = errors.New("no available image info")
|
||||
var errorNoContainerInfo = errors.New("no available container info")
|
||||
var errorNoExposedPorts = errors.New("exposed ports does not match port bindings")
|
||||
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
|
||||
|
||||
import "strconv"
|
||||
|
||||
const (
|
||||
watchtowerLabel = "com.centurylinklabs.watchtower"
|
||||
signalLabel = "com.centurylinklabs.watchtower.stop-signal"
|
||||
enableLabel = "com.centurylinklabs.watchtower.enable"
|
||||
monitorOnlyLabel = "com.centurylinklabs.watchtower.monitor-only"
|
||||
dependsOnLabel = "com.centurylinklabs.watchtower.depends-on"
|
||||
zodiacLabel = "com.centurylinklabs.zodiac.original-image"
|
||||
scope = "com.centurylinklabs.watchtower.scope"
|
||||
preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check"
|
||||
postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check"
|
||||
preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update"
|
||||
postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update"
|
||||
preUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout"
|
||||
watchtowerLabel = "com.centurylinklabs.watchtower"
|
||||
signalLabel = "com.centurylinklabs.watchtower.stop-signal"
|
||||
enableLabel = "com.centurylinklabs.watchtower.enable"
|
||||
monitorOnlyLabel = "com.centurylinklabs.watchtower.monitor-only"
|
||||
noPullLabel = "com.centurylinklabs.watchtower.no-pull"
|
||||
dependsOnLabel = "com.centurylinklabs.watchtower.depends-on"
|
||||
zodiacLabel = "com.centurylinklabs.zodiac.original-image"
|
||||
scope = "com.centurylinklabs.watchtower.scope"
|
||||
preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check"
|
||||
postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check"
|
||||
preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update"
|
||||
postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update"
|
||||
preUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.pre-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]
|
||||
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,22 +3,26 @@ package mocks
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/onsi/ginkgo"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
O "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/ghttp"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func getMockJSONFile(relPath string) ([]byte, error) {
|
||||
absPath, _ := filepath.Abs(relPath)
|
||||
buf, err := ioutil.ReadFile(absPath)
|
||||
buf, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
// logrus.WithError(err).WithField("file", absPath).Error(err)
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("mock JSON file %q not found: %e", absPath, err)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
@ -39,19 +43,22 @@ func respondWithJSONFile(relPath string, statusCode int, optionalHeader ...http.
|
|||
}
|
||||
|
||||
// GetContainerHandlers returns the handlers serving lookups for the supplied container mock files
|
||||
func GetContainerHandlers(containerFiles ...string) []http.HandlerFunc {
|
||||
handlers := make([]http.HandlerFunc, 0, len(containerFiles)*2)
|
||||
for _, file := range containerFiles {
|
||||
handlers = append(handlers, getContainerHandler(file))
|
||||
func GetContainerHandlers(containerRefs ...*ContainerRef) []http.HandlerFunc {
|
||||
handlers := make([]http.HandlerFunc, 0, len(containerRefs)*3)
|
||||
for _, containerRef := range containerRefs {
|
||||
handlers = append(handlers, getContainerFileHandler(containerRef))
|
||||
|
||||
// Also append any containers that the container references, if any
|
||||
for _, ref := range containerRef.references {
|
||||
handlers = append(handlers, getContainerFileHandler(ref))
|
||||
}
|
||||
|
||||
// Also append the image request since that will be called for every container
|
||||
if file == "running" {
|
||||
// The "running" container is the only one using image02
|
||||
handlers = append(handlers, getImageHandler(1))
|
||||
} else {
|
||||
handlers = append(handlers, getImageHandler(0))
|
||||
}
|
||||
handlers = append(handlers, getImageHandler(containerRef.image.id,
|
||||
RespondWithJSONFile(containerRef.image.getFileName(), http.StatusOK),
|
||||
))
|
||||
}
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
|
@ -63,34 +70,120 @@ func createFilterArgs(statuses []string) filters.Args {
|
|||
return args
|
||||
}
|
||||
|
||||
var containerFileIds = map[string]string{
|
||||
"stopped": "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65",
|
||||
"watchtower": "3d88e0e3543281c747d88b27e246578b65ae8964ba86c7cd7522cf84e0978134",
|
||||
"running": "b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008",
|
||||
"restarting": "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b67",
|
||||
var defaultImage = imageRef{
|
||||
// watchtower
|
||||
id: t.ImageID("sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa"),
|
||||
file: "default",
|
||||
}
|
||||
|
||||
var imageIds = []string{
|
||||
"sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa",
|
||||
"sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd",
|
||||
var Watchtower = ContainerRef{
|
||||
name: "watchtower",
|
||||
id: "3d88e0e3543281c747d88b27e246578b65ae8964ba86c7cd7522cf84e0978134",
|
||||
image: &defaultImage,
|
||||
}
|
||||
var Stopped = ContainerRef{
|
||||
name: "stopped",
|
||||
id: "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65",
|
||||
image: &defaultImage,
|
||||
}
|
||||
var Running = ContainerRef{
|
||||
name: "running",
|
||||
id: "b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008",
|
||||
image: &imageRef{
|
||||
// portainer
|
||||
id: t.ImageID("sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd"),
|
||||
file: "running",
|
||||
},
|
||||
}
|
||||
var Restarting = ContainerRef{
|
||||
name: "restarting",
|
||||
id: "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b67",
|
||||
image: &defaultImage,
|
||||
}
|
||||
|
||||
func getContainerHandler(file string) http.HandlerFunc {
|
||||
id, ok := containerFileIds[file]
|
||||
failTestUnless(ok)
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("GET", O.HaveSuffix("/containers/%v/json", id)),
|
||||
RespondWithJSONFile(fmt.Sprintf("./mocks/data/container_%v.json", file), http.StatusOK),
|
||||
var netSupplierOK = ContainerRef{
|
||||
id: "25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2",
|
||||
name: "net_supplier",
|
||||
image: &imageRef{
|
||||
// gluetun
|
||||
id: t.ImageID("sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51"),
|
||||
file: "net_producer",
|
||||
},
|
||||
}
|
||||
var netSupplierNotFound = ContainerRef{
|
||||
id: NetSupplierNotFoundID,
|
||||
name: netSupplierOK.name,
|
||||
isMissing: true,
|
||||
}
|
||||
|
||||
// NetConsumerOK is used for testing `container` networking mode
|
||||
// returns a container that consumes an existing supplier container
|
||||
var NetConsumerOK = ContainerRef{
|
||||
id: "1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6",
|
||||
name: "net_consumer",
|
||||
image: &imageRef{
|
||||
id: t.ImageID("sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8"), // nginx
|
||||
file: "net_consumer",
|
||||
},
|
||||
references: []*ContainerRef{&netSupplierOK},
|
||||
}
|
||||
|
||||
// NetConsumerInvalidSupplier is used for testing `container` networking mode
|
||||
// returns a container that references a supplying container that does not exist
|
||||
var NetConsumerInvalidSupplier = ContainerRef{
|
||||
id: NetConsumerOK.id,
|
||||
name: "net_consumer-missing_supplier",
|
||||
image: NetConsumerOK.image,
|
||||
references: []*ContainerRef{&netSupplierNotFound},
|
||||
}
|
||||
|
||||
const NetSupplierNotFoundID = "badc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc"
|
||||
const NetSupplierContainerName = "/wt-contnet-producer-1"
|
||||
|
||||
func getContainerFileHandler(cr *ContainerRef) http.HandlerFunc {
|
||||
|
||||
if cr.isMissing {
|
||||
return containerNotFoundResponse(string(cr.id))
|
||||
}
|
||||
|
||||
containerFile, err := cr.getContainerFile()
|
||||
if err != nil {
|
||||
ginkgo.Fail(fmt.Sprintf("Failed to get container mock file: %v", err))
|
||||
}
|
||||
|
||||
return getContainerHandler(
|
||||
string(cr.id),
|
||||
RespondWithJSONFile(containerFile, http.StatusOK),
|
||||
)
|
||||
}
|
||||
|
||||
func getContainerHandler(containerId string, responseHandler http.HandlerFunc) http.HandlerFunc {
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("GET", O.HaveSuffix("/containers/%v/json", containerId)),
|
||||
responseHandler,
|
||||
)
|
||||
}
|
||||
|
||||
// GetContainerHandler mocks the GET containers/{id}/json endpoint
|
||||
func GetContainerHandler(containerID string, containerInfo *types.ContainerJSON) http.HandlerFunc {
|
||||
responseHandler := containerNotFoundResponse(containerID)
|
||||
if containerInfo != nil {
|
||||
responseHandler = ghttp.RespondWithJSONEncoded(http.StatusOK, containerInfo)
|
||||
}
|
||||
return getContainerHandler(containerID, responseHandler)
|
||||
}
|
||||
|
||||
// GetImageHandler mocks the GET images/{id}/json endpoint
|
||||
func GetImageHandler(imageInfo *types.ImageInspect) http.HandlerFunc {
|
||||
return getImageHandler(t.ImageID(imageInfo.ID), ghttp.RespondWithJSONEncoded(http.StatusOK, imageInfo))
|
||||
}
|
||||
|
||||
// ListContainersHandler mocks the GET containers/json endpoint, filtering the returned containers based on statuses
|
||||
func ListContainersHandler(statuses ...string) http.HandlerFunc {
|
||||
filterArgs := createFilterArgs(statuses)
|
||||
bytes, err := filterArgs.MarshalJSON()
|
||||
O.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred())
|
||||
query := url.Values{
|
||||
"limit": []string{"0"},
|
||||
"filters": []string{string(bytes)},
|
||||
}
|
||||
return ghttp.CombineHandlers(
|
||||
|
@ -116,13 +209,72 @@ func respondWithFilteredContainers(filters filters.Args) http.HandlerFunc {
|
|||
return ghttp.RespondWithJSONEncoded(http.StatusOK, filteredContainers)
|
||||
}
|
||||
|
||||
func getImageHandler(index int) http.HandlerFunc {
|
||||
func getImageHandler(imageId t.ImageID, responseHandler http.HandlerFunc) http.HandlerFunc {
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%v/json", imageIds[index])),
|
||||
RespondWithJSONFile(fmt.Sprintf("./mocks/data/image%02d.json", index+1), http.StatusOK),
|
||||
ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%s/json", imageId)),
|
||||
responseHandler,
|
||||
)
|
||||
}
|
||||
|
||||
func failTestUnless(ok bool) {
|
||||
O.ExpectWithOffset(2, ok).To(O.BeTrue(), "test setup failed")
|
||||
// KillContainerHandler mocks the POST containers/{id}/kill endpoint
|
||||
func KillContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
|
||||
responseHandler := noContentStatusResponse
|
||||
if !found {
|
||||
responseHandler = containerNotFoundResponse(containerID)
|
||||
}
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("POST", O.HaveSuffix("containers/%s/kill", containerID)),
|
||||
responseHandler,
|
||||
)
|
||||
}
|
||||
|
||||
// RemoveContainerHandler mocks the DELETE containers/{id} endpoint
|
||||
func RemoveContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
|
||||
responseHandler := noContentStatusResponse
|
||||
if !found {
|
||||
responseHandler = containerNotFoundResponse(containerID)
|
||||
}
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("DELETE", O.HaveSuffix("containers/%s", containerID)),
|
||||
responseHandler,
|
||||
)
|
||||
}
|
||||
|
||||
func containerNotFoundResponse(containerID string) http.HandlerFunc {
|
||||
return ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{message: "No such container: " + string(containerID)})
|
||||
}
|
||||
|
||||
var noContentStatusResponse = ghttp.RespondWith(http.StatusNoContent, nil)
|
||||
|
||||
type FoundStatus bool
|
||||
|
||||
const (
|
||||
Found FoundStatus = true
|
||||
Missing FoundStatus = false
|
||||
)
|
||||
|
||||
// RemoveImageHandler mocks the DELETE images/ID endpoint, simulating removal of the given imagesWithParents
|
||||
func RemoveImageHandler(imagesWithParents map[string][]string) http.HandlerFunc {
|
||||
return ghttp.CombineHandlers(
|
||||
ghttp.VerifyRequest("DELETE", O.MatchRegexp("/images/.*")),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(r.URL.Path, `/`)
|
||||
image := parts[len(parts)-1]
|
||||
|
||||
if parents, found := imagesWithParents[image]; found {
|
||||
items := []types.ImageDeleteResponseItem{
|
||||
{Untagged: image},
|
||||
{Deleted: image},
|
||||
}
|
||||
for _, parent := range parents {
|
||||
items = append(items, types.ImageDeleteResponseItem{Deleted: parent})
|
||||
}
|
||||
ghttp.RespondWithJSONEncoded(http.StatusOK, items)(w, r)
|
||||
} else {
|
||||
ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{
|
||||
message: "Something went wrong.",
|
||||
})(w, r)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -78,3 +78,17 @@ func (_m *FilterableContainer) Scope() (string, bool) {
|
|||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ImageName provides a mock function with given fields:
|
||||
func (_m *FilterableContainer) ImageName() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
|
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"
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
package filters
|
||||
|
||||
import (
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
// WatchtowerContainersFilter filters only watchtower containers
|
||||
|
@ -11,7 +13,7 @@ func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatch
|
|||
// NoFilter will not filter out any containers
|
||||
func NoFilter(t.FilterableContainer) bool { return true }
|
||||
|
||||
// FilterByNames returns all containers that match the specified name
|
||||
// FilterByNames returns all containers that match one of the specified names
|
||||
func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
|
||||
if len(names) == 0 {
|
||||
return baseFilter
|
||||
|
@ -19,14 +21,42 @@ func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
|
|||
|
||||
return func(c t.FilterableContainer) bool {
|
||||
for _, name := range names {
|
||||
if (name == c.Name()) || (name == c.Name()[1:]) {
|
||||
if name == c.Name() || name == c.Name()[1:] {
|
||||
return baseFilter(c)
|
||||
}
|
||||
|
||||
if re, err := regexp.Compile(name); err == nil {
|
||||
indices := re.FindStringIndex(c.Name())
|
||||
if indices == nil {
|
||||
continue
|
||||
}
|
||||
start := indices[0]
|
||||
end := indices[1]
|
||||
if start <= 1 && end >= len(c.Name())-1 {
|
||||
return baseFilter(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// FilterByDisableNames returns all containers that don't match any of the specified names
|
||||
func FilterByDisableNames(disableNames []string, baseFilter t.Filter) t.Filter {
|
||||
if len(disableNames) == 0 {
|
||||
return baseFilter
|
||||
}
|
||||
|
||||
return func(c t.FilterableContainer) bool {
|
||||
for _, name := range disableNames {
|
||||
if name == c.Name() || name == c.Name()[1:] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return baseFilter(c)
|
||||
}
|
||||
}
|
||||
|
||||
// FilterByEnableLabel returns all containers that have the enabled label set
|
||||
func FilterByEnableLabel(baseFilter t.Filter) t.Filter {
|
||||
return func(c t.FilterableContainer) bool {
|
||||
|
@ -56,13 +86,14 @@ func FilterByDisabledLabel(baseFilter t.Filter) t.Filter {
|
|||
|
||||
// FilterByScope returns all containers that belongs to a specific scope
|
||||
func FilterByScope(scope string, baseFilter t.Filter) t.Filter {
|
||||
if scope == "" {
|
||||
return baseFilter
|
||||
}
|
||||
|
||||
return func(c t.FilterableContainer) bool {
|
||||
containerScope, ok := c.Scope()
|
||||
if ok && containerScope == scope {
|
||||
containerScope, containerHasScope := c.Scope()
|
||||
|
||||
if !containerHasScope || containerScope == "" {
|
||||
containerScope = "none"
|
||||
}
|
||||
|
||||
if containerScope == scope {
|
||||
return baseFilter(c)
|
||||
}
|
||||
|
||||
|
@ -70,14 +101,33 @@ func FilterByScope(scope string, baseFilter t.Filter) t.Filter {
|
|||
}
|
||||
}
|
||||
|
||||
// FilterByImage returns all containers that have a specific image
|
||||
func FilterByImage(images []string, baseFilter t.Filter) t.Filter {
|
||||
if images == nil {
|
||||
return baseFilter
|
||||
}
|
||||
|
||||
return func(c t.FilterableContainer) bool {
|
||||
image := strings.Split(c.ImageName(), ":")[0]
|
||||
for _, targetImage := range images {
|
||||
if image == targetImage {
|
||||
return baseFilter(c)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// BuildFilter creates the needed filter of containers
|
||||
func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, string) {
|
||||
func BuildFilter(names []string, disableNames []string, enableLabel bool, scope string) (t.Filter, string) {
|
||||
sb := strings.Builder{}
|
||||
filter := NoFilter
|
||||
filter = FilterByNames(names, filter)
|
||||
filter = FilterByDisableNames(disableNames, filter)
|
||||
|
||||
if len(names) > 0 {
|
||||
sb.WriteString("with name \"")
|
||||
sb.WriteString("which name matches \"")
|
||||
for i, n := range names {
|
||||
sb.WriteString(n)
|
||||
if i < len(names)-1 {
|
||||
|
@ -86,6 +136,16 @@ func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, stri
|
|||
}
|
||||
sb.WriteString(`", `)
|
||||
}
|
||||
if len(disableNames) > 0 {
|
||||
sb.WriteString("not named one of \"")
|
||||
for i, n := range disableNames {
|
||||
sb.WriteString(n)
|
||||
if i < len(disableNames)-1 {
|
||||
sb.WriteString(`" or "`)
|
||||
}
|
||||
}
|
||||
sb.WriteString(`", `)
|
||||
}
|
||||
|
||||
if enableLabel {
|
||||
// If label filtering is enabled, containers should only be considered
|
||||
|
@ -93,7 +153,13 @@ func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, stri
|
|||
filter = FilterByEnableLabel(filter)
|
||||
sb.WriteString("using enable label, ")
|
||||
}
|
||||
if scope != "" {
|
||||
|
||||
if scope == "none" {
|
||||
// If a scope has explicitly defined as "none", containers should only be considered
|
||||
// if they do not have a scope defined, or if it's explicitly set to "none".
|
||||
filter = FilterByScope(scope, filter)
|
||||
sb.WriteString(`without a scope, "`)
|
||||
} else if scope != "" {
|
||||
// If a scope has been defined, containers should only be considered
|
||||
// if the scope is specifically set.
|
||||
filter = FilterByScope(scope, filter)
|
||||
|
|
|
@ -47,6 +47,28 @@ func TestFilterByNames(t *testing.T) {
|
|||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestFilterByNamesRegex(t *testing.T) {
|
||||
names := []string{`ba(b|ll)oon`}
|
||||
|
||||
filter := FilterByNames(names, NoFilter)
|
||||
assert.NotNil(t, filter)
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("balloon")
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("spoon")
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("baboonious")
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestFilterByEnableLabel(t *testing.T) {
|
||||
filter := FilterByEnableLabel(NoFilter)
|
||||
assert.NotNil(t, filter)
|
||||
|
@ -68,8 +90,7 @@ func TestFilterByEnableLabel(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFilterByScope(t *testing.T) {
|
||||
var scope string
|
||||
scope = "testscope"
|
||||
scope := "testscope"
|
||||
|
||||
filter := FilterByScope(scope, NoFilter)
|
||||
assert.NotNil(t, filter)
|
||||
|
@ -90,6 +111,53 @@ func TestFilterByScope(t *testing.T) {
|
|||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestFilterByNoneScope(t *testing.T) {
|
||||
scope := "none"
|
||||
|
||||
filter := FilterByScope(scope, NoFilter)
|
||||
assert.NotNil(t, filter)
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("Scope").Return("anyscope", true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Scope").Return("", false)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Scope").Return("", true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Scope").Return("none", true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestBuildFilterNoneScope(t *testing.T) {
|
||||
filter, desc := BuildFilter(nil, nil, false, "none")
|
||||
|
||||
assert.Contains(t, desc, "without a scope")
|
||||
|
||||
scoped := new(mocks.FilterableContainer)
|
||||
scoped.On("Enabled").Return(false, false)
|
||||
scoped.On("Scope").Return("anyscope", true)
|
||||
|
||||
unscoped := new(mocks.FilterableContainer)
|
||||
unscoped.On("Enabled").Return(false, false)
|
||||
unscoped.On("Scope").Return("", false)
|
||||
|
||||
assert.False(t, filter(scoped))
|
||||
assert.True(t, filter(unscoped))
|
||||
|
||||
scoped.AssertExpectations(t)
|
||||
unscoped.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestFilterByDisabledLabel(t *testing.T) {
|
||||
filter := FilterByDisabledLabel(NoFilter)
|
||||
assert.NotNil(t, filter)
|
||||
|
@ -110,12 +178,50 @@ func TestFilterByDisabledLabel(t *testing.T) {
|
|||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestBuildFilter(t *testing.T) {
|
||||
var names []string
|
||||
names = append(names, "test")
|
||||
func TestFilterByImage(t *testing.T) {
|
||||
filterEmpty := FilterByImage(nil, NoFilter)
|
||||
filterSingle := FilterByImage([]string{"registry"}, NoFilter)
|
||||
filterMultiple := FilterByImage([]string{"registry", "bla"}, NoFilter)
|
||||
assert.NotNil(t, filterSingle)
|
||||
assert.NotNil(t, filterMultiple)
|
||||
|
||||
filter, desc := BuildFilter(names, false, "")
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("ImageName").Return("registry:2")
|
||||
assert.True(t, filterEmpty(container))
|
||||
assert.True(t, filterSingle(container))
|
||||
assert.True(t, filterMultiple(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("ImageName").Return("registry:latest")
|
||||
assert.True(t, filterEmpty(container))
|
||||
assert.True(t, filterSingle(container))
|
||||
assert.True(t, filterMultiple(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("ImageName").Return("abcdef1234")
|
||||
assert.True(t, filterEmpty(container))
|
||||
assert.False(t, filterSingle(container))
|
||||
assert.False(t, filterMultiple(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("ImageName").Return("bla:latest")
|
||||
assert.True(t, filterEmpty(container))
|
||||
assert.False(t, filterSingle(container))
|
||||
assert.True(t, filterMultiple(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
}
|
||||
|
||||
func TestBuildFilter(t *testing.T) {
|
||||
names := []string{"test", "valid"}
|
||||
|
||||
filter, desc := BuildFilter(names, []string{}, false, "")
|
||||
assert.Contains(t, desc, "test")
|
||||
assert.Contains(t, desc, "or")
|
||||
assert.Contains(t, desc, "valid")
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("Invalid")
|
||||
|
@ -151,7 +257,7 @@ func TestBuildFilterEnableLabel(t *testing.T) {
|
|||
var names []string
|
||||
names = append(names, "test")
|
||||
|
||||
filter, desc := BuildFilter(names, true, "")
|
||||
filter, desc := BuildFilter(names, []string{}, true, "")
|
||||
assert.Contains(t, desc, "using enable label")
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
|
@ -176,3 +282,52 @@ func TestBuildFilterEnableLabel(t *testing.T) {
|
|||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestBuildFilterDisableContainer(t *testing.T) {
|
||||
filter, desc := BuildFilter([]string{}, []string{"excluded", "notfound"}, false, "")
|
||||
assert.Contains(t, desc, "not named")
|
||||
assert.Contains(t, desc, "excluded")
|
||||
assert.Contains(t, desc, "or")
|
||||
assert.Contains(t, desc, "notfound")
|
||||
|
||||
container := new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("Another")
|
||||
container.On("Enabled").Return(false, false)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("AnotherOne")
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("test")
|
||||
container.On("Enabled").Return(false, false)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("excluded")
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("excludedAsSubstring")
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.True(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Name").Return("notfound")
|
||||
container.On("Enabled").Return(true, true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
|
||||
container = new(mocks.FilterableContainer)
|
||||
container.On("Enabled").Return(false, true)
|
||||
assert.False(t, filter(container))
|
||||
container.AssertExpectations(t)
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ func ExecutePostChecks(client container.Client, params types.UpdateParams) {
|
|||
}
|
||||
|
||||
// ExecutePreCheckCommand tries to run the pre-check lifecycle hook for a single container.
|
||||
func ExecutePreCheckCommand(client container.Client, container container.Container) {
|
||||
func ExecutePreCheckCommand(client container.Client, container types.Container) {
|
||||
clog := log.WithField("container", container.Name())
|
||||
command := container.GetLifecyclePreCheckCommand()
|
||||
if len(command) == 0 {
|
||||
|
@ -45,7 +45,7 @@ func ExecutePreCheckCommand(client container.Client, container container.Contain
|
|||
}
|
||||
|
||||
// ExecutePostCheckCommand tries to run the post-check lifecycle hook for a single container.
|
||||
func ExecutePostCheckCommand(client container.Client, container container.Container) {
|
||||
func ExecutePostCheckCommand(client container.Client, container types.Container) {
|
||||
clog := log.WithField("container", container.Name())
|
||||
command := container.GetLifecyclePostCheckCommand()
|
||||
if len(command) == 0 {
|
||||
|
@ -61,7 +61,7 @@ func ExecutePostCheckCommand(client container.Client, container container.Contai
|
|||
}
|
||||
|
||||
// ExecutePreUpdateCommand tries to run the pre-update lifecycle hook for a single container.
|
||||
func ExecutePreUpdateCommand(client container.Client, container container.Container) (SkipUpdate bool, err error) {
|
||||
func ExecutePreUpdateCommand(client container.Client, container types.Container) (SkipUpdate bool, err error) {
|
||||
timeout := container.PreUpdateTimeout()
|
||||
command := container.GetLifecyclePreUpdateCommand()
|
||||
clog := log.WithField("container", container.Name())
|
||||
|
|
40
pkg/notifications/common_templates.go
Normal file
40
pkg/notifications/common_templates.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package notifications
|
||||
|
||||
var commonTemplates = map[string]string{
|
||||
`default-legacy`: "{{range .}}{{.Message}}{{println}}{{end}}",
|
||||
|
||||
`default`: `
|
||||
{{- if .Report -}}
|
||||
{{- with .Report -}}
|
||||
{{- if ( or .Updated .Failed ) -}}
|
||||
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
|
||||
{{- range .Updated}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
|
||||
{{- end -}}
|
||||
{{- range .Fresh}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}
|
||||
{{- end -}}
|
||||
{{- range .Skipped}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- range .Failed}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
|
||||
{{- end -}}`,
|
||||
|
||||
`porcelain.v1.summary-no-log`: `
|
||||
{{- if .Report -}}
|
||||
{{- range .Report.All }}
|
||||
{{- .Name}} ({{.ImageName}}): {{.State -}}
|
||||
{{- with .Error}} Error: {{.}}{{end}}{{ println }}
|
||||
{{- else -}}
|
||||
no containers matched filter
|
||||
{{- end -}}
|
||||
{{- end -}}`,
|
||||
|
||||
`json.v1`: `{{ . | ToJSON }}`,
|
||||
}
|
|
@ -15,18 +15,16 @@ const (
|
|||
)
|
||||
|
||||
type emailTypeNotifier struct {
|
||||
url string
|
||||
From, To string
|
||||
Server, User, Password, SubjectTag string
|
||||
Port int
|
||||
tlsSkipVerify bool
|
||||
entries []*log.Entry
|
||||
logLevels []log.Level
|
||||
delay time.Duration
|
||||
From, To string
|
||||
Server, User, Password string
|
||||
Port int
|
||||
tlsSkipVerify bool
|
||||
entries []*log.Entry
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
flags := c.PersistentFlags()
|
||||
func newEmailNotifier(c *cobra.Command) t.ConvertibleNotifier {
|
||||
flags := c.Flags()
|
||||
|
||||
from, _ := flags.GetString("notification-email-from")
|
||||
to, _ := flags.GetString("notification-email-to")
|
||||
|
@ -36,7 +34,6 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert
|
|||
port, _ := flags.GetInt("notification-email-server-port")
|
||||
tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify")
|
||||
delay, _ := flags.GetInt("notification-email-delay")
|
||||
subjecttag, _ := flags.GetString("notification-email-subjecttag")
|
||||
|
||||
n := &emailTypeNotifier{
|
||||
entries: []*log.Entry{},
|
||||
|
@ -47,28 +44,26 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert
|
|||
Password: password,
|
||||
Port: port,
|
||||
tlsSkipVerify: tlsSkipVerify,
|
||||
logLevels: acceptedLogLevels,
|
||||
delay: time.Duration(delay) * time.Second,
|
||||
SubjectTag: subjecttag,
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (e *emailTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) {
|
||||
func (e *emailTypeNotifier) GetURL(c *cobra.Command) (string, error) {
|
||||
conf := &shoutrrrSmtp.Config{
|
||||
FromAddress: e.From,
|
||||
FromName: "Watchtower",
|
||||
ToAddresses: []string{e.To},
|
||||
Port: uint16(e.Port),
|
||||
Host: e.Server,
|
||||
Subject: e.getSubject(c, title),
|
||||
Username: e.User,
|
||||
Password: e.Password,
|
||||
UseStartTLS: !e.tlsSkipVerify,
|
||||
UseHTML: false,
|
||||
Encryption: shoutrrrSmtp.EncMethods.Auto,
|
||||
Auth: shoutrrrSmtp.AuthTypes.None,
|
||||
ClientHost: "localhost",
|
||||
}
|
||||
|
||||
if len(e.User) > 0 {
|
||||
|
@ -85,11 +80,3 @@ func (e *emailTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro
|
|||
func (e *emailTypeNotifier) GetDelay() time.Duration {
|
||||
return e.delay
|
||||
}
|
||||
|
||||
func (e *emailTypeNotifier) getSubject(_ *cobra.Command, title string) string {
|
||||
if e.SubjectTag != "" {
|
||||
return e.SubjectTag + " " + title
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
|
|
@ -19,11 +19,10 @@ type gotifyTypeNotifier struct {
|
|||
gotifyURL string
|
||||
gotifyAppToken string
|
||||
gotifyInsecureSkipVerify bool
|
||||
logLevels []log.Level
|
||||
}
|
||||
|
||||
func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifier {
|
||||
flags := c.PersistentFlags()
|
||||
func newGotifyNotifier(c *cobra.Command) t.ConvertibleNotifier {
|
||||
flags := c.Flags()
|
||||
|
||||
apiURL := getGotifyURL(flags)
|
||||
token := getGotifyToken(flags)
|
||||
|
@ -34,7 +33,6 @@ func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifi
|
|||
gotifyURL: apiURL,
|
||||
gotifyAppToken: token,
|
||||
gotifyInsecureSkipVerify: skipVerify,
|
||||
logLevels: levels,
|
||||
}
|
||||
|
||||
return n
|
||||
|
@ -62,7 +60,7 @@ func getGotifyURL(flags *pflag.FlagSet) string {
|
|||
return gotifyURL
|
||||
}
|
||||
|
||||
func (n *gotifyTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) {
|
||||
func (n *gotifyTypeNotifier) GetURL(c *cobra.Command) (string, error) {
|
||||
apiURL, err := url.Parse(n.gotifyURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -72,7 +70,6 @@ func (n *gotifyTypeNotifier) GetURL(c *cobra.Command, title string) (string, err
|
|||
Host: apiURL.Host,
|
||||
Path: apiURL.Path,
|
||||
DisableTLS: apiURL.Scheme == "http",
|
||||
Title: title,
|
||||
Token: n.gotifyAppToken,
|
||||
}
|
||||
|
||||
|
|
61
pkg/notifications/json.go
Normal file
61
pkg/notifications/json.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
)
|
||||
|
||||
type jsonMap = map[string]interface{}
|
||||
|
||||
// MarshalJSON implements json.Marshaler
|
||||
func (d Data) MarshalJSON() ([]byte, error) {
|
||||
var entries = make([]jsonMap, len(d.Entries))
|
||||
for i, entry := range d.Entries {
|
||||
entries[i] = jsonMap{
|
||||
`level`: entry.Level,
|
||||
`message`: entry.Message,
|
||||
`data`: entry.Data,
|
||||
`time`: entry.Time,
|
||||
}
|
||||
}
|
||||
|
||||
var report jsonMap
|
||||
if d.Report != nil {
|
||||
report = jsonMap{
|
||||
`scanned`: marshalReports(d.Report.Scanned()),
|
||||
`updated`: marshalReports(d.Report.Updated()),
|
||||
`failed`: marshalReports(d.Report.Failed()),
|
||||
`skipped`: marshalReports(d.Report.Skipped()),
|
||||
`stale`: marshalReports(d.Report.Stale()),
|
||||
`fresh`: marshalReports(d.Report.Fresh()),
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(jsonMap{
|
||||
`report`: report,
|
||||
`title`: d.Title,
|
||||
`host`: d.Host,
|
||||
`entries`: entries,
|
||||
})
|
||||
}
|
||||
|
||||
func marshalReports(reports []t.ContainerReport) []jsonMap {
|
||||
jsonReports := make([]jsonMap, len(reports))
|
||||
for i, report := range reports {
|
||||
jsonReports[i] = jsonMap{
|
||||
`id`: report.ID().ShortID(),
|
||||
`name`: report.Name(),
|
||||
`currentImageId`: report.CurrentImageID().ShortID(),
|
||||
`latestImageId`: report.LatestImageID().ShortID(),
|
||||
`imageName`: report.ImageName(),
|
||||
`state`: report.State(),
|
||||
}
|
||||
if errorMessage := report.Error(); errorMessage != "" {
|
||||
jsonReports[i][`error`] = errorMessage
|
||||
}
|
||||
}
|
||||
return jsonReports
|
||||
}
|
||||
|
||||
var _ json.Marshaler = &Data{}
|
118
pkg/notifications/json_test.go
Normal file
118
pkg/notifications/json_test.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
s "github.com/containrrr/watchtower/pkg/session"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("JSON template", func() {
|
||||
When("using report templates", func() {
|
||||
When("JSON template is used", func() {
|
||||
It("should format the messages to the expected format", func() {
|
||||
expected := `{
|
||||
"entries": [
|
||||
{
|
||||
"data": null,
|
||||
"level": "info",
|
||||
"message": "foo Bar",
|
||||
"time": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"host": "Mock",
|
||||
"report": {
|
||||
"failed": [
|
||||
{
|
||||
"currentImageId": "01d210000000",
|
||||
"error": "accidentally the whole container",
|
||||
"id": "c79210000000",
|
||||
"imageName": "mock/fail1:latest",
|
||||
"latestImageId": "d0a210000000",
|
||||
"name": "fail1",
|
||||
"state": "Failed"
|
||||
}
|
||||
],
|
||||
"fresh": [
|
||||
{
|
||||
"currentImageId": "01d310000000",
|
||||
"id": "c79310000000",
|
||||
"imageName": "mock/frsh1:latest",
|
||||
"latestImageId": "01d310000000",
|
||||
"name": "frsh1",
|
||||
"state": "Fresh"
|
||||
}
|
||||
],
|
||||
"scanned": [
|
||||
{
|
||||
"currentImageId": "01d110000000",
|
||||
"id": "c79110000000",
|
||||
"imageName": "mock/updt1:latest",
|
||||
"latestImageId": "d0a110000000",
|
||||
"name": "updt1",
|
||||
"state": "Updated"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d120000000",
|
||||
"id": "c79120000000",
|
||||
"imageName": "mock/updt2:latest",
|
||||
"latestImageId": "d0a120000000",
|
||||
"name": "updt2",
|
||||
"state": "Updated"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d210000000",
|
||||
"error": "accidentally the whole container",
|
||||
"id": "c79210000000",
|
||||
"imageName": "mock/fail1:latest",
|
||||
"latestImageId": "d0a210000000",
|
||||
"name": "fail1",
|
||||
"state": "Failed"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d310000000",
|
||||
"id": "c79310000000",
|
||||
"imageName": "mock/frsh1:latest",
|
||||
"latestImageId": "01d310000000",
|
||||
"name": "frsh1",
|
||||
"state": "Fresh"
|
||||
}
|
||||
],
|
||||
"skipped": [
|
||||
{
|
||||
"currentImageId": "01d410000000",
|
||||
"error": "unpossible",
|
||||
"id": "c79410000000",
|
||||
"imageName": "mock/skip1:latest",
|
||||
"latestImageId": "01d410000000",
|
||||
"name": "skip1",
|
||||
"state": "Skipped"
|
||||
}
|
||||
],
|
||||
"stale": [],
|
||||
"updated": [
|
||||
{
|
||||
"currentImageId": "01d110000000",
|
||||
"id": "c79110000000",
|
||||
"imageName": "mock/updt1:latest",
|
||||
"latestImageId": "d0a110000000",
|
||||
"name": "updt1",
|
||||
"state": "Updated"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d120000000",
|
||||
"id": "c79120000000",
|
||||
"imageName": "mock/updt2:latest",
|
||||
"latestImageId": "d0a120000000",
|
||||
"name": "updt2",
|
||||
"state": "Updated"
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "Watchtower updates on Mock"
|
||||
}`
|
||||
data := mockDataFromStates(s.UpdatedState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState)
|
||||
Expect(getTemplatedResult(`json.v1`, false, data)).To(MatchJSON(expected))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
19
pkg/notifications/model.go
Normal file
19
pkg/notifications/model.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// StaticData is the part of the notification template data model set upon initialization
|
||||
type StaticData struct {
|
||||
Title string
|
||||
Host string
|
||||
}
|
||||
|
||||
// Data is the notification template data model
|
||||
type Data struct {
|
||||
StaticData
|
||||
Entries []*log.Entry
|
||||
Report t.Report
|
||||
}
|
|
@ -15,13 +15,12 @@ const (
|
|||
|
||||
type msTeamsTypeNotifier struct {
|
||||
webHookURL string
|
||||
levels []log.Level
|
||||
data bool
|
||||
}
|
||||
|
||||
func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
func newMsTeamsNotifier(cmd *cobra.Command) t.ConvertibleNotifier {
|
||||
|
||||
flags := cmd.PersistentFlags()
|
||||
flags := cmd.Flags()
|
||||
|
||||
webHookURL, _ := flags.GetString("notification-msteams-hook")
|
||||
if len(webHookURL) <= 0 {
|
||||
|
@ -30,7 +29,6 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con
|
|||
|
||||
withData, _ := flags.GetBool("notification-msteams-data")
|
||||
n := &msTeamsTypeNotifier{
|
||||
levels: acceptedLogLevels,
|
||||
webHookURL: webHookURL,
|
||||
data: withData,
|
||||
}
|
||||
|
@ -38,7 +36,7 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con
|
|||
return n
|
||||
}
|
||||
|
||||
func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) {
|
||||
func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command) (string, error) {
|
||||
webhookURL, err := url.Parse(n.webHookURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -50,7 +48,6 @@ func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command, title string) (string, er
|
|||
}
|
||||
|
||||
config.Color = ColorHex
|
||||
config.Title = title
|
||||
|
||||
return config.GetURL().String(), nil
|
||||
}
|
||||
|
|
|
@ -2,17 +2,17 @@ package notifications
|
|||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ty "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/johntdyer/slackrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewNotifier creates and returns a new Notifier, using global configuration.
|
||||
func NewNotifier(c *cobra.Command) ty.Notifier {
|
||||
f := c.PersistentFlags()
|
||||
f := c.Flags()
|
||||
|
||||
level, _ := f.GetString("notifications-level")
|
||||
logLevel, err := log.ParseLevel(level)
|
||||
|
@ -20,24 +20,19 @@ func NewNotifier(c *cobra.Command) ty.Notifier {
|
|||
log.Fatalf("Notifications invalid log level: %s", err.Error())
|
||||
}
|
||||
|
||||
acceptedLogLevels := slackrus.LevelThreshold(logLevel)
|
||||
// slackrus does not allow log level TRACE, even though it's an accepted log level for logrus
|
||||
if len(acceptedLogLevels) == 0 {
|
||||
log.Fatalf("Unsupported notification log level provided: %s", level)
|
||||
}
|
||||
|
||||
reportTemplate, _ := f.GetBool("notification-report")
|
||||
stdout, _ := f.GetBool("notification-log-stdout")
|
||||
tplString, _ := f.GetString("notification-template")
|
||||
urls, _ := f.GetStringArray("notification-url")
|
||||
|
||||
hostname := GetHostname(c)
|
||||
urls, delay := AppendLegacyUrls(urls, c, GetTitle(hostname))
|
||||
data := GetTemplateData(c)
|
||||
urls, delay := AppendLegacyUrls(urls, c)
|
||||
|
||||
return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, hostname, delay, urls...)
|
||||
return createNotifier(urls, logLevel, tplString, !reportTemplate, data, stdout, delay)
|
||||
}
|
||||
|
||||
// AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags
|
||||
func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string, time.Duration) {
|
||||
func AppendLegacyUrls(urls []string, cmd *cobra.Command) ([]string, time.Duration) {
|
||||
|
||||
// Parse types and create notifiers.
|
||||
types, err := cmd.Flags().GetStringSlice("notifications")
|
||||
|
@ -45,7 +40,7 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string
|
|||
log.WithError(err).Fatal("could not read notifications argument")
|
||||
}
|
||||
|
||||
delay := time.Duration(0)
|
||||
legacyDelay := time.Duration(0)
|
||||
|
||||
for _, t := range types {
|
||||
|
||||
|
@ -54,13 +49,13 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string
|
|||
|
||||
switch t {
|
||||
case emailType:
|
||||
legacyNotifier = newEmailNotifier(cmd, []log.Level{})
|
||||
legacyNotifier = newEmailNotifier(cmd)
|
||||
case slackType:
|
||||
legacyNotifier = newSlackNotifier(cmd, []log.Level{})
|
||||
legacyNotifier = newSlackNotifier(cmd)
|
||||
case msTeamsType:
|
||||
legacyNotifier = newMsTeamsNotifier(cmd, []log.Level{})
|
||||
legacyNotifier = newMsTeamsNotifier(cmd)
|
||||
case gotifyType:
|
||||
legacyNotifier = newGotifyNotifier(cmd, []log.Level{})
|
||||
legacyNotifier = newGotifyNotifier(cmd)
|
||||
case shoutrrrType:
|
||||
continue
|
||||
default:
|
||||
|
@ -69,43 +64,80 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string
|
|||
continue
|
||||
}
|
||||
|
||||
shoutrrrURL, err := legacyNotifier.GetURL(cmd, title)
|
||||
shoutrrrURL, err := legacyNotifier.GetURL(cmd)
|
||||
if err != nil {
|
||||
log.Fatal("failed to create notification config: ", err)
|
||||
}
|
||||
urls = append(urls, shoutrrrURL)
|
||||
|
||||
if delayNotifier, ok := legacyNotifier.(ty.DelayNotifier); ok {
|
||||
delay = delayNotifier.GetDelay()
|
||||
legacyDelay = delayNotifier.GetDelay()
|
||||
}
|
||||
|
||||
log.WithField("URL", shoutrrrURL).Trace("created Shoutrrr URL from legacy notifier")
|
||||
}
|
||||
|
||||
delay := GetDelay(cmd, legacyDelay)
|
||||
return urls, delay
|
||||
}
|
||||
|
||||
// GetTitle returns a common notification title with hostname appended
|
||||
func GetTitle(hostname string) string {
|
||||
title := "Watchtower updates"
|
||||
if hostname != "" {
|
||||
title += " on " + hostname
|
||||
// GetDelay returns the legacy delay if defined, otherwise the delay as set by args is returned
|
||||
func GetDelay(c *cobra.Command, legacyDelay time.Duration) time.Duration {
|
||||
if legacyDelay > 0 {
|
||||
return legacyDelay
|
||||
}
|
||||
return title
|
||||
|
||||
delay, _ := c.PersistentFlags().GetInt("notifications-delay")
|
||||
if delay > 0 {
|
||||
return time.Duration(delay) * time.Second
|
||||
}
|
||||
return time.Duration(0)
|
||||
}
|
||||
|
||||
// GetHostname returns the hostname as set by args or resolved from OS
|
||||
func GetHostname(c *cobra.Command) string {
|
||||
// GetTitle formats the title based on the passed hostname and tag
|
||||
func GetTitle(hostname string, tag string) string {
|
||||
tb := strings.Builder{}
|
||||
|
||||
f := c.PersistentFlags()
|
||||
hostname, _ := f.GetString("notifications-hostname")
|
||||
|
||||
if hostname != "" {
|
||||
return hostname
|
||||
} else if hostname, err := os.Hostname(); err == nil {
|
||||
return hostname
|
||||
if tag != "" {
|
||||
tb.WriteRune('[')
|
||||
tb.WriteString(tag)
|
||||
tb.WriteRune(']')
|
||||
tb.WriteRune(' ')
|
||||
}
|
||||
|
||||
return ""
|
||||
tb.WriteString("Watchtower updates")
|
||||
|
||||
if hostname != "" {
|
||||
tb.WriteString(" on ")
|
||||
tb.WriteString(hostname)
|
||||
}
|
||||
|
||||
return tb.String()
|
||||
}
|
||||
|
||||
// GetTemplateData populates the static notification data from flags and environment
|
||||
func GetTemplateData(c *cobra.Command) StaticData {
|
||||
f := c.PersistentFlags()
|
||||
|
||||
hostname, _ := f.GetString("notifications-hostname")
|
||||
if hostname == "" {
|
||||
hostname, _ = os.Hostname()
|
||||
}
|
||||
|
||||
title := ""
|
||||
if skip, _ := f.GetBool("notification-skip-title"); !skip {
|
||||
tag, _ := f.GetString("notification-title-tag")
|
||||
if tag == "" {
|
||||
// For legacy email support
|
||||
tag, _ = f.GetString("notification-email-subjecttag")
|
||||
}
|
||||
title = GetTitle(hostname, tag)
|
||||
}
|
||||
|
||||
return StaticData{
|
||||
Host: hostname,
|
||||
Title: title,
|
||||
}
|
||||
}
|
||||
|
||||
// ColorHex is the default notification color used for services that support it (formatted as a CSS hex string)
|
||||
|
|
|
@ -3,7 +3,7 @@ package notifications_test
|
|||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/cmd"
|
||||
"github.com/containrrr/watchtower/internal/flags"
|
||||
|
@ -38,17 +38,103 @@ var _ = Describe("notifications", func() {
|
|||
"test.host",
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
hostname := notifications.GetHostname(command)
|
||||
title := notifications.GetTitle(hostname)
|
||||
data := notifications.GetTemplateData(command)
|
||||
title := data.Title
|
||||
Expect(title).To(Equal("Watchtower updates on test.host"))
|
||||
})
|
||||
})
|
||||
When("no hostname can be resolved", func() {
|
||||
It("should use the default simple title", func() {
|
||||
title := notifications.GetTitle("")
|
||||
title := notifications.GetTitle("", "")
|
||||
Expect(title).To(Equal("Watchtower updates"))
|
||||
})
|
||||
})
|
||||
When("title tag is set", func() {
|
||||
It("should use the prefix in the title", func() {
|
||||
command := cmd.NewRootCommand()
|
||||
flags.RegisterNotificationFlags(command)
|
||||
|
||||
Expect(command.ParseFlags([]string{
|
||||
"--notification-title-tag",
|
||||
"PREFIX",
|
||||
})).To(Succeed())
|
||||
|
||||
data := notifications.GetTemplateData(command)
|
||||
Expect(data.Title).To(HavePrefix("[PREFIX]"))
|
||||
})
|
||||
})
|
||||
When("legacy email tag is set", func() {
|
||||
It("should use the prefix in the title", func() {
|
||||
command := cmd.NewRootCommand()
|
||||
flags.RegisterNotificationFlags(command)
|
||||
|
||||
Expect(command.ParseFlags([]string{
|
||||
"--notification-email-subjecttag",
|
||||
"PREFIX",
|
||||
})).To(Succeed())
|
||||
|
||||
data := notifications.GetTemplateData(command)
|
||||
Expect(data.Title).To(HavePrefix("[PREFIX]"))
|
||||
})
|
||||
})
|
||||
When("the skip title flag is set", func() {
|
||||
It("should return an empty title", func() {
|
||||
command := cmd.NewRootCommand()
|
||||
flags.RegisterNotificationFlags(command)
|
||||
|
||||
Expect(command.ParseFlags([]string{
|
||||
"--notification-skip-title",
|
||||
})).To(Succeed())
|
||||
|
||||
data := notifications.GetTemplateData(command)
|
||||
Expect(data.Title).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
When("no delay is defined", func() {
|
||||
It("should use the default delay", func() {
|
||||
command := cmd.NewRootCommand()
|
||||
flags.RegisterNotificationFlags(command)
|
||||
|
||||
delay := notifications.GetDelay(command, time.Duration(0))
|
||||
Expect(delay).To(Equal(time.Duration(0)))
|
||||
})
|
||||
})
|
||||
When("delay is defined", func() {
|
||||
It("should use the specified delay", func() {
|
||||
command := cmd.NewRootCommand()
|
||||
flags.RegisterNotificationFlags(command)
|
||||
|
||||
err := command.ParseFlags([]string{
|
||||
"--notifications-delay",
|
||||
"5",
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
delay := notifications.GetDelay(command, time.Duration(0))
|
||||
Expect(delay).To(Equal(time.Duration(5) * time.Second))
|
||||
})
|
||||
})
|
||||
When("legacy delay is defined", func() {
|
||||
It("should use the specified legacy delay", func() {
|
||||
command := cmd.NewRootCommand()
|
||||
flags.RegisterNotificationFlags(command)
|
||||
delay := notifications.GetDelay(command, time.Duration(5)*time.Second)
|
||||
Expect(delay).To(Equal(time.Duration(5) * time.Second))
|
||||
})
|
||||
})
|
||||
When("legacy delay and delay is defined", func() {
|
||||
It("should use the specified legacy delay and ignore the specified delay", func() {
|
||||
command := cmd.NewRootCommand()
|
||||
flags.RegisterNotificationFlags(command)
|
||||
|
||||
err := command.ParseFlags([]string{
|
||||
"--notifications-delay",
|
||||
"0",
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
delay := notifications.GetDelay(command, time.Duration(7)*time.Second)
|
||||
Expect(delay).To(Equal(time.Duration(7) * time.Second))
|
||||
})
|
||||
})
|
||||
})
|
||||
Describe("the slack notifier", func() {
|
||||
// builderFn := notifications.NewSlackNotifier
|
||||
|
@ -60,9 +146,9 @@ var _ = Describe("notifications", func() {
|
|||
channel := "123456789"
|
||||
token := "abvsihdbau"
|
||||
color := notifications.ColorInt
|
||||
hostname := notifications.GetHostname(command)
|
||||
title := url.QueryEscape(notifications.GetTitle(hostname))
|
||||
expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&title=%s&username=watchtower", token, channel, color, title)
|
||||
username := "containrrrbot"
|
||||
iconURL := "https://containrrr.dev/watchtower-sq180.png"
|
||||
expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&username=watchtower", token, channel, color)
|
||||
buildArgs := func(url string) []string {
|
||||
return []string{
|
||||
"--notifications",
|
||||
|
@ -74,11 +160,32 @@ var _ = Describe("notifications", func() {
|
|||
|
||||
It("should return a discord url when using a hook url with the domain discord.com", func() {
|
||||
hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discord.com", channel, token)
|
||||
testURL(buildArgs(hookURL), expected)
|
||||
testURL(buildArgs(hookURL), expected, time.Duration(0))
|
||||
})
|
||||
It("should return a discord url when using a hook url with the domain discordapp.com", func() {
|
||||
hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discordapp.com", channel, token)
|
||||
testURL(buildArgs(hookURL), expected)
|
||||
testURL(buildArgs(hookURL), expected, time.Duration(0))
|
||||
})
|
||||
When("icon URL and username are specified", func() {
|
||||
It("should return the expected URL", func() {
|
||||
hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discord.com", channel, token)
|
||||
expectedOutput := fmt.Sprintf("discord://%s@%s?avatar=%s&color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&username=%s", token, channel, url.QueryEscape(iconURL), color, username)
|
||||
expectedDelay := time.Duration(7) * time.Second
|
||||
args := []string{
|
||||
"--notifications",
|
||||
"slack",
|
||||
"--notification-slack-hook-url",
|
||||
hookURL,
|
||||
"--notification-slack-identifier",
|
||||
username,
|
||||
"--notification-slack-icon-url",
|
||||
iconURL,
|
||||
"--notifications-delay",
|
||||
fmt.Sprint(expectedDelay.Seconds()),
|
||||
}
|
||||
|
||||
testURL(args, expectedOutput, expectedDelay)
|
||||
})
|
||||
})
|
||||
})
|
||||
When("converting a slack service config into a shoutrrr url", func() {
|
||||
|
@ -89,8 +196,6 @@ var _ = Describe("notifications", func() {
|
|||
tokenB := "BBBBBBBBB"
|
||||
tokenC := "123456789123456789123456"
|
||||
color := url.QueryEscape(notifications.ColorHex)
|
||||
hostname := notifications.GetHostname(command)
|
||||
title := url.QueryEscape(notifications.GetTitle(hostname))
|
||||
iconURL := "https://containrrr.dev/watchtower-sq180.png"
|
||||
iconEmoji := "whale"
|
||||
|
||||
|
@ -98,7 +203,8 @@ var _ = Describe("notifications", func() {
|
|||
It("should return the expected URL", func() {
|
||||
|
||||
hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC)
|
||||
expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s&title=%s", tokenA, tokenB, tokenC, username, color, url.QueryEscape(iconURL), title)
|
||||
expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s", tokenA, tokenB, tokenC, username, color, url.QueryEscape(iconURL))
|
||||
expectedDelay := time.Duration(7) * time.Second
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
|
@ -109,16 +215,18 @@ var _ = Describe("notifications", func() {
|
|||
username,
|
||||
"--notification-slack-icon-url",
|
||||
iconURL,
|
||||
"--notifications-delay",
|
||||
fmt.Sprint(expectedDelay.Seconds()),
|
||||
}
|
||||
|
||||
testURL(args, expectedOutput)
|
||||
testURL(args, expectedOutput, expectedDelay)
|
||||
})
|
||||
})
|
||||
|
||||
When("icon emoji is specified", func() {
|
||||
It("should return the expected URL", func() {
|
||||
hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC)
|
||||
expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s&title=%s", tokenA, tokenB, tokenC, username, color, iconEmoji, title)
|
||||
expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s", tokenA, tokenB, tokenC, username, color, iconEmoji)
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
|
@ -131,7 +239,7 @@ var _ = Describe("notifications", func() {
|
|||
iconEmoji,
|
||||
}
|
||||
|
||||
testURL(args, expectedOutput)
|
||||
testURL(args, expectedOutput, time.Duration(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -145,10 +253,8 @@ var _ = Describe("notifications", func() {
|
|||
|
||||
token := "aaa"
|
||||
host := "shoutrrr.local"
|
||||
hostname := notifications.GetHostname(command)
|
||||
title := url.QueryEscape(notifications.GetTitle(hostname))
|
||||
|
||||
expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title)
|
||||
expectedOutput := fmt.Sprintf("gotify://%s/%s?title=", host, token)
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
|
@ -159,7 +265,7 @@ var _ = Describe("notifications", func() {
|
|||
token,
|
||||
}
|
||||
|
||||
testURL(args, expectedOutput)
|
||||
testURL(args, expectedOutput, time.Duration(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -174,11 +280,9 @@ var _ = Describe("notifications", func() {
|
|||
tokenB := "33333333012222222222333333333344"
|
||||
tokenC := "44444444-4444-4444-8444-cccccccccccc"
|
||||
color := url.QueryEscape(notifications.ColorHex)
|
||||
hostname := notifications.GetHostname(command)
|
||||
title := url.QueryEscape(notifications.GetTitle(hostname))
|
||||
|
||||
hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC)
|
||||
expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s&title=%s", tokenA, tokenB, tokenC, color, title)
|
||||
expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s", tokenA, tokenB, tokenC, color)
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
|
@ -187,7 +291,7 @@ var _ = Describe("notifications", func() {
|
|||
hookURL,
|
||||
}
|
||||
|
||||
testURL(args, expectedOutput)
|
||||
testURL(args, expectedOutput, time.Duration(0))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -197,6 +301,8 @@ var _ = Describe("notifications", func() {
|
|||
It("should set the from address in the URL", func() {
|
||||
fromAddress := "lala@example.com"
|
||||
expectedOutput := buildExpectedURL("containrrrbot", "secret-password", "mail.containrrr.dev", 25, fromAddress, "mail@example.com", "Plain")
|
||||
expectedDelay := time.Duration(7) * time.Second
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
"email",
|
||||
|
@ -210,8 +316,10 @@ var _ = Describe("notifications", func() {
|
|||
"secret-password",
|
||||
"--notification-email-server",
|
||||
"mail.containrrr.dev",
|
||||
"--notifications-delay",
|
||||
fmt.Sprint(expectedDelay.Seconds()),
|
||||
}
|
||||
testURL(args, expectedOutput)
|
||||
testURL(args, expectedOutput, expectedDelay)
|
||||
})
|
||||
|
||||
It("should return the expected URL", func() {
|
||||
|
@ -219,6 +327,7 @@ var _ = Describe("notifications", func() {
|
|||
fromAddress := "sender@example.com"
|
||||
toAddress := "receiver@example.com"
|
||||
expectedOutput := buildExpectedURL("containrrrbot", "secret-password", "mail.containrrr.dev", 25, fromAddress, toAddress, "Plain")
|
||||
expectedDelay := time.Duration(7) * time.Second
|
||||
|
||||
args := []string{
|
||||
"--notifications",
|
||||
|
@ -233,44 +342,36 @@ var _ = Describe("notifications", func() {
|
|||
"secret-password",
|
||||
"--notification-email-server",
|
||||
"mail.containrrr.dev",
|
||||
"--notification-email-delay",
|
||||
fmt.Sprint(expectedDelay.Seconds()),
|
||||
}
|
||||
|
||||
testURL(args, expectedOutput)
|
||||
testURL(args, expectedOutput, expectedDelay)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
func buildExpectedURL(username string, password string, host string, port int, from string, to string, auth string) string {
|
||||
hostname, err := os.Hostname()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
subject := fmt.Sprintf("Watchtower updates on %s", hostname)
|
||||
|
||||
var template = "smtp://%s:%s@%s:%d/?auth=%s&fromaddress=%s&fromname=Watchtower&subject=%s&toaddresses=%s"
|
||||
var template = "smtp://%s:%s@%s:%d/?auth=%s&fromaddress=%s&fromname=Watchtower&subject=&toaddresses=%s"
|
||||
return fmt.Sprintf(template,
|
||||
url.QueryEscape(username),
|
||||
url.QueryEscape(password),
|
||||
host, port, auth,
|
||||
url.QueryEscape(from),
|
||||
url.QueryEscape(subject),
|
||||
url.QueryEscape(to))
|
||||
}
|
||||
|
||||
func testURL(args []string, expectedURL string) {
|
||||
func testURL(args []string, expectedURL string, expectedDelay time.Duration) {
|
||||
defer GinkgoRecover()
|
||||
|
||||
command := cmd.NewRootCommand()
|
||||
flags.RegisterNotificationFlags(command)
|
||||
|
||||
err := command.ParseFlags(args)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(command.ParseFlags(args)).To(Succeed())
|
||||
|
||||
hostname := notifications.GetHostname(command)
|
||||
title := notifications.GetTitle(hostname)
|
||||
urls, _ := notifications.AppendLegacyUrls([]string{}, command, title)
|
||||
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
urls, delay := notifications.AppendLegacyUrls([]string{}, command)
|
||||
|
||||
Expect(urls).To(ContainElement(expectedURL))
|
||||
Expect(delay).To(Equal(expectedDelay))
|
||||
}
|
||||
|
|
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
|
||||
}
|
|
@ -3,12 +3,14 @@ package notifications
|
|||
import (
|
||||
"bytes"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/shoutrrr"
|
||||
"github.com/containrrr/shoutrrr/pkg/types"
|
||||
"github.com/containrrr/watchtower/pkg/notifications/templates"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -17,29 +19,6 @@ import (
|
|||
var LocalLog = log.WithField("notify", "no")
|
||||
|
||||
const (
|
||||
shoutrrrDefaultLegacyTemplate = "{{range .}}{{.Message}}{{println}}{{end}}"
|
||||
shoutrrrDefaultTemplate = `
|
||||
{{- if .Report -}}
|
||||
{{- with .Report -}}
|
||||
{{- if ( or .Updated .Failed ) -}}
|
||||
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
|
||||
{{- range .Updated}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
|
||||
{{- end -}}
|
||||
{{- range .Fresh}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}
|
||||
{{- end -}}
|
||||
{{- range .Skipped}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- range .Failed}}
|
||||
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
|
||||
{{- end -}}`
|
||||
shoutrrrType = "shoutrrr"
|
||||
)
|
||||
|
||||
|
@ -52,13 +31,15 @@ type shoutrrrTypeNotifier struct {
|
|||
Urls []string
|
||||
Router router
|
||||
entries []*log.Entry
|
||||
logLevels []log.Level
|
||||
logLevel log.Level
|
||||
template *template.Template
|
||||
messages chan string
|
||||
done chan bool
|
||||
legacyTemplate bool
|
||||
params *types.Params
|
||||
hostname string
|
||||
data StaticData
|
||||
receiving bool
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
// GetScheme returns the scheme part of a Shoutrrr URL
|
||||
|
@ -79,45 +60,62 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
|
|||
return names
|
||||
}
|
||||
|
||||
func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, hostname string, delay time.Duration, urls ...string) t.Notifier {
|
||||
|
||||
notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy)
|
||||
notifier.hostname = hostname
|
||||
notifier.params = &types.Params{"title": GetTitle(hostname)}
|
||||
log.AddHook(notifier)
|
||||
|
||||
// Do the sending in a separate goroutine so we don't block the main process.
|
||||
go sendNotifications(notifier, delay)
|
||||
|
||||
return notifier
|
||||
// GetURLs returns a list of URLs for notification services that has been added
|
||||
func (n *shoutrrrTypeNotifier) GetURLs() []string {
|
||||
return n.Urls
|
||||
}
|
||||
|
||||
func createNotifier(urls []string, levels []log.Level, tplString string, legacy bool) *shoutrrrTypeNotifier {
|
||||
// AddLogHook adds the notifier as a receiver of log messages and starts a go func for processing them
|
||||
func (n *shoutrrrTypeNotifier) AddLogHook() {
|
||||
if n.receiving {
|
||||
return
|
||||
}
|
||||
n.receiving = true
|
||||
log.AddHook(n)
|
||||
|
||||
// Do the sending in a separate goroutine, so we don't block the main process.
|
||||
go sendNotifications(n)
|
||||
}
|
||||
|
||||
func createNotifier(urls []string, level log.Level, tplString string, legacy bool, data StaticData, stdout bool, delay time.Duration) *shoutrrrTypeNotifier {
|
||||
tpl, err := getShoutrrrTemplate(tplString, legacy)
|
||||
if err != nil {
|
||||
log.Errorf("Could not use configured notification template: %s. Using default template", err)
|
||||
}
|
||||
|
||||
traceWriter := log.StandardLogger().WriterLevel(log.TraceLevel)
|
||||
r, err := shoutrrr.NewSender(stdlog.New(traceWriter, "Shoutrrr: ", 0), urls...)
|
||||
var logger types.StdLogger
|
||||
if stdout {
|
||||
logger = stdlog.New(os.Stdout, ``, 0)
|
||||
} else {
|
||||
logger = stdlog.New(log.StandardLogger().WriterLevel(log.TraceLevel), "Shoutrrr: ", 0)
|
||||
}
|
||||
r, err := shoutrrr.NewSender(logger, urls...)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error())
|
||||
}
|
||||
|
||||
params := &types.Params{}
|
||||
if data.Title != "" {
|
||||
params.SetTitle(data.Title)
|
||||
}
|
||||
|
||||
return &shoutrrrTypeNotifier{
|
||||
Urls: urls,
|
||||
Router: r,
|
||||
messages: make(chan string, 1),
|
||||
done: make(chan bool),
|
||||
logLevels: levels,
|
||||
logLevel: level,
|
||||
template: tpl,
|
||||
legacyTemplate: legacy,
|
||||
data: data,
|
||||
params: params,
|
||||
delay: delay,
|
||||
}
|
||||
}
|
||||
|
||||
func sendNotifications(n *shoutrrrTypeNotifier, delay time.Duration) {
|
||||
func sendNotifications(n *shoutrrrTypeNotifier) {
|
||||
for msg := range n.messages {
|
||||
time.Sleep(delay)
|
||||
time.Sleep(n.delay)
|
||||
errs := n.Router.Send(msg, n.params)
|
||||
|
||||
for i, err := range errs {
|
||||
|
@ -149,9 +147,7 @@ func (n *shoutrrrTypeNotifier) buildMessage(data Data) (string, error) {
|
|||
}
|
||||
|
||||
func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report) {
|
||||
title, _ := n.params.Title()
|
||||
host := n.hostname
|
||||
msg, err := n.buildMessage(Data{entries, report, title, host})
|
||||
msg, err := n.buildMessage(Data{n.data, entries, report})
|
||||
|
||||
if msg == "" {
|
||||
// Log in go func in case we entered from Fire to avoid stalling
|
||||
|
@ -187,12 +183,12 @@ func (n *shoutrrrTypeNotifier) Close() {
|
|||
// Use fmt so it doesn't trigger another notification.
|
||||
LocalLog.Info("Waiting for the notification goroutine to finish")
|
||||
|
||||
_ = <-n.done
|
||||
<-n.done
|
||||
}
|
||||
|
||||
// Levels return what log levels trigger notifications
|
||||
func (n *shoutrrrTypeNotifier) Levels() []log.Level {
|
||||
return n.logLevels
|
||||
return log.AllLevels[:n.logLevel+1]
|
||||
}
|
||||
|
||||
// Fire is the hook that logrus calls on a new log message
|
||||
|
@ -211,12 +207,13 @@ func (n *shoutrrrTypeNotifier) Fire(entry *log.Entry) error {
|
|||
}
|
||||
|
||||
func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, err error) {
|
||||
funcs := template.FuncMap{
|
||||
"ToUpper": strings.ToUpper,
|
||||
"ToLower": strings.ToLower,
|
||||
"Title": strings.Title,
|
||||
|
||||
tplBase := template.New("").Funcs(templates.Funcs)
|
||||
|
||||
if builtin, found := commonTemplates[tplString]; found {
|
||||
log.WithField(`template`, tplString).Debug(`Using common template`)
|
||||
tplString = builtin
|
||||
}
|
||||
tplBase := template.New("").Funcs(funcs)
|
||||
|
||||
// If we succeed in getting a non-empty template configuration
|
||||
// try to parse the template string.
|
||||
|
@ -225,25 +222,17 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
|
|||
}
|
||||
|
||||
// If we had an error (either from parsing the template string
|
||||
// or from getting the template configuration) or we a
|
||||
// or from getting the template configuration) or a
|
||||
// template wasn't configured (the empty template string)
|
||||
// fallback to using the default template.
|
||||
if err != nil || tplString == "" {
|
||||
defaultTemplate := shoutrrrDefaultTemplate
|
||||
defaultKey := `default`
|
||||
if legacy {
|
||||
defaultTemplate = shoutrrrDefaultLegacyTemplate
|
||||
defaultKey = `default-legacy`
|
||||
}
|
||||
|
||||
tpl = template.Must(tplBase.Parse(defaultTemplate))
|
||||
tpl = template.Must(tplBase.Parse(commonTemplates[defaultKey]))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Data is the notification template data model
|
||||
type Data struct {
|
||||
Entries []*log.Entry
|
||||
Report t.Report
|
||||
Title string
|
||||
Host string
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var allButTrace = logrus.AllLevels[0:logrus.TraceLevel]
|
||||
var allButTrace = logrus.DebugLevel
|
||||
|
||||
var legacyMockData = Data{
|
||||
Entries: []*logrus.Entry{
|
||||
|
@ -49,11 +49,14 @@ var mockDataAllFresh = Data{
|
|||
|
||||
func mockDataFromStates(states ...s.State) Data {
|
||||
hostname := "Mock"
|
||||
prefix := ""
|
||||
return Data{
|
||||
Entries: legacyMockData.Entries,
|
||||
Report: mocks.CreateMockProgressReport(states...),
|
||||
Title: GetTitle(hostname),
|
||||
Host: hostname,
|
||||
StaticData: StaticData{
|
||||
Title: GetTitle(hostname, prefix),
|
||||
Host: hostname,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,6 +73,40 @@ var _ = Describe("Shoutrrr", func() {
|
|||
})
|
||||
})
|
||||
|
||||
When("passing a common template name", func() {
|
||||
It("should format using that template", func() {
|
||||
expected := `
|
||||
updt1 (mock/updt1:latest): Updated
|
||||
`[1:]
|
||||
data := mockDataFromStates(s.UpdatedState)
|
||||
Expect(getTemplatedResult(`porcelain.v1.summary-no-log`, false, data)).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
|
||||
When("adding a log hook", func() {
|
||||
When("it has not been added before", func() {
|
||||
It("should be added to the logrus hooks", func() {
|
||||
level := logrus.TraceLevel
|
||||
hooksBefore := len(logrus.StandardLogger().Hooks[level])
|
||||
shoutrrr := createNotifier([]string{}, level, "", true, StaticData{}, false, time.Second)
|
||||
shoutrrr.AddLogHook()
|
||||
hooksAfter := len(logrus.StandardLogger().Hooks[level])
|
||||
Expect(hooksAfter).To(BeNumerically(">", hooksBefore))
|
||||
})
|
||||
})
|
||||
When("it is being added a second time", func() {
|
||||
It("should not be added to the logrus hooks", func() {
|
||||
level := logrus.TraceLevel
|
||||
shoutrrr := createNotifier([]string{}, level, "", true, StaticData{}, false, time.Second)
|
||||
shoutrrr.AddLogHook()
|
||||
hooksBefore := len(logrus.StandardLogger().Hooks[level])
|
||||
shoutrrr.AddLogHook()
|
||||
hooksAfter := len(logrus.StandardLogger().Hooks[level])
|
||||
Expect(hooksAfter).To(Equal(hooksBefore))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
When("using legacy templates", func() {
|
||||
|
||||
When("no custom template is provided", func() {
|
||||
|
@ -77,7 +114,7 @@ var _ = Describe("Shoutrrr", func() {
|
|||
cmd := new(cobra.Command)
|
||||
flags.RegisterNotificationFlags(cmd)
|
||||
|
||||
shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true)
|
||||
shoutrrr := createNotifier([]string{}, logrus.TraceLevel, "", true, StaticData{}, false, time.Second)
|
||||
|
||||
entries := []*logrus.Entry{
|
||||
{
|
||||
|
@ -165,7 +202,6 @@ var _ = Describe("Shoutrrr", func() {
|
|||
})
|
||||
|
||||
When("using report templates", func() {
|
||||
|
||||
When("no custom template is provided", func() {
|
||||
It("should format the messages using the default template", func() {
|
||||
expected := `4 Scanned, 2 Updated, 1 Failed
|
||||
|
@ -233,7 +269,7 @@ Turns out everything is on fire
|
|||
When("batching notifications", func() {
|
||||
When("no messages are queued", func() {
|
||||
It("should not send any notification", func() {
|
||||
shoutrrr := newShoutrrrNotifier("", allButTrace, true, "", time.Duration(0), "logger://")
|
||||
shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{}, false, time.Duration(0))
|
||||
shoutrrr.StartNotification()
|
||||
shoutrrr.SendNotification(nil)
|
||||
Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`))
|
||||
|
@ -241,7 +277,8 @@ Turns out everything is on fire
|
|||
})
|
||||
When("at least one message is queued", func() {
|
||||
It("should send a notification", func() {
|
||||
shoutrrr := newShoutrrrNotifier("", allButTrace, true, "", time.Duration(0), "logger://")
|
||||
shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{}, false, time.Duration(0))
|
||||
shoutrrr.AddLogHook()
|
||||
shoutrrr.StartNotification()
|
||||
logrus.Info("This log message is sponsored by ContainrrrVPN")
|
||||
shoutrrr.SendNotification(nil)
|
||||
|
@ -250,6 +287,17 @@ Turns out everything is on fire
|
|||
})
|
||||
})
|
||||
|
||||
When("the title data field is empty", func() {
|
||||
It("should not have set the title param", func() {
|
||||
shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{
|
||||
Host: "test.host",
|
||||
Title: "",
|
||||
}, false, time.Second)
|
||||
_, found := shoutrrr.params.Title()
|
||||
Expect(found).ToNot(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
When("sending notifications", func() {
|
||||
|
||||
It("SlowNotificationNotSent", func() {
|
||||
|
@ -276,7 +324,7 @@ type blockingRouter struct {
|
|||
}
|
||||
|
||||
func (b blockingRouter) Send(_ string, _ *types.Params) []error {
|
||||
_ = <-b.unlock
|
||||
<-b.unlock
|
||||
b.sent <- true
|
||||
return nil
|
||||
}
|
||||
|
@ -298,13 +346,14 @@ func sendNotificationsWithBlockingRouter(legacy bool) (*shoutrrrTypeNotifier, *b
|
|||
Router: router,
|
||||
legacyTemplate: legacy,
|
||||
params: &types.Params{},
|
||||
delay: time.Duration(0),
|
||||
}
|
||||
|
||||
entry := &logrus.Entry{
|
||||
Message: "foo bar",
|
||||
}
|
||||
|
||||
go sendNotifications(shoutrrr, time.Duration(0))
|
||||
go sendNotifications(shoutrrr)
|
||||
|
||||
shoutrrr.StartNotification()
|
||||
_ = shoutrrr.Fire(entry)
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
shoutrrrDisco "github.com/containrrr/shoutrrr/pkg/services/discord"
|
||||
shoutrrrSlack "github.com/containrrr/shoutrrr/pkg/services/slack"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/johntdyer/slackrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -16,11 +15,15 @@ const (
|
|||
)
|
||||
|
||||
type slackTypeNotifier struct {
|
||||
slackrus.SlackrusHook
|
||||
HookURL string
|
||||
Username string
|
||||
Channel string
|
||||
IconEmoji string
|
||||
IconURL string
|
||||
}
|
||||
|
||||
func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier {
|
||||
flags := c.PersistentFlags()
|
||||
func newSlackNotifier(c *cobra.Command) t.ConvertibleNotifier {
|
||||
flags := c.Flags()
|
||||
|
||||
hookURL, _ := flags.GetString("notification-slack-hook-url")
|
||||
userName, _ := flags.GetString("notification-slack-identifier")
|
||||
|
@ -29,21 +32,18 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert
|
|||
iconURL, _ := flags.GetString("notification-slack-icon-url")
|
||||
|
||||
n := &slackTypeNotifier{
|
||||
SlackrusHook: slackrus.SlackrusHook{
|
||||
HookURL: hookURL,
|
||||
Username: userName,
|
||||
Channel: channel,
|
||||
IconEmoji: emoji,
|
||||
IconURL: iconURL,
|
||||
AcceptedLevels: acceptedLogLevels,
|
||||
},
|
||||
HookURL: hookURL,
|
||||
Username: userName,
|
||||
Channel: channel,
|
||||
IconEmoji: emoji,
|
||||
IconURL: iconURL,
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) {
|
||||
func (s *slackTypeNotifier) GetURL(c *cobra.Command) (string, error) {
|
||||
trimmedURL := strings.TrimRight(s.HookURL, "/")
|
||||
trimmedURL = strings.TrimLeft(trimmedURL, "https://")
|
||||
trimmedURL = strings.TrimPrefix(trimmedURL, "https://")
|
||||
parts := strings.Split(trimmedURL, "/")
|
||||
|
||||
if parts[0] == "discord.com" || parts[0] == "discordapp.com" {
|
||||
|
@ -52,10 +52,14 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro
|
|||
WebhookID: parts[len(parts)-3],
|
||||
Token: parts[len(parts)-2],
|
||||
Color: ColorInt,
|
||||
Title: title,
|
||||
SplitLines: true,
|
||||
Username: s.Username,
|
||||
}
|
||||
|
||||
if s.IconURL != "" {
|
||||
conf.Avatar = s.IconURL
|
||||
}
|
||||
|
||||
return conf.GetURL().String(), nil
|
||||
}
|
||||
|
||||
|
@ -65,7 +69,6 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro
|
|||
BotName: s.Username,
|
||||
Color: ColorHex,
|
||||
Channel: "webhook",
|
||||
Title: title,
|
||||
}
|
||||
|
||||
if s.IconURL != "" {
|
||||
|
|
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,15 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/containrrr/watchtower/pkg/registry/helpers"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
ref "github.com/distribution/reference"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ChallengeHeader is the HTTP Header containing challenge instructions
|
||||
|
@ -19,13 +20,13 @@ const ChallengeHeader = "WWW-Authenticate"
|
|||
|
||||
// GetToken fetches a token for the registry hosting the provided image
|
||||
func GetToken(container types.Container, registryAuth string) (string, error) {
|
||||
var err error
|
||||
var URL url.URL
|
||||
|
||||
if URL, err = GetChallengeURL(container.ImageName()); err != nil {
|
||||
normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
logrus.WithField("URL", URL.String()).Debug("Building challenge URL")
|
||||
|
||||
URL := GetChallengeURL(normalizedRef)
|
||||
logrus.WithField("URL", URL.String()).Debug("Built challenge URL")
|
||||
|
||||
var req *http.Request
|
||||
if req, err = GetChallengeRequest(URL); err != nil {
|
||||
|
@ -54,7 +55,7 @@ func GetToken(container types.Container, registryAuth string) (string, error) {
|
|||
return fmt.Sprintf("Basic %s", registryAuth), nil
|
||||
}
|
||||
if strings.HasPrefix(challenge, "bearer") {
|
||||
return GetBearerHeader(challenge, container.ImageName(), err, registryAuth)
|
||||
return GetBearerHeader(challenge, normalizedRef, registryAuth)
|
||||
}
|
||||
|
||||
return "", errors.New("unsupported challenge type from registry")
|
||||
|
@ -72,12 +73,9 @@ func GetChallengeRequest(URL url.URL) (*http.Request, error) {
|
|||
}
|
||||
|
||||
// GetBearerHeader tries to fetch a bearer token from the registry based on the challenge instructions
|
||||
func GetBearerHeader(challenge string, img string, err error, registryAuth string) (string, error) {
|
||||
func GetBearerHeader(challenge string, imageRef ref.Named, registryAuth string) (string, error) {
|
||||
client := http.Client{}
|
||||
if strings.Contains(img, ":") {
|
||||
img = strings.Split(img, ":")[0]
|
||||
}
|
||||
authURL, err := GetAuthURL(challenge, img)
|
||||
authURL, err := GetAuthURL(challenge, imageRef)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -90,7 +88,8 @@ func GetBearerHeader(challenge string, img string, err error, registryAuth strin
|
|||
|
||||
if registryAuth != "" {
|
||||
logrus.Debug("Credentials found.")
|
||||
logrus.Tracef("Credentials: %v", registryAuth)
|
||||
// CREDENTIAL: Uncomment to log registry credentials
|
||||
// logrus.Tracef("Credentials: %v", registryAuth)
|
||||
r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth))
|
||||
} else {
|
||||
logrus.Debug("No credentials found.")
|
||||
|
@ -101,7 +100,7 @@ func GetBearerHeader(challenge string, img string, err error, registryAuth strin
|
|||
return "", err
|
||||
}
|
||||
|
||||
body, _ := ioutil.ReadAll(authResponse.Body)
|
||||
body, _ := io.ReadAll(authResponse.Body)
|
||||
tokenResponse := &types.TokenResponse{}
|
||||
|
||||
err = json.Unmarshal(body, tokenResponse)
|
||||
|
@ -113,7 +112,7 @@ func GetBearerHeader(challenge string, img string, err error, registryAuth strin
|
|||
}
|
||||
|
||||
// GetAuthURL from the instructions in the challenge
|
||||
func GetAuthURL(challenge string, img string) (*url.URL, error) {
|
||||
func GetAuthURL(challenge string, imageRef ref.Named) (*url.URL, error) {
|
||||
loweredChallenge := strings.ToLower(challenge)
|
||||
raw := strings.TrimPrefix(loweredChallenge, "bearer")
|
||||
|
||||
|
@ -122,10 +121,9 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) {
|
|||
|
||||
for _, pair := range pairs {
|
||||
trimmed := strings.Trim(pair, " ")
|
||||
kv := strings.Split(trimmed, "=")
|
||||
key := kv[0]
|
||||
val := strings.Trim(kv[1], "\"")
|
||||
values[key] = val
|
||||
if key, val, ok := strings.Cut(trimmed, "="); ok {
|
||||
values[key] = strings.Trim(val, `"`)
|
||||
}
|
||||
}
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"realm": values["realm"],
|
||||
|
@ -136,57 +134,29 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) {
|
|||
return nil, fmt.Errorf("challenge header did not include all values needed to construct an auth url")
|
||||
}
|
||||
|
||||
authURL, _ := url.Parse(fmt.Sprintf("%s", values["realm"]))
|
||||
authURL, _ := url.Parse(values["realm"])
|
||||
q := authURL.Query()
|
||||
q.Add("service", values["service"])
|
||||
|
||||
scopeImage := GetScopeFromImageName(img, values["service"])
|
||||
scopeImage := ref.Path(imageRef)
|
||||
|
||||
scope := fmt.Sprintf("repository:%s:pull", scopeImage)
|
||||
logrus.WithFields(logrus.Fields{"scope": scope, "image": img}).Debug("Setting scope for auth token")
|
||||
logrus.WithFields(logrus.Fields{"scope": scope, "image": imageRef.Name()}).Debug("Setting scope for auth token")
|
||||
q.Add("scope", scope)
|
||||
|
||||
authURL.RawQuery = q.Encode()
|
||||
return authURL, nil
|
||||
}
|
||||
|
||||
// GetScopeFromImageName normalizes an image name for use as scope during auth and head requests
|
||||
func GetScopeFromImageName(img, svc string) string {
|
||||
parts := strings.Split(img, "/")
|
||||
|
||||
if len(parts) > 2 {
|
||||
if strings.Contains(svc, "docker.io") {
|
||||
return fmt.Sprintf("%s/%s", parts[1], strings.Join(parts[2:], "/"))
|
||||
}
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
if len(parts) == 2 {
|
||||
if strings.Contains(parts[0], "docker.io") {
|
||||
return fmt.Sprintf("library/%s", parts[1])
|
||||
}
|
||||
return strings.Replace(img, svc+"/", "", 1)
|
||||
}
|
||||
|
||||
if strings.Contains(svc, "docker.io") {
|
||||
return fmt.Sprintf("library/%s", parts[0])
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// GetChallengeURL creates a URL object based on the image info
|
||||
func GetChallengeURL(img string) (url.URL, error) {
|
||||
|
||||
normalizedNamed, _ := reference.ParseNormalizedNamed(img)
|
||||
host, err := helpers.NormalizeRegistry(normalizedNamed.String())
|
||||
if err != nil {
|
||||
return url.URL{}, err
|
||||
}
|
||||
// GetChallengeURL returns the URL to check auth requirements
|
||||
// for access to a given image
|
||||
func GetChallengeURL(imageRef ref.Named) url.URL {
|
||||
host, _ := helpers.GetRegistryAddress(imageRef.Name())
|
||||
|
||||
URL := url.URL{
|
||||
Scheme: "https",
|
||||
Host: host,
|
||||
Path: "/v2/",
|
||||
}
|
||||
return URL, nil
|
||||
return URL
|
||||
}
|
||||
|
|
|
@ -2,14 +2,17 @@ package auth_test
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/containrrr/watchtower/internal/actions/mocks"
|
||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/actions/mocks"
|
||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
||||
|
||||
wtTypes "github.com/containrrr/watchtower/pkg/types"
|
||||
ref "github.com/distribution/reference"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
@ -51,7 +54,7 @@ var _ = Describe("the auth module", func() {
|
|||
mockCreated,
|
||||
mockDigest)
|
||||
|
||||
When("getting an auth url", func() {
|
||||
Describe("GetToken", func() {
|
||||
It("should parse the token from the response",
|
||||
SkipIfCredentialsEmpty(GHCRCredentials, func() {
|
||||
creds := fmt.Sprintf("%s:%s", GHCRCredentials.Username, GHCRCredentials.Password)
|
||||
|
@ -60,61 +63,100 @@ var _ = Describe("the auth module", func() {
|
|||
Expect(token).NotTo(Equal(""))
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
Describe("GetAuthURL", func() {
|
||||
It("should create a valid auth url object based on the challenge header supplied", func() {
|
||||
input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"`
|
||||
challenge := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"`
|
||||
imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
expected := &url.URL{
|
||||
Host: "ghcr.io",
|
||||
Scheme: "https",
|
||||
Path: "/token",
|
||||
RawQuery: "scope=repository%3Acontainrrr%2Fwatchtower%3Apull&service=ghcr.io",
|
||||
}
|
||||
res, err := auth.GetAuthURL(input, "containrrr/watchtower")
|
||||
|
||||
URL, err := auth.GetAuthURL(challenge, imageRef)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal(expected))
|
||||
Expect(URL).To(Equal(expected))
|
||||
})
|
||||
It("should create a valid auth url object based on the challenge header supplied", func() {
|
||||
input := `bearer realm="https://ghcr.io/token"`
|
||||
res, err := auth.GetAuthURL(input, "containrrr/watchtower")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(res).To(BeNil())
|
||||
|
||||
When("given an invalid challenge header", func() {
|
||||
It("should return an error", func() {
|
||||
challenge := `bearer realm="https://ghcr.io/token"`
|
||||
imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
URL, err := auth.GetAuthURL(challenge, imageRef)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(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() {
|
||||
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/"}
|
||||
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/"}
|
||||
Expect(auth.GetChallengeURL("docker.io/containrrr/watchtower:latest")).To(Equal(expected))
|
||||
Expect(auth.GetChallengeURL("registry-1.docker.io/containrrr/watchtower:latest")).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
When("getting the auth scope from an image name", func() {
|
||||
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"))
|
||||
imageRef, _ := ref.ParseNormalizedNamed("docker.io/containrrr/watchtower:latest")
|
||||
Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var scopeImageRegexp = MatchRegexp("^repository:[a-z0-9]+(/[a-z0-9]+)*:pull$")
|
||||
|
||||
func getScopeFromImageAuthURL(imageName string) string {
|
||||
normalizedRef, _ := ref.ParseNormalizedNamed(imageName)
|
||||
challenge := `bearer realm="https://dummy.host/token",service="dummy.host",scope="repository:user/image:pull"`
|
||||
URL, _ := auth.GetAuthURL(challenge, normalizedRef)
|
||||
|
||||
scope := URL.Query().Get("scope")
|
||||
Expect(scopeImageRegexp.Match(scope)).To(BeTrue())
|
||||
return strings.Replace(scope[11:], ":pull", "", 1)
|
||||
}
|
||||
|
|
|
@ -6,15 +6,16 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/meta"
|
||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
||||
"github.com/containrrr/watchtower/pkg/registry/manifest"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ContentDigestHeader is the key for the key-value pair containing the digest header
|
||||
|
@ -93,16 +94,18 @@ func GetDigest(url string, token string) (string, error) {
|
|||
req, _ := http.NewRequest("HEAD", url, nil)
|
||||
req.Header.Set("User-Agent", meta.UserAgent)
|
||||
|
||||
if token != "" {
|
||||
logrus.WithField("token", token).Trace("Setting request token")
|
||||
} else {
|
||||
if token == "" {
|
||||
return "", errors.New("could not fetch token")
|
||||
}
|
||||
|
||||
// CREDENTIAL: Uncomment to log the request token
|
||||
// logrus.WithField("token", token).Trace("Setting request token")
|
||||
|
||||
req.Header.Add("Authorization", token)
|
||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v2+json")
|
||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v1+json")
|
||||
req.Header.Add("Accept", "application/vnd.oci.image.index.v1+json")
|
||||
|
||||
logrus.WithField("url", url).Debug("Doing a HEAD request to fetch a digest")
|
||||
|
||||
|
|
|
@ -1,36 +1,28 @@
|
|||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
url2 "net/url"
|
||||
"github.com/distribution/reference"
|
||||
)
|
||||
|
||||
// ConvertToHostname strips a url from everything but the hostname part
|
||||
func ConvertToHostname(url string) (string, string, error) {
|
||||
urlWithSchema := fmt.Sprintf("x://%s", url)
|
||||
u, err := url2.Parse(urlWithSchema)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
hostName := u.Hostname()
|
||||
port := u.Port()
|
||||
// domains for Docker Hub, the default registry
|
||||
const (
|
||||
DefaultRegistryDomain = "docker.io"
|
||||
DefaultRegistryHost = "index.docker.io"
|
||||
LegacyDefaultRegistryDomain = "index.docker.io"
|
||||
)
|
||||
|
||||
return hostName, port, err
|
||||
}
|
||||
|
||||
// NormalizeRegistry makes sure variations of DockerHubs registry
|
||||
func NormalizeRegistry(registry string) (string, error) {
|
||||
hostName, port, err := ConvertToHostname(registry)
|
||||
// GetRegistryAddress parses an image name
|
||||
// and returns the address of the specified registry
|
||||
func GetRegistryAddress(imageRef string) (string, error) {
|
||||
normalizedRef, err := reference.ParseNormalizedNamed(imageRef)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if hostName == "registry-1.docker.io" || hostName == "docker.io" {
|
||||
hostName = "index.docker.io"
|
||||
}
|
||||
address := reference.Domain(normalizedRef)
|
||||
|
||||
if port != "" {
|
||||
return fmt.Sprintf("%s:%s", hostName, port), nil
|
||||
if address == DefaultRegistryDomain {
|
||||
address = DefaultRegistryHost
|
||||
}
|
||||
return hostName, nil
|
||||
return address, nil
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue