mirror of
https://github.com/containrrr/watchtower.git
synced 2025-12-16 15:10:12 +01:00
Merge branch 'main' into fix/ref-processing
This commit is contained in:
commit
c5c37f8795
26 changed files with 945 additions and 189 deletions
8
.github/workflows/pull-request.yml
vendored
8
.github/workflows/pull-request.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
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@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
||||
|
|
@ -41,7 +41,7 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
- name: Run tests
|
||||
|
|
@ -60,11 +60,11 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
- name: Build
|
||||
uses: goreleaser/goreleaser-action@8f67e590f2d095516493f017008adc464e63adb1 #v3
|
||||
uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b #v3
|
||||
with:
|
||||
version: v0.155.0
|
||||
args: --snapshot --skip-publish --debug
|
||||
|
|
|
|||
4
.github/workflows/release-dev.yaml
vendored
4
.github/workflows/release-dev.yaml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
- name: Build
|
||||
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
- name: Test
|
||||
|
|
|
|||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
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@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
|
||||
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
- name: Run tests
|
||||
|
|
@ -66,7 +66,7 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
- name: Login to Docker Hub
|
||||
|
|
@ -81,7 +81,7 @@ jobs:
|
|||
password: ${{ secrets.BOT_GHCR_PAT }}
|
||||
registry: ghcr.io
|
||||
- name: Build
|
||||
uses: goreleaser/goreleaser-action@8f67e590f2d095516493f017008adc464e63adb1 #v3
|
||||
uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b #v3
|
||||
with:
|
||||
version: v0.155.0
|
||||
args: --debug
|
||||
|
|
@ -191,7 +191,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
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -182,7 +182,10 @@ 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
|
||||
// we need to trigger the startup messages manually.
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
ca-certificates \
|
||||
|
|
|
|||
|
|
@ -234,6 +234,9 @@ 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.
|
||||
|
||||
## Without sending a startup message
|
||||
Do not send a message after watchtower started. Otherwise there will be an info-level notification.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
||||
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
|
||||
URLs. (See example below)
|
||||
|
||||
|
|
@ -110,7 +110,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 +130,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
|
||||
|
|
|
|||
42
go.mod
42
go.mod
|
|
@ -3,21 +3,21 @@ module github.com/containrrr/watchtower
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/containrrr/shoutrrr v0.6.1
|
||||
github.com/docker/cli v20.10.22+incompatible
|
||||
github.com/containrrr/shoutrrr v0.7.1
|
||||
github.com/docker/cli v23.0.3+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/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/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
||||
github.com/robfig/cron v1.2.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/viper v1.14.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/net v0.5.0
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
golang.org/x/net v0.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -31,13 +31,13 @@ require (
|
|||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // 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/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/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // 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/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.0.6 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/spf13/afero v1.9.2 // indirect
|
||||
github.com/spf13/afero v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/text v0.9.0
|
||||
golang.org/x/time v0.1.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // 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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package actions
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/util"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
|
|
@ -260,10 +259,6 @@ func UpdateImplicitRestart(containers []container.Container) {
|
|||
// container marked for restart
|
||||
func linkedContainerMarkedForRestart(links []string, containers []container.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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
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) {
|
||||
ctx := context.Background()
|
||||
|
||||
if !client.PullImages {
|
||||
if !client.PullImages || container.IsNoPull() {
|
||||
log.Debugf("Skipping image pull.")
|
||||
} else if err := client.PullImage(ctx, container); err != nil {
|
||||
return false, container.SafeImageID(), err
|
||||
|
|
|
|||
|
|
@ -125,6 +125,22 @@ func (c Container) IsMonitorOnly() bool {
|
|||
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
|
||||
// was set.
|
||||
func (c Container) Scope() (string, bool) {
|
||||
|
|
@ -144,7 +160,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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -178,14 +178,21 @@ var _ = Describe("the container", func() {
|
|||
"com.centurylinklabs.watchtower.depends-on": "postgres",
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("postgres"), HaveLen(1)))
|
||||
Expect(links).To(SatisfyAll(ContainElement("/postgres"), HaveLen(1)))
|
||||
})
|
||||
It("should fetch depending containers if there are many", func() {
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.depends-on": "postgres,redis",
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("postgres"), ContainElement("redis"), HaveLen(2)))
|
||||
Expect(links).To(SatisfyAll(ContainElement("/postgres"), ContainElement("/redis"), HaveLen(2)))
|
||||
})
|
||||
It("should only add slashes to names when they are missing", func() {
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
"com.centurylinklabs.watchtower.depends-on": "/postgres,redis",
|
||||
}))
|
||||
links := c.Links()
|
||||
Expect(links).To(SatisfyAll(ContainElement("/postgres"), ContainElement("/redis")))
|
||||
})
|
||||
It("should fetch depending containers if label is blank", func() {
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
|
|
@ -207,6 +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() {
|
||||
It("should return minute values", func() {
|
||||
c = MockContainer(WithLabels(map[string]string{
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
package container
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ func ListContainersHandler(statuses ...string) http.HandlerFunc {
|
|||
bytes, err := filterArgs.MarshalJSON()
|
||||
O.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred())
|
||||
query := url.Values{
|
||||
"limit": []string{"0"},
|
||||
"filters": []string{string(bytes)},
|
||||
}
|
||||
return ghttp.CombineHandlers(
|
||||
|
|
|
|||
|
|
@ -35,5 +35,6 @@ var commonTemplates = map[string]string{
|
|||
no containers matched filter
|
||||
{{- end -}}
|
||||
{{- end -}}`,
|
||||
}
|
||||
|
||||
`json.v1`: `{{ . | ToJSON }}`,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
71
pkg/notifications/json.go
Normal file
71
pkg/notifications/json.go
Normal 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)
|
||||
}
|
||||
118
pkg/notifications/json_test.go
Normal file
118
pkg/notifications/json_test.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
s "github.com/containrrr/watchtower/pkg/session"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("JSON template", func() {
|
||||
When("using report templates", func() {
|
||||
When("JSON template is used", func() {
|
||||
It("should format the messages to the expected format", func() {
|
||||
expected := `{
|
||||
"entries": [
|
||||
{
|
||||
"data": null,
|
||||
"level": "info",
|
||||
"message": "foo Bar",
|
||||
"time": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"host": "Mock",
|
||||
"report": {
|
||||
"failed": [
|
||||
{
|
||||
"currentImageId": "01d210000000",
|
||||
"error": "accidentally the whole container",
|
||||
"id": "c79210000000",
|
||||
"imageName": "mock/fail1:latest",
|
||||
"latestImageId": "d0a210000000",
|
||||
"name": "fail1",
|
||||
"state": "Failed"
|
||||
}
|
||||
],
|
||||
"fresh": [
|
||||
{
|
||||
"currentImageId": "01d310000000",
|
||||
"id": "c79310000000",
|
||||
"imageName": "mock/frsh1:latest",
|
||||
"latestImageId": "01d310000000",
|
||||
"name": "frsh1",
|
||||
"state": "Fresh"
|
||||
}
|
||||
],
|
||||
"scanned": [
|
||||
{
|
||||
"currentImageId": "01d110000000",
|
||||
"id": "c79110000000",
|
||||
"imageName": "mock/updt1:latest",
|
||||
"latestImageId": "d0a110000000",
|
||||
"name": "updt1",
|
||||
"state": "Updated"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d120000000",
|
||||
"id": "c79120000000",
|
||||
"imageName": "mock/updt2:latest",
|
||||
"latestImageId": "d0a120000000",
|
||||
"name": "updt2",
|
||||
"state": "Updated"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d210000000",
|
||||
"error": "accidentally the whole container",
|
||||
"id": "c79210000000",
|
||||
"imageName": "mock/fail1:latest",
|
||||
"latestImageId": "d0a210000000",
|
||||
"name": "fail1",
|
||||
"state": "Failed"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d310000000",
|
||||
"id": "c79310000000",
|
||||
"imageName": "mock/frsh1:latest",
|
||||
"latestImageId": "01d310000000",
|
||||
"name": "frsh1",
|
||||
"state": "Fresh"
|
||||
}
|
||||
],
|
||||
"skipped": [
|
||||
{
|
||||
"currentImageId": "01d410000000",
|
||||
"error": "unpossible",
|
||||
"id": "c79410000000",
|
||||
"imageName": "mock/skip1:latest",
|
||||
"latestImageId": "01d410000000",
|
||||
"name": "skip1",
|
||||
"state": "Skipped"
|
||||
}
|
||||
],
|
||||
"stale": [],
|
||||
"updated": [
|
||||
{
|
||||
"currentImageId": "01d110000000",
|
||||
"id": "c79110000000",
|
||||
"imageName": "mock/updt1:latest",
|
||||
"latestImageId": "d0a110000000",
|
||||
"name": "updt1",
|
||||
"state": "Updated"
|
||||
},
|
||||
{
|
||||
"currentImageId": "01d120000000",
|
||||
"id": "c79120000000",
|
||||
"imageName": "mock/updt2:latest",
|
||||
"latestImageId": "d0a120000000",
|
||||
"name": "updt2",
|
||||
"state": "Updated"
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "Watchtower updates on Mock"
|
||||
}`
|
||||
data := mockDataFromStates(s.UpdatedState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState)
|
||||
Expect(getTemplatedResult(`json.v1`, false, data)).To(MatchJSON(expected))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
19
pkg/notifications/model.go
Normal file
19
pkg/notifications/model.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// StaticData is the part of the notification template data model set upon initialization
|
||||
type StaticData struct {
|
||||
Title string
|
||||
Host string
|
||||
}
|
||||
|
||||
// Data is the notification template data model
|
||||
type Data struct {
|
||||
StaticData
|
||||
Entries []*log.Entry
|
||||
Report t.Report
|
||||
}
|
||||
|
|
@ -210,6 +210,7 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
|
|||
funcs := template.FuncMap{
|
||||
"ToUpper": strings.ToUpper,
|
||||
"ToLower": strings.ToLower,
|
||||
"ToJSON": toJSON,
|
||||
"Title": cases.Title(language.AmericanEnglish).String,
|
||||
}
|
||||
tplBase := template.New("").Funcs(funcs)
|
||||
|
|
@ -240,16 +241,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ func GetBearerHeader(challenge string, imageRef ref.Named, registryAuth 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.")
|
||||
|
|
@ -120,10 +121,9 @@ func GetAuthURL(challenge string, imageRef ref.Named) (*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"],
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/containrrr/watchtower/internal/actions/mocks"
|
||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
||||
|
||||
wtTypes "github.com/containrrr/watchtower/pkg/types"
|
||||
ref "github.com/docker/distribution/reference"
|
||||
. "github.com/onsi/ginkgo"
|
||||
|
|
@ -109,6 +110,18 @@ var _ = Describe("the auth module", func() {
|
|||
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() {
|
||||
|
|
|
|||
|
|
@ -6,15 +6,16 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/meta"
|
||||
"github.com/containrrr/watchtower/pkg/registry/auth"
|
||||
"github.com/containrrr/watchtower/pkg/registry/manifest"
|
||||
"github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ContentDigestHeader is the key for the key-value pair containing the digest header
|
||||
|
|
@ -93,16 +94,18 @@ func GetDigest(url string, token string) (string, error) {
|
|||
req, _ := http.NewRequest("HEAD", url, nil)
|
||||
req.Header.Set("User-Agent", meta.UserAgent)
|
||||
|
||||
if token != "" {
|
||||
logrus.WithField("token", token).Trace("Setting request token")
|
||||
} else {
|
||||
if token == "" {
|
||||
return "", errors.New("could not fetch token")
|
||||
}
|
||||
|
||||
// CREDENTIAL: Uncomment to log the request token
|
||||
// logrus.WithField("token", token).Trace("Setting request token")
|
||||
|
||||
req.Header.Add("Authorization", token)
|
||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")
|
||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v2+json")
|
||||
req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v1+json")
|
||||
req.Header.Add("Accept", "application/vnd.oci.image.index.v1+json")
|
||||
|
||||
logrus.WithField("url", url).Debug("Doing a HEAD request to fetch a digest")
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -36,8 +36,11 @@ func EncodedEnvAuth() (string, error) {
|
|||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
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 "", errors.New("registry auth environment variables (REPO_USER, REPO_PASS) not set")
|
||||
|
|
@ -71,7 +74,8 @@ func EncodedConfigAuth(imageRef string) (string, error) {
|
|||
return "", nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue