Merge branch 'main' into fix/ref-processing

This commit is contained in:
nils måsén 2023-04-12 08:28:24 +02:00 committed by GitHub
commit c5c37f8795
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 945 additions and 189 deletions

View file

@ -16,7 +16,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: 1.18.x go-version: 1.18.x
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0 - uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
@ -41,7 +41,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: 1.18.x go-version: 1.18.x
- name: Run tests - name: Run tests
@ -60,11 +60,11 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: 1.18.x go-version: 1.18.x
- name: Build - name: Build
uses: goreleaser/goreleaser-action@8f67e590f2d095516493f017008adc464e63adb1 #v3 uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b #v3
with: with:
version: v0.155.0 version: v0.155.0
args: --snapshot --skip-publish --debug args: --snapshot --skip-publish --debug

View file

@ -12,7 +12,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: 1.18 go-version: 1.18
- name: Build - name: Build
@ -22,7 +22,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: 1.18 go-version: 1.18
- name: Test - name: Test

View file

@ -19,7 +19,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: 1.18.x go-version: 1.18.x
- uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0 - uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
@ -44,7 +44,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: 1.18.x go-version: 1.18.x
- name: Run tests - name: Run tests
@ -66,7 +66,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: 1.18.x go-version: 1.18.x
- name: Login to Docker Hub - name: Login to Docker Hub
@ -81,7 +81,7 @@ jobs:
password: ${{ secrets.BOT_GHCR_PAT }} password: ${{ secrets.BOT_GHCR_PAT }}
registry: ghcr.io registry: ghcr.io
- name: Build - name: Build
uses: goreleaser/goreleaser-action@8f67e590f2d095516493f017008adc464e63adb1 #v3 uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b #v3
with: with:
version: v0.155.0 version: v0.155.0
args: --debug args: --debug
@ -191,7 +191,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Pull new module version - name: Pull new module version
uses: andrewslotin/go-proxy-pull-action@bfc19ec6536e1638181b2ad6a03e16c7ccfb122f #master@2022-10-14 uses: andrewslotin/go-proxy-pull-action@50fea06a976087614babb9508e5c528b464f4645 #master@2022-10-14

View file

@ -182,7 +182,10 @@ func Run(c *cobra.Command, names []string) {
httpAPI := api.New(apiToken) httpAPI := api.New(apiToken)
if enableUpdateAPI { 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) httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle)
// If polling isn't enabled the scheduler is never started and // If polling isn't enabled the scheduler is never started and
// we need to trigger the startup messages manually. // we need to trigger the startup messages manually.

View file

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM alpine:3.17.1 as alpine FROM --platform=$BUILDPLATFORM alpine:3.17.3 as alpine
RUN apk add --no-cache \ RUN apk add --no-cache \
ca-certificates \ ca-certificates \

View file

@ -234,6 +234,9 @@ Environment Variable: WATCHTOWER_NO_PULL
Default: false Default: false
``` ```
Note that no-pull can also be specified on a per-container basis with the
`com.centurylinklabs.watchtower.no-pull` label set on those containers.
## Without sending a startup message ## Without sending a startup message
Do not send a message after watchtower started. Otherwise there will be an info-level notification. Do not send a message after watchtower started. Otherwise there will be an info-level notification.

View file

@ -30,7 +30,7 @@ To send notifications via shoutrrr, the following command-line options, or their
- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. This option can also reference a file, in which case the contents of the file are used. - `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. This option can also reference a file, in which case the contents of the file are used.
Go to [containrrr.dev/shoutrrr/v0.6/services/overview](https://containrrr.dev/shoutrrr/v0.6/services/overview) to Go to [containrrr.dev/shoutrrr/v0.7/services/overview](https://containrrr.dev/shoutrrr/v0.6/services/overview) to
learn more about the different service URLs you can use. You can define multiple services by space separating the learn more about the different service URLs you can use. You can define multiple services by space separating the
URLs. (See example below) URLs. (See example below)
@ -110,7 +110,7 @@ Example using a custom report template that always sends a session report after
docker run -d \ docker run -d \
--name watchtower \ --name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-e WATCHTOWER_NOTIFICATION_REPORT="true" -e WATCHTOWER_NOTIFICATION_REPORT="true" \
-e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \ -e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \
-e WATCHTOWER_NOTIFICATION_TEMPLATE=" -e WATCHTOWER_NOTIFICATION_TEMPLATE="
{{- if .Report -}} {{- if .Report -}}
@ -130,7 +130,7 @@ Example using a custom report template that always sends a session report after
{{- end -}} {{- end -}}
{{- end -}} {{- end -}}
{{- else -}} {{- else -}}
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}} {{range .Entries -}}{{.Message}}{{\"\n\"}}{{- end -}}
{{- end -}} {{- end -}}
" \ " \
containrrr/watchtower containrrr/watchtower

42
go.mod
View file

@ -3,21 +3,21 @@ module github.com/containrrr/watchtower
go 1.18 go 1.18
require ( require (
github.com/containrrr/shoutrrr v0.6.1 github.com/containrrr/shoutrrr v0.7.1
github.com/docker/cli v20.10.22+incompatible github.com/docker/cli v23.0.3+incompatible
github.com/docker/distribution v2.8.1+incompatible github.com/docker/distribution v2.8.1+incompatible
github.com/docker/docker v20.10.22+incompatible github.com/docker/docker v23.0.3+incompatible
github.com/docker/go-connections v0.4.0 github.com/docker/go-connections v0.4.0
github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.24.2 github.com/onsi/gomega v1.27.6
github.com/prometheus/client_golang v1.14.0 github.com/prometheus/client_golang v1.14.0
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 github.com/robfig/cron v1.2.0
github.com/sirupsen/logrus v1.9.0 github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.14.0 github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.2
golang.org/x/net v0.5.0 golang.org/x/net v0.9.0
) )
require ( require (
@ -31,13 +31,13 @@ require (
github.com/fatih/color v1.13.0 // indirect github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-cmp v0.5.9 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
@ -45,25 +45,23 @@ require (
github.com/nxadm/tail v1.4.8 // indirect github.com/nxadm/tail v1.4.8 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/spf13/afero v1.9.2 // indirect github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/sys v0.4.0 // indirect golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.6.0 golang.org/x/text v0.9.0
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect golang.org/x/time v0.1.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.0.3 // indirect gotest.tools/v3 v3.0.3 // indirect
) )

670
go.sum

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,6 @@ package actions
import ( import (
"errors" "errors"
"strings"
"github.com/containrrr/watchtower/internal/util" "github.com/containrrr/watchtower/internal/util"
"github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/container"
@ -260,10 +259,6 @@ func UpdateImplicitRestart(containers []container.Container) {
// container marked for restart // container marked for restart
func linkedContainerMarkedForRestart(links []string, containers []container.Container) string { func linkedContainerMarkedForRestart(links []string, containers []container.Container) string {
for _, linkName := range links { for _, linkName := range links {
// Since the container names need to start with '/', let's prepend it if it's missing
if !strings.HasPrefix(linkName, "/") {
linkName = "/" + linkName
}
for _, candidate := range containers { for _, candidate := range containers {
if candidate.Name() == linkName && candidate.ToRestart() { if candidate.Name() == linkName && candidate.ToRestart() {
return linkName return linkName

View file

@ -260,7 +260,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 Container, creation container.CreateResponse) error {
name := c.Name() name := c.Name()
log.Debugf("Starting container %s (%s)", name, t.ContainerID(creation.ID).ShortID()) log.Debugf("Starting container %s (%s)", name, t.ContainerID(creation.ID).ShortID())
@ -280,7 +280,7 @@ func (client dockerClient) RenameContainer(c Container, newName string) error {
func (client dockerClient) IsContainerStale(container Container) (stale bool, latestImage t.ImageID, err error) { func (client dockerClient) IsContainerStale(container Container) (stale bool, latestImage t.ImageID, err error) {
ctx := context.Background() ctx := context.Background()
if !client.PullImages { if !client.PullImages || container.IsNoPull() {
log.Debugf("Skipping image pull.") log.Debugf("Skipping image pull.")
} else if err := client.PullImage(ctx, container); err != nil { } else if err := client.PullImage(ctx, container); err != nil {
return false, container.SafeImageID(), err return false, container.SafeImageID(), err

View file

@ -125,6 +125,22 @@ func (c Container) IsMonitorOnly() bool {
return parsedBool return parsedBool
} }
// IsNoPull returns the value of the no-pull label. If the label is not set
// then false is returned.
func (c Container) IsNoPull() bool {
rawBool, ok := c.getLabelValue(noPullLabel)
if !ok {
return false
}
parsedBool, err := strconv.ParseBool(rawBool)
if err != nil {
return false
}
return parsedBool
}
// Scope returns the value of the scope UID label and if the label // Scope returns the value of the scope UID label and if the label
// was set. // was set.
func (c Container) Scope() (string, bool) { func (c Container) Scope() (string, bool) {
@ -144,7 +160,14 @@ func (c Container) Links() []string {
dependsOnLabelValue := c.getLabelValueOrEmpty(dependsOnLabel) dependsOnLabelValue := c.getLabelValueOrEmpty(dependsOnLabel)
if dependsOnLabelValue != "" { if dependsOnLabelValue != "" {
links := strings.Split(dependsOnLabelValue, ",") for _, link := range strings.Split(dependsOnLabelValue, ",") {
// Since the container names need to start with '/', let's prepend it if it's missing
if !strings.HasPrefix(link, "/") {
link = "/" + link
}
links = append(links, link)
}
return links return links
} }

View file

@ -178,14 +178,21 @@ var _ = Describe("the container", func() {
"com.centurylinklabs.watchtower.depends-on": "postgres", "com.centurylinklabs.watchtower.depends-on": "postgres",
})) }))
links := c.Links() links := c.Links()
Expect(links).To(SatisfyAll(ContainElement("postgres"), HaveLen(1))) Expect(links).To(SatisfyAll(ContainElement("/postgres"), HaveLen(1)))
}) })
It("should fetch depending containers if there are many", func() { It("should fetch depending containers if there are many", func() {
c = MockContainer(WithLabels(map[string]string{ c = MockContainer(WithLabels(map[string]string{
"com.centurylinklabs.watchtower.depends-on": "postgres,redis", "com.centurylinklabs.watchtower.depends-on": "postgres,redis",
})) }))
links := c.Links() links := c.Links()
Expect(links).To(SatisfyAll(ContainElement("postgres"), ContainElement("redis"), HaveLen(2))) Expect(links).To(SatisfyAll(ContainElement("/postgres"), ContainElement("/redis"), HaveLen(2)))
})
It("should only add slashes to names when they are missing", func() {
c = MockContainer(WithLabels(map[string]string{
"com.centurylinklabs.watchtower.depends-on": "/postgres,redis",
}))
links := c.Links()
Expect(links).To(SatisfyAll(ContainElement("/postgres"), ContainElement("/redis")))
}) })
It("should fetch depending containers if label is blank", func() { It("should fetch depending containers if label is blank", func() {
c = MockContainer(WithLabels(map[string]string{ c = MockContainer(WithLabels(map[string]string{
@ -207,6 +214,39 @@ var _ = Describe("the container", func() {
}) })
}) })
When("checking no-pull label", 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()).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()).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()).To(Equal(false))
})
})
When("no-pull label is unset", func() {
c = MockContainer(WithLabels(map[string]string{}))
It("should return false", func() {
Expect(c.IsNoPull()).To(Equal(false))
})
})
})
When("there is a pre or post update timeout", func() { When("there is a pre or post update timeout", func() {
It("should return minute values", func() { It("should return minute values", func() {
c = MockContainer(WithLabels(map[string]string{ c = MockContainer(WithLabels(map[string]string{

View file

@ -1,18 +1,19 @@
package container package container
const ( const (
watchtowerLabel = "com.centurylinklabs.watchtower" watchtowerLabel = "com.centurylinklabs.watchtower"
signalLabel = "com.centurylinklabs.watchtower.stop-signal" signalLabel = "com.centurylinklabs.watchtower.stop-signal"
enableLabel = "com.centurylinklabs.watchtower.enable" enableLabel = "com.centurylinklabs.watchtower.enable"
monitorOnlyLabel = "com.centurylinklabs.watchtower.monitor-only" monitorOnlyLabel = "com.centurylinklabs.watchtower.monitor-only"
dependsOnLabel = "com.centurylinklabs.watchtower.depends-on" noPullLabel = "com.centurylinklabs.watchtower.no-pull"
zodiacLabel = "com.centurylinklabs.zodiac.original-image" dependsOnLabel = "com.centurylinklabs.watchtower.depends-on"
scope = "com.centurylinklabs.watchtower.scope" zodiacLabel = "com.centurylinklabs.zodiac.original-image"
preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check" scope = "com.centurylinklabs.watchtower.scope"
postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check" preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check"
preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update" postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check"
postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update" preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update"
preUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout" postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update"
preUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout"
postUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.post-update-timeout" postUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.post-update-timeout"
) )

View file

@ -112,7 +112,6 @@ func ListContainersHandler(statuses ...string) http.HandlerFunc {
bytes, err := filterArgs.MarshalJSON() bytes, err := filterArgs.MarshalJSON()
O.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred()) O.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred())
query := url.Values{ query := url.Values{
"limit": []string{"0"},
"filters": []string{string(bytes)}, "filters": []string{string(bytes)},
} }
return ghttp.CombineHandlers( return ghttp.CombineHandlers(

View file

@ -35,5 +35,6 @@ var commonTemplates = map[string]string{
no containers matched filter no containers matched filter
{{- end -}} {{- end -}}
{{- end -}}`, {{- end -}}`,
}
`json.v1`: `{{ . | ToJSON }}`,
}

View file

@ -63,6 +63,7 @@ func (e *emailTypeNotifier) GetURL(c *cobra.Command) (string, error) {
UseHTML: false, UseHTML: false,
Encryption: shoutrrrSmtp.EncMethods.Auto, Encryption: shoutrrrSmtp.EncMethods.Auto,
Auth: shoutrrrSmtp.AuthTypes.None, Auth: shoutrrrSmtp.AuthTypes.None,
ClientHost: "localhost",
} }
if len(e.User) > 0 { if len(e.User) > 0 {

71
pkg/notifications/json.go Normal file
View file

@ -0,0 +1,71 @@
package notifications
import (
"encoding/json"
t "github.com/containrrr/watchtower/pkg/types"
)
type jsonMap = map[string]interface{}
// MarshalJSON implements json.Marshaler
func (d Data) MarshalJSON() ([]byte, error) {
var entries = make([]jsonMap, len(d.Entries))
for i, entry := range d.Entries {
entries[i] = jsonMap{
`level`: entry.Level,
`message`: entry.Message,
`data`: entry.Data,
`time`: entry.Time,
}
}
var report jsonMap
if d.Report != nil {
report = jsonMap{
`scanned`: marshalReports(d.Report.Scanned()),
`updated`: marshalReports(d.Report.Updated()),
`failed`: marshalReports(d.Report.Failed()),
`skipped`: marshalReports(d.Report.Skipped()),
`stale`: marshalReports(d.Report.Stale()),
`fresh`: marshalReports(d.Report.Fresh()),
}
}
return json.Marshal(jsonMap{
`report`: report,
`title`: d.Title,
`host`: d.Host,
`entries`: entries,
})
}
func marshalReports(reports []t.ContainerReport) []jsonMap {
jsonReports := make([]jsonMap, len(reports))
for i, report := range reports {
jsonReports[i] = jsonMap{
`id`: report.ID().ShortID(),
`name`: report.Name(),
`currentImageId`: report.CurrentImageID().ShortID(),
`latestImageId`: report.LatestImageID().ShortID(),
`imageName`: report.ImageName(),
`state`: report.State(),
}
if errorMessage := report.Error(); errorMessage != "" {
jsonReports[i][`error`] = errorMessage
}
}
return jsonReports
}
var _ json.Marshaler = &Data{}
func toJSON(v interface{}) string {
var bytes []byte
var err error
if bytes, err = json.MarshalIndent(v, "", " "); err != nil {
LocalLog.Errorf("failed to marshal JSON in notification template: %v", err)
return ""
}
return string(bytes)
}

View file

@ -0,0 +1,118 @@
package notifications
import (
s "github.com/containrrr/watchtower/pkg/session"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("JSON template", func() {
When("using report templates", func() {
When("JSON template is used", func() {
It("should format the messages to the expected format", func() {
expected := `{
"entries": [
{
"data": null,
"level": "info",
"message": "foo Bar",
"time": "0001-01-01T00:00:00Z"
}
],
"host": "Mock",
"report": {
"failed": [
{
"currentImageId": "01d210000000",
"error": "accidentally the whole container",
"id": "c79210000000",
"imageName": "mock/fail1:latest",
"latestImageId": "d0a210000000",
"name": "fail1",
"state": "Failed"
}
],
"fresh": [
{
"currentImageId": "01d310000000",
"id": "c79310000000",
"imageName": "mock/frsh1:latest",
"latestImageId": "01d310000000",
"name": "frsh1",
"state": "Fresh"
}
],
"scanned": [
{
"currentImageId": "01d110000000",
"id": "c79110000000",
"imageName": "mock/updt1:latest",
"latestImageId": "d0a110000000",
"name": "updt1",
"state": "Updated"
},
{
"currentImageId": "01d120000000",
"id": "c79120000000",
"imageName": "mock/updt2:latest",
"latestImageId": "d0a120000000",
"name": "updt2",
"state": "Updated"
},
{
"currentImageId": "01d210000000",
"error": "accidentally the whole container",
"id": "c79210000000",
"imageName": "mock/fail1:latest",
"latestImageId": "d0a210000000",
"name": "fail1",
"state": "Failed"
},
{
"currentImageId": "01d310000000",
"id": "c79310000000",
"imageName": "mock/frsh1:latest",
"latestImageId": "01d310000000",
"name": "frsh1",
"state": "Fresh"
}
],
"skipped": [
{
"currentImageId": "01d410000000",
"error": "unpossible",
"id": "c79410000000",
"imageName": "mock/skip1:latest",
"latestImageId": "01d410000000",
"name": "skip1",
"state": "Skipped"
}
],
"stale": [],
"updated": [
{
"currentImageId": "01d110000000",
"id": "c79110000000",
"imageName": "mock/updt1:latest",
"latestImageId": "d0a110000000",
"name": "updt1",
"state": "Updated"
},
{
"currentImageId": "01d120000000",
"id": "c79120000000",
"imageName": "mock/updt2:latest",
"latestImageId": "d0a120000000",
"name": "updt2",
"state": "Updated"
}
]
},
"title": "Watchtower updates on Mock"
}`
data := mockDataFromStates(s.UpdatedState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState)
Expect(getTemplatedResult(`json.v1`, false, data)).To(MatchJSON(expected))
})
})
})
})

View file

@ -0,0 +1,19 @@
package notifications
import (
t "github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus"
)
// StaticData is the part of the notification template data model set upon initialization
type StaticData struct {
Title string
Host string
}
// Data is the notification template data model
type Data struct {
StaticData
Entries []*log.Entry
Report t.Report
}

View file

@ -210,6 +210,7 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
funcs := template.FuncMap{ funcs := template.FuncMap{
"ToUpper": strings.ToUpper, "ToUpper": strings.ToUpper,
"ToLower": strings.ToLower, "ToLower": strings.ToLower,
"ToJSON": toJSON,
"Title": cases.Title(language.AmericanEnglish).String, "Title": cases.Title(language.AmericanEnglish).String,
} }
tplBase := template.New("").Funcs(funcs) tplBase := template.New("").Funcs(funcs)
@ -240,16 +241,3 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
return 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
}

View file

@ -88,7 +88,8 @@ func GetBearerHeader(challenge string, imageRef ref.Named, registryAuth string)
if registryAuth != "" { if registryAuth != "" {
logrus.Debug("Credentials found.") logrus.Debug("Credentials found.")
logrus.Tracef("Credentials: %v", registryAuth) // CREDENTIAL: Uncomment to log registry credentials
// logrus.Tracef("Credentials: %v", registryAuth)
r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth)) r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth))
} else { } else {
logrus.Debug("No credentials found.") logrus.Debug("No credentials found.")
@ -120,10 +121,9 @@ func GetAuthURL(challenge string, imageRef ref.Named) (*url.URL, error) {
for _, pair := range pairs { for _, pair := range pairs {
trimmed := strings.Trim(pair, " ") trimmed := strings.Trim(pair, " ")
kv := strings.Split(trimmed, "=") if key, val, ok := strings.Cut(trimmed, "="); ok {
key := kv[0] values[key] = strings.Trim(val, `"`)
val := strings.Trim(kv[1], "\"") }
values[key] = val
} }
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"realm": values["realm"], "realm": values["realm"],

View file

@ -10,6 +10,7 @@ import (
"github.com/containrrr/watchtower/internal/actions/mocks" "github.com/containrrr/watchtower/internal/actions/mocks"
"github.com/containrrr/watchtower/pkg/registry/auth" "github.com/containrrr/watchtower/pkg/registry/auth"
wtTypes "github.com/containrrr/watchtower/pkg/types" wtTypes "github.com/containrrr/watchtower/pkg/types"
ref "github.com/docker/distribution/reference" ref "github.com/docker/distribution/reference"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
@ -109,6 +110,18 @@ var _ = Describe("the auth module", func() {
Expect(getScopeFromImageAuthURL("ghcr.io/containrrr/watchtower")).To(Equal("containrrr/watchtower")) Expect(getScopeFromImageAuthURL("ghcr.io/containrrr/watchtower")).To(Equal("containrrr/watchtower"))
}) })
}) })
It("should not crash when an empty field is recieved", func() {
input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",`
res, err := auth.GetAuthURL(input, "containrrr/watchtower")
Expect(err).NotTo(HaveOccurred())
Expect(res).NotTo(BeNil())
})
It("should not crash when a field without a value is recieved", func() {
input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",valuelesskey`
res, err := auth.GetAuthURL(input, "containrrr/watchtower")
Expect(err).NotTo(HaveOccurred())
Expect(res).NotTo(BeNil())
})
}) })
Describe("GetChallengeURL", func() { Describe("GetChallengeURL", func() {

View file

@ -6,15 +6,16 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net"
"net/http"
"strings"
"time"
"github.com/containrrr/watchtower/internal/meta" "github.com/containrrr/watchtower/internal/meta"
"github.com/containrrr/watchtower/pkg/registry/auth" "github.com/containrrr/watchtower/pkg/registry/auth"
"github.com/containrrr/watchtower/pkg/registry/manifest" "github.com/containrrr/watchtower/pkg/registry/manifest"
"github.com/containrrr/watchtower/pkg/types" "github.com/containrrr/watchtower/pkg/types"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net"
"net/http"
"strings"
"time"
) )
// ContentDigestHeader is the key for the key-value pair containing the digest header // ContentDigestHeader is the key for the key-value pair containing the digest header
@ -25,7 +26,7 @@ func CompareDigest(container types.Container, registryAuth string) (bool, error)
if !container.HasImageInfo() { if !container.HasImageInfo() {
return false, errors.New("container image info missing") return false, errors.New("container image info missing")
} }
var digest string var digest string
registryAuth = TransformAuth(registryAuth) registryAuth = TransformAuth(registryAuth)
@ -93,16 +94,18 @@ func GetDigest(url string, token string) (string, error) {
req, _ := http.NewRequest("HEAD", url, nil) req, _ := http.NewRequest("HEAD", url, nil)
req.Header.Set("User-Agent", meta.UserAgent) req.Header.Set("User-Agent", meta.UserAgent)
if token != "" { if token == "" {
logrus.WithField("token", token).Trace("Setting request token")
} else {
return "", errors.New("could not fetch token") return "", errors.New("could not fetch token")
} }
// CREDENTIAL: Uncomment to log the request token
// logrus.WithField("token", token).Trace("Setting request token")
req.Header.Add("Authorization", token) req.Header.Add("Authorization", token)
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json") req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v2+json") req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v2+json")
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v1+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") logrus.WithField("url", url).Debug("Doing a HEAD request to fetch a digest")

View file

@ -19,7 +19,9 @@ func GetPullOptions(imageName string) (types.ImagePullOptions, error) {
if auth == "" { if auth == "" {
return types.ImagePullOptions{}, nil return types.ImagePullOptions{}, nil
} }
log.Tracef("Got auth value: %s", auth)
// CREDENTIAL: Uncomment to log docker config auth
// log.Tracef("Got auth value: %s", auth)
return types.ImagePullOptions{ return types.ImagePullOptions{
RegistryAuth: auth, RegistryAuth: auth,

View file

@ -36,8 +36,11 @@ func EncodedEnvAuth() (string, error) {
Username: username, Username: username,
Password: password, Password: password,
} }
log.Debugf("Loaded auth credentials for registry user %s from environment", auth.Username) log.Debugf("Loaded auth credentials for registry user %s from environment", auth.Username)
log.Tracef("Using auth password %s", auth.Password) // CREDENTIAL: Uncomment to log REPO_PASS environment variable
// log.Tracef("Using auth password %s", auth.Password)
return EncodeAuth(auth) return EncodeAuth(auth)
} }
return "", errors.New("registry auth environment variables (REPO_USER, REPO_PASS) not set") return "", errors.New("registry auth environment variables (REPO_USER, REPO_PASS) not set")
@ -71,7 +74,8 @@ func EncodedConfigAuth(imageRef string) (string, error) {
return "", nil return "", nil
} }
log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, server, configFile.Filename) log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, server, configFile.Filename)
log.Tracef("Using auth password %s", auth.Password) // CREDENTIAL: Uncomment to log docker config password
// log.Tracef("Using auth password %s", auth.Password)
return EncodeAuth(auth) return EncodeAuth(auth)
} }