diff --git a/.all-contributorsrc b/.all-contributorsrc
index d50406c..270f462 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -5,6 +5,30 @@
"imageSize": 100,
"commit": false,
"contributors": [
+ {
+ "login": "piksel",
+ "name": "nils måsén",
+ "avatar_url": "https://avatars2.githubusercontent.com/u/807383?v=4",
+ "profile": "https://piksel.se",
+ "contributions": [
+ "code",
+ "doc",
+ "maintenance",
+ "review"
+ ]
+ },
+ {
+ "login": "simskij",
+ "name": "Simon Aronsson",
+ "avatar_url": "https://avatars0.githubusercontent.com/u/1596025?v=4",
+ "profile": "http://simme.dev",
+ "contributions": [
+ "code",
+ "doc",
+ "maintenance",
+ "review"
+ ]
+ },
{
"login": "Codelica",
"name": "James",
@@ -273,18 +297,6 @@
"code"
]
},
- {
- "login": "simskij",
- "name": "Simon Aronsson",
- "avatar_url": "https://avatars0.githubusercontent.com/u/1596025?v=4",
- "profile": "http://simme.dev",
- "contributions": [
- "code",
- "maintenance",
- "review",
- "doc"
- ]
- },
{
"login": "Ansem93",
"name": "Ansem93",
@@ -508,16 +520,6 @@
"doc"
]
},
- {
- "login": "piksel",
- "name": "nils måsén",
- "avatar_url": "https://avatars2.githubusercontent.com/u/807383?v=4",
- "profile": "https://piksel.se",
- "contributions": [
- "doc",
- "code"
- ]
- },
{
"login": "arnested",
"name": "Arne Jørgensen",
@@ -841,6 +843,12 @@
"code"
]
},
+ {
+ "login": "andriibratanin",
+ "name": "Andrii Bratanin",
+ "avatar_url": "https://avatars.githubusercontent.com/u/20169213?v=4",
+ "profile": "https://github.com/andriibratanin"
+ },
{
"login": "IAmTamal",
"name": "Tamal Das ",
@@ -849,6 +857,25 @@
"contributions": [
"doc"
]
+ },
+ {
+ "login": "testwill",
+ "name": "guangwu",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8717479?v=4",
+ "profile": "https://github.com/testwill",
+ "contributions": [
+ "doc"
+ ]
+ },
+ {
+ "login": "nothub",
+ "name": "Florian Hübner",
+ "avatar_url": "https://avatars.githubusercontent.com/u/48992448?v=4",
+ "profile": "http://hub.lol",
+ "contributions": [
+ "doc",
+ "code"
+ ]
}
],
"contributorsPerLine": 7,
@@ -857,5 +884,6 @@
"repoType": "github",
"repoHost": "https://github.com",
"commitConvention": "none",
- "skipCi": true
+ "skipCi": true,
+ "commitType": "docs"
}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 98f8387..c479d05 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -44,7 +44,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
+ uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -55,7 +55,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ uses: github/codeql-action/autobuild@v3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -69,4 +69,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
+ uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/dependabot-approve.yml b/.github/workflows/dependabot-approve.yml
new file mode 100644
index 0000000..46f9d18
--- /dev/null
+++ b/.github/workflows/dependabot-approve.yml
@@ -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
diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml
index e7ac0c7..4818e54 100644
--- a/.github/workflows/publish-docs.yml
+++ b/.github/workflows/publish-docs.yml
@@ -14,11 +14,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: 1.20.x
+ - name: Build tplprev
+ run: scripts/build-tplprev.sh
- name: Setup python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index a99bef1..f6866af 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -12,16 +12,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
- uses: actions/setup-go@v3
+ uses: actions/setup-go@v4
with:
- go-version: 1.18.x
- - uses: dominikh/staticcheck-action@a3513ade2e5cb8075ba1c1ed1890a989cf0f2aa0 #v1.2.0
+ go-version: 1.20.x
+ - uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
with:
- version: "2022.1.1"
+ version: "2023.1.6"
install-go: "false" # StaticCheck uses go v1.17 which does not support `any`
test:
name: Test
@@ -29,7 +29,7 @@ jobs:
fail-fast: false
matrix:
go-version:
- - 1.18.x
+ - 1.20.x
platform:
- macos-latest
- windows-latest
@@ -37,13 +37,13 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
- uses: actions/setup-go@v3
+ uses: actions/setup-go@v4
with:
- go-version: 1.18.x
+ go-version: 1.20.x
- name: Run tests
run: |
go test -v -coverprofile coverage.out -covermode atomic ./...
@@ -56,15 +56,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
- uses: actions/setup-go@v3
+ uses: actions/setup-go@v4
with:
- go-version: 1.18.x
+ go-version: 1.20.x
- name: Build
- uses: goreleaser/goreleaser-action@b508e2e3ef3b19d4e4146d4f8fb3ba9db644a757 #v3
+ uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3
with:
version: v0.155.0
args: --snapshot --skip-publish --debug
diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml
index 1aa1373..95ee68d 100644
--- a/.github/workflows/release-dev.yaml
+++ b/.github/workflows/release-dev.yaml
@@ -10,21 +10,23 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
- - name: Set up Go
- uses: actions/setup-go@v3
+ - uses: actions/checkout@v4
with:
- go-version: 1.18
+ fetch-depth: 0
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: 1.20.x
- name: Build
run: ./build.sh
test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Go
- uses: actions/setup-go@v3
+ uses: actions/setup-go@v4
with:
- go-version: 1.18
+ go-version: 1.20.x
- name: Test
run: go test -v -coverprofile coverage.out -covermode atomic ./...
- name: Publish coverage
@@ -37,7 +39,7 @@ jobs:
- test
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Publish to Docker Hub
uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index bf0d61d..370d395 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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,14 +13,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
- uses: actions/setup-go@v3
+ uses: actions/setup-go@v4
with:
- go-version: 1.18.x
- - uses: dominikh/staticcheck-action@a3513ade2e5cb8075ba1c1ed1890a989cf0f2aa0 #v1.2.0
+ 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`
@@ -32,7 +30,7 @@ jobs:
strategy:
matrix:
go-version:
- - 1.18.x
+ - 1.20.x
platform:
- ubuntu-latest
- macos-latest
@@ -40,13 +38,13 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
- uses: actions/setup-go@v3
+ uses: actions/setup-go@v4
with:
- go-version: 1.18.x
+ go-version: 1.20.x
- name: Run tests
run: |
go test ./... -coverprofile coverage.out
@@ -59,29 +57,29 @@ jobs:
- lint
env:
CGO_ENABLED: 0
- TAG: ${{ github.event.release.tag_name }}
+ TAG: ${{ github.ref_name }}
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
- uses: actions/setup-go@v3
+ uses: actions/setup-go@v4
with:
- go-version: 1.18.x
+ go-version: 1.20.x
- name: Login to Docker Hub
- uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2
+ uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
- uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2
+ uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2
with:
username: ${{ secrets.BOT_USERNAME }}
password: ${{ secrets.BOT_GHCR_PAT }}
registry: ghcr.io
- name: Build
- uses: goreleaser/goreleaser-action@b508e2e3ef3b19d4e4146d4f8fb3ba9db644a757 #v3
+ uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3
with:
version: v0.155.0
args: --debug
@@ -93,7 +91,7 @@ jobs:
echo '{"experimental": "enabled"}' > ~/.docker/config.json
- name: Create manifest for version
run: |
- export DH_TAG=$(echo $TAG | sed 's/^v*//')
+ export DH_TAG=$(git tag --points-at HEAD | sed 's/^v*//')
docker manifest create \
containrrr/watchtower:$DH_TAG \
containrrr/watchtower:amd64-$DH_TAG \
@@ -191,7 +189,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Pull new module version
- uses: andrewslotin/go-proxy-pull-action@bfc19ec6536e1638181b2ad6a03e16c7ccfb122f #master@2022-10-14
+ uses: andrewslotin/go-proxy-pull-action@50fea06a976087614babb9508e5c528b464f4645 #master@2022-10-14
diff --git a/.gitignore b/.gitignore
index c371f41..9519257 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,7 @@ dist
.DS_Store
/site
coverage.out
-*.coverprofile
\ No newline at end of file
+*.coverprofile
+
+docs/assets/wasm_exec.js
+docs/assets/*.wasm
\ No newline at end of file
diff --git a/README.md b/README.md
index cc8c0bb..f550302 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,8 @@ $ docker run --detach \
containrrr/watchtower
```
+Watchtower is intended to be used in homelabs, media centers, local dev environments, and similar. We do **not** recommend using Watchtower in a commercial or production environment. If that is you, you should be looking into using Kubernetes. If that feels like too big a step for you, please look into solutions like [MicroK8s](https://microk8s.io/) and [k3s](https://k3s.io/) that take away a lot of the toil of running a Kubernetes cluster.
+
## Documentation
The full documentation is available at https://containrrr.dev/watchtower.
@@ -44,126 +46,128 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
diff --git a/cmd/root.go b/cmd/root.go
index 8e20b95..eef13ce 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,6 +1,7 @@
package cmd
import (
+ "errors"
"math"
"net/http"
"os"
@@ -28,17 +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
+ 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()
@@ -77,23 +81,8 @@ func Execute() {
func PreRun(cmd *cobra.Command, _ []string) {
f := cmd.PersistentFlags()
flags.ProcessFlagAliases(f)
-
- if enabled, _ := f.GetBool("no-color"); enabled {
- log.SetFormatter(&log.TextFormatter{
- DisableColors: true,
- })
- } else {
- // enable logrus built-in support for https://bixense.com/clicolors/
- log.SetFormatter(&log.TextFormatter{
- EnvironmentOverrideColors: true,
- })
- }
-
- rawLogLevel, _ := f.GetString(`log-level`)
- if logLevel, err := log.ParseLevel(rawLogLevel); err != nil {
- log.Fatalf("Invalid log level: %s", err.Error())
- } else {
- log.SetLevel(logLevel)
+ if err := flags.SetupLogging(f); err != nil {
+ log.Fatalf("Failed to initialize logging: %s", err.Error())
}
scheduleSpec, _ = f.GetString("schedule")
@@ -106,9 +95,11 @@ func PreRun(cmd *cobra.Command, _ []string) {
}
enableLabel, _ = f.GetBool("label-enable")
+ disableContainers, _ = f.GetStringSlice("disable-containers")
lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
rollingRestart, _ = f.GetBool("rolling-restart")
scope, _ = f.GetString("scope")
+ labelPrecedence, _ = f.GetBool("label-take-precedence")
if scope != "" {
log.Debugf(`Using scope %q`, scope)
@@ -120,7 +111,7 @@ func PreRun(cmd *cobra.Command, _ []string) {
log.Fatal(err)
}
- noPull, _ := f.GetBool("no-pull")
+ noPull, _ = f.GetBool("no-pull")
includeStopped, _ := f.GetBool("include-stopped")
includeRestarting, _ := f.GetBool("include-restarting")
reviveStopped, _ := f.GetBool("revive-stopped")
@@ -132,7 +123,6 @@ func PreRun(cmd *cobra.Command, _ []string) {
}
client = container.NewClient(container.ClientOptions{
- PullImages: !noPull,
IncludeStopped: includeStopped,
ReviveStopped: reviveStopped,
RemoveVolumes: removeVolumes,
@@ -146,12 +136,22 @@ func PreRun(cmd *cobra.Command, _ []string) {
// Run is the main execution flow of the command
func Run(c *cobra.Command, names []string) {
- filter, filterDesc := filters.BuildFilter(names, enableLabel, scope)
+ filter, filterDesc := filters.BuildFilter(names, disableContainers, enableLabel, scope)
runOnce, _ := c.PersistentFlags().GetBool("run-once")
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")
unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls")
apiToken, _ := c.PersistentFlags().GetString("http-api-token")
+ healthCheck, _ := c.PersistentFlags().GetBool("health-check")
+
+ if healthCheck {
+ // health check should not have pid 1
+ if os.Getpid() == 1 {
+ time.Sleep(1 * time.Second)
+ log.Fatal("The health check flag should never be passed to the main watchtower container process")
+ }
+ os.Exit(0)
+ }
if rollingRestart && monitorOnly {
log.Fatal("Rolling restarts is not compatible with the global monitor only flag")
@@ -182,9 +182,12 @@ func Run(c *cobra.Command, names []string) {
httpAPI := api.New(apiToken)
if enableUpdateAPI {
- updateHandler := update.New(func(images []string) { runUpdatesWithNotifications(filters.FilterByImage(images, filter)) }, updateLock)
+ updateHandler := update.New(func(images []string) {
+ metric := runUpdatesWithNotifications(filters.FilterByImage(images, filter))
+ metrics.RegisterScan(metric)
+ }, updateLock)
httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle)
- // If polling isn't enabled the scheduler is never started and
+ // 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)
@@ -196,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)
}
@@ -356,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 {
diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile
index 2d5a181..2fc571d 100644
--- a/dockerfiles/Dockerfile
+++ b/dockerfiles/Dockerfile
@@ -1,4 +1,4 @@
-FROM --platform=$BUILDPLATFORM alpine:3.16.2 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"]
diff --git a/dockerfiles/Dockerfile.dev-self-contained b/dockerfiles/Dockerfile.dev-self-contained
index 79dbe39..1a39c26 100644
--- a/dockerfiles/Dockerfile.dev-self-contained
+++ b/dockerfiles/Dockerfile.dev-self-contained
@@ -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"]
diff --git a/dockerfiles/Dockerfile.self-contained b/dockerfiles/Dockerfile.self-contained
index 303fc53..04a6047 100644
--- a/dockerfiles/Dockerfile.self-contained
+++ b/dockerfiles/Dockerfile.self-contained
@@ -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"]
diff --git a/dockerfiles/container-networking/docker-compose.yml b/dockerfiles/container-networking/docker-compose.yml
new file mode 100644
index 0000000..24cd00d
--- /dev/null
+++ b/dockerfiles/container-networking/docker-compose.yml
@@ -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"
diff --git a/docs/arguments.md b/docs/arguments.md
index 58e7e7b..d7ed0b0 100644
--- a/docs/arguments.md
+++ b/docs/arguments.md
@@ -27,6 +27,33 @@ In the example above, watchtower will execute an upgrade attempt on the containe
When no arguments are specified, watchtower will monitor all running containers.
+## Secrets/Files
+
+Some arguments can also reference a file, in which case the contents of the file are used as the value.
+This can be used to avoid putting secrets in the configuration file or command line.
+
+The following arguments are currently supported (including their corresponding `WATCHTOWER_` environment variables):
+ - `notification-url`
+ - `notification-email-server-password`
+ - `notification-slack-hook-url`
+ - `notification-msteams-hook`
+ - `notification-gotify-token`
+ - `http-api-token`
+
+### Example docker-compose usage
+```yaml
+secrets:
+ access_token:
+ file: access_token
+
+services:
+ watchtower:
+ secrets:
+ - access_token
+ environment:
+ - WATCHTOWER_HTTP_API_TOKEN=/run/secrets/access_token
+```
+
## Help
Shows documentation about the supported flags.
@@ -58,8 +85,8 @@ Environment Variable: WATCHTOWER_CLEANUP
Default: false
```
-## Remove attached volumes
-Removes attached volumes after updating. When this flag is specified, watchtower will remove all attached volumes from the container before restarting with a new image. Use this option to force new volumes to be populated as containers are updated.
+## Remove anonymous volumes
+Removes anonymous volumes after updating. When this flag is specified, watchtower will remove all anonymous volumes from the container before restarting with a new image. Named volumes will not be removed!
```text
Argument: --remove-volumes
@@ -107,6 +134,17 @@ Environment Variable: WATCHTOWER_LOG_LEVEL
Default: info
```
+## Logging format
+
+Sets what logging format to use for console output.
+
+```text
+ Argument: --log-format, -l
+Environment Variable: WATCHTOWER_LOG_FORMAT
+ Possible values: Auto, LogFmt, Pretty or JSON
+ Default: Auto
+```
+
## ANSI colors
Disable ANSI color escape codes in log output.
@@ -151,7 +189,7 @@ Environment Variable: WATCHTOWER_INCLUDE_RESTARTING
Will also include created and exited containers.
```text
- Argument: --include-stopped
+ Argument: --include-stopped, -S
Environment Variable: WATCHTOWER_INCLUDE_STOPPED
Type: Boolean
Default: false
@@ -178,7 +216,7 @@ Environment Variable: WATCHTOWER_POLL_INTERVAL
```
## Filter by enable label
-Update containers that have a `com.centurylinklabs.watchtower.enable` label set to true.
+Monitor and update containers that have a `com.centurylinklabs.watchtower.enable` label set to true.
```text
Argument: --label-enable
@@ -188,10 +226,23 @@ Environment Variable: WATCHTOWER_LABEL_ENABLE
```
## Filter by disable label
-__Do not__ update containers that have `com.centurylinklabs.watchtower.enable` label set to false and
+__Do not__ Monitor and update containers that have `com.centurylinklabs.watchtower.enable` label set to false and
no `--label-enable` argument is passed. Note that only one or the other (targeting by enable label) can be
used at the same time to target containers.
+## Filter by disabling specific container names
+Monitor and update containers whose names are not in a given set of names.
+
+This can be used to exclude specific containers, when setting labels is not an option.
+The listed containers will be excluded even if they have the enable filter set to true.
+
+```text
+ Argument: --disable-containers, -x
+Environment Variable: WATCHTOWER_DISABLE_CONTAINERS
+ Type: Comma- or space-separated string list
+ Default: ""
+```
+
## Without updating containers
Will only monitor for new images, send notifications and invoke
the [pre-check/post-check hooks](https://containrrr.dev/watchtower/lifecycle-hooks/), but will __not__ update the
@@ -211,6 +262,19 @@ Environment Variable: WATCHTOWER_MONITOR_ONLY
Note that monitor-only can also be specified on a per-container basis with the `com.centurylinklabs.watchtower.monitor-only` label set on those containers.
+See [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set
+
+## With label taking precedence over arguments
+
+By default, arguments will take precedence over labels. This means that if you set `WATCHTOWER_MONITOR_ONLY` to true or use `--monitor-only`, a container with `com.centurylinklabs.watchtower.monitor-only` set to false will not be updated. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will also be updated. This also apply to the no pull option. if you set `WATCHTOWER_NO_PULL` to true or use `--no-pull`, a container with `com.centurylinklabs.watchtower.no-pull` set to false will not pull the new image. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will pull image
+
+```text
+ Argument: --label-take-precedence
+Environment Variable: WATCHTOWER_LABEL_TAKE_PRECEDENCE
+ Type: Boolean
+ Default: false
+```
+
## Without restarting containers
Do not restart containers after updating. This option can be useful when the start of the containers
is managed by an external system such as systemd.
@@ -234,6 +298,11 @@ Environment Variable: WATCHTOWER_NO_PULL
Default: false
```
+Note that no-pull can also be specified on a per-container basis with the
+`com.centurylinklabs.watchtower.no-pull` label set on those containers.
+
+See [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set
+
## Without sending a startup message
Do not send a message after watchtower started. Otherwise there will be an info-level notification.
@@ -248,7 +317,7 @@ Environment Variable: WATCHTOWER_NO_STARTUP_MESSAGE
Run an update attempt against a container name list one time immediately and exit.
```text
- Argument: --run-once
+ Argument: --run-once, -R
Environment Variable: WATCHTOWER_RUN_ONCE
Type: Boolean
Default: false
@@ -267,6 +336,7 @@ Environment Variable: WATCHTOWER_HTTP_API_UPDATE
## HTTP API Token
Sets an authentication token to HTTP API requests.
+Can also reference a file, in which case the contents of the file are used.
```text
Argument: --http-api-token
@@ -289,6 +359,11 @@ Environment Variable: WATCHTOWER_HTTP_API_PERIODIC_POLLS
Update containers that have a `com.centurylinklabs.watchtower.scope` label set with the same value as the given argument.
This enables [running multiple instances](https://containrrr.dev/watchtower/running-multiple-instances).
+!!! note "Filter by lack of scope"
+ If you want other instances of watchtower to ignore the scoped containers, set this argument to `none`.
+ When omitted, watchtower will update all containers regardless of scope.
+
+
```text
Argument: --scope
Environment Variable: WATCHTOWER_SCOPE
@@ -360,4 +435,33 @@ requests and may rate limit pull requests (mainly docker.io).
Environment Variable: WATCHTOWER_WARN_ON_HEAD_FAILURE
Possible values: always, auto, never
Default: auto
-```
\ No newline at end of file
+```
+
+## 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: -
+```
diff --git a/docs/container-selection.md b/docs/container-selection.md
index 4b6facd..8327c66 100644
--- a/docs/container-selection.md
+++ b/docs/container-selection.md
@@ -58,6 +58,7 @@ If instead you want to [only include containers with the enable label](https://c
If you wish to create a monitoring scope, you will need to [run multiple instances and set a scope for each of them](https://containrrr.github.io/watchtower/running-multiple-instances).
Watchtower filters running containers by testing them against each configured criteria. A container is monitored if all criteria are met. For example:
+
- If a container's name is on the monitoring name list (not empty `--name` argument) but it is not enabled (_centurylinklabs.watchtower.enable=false_), it won't be monitored;
- If a container's name is not on the monitoring name list (not empty `--name` argument), even if it is enabled (_centurylinklabs.watchtower.enable=true_ and `--label-enable` flag is set), it won't be monitored;
diff --git a/docs/http-api-mode.md b/docs/http-api-mode.md
index 2cf082a..69812bb 100644
--- a/docs/http-api-mode.md
+++ b/docs/http-api-mode.md
@@ -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
+```
diff --git a/docs/introduction.md b/docs/introduction.md
index ded074f..cbbc3a3 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -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).
diff --git a/docs/linked-containers.md b/docs/linked-containers.md
index 240fb97..c7e9be8 100644
--- a/docs/linked-containers.md
+++ b/docs/linked-containers.md
@@ -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.
diff --git a/docs/metrics.md b/docs/metrics.md
index 7bb6383..480d7c6 100644
--- a/docs/metrics.md
+++ b/docs/metrics.md
@@ -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`.
diff --git a/docs/notifications.md b/docs/notifications.md
index 3905abf..d5da4fe 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -18,19 +18,20 @@ system, [logrus](http://github.com/sirupsen/logrus).
- `--notifications-level` (env. `WATCHTOWER_NOTIFICATIONS_LEVEL`): Controls the log level which is used for the notifications. If omitted, the default log level is `info`. Possible values are: `panic`, `fatal`, `error`, `warn`, `info`, `debug` or `trace`.
- `--notifications-hostname` (env. `WATCHTOWER_NOTIFICATIONS_HOSTNAME`): Custom hostname specified in subject/title. Useful to override the operating system hostname.
-- `--notifications-delay` (env. `WATCHTOWER_NOTIFICATION_DELAY`): Delay before sending notifications expressed in seconds.
+- `--notifications-delay` (env. `WATCHTOWER_NOTIFICATIONS_DELAY`): Delay before sending notifications expressed in seconds.
- Watchtower will post a notification every time it is started. This behavior [can be changed](https://containrrr.github.io/watchtower/arguments/#without_sending_a_startup_message) with an argument.
-- `notification-title-tag` (env. `WATCHTOWER_NOTIFICATION_TITLE_TAG`): Prefix to include in the title. Useful when running multiple watchtowers.
-- `notification-skip-title` (env. `WATCHTOWER_NOTIFICATION_SKIP_TITLE`): Do not pass the title param to notifications. This will not pass a dynamic title override to notification services. If no title is configured for the service, it will remove the title all together.
+- `--notification-title-tag` (env. `WATCHTOWER_NOTIFICATION_TITLE_TAG`): Prefix to include in the title. Useful when running multiple watchtowers.
+- `--notification-skip-title` (env. `WATCHTOWER_NOTIFICATION_SKIP_TITLE`): Do not pass the title param to notifications. This will not pass a dynamic title override to notification services. If no title is configured for the service, it will remove the title all together.
+- `--notification-log-stdout` (env. `WATCHTOWER_NOTIFICATION_LOG_STDOUT`): Enable output from `logger://` shoutrrr service to stdout.
-## [shoutrrr](https://github.com/containrrr/shoutrrr) notifications
+## [Shoutrrr](https://github.com/containrrr/shoutrrr) notifications
To send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set:
- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. This option can also reference a file, in which case the contents of the file are used.
-Go to [containrrr.dev/shoutrrr/v0.6/services/overview](https://containrrr.dev/shoutrrr/v0.6/services/overview) to
+Go to [containrrr.dev/shoutrrr/v0.8/services/overview](https://containrrr.dev/shoutrrr/v0.8/services/overview) to
learn more about the different service URLs you can use. You can define multiple services by space separating the
URLs. (See example below)
@@ -56,6 +57,10 @@ outputs timestamp and log level.
custom format.
i.e., The day of the year has to be 1, the month has to be 2 (february), the hour 3 (or 15 for 24h time) etc.
+!!! note "Skipping notifications"
+ To skip sending notifications that do not contain any information, you can wrap your template with `{{if .}}` and `{{end}}`.
+
+
Example:
```bash
@@ -110,7 +115,7 @@ Example using a custom report template that always sends a session report after
docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
- -e WATCHTOWER_NOTIFICATION_REPORT="true"
+ -e WATCHTOWER_NOTIFICATION_REPORT="true" \
-e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \
-e WATCHTOWER_NOTIFICATION_TEMPLATE="
{{- if .Report -}}
@@ -130,7 +135,7 @@ Example using a custom report template that always sends a session report after
{{- end -}}
{{- end -}}
{{- else -}}
- {{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
+ {{range .Entries -}}{{.Message}}{{\"\n\"}}{{- end -}}
{{- end -}}
" \
containrrr/watchtower
diff --git a/docs/private-registries.md b/docs/private-registries.md
index ee4ed41..5367a8c 100644
--- a/docs/private-registries.md
+++ b/docs/private-registries.md
@@ -23,19 +23,29 @@ password `auth` string:
```
`` 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,
+ `` 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.
+
+ Watchtower will recognize credentials with `` `index.docker.io`,
+ but the Docker CLI will not.
+!!! 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
- /.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
diff --git a/docs/running-multiple-instances.md b/docs/running-multiple-instances.md
index 3899095..5a82c80 100644
--- a/docs/running-multiple-instances.md
+++ b/docs/running-multiple-instances.md
@@ -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
```
diff --git a/docs/stylesheets/theme.css b/docs/stylesheets/theme.css
index fb5e675..e552129 100644
--- a/docs/stylesheets/theme.css
+++ b/docs/stylesheets/theme.css
@@ -14,56 +14,56 @@
--md-hue: 199;
/* Primary and accent */
- --md-primary-fg-color: hsla(199deg 27% 35% 100%);
- --md-primary-fg-color--link: hsla(199deg 45% 65% 100%);
- --md-primary-fg-color--light: hsla(198deg 19% 73% 100%);
- --md-primary-fg-color--dark: hsla(194deg 100% 13% 100%);
- --md-accent-fg-color: hsla(194deg 45% 50% 100%);
- --md-accent-fg-color--transparent: hsla(194deg 45% 50% 6.3%);
+ --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: hsla(var(--md-hue) 75% 95% 100%);
- --md-default-fg-color--light: hsla(var(--md-hue) 75% 90% 62%);
- --md-default-fg-color--lighter: hsla(var(--md-hue) 75% 90% 32%);
- --md-default-fg-color--lightest: hsla(var(--md-hue) 75% 90% 12%);
- --md-default-bg-color: hsla(var(--md-hue) 15% 21% 100%);
- --md-default-bg-color--light: hsla(var(--md-hue) 15% 21% 54%);
- --md-default-bg-color--lighter: hsla(var(--md-hue) 15% 21% 26%);
- --md-default-bg-color--lightest: hsla(var(--md-hue) 15% 21% 7%);
+ --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: hsla(var(--md-hue) 18% 86% 100%);
- --md-code-bg-color: hsla(var(--md-hue) 15% 15% 100%);
- --md-code-hl-color: hsla(218deg 100% 63% 15%);
- --md-code-hl-number-color: hsla(346deg 74% 63% 100%);
- --md-code-hl-special-color: hsla(320deg 83% 66% 100%);
- --md-code-hl-function-color: hsla(271deg 57% 65% 100%);
- --md-code-hl-constant-color: hsla(230deg 62% 70% 100%);
- --md-code-hl-keyword-color: hsla(199deg 33% 64% 100%);
- --md-code-hl-string-color: hsla( 50deg 34% 74% 100%);
+ --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: hsla(241deg 22% 60% 100%);
+ --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: hsla(218deg 100% 63% 30%);
- --md-typeset-kbd-color: hsla(var(--md-hue) 15% 94% 12%);
- --md-typeset-kbd-accent-color: hsla(var(--md-hue) 15% 94% 20%);
- --md-typeset-kbd-border-color: hsla(var(--md-hue) 15% 14% 100%);
- --md-typeset-table-color: hsla(var(--md-hue) 75% 95% 12%);
+ --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: hsla(var(--md-hue) 15% 12% 87%);
- --md-footer-bg-color--dark: hsla(var(--md-hue) 15% 10% 100%);
+ --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:
diff --git a/docs/template-preview.md b/docs/template-preview.md
new file mode 100644
index 0000000..3d99ce9
--- /dev/null
+++ b/docs/template-preview.md
@@ -0,0 +1,251 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/usage-overview.md b/docs/usage-overview.md
index 8c1e12f..1cac352 100644
--- a/docs/usage-overview.md
+++ b/docs/usage-overview.md
@@ -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//:
+ image: ghcr.io//:
ports:
- "443:3443"
- "80:3080"
diff --git a/go.mod b/go.mod
index 87875bd..6c20d11 100644
--- a/go.mod
+++ b/go.mod
@@ -1,69 +1,74 @@
module github.com/containrrr/watchtower
-go 1.18
+go 1.20
require (
- github.com/containrrr/shoutrrr v0.6.1
- github.com/docker/cli v20.10.21+incompatible
- github.com/docker/distribution v2.8.1+incompatible
- github.com/docker/docker v20.10.21+incompatible
+ github.com/containrrr/shoutrrr v0.8.0
+ github.com/distribution/reference v0.5.0
+ github.com/docker/cli v24.0.7+incompatible
+ github.com/docker/docker v24.0.7+incompatible
github.com/docker/go-connections v0.4.0
github.com/onsi/ginkgo v1.16.5
- github.com/onsi/gomega v1.23.0
- github.com/prometheus/client_golang v1.13.0
- github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
- github.com/sirupsen/logrus v1.9.0
- github.com/spf13/cobra v1.6.0
+ 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.13.0
- github.com/stretchr/testify v1.8.1
- golang.org/x/net v0.1.0
+ github.com/spf13/viper v1.18.2
+ github.com/stretchr/testify v1.8.4
+ golang.org/x/net v0.19.0
)
+require github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
+
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.4.17 // indirect
github.com/beorn7/perks v1.0.1 // indirect
- github.com/cespare/xxhash/v2 v2.1.2 // indirect
- github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.1 // indirect
github.com/docker/go-units v0.4.0 // indirect
- github.com/fatih/color v1.13.0 // indirect
- github.com/fsnotify/fsnotify v1.5.4 // indirect
+ github.com/fatih/color v1.15.0 // indirect
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
- github.com/golang/protobuf v1.5.2 // indirect
- github.com/google/go-cmp v0.5.9 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
- github.com/inconshreveable/mousetrap v1.0.1 // indirect
- github.com/magiconair/properties v1.8.6 // indirect
- github.com/mattn/go-colorable v0.1.12 // indirect
- github.com/mattn/go-isatty v0.0.14 // indirect
- github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // 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/nxadm/tail v1.4.8 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
- github.com/pelletier/go-toml v1.9.5 // indirect
- github.com/pelletier/go-toml/v2 v2.0.5 // indirect
+ github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
- github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/prometheus/client_model v0.2.0 // indirect
- github.com/prometheus/common v0.37.0 // indirect
- github.com/prometheus/procfs v0.8.0 // indirect
- github.com/spf13/afero v1.8.2 // indirect
- github.com/spf13/cast v1.5.0 // indirect
- github.com/spf13/jwalterweatherman v1.1.0 // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/prometheus/client_model v0.5.0 // indirect
+ github.com/prometheus/common v0.45.0 // indirect
+ github.com/prometheus/procfs v0.12.0 // indirect
+ github.com/sagikazarmark/locafero v0.4.0 // indirect
+ github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.11.0 // indirect
+ github.com/spf13/cast v1.6.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
- github.com/subosito/gotenv v1.4.1 // indirect
- golang.org/x/sys v0.1.0 // indirect
- golang.org/x/text v0.4.0
- golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
- google.golang.org/protobuf v1.28.1 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ go.uber.org/atomic v1.9.0 // indirect
+ go.uber.org/multierr v1.9.0 // indirect
+ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/text v0.14.0
+ golang.org/x/time v0.5.0 // indirect
+ google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
- gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.0.3 // indirect
)
diff --git a/go.sum b/go.sum
index c57ef97..cab338f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,763 +1,240 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.4.17 h1:iT12IBVClFevaf8PuVyi3UmZOVh4OqnaLxDTW2O6j3w=
github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
-github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/containrrr/shoutrrr v0.6.1 h1:6ih7jA6mo3t6C97MZbd3SxL/kRizOE3bI9CpBQZ6wzg=
-github.com/containrrr/shoutrrr v0.6.1/go.mod h1:ye9jGX5YzMnJ76waaNVWlJ4luhMEyt1EWU5unYTQSb0=
-github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
+github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/docker/cli v20.10.21+incompatible h1:qVkgyYUnOLQ98LtXBrwd/duVqPT2X4SHndOuGsfwyhU=
-github.com/docker/cli v20.10.21+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
-github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog=
-github.com/docker/docker v20.10.21+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
+github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg=
+github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
+github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
+github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.1 h1:Dq4iIfcM7cNtddhLVWe9h4QDjsi4OER3Z8voPu/I52g=
github.com/docker/docker-credential-helpers v0.6.1/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
-github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
-github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
-github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
+github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
-github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
-github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
-github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
-github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA=
-github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
-github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
-github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
-github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
-github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
-github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
-github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs=
+github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys=
-github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg=
+github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
+github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
-github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
-github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
-github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
-github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
+github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
-github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
-github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
-github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
-github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
-github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
-github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
-github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE=
-github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
-github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
+github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
+github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
+github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
-github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
-github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
-github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo=
-github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
-github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
-github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
-github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
-github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
-github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
-github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
-github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU=
-github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw=
+github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
+github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
-github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
-go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
-go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
-golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
-golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
-google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/internal/actions/actions_suite_test.go b/internal/actions/actions_suite_test.go
index 5110fea..c320564 100644
--- a/internal/actions/actions_suite_test.go
+++ b/internal/actions/actions_suite_test.go
@@ -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",
diff --git a/internal/actions/check.go b/internal/actions/check.go
index 436931f..77a2266 100644
--- a/internal/actions/check.go
+++ b/internal/actions/check.go
@@ -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))
diff --git a/internal/actions/mocks/client.go b/internal/actions/mocks/client.go
index 2afc43c..737404a 100644
--- a/internal/actions/mocks/client.go
+++ b/internal/actions/mocks/client.go
@@ -5,8 +5,6 @@ import (
"fmt"
"time"
- "github.com/containrrr/watchtower/pkg/container"
-
t "github.com/containrrr/watchtower/pkg/types"
)
@@ -21,7 +19,7 @@ type MockClient struct {
type TestData struct {
TriedToRemoveImageCount int
NameOfContainerToKeep string
- Containers []container.Container
+ Containers []t.Container
Staleness map[string]bool
}
@@ -40,12 +38,12 @@ func CreateMockClient(data *TestData, pullImages bool, removeVolumes bool) MockC
}
// ListContainers is a mock method returning the provided container testdata
-func (client MockClient) ListContainers(_ t.Filter) ([]container.Container, error) {
+func (client MockClient) ListContainers(_ t.Filter) ([]t.Container, error) {
return client.TestData.Containers, nil
}
// StopContainer is a mock method
-func (client MockClient) StopContainer(c container.Container, _ time.Duration) error {
+func (client MockClient) StopContainer(c t.Container, _ time.Duration) error {
if c.Name() == client.TestData.NameOfContainerToKeep {
return errors.New("tried to stop the instance we want to keep")
}
@@ -53,12 +51,12 @@ func (client MockClient) StopContainer(c container.Container, _ time.Duration) e
}
// StartContainer is a mock method
-func (client MockClient) StartContainer(_ container.Container) (t.ContainerID, error) {
+func (client MockClient) StartContainer(_ t.Container) (t.ContainerID, error) {
return "", nil
}
// RenameContainer is a mock method
-func (client MockClient) RenameContainer(_ container.Container, _ string) error {
+func (client MockClient) RenameContainer(_ t.Container, _ string) error {
return nil
}
@@ -69,7 +67,7 @@ func (client MockClient) RemoveImageByID(_ t.ImageID) error {
}
// GetContainer is a mock method
-func (client MockClient) GetContainer(_ t.ContainerID) (container.Container, error) {
+func (client MockClient) GetContainer(_ t.ContainerID) (t.Container, error) {
return client.TestData.Containers[0], nil
}
@@ -88,7 +86,7 @@ func (client MockClient) ExecuteCommand(_ t.ContainerID, command string, _ int)
}
// IsContainerStale is true if not explicitly stated in TestData for the mock client
-func (client MockClient) IsContainerStale(cont container.Container) (bool, t.ImageID, error) {
+func (client MockClient) IsContainerStale(cont t.Container, params t.UpdateParams) (bool, t.ImageID, error) {
stale, found := client.TestData.Staleness[cont.Name()]
if !found {
stale = true
@@ -97,6 +95,6 @@ func (client MockClient) IsContainerStale(cont container.Container) (bool, t.Ima
}
// WarnOnHeadPullFailed is always true for the mock client
-func (client MockClient) WarnOnHeadPullFailed(_ container.Container) bool {
+func (client MockClient) WarnOnHeadPullFailed(_ t.Container) bool {
return true
}
diff --git a/internal/actions/mocks/container.go b/internal/actions/mocks/container.go
index 3272d63..e830587 100644
--- a/internal/actions/mocks/container.go
+++ b/internal/actions/mocks/container.go
@@ -14,7 +14,7 @@ import (
)
// CreateMockContainer creates a container substitute valid for testing
-func CreateMockContainer(id string, name string, image string, created time.Time) container.Container {
+func CreateMockContainer(id string, name string, image string, created time.Time) wt.Container {
content := types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
ID: id,
@@ -31,7 +31,7 @@ func CreateMockContainer(id string, name string, image string, created time.Time
ExposedPorts: map[nat.Port]struct{}{},
},
}
- return *container.NewContainer(
+ return container.NewContainer(
&content,
CreateMockImageInfo(image),
)
@@ -48,12 +48,12 @@ func CreateMockImageInfo(image string) *types.ImageInspect {
}
// CreateMockContainerWithImageInfo should only be used for testing
-func CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) container.Container {
+func CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) wt.Container {
return CreateMockContainerWithImageInfoP(id, name, image, created, &imageInfo)
}
// CreateMockContainerWithImageInfoP should only be used for testing
-func CreateMockContainerWithImageInfoP(id string, name string, image string, created time.Time, imageInfo *types.ImageInspect) container.Container {
+func CreateMockContainerWithImageInfoP(id string, name string, image string, created time.Time, imageInfo *types.ImageInspect) wt.Container {
content := types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
ID: id,
@@ -66,21 +66,21 @@ func CreateMockContainerWithImageInfoP(id string, name string, image string, cre
Labels: make(map[string]string),
},
}
- return *container.NewContainer(
+ return container.NewContainer(
&content,
imageInfo,
)
}
// CreateMockContainerWithDigest should only be used for testing
-func CreateMockContainerWithDigest(id string, name string, image string, created time.Time, digest string) container.Container {
+func CreateMockContainerWithDigest(id string, name string, image string, created time.Time, digest string) wt.Container {
c := CreateMockContainer(id, name, image, created)
c.ImageInfo().RepoDigests = []string{digest}
return c
}
// CreateMockContainerWithConfig creates a container substitute valid for testing
-func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) container.Container {
+func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) wt.Container {
content := types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
ID: id,
@@ -97,14 +97,14 @@ func CreateMockContainerWithConfig(id string, name string, image string, running
},
Config: config,
}
- return *container.NewContainer(
+ return container.NewContainer(
&content,
CreateMockImageInfo(image),
)
}
// CreateContainerForProgress creates a container substitute for tracking session/update progress
-func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (container.Container, wt.ImageID) {
+func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (wt.Container, wt.ImageID) {
indexStr := strconv.Itoa(idPrefix + index)
mockID := indexStr + strings.Repeat("0", 61-len(indexStr))
contID := "c79" + mockID
@@ -120,7 +120,7 @@ func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (con
}
// CreateMockContainerWithLinks should only be used for testing
-func CreateMockContainerWithLinks(id string, name string, image string, created time.Time, links []string, imageInfo *types.ImageInspect) container.Container {
+func CreateMockContainerWithLinks(id string, name string, image string, created time.Time, links []string, imageInfo *types.ImageInspect) wt.Container {
content := types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
ID: id,
@@ -136,7 +136,7 @@ func CreateMockContainerWithLinks(id string, name string, image string, created
Labels: make(map[string]string),
},
}
- return *container.NewContainer(
+ return container.NewContainer(
&content,
imageInfo,
)
diff --git a/internal/actions/update.go b/internal/actions/update.go
index bd3791f..8853c6e 100644
--- a/internal/actions/update.go
+++ b/internal/actions/update.go
@@ -2,7 +2,6 @@ package actions
import (
"errors"
- "strings"
"github.com/containrrr/watchtower/internal/util"
"github.com/containrrr/watchtower/pkg/container"
@@ -34,8 +33,8 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
staleCheckFailed := 0
for i, targetContainer := range containers {
- stale, newestImage, err := client.IsContainerStale(targetContainer)
- shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly()
+ stale, newestImage, err := client.IsContainerStale(targetContainer, params)
+ shouldUpdate := stale && !params.NoRestart && !targetContainer.IsMonitorOnly(params)
if err == nil && shouldUpdate {
// Check to make sure we have all the necessary information for recreating the container
err = targetContainer.VerifyConfiguration()
@@ -58,7 +57,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
} else {
progress.AddScanned(targetContainer, newestImage)
}
- containers[i].Stale = stale
+ containers[i].SetStale(stale)
if stale {
staleCount++
@@ -72,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())
}
}
@@ -97,7 +94,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
return progress.Report(), nil
}
-func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {
+func performRollingRestart(containers []types.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {
cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
failed := make(map[types.ContainerID]error, len(containers))
@@ -109,7 +106,7 @@ func performRollingRestart(containers []container.Container, client container.Cl
} else {
if err := restartStaleContainer(containers[i], client, params); err != nil {
failed[containers[i].ID()] = err
- } else if containers[i].Stale {
+ } else if containers[i].IsStale() {
// Only add (previously) stale containers' images to cleanup
cleanupImageIDs[containers[i].ImageID()] = true
}
@@ -123,7 +120,7 @@ func performRollingRestart(containers []container.Container, client container.Cl
return failed
}
-func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) {
+func stopContainersInReversedOrder(containers []types.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) {
failed = make(map[types.ContainerID]error, len(containers))
stopped = make(map[types.ImageID]bool, len(containers))
for i := len(containers) - 1; i >= 0; i-- {
@@ -138,7 +135,7 @@ func stopContainersInReversedOrder(containers []container.Container, client cont
return
}
-func stopStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error {
+func stopStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error {
if container.IsWatchtower() {
log.Debugf("This is the watchtower container %s", container.Name())
return nil
@@ -149,7 +146,7 @@ func stopStaleContainer(container container.Container, client container.Client,
}
// Perform an additional check here to prevent us from stopping a linked container we cannot restart
- if container.LinkedToRestarting {
+ if container.IsLinkedToRestarting() {
if err := container.VerifyConfiguration(); err != nil {
return err
}
@@ -175,7 +172,7 @@ func stopStaleContainer(container container.Container, client container.Client,
return nil
}
-func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error {
+func restartContainersInSortedOrder(containers []types.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error {
cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
failed := make(map[types.ContainerID]error, len(containers))
@@ -186,7 +183,7 @@ func restartContainersInSortedOrder(containers []container.Container, client con
if stoppedImages[c.SafeImageID()] {
if err := restartStaleContainer(c, client, params); err != nil {
failed[c.ID()] = err
- } else if c.Stale {
+ } else if c.IsStale() {
// Only add (previously) stale containers' images to cleanup
cleanupImageIDs[c.ImageID()] = true
}
@@ -211,7 +208,7 @@ func cleanupImages(client container.Client, imageIDs map[types.ImageID]bool) {
}
}
-func restartStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error {
+func restartStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error {
// Since we can't shutdown a watchtower container immediately, we need to
// start the new one while the old one is still running. This prevents us
// from re-using the same container name so we first rename the current
@@ -236,7 +233,7 @@ func restartStaleContainer(container container.Container, client container.Clien
// UpdateImplicitRestart iterates through the passed containers, setting the
// `LinkedToRestarting` flag if any of it's linked containers are marked for restart
-func UpdateImplicitRestart(containers []container.Container) {
+func UpdateImplicitRestart(containers []types.Container) {
for ci, c := range containers {
if c.ToRestart() {
@@ -250,7 +247,7 @@ func UpdateImplicitRestart(containers []container.Container) {
"linked": c.Name(),
}).Debug("container is linked to restarting")
// NOTE: To mutate the array, the `c` variable cannot be used as it's a copy
- containers[ci].LinkedToRestarting = true
+ containers[ci].SetLinkedToRestarting(true)
}
}
@@ -258,12 +255,8 @@ func UpdateImplicitRestart(containers []container.Container) {
// linkedContainerMarkedForRestart returns the name of the first link that matches a
// container marked for restart
-func linkedContainerMarkedForRestart(links []string, containers []container.Container) string {
+func linkedContainerMarkedForRestart(links []string, containers []types.Container) string {
for _, linkName := range links {
- // Since the container names need to start with '/', let's prepend it if it's missing
- if !strings.HasPrefix(linkName, "/") {
- linkName = "/" + linkName
- }
for _, candidate := range containers {
if candidate.Name() == linkName && candidate.ToRestart() {
return linkName
diff --git a/internal/actions/update_test.go b/internal/actions/update_test.go
index eb540b1..9209dcd 100644
--- a/internal/actions/update_test.go
+++ b/internal/actions/update_test.go
@@ -4,7 +4,6 @@ import (
"time"
"github.com/containrrr/watchtower/internal/actions"
- "github.com/containrrr/watchtower/pkg/container"
"github.com/containrrr/watchtower/pkg/types"
dockerTypes "github.com/docker/docker/api/types"
dockerContainer "github.com/docker/docker/api/types/container"
@@ -18,7 +17,7 @@ import (
func getCommonTestData(keepContainer string) *TestData {
return &TestData{
NameOfContainerToKeep: keepContainer,
- Containers: []container.Container{
+ Containers: []types.Container{
CreateMockContainer(
"test-container-01",
"test-container-01",
@@ -59,7 +58,7 @@ func getLinkedTestData(withImageInfo bool) *TestData {
return &TestData{
Staleness: map[string]bool{linkingContainer.Name(): false},
- Containers: []container.Container{
+ Containers: []types.Container{
staleContainer,
linkingContainer,
},
@@ -130,7 +129,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient(
&TestData{
NameOfContainerToKeep: "test-container-02",
- Containers: []container.Container{
+ Containers: []types.Container{
CreateMockContainer(
"test-container-01",
"test-container-01",
@@ -163,7 +162,7 @@ var _ = Describe("the update action", func() {
It("should not update any containers", func() {
client := CreateMockClient(
&TestData{
- Containers: []container.Container{
+ Containers: []types.Container{
CreateMockContainer(
"test-container-01",
"test-container-01",
@@ -179,12 +178,84 @@ var _ = Describe("the update action", func() {
false,
false,
)
- _, err := actions.Update(client, types.UpdateParams{MonitorOnly: true})
+ _, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true})
Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
})
- })
+ When("watchtower has been instructed to have label take precedence", func() {
+ It("it should update containers when monitor only is set to false", func() {
+ client := CreateMockClient(
+ &TestData{
+ //NameOfContainerToKeep: "test-container-02",
+ Containers: []types.Container{
+ CreateMockContainerWithConfig(
+ "test-container-02",
+ "test-container-02",
+ "fake-image2:latest",
+ false,
+ false,
+ time.Now(),
+ &dockerContainer.Config{
+ Labels: map[string]string{
+ "com.centurylinklabs.watchtower.monitor-only": "false",
+ },
+ }),
+ },
+ },
+ false,
+ false,
+ )
+ _, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
+ })
+ It("it should update not containers when monitor only is set to true", func() {
+ client := CreateMockClient(
+ &TestData{
+ //NameOfContainerToKeep: "test-container-02",
+ Containers: []types.Container{
+ CreateMockContainerWithConfig(
+ "test-container-02",
+ "test-container-02",
+ "fake-image2:latest",
+ false,
+ false,
+ time.Now(),
+ &dockerContainer.Config{
+ Labels: map[string]string{
+ "com.centurylinklabs.watchtower.monitor-only": "true",
+ },
+ }),
+ },
+ },
+ false,
+ false,
+ )
+ _, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
+ })
+ It("it should update not containers when monitor only is not set", func() {
+ client := CreateMockClient(
+ &TestData{
+ Containers: []types.Container{
+ CreateMockContainer(
+ "test-container-01",
+ "test-container-01",
+ "fake-image:latest",
+ time.Now()),
+ },
+ },
+ false,
+ false,
+ )
+ _, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true})
+ Expect(err).NotTo(HaveOccurred())
+ Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
+ })
+ })
+ })
})
When("watchtower has been instructed to run lifecycle hooks", func() {
@@ -194,7 +265,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient(
&TestData{
//NameOfContainerToKeep: "test-container-02",
- Containers: []container.Container{
+ Containers: []types.Container{
CreateMockContainerWithConfig(
"test-container-02",
"test-container-02",
@@ -227,7 +298,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient(
&TestData{
//NameOfContainerToKeep: "test-container-02",
- Containers: []container.Container{
+ Containers: []types.Container{
CreateMockContainerWithConfig(
"test-container-02",
"test-container-02",
@@ -259,7 +330,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient(
&TestData{
//NameOfContainerToKeep: "test-container-02",
- Containers: []container.Container{
+ Containers: []types.Container{
CreateMockContainerWithConfig(
"test-container-02",
"test-container-02",
@@ -300,7 +371,7 @@ var _ = Describe("the update action", func() {
ExposedPorts: map[nat.Port]struct{}{},
})
- provider.Stale = true
+ provider.SetStale(true)
consumer := CreateMockContainerWithConfig(
"test-container-consumer",
@@ -316,7 +387,7 @@ var _ = Describe("the update action", func() {
ExposedPorts: map[nat.Port]struct{}{},
})
- containers := []container.Container{
+ containers := []types.Container{
provider,
consumer,
}
@@ -338,7 +409,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient(
&TestData{
//NameOfContainerToKeep: "test-container-02",
- Containers: []container.Container{
+ Containers: []types.Container{
CreateMockContainerWithConfig(
"test-container-02",
"test-container-02",
@@ -370,7 +441,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient(
&TestData{
//NameOfContainerToKeep: "test-container-02",
- Containers: []container.Container{
+ Containers: []types.Container{
CreateMockContainerWithConfig(
"test-container-02",
"test-container-02",
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index 5428b95..c11cdae 100644
--- a/internal/flags/flags.go
+++ b/internal/flags/flags.go
@@ -4,8 +4,8 @@ import (
"bufio"
"errors"
"fmt"
- "io/ioutil"
"os"
+ "regexp"
"strings"
"time"
@@ -24,9 +24,9 @@ var defaultInterval = int((time.Hour * 24).Seconds())
// RegisterDockerFlags that are used directly by the docker api client
func RegisterDockerFlags(rootCmd *cobra.Command) {
flags := rootCmd.PersistentFlags()
- flags.StringP("host", "H", viper.GetString("DOCKER_HOST"), "daemon socket to connect to")
- flags.BoolP("tlsverify", "v", viper.GetBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote")
- flags.StringP("api-version", "a", viper.GetString("DOCKER_API_VERSION"), "api version to use by docker client")
+ flags.StringP("host", "H", envString("DOCKER_HOST"), "daemon socket to connect to")
+ flags.BoolP("tlsverify", "v", envBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote")
+ flags.StringP("api-version", "a", envString("DOCKER_API_VERSION"), "api version to use by docker client")
}
// RegisterSystemFlags that are used by watchtower to modify the program flow
@@ -35,132 +35,145 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
flags.IntP(
"interval",
"i",
- viper.GetInt("WATCHTOWER_POLL_INTERVAL"),
+ envInt("WATCHTOWER_POLL_INTERVAL"),
"Poll interval (in seconds)")
flags.StringP(
"schedule",
"s",
- viper.GetString("WATCHTOWER_SCHEDULE"),
+ envString("WATCHTOWER_SCHEDULE"),
"The cron expression which defines when to update")
flags.DurationP(
"stop-timeout",
"t",
- viper.GetDuration("WATCHTOWER_TIMEOUT"),
+ envDuration("WATCHTOWER_TIMEOUT"),
"Timeout before a container is forcefully stopped")
flags.BoolP(
"no-pull",
"",
- viper.GetBool("WATCHTOWER_NO_PULL"),
+ envBool("WATCHTOWER_NO_PULL"),
"Do not pull any new images")
flags.BoolP(
"no-restart",
"",
- viper.GetBool("WATCHTOWER_NO_RESTART"),
+ envBool("WATCHTOWER_NO_RESTART"),
"Do not restart any containers")
flags.BoolP(
"no-startup-message",
"",
- viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"),
+ envBool("WATCHTOWER_NO_STARTUP_MESSAGE"),
"Prevents watchtower from sending a startup message")
flags.BoolP(
"cleanup",
"c",
- viper.GetBool("WATCHTOWER_CLEANUP"),
+ envBool("WATCHTOWER_CLEANUP"),
"Remove previously used images after updating")
flags.BoolP(
"remove-volumes",
"",
- viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"),
+ envBool("WATCHTOWER_REMOVE_VOLUMES"),
"Remove attached volumes before updating")
flags.BoolP(
"label-enable",
"e",
- viper.GetBool("WATCHTOWER_LABEL_ENABLE"),
+ envBool("WATCHTOWER_LABEL_ENABLE"),
"Watch containers where the com.centurylinklabs.watchtower.enable label is true")
+ flags.StringSliceP(
+ "disable-containers",
+ "x",
+ // Due to issue spf13/viper#380, can't use viper.GetStringSlice:
+ regexp.MustCompile("[, ]+").Split(envString("WATCHTOWER_DISABLE_CONTAINERS"), -1),
+ "Comma-separated list of containers to explicitly exclude from watching.")
+
+ flags.StringP(
+ "log-format",
+ "l",
+ viper.GetString("WATCHTOWER_LOG_FORMAT"),
+ "Sets what logging format to use for console output. Possible values: Auto, LogFmt, Pretty, JSON")
+
flags.BoolP(
"debug",
"d",
- viper.GetBool("WATCHTOWER_DEBUG"),
+ envBool("WATCHTOWER_DEBUG"),
"Enable debug mode with verbose logging")
flags.BoolP(
"trace",
"",
- viper.GetBool("WATCHTOWER_TRACE"),
+ envBool("WATCHTOWER_TRACE"),
"Enable trace mode with very verbose logging - caution, exposes credentials")
flags.BoolP(
"monitor-only",
"m",
- viper.GetBool("WATCHTOWER_MONITOR_ONLY"),
+ envBool("WATCHTOWER_MONITOR_ONLY"),
"Will only monitor for new images, not update the containers")
flags.BoolP(
"run-once",
"R",
- viper.GetBool("WATCHTOWER_RUN_ONCE"),
+ envBool("WATCHTOWER_RUN_ONCE"),
"Run once now and exit")
flags.BoolP(
"include-restarting",
"",
- viper.GetBool("WATCHTOWER_INCLUDE_RESTARTING"),
+ envBool("WATCHTOWER_INCLUDE_RESTARTING"),
"Will also include restarting containers")
flags.BoolP(
"include-stopped",
"S",
- viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"),
+ envBool("WATCHTOWER_INCLUDE_STOPPED"),
"Will also include created and exited containers")
flags.BoolP(
"revive-stopped",
"",
- viper.GetBool("WATCHTOWER_REVIVE_STOPPED"),
+ envBool("WATCHTOWER_REVIVE_STOPPED"),
"Will also start stopped containers that were updated, if include-stopped is active")
flags.BoolP(
"enable-lifecycle-hooks",
"",
- viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"),
+ envBool("WATCHTOWER_LIFECYCLE_HOOKS"),
"Enable the execution of commands triggered by pre- and post-update lifecycle hooks")
flags.BoolP(
"rolling-restart",
"",
- viper.GetBool("WATCHTOWER_ROLLING_RESTART"),
+ envBool("WATCHTOWER_ROLLING_RESTART"),
"Restart containers one at a time")
flags.BoolP(
"http-api-update",
"",
- viper.GetBool("WATCHTOWER_HTTP_API_UPDATE"),
+ envBool("WATCHTOWER_HTTP_API_UPDATE"),
"Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request")
flags.BoolP(
"http-api-metrics",
"",
- viper.GetBool("WATCHTOWER_HTTP_API_METRICS"),
+ envBool("WATCHTOWER_HTTP_API_METRICS"),
"Runs Watchtower with the Prometheus metrics API enabled")
flags.StringP(
"http-api-token",
"",
- viper.GetString("WATCHTOWER_HTTP_API_TOKEN"),
+ envString("WATCHTOWER_HTTP_API_TOKEN"),
"Sets an authentication token to HTTP API requests.")
flags.BoolP(
"http-api-periodic-polls",
"",
- viper.GetBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"),
+ envBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"),
"Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled")
// https://no-color.org/
@@ -173,19 +186,31 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
flags.StringP(
"scope",
"",
- viper.GetString("WATCHTOWER_SCOPE"),
+ envString("WATCHTOWER_SCOPE"),
"Defines a monitoring scope for the Watchtower instance.")
flags.StringP(
"porcelain",
"P",
- viper.GetString("WATCHTOWER_PORCELAIN"),
+ envString("WATCHTOWER_PORCELAIN"),
`Write session results to stdout using a stable versioned format. Supported values: "v1"`)
flags.String(
"log-level",
- viper.GetString("WATCHTOWER_LOG_LEVEL"),
+ envString("WATCHTOWER_LOG_LEVEL"),
"The maximum log level that will be written to STDERR. Possible values: panic, fatal, error, warn, info, debug or trace")
+
+ flags.BoolP(
+ "health-check",
+ "",
+ false,
+ "Do health check and exit")
+
+ flags.BoolP(
+ "label-take-precedence",
+ "",
+ envBool("WATCHTOWER_LABEL_TAKE_PRECEDENCE"),
+ "Label applied to containers take precedence over arguments")
}
// RegisterNotificationFlags that are used by watchtower to send notifications
@@ -195,177 +220,202 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) {
flags.StringSliceP(
"notifications",
"n",
- viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"),
+ envStringSlice("WATCHTOWER_NOTIFICATIONS"),
" Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)")
flags.String(
"notifications-level",
- viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"),
+ envString("WATCHTOWER_NOTIFICATIONS_LEVEL"),
"The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug")
flags.IntP(
"notifications-delay",
"",
- viper.GetInt("WATCHTOWER_NOTIFICATIONS_DELAY"),
+ envInt("WATCHTOWER_NOTIFICATIONS_DELAY"),
"Delay before sending notifications, expressed in seconds")
flags.StringP(
"notifications-hostname",
"",
- viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"),
+ envString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"),
"Custom hostname for notification titles")
flags.StringP(
"notification-email-from",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"),
+ envString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"),
"Address to send notification emails from")
flags.StringP(
"notification-email-to",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"),
+ envString("WATCHTOWER_NOTIFICATION_EMAIL_TO"),
"Address to send notification emails to")
flags.IntP(
"notification-email-delay",
"",
- viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"),
+ envInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"),
"Delay before sending notifications, expressed in seconds")
flags.StringP(
"notification-email-server",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"),
+ envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"),
"SMTP server to send notification emails through")
flags.IntP(
"notification-email-server-port",
"",
- viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"),
+ envInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"),
"SMTP server port to send notification emails through")
flags.BoolP(
"notification-email-server-tls-skip-verify",
"",
- viper.GetBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"),
+ envBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"),
`Controls whether watchtower verifies the SMTP server's certificate chain and host name.
Should only be used for testing.`)
flags.StringP(
"notification-email-server-user",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"),
+ envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"),
"SMTP server user for sending notifications")
flags.StringP(
"notification-email-server-password",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"),
+ envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"),
"SMTP server password for sending notifications")
flags.StringP(
"notification-email-subjecttag",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"),
+ envString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"),
"Subject prefix tag for notifications via mail")
flags.StringP(
"notification-slack-hook-url",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"),
+ envString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"),
"The Slack Hook URL to send notifications to")
flags.StringP(
"notification-slack-identifier",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"),
+ envString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"),
"A string which will be used to identify the messages coming from this watchtower instance")
flags.StringP(
"notification-slack-channel",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"),
+ envString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"),
"A string which overrides the webhook's default channel. Example: #my-custom-channel")
flags.StringP(
"notification-slack-icon-emoji",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"),
+ envString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"),
"An emoji code string to use in place of the default icon")
flags.StringP(
"notification-slack-icon-url",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"),
+ envString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"),
"An icon image URL string to use in place of the default icon")
flags.StringP(
"notification-msteams-hook",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"),
+ envString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"),
"The MSTeams WebHook URL to send notifications to")
flags.BoolP(
"notification-msteams-data",
"",
- viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"),
+ envBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"),
"The MSTeams notifier will try to extract log entry fields as MSTeams message facts")
flags.StringP(
"notification-gotify-url",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"),
+ envString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"),
"The Gotify URL to send notifications to")
flags.StringP(
"notification-gotify-token",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"),
+ envString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"),
"The Gotify Application required to query the Gotify API")
flags.BoolP(
"notification-gotify-tls-skip-verify",
"",
- viper.GetBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"),
+ envBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"),
`Controls whether watchtower verifies the Gotify server's certificate chain and host name.
Should only be used for testing.`)
flags.String(
"notification-template",
- viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"),
+ envString("WATCHTOWER_NOTIFICATION_TEMPLATE"),
"The shoutrrr text/template for the messages")
flags.StringArray(
"notification-url",
- viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"),
+ envStringSlice("WATCHTOWER_NOTIFICATION_URL"),
"The shoutrrr URL to send notifications to")
flags.Bool("notification-report",
- viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"),
+ envBool("WATCHTOWER_NOTIFICATION_REPORT"),
"Use the session report as the notification template data")
flags.StringP(
"notification-title-tag",
"",
- viper.GetString("WATCHTOWER_NOTIFICATION_TITLE_TAG"),
+ envString("WATCHTOWER_NOTIFICATION_TITLE_TAG"),
"Title prefix tag for notifications")
flags.Bool("notification-skip-title",
- viper.GetBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"),
+ envBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"),
"Do not pass the title param to notifications")
flags.String(
"warn-on-head-failure",
- viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),
+ envString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),
"When to warn about HEAD pull requests failing. Possible values: always, auto or never")
flags.Bool(
"notification-log-stdout",
- viper.GetBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"),
+ envBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"),
"Write notification logs to stdout instead of logging (to stderr)")
}
+func envString(key string) string {
+ viper.MustBindEnv(key)
+ return viper.GetString(key)
+}
+
+func envStringSlice(key string) []string {
+ viper.MustBindEnv(key)
+ return viper.GetStringSlice(key)
+}
+
+func envInt(key string) int {
+ viper.MustBindEnv(key)
+ return viper.GetInt(key)
+}
+
+func envBool(key string) bool {
+ viper.MustBindEnv(key)
+ return viper.GetBool(key)
+}
+
+func envDuration(key string) time.Duration {
+ viper.MustBindEnv(key)
+ return viper.GetDuration(key)
+}
+
// SetDefaults provides default values for environment variables
func SetDefaults() {
viper.AutomaticEnv()
@@ -379,6 +429,7 @@ func SetDefaults() {
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "")
viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower")
viper.SetDefault("WATCHTOWER_LOG_LEVEL", "info")
+ viper.SetDefault("WATCHTOWER_LOG_FORMAT", "auto")
}
// EnvConfig translates the command-line options into environment variables
@@ -467,14 +518,17 @@ func GetSecretsFromFiles(rootCmd *cobra.Command) {
"notification-msteams-hook",
"notification-gotify-token",
"notification-url",
+ "http-api-token",
}
for _, secret := range secrets {
- getSecretFromFile(flags, secret)
+ if err := getSecretFromFile(flags, secret); err != nil {
+ log.Fatalf("failed to get secret from flag %v: %s", secret, err)
+ }
}
}
// getSecretFromFile will check if the flag contains a reference to a file; if it does, replaces the value of the flag with the contents of the file.
-func getSecretFromFile(flags *pflag.FlagSet, secret string) {
+func getSecretFromFile(flags *pflag.FlagSet, secret string) error {
flag := flags.Lookup(secret)
if sliceValue, ok := flag.Value.(pflag.SliceValue); ok {
oldValues := sliceValue.GetSlice()
@@ -483,7 +537,7 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) {
if value != "" && isFile(value) {
file, err := os.Open(value)
if err != nil {
- log.Fatal(err)
+ return err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
@@ -493,25 +547,26 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) {
}
values = append(values, line)
}
+ if err := file.Close(); err != nil {
+ return err
+ }
} else {
values = append(values, value)
}
}
- sliceValue.Replace(values)
- return
+ return sliceValue.Replace(values)
}
value := flag.Value.String()
if value != "" && isFile(value) {
- file, err := ioutil.ReadFile(value)
+ content, err := os.ReadFile(value)
if err != nil {
- log.Fatal(err)
- }
- err = flags.Set(secret, strings.TrimSpace(string(file)))
- if err != nil {
- log.Error(err)
+ return err
}
+ return flags.Set(secret, strings.TrimSpace(string(content)))
}
+
+ return nil
}
func isFile(s string) bool {
@@ -564,19 +619,59 @@ func ProcessFlagAliases(flags *pflag.FlagSet) {
// update schedule flag to match interval if it's set, or to the default if none of them are
if intervalChanged || !scheduleChanged {
interval, _ := flags.GetInt(`interval`)
- flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
+ _ = flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval))
}
if flagIsEnabled(flags, `debug`) {
- flags.Set(`log-level`, `debug`)
+ _ = flags.Set(`log-level`, `debug`)
}
if flagIsEnabled(flags, `trace`) {
- flags.Set(`log-level`, `trace`)
+ _ = flags.Set(`log-level`, `trace`)
}
}
+// SetupLogging reads only the flags that is needed to set up logging and applies them to the global logger
+func SetupLogging(f *pflag.FlagSet) error {
+ logFormat, _ := f.GetString(`log-format`)
+ noColor, _ := f.GetBool("no-color")
+
+ switch strings.ToLower(logFormat) {
+ case "auto":
+ // This will either use the "pretty" or "logfmt" format, based on whether the standard out is connected to a TTY
+ log.SetFormatter(&log.TextFormatter{
+ DisableColors: noColor,
+ // enable logrus built-in support for https://bixense.com/clicolors/
+ EnvironmentOverrideColors: true,
+ })
+ case "json":
+ log.SetFormatter(&log.JSONFormatter{})
+ case "logfmt":
+ log.SetFormatter(&log.TextFormatter{
+ DisableColors: true,
+ FullTimestamp: true,
+ })
+ case "pretty":
+ log.SetFormatter(&log.TextFormatter{
+ // "Pretty" format combined with `--no-color` will only change the timestamp to the time since start
+ ForceColors: !noColor,
+ FullTimestamp: false,
+ })
+ default:
+ return fmt.Errorf("invalid log format: %s", logFormat)
+ }
+
+ rawLogLevel, _ := f.GetString(`log-level`)
+ if logLevel, err := log.ParseLevel(rawLogLevel); err != nil {
+ return fmt.Errorf("invalid log level: %e", err)
+ } else {
+ log.SetLevel(logLevel)
+ }
+
+ return nil
+}
+
func flagIsEnabled(flags *pflag.FlagSet, name string) bool {
value, err := flags.GetBool(name)
if err != nil {
@@ -593,7 +688,7 @@ func appendFlagValue(flags *pflag.FlagSet, name string, values ...string) error
if flagValues, ok := flag.Value.(pflag.SliceValue); ok {
for _, value := range values {
- flagValues.Append(value)
+ _ = flagValues.Append(value)
}
} else {
return fmt.Errorf(`the value for flag %q is not a slice value`, name)
diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go
index ca6f4ae..2856456 100644
--- a/internal/flags/flags_test.go
+++ b/internal/flags/flags_test.go
@@ -1,20 +1,22 @@
package flags
import (
- "io/ioutil"
"os"
+ "strings"
"testing"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+ "github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEnvConfig_Defaults(t *testing.T) {
// Unset testing environments own variables, since those are not what is under test
- os.Unsetenv("DOCKER_TLS_VERIFY")
- os.Unsetenv("DOCKER_HOST")
+ _ = os.Unsetenv("DOCKER_TLS_VERIFY")
+ _ = os.Unsetenv("DOCKER_HOST")
cmd := new(cobra.Command)
SetDefaults()
@@ -48,10 +50,7 @@ func TestEnvConfig_Custom(t *testing.T) {
func TestGetSecretsFromFilesWithString(t *testing.T) {
value := "supersecretstring"
-
- err := os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
- require.NoError(t, err)
- defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD")
+ t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
testGetSecretsFromFiles(t, "notification-email-server-password", value)
}
@@ -60,18 +59,15 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) {
value := "megasecretstring"
// Create the temporary file which will contain a secret.
- file, err := ioutil.TempFile(os.TempDir(), "watchtower-")
+ file, err := os.CreateTemp(t.TempDir(), "watchtower-")
require.NoError(t, err)
- defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
// Write the secret to the temporary file.
- secret := []byte(value)
- _, err = file.Write(secret)
+ _, err = file.Write([]byte(value))
require.NoError(t, err)
+ require.NoError(t, file.Close())
- err = os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
- require.NoError(t, err)
- defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD")
+ t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
testGetSecretsFromFiles(t, "notification-email-server-password", value)
}
@@ -80,16 +76,15 @@ func TestGetSliceSecretsFromFiles(t *testing.T) {
values := []string{"entry2", "", "entry3"}
// Create the temporary file which will contain a secret.
- file, err := ioutil.TempFile(os.TempDir(), "watchtower-")
+ file, err := os.CreateTemp(t.TempDir(), "watchtower-")
require.NoError(t, err)
- defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
// Write the secret to the temporary file.
for _, value := range values {
_, err = file.WriteString("\n" + value)
require.NoError(t, err)
}
- file.Close()
+ require.NoError(t, file.Close())
testGetSecretsFromFiles(t, "notification-url", `[entry1,entry2,entry3]`,
`--notification-url`, "entry1",
@@ -99,6 +94,7 @@ func TestGetSliceSecretsFromFiles(t *testing.T) {
func testGetSecretsFromFiles(t *testing.T, flagName string, expected string, args ...string) {
cmd := new(cobra.Command)
SetDefaults()
+ RegisterSystemFlags(cmd)
RegisterNotificationFlags(cmd)
require.NoError(t, cmd.ParseFlags(args))
GetSecretsFromFiles(cmd)
@@ -166,9 +162,7 @@ func TestProcessFlagAliases(t *testing.T) {
func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
cmd := new(cobra.Command)
- err := os.Setenv("WATCHTOWER_DEBUG", `true`)
- require.NoError(t, err)
- defer os.Unsetenv("WATCHTOWER_DEBUG")
+ t.Setenv("WATCHTOWER_DEBUG", `true`)
SetDefaults()
RegisterDockerFlags(cmd)
@@ -183,6 +177,57 @@ func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) {
assert.Equal(t, `debug`, logLevel)
}
+func TestLogFormatFlag(t *testing.T) {
+ cmd := new(cobra.Command)
+
+ SetDefaults()
+ RegisterDockerFlags(cmd)
+ RegisterSystemFlags(cmd)
+
+ // Ensure the default value is Auto
+ require.NoError(t, cmd.ParseFlags([]string{}))
+ require.NoError(t, SetupLogging(cmd.Flags()))
+ assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)
+
+ // Test JSON format
+ require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `JSON`}))
+ require.NoError(t, SetupLogging(cmd.Flags()))
+ assert.IsType(t, &logrus.JSONFormatter{}, logrus.StandardLogger().Formatter)
+
+ // Test Pretty format
+ require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `pretty`}))
+ require.NoError(t, SetupLogging(cmd.Flags()))
+ assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter)
+ textFormatter, ok := (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)
+ assert.True(t, ok)
+ assert.True(t, textFormatter.ForceColors)
+ assert.False(t, textFormatter.FullTimestamp)
+
+ // Test LogFmt format
+ require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `logfmt`}))
+ require.NoError(t, SetupLogging(cmd.Flags()))
+ textFormatter, ok = (logrus.StandardLogger().Formatter).(*logrus.TextFormatter)
+ assert.True(t, ok)
+ assert.True(t, textFormatter.DisableColors)
+ assert.True(t, textFormatter.FullTimestamp)
+
+ // Test invalid format
+ require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `cowsay`}))
+ require.Error(t, SetupLogging(cmd.Flags()))
+}
+
+func TestLogLevelFlag(t *testing.T) {
+ cmd := new(cobra.Command)
+
+ SetDefaults()
+ RegisterDockerFlags(cmd)
+ RegisterSystemFlags(cmd)
+
+ // Test invalid format
+ require.NoError(t, cmd.ParseFlags([]string{`--log-level`, `gossip`}))
+ require.Error(t, SetupLogging(cmd.Flags()))
+}
+
func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) }
cmd := new(cobra.Command)
@@ -202,9 +247,7 @@ func TestProcessFlagAliasesSchedAndInterval(t *testing.T) {
func TestProcessFlagAliasesScheduleFromEnvironment(t *testing.T) {
cmd := new(cobra.Command)
- err := os.Setenv("WATCHTOWER_SCHEDULE", `@hourly`)
- require.NoError(t, err)
- defer os.Unsetenv("WATCHTOWER_SCHEDULE")
+ t.Setenv("WATCHTOWER_SCHEDULE", `@hourly`)
SetDefaults()
RegisterDockerFlags(cmd)
@@ -234,3 +277,63 @@ func TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) {
ProcessFlagAliases(flags)
})
}
+
+func TestFlagsArePrecentInDocumentation(t *testing.T) {
+
+ // Legacy notifcations are ignored, since they are (soft) deprecated
+ ignoredEnvs := map[string]string{
+ "WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI": "legacy",
+ "WATCHTOWER_NOTIFICATION_SLACK_ICON_URL": "legacy",
+ }
+
+ ignoredFlags := map[string]string{
+ "notification-gotify-url": "legacy",
+ "notification-slack-icon-emoji": "legacy",
+ "notification-slack-icon-url": "legacy",
+ }
+
+ cmd := new(cobra.Command)
+ SetDefaults()
+ RegisterDockerFlags(cmd)
+ RegisterSystemFlags(cmd)
+ RegisterNotificationFlags(cmd)
+
+ flags := cmd.PersistentFlags()
+
+ docFiles := []string{
+ "../../docs/arguments.md",
+ "../../docs/lifecycle-hooks.md",
+ "../../docs/notifications.md",
+ }
+ allDocs := ""
+ for _, f := range docFiles {
+ bytes, err := os.ReadFile(f)
+ if err != nil {
+ t.Fatalf("Could not load docs file %q: %v", f, err)
+ }
+ allDocs += string(bytes)
+ }
+
+ flags.VisitAll(func(f *pflag.Flag) {
+ if !strings.Contains(allDocs, "--"+f.Name) {
+ if _, found := ignoredFlags[f.Name]; !found {
+ t.Logf("Docs does not mention flag long name %q", f.Name)
+ t.Fail()
+ }
+ }
+ if !strings.Contains(allDocs, "-"+f.Shorthand) {
+ t.Logf("Docs does not mention flag shorthand %q (%q)", f.Shorthand, f.Name)
+ t.Fail()
+ }
+ })
+
+ for _, key := range viper.AllKeys() {
+ envKey := strings.ToUpper(key)
+ if !strings.Contains(allDocs, envKey) {
+ if _, found := ignoredEnvs[envKey]; !found {
+ t.Logf("Docs does not mention environment variable %q", envKey)
+ t.Fail()
+ }
+ }
+ }
+}
diff --git a/internal/util/rand_sha256.go b/internal/util/rand_sha256.go
new file mode 100644
index 0000000..38a3736
--- /dev/null
+++ b/internal/util/rand_sha256.go
@@ -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()
+}
diff --git a/internal/util/util_test.go b/internal/util/util_test.go
index a6dd657..0b2c36c 100644
--- a/internal/util/util_test.go
+++ b/internal/util/util_test.go
@@ -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)
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index f87708f..5227004 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -48,6 +48,7 @@ nav:
- 'Stop signals': 'stop-signals.md'
- 'Lifecycle hooks': 'lifecycle-hooks.md'
- 'Running multiple instances': 'running-multiple-instances.md'
+ - 'HTTP API Mode': 'http-api-mode.md'
- 'Metrics': 'metrics.md'
plugins:
- search
diff --git a/oryxBuildBinary b/oryxBuildBinary
new file mode 100755
index 0000000..86cbe57
Binary files /dev/null and b/oryxBuildBinary differ
diff --git a/pkg/api/metrics/metrics_test.go b/pkg/api/metrics/metrics_test.go
index 5120f8d..48b6dd7 100644
--- a/pkg/api/metrics/metrics_test.go
+++ b/pkg/api/metrics/metrics_test.go
@@ -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] == '#' {
diff --git a/pkg/container/client.go b/pkg/container/client.go
index f534bd0..c6c37de 100644
--- a/pkg/container/client.go
+++ b/pkg/container/client.go
@@ -3,14 +3,10 @@ package container
import (
"bytes"
"fmt"
- "io/ioutil"
+ "io"
"strings"
"time"
- "github.com/containrrr/watchtower/pkg/registry"
- "github.com/containrrr/watchtower/pkg/registry/digest"
-
- t "github.com/containrrr/watchtower/pkg/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
@@ -18,6 +14,10 @@ import (
sdkClient "github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
"golang.org/x/net/context"
+
+ "github.com/containrrr/watchtower/pkg/registry"
+ "github.com/containrrr/watchtower/pkg/registry/digest"
+ t "github.com/containrrr/watchtower/pkg/types"
)
const defaultStopSignal = "SIGTERM"
@@ -25,23 +25,23 @@ const defaultStopSignal = "SIGTERM"
// A Client is the interface through which watchtower interacts with the
// Docker API.
type Client interface {
- ListContainers(t.Filter) ([]Container, error)
- GetContainer(containerID t.ContainerID) (Container, error)
- StopContainer(Container, time.Duration) error
- StartContainer(Container) (t.ContainerID, error)
- RenameContainer(Container, string) error
- IsContainerStale(Container) (stale bool, latestImage t.ImageID, err error)
+ ListContainers(t.Filter) ([]t.Container, error)
+ GetContainer(containerID t.ContainerID) (t.Container, error)
+ StopContainer(t.Container, time.Duration) error
+ StartContainer(t.Container) (t.ContainerID, error)
+ RenameContainer(t.Container, string) error
+ IsContainerStale(t.Container, t.UpdateParams) (stale bool, latestImage t.ImageID, err error)
ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error)
RemoveImageByID(t.ImageID) error
- WarnOnHeadPullFailed(container Container) bool
+ WarnOnHeadPullFailed(container t.Container) bool
}
// NewClient returns a new Client instance which can be used to interact with
// the Docker API.
// The client reads its configuration from the following environment variables:
-// * DOCKER_HOST the docker-engine host to send api requests to
-// * DOCKER_TLS_VERIFY whether to verify tls certificates
-// * DOCKER_API_VERSION the minimum docker api version to work with
+// - DOCKER_HOST the docker-engine host to send api requests to
+// - DOCKER_TLS_VERIFY whether to verify tls certificates
+// - DOCKER_API_VERSION the minimum docker api version to work with
func NewClient(opts ClientOptions) Client {
cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv)
@@ -57,7 +57,6 @@ func NewClient(opts ClientOptions) Client {
// ClientOptions contains the options for how the docker client wrapper should behave
type ClientOptions struct {
- PullImages bool
RemoveVolumes bool
IncludeStopped bool
ReviveStopped bool
@@ -82,7 +81,7 @@ type dockerClient struct {
ClientOptions
}
-func (client dockerClient) WarnOnHeadPullFailed(container Container) bool {
+func (client dockerClient) WarnOnHeadPullFailed(container t.Container) bool {
if client.WarnOnHeadFailed == WarnAlways {
return true
}
@@ -93,8 +92,8 @@ func (client dockerClient) WarnOnHeadPullFailed(container Container) bool {
return registry.WarnOnAPIConsumption(container)
}
-func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) {
- cs := []Container{}
+func (client dockerClient) ListContainers(fn t.Filter) ([]t.Container, error) {
+ cs := []t.Container{}
bg := context.Background()
if client.IncludeStopped && client.IncludeRestarting {
@@ -149,24 +148,40 @@ func (client dockerClient) createListFilter() filters.Args {
return filterArgs
}
-func (client dockerClient) GetContainer(containerID t.ContainerID) (Container, error) {
+func (client dockerClient) GetContainer(containerID t.ContainerID) (t.Container, error) {
bg := context.Background()
containerInfo, err := client.api.ContainerInspect(bg, string(containerID))
if err != nil {
- return Container{}, err
+ return &Container{}, err
+ }
+
+ netType, netContainerId, found := strings.Cut(string(containerInfo.HostConfig.NetworkMode), ":")
+ if found && netType == "container" {
+ parentContainer, err := client.api.ContainerInspect(bg, netContainerId)
+ if err != nil {
+ log.WithFields(map[string]interface{}{
+ "container": containerInfo.Name,
+ "error": err,
+ "network-container": netContainerId,
+ }).Warnf("Unable to resolve network container: %v", err)
+
+ } else {
+ // Replace the container ID with a container name to allow it to reference the re-created network container
+ containerInfo.HostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf("container:%s", parentContainer.Name))
+ }
}
imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
if err != nil {
log.Warnf("Failed to retrieve container image info: %v", err)
- return Container{containerInfo: &containerInfo, imageInfo: nil}, nil
+ return &Container{containerInfo: &containerInfo, imageInfo: nil}, nil
}
- return Container{containerInfo: &containerInfo, imageInfo: &imageInfo}, nil
+ return &Container{containerInfo: &containerInfo, imageInfo: &imageInfo}, nil
}
-func (client dockerClient) StopContainer(c Container, timeout time.Duration) error {
+func (client dockerClient) StopContainer(c t.Container, timeout time.Duration) error {
bg := context.Background()
signal := c.StopSignal()
if signal == "" {
@@ -186,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 sdkClient.IsErrNotFound(err) {
+ log.Debugf("Container %s not found, skipping removal.", shortID)
+ return nil
+ }
return err
}
}
@@ -204,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 {
@@ -224,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
@@ -256,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())
@@ -267,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
@@ -285,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)
@@ -306,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()
@@ -355,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
}
@@ -365,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
}
@@ -474,7 +538,7 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e
return false, nil
}
-func (client dockerClient) waitForStopOrTimeout(c Container, waitTime time.Duration) error {
+func (client dockerClient) waitForStopOrTimeout(c t.Container, waitTime time.Duration) error {
bg := context.Background()
timeout := time.After(waitTime)
diff --git a/pkg/container/client_test.go b/pkg/container/client_test.go
index 02b31eb..4e75409 100644
--- a/pkg/container/client_test.go
+++ b/pkg/container/client_test.go
@@ -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,6 +12,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
cli "github.com/docker/docker/client"
+ "github.com/docker/docker/errdefs"
"github.com/onsi/gomega/gbytes"
"github.com/onsi/gomega/ghttp"
"github.com/sirupsen/logrus"
@@ -33,8 +38,8 @@ var _ = Describe("the client", func() {
mockServer.Close()
})
Describe("WarnOnHeadPullFailed", func() {
- containerUnknown := *mockContainerWithImageName("unknown.repo/prefix/imagename:latest")
- containerKnown := *mockContainerWithImageName("docker.io/prefix/imagename:latest")
+ containerUnknown := MockContainer(WithImageName("unknown.repo/prefix/imagename:latest"))
+ containerKnown := MockContainer(WithImageName("docker.io/prefix/imagename:latest"))
When(`warn on head failure is set to "always"`, func() {
c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}}
@@ -64,8 +69,72 @@ var _ = Describe("the client", func() {
When("the image consist of a pinned hash", func() {
It("should gracefully fail with a useful message", func() {
c := dockerClient{}
- pinnedContainer := *mockContainerWithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b")
- c.PullImage(context.Background(), pinnedContainer)
+ pinnedContainer := MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b"))
+ err := c.PullImage(context.Background(), pinnedContainer)
+ Expect(err).To(MatchError(`container uses a pinned image, and cannot be updated by watchtower`))
+ })
+ })
+ })
+ When("removing a running container", func() {
+ When("the container still exist after stopping", func() {
+ It("should attempt to remove the container", func() {
+ container := MockContainer(WithContainerState(types.ContainerState{Running: true}))
+ containerStopped := MockContainer(WithContainerState(types.ContainerState{Running: false}))
+
+ 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())
})
})
})
@@ -73,10 +142,10 @@ var _ = Describe("the client", func() {
When("no filter is provided", func() {
It("should return all available containers", func() {
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
- mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
+ mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
client := dockerClient{
api: docker,
- ClientOptions: ClientOptions{PullImages: false},
+ ClientOptions: ClientOptions{},
}
containers, err := client.ListContainers(filters.NoFilter)
Expect(err).NotTo(HaveOccurred())
@@ -86,11 +155,11 @@ var _ = Describe("the client", func() {
When("a filter matching nothing", func() {
It("should return an empty array", func() {
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
- mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
+ mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
filter := filters.FilterByNames([]string{"lollercoaster"}, filters.NoFilter)
client := dockerClient{
api: docker,
- ClientOptions: ClientOptions{PullImages: false},
+ ClientOptions: ClientOptions{},
}
containers, err := client.ListContainers(filter)
Expect(err).NotTo(HaveOccurred())
@@ -100,10 +169,10 @@ var _ = Describe("the client", func() {
When("a watchtower filter is provided", func() {
It("should return only the watchtower container", func() {
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
- mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
+ mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
client := dockerClient{
api: docker,
- ClientOptions: ClientOptions{PullImages: false},
+ ClientOptions: ClientOptions{},
}
containers, err := client.ListContainers(filters.WatchtowerContainersFilter)
Expect(err).NotTo(HaveOccurred())
@@ -113,10 +182,10 @@ var _ = Describe("the client", func() {
When(`include stopped is enabled`, func() {
It("should return both stopped and running containers", func() {
mockServer.AppendHandlers(mocks.ListContainersHandler("running", "exited", "created"))
- mockServer.AppendHandlers(mocks.GetContainerHandlers("stopped", "watchtower", "running")...)
+ mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Stopped, &mocks.Watchtower, &mocks.Running)...)
client := dockerClient{
api: docker,
- ClientOptions: ClientOptions{PullImages: false, IncludeStopped: true},
+ ClientOptions: ClientOptions{IncludeStopped: true},
}
containers, err := client.ListContainers(filters.NoFilter)
Expect(err).NotTo(HaveOccurred())
@@ -126,10 +195,10 @@ var _ = Describe("the client", func() {
When(`include restarting is enabled`, func() {
It("should return both restarting and running containers", func() {
mockServer.AppendHandlers(mocks.ListContainersHandler("running", "restarting"))
- mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running", "restarting")...)
+ mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running, &mocks.Restarting)...)
client := dockerClient{
api: docker,
- ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: true},
+ ClientOptions: ClientOptions{IncludeRestarting: true},
}
containers, err := client.ListContainers(filters.NoFilter)
Expect(err).NotTo(HaveOccurred())
@@ -139,30 +208,58 @@ var _ = Describe("the client", func() {
When(`include restarting is disabled`, func() {
It("should not return restarting containers", func() {
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
- mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
+ mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
client := dockerClient{
api: docker,
- ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: false},
+ ClientOptions: ClientOptions{IncludeRestarting: false},
}
containers, err := client.ListContainers(filters.NoFilter)
Expect(err).NotTo(HaveOccurred())
Expect(containers).NotTo(ContainElement(havingRestartingState(true)))
})
})
+ When(`a container uses container network mode`, func() {
+ When(`the network container can be resolved`, func() {
+ It("should return the container name instead of the ID", func() {
+ consumerContainerRef := mocks.NetConsumerOK
+ mockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...)
+ client := dockerClient{
+ api: docker,
+ ClientOptions: ClientOptions{},
+ }
+ container, err := client.GetContainer(consumerContainerRef.ContainerID())
+ Expect(err).NotTo(HaveOccurred())
+ networkMode := container.ContainerInfo().HostConfig.NetworkMode
+ Expect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierContainerName))
+ })
+ })
+ When(`the network container cannot be resolved`, func() {
+ It("should still return the container ID", func() {
+ consumerContainerRef := mocks.NetConsumerInvalidSupplier
+ mockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...)
+ client := dockerClient{
+ api: docker,
+ ClientOptions: ClientOptions{},
+ }
+ container, err := client.GetContainer(consumerContainerRef.ContainerID())
+ Expect(err).NotTo(HaveOccurred())
+ networkMode := container.ContainerInfo().HostConfig.NetworkMode
+ Expect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierNotFoundID))
+ })
+ })
+ })
})
Describe(`ExecuteCommand`, func() {
When(`logging`, func() {
It("should include container id field", func() {
client := dockerClient{
api: docker,
- ClientOptions: ClientOptions{PullImages: false},
+ ClientOptions: ClientOptions{},
}
// Capture logrus output in buffer
- logbuf := gbytes.NewBuffer()
- origOut := logrus.StandardLogger().Out
- defer logrus.SetOutput(origOut)
- logrus.SetOutput(logbuf)
+ resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
+ defer resetLogrus()
user := ""
containerID := t.ContainerID("ex-cont-id")
@@ -219,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 gt.GomegaMatcher) gt.GomegaMatcher {
return WithTransform(containerImageName, matcher)
}
-func containerImageName(container Container) string {
+func containerImageName(container t.Container) string {
return container.ImageName()
}
func havingRestartingState(expected bool) gt.GomegaMatcher {
- return WithTransform(func(container Container) bool {
- return container.containerInfo.State.Restarting
+ return WithTransform(func(container t.Container) bool {
+ return container.ContainerInfo().State.Restarting
}, Equal(expected))
}
func havingRunningState(expected bool) gt.GomegaMatcher {
- return WithTransform(func(container Container) bool {
- return container.containerInfo.State.Running
+ return WithTransform(func(container t.Container) bool {
+ return container.ContainerInfo().State.Running
}, Equal(expected))
}
diff --git a/pkg/container/container.go b/pkg/container/container.go
index 0bbea16..10ed677 100644
--- a/pkg/container/container.go
+++ b/pkg/container/container.go
@@ -2,12 +2,14 @@
package container
import (
+ "errors"
"fmt"
"strconv"
"strings"
"github.com/containrrr/watchtower/internal/util"
wt "github.com/containrrr/watchtower/pkg/types"
+ "github.com/sirupsen/logrus"
"github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
@@ -32,6 +34,26 @@ type Container struct {
imageInfo *types.ImageInspect
}
+// IsLinkedToRestarting returns the current value of the LinkedToRestarting field for the container
+func (c *Container) IsLinkedToRestarting() bool {
+ return c.LinkedToRestarting
+}
+
+// IsStale returns the current value of the Stale field for the container
+func (c *Container) IsStale() bool {
+ return c.Stale
+}
+
+// SetLinkedToRestarting sets the LinkedToRestarting field for the container
+func (c *Container) SetLinkedToRestarting(value bool) {
+ c.LinkedToRestarting = value
+}
+
+// SetStale implements sets the Stale field for the container
+func (c *Container) SetStale(value bool) {
+ c.Stale = value
+}
+
// ContainerInfo fetches JSON info for the container
func (c Container) ContainerInfo() *types.ContainerJSON {
return c.containerInfo
@@ -109,20 +131,31 @@ func (c Container) Enabled() (bool, bool) {
return parsedBool, true
}
-// IsMonitorOnly returns the value of the monitor-only label. If the label
-// is not set then false is returned.
-func (c Container) IsMonitorOnly() bool {
- rawBool, ok := c.getLabelValue(monitorOnlyLabel)
- if !ok {
- return false
- }
+// IsMonitorOnly returns whether the container should only be monitored based on values of
+// the monitor-only label, the monitor-only argument and the label-take-precedence argument.
+func (c Container) IsMonitorOnly(params wt.UpdateParams) bool {
+ return c.getContainerOrGlobalBool(params.MonitorOnly, monitorOnlyLabel, params.LabelPrecedence)
+}
- parsedBool, err := strconv.ParseBool(rawBool)
- if err != nil {
- return false
- }
+// IsNoPull returns whether the image should be pulled based on values of
+// the no-pull label, the no-pull argument and the label-take-precedence argument.
+func (c Container) IsNoPull(params wt.UpdateParams) bool {
+ return c.getContainerOrGlobalBool(params.NoPull, noPullLabel, params.LabelPrecedence)
+}
- return parsedBool
+func (c Container) getContainerOrGlobalBool(globalVal bool, label string, contPrecedence bool) bool {
+ if contVal, err := c.getBoolLabelValue(label); err != nil {
+ if !errors.Is(err, errorLabelNotFound) {
+ logrus.WithField("error", err).WithField("label", label).Warn("Failed to parse label value")
+ }
+ return globalVal
+ } else {
+ if contPrecedence {
+ return contVal
+ } else {
+ return contVal || globalVal
+ }
+ }
}
// Scope returns the value of the scope UID label and if the label
@@ -144,7 +177,14 @@ func (c Container) Links() []string {
dependsOnLabelValue := c.getLabelValueOrEmpty(dependsOnLabel)
if dependsOnLabelValue != "" {
- links := strings.Split(dependsOnLabelValue, ",")
+ for _, link := range strings.Split(dependsOnLabelValue, ",") {
+ // Since the container names need to start with '/', let's prepend it if it's missing
+ if !strings.HasPrefix(link, "/") {
+ link = "/" + link
+ }
+ links = append(links, link)
+ }
+
return links
}
@@ -153,6 +193,13 @@ func (c Container) Links() []string {
name := strings.Split(link, ":")[0]
links = append(links, name)
}
+
+ // If the container uses another container for networking, it can be considered an implicit link
+ // since the container would stop working if the network supplier were to be recreated
+ networkMode := c.containerInfo.HostConfig.NetworkMode
+ if networkMode.IsContainer() {
+ links = append(links, networkMode.ConnectedContainer())
+ }
}
return links
@@ -217,18 +264,23 @@ func (c Container) StopSignal() string {
return c.getLabelValueOrEmpty(signalLabel)
}
+// GetCreateConfig returns the container's current Config converted into a format
+// that can be re-submitted to the Docker create API.
+//
// Ideally, we'd just be able to take the ContainerConfig from the old container
// and use it as the starting point for creating the new container; however,
// the ContainerConfig that comes back from the Inspect call merges the default
// configuration (the stuff specified in the metadata for the image itself)
// with the overridden configuration (the stuff that you might specify as part
-// of the "docker run"). In order to avoid unintentionally overriding the
+// of the "docker run").
+//
+// In order to avoid unintentionally overriding the
// defaults in the new image we need to separate the override options from the
// default options. To do this we have to compare the ContainerConfig for the
// running container with the ContainerConfig from the image that container was
// started from. This function returns a ContainerConfig which contains just
// the options overridden at runtime.
-func (c Container) runtimeConfig() *dockercontainer.Config {
+func (c Container) GetCreateConfig() *dockercontainer.Config {
config := c.containerInfo.Config
hostConfig := c.containerInfo.HostConfig
imageConfig := c.imageInfo.Config
@@ -252,6 +304,29 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
}
}
+ // Clear HEALTHCHECK configuration (if default)
+ if config.Healthcheck != nil && imageConfig.Healthcheck != nil {
+ if util.SliceEqual(config.Healthcheck.Test, imageConfig.Healthcheck.Test) {
+ config.Healthcheck.Test = nil
+ }
+
+ if config.Healthcheck.Retries == imageConfig.Healthcheck.Retries {
+ config.Healthcheck.Retries = 0
+ }
+
+ if config.Healthcheck.Interval == imageConfig.Healthcheck.Interval {
+ config.Healthcheck.Interval = 0
+ }
+
+ if config.Healthcheck.Timeout == imageConfig.Healthcheck.Timeout {
+ config.Healthcheck.Timeout = 0
+ }
+
+ if config.Healthcheck.StartPeriod == imageConfig.Healthcheck.StartPeriod {
+ config.Healthcheck.StartPeriod = 0
+ }
+ }
+
config.Env = util.SliceSubtract(config.Env, imageConfig.Env)
config.Labels = util.StringMapSubtract(config.Labels, imageConfig.Labels)
@@ -272,9 +347,9 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
return config
}
-// Any links in the HostConfig need to be re-written before they can be
-// re-submitted to the Docker create API.
-func (c Container) hostConfig() *dockercontainer.HostConfig {
+// GetCreateHostConfig returns the container's current HostConfig with any links
+// re-written so that they can be re-submitted to the Docker create API.
+func (c Container) GetCreateHostConfig() *dockercontainer.HostConfig {
hostConfig := c.containerInfo.HostConfig
for i, link := range hostConfig.Links {
diff --git a/pkg/container/container_mock_test.go b/pkg/container/container_mock_test.go
new file mode 100644
index 0000000..8aa1470
--- /dev/null
+++ b/pkg/container/container_mock_test.go
@@ -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
+ }
+}
diff --git a/pkg/container/container_test.go b/pkg/container/container_test.go
index 6cd5c86..a129afe 100644
--- a/pkg/container/container_test.go
+++ b/pkg/container/container_test.go
@@ -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,45 +263,123 @@ 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{
+ c = MockContainer(WithLabels(map[string]string{
"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "3",
"com.centurylinklabs.watchtower.lifecycle.post-update-timeout": "5",
- })
+ }))
preTimeout := c.PreUpdateTimeout()
Expect(preTimeout).To(Equal(3))
postTimeout := c.PostUpdateTimeout()
@@ -232,53 +389,3 @@ var _ = Describe("the container", func() {
})
})
-
-func mockContainerWithPortBindings(portBindingSources ...string) *Container {
- mockContainer := mockContainerWithLabels(nil)
- mockContainer.imageInfo = &types.ImageInspect{}
- hostConfig := &container.HostConfig{
- PortBindings: nat.PortMap{},
- }
- for _, pbs := range portBindingSources {
- hostConfig.PortBindings[nat.Port(pbs)] = []nat.PortBinding{}
- }
- mockContainer.containerInfo.HostConfig = hostConfig
- return mockContainer
-}
-
-func mockContainerWithImageName(name string) *Container {
- mockContainer := mockContainerWithLabels(nil)
- mockContainer.containerInfo.Config.Image = name
- return mockContainer
-}
-
-func mockContainerWithLinks(links []string) *Container {
- content := types.ContainerJSON{
- ContainerJSONBase: &types.ContainerJSONBase{
- ID: "container_id",
- Image: "image",
- Name: "test-containrrr",
- HostConfig: &container.HostConfig{
- Links: links,
- },
- },
- Config: &container.Config{
- Labels: map[string]string{},
- },
- }
- return NewContainer(&content, nil)
-}
-
-func mockContainerWithLabels(labels map[string]string) *Container {
- content := types.ContainerJSON{
- ContainerJSONBase: &types.ContainerJSONBase{
- ID: "container_id",
- Image: "image",
- Name: "test-containrrr",
- },
- Config: &container.Config{
- Labels: labels,
- },
- }
- return NewContainer(&content, nil)
-}
diff --git a/pkg/container/errors.go b/pkg/container/errors.go
index 0b72067..05dc722 100644
--- a/pkg/container/errors.go
+++ b/pkg/container/errors.go
@@ -5,3 +5,4 @@ import "errors"
var errorNoImageInfo = errors.New("no available image info")
var errorNoContainerInfo = errors.New("no available container info")
var errorInvalidConfig = errors.New("container configuration missing or invalid")
+var errorLabelNotFound = errors.New("label was not found in container")
diff --git a/pkg/container/metadata.go b/pkg/container/metadata.go
index ee9fddf..8ac5f34 100644
--- a/pkg/container/metadata.go
+++ b/pkg/container/metadata.go
@@ -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
+}
diff --git a/pkg/container/mocks/ApiServer.go b/pkg/container/mocks/ApiServer.go
index 20610cd..84756f0 100644
--- a/pkg/container/mocks/ApiServer.go
+++ b/pkg/container/mocks/ApiServer.go
@@ -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)
+ }
+ },
+ )
}
diff --git a/pkg/container/mocks/container_ref.go b/pkg/container/mocks/container_ref.go
new file mode 100644
index 0000000..c46eb93
--- /dev/null
+++ b/pkg/container/mocks/container_ref.go
@@ -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
+}
diff --git a/pkg/container/mocks/data/container_net_consumer-missing_supplier.json b/pkg/container/mocks/data/container_net_consumer-missing_supplier.json
new file mode 100644
index 0000000..c1a233b
--- /dev/null
+++ b/pkg/container/mocks/data/container_net_consumer-missing_supplier.json
@@ -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": {}
+ }
+}
diff --git a/pkg/container/mocks/data/container_net_consumer.json b/pkg/container/mocks/data/container_net_consumer.json
new file mode 100644
index 0000000..2e64f89
--- /dev/null
+++ b/pkg/container/mocks/data/container_net_consumer.json
@@ -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": {}
+ }
+}
diff --git a/pkg/container/mocks/data/container_net_supplier.json b/pkg/container/mocks/data/container_net_supplier.json
new file mode 100644
index 0000000..24db841
--- /dev/null
+++ b/pkg/container/mocks/data/container_net_supplier.json
@@ -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
+ }
+ }
+ }
+}
diff --git a/pkg/container/mocks/data/image01.json b/pkg/container/mocks/data/image_default.json
similarity index 100%
rename from pkg/container/mocks/data/image01.json
rename to pkg/container/mocks/data/image_default.json
diff --git a/pkg/container/mocks/data/image_net_consumer.json b/pkg/container/mocks/data/image_net_consumer.json
new file mode 100644
index 0000000..add6edf
--- /dev/null
+++ b/pkg/container/mocks/data/image_net_consumer.json
@@ -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 "
+ },
+ "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 "
+ },
+ "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"
+ }
+}
diff --git a/pkg/container/mocks/data/image_net_producer.json b/pkg/container/mocks/data/image_net_producer.json
new file mode 100644
index 0000000..563ad95
--- /dev/null
+++ b/pkg/container/mocks/data/image_net_producer.json
@@ -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"
+ }
+}
diff --git a/pkg/container/mocks/data/image02.json b/pkg/container/mocks/data/image_running.json
similarity index 100%
rename from pkg/container/mocks/data/image02.json
rename to pkg/container/mocks/data/image_running.json
diff --git a/pkg/filters/filters.go b/pkg/filters/filters.go
index fa5ed2a..4fa0bcd 100644
--- a/pkg/filters/filters.go
+++ b/pkg/filters/filters.go
@@ -13,7 +13,7 @@ func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatch
// NoFilter will not filter out any containers
func NoFilter(t.FilterableContainer) bool { return true }
-// FilterByNames returns all containers that match the specified name
+// FilterByNames returns all containers that match one of the specified names
func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
if len(names) == 0 {
return baseFilter
@@ -41,6 +41,22 @@ func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
}
}
+// FilterByDisableNames returns all containers that don't match any of the specified names
+func FilterByDisableNames(disableNames []string, baseFilter t.Filter) t.Filter {
+ if len(disableNames) == 0 {
+ return baseFilter
+ }
+
+ return func(c t.FilterableContainer) bool {
+ for _, name := range disableNames {
+ if name == c.Name() || name == c.Name()[1:] {
+ return false
+ }
+ }
+ return baseFilter(c)
+ }
+}
+
// FilterByEnableLabel returns all containers that have the enabled label set
func FilterByEnableLabel(baseFilter t.Filter) t.Filter {
return func(c t.FilterableContainer) bool {
@@ -70,13 +86,14 @@ func FilterByDisabledLabel(baseFilter t.Filter) t.Filter {
// FilterByScope returns all containers that belongs to a specific scope
func FilterByScope(scope string, baseFilter t.Filter) t.Filter {
- if scope == "" {
- return baseFilter
- }
-
return func(c t.FilterableContainer) bool {
- containerScope, ok := c.Scope()
- if ok && containerScope == scope {
+ containerScope, containerHasScope := c.Scope()
+
+ if !containerHasScope || containerScope == "" {
+ containerScope = "none"
+ }
+
+ if containerScope == scope {
return baseFilter(c)
}
@@ -103,10 +120,11 @@ func FilterByImage(images []string, baseFilter t.Filter) t.Filter {
}
// BuildFilter creates the needed filter of containers
-func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, string) {
+func BuildFilter(names []string, disableNames []string, enableLabel bool, scope string) (t.Filter, string) {
sb := strings.Builder{}
filter := NoFilter
filter = FilterByNames(names, filter)
+ filter = FilterByDisableNames(disableNames, filter)
if len(names) > 0 {
sb.WriteString("which name matches \"")
@@ -118,6 +136,16 @@ func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, stri
}
sb.WriteString(`", `)
}
+ if len(disableNames) > 0 {
+ sb.WriteString("not named one of \"")
+ for i, n := range disableNames {
+ sb.WriteString(n)
+ if i < len(disableNames)-1 {
+ sb.WriteString(`" or "`)
+ }
+ }
+ sb.WriteString(`", `)
+ }
if enableLabel {
// If label filtering is enabled, containers should only be considered
@@ -125,7 +153,13 @@ func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, stri
filter = FilterByEnableLabel(filter)
sb.WriteString("using enable label, ")
}
- if scope != "" {
+
+ if scope == "none" {
+ // If a scope has explicitly defined as "none", containers should only be considered
+ // if they do not have a scope defined, or if it's explicitly set to "none".
+ filter = FilterByScope(scope, filter)
+ sb.WriteString(`without a scope, "`)
+ } else if scope != "" {
// If a scope has been defined, containers should only be considered
// if the scope is specifically set.
filter = FilterByScope(scope, filter)
diff --git a/pkg/filters/filters_test.go b/pkg/filters/filters_test.go
index 951e7ca..2b5cb5e 100644
--- a/pkg/filters/filters_test.go
+++ b/pkg/filters/filters_test.go
@@ -111,6 +111,53 @@ func TestFilterByScope(t *testing.T) {
container.AssertExpectations(t)
}
+func TestFilterByNoneScope(t *testing.T) {
+ scope := "none"
+
+ filter := FilterByScope(scope, NoFilter)
+ assert.NotNil(t, filter)
+
+ container := new(mocks.FilterableContainer)
+ container.On("Scope").Return("anyscope", true)
+ assert.False(t, filter(container))
+ container.AssertExpectations(t)
+
+ container = new(mocks.FilterableContainer)
+ container.On("Scope").Return("", false)
+ assert.True(t, filter(container))
+ container.AssertExpectations(t)
+
+ container = new(mocks.FilterableContainer)
+ container.On("Scope").Return("", true)
+ assert.True(t, filter(container))
+ container.AssertExpectations(t)
+
+ container = new(mocks.FilterableContainer)
+ container.On("Scope").Return("none", true)
+ assert.True(t, filter(container))
+ container.AssertExpectations(t)
+}
+
+func TestBuildFilterNoneScope(t *testing.T) {
+ filter, desc := BuildFilter(nil, nil, false, "none")
+
+ assert.Contains(t, desc, "without a scope")
+
+ scoped := new(mocks.FilterableContainer)
+ scoped.On("Enabled").Return(false, false)
+ scoped.On("Scope").Return("anyscope", true)
+
+ unscoped := new(mocks.FilterableContainer)
+ unscoped.On("Enabled").Return(false, false)
+ unscoped.On("Scope").Return("", false)
+
+ assert.False(t, filter(scoped))
+ assert.True(t, filter(unscoped))
+
+ scoped.AssertExpectations(t)
+ unscoped.AssertExpectations(t)
+}
+
func TestFilterByDisabledLabel(t *testing.T) {
filter := FilterByDisabledLabel(NoFilter)
assert.NotNil(t, filter)
@@ -171,7 +218,7 @@ func TestFilterByImage(t *testing.T) {
func TestBuildFilter(t *testing.T) {
names := []string{"test", "valid"}
- filter, desc := BuildFilter(names, false, "")
+ filter, desc := BuildFilter(names, []string{}, false, "")
assert.Contains(t, desc, "test")
assert.Contains(t, desc, "or")
assert.Contains(t, desc, "valid")
@@ -210,7 +257,7 @@ func TestBuildFilterEnableLabel(t *testing.T) {
var names []string
names = append(names, "test")
- filter, desc := BuildFilter(names, true, "")
+ filter, desc := BuildFilter(names, []string{}, true, "")
assert.Contains(t, desc, "using enable label")
container := new(mocks.FilterableContainer)
@@ -235,3 +282,52 @@ func TestBuildFilterEnableLabel(t *testing.T) {
assert.False(t, filter(container))
container.AssertExpectations(t)
}
+
+func TestBuildFilterDisableContainer(t *testing.T) {
+ filter, desc := BuildFilter([]string{}, []string{"excluded", "notfound"}, false, "")
+ assert.Contains(t, desc, "not named")
+ assert.Contains(t, desc, "excluded")
+ assert.Contains(t, desc, "or")
+ assert.Contains(t, desc, "notfound")
+
+ container := new(mocks.FilterableContainer)
+ container.On("Name").Return("Another")
+ container.On("Enabled").Return(false, false)
+ assert.True(t, filter(container))
+ container.AssertExpectations(t)
+
+ container = new(mocks.FilterableContainer)
+ container.On("Name").Return("AnotherOne")
+ container.On("Enabled").Return(true, true)
+ assert.True(t, filter(container))
+ container.AssertExpectations(t)
+
+ container = new(mocks.FilterableContainer)
+ container.On("Name").Return("test")
+ container.On("Enabled").Return(false, false)
+ assert.True(t, filter(container))
+ container.AssertExpectations(t)
+
+ container = new(mocks.FilterableContainer)
+ container.On("Name").Return("excluded")
+ container.On("Enabled").Return(true, true)
+ assert.False(t, filter(container))
+ container.AssertExpectations(t)
+
+ container = new(mocks.FilterableContainer)
+ container.On("Name").Return("excludedAsSubstring")
+ container.On("Enabled").Return(true, true)
+ assert.True(t, filter(container))
+ container.AssertExpectations(t)
+
+ container = new(mocks.FilterableContainer)
+ container.On("Name").Return("notfound")
+ container.On("Enabled").Return(true, true)
+ assert.False(t, filter(container))
+ container.AssertExpectations(t)
+
+ container = new(mocks.FilterableContainer)
+ container.On("Enabled").Return(false, true)
+ assert.False(t, filter(container))
+ container.AssertExpectations(t)
+}
diff --git a/pkg/lifecycle/lifecycle.go b/pkg/lifecycle/lifecycle.go
index ed4ac20..c0f962e 100644
--- a/pkg/lifecycle/lifecycle.go
+++ b/pkg/lifecycle/lifecycle.go
@@ -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())
diff --git a/pkg/notifications/common_templates.go b/pkg/notifications/common_templates.go
index 64a53c0..84c0f54 100644
--- a/pkg/notifications/common_templates.go
+++ b/pkg/notifications/common_templates.go
@@ -35,5 +35,6 @@ var commonTemplates = map[string]string{
no containers matched filter
{{- end -}}
{{- end -}}`,
-}
+ `json.v1`: `{{ . | ToJSON }}`,
+}
diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go
index b6883a2..9103d38 100644
--- a/pkg/notifications/email.go
+++ b/pkg/notifications/email.go
@@ -63,6 +63,7 @@ func (e *emailTypeNotifier) GetURL(c *cobra.Command) (string, error) {
UseHTML: false,
Encryption: shoutrrrSmtp.EncMethods.Auto,
Auth: shoutrrrSmtp.AuthTypes.None,
+ ClientHost: "localhost",
}
if len(e.User) > 0 {
diff --git a/pkg/notifications/json.go b/pkg/notifications/json.go
new file mode 100644
index 0000000..20da92b
--- /dev/null
+++ b/pkg/notifications/json.go
@@ -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{}
diff --git a/pkg/notifications/json_test.go b/pkg/notifications/json_test.go
new file mode 100644
index 0000000..ef30c59
--- /dev/null
+++ b/pkg/notifications/json_test.go
@@ -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))
+ })
+ })
+ })
+})
diff --git a/pkg/notifications/model.go b/pkg/notifications/model.go
new file mode 100644
index 0000000..83c97ba
--- /dev/null
+++ b/pkg/notifications/model.go
@@ -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
+}
diff --git a/pkg/notifications/preview/data/data.go b/pkg/notifications/preview/data/data.go
new file mode 100644
index 0000000..4a002ed
--- /dev/null
+++ b/pkg/notifications/preview/data/data.go
@@ -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"
+}
diff --git a/pkg/notifications/preview/data/logs.go b/pkg/notifications/preview/data/logs.go
new file mode 100644
index 0000000..3ca7710
--- /dev/null
+++ b/pkg/notifications/preview/data/logs.go
@@ -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)
+}
diff --git a/pkg/notifications/preview/data/preview_strings.go b/pkg/notifications/preview/data/preview_strings.go
new file mode 100644
index 0000000..9212a71
--- /dev/null
+++ b/pkg/notifications/preview/data/preview_strings.go
@@ -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.",
+}
diff --git a/pkg/notifications/preview/data/report.go b/pkg/notifications/preview/data/report.go
new file mode 100644
index 0000000..2c8627f
--- /dev/null
+++ b/pkg/notifications/preview/data/report.go
@@ -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] }
diff --git a/pkg/notifications/preview/data/status.go b/pkg/notifications/preview/data/status.go
new file mode 100644
index 0000000..33f9bec
--- /dev/null
+++ b/pkg/notifications/preview/data/status.go
@@ -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)
+}
diff --git a/pkg/notifications/preview/tplprev.go b/pkg/notifications/preview/tplprev.go
new file mode 100644
index 0000000..8855416
--- /dev/null
+++ b/pkg/notifications/preview/tplprev.go
@@ -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
+}
diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go
index 47141e8..cc3a931 100644
--- a/pkg/notifications/shoutrrr.go
+++ b/pkg/notifications/shoutrrr.go
@@ -10,10 +10,9 @@ import (
"github.com/containrrr/shoutrrr"
"github.com/containrrr/shoutrrr/pkg/types"
+ "github.com/containrrr/watchtower/pkg/notifications/templates"
t "github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus"
- "golang.org/x/text/cases"
- "golang.org/x/text/language"
)
// LocalLog is a logrus logger that does not send entries as notifications
@@ -61,7 +60,7 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
return names
}
-// GetNames returns a list of URLs for notification services that has been added
+// GetURLs returns a list of URLs for notification services that has been added
func (n *shoutrrrTypeNotifier) GetURLs() []string {
return n.Urls
}
@@ -74,7 +73,7 @@ func (n *shoutrrrTypeNotifier) AddLogHook() {
n.receiving = true
log.AddHook(n)
- // Do the sending in a separate goroutine so we don't block the main process.
+ // Do the sending in a separate goroutine, so we don't block the main process.
go sendNotifications(n)
}
@@ -110,6 +109,7 @@ func createNotifier(urls []string, level log.Level, tplString string, legacy boo
legacyTemplate: legacy,
data: data,
params: params,
+ delay: delay,
}
}
@@ -207,12 +207,8 @@ func (n *shoutrrrTypeNotifier) Fire(entry *log.Entry) error {
}
func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, err error) {
- funcs := template.FuncMap{
- "ToUpper": strings.ToUpper,
- "ToLower": strings.ToLower,
- "Title": cases.Title(language.AmericanEnglish).String,
- }
- tplBase := template.New("").Funcs(funcs)
+
+ tplBase := template.New("").Funcs(templates.Funcs)
if builtin, found := commonTemplates[tplString]; found {
log.WithField(`template`, tplString).Debug(`Using common template`)
@@ -240,16 +236,3 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
return
}
-
-// StaticData is the part of the notification template data model set upon initialization
-type StaticData struct {
- Title string
- Host string
-}
-
-// Data is the notification template data model
-type Data struct {
- StaticData
- Entries []*log.Entry
- Report t.Report
-}
diff --git a/pkg/notifications/templates/funcs.go b/pkg/notifications/templates/funcs.go
new file mode 100644
index 0000000..6958c1a
--- /dev/null
+++ b/pkg/notifications/templates/funcs.go
@@ -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)
+}
diff --git a/pkg/registry/auth/auth.go b/pkg/registry/auth/auth.go
index 23aef60..99b05c9 100644
--- a/pkg/registry/auth/auth.go
+++ b/pkg/registry/auth/auth.go
@@ -4,14 +4,14 @@ import (
"encoding/json"
"errors"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"net/url"
"strings"
"github.com/containrrr/watchtower/pkg/registry/helpers"
"github.com/containrrr/watchtower/pkg/types"
- "github.com/docker/distribution/reference"
+ ref "github.com/distribution/reference"
"github.com/sirupsen/logrus"
)
@@ -20,13 +20,13 @@ const ChallengeHeader = "WWW-Authenticate"
// GetToken fetches a token for the registry hosting the provided image
func GetToken(container types.Container, registryAuth string) (string, error) {
- var err error
- var URL url.URL
-
- if URL, err = GetChallengeURL(container.ImageName()); err != nil {
+ normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName())
+ if err != nil {
return "", err
}
- logrus.WithField("URL", URL.String()).Debug("Building challenge URL")
+
+ URL := GetChallengeURL(normalizedRef)
+ logrus.WithField("URL", URL.String()).Debug("Built challenge URL")
var req *http.Request
if req, err = GetChallengeRequest(URL); err != nil {
@@ -55,7 +55,7 @@ func GetToken(container types.Container, registryAuth string) (string, error) {
return fmt.Sprintf("Basic %s", registryAuth), nil
}
if strings.HasPrefix(challenge, "bearer") {
- return GetBearerHeader(challenge, container.ImageName(), registryAuth)
+ return GetBearerHeader(challenge, normalizedRef, registryAuth)
}
return "", errors.New("unsupported challenge type from registry")
@@ -73,12 +73,9 @@ func GetChallengeRequest(URL url.URL) (*http.Request, error) {
}
// GetBearerHeader tries to fetch a bearer token from the registry based on the challenge instructions
-func GetBearerHeader(challenge string, img string, registryAuth string) (string, error) {
+func GetBearerHeader(challenge string, imageRef ref.Named, registryAuth string) (string, error) {
client := http.Client{}
- if strings.Contains(img, ":") {
- img = strings.Split(img, ":")[0]
- }
- authURL, err := GetAuthURL(challenge, img)
+ authURL, err := GetAuthURL(challenge, imageRef)
if err != nil {
return "", err
@@ -91,7 +88,8 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string,
if registryAuth != "" {
logrus.Debug("Credentials found.")
- logrus.Tracef("Credentials: %v", registryAuth)
+ // CREDENTIAL: Uncomment to log registry credentials
+ // logrus.Tracef("Credentials: %v", registryAuth)
r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth))
} else {
logrus.Debug("No credentials found.")
@@ -102,7 +100,7 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string,
return "", err
}
- body, _ := ioutil.ReadAll(authResponse.Body)
+ body, _ := io.ReadAll(authResponse.Body)
tokenResponse := &types.TokenResponse{}
err = json.Unmarshal(body, tokenResponse)
@@ -114,7 +112,7 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string,
}
// GetAuthURL from the instructions in the challenge
-func GetAuthURL(challenge string, img string) (*url.URL, error) {
+func GetAuthURL(challenge string, imageRef ref.Named) (*url.URL, error) {
loweredChallenge := strings.ToLower(challenge)
raw := strings.TrimPrefix(loweredChallenge, "bearer")
@@ -123,10 +121,9 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) {
for _, pair := range pairs {
trimmed := strings.Trim(pair, " ")
- kv := strings.Split(trimmed, "=")
- key := kv[0]
- val := strings.Trim(kv[1], "\"")
- values[key] = val
+ if key, val, ok := strings.Cut(trimmed, "="); ok {
+ values[key] = strings.Trim(val, `"`)
+ }
}
logrus.WithFields(logrus.Fields{
"realm": values["realm"],
@@ -141,53 +138,25 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) {
q := authURL.Query()
q.Add("service", values["service"])
- scopeImage := GetScopeFromImageName(img, values["service"])
+ scopeImage := ref.Path(imageRef)
scope := fmt.Sprintf("repository:%s:pull", scopeImage)
- logrus.WithFields(logrus.Fields{"scope": scope, "image": img}).Debug("Setting scope for auth token")
+ logrus.WithFields(logrus.Fields{"scope": scope, "image": imageRef.Name()}).Debug("Setting scope for auth token")
q.Add("scope", scope)
authURL.RawQuery = q.Encode()
return authURL, nil
}
-// GetScopeFromImageName normalizes an image name for use as scope during auth and head requests
-func GetScopeFromImageName(img, svc string) string {
- parts := strings.Split(img, "/")
-
- if len(parts) > 2 {
- if strings.Contains(svc, "docker.io") {
- return fmt.Sprintf("%s/%s", parts[1], strings.Join(parts[2:], "/"))
- }
- return strings.Join(parts, "/")
- }
-
- if len(parts) == 2 {
- if strings.Contains(parts[0], "docker.io") {
- return fmt.Sprintf("library/%s", parts[1])
- }
- return strings.Replace(img, svc+"/", "", 1)
- }
-
- if strings.Contains(svc, "docker.io") {
- return fmt.Sprintf("library/%s", parts[0])
- }
- return img
-}
-
-// GetChallengeURL creates a URL object based on the image info
-func GetChallengeURL(img string) (url.URL, error) {
-
- normalizedNamed, _ := reference.ParseNormalizedNamed(img)
- host, err := helpers.NormalizeRegistry(normalizedNamed.String())
- if err != nil {
- return url.URL{}, err
- }
+// GetChallengeURL returns the URL to check auth requirements
+// for access to a given image
+func GetChallengeURL(imageRef ref.Named) url.URL {
+ host, _ := helpers.GetRegistryAddress(imageRef.Name())
URL := url.URL{
Scheme: "https",
Host: host,
Path: "/v2/",
}
- return URL, nil
+ return URL
}
diff --git a/pkg/registry/auth/auth_test.go b/pkg/registry/auth/auth_test.go
index 6ad2307..d295310 100644
--- a/pkg/registry/auth/auth_test.go
+++ b/pkg/registry/auth/auth_test.go
@@ -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)
+}
diff --git a/pkg/registry/digest/digest.go b/pkg/registry/digest/digest.go
index 26fbd8e..e569599 100644
--- a/pkg/registry/digest/digest.go
+++ b/pkg/registry/digest/digest.go
@@ -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
@@ -25,7 +26,7 @@ func CompareDigest(container types.Container, registryAuth string) (bool, error)
if !container.HasImageInfo() {
return false, errors.New("container image info missing")
}
-
+
var digest string
registryAuth = TransformAuth(registryAuth)
@@ -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")
diff --git a/pkg/registry/helpers/helpers.go b/pkg/registry/helpers/helpers.go
index 1469331..35d6ca3 100644
--- a/pkg/registry/helpers/helpers.go
+++ b/pkg/registry/helpers/helpers.go
@@ -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
}
diff --git a/pkg/registry/helpers/helpers_test.go b/pkg/registry/helpers/helpers_test.go
index 92e9116..a561c2c 100644
--- a/pkg/registry/helpers/helpers_test.go
+++ b/pkg/registry/helpers/helpers_test.go
@@ -1,9 +1,10 @@
package helpers
import (
+ "testing"
+
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
- "testing"
)
func TestHelpers(t *testing.T) {
@@ -12,20 +13,25 @@ func TestHelpers(t *testing.T) {
}
var _ = Describe("the helpers", func() {
-
- When("converting an url to a hostname", func() {
- It("should return docker.io given docker.io/containrrr/watchtower:latest", func() {
- host, port, err := ConvertToHostname("docker.io/containrrr/watchtower:latest")
- Expect(err).NotTo(HaveOccurred())
- Expect(host).To(Equal("docker.io"))
- Expect(port).To(BeEmpty())
+ Describe("GetRegistryAddress", func() {
+ It("should return error if passed empty string", func() {
+ _, err := GetRegistryAddress("")
+ Expect(err).To(HaveOccurred())
})
- })
- When("normalizing the registry information", func() {
- It("should return index.docker.io given docker.io", func() {
- out, err := NormalizeRegistry("docker.io/containrrr/watchtower:latest")
- Expect(err).NotTo(HaveOccurred())
- Expect(out).To(Equal("index.docker.io"))
+ It("should return index.docker.io for image refs with no explicit registry", func() {
+ Expect(GetRegistryAddress("watchtower")).To(Equal("index.docker.io"))
+ Expect(GetRegistryAddress("containrrr/watchtower")).To(Equal("index.docker.io"))
+ })
+ It("should return index.docker.io for image refs with docker.io domain", func() {
+ Expect(GetRegistryAddress("docker.io/watchtower")).To(Equal("index.docker.io"))
+ Expect(GetRegistryAddress("docker.io/containrrr/watchtower")).To(Equal("index.docker.io"))
+ })
+ It("should return the host if passed an image name containing a local host", func() {
+ Expect(GetRegistryAddress("henk:80/watchtower")).To(Equal("henk:80"))
+ Expect(GetRegistryAddress("localhost/watchtower")).To(Equal("localhost"))
+ })
+ It("should return the server address if passed a fully qualified image name", func() {
+ Expect(GetRegistryAddress("github.com/containrrr/config")).To(Equal("github.com"))
})
})
})
diff --git a/pkg/registry/manifest/manifest.go b/pkg/registry/manifest/manifest.go
index facbb6c..c732bae 100644
--- a/pkg/registry/manifest/manifest.go
+++ b/pkg/registry/manifest/manifest.go
@@ -1,42 +1,41 @@
package manifest
import (
+ "errors"
"fmt"
- "github.com/containrrr/watchtower/pkg/registry/auth"
+ url2 "net/url"
+
"github.com/containrrr/watchtower/pkg/registry/helpers"
"github.com/containrrr/watchtower/pkg/types"
- ref "github.com/docker/distribution/reference"
+ ref "github.com/distribution/reference"
"github.com/sirupsen/logrus"
- url2 "net/url"
- "strings"
)
// BuildManifestURL from raw image data
func BuildManifestURL(container types.Container) (string, error) {
-
- normalizedName, err := ref.ParseNormalizedNamed(container.ImageName())
+ normalizedRef, err := ref.ParseDockerRef(container.ImageName())
if err != nil {
return "", err
}
+ normalizedTaggedRef, isTagged := normalizedRef.(ref.NamedTagged)
+ if !isTagged {
+ return "", errors.New("Parsed container image ref has no tag: " + normalizedRef.String())
+ }
- host, err := helpers.NormalizeRegistry(normalizedName.String())
- img, tag := ExtractImageAndTag(strings.TrimPrefix(container.ImageName(), host+"/"))
+ host, _ := helpers.GetRegistryAddress(normalizedTaggedRef.Name())
+ img, tag := ref.Path(normalizedTaggedRef), normalizedTaggedRef.Tag()
logrus.WithFields(logrus.Fields{
"image": img,
"tag": tag,
- "normalized": normalizedName,
+ "normalized": normalizedTaggedRef.Name(),
"host": host,
}).Debug("Parsing image ref")
if err != nil {
return "", err
}
- img = auth.GetScopeFromImageName(img, host)
- if !strings.Contains(img, "/") {
- img = "library/" + img
- }
url := url2.URL{
Scheme: "https",
Host: host,
@@ -44,24 +43,3 @@ func BuildManifestURL(container types.Container) (string, error) {
}
return url.String(), nil
}
-
-// ExtractImageAndTag from a concatenated string
-func ExtractImageAndTag(imageName string) (string, string) {
- var img string
- var tag string
-
- if strings.Contains(imageName, ":") {
- parts := strings.Split(imageName, ":")
- if len(parts) > 2 {
- img = parts[0]
- tag = strings.Join(parts[1:], ":")
- } else {
- img = parts[0]
- tag = parts[1]
- }
- } else {
- img = imageName
- tag = "latest"
- }
- return img, tag
-}
diff --git a/pkg/registry/manifest/manifest_test.go b/pkg/registry/manifest/manifest_test.go
index 95f196b..b24d9bc 100644
--- a/pkg/registry/manifest/manifest_test.go
+++ b/pkg/registry/manifest/manifest_test.go
@@ -1,13 +1,14 @@
package manifest_test
import (
+ "testing"
+ "time"
+
"github.com/containrrr/watchtower/internal/actions/mocks"
"github.com/containrrr/watchtower/pkg/registry/manifest"
apiTypes "github.com/docker/docker/api/types"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
- "testing"
- "time"
)
func TestManifest(t *testing.T) {
@@ -16,60 +17,58 @@ func TestManifest(t *testing.T) {
}
var _ = Describe("the manifest module", func() {
- mockId := "mock-id"
- mockName := "mock-container"
- mockCreated := time.Now()
-
- When("building a manifest url", func() {
+ Describe("BuildManifestURL", func() {
It("should return a valid url given a fully qualified image", func() {
- expected := "https://ghcr.io/v2/containrrr/watchtower/manifests/latest"
- imageInfo := apiTypes.ImageInspect{
- RepoTags: []string{
- "ghcr.io/k6io/operator:latest",
- },
- }
- mock := mocks.CreateMockContainerWithImageInfo(mockId, mockName, "ghcr.io/containrrr/watchtower:latest", mockCreated, imageInfo)
- res, err := manifest.BuildManifestURL(mock)
+ imageRef := "ghcr.io/containrrr/watchtower:mytag"
+ expected := "https://ghcr.io/v2/containrrr/watchtower/manifests/mytag"
+
+ URL, err := buildMockContainerManifestURL(imageRef)
Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal(expected))
+ Expect(URL).To(Equal(expected))
})
- It("should assume dockerhub for non-qualified images", func() {
+ It("should assume Docker Hub for image refs with no explicit registry", func() {
+ imageRef := "containrrr/watchtower:latest"
expected := "https://index.docker.io/v2/containrrr/watchtower/manifests/latest"
- imageInfo := apiTypes.ImageInspect{
- RepoTags: []string{
- "containrrr/watchtower:latest",
- },
- }
- mock := mocks.CreateMockContainerWithImageInfo(mockId, mockName, "containrrr/watchtower:latest", mockCreated, imageInfo)
- res, err := manifest.BuildManifestURL(mock)
+ URL, err := buildMockContainerManifestURL(imageRef)
Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal(expected))
+ Expect(URL).To(Equal(expected))
})
- It("should assume latest for images that lack an explicit tag", func() {
+ It("should assume latest for image refs with no explicit tag", func() {
+ imageRef := "containrrr/watchtower"
expected := "https://index.docker.io/v2/containrrr/watchtower/manifests/latest"
- imageInfo := apiTypes.ImageInspect{
- RepoTags: []string{
- "containrrr/watchtower",
- },
- }
-
- mock := mocks.CreateMockContainerWithImageInfo(mockId, mockName, "containrrr/watchtower", mockCreated, imageInfo)
-
- res, err := manifest.BuildManifestURL(mock)
+ URL, err := buildMockContainerManifestURL(imageRef)
Expect(err).NotTo(HaveOccurred())
- Expect(res).To(Equal(expected))
+ Expect(URL).To(Equal(expected))
})
- It("should combine the tag name and digest pinning into one digest, given multiple colons", func() {
- in := "containrrr/watchtower:latest@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7"
- image, tag := "containrrr/watchtower", "latest@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7"
+ It("should not prepend library/ for single-part container names in registries other than Docker Hub", func() {
+ imageRef := "docker-registry.domain/imagename:latest"
+ expected := "https://docker-registry.domain/v2/imagename/manifests/latest"
- imageOut, tagOut := manifest.ExtractImageAndTag(in)
-
- Expect(imageOut).To(Equal(image))
- Expect(tagOut).To(Equal(tag))
+ URL, err := buildMockContainerManifestURL(imageRef)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(URL).To(Equal(expected))
+ })
+ It("should throw an error on pinned images", func() {
+ imageRef := "docker-registry.domain/imagename@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7"
+ URL, err := buildMockContainerManifestURL(imageRef)
+ Expect(err).To(HaveOccurred())
+ Expect(URL).To(BeEmpty())
})
})
-
})
+
+func buildMockContainerManifestURL(imageRef string) (string, error) {
+ imageInfo := apiTypes.ImageInspect{
+ RepoTags: []string{
+ imageRef,
+ },
+ }
+ mockID := "mock-id"
+ mockName := "mock-container"
+ mockCreated := time.Now()
+ mock := mocks.CreateMockContainerWithImageInfo(mockID, mockName, imageRef, mockCreated, imageInfo)
+
+ return manifest.BuildManifestURL(mock)
+}
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
index 9edd66f..430b401 100644
--- a/pkg/registry/registry.go
+++ b/pkg/registry/registry.go
@@ -3,7 +3,7 @@ package registry
import (
"github.com/containrrr/watchtower/pkg/registry/helpers"
watchtowerTypes "github.com/containrrr/watchtower/pkg/types"
- ref "github.com/docker/distribution/reference"
+ ref "github.com/distribution/reference"
"github.com/docker/docker/api/types"
log "github.com/sirupsen/logrus"
)
@@ -19,7 +19,9 @@ func GetPullOptions(imageName string) (types.ImagePullOptions, error) {
if auth == "" {
return types.ImagePullOptions{}, nil
}
- log.Tracef("Got auth value: %s", auth)
+
+ // CREDENTIAL: Uncomment to log docker config auth
+ // log.Tracef("Got auth value: %s", auth)
return types.ImagePullOptions{
RegistryAuth: auth,
@@ -41,17 +43,17 @@ func DefaultAuthHandler() (string, error) {
// Will return false if behavior for container is unknown.
func WarnOnAPIConsumption(container watchtowerTypes.Container) bool {
- normalizedName, err := ref.ParseNormalizedNamed(container.ImageName())
+ normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName())
if err != nil {
return true
}
- containerHost, err := helpers.NormalizeRegistry(normalizedName.String())
+ containerHost, err := helpers.GetRegistryAddress(normalizedRef.Name())
if err != nil {
return true
}
- if containerHost == "index.docker.io" || containerHost == "ghcr.io" {
+ if containerHost == helpers.DefaultRegistryHost || containerHost == "ghcr.io" {
return true
}
diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go
index 5f3f57f..481c91d 100644
--- a/pkg/registry/registry_test.go
+++ b/pkg/registry/registry_test.go
@@ -23,11 +23,9 @@ var _ = Describe("Registry", func() {
})
When("Given a container with an image explicitly from dockerhub", func() {
It("should want to warn", func() {
- Expect(testContainerWithImage("registry-1.docker.io/docker:latest")).To(BeTrue())
Expect(testContainerWithImage("index.docker.io/docker:latest")).To(BeTrue())
Expect(testContainerWithImage("docker.io/docker:latest")).To(BeTrue())
})
-
})
When("Given a container with an image from some other registry", func() {
It("should not want to warn", func() {
diff --git a/pkg/registry/trust.go b/pkg/registry/trust.go
index fa17bbc..0b20248 100644
--- a/pkg/registry/trust.go
+++ b/pkg/registry/trust.go
@@ -5,13 +5,12 @@ import (
"encoding/json"
"errors"
"os"
- "strings"
+ "github.com/containrrr/watchtower/pkg/registry/helpers"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/types"
- "github.com/docker/distribution/reference"
log "github.com/sirupsen/logrus"
)
@@ -19,7 +18,7 @@ import (
// loaded from environment variables or docker config
// as available in that order
func EncodedAuth(ref string) (string, error) {
- auth, err := EncodedEnvAuth(ref)
+ auth, err := EncodedEnvAuth()
if err != nil {
auth, err = EncodedConfigAuth(ref)
}
@@ -29,7 +28,7 @@ func EncodedAuth(ref string) (string, error) {
// EncodedEnvAuth returns an encoded auth config for the given registry
// loaded from environment variables
// Returns an error if authentication environment variables have not been set
-func EncodedEnvAuth(ref string) (string, error) {
+func EncodedEnvAuth() (string, error) {
username := os.Getenv("REPO_USER")
password := os.Getenv("REPO_PASS")
if username != "" && password != "" {
@@ -37,8 +36,11 @@ func EncodedEnvAuth(ref string) (string, error) {
Username: username,
Password: password,
}
- log.Debugf("Loaded auth credentials for user %s on registry %s", auth.Username, ref)
- log.Tracef("Using auth password %s", auth.Password)
+
+ log.Debugf("Loaded auth credentials for registry user %s from environment", auth.Username)
+ // CREDENTIAL: Uncomment to log REPO_PASS environment variable
+ // log.Tracef("Using auth password %s", auth.Password)
+
return EncodeAuth(auth)
}
return "", errors.New("registry auth environment variables (REPO_USER, REPO_PASS) not set")
@@ -48,19 +50,20 @@ func EncodedEnvAuth(ref string) (string, error) {
// loaded from the docker config
// Returns an empty string if credentials cannot be found for the referenced server
// The docker config must be mounted on the container
-func EncodedConfigAuth(ref string) (string, error) {
- server, err := ParseServerAddress(ref)
+func EncodedConfigAuth(imageRef string) (string, error) {
+ server, err := helpers.GetRegistryAddress(imageRef)
if err != nil {
- log.Errorf("Unable to parse the image ref %s", err)
+ log.Errorf("Could not get registry from image ref %s", imageRef)
return "", err
}
+
configDir := os.Getenv("DOCKER_CONFIG")
if configDir == "" {
configDir = "/"
}
configFile, err := cliconfig.Load(configDir)
if err != nil {
- log.Errorf("Unable to find default config file %s", err)
+ log.Errorf("Unable to find default config file: %s", err)
return "", err
}
credStore := CredentialsStore(*configFile)
@@ -70,23 +73,12 @@ func EncodedConfigAuth(ref string) (string, error) {
log.WithField("config_file", configFile.Filename).Debugf("No credentials for %s found", server)
return "", nil
}
- log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, ref, configFile.Filename)
- log.Tracef("Using auth password %s", auth.Password)
+ log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, server, configFile.Filename)
+ // CREDENTIAL: Uncomment to log docker config password
+ // log.Tracef("Using auth password %s", auth.Password)
return EncodeAuth(auth)
}
-// ParseServerAddress extracts the server part from a container image ref
-func ParseServerAddress(ref string) (string, error) {
-
- parsedRef, err := reference.Parse(ref)
- if err != nil {
- return ref, err
- }
-
- parts := strings.Split(parsedRef.String(), "/")
- return parts[0], nil
-}
-
// CredentialsStore returns a new credentials store based
// on the settings provided in the configuration file.
func CredentialsStore(configFile configfile.ConfigFile) credentials.Store {
diff --git a/pkg/registry/trust_test.go b/pkg/registry/trust_test.go
index 3dab6ad..00fc8a7 100644
--- a/pkg/registry/trust_test.go
+++ b/pkg/registry/trust_test.go
@@ -1,65 +1,49 @@
package registry
import (
+ "os"
+
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
- "os"
)
-var _ = Describe("Testing with Ginkgo", func() {
- It("encoded env auth_ should return an error if repo envs are unset", func() {
- _ = os.Unsetenv("REPO_USER")
- _ = os.Unsetenv("REPO_PASS")
+var _ = Describe("Registry credential helpers", func() {
+ Describe("EncodedAuth", func() {
+ It("should return repo credentials from env when set", func() {
+ var err error
+ expected := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0="
- _, err := EncodedEnvAuth("")
- Expect(err).To(HaveOccurred())
+ err = os.Setenv("REPO_USER", "containrrr-user")
+ Expect(err).NotTo(HaveOccurred())
+
+ err = os.Setenv("REPO_PASS", "containrrr-pass")
+ Expect(err).NotTo(HaveOccurred())
+
+ config, err := EncodedEnvAuth()
+ Expect(config).To(Equal(expected))
+ Expect(err).NotTo(HaveOccurred())
+ })
})
- It("encoded env auth_ should return auth hash if repo envs are set", func() {
- var err error
- expectedHash := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0="
- err = os.Setenv("REPO_USER", "containrrr-user")
- Expect(err).NotTo(HaveOccurred())
+ Describe("EncodedEnvAuth", func() {
+ It("should return an error if repo envs are unset", func() {
+ _ = os.Unsetenv("REPO_USER")
+ _ = os.Unsetenv("REPO_PASS")
- err = os.Setenv("REPO_PASS", "containrrr-pass")
- Expect(err).NotTo(HaveOccurred())
-
- config, err := EncodedEnvAuth("")
- Expect(config).To(Equal(expectedHash))
- Expect(err).NotTo(HaveOccurred())
+ _, err := EncodedEnvAuth()
+ Expect(err).To(HaveOccurred())
+ })
})
- It("encoded config auth_ should return an error if file is not present", func() {
- var err error
- err = os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail")
- Expect(err).NotTo(HaveOccurred())
+ Describe("EncodedConfigAuth", func() {
+ It("should return an error if file is not present", func() {
+ var err error
- _, err = EncodedConfigAuth("")
- Expect(err).To(HaveOccurred())
+ err = os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail")
+ Expect(err).NotTo(HaveOccurred())
- })
- /*
- * TODO:
- * This part only confirms that it still works in the same way as it did
- * with the old version of the docker api client sdk. I'd say that
- * ParseServerAddress likely needs to be elaborated a bit to default to
- * dockerhub in case no server address was provided.
- *
- * ++ @simskij, 2019-04-04
- */
- It("parse server address_ should return error if passed empty string", func() {
-
- _, err := ParseServerAddress("")
- Expect(err).To(HaveOccurred())
- })
- It("parse server address_ should return the organization part if passed an image name missing server name", func() {
-
- val, _ := ParseServerAddress("containrrr/config")
- Expect(val).To(Equal("containrrr"))
- })
- It("parse server address_ should return the server name if passed a fully qualified image name", func() {
-
- val, _ := ParseServerAddress("github.com/containrrrr/config")
- Expect(val).To(Equal("github.com"))
+ _, err = EncodedConfigAuth("")
+ Expect(err).To(HaveOccurred())
+ })
})
})
diff --git a/pkg/sorter/sort.go b/pkg/sorter/sort.go
index 1e27f1b..b9d1e12 100644
--- a/pkg/sorter/sort.go
+++ b/pkg/sorter/sort.go
@@ -2,13 +2,14 @@ package sorter
import (
"fmt"
- "github.com/containrrr/watchtower/pkg/container"
"time"
+
+ "github.com/containrrr/watchtower/pkg/types"
)
// ByCreated allows a list of Container structs to be sorted by the container's
// created date.
-type ByCreated []container.Container
+type ByCreated []types.Container
func (c ByCreated) Len() int { return len(c) }
func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
@@ -34,18 +35,18 @@ func (c ByCreated) Less(i, j int) bool {
// the front of the list while containers with links will be sorted after all
// of their dependencies. This sort order ensures that linked containers can
// be started in the correct order.
-func SortByDependencies(containers []container.Container) ([]container.Container, error) {
+func SortByDependencies(containers []types.Container) ([]types.Container, error) {
sorter := dependencySorter{}
return sorter.Sort(containers)
}
type dependencySorter struct {
- unvisited []container.Container
+ unvisited []types.Container
marked map[string]bool
- sorted []container.Container
+ sorted []types.Container
}
-func (ds *dependencySorter) Sort(containers []container.Container) ([]container.Container, error) {
+func (ds *dependencySorter) Sort(containers []types.Container) ([]types.Container, error) {
ds.unvisited = containers
ds.marked = map[string]bool{}
@@ -58,7 +59,7 @@ func (ds *dependencySorter) Sort(containers []container.Container) ([]container.
return ds.sorted, nil
}
-func (ds *dependencySorter) visit(c container.Container) error {
+func (ds *dependencySorter) visit(c types.Container) error {
if _, ok := ds.marked[c.Name()]; ok {
return fmt.Errorf("circular reference to %s", c.Name())
@@ -84,7 +85,7 @@ func (ds *dependencySorter) visit(c container.Container) error {
return nil
}
-func (ds *dependencySorter) findUnvisited(name string) *container.Container {
+func (ds *dependencySorter) findUnvisited(name string) *types.Container {
for _, c := range ds.unvisited {
if c.Name() == name {
return &c
@@ -94,7 +95,7 @@ func (ds *dependencySorter) findUnvisited(name string) *container.Container {
return nil
}
-func (ds *dependencySorter) removeUnvisited(c container.Container) {
+func (ds *dependencySorter) removeUnvisited(c types.Container) {
var idx int
for i := range ds.unvisited {
if ds.unvisited[i].Name() == c.Name() {
diff --git a/pkg/types/container.go b/pkg/types/container.go
index 22742e9..8a22f44 100644
--- a/pkg/types/container.go
+++ b/pkg/types/container.go
@@ -1,8 +1,10 @@
package types
import (
- "github.com/docker/docker/api/types"
"strings"
+
+ "github.com/docker/docker/api/types"
+ dc "github.com/docker/docker/api/types/container"
)
// ImageID is a hash string representing a container image
@@ -50,7 +52,7 @@ type Container interface {
SafeImageID() ImageID
ImageName() string
Enabled() (bool, bool)
- IsMonitorOnly() bool
+ IsMonitorOnly(UpdateParams) bool
Scope() (string, bool)
Links() []string
ToRestart() bool
@@ -62,4 +64,15 @@ type Container interface {
GetLifecyclePostCheckCommand() string
GetLifecyclePreUpdateCommand() string
GetLifecyclePostUpdateCommand() string
+ VerifyConfiguration() error
+ SetStale(bool)
+ IsStale() bool
+ IsNoPull(UpdateParams) bool
+ SetLinkedToRestarting(bool)
+ IsLinkedToRestarting() bool
+ PreUpdateTimeout() int
+ PostUpdateTimeout() int
+ IsRestarting() bool
+ GetCreateConfig() *dc.Config
+ GetCreateHostConfig() *dc.HostConfig
}
diff --git a/pkg/types/update_params.go b/pkg/types/update_params.go
index 611cc70..2b6d3c4 100644
--- a/pkg/types/update_params.go
+++ b/pkg/types/update_params.go
@@ -6,11 +6,13 @@ import (
// UpdateParams contains all different options available to alter the behavior of the Update func
type UpdateParams struct {
- Filter Filter
- Cleanup bool
- NoRestart bool
- Timeout time.Duration
- MonitorOnly bool
- LifecycleHooks bool
- RollingRestart bool
+ Filter Filter
+ Cleanup bool
+ NoRestart bool
+ Timeout time.Duration
+ MonitorOnly bool
+ NoPull bool
+ LifecycleHooks bool
+ RollingRestart bool
+ LabelPrecedence bool
}
diff --git a/scripts/build-tplprev.sh b/scripts/build-tplprev.sh
new file mode 100755
index 0000000..293710c
--- /dev/null
+++ b/scripts/build-tplprev.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+cd $(git rev-parse --show-toplevel)
+
+cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./docs/assets/
+
+GOARCH=wasm GOOS=js go build -o ./docs/assets/tplprev.wasm ./tplprev
\ No newline at end of file
diff --git a/scripts/contnet-tests.sh b/scripts/contnet-tests.sh
new file mode 100755
index 0000000..25269dc
--- /dev/null
+++ b/scripts/contnet-tests.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+
+set -e
+
+function exit_env_err() {
+ >&2 echo "Required environment variable not set: $1"
+ exit 1
+}
+
+if [ -z "$VPN_SERVICE_PROVIDER" ]; then exit_env_err "VPN_SERVICE_PROVIDER"; fi
+if [ -z "$OPENVPN_USER" ]; then exit_env_err "OPENVPN_USER"; fi
+if [ -z "$OPENVPN_PASSWORD" ]; then exit_env_err "OPENVPN_PASSWORD"; fi
+# if [ -z "$SERVER_COUNTRIES" ]; then exit_env_err "SERVER_COUNTRIES"; fi
+
+
+export SERVER_COUNTRIES=${SERVER_COUNTRIES:"Sweden"}
+REPO_ROOT="$(git rev-parse --show-toplevel)"
+COMPOSE_FILE="$REPO_ROOT/dockerfiles/container-networking/docker-compose.yml"
+DEFAULT_WATCHTOWER="$REPO_ROOT/watchtower"
+WATCHTOWER="$*"
+WATCHTOWER=${WATCHTOWER:-$DEFAULT_WATCHTOWER}
+echo "repo root path is $REPO_ROOT"
+echo "watchtower path is $WATCHTOWER"
+echo "compose file path is $COMPOSE_FILE"
+
+echo; echo "=== Forcing network container producer update..."
+
+echo "Pull previous version of gluetun..."
+docker pull qmcgaw/gluetun:v3.34.3
+echo "Fake new version of gluetun by retagging v3.34.4 as v3.35.0..."
+docker tag qmcgaw/gluetun:v3.34.3 qmcgaw/gluetun:v3.35.0
+
+echo; echo "=== Creating containers..."
+
+docker compose -p "wt-contnet" -f "$COMPOSE_FILE" up -d
+
+echo; echo "=== Running watchtower"
+$WATCHTOWER --run-once
+
+echo; echo "=== Removing containers..."
+
+docker compose -p "wt-contnet" -f "$COMPOSE_FILE" down
diff --git a/tplprev/main.go b/tplprev/main.go
new file mode 100644
index 0000000..120f968
--- /dev/null
+++ b/tplprev/main.go
@@ -0,0 +1,49 @@
+//go:build !wasm
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ "github.com/containrrr/watchtower/internal/meta"
+ "github.com/containrrr/watchtower/pkg/notifications/preview"
+ "github.com/containrrr/watchtower/pkg/notifications/preview/data"
+)
+
+func main() {
+ fmt.Fprintf(os.Stderr, "watchtower/tplprev %v\n\n", meta.Version)
+
+ var states string
+ var entries string
+
+ flag.StringVar(&states, "states", "cccuuueeekkktttfff", "sCanned, Updated, failEd, sKipped, sTale, Fresh")
+ flag.StringVar(&entries, "entries", "ewwiiidddd", "Fatal,Error,Warn,Info,Debug,Trace")
+
+ flag.Parse()
+
+ if len(flag.Args()) < 1 {
+ fmt.Fprintln(os.Stderr, "Missing required argument TEMPLATE")
+ flag.Usage()
+ os.Exit(1)
+ return
+ }
+
+ input, err := os.ReadFile(flag.Arg(0))
+ if err != nil {
+
+ fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err)
+ os.Exit(1)
+ return
+ }
+
+ result, err := preview.Render(string(input), data.StatesFromString(states), data.LevelsFromString(entries))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err)
+ os.Exit(1)
+ return
+ }
+
+ fmt.Println(result)
+}
diff --git a/tplprev/main_wasm.go b/tplprev/main_wasm.go
new file mode 100644
index 0000000..5e2ce6a
--- /dev/null
+++ b/tplprev/main_wasm.go
@@ -0,0 +1,62 @@
+//go:build wasm
+
+package main
+
+import (
+ "fmt"
+
+ "github.com/containrrr/watchtower/internal/meta"
+ "github.com/containrrr/watchtower/pkg/notifications/preview"
+ "github.com/containrrr/watchtower/pkg/notifications/preview/data"
+
+ "syscall/js"
+)
+
+func main() {
+ fmt.Println("watchtower/tplprev v" + meta.Version)
+
+ js.Global().Set("WATCHTOWER", js.ValueOf(map[string]any{
+ "tplprev": js.FuncOf(jsTplPrev),
+ }))
+ <-make(chan bool)
+
+}
+
+func jsTplPrev(this js.Value, args []js.Value) any {
+
+ if len(args) < 3 {
+ return "Requires 3 arguments passed"
+ }
+
+ input := args[0].String()
+
+ statesArg := args[1]
+ var states []data.State
+
+ if statesArg.Type() == js.TypeString {
+ states = data.StatesFromString(statesArg.String())
+ } else {
+ for i := 0; i < statesArg.Length(); i++ {
+ state := data.State(statesArg.Index(i).String())
+ states = append(states, state)
+ }
+ }
+
+ levelsArg := args[2]
+ var levels []data.LogLevel
+
+ if levelsArg.Type() == js.TypeString {
+ levels = data.LevelsFromString(statesArg.String())
+ } else {
+ for i := 0; i < levelsArg.Length(); i++ {
+ level := data.LogLevel(levelsArg.Index(i).String())
+ levels = append(levels, level)
+ }
+ }
+
+ result, err := preview.Render(input, states, levels)
+ if err != nil {
+ return "Error: " + err.Error()
+ }
+ return result
+}