Merge branch 'main' into fix/container-interface

This commit is contained in:
nils måsén 2023-04-12 17:17:28 +02:00 committed by GitHub
commit f28ffc611f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1207 additions and 486 deletions

View file

@ -0,0 +1,12 @@
name: Auto approve dependabot PRs
on: pull_request_target
jobs:
auto-approve:
runs-on: ubuntu-latest
permissions:
pull-requests: write
if: github.actor == 'dependabot[bot]'
steps:
- uses: hmarr/auto-approve-action@v3

View file

@ -16,10 +16,10 @@ 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@a3513ade2e5cb8075ba1c1ed1890a989cf0f2aa0 #v1.2.0 - uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
with: with:
version: "2022.1.1" version: "2022.1.1"
install-go: "false" # StaticCheck uses go v1.17 which does not support `any` install-go: "false" # StaticCheck uses go v1.17 which does not support `any`
@ -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,10 +19,10 @@ 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@a3513ade2e5cb8075ba1c1ed1890a989cf0f2aa0 #v1.2.0 - uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0
with: with:
version: "2022.1.1" version: "2022.1.1"
install-go: "false" # StaticCheck uses go v1.17 which does not support `any` install-go: "false" # StaticCheck uses go v1.17 which does not support `any`
@ -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.0 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

@ -4,7 +4,7 @@
Metrics can be used to track how Watchtower behaves over time. Metrics can be used to track how Watchtower behaves over time.
To use this feature, you have to set an [API token](arguments.md#http-api-token) and [enable the metrics API](arguments.md#http-api-metrics), To use this feature, you have to set an [API token](arguments.md#http_api_token) and [enable the metrics API](arguments.md#http_api_metrics),
as well as creating a port mapping for your container for port `8080`. as well as creating a port mapping for your container for port `8080`.
The metrics API endpoint is `/v1/metrics`. The metrics API endpoint is `/v1/metrics`.

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

View file

@ -23,19 +23,29 @@ password `auth` string:
``` ```
`<REGISTRY_NAME>` needs to be replaced by the name of your private registry `<REGISTRY_NAME>` needs to be replaced by the name of your private registry
(e.g., `my-private-registry.example.org`) (e.g., `my-private-registry.example.org`).
!!! important "Using private images on docker hub" !!! info "Using private images on Docker Hub"
When using private images on docker hub, the containers beeing watched needs to use the full image name, including the repository prefix `index.docker.io`. To access private repositories on Docker Hub,
So instead of `<REGISTRY_NAME>` should be `https://index.docker.io/v1/`.
``` In this special case, the registry domain does not have to be specified
docker run -d myuser/myimage in `docker run` or `docker-compose`. Like Docker, Watchtower will use the
``` Docker Hub registry and its credentials when no registry domain is specified.
you would run it as
```
docker run -d index.docker.io/myuser/myimage
```
<sub>Watchtower will recognize credentials with `<REGISTRY_NAME>` `index.docker.io`,
but the Docker CLI will not.</sub>
!!! important "Using a private registry on a local host"
To use a private registry hosted locally, make sure to correctly specify the registry host
in both `config.json` and the `docker run` command or `docker-compose` file.
Valid hosts are `localhost[:PORT]`, `HOST:PORT`,
or any multi-part `domain.name` or IP-address with or without a port.
Examples:
* `localhost` -> `localhost/myimage`
* `127.0.0.1` -> `127.0.0.1/myimage:mytag`
* `host.domain` -> `host.domain/myorganization/myimage`
* `other-lan-host:80` -> `other-lan-host:80/imagename:latest`
The required `auth` string can be generated as follows: The required `auth` string can be generated as follows:
@ -75,7 +85,7 @@ When creating the watchtower container via docker-compose, use the following lin
version: "3.4" version: "3.4"
services: services:
watchtower: watchtower:
image: index.docker.io/containrrr/watchtower:latest image: containrrr/watchtower:latest
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- <PATH_TO_HOME_DIR>/.docker/config.json:/config.json - <PATH_TO_HOME_DIR>/.docker/config.json:/config.json

View file

@ -48,14 +48,14 @@ docker run -d \
If you mount the config file as described above, be sure to also prepend the URL for the registry when starting up your If you mount the config file as described above, be sure to also prepend the URL for the registry when starting up your
watched image (you can omit the https://). Here is a complete docker-compose.yml file that starts up a docker container watched image (you can omit the https://). Here is a complete docker-compose.yml file that starts up a docker container
from a private repo at Docker Hub and monitors it with watchtower. Note the command argument changing the interval to from a private repo on the GitHub Registry and monitors it with watchtower. Note the command argument changing the interval
30s rather than the default 24 hours. to 30s rather than the default 24 hours.
```yaml ```yaml
version: "3" version: "3"
services: services:
cavo: cavo:
image: index.docker.io/<org>/<image>:<tag> image: ghcr.io/<org>/<image>:<tag>
ports: ports:
- "443:3443" - "443:3443"
- "80:3080" - "80:3080"

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.4.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.3.0 // indirect golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.5.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 []types.Container) {
// container marked for restart // container marked for restart
func linkedContainerMarkedForRestart(links []string, containers []types.Container) string { func linkedContainerMarkedForRestart(links []string, containers []types.Container) string {
for _, linkName := range links { for _, linkName := range links {
// Since the container names need to start with '/', let's prepend it if it's missing
if !strings.HasPrefix(linkName, "/") {
linkName = "/" + linkName
}
for _, candidate := range containers { for _, candidate := range containers {
if candidate.Name() == linkName && candidate.ToRestart() { if candidate.Name() == linkName && candidate.ToRestart() {
return linkName return linkName

View file

@ -260,7 +260,7 @@ func (client dockerClient) StartContainer(c t.Container) (t.ContainerID, error)
} }
func (client dockerClient) doStartContainer(bg context.Context, c t.Container, creation container.ContainerCreateCreatedBody) error { func (client dockerClient) doStartContainer(bg context.Context, c t.Container, creation container.CreateResponse) error {
name := c.Name() name := c.Name()
log.Debugf("Starting container %s (%s)", name, t.ContainerID(creation.ID).ShortID()) log.Debugf("Starting container %s (%s)", name, t.ContainerID(creation.ID).ShortID())
@ -280,7 +280,7 @@ func (client dockerClient) RenameContainer(c t.Container, newName string) error
func (client dockerClient) IsContainerStale(container t.Container) (stale bool, latestImage t.ImageID, err error) { func (client dockerClient) IsContainerStale(container t.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

@ -145,6 +145,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) {
@ -164,7 +180,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

@ -5,6 +5,7 @@ const (
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"
noPullLabel = "com.centurylinklabs.watchtower.no-pull"
dependsOnLabel = "com.centurylinklabs.watchtower.depends-on" dependsOnLabel = "com.centurylinklabs.watchtower.depends-on"
zodiacLabel = "com.centurylinklabs.zodiac.original-image" zodiacLabel = "com.centurylinklabs.zodiac.original-image"
scope = "com.centurylinklabs.watchtower.scope" scope = "com.centurylinklabs.watchtower.scope"

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

@ -4,14 +4,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"github.com/containrrr/watchtower/pkg/registry/helpers" "github.com/containrrr/watchtower/pkg/registry/helpers"
"github.com/containrrr/watchtower/pkg/types" "github.com/containrrr/watchtower/pkg/types"
"github.com/docker/distribution/reference" ref "github.com/docker/distribution/reference"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -20,13 +20,13 @@ const ChallengeHeader = "WWW-Authenticate"
// GetToken fetches a token for the registry hosting the provided image // GetToken fetches a token for the registry hosting the provided image
func GetToken(container types.Container, registryAuth string) (string, error) { func GetToken(container types.Container, registryAuth string) (string, error) {
var err error normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName())
var URL url.URL if err != nil {
if URL, err = GetChallengeURL(container.ImageName()); err != nil {
return "", err return "", err
} }
logrus.WithField("URL", URL.String()).Debug("Building challenge URL")
URL := GetChallengeURL(normalizedRef)
logrus.WithField("URL", URL.String()).Debug("Built challenge URL")
var req *http.Request var req *http.Request
if req, err = GetChallengeRequest(URL); err != nil { if req, err = GetChallengeRequest(URL); err != nil {
@ -55,7 +55,7 @@ func GetToken(container types.Container, registryAuth string) (string, error) {
return fmt.Sprintf("Basic %s", registryAuth), nil return fmt.Sprintf("Basic %s", registryAuth), nil
} }
if strings.HasPrefix(challenge, "bearer") { if strings.HasPrefix(challenge, "bearer") {
return GetBearerHeader(challenge, container.ImageName(), registryAuth) return GetBearerHeader(challenge, normalizedRef, registryAuth)
} }
return "", errors.New("unsupported challenge type from registry") return "", errors.New("unsupported challenge type from registry")
@ -73,12 +73,9 @@ func GetChallengeRequest(URL url.URL) (*http.Request, error) {
} }
// GetBearerHeader tries to fetch a bearer token from the registry based on the challenge instructions // GetBearerHeader tries to fetch a bearer token from the registry based on the challenge instructions
func GetBearerHeader(challenge string, img string, registryAuth string) (string, error) { func GetBearerHeader(challenge string, imageRef ref.Named, registryAuth string) (string, error) {
client := http.Client{} client := http.Client{}
if strings.Contains(img, ":") { authURL, err := GetAuthURL(challenge, imageRef)
img = strings.Split(img, ":")[0]
}
authURL, err := GetAuthURL(challenge, img)
if err != nil { if err != nil {
return "", err return "", err
@ -91,7 +88,8 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string,
if registryAuth != "" { if registryAuth != "" {
logrus.Debug("Credentials found.") logrus.Debug("Credentials found.")
logrus.Tracef("Credentials: %v", registryAuth) // CREDENTIAL: Uncomment to log registry credentials
// logrus.Tracef("Credentials: %v", registryAuth)
r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth)) r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth))
} else { } else {
logrus.Debug("No credentials found.") logrus.Debug("No credentials found.")
@ -102,7 +100,7 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string,
return "", err return "", err
} }
body, _ := ioutil.ReadAll(authResponse.Body) body, _ := io.ReadAll(authResponse.Body)
tokenResponse := &types.TokenResponse{} tokenResponse := &types.TokenResponse{}
err = json.Unmarshal(body, tokenResponse) err = json.Unmarshal(body, tokenResponse)
@ -114,7 +112,7 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string,
} }
// GetAuthURL from the instructions in the challenge // GetAuthURL from the instructions in the challenge
func GetAuthURL(challenge string, img string) (*url.URL, error) { func GetAuthURL(challenge string, imageRef ref.Named) (*url.URL, error) {
loweredChallenge := strings.ToLower(challenge) loweredChallenge := strings.ToLower(challenge)
raw := strings.TrimPrefix(loweredChallenge, "bearer") raw := strings.TrimPrefix(loweredChallenge, "bearer")
@ -123,10 +121,9 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) {
for _, pair := range pairs { for _, pair := range pairs {
trimmed := strings.Trim(pair, " ") trimmed := strings.Trim(pair, " ")
kv := strings.Split(trimmed, "=") if key, val, ok := strings.Cut(trimmed, "="); ok {
key := kv[0] values[key] = strings.Trim(val, `"`)
val := strings.Trim(kv[1], "\"") }
values[key] = val
} }
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"realm": values["realm"], "realm": values["realm"],
@ -141,53 +138,25 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) {
q := authURL.Query() q := authURL.Query()
q.Add("service", values["service"]) q.Add("service", values["service"])
scopeImage := GetScopeFromImageName(img, values["service"]) scopeImage := ref.Path(imageRef)
scope := fmt.Sprintf("repository:%s:pull", scopeImage) scope := fmt.Sprintf("repository:%s:pull", scopeImage)
logrus.WithFields(logrus.Fields{"scope": scope, "image": img}).Debug("Setting scope for auth token") logrus.WithFields(logrus.Fields{"scope": scope, "image": imageRef.Name()}).Debug("Setting scope for auth token")
q.Add("scope", scope) q.Add("scope", scope)
authURL.RawQuery = q.Encode() authURL.RawQuery = q.Encode()
return authURL, nil return authURL, nil
} }
// GetScopeFromImageName normalizes an image name for use as scope during auth and head requests // GetChallengeURL returns the URL to check auth requirements
func GetScopeFromImageName(img, svc string) string { // for access to a given image
parts := strings.Split(img, "/") func GetChallengeURL(imageRef ref.Named) url.URL {
host, _ := helpers.GetRegistryAddress(imageRef.Name())
if len(parts) > 2 {
if strings.Contains(svc, "docker.io") {
return fmt.Sprintf("%s/%s", parts[1], strings.Join(parts[2:], "/"))
}
return strings.Join(parts, "/")
}
if len(parts) == 2 {
if strings.Contains(parts[0], "docker.io") {
return fmt.Sprintf("library/%s", parts[1])
}
return strings.Replace(img, svc+"/", "", 1)
}
if strings.Contains(svc, "docker.io") {
return fmt.Sprintf("library/%s", parts[0])
}
return img
}
// GetChallengeURL creates a URL object based on the image info
func GetChallengeURL(img string) (url.URL, error) {
normalizedNamed, _ := reference.ParseNormalizedNamed(img)
host, err := helpers.NormalizeRegistry(normalizedNamed.String())
if err != nil {
return url.URL{}, err
}
URL := url.URL{ URL := url.URL{
Scheme: "https", Scheme: "https",
Host: host, Host: host,
Path: "/v2/", Path: "/v2/",
} }
return URL, nil return URL
} }

View file

@ -2,14 +2,17 @@ package auth_test
import ( import (
"fmt" "fmt"
"github.com/containrrr/watchtower/internal/actions/mocks"
"github.com/containrrr/watchtower/pkg/registry/auth"
"net/url" "net/url"
"os" "os"
"strings"
"testing" "testing"
"time" "time"
"github.com/containrrr/watchtower/internal/actions/mocks"
"github.com/containrrr/watchtower/pkg/registry/auth"
wtTypes "github.com/containrrr/watchtower/pkg/types" wtTypes "github.com/containrrr/watchtower/pkg/types"
ref "github.com/docker/distribution/reference"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
@ -51,7 +54,7 @@ var _ = Describe("the auth module", func() {
mockCreated, mockCreated,
mockDigest) mockDigest)
When("getting an auth url", func() { Describe("GetToken", func() {
It("should parse the token from the response", It("should parse the token from the response",
SkipIfCredentialsEmpty(GHCRCredentials, func() { SkipIfCredentialsEmpty(GHCRCredentials, func() {
creds := fmt.Sprintf("%s:%s", GHCRCredentials.Username, GHCRCredentials.Password) creds := fmt.Sprintf("%s:%s", GHCRCredentials.Username, GHCRCredentials.Password)
@ -60,61 +63,100 @@ var _ = Describe("the auth module", func() {
Expect(token).NotTo(Equal("")) Expect(token).NotTo(Equal(""))
}), }),
) )
})
Describe("GetAuthURL", func() {
It("should create a valid auth url object based on the challenge header supplied", func() { It("should create a valid auth url object based on the challenge header supplied", func() {
input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"` challenge := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"`
imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower")
Expect(err).NotTo(HaveOccurred())
expected := &url.URL{ expected := &url.URL{
Host: "ghcr.io", Host: "ghcr.io",
Scheme: "https", Scheme: "https",
Path: "/token", Path: "/token",
RawQuery: "scope=repository%3Acontainrrr%2Fwatchtower%3Apull&service=ghcr.io", RawQuery: "scope=repository%3Acontainrrr%2Fwatchtower%3Apull&service=ghcr.io",
} }
res, err := auth.GetAuthURL(input, "containrrr/watchtower")
URL, err := auth.GetAuthURL(challenge, imageRef)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal(expected)) Expect(URL).To(Equal(expected))
}) })
It("should create a valid auth url object based on the challenge header supplied", func() {
input := `bearer realm="https://ghcr.io/token"` When("given an invalid challenge header", func() {
res, err := auth.GetAuthURL(input, "containrrr/watchtower") It("should return an error", func() {
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(err).To(HaveOccurred())
Expect(res).To(BeNil()) Expect(URL).To(BeNil())
}) })
}) })
When("getting a challenge url", func() {
It("should create a valid challenge url object based on the image ref supplied", func() { When("deriving the auth scope from an image name", func() {
expected := url.URL{Host: "ghcr.io", Scheme: "https", Path: "/v2/"}
Expect(auth.GetChallengeURL("ghcr.io/containrrr/watchtower:latest")).To(Equal(expected))
})
It("should assume dockerhub if the image ref is not fully qualified", func() {
expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"}
Expect(auth.GetChallengeURL("containrrr/watchtower:latest")).To(Equal(expected))
})
It("should convert legacy dockerhub hostnames to index.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() { It("should prepend official dockerhub images with \"library/\"", func() {
Expect(auth.GetScopeFromImageName("docker.io/registry", "index.docker.io")).To(Equal("library/registry")) Expect(getScopeFromImageAuthURL("registry")).To(Equal("library/registry"))
Expect(auth.GetScopeFromImageName("docker.io/registry", "docker.io")).To(Equal("library/registry")) Expect(getScopeFromImageAuthURL("docker.io/registry")).To(Equal("library/registry"))
Expect(getScopeFromImageAuthURL("index.docker.io/registry")).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() { It("should not include vanity hosts\"", func() {
Expect(auth.GetScopeFromImageName("docker.io/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower")) Expect(getScopeFromImageAuthURL("docker.io/containrrr/watchtower")).To(Equal("containrrr/watchtower"))
Expect(auth.GetScopeFromImageName("index.docker.io/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower")) Expect(getScopeFromImageAuthURL("index.docker.io/containrrr/watchtower")).To(Equal("containrrr/watchtower"))
}) })
It("should not destroy three segment image names\"", func() { It("should not destroy three segment image names\"", func() {
Expect(auth.GetScopeFromImageName("piksel/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower")) Expect(getScopeFromImageAuthURL("piksel/containrrr/watchtower")).To(Equal("piksel/containrrr/watchtower"))
Expect(auth.GetScopeFromImageName("piksel/containrrr/watchtower", "ghcr.io")).To(Equal("piksel/containrrr/watchtower")) Expect(getScopeFromImageAuthURL("ghcr.io/piksel/containrrr/watchtower")).To(Equal("piksel/containrrr/watchtower"))
}) })
It("should not add \"library/\" for one segment image names if they're not on dockerhub", func() { It("should not prepend library/ to image names if they're not on dockerhub", func() {
Expect(auth.GetScopeFromImageName("ghcr.io/watchtower", "ghcr.io")).To(Equal("watchtower")) Expect(getScopeFromImageAuthURL("ghcr.io/watchtower")).To(Equal("watchtower"))
Expect(auth.GetScopeFromImageName("watchtower", "ghcr.io")).To(Equal("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",`
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 recieved", 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())
})
})
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/"}
imageRef, _ := ref.ParseNormalizedNamed("ghcr.io/containrrr/watchtower:latest")
Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected))
})
It("should assume Docker Hub for image refs with no explicit registry", func() {
expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"}
imageRef, _ := ref.ParseNormalizedNamed("containrrr/watchtower:latest")
Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected))
})
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/"}
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)
}

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
@ -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

@ -1,36 +1,28 @@
package helpers package helpers
import ( import (
"fmt" "github.com/docker/distribution/reference"
url2 "net/url"
) )
// ConvertToHostname strips a url from everything but the hostname part // domains for Docker Hub, the default registry
func ConvertToHostname(url string) (string, string, error) { const (
urlWithSchema := fmt.Sprintf("x://%s", url) DefaultRegistryDomain = "docker.io"
u, err := url2.Parse(urlWithSchema) DefaultRegistryHost = "index.docker.io"
if err != nil { LegacyDefaultRegistryDomain = "index.docker.io"
return "", "", err )
}
hostName := u.Hostname()
port := u.Port()
return hostName, port, err // GetRegistryAddress parses an image name
} // and returns the address of the specified registry
func GetRegistryAddress(imageRef string) (string, error) {
// NormalizeRegistry makes sure variations of DockerHubs registry normalizedRef, err := reference.ParseNormalizedNamed(imageRef)
func NormalizeRegistry(registry string) (string, error) {
hostName, port, err := ConvertToHostname(registry)
if err != nil { if err != nil {
return "", err return "", err
} }
if hostName == "registry-1.docker.io" || hostName == "docker.io" { address := reference.Domain(normalizedRef)
hostName = "index.docker.io"
}
if port != "" { if address == DefaultRegistryDomain {
return fmt.Sprintf("%s:%s", hostName, port), nil address = DefaultRegistryHost
} }
return hostName, nil return address, nil
} }

View file

@ -1,9 +1,10 @@
package helpers package helpers
import ( import (
"testing"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"testing"
) )
func TestHelpers(t *testing.T) { func TestHelpers(t *testing.T) {
@ -12,20 +13,25 @@ func TestHelpers(t *testing.T) {
} }
var _ = Describe("the helpers", func() { var _ = Describe("the helpers", func() {
Describe("GetRegistryAddress", func() {
When("converting an url to a hostname", func() { It("should return error if passed empty string", func() {
It("should return docker.io given docker.io/containrrr/watchtower:latest", func() { _, err := GetRegistryAddress("")
host, port, err := ConvertToHostname("docker.io/containrrr/watchtower:latest") Expect(err).To(HaveOccurred())
Expect(err).NotTo(HaveOccurred())
Expect(host).To(Equal("docker.io"))
Expect(port).To(BeEmpty())
}) })
It("should return index.docker.io for image refs with no explicit registry", func() {
Expect(GetRegistryAddress("watchtower")).To(Equal("index.docker.io"))
Expect(GetRegistryAddress("containrrr/watchtower")).To(Equal("index.docker.io"))
}) })
When("normalizing the registry information", func() { It("should return index.docker.io for image refs with docker.io domain", func() {
It("should return index.docker.io given docker.io", func() { Expect(GetRegistryAddress("docker.io/watchtower")).To(Equal("index.docker.io"))
out, err := NormalizeRegistry("docker.io/containrrr/watchtower:latest") Expect(GetRegistryAddress("docker.io/containrrr/watchtower")).To(Equal("index.docker.io"))
Expect(err).NotTo(HaveOccurred()) })
Expect(out).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"))
}) })
}) })
}) })

View file

@ -1,42 +1,41 @@
package manifest package manifest
import ( import (
"errors"
"fmt" "fmt"
"github.com/containrrr/watchtower/pkg/registry/auth" url2 "net/url"
"github.com/containrrr/watchtower/pkg/registry/helpers" "github.com/containrrr/watchtower/pkg/registry/helpers"
"github.com/containrrr/watchtower/pkg/types" "github.com/containrrr/watchtower/pkg/types"
ref "github.com/docker/distribution/reference" ref "github.com/docker/distribution/reference"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
url2 "net/url"
"strings"
) )
// BuildManifestURL from raw image data // BuildManifestURL from raw image data
func BuildManifestURL(container types.Container) (string, error) { func BuildManifestURL(container types.Container) (string, error) {
normalizedRef, err := ref.ParseDockerRef(container.ImageName())
normalizedName, err := ref.ParseNormalizedNamed(container.ImageName())
if err != nil { if err != nil {
return "", err return "", err
} }
normalizedTaggedRef, isTagged := normalizedRef.(ref.NamedTagged)
if !isTagged {
return "", errors.New("Parsed container image ref has no tag: " + normalizedRef.String())
}
host, err := helpers.NormalizeRegistry(normalizedName.String()) host, _ := helpers.GetRegistryAddress(normalizedTaggedRef.Name())
img, tag := ExtractImageAndTag(strings.TrimPrefix(container.ImageName(), host+"/")) img, tag := ref.Path(normalizedTaggedRef), normalizedTaggedRef.Tag()
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"image": img, "image": img,
"tag": tag, "tag": tag,
"normalized": normalizedName, "normalized": normalizedTaggedRef.Name(),
"host": host, "host": host,
}).Debug("Parsing image ref") }).Debug("Parsing image ref")
if err != nil { if err != nil {
return "", err return "", err
} }
img = auth.GetScopeFromImageName(img, host)
if !strings.Contains(img, "/") {
img = "library/" + img
}
url := url2.URL{ url := url2.URL{
Scheme: "https", Scheme: "https",
Host: host, Host: host,
@ -44,24 +43,3 @@ func BuildManifestURL(container types.Container) (string, error) {
} }
return url.String(), nil return url.String(), nil
} }
// ExtractImageAndTag from a concatenated string
func ExtractImageAndTag(imageName string) (string, string) {
var img string
var tag string
if strings.Contains(imageName, ":") {
parts := strings.Split(imageName, ":")
if len(parts) > 2 {
img = parts[0]
tag = strings.Join(parts[1:], ":")
} else {
img = parts[0]
tag = parts[1]
}
} else {
img = imageName
tag = "latest"
}
return img, tag
}

View file

@ -1,13 +1,14 @@
package manifest_test package manifest_test
import ( import (
"testing"
"time"
"github.com/containrrr/watchtower/internal/actions/mocks" "github.com/containrrr/watchtower/internal/actions/mocks"
"github.com/containrrr/watchtower/pkg/registry/manifest" "github.com/containrrr/watchtower/pkg/registry/manifest"
apiTypes "github.com/docker/docker/api/types" apiTypes "github.com/docker/docker/api/types"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"testing"
"time"
) )
func TestManifest(t *testing.T) { func TestManifest(t *testing.T) {
@ -16,60 +17,58 @@ func TestManifest(t *testing.T) {
} }
var _ = Describe("the manifest module", func() { var _ = Describe("the manifest module", func() {
mockId := "mock-id" Describe("BuildManifestURL", func() {
It("should return a valid url given a fully qualified image", func() {
imageRef := "ghcr.io/containrrr/watchtower:mytag"
expected := "https://ghcr.io/v2/containrrr/watchtower/manifests/mytag"
URL, err := buildMockContainerManifestURL(imageRef)
Expect(err).NotTo(HaveOccurred())
Expect(URL).To(Equal(expected))
})
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"
URL, err := buildMockContainerManifestURL(imageRef)
Expect(err).NotTo(HaveOccurred())
Expect(URL).To(Equal(expected))
})
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"
URL, err := buildMockContainerManifestURL(imageRef)
Expect(err).NotTo(HaveOccurred())
Expect(URL).To(Equal(expected))
})
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"
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" mockName := "mock-container"
mockCreated := time.Now() mockCreated := time.Now()
mock := mocks.CreateMockContainerWithImageInfo(mockID, mockName, imageRef, mockCreated, imageInfo)
When("building a manifest url", func() { return manifest.BuildManifestURL(mock)
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)
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal(expected))
})
It("should assume dockerhub for non-qualified images", func() {
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)
Expect(err).NotTo(HaveOccurred())
Expect(res).To(Equal(expected))
})
It("should assume latest for images that lack an explicit tag", func() {
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)
Expect(err).NotTo(HaveOccurred())
Expect(res).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"
imageOut, tagOut := manifest.ExtractImageAndTag(in)
Expect(imageOut).To(Equal(image))
Expect(tagOut).To(Equal(tag))
})
})
})

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,
@ -41,17 +43,17 @@ func DefaultAuthHandler() (string, error) {
// Will return false if behavior for container is unknown. // Will return false if behavior for container is unknown.
func WarnOnAPIConsumption(container watchtowerTypes.Container) bool { func WarnOnAPIConsumption(container watchtowerTypes.Container) bool {
normalizedName, err := ref.ParseNormalizedNamed(container.ImageName()) normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName())
if err != nil { if err != nil {
return true return true
} }
containerHost, err := helpers.NormalizeRegistry(normalizedName.String()) containerHost, err := helpers.GetRegistryAddress(normalizedRef.Name())
if err != nil { if err != nil {
return true return true
} }
if containerHost == "index.docker.io" || containerHost == "ghcr.io" { if containerHost == helpers.DefaultRegistryHost || containerHost == "ghcr.io" {
return true return true
} }

View file

@ -23,11 +23,9 @@ var _ = Describe("Registry", func() {
}) })
When("Given a container with an image explicitly from dockerhub", func() { When("Given a container with an image explicitly from dockerhub", func() {
It("should want to warn", func() { It("should want to warn", func() {
Expect(testContainerWithImage("registry-1.docker.io/docker:latest")).To(BeTrue())
Expect(testContainerWithImage("index.docker.io/docker:latest")).To(BeTrue()) Expect(testContainerWithImage("index.docker.io/docker:latest")).To(BeTrue())
Expect(testContainerWithImage("docker.io/docker:latest")).To(BeTrue()) Expect(testContainerWithImage("docker.io/docker:latest")).To(BeTrue())
}) })
}) })
When("Given a container with an image from some other registry", func() { When("Given a container with an image from some other registry", func() {
It("should not want to warn", func() { It("should not want to warn", func() {

View file

@ -5,13 +5,12 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"os" "os"
"strings"
"github.com/containrrr/watchtower/pkg/registry/helpers"
cliconfig "github.com/docker/cli/cli/config" cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/types" "github.com/docker/cli/cli/config/types"
"github.com/docker/distribution/reference"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -19,7 +18,7 @@ import (
// loaded from environment variables or docker config // loaded from environment variables or docker config
// as available in that order // as available in that order
func EncodedAuth(ref string) (string, error) { func EncodedAuth(ref string) (string, error) {
auth, err := EncodedEnvAuth(ref) auth, err := EncodedEnvAuth()
if err != nil { if err != nil {
auth, err = EncodedConfigAuth(ref) auth, err = EncodedConfigAuth(ref)
} }
@ -29,7 +28,7 @@ func EncodedAuth(ref string) (string, error) {
// EncodedEnvAuth returns an encoded auth config for the given registry // EncodedEnvAuth returns an encoded auth config for the given registry
// loaded from environment variables // loaded from environment variables
// Returns an error if authentication environment variables have not been set // Returns an error if authentication environment variables have not been set
func EncodedEnvAuth(ref string) (string, error) { func EncodedEnvAuth() (string, error) {
username := os.Getenv("REPO_USER") username := os.Getenv("REPO_USER")
password := os.Getenv("REPO_PASS") password := os.Getenv("REPO_PASS")
if username != "" && password != "" { if username != "" && password != "" {
@ -37,8 +36,11 @@ func EncodedEnvAuth(ref string) (string, error) {
Username: username, Username: username,
Password: password, Password: password,
} }
log.Debugf("Loaded auth credentials for user %s on registry %s", auth.Username, ref)
log.Tracef("Using auth password %s", auth.Password) log.Debugf("Loaded auth credentials for registry user %s from environment", auth.Username)
// CREDENTIAL: Uncomment to log REPO_PASS environment variable
// log.Tracef("Using auth password %s", auth.Password)
return EncodeAuth(auth) return EncodeAuth(auth)
} }
return "", errors.New("registry auth environment variables (REPO_USER, REPO_PASS) not set") return "", errors.New("registry auth environment variables (REPO_USER, REPO_PASS) not set")
@ -48,19 +50,20 @@ func EncodedEnvAuth(ref string) (string, error) {
// loaded from the docker config // loaded from the docker config
// Returns an empty string if credentials cannot be found for the referenced server // Returns an empty string if credentials cannot be found for the referenced server
// The docker config must be mounted on the container // The docker config must be mounted on the container
func EncodedConfigAuth(ref string) (string, error) { func EncodedConfigAuth(imageRef string) (string, error) {
server, err := ParseServerAddress(ref) server, err := helpers.GetRegistryAddress(imageRef)
if err != nil { if err != nil {
log.Errorf("Unable to parse the image ref %s", err) log.Errorf("Could not get registry from image ref %s", imageRef)
return "", err return "", err
} }
configDir := os.Getenv("DOCKER_CONFIG") configDir := os.Getenv("DOCKER_CONFIG")
if configDir == "" { if configDir == "" {
configDir = "/" configDir = "/"
} }
configFile, err := cliconfig.Load(configDir) configFile, err := cliconfig.Load(configDir)
if err != nil { if err != nil {
log.Errorf("Unable to find default config file %s", err) log.Errorf("Unable to find default config file: %s", err)
return "", err return "", err
} }
credStore := CredentialsStore(*configFile) credStore := CredentialsStore(*configFile)
@ -70,23 +73,12 @@ func EncodedConfigAuth(ref string) (string, error) {
log.WithField("config_file", configFile.Filename).Debugf("No credentials for %s found", server) log.WithField("config_file", configFile.Filename).Debugf("No credentials for %s found", server)
return "", nil return "", nil
} }
log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, ref, configFile.Filename) log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, server, configFile.Filename)
log.Tracef("Using auth password %s", auth.Password) // CREDENTIAL: Uncomment to log docker config password
// log.Tracef("Using auth password %s", auth.Password)
return EncodeAuth(auth) return EncodeAuth(auth)
} }
// ParseServerAddress extracts the server part from a container image ref
func ParseServerAddress(ref string) (string, error) {
parsedRef, err := reference.Parse(ref)
if err != nil {
return ref, err
}
parts := strings.Split(parsedRef.String(), "/")
return parts[0], nil
}
// CredentialsStore returns a new credentials store based // CredentialsStore returns a new credentials store based
// on the settings provided in the configuration file. // on the settings provided in the configuration file.
func CredentialsStore(configFile configfile.ConfigFile) credentials.Store { func CredentialsStore(configFile configfile.ConfigFile) credentials.Store {

View file

@ -1,22 +1,17 @@
package registry package registry
import ( import (
"os"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"os"
) )
var _ = Describe("Testing with Ginkgo", func() { var _ = Describe("Registry credential helpers", func() {
It("encoded env auth_ should return an error if repo envs are unset", func() { Describe("EncodedAuth", func() {
_ = os.Unsetenv("REPO_USER") It("should return repo credentials from env when set", func() {
_ = os.Unsetenv("REPO_PASS")
_, err := EncodedEnvAuth("")
Expect(err).To(HaveOccurred())
})
It("encoded env auth_ should return auth hash if repo envs are set", func() {
var err error var err error
expectedHash := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0=" expected := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0="
err = os.Setenv("REPO_USER", "containrrr-user") err = os.Setenv("REPO_USER", "containrrr-user")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
@ -24,11 +19,24 @@ var _ = Describe("Testing with Ginkgo", func() {
err = os.Setenv("REPO_PASS", "containrrr-pass") err = os.Setenv("REPO_PASS", "containrrr-pass")
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
config, err := EncodedEnvAuth("") config, err := EncodedEnvAuth()
Expect(config).To(Equal(expectedHash)) Expect(config).To(Equal(expected))
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
}) })
It("encoded config auth_ should return an error if file is not present", func() { })
Describe("EncodedEnvAuth", func() {
It("should return an error if repo envs are unset", func() {
_ = os.Unsetenv("REPO_USER")
_ = os.Unsetenv("REPO_PASS")
_, err := EncodedEnvAuth()
Expect(err).To(HaveOccurred())
})
})
Describe("EncodedConfigAuth", func() {
It("should return an error if file is not present", func() {
var err error var err error
err = os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail") err = os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail")
@ -36,30 +44,6 @@ var _ = Describe("Testing with Ginkgo", func() {
_, err = EncodedConfigAuth("") _, err = EncodedConfigAuth("")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
}) })
/*
* TODO:
* This part only confirms that it still works in the same way as it did
* with the old version of the docker api client sdk. I'd say that
* ParseServerAddress likely needs to be elaborated a bit to default to
* dockerhub in case no server address was provided.
*
* ++ @simskij, 2019-04-04
*/
It("parse server address_ should return error if passed empty string", func() {
_, err := ParseServerAddress("")
Expect(err).To(HaveOccurred())
})
It("parse server address_ should return the organization part if passed an image name missing server name", func() {
val, _ := ParseServerAddress("containrrr/config")
Expect(val).To(Equal("containrrr"))
})
It("parse server address_ should return the server name if passed a fully qualified image name", func() {
val, _ := ParseServerAddress("github.com/containrrrr/config")
Expect(val).To(Equal("github.com"))
}) })
}) })