Skip to content

Commit e983beb

Browse files
authored
fix: gracefully skip pinned images (#1277)
* move client args to opts struct * gracefully skip pinned images * replace newClientNoAPI with literals
1 parent de40b0c commit e983beb

File tree

4 files changed

+98
-88
lines changed

4 files changed

+98
-88
lines changed

cmd/root.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cmd
22

33
import (
4-
"github.com/containrrr/watchtower/internal/meta"
54
"math"
65
"net/http"
76
"os"
@@ -11,12 +10,12 @@ import (
1110
"syscall"
1211
"time"
1312

14-
apiMetrics "github.com/containrrr/watchtower/pkg/api/metrics"
15-
"github.com/containrrr/watchtower/pkg/api/update"
16-
1713
"github.com/containrrr/watchtower/internal/actions"
1814
"github.com/containrrr/watchtower/internal/flags"
15+
"github.com/containrrr/watchtower/internal/meta"
1916
"github.com/containrrr/watchtower/pkg/api"
17+
apiMetrics "github.com/containrrr/watchtower/pkg/api/metrics"
18+
"github.com/containrrr/watchtower/pkg/api/update"
2019
"github.com/containrrr/watchtower/pkg/container"
2120
"github.com/containrrr/watchtower/pkg/filters"
2221
"github.com/containrrr/watchtower/pkg/metrics"
@@ -139,14 +138,14 @@ func PreRun(cmd *cobra.Command, _ []string) {
139138
log.Warn("Using `WATCHTOWER_NO_PULL` and `WATCHTOWER_MONITOR_ONLY` simultaneously might lead to no action being taken at all. If this is intentional, you may safely ignore this message.")
140139
}
141140

142-
client = container.NewClient(
143-
!noPull,
144-
includeStopped,
145-
reviveStopped,
146-
removeVolumes,
147-
includeRestarting,
148-
warnOnHeadPullFailed,
149-
)
141+
client = container.NewClient(container.ClientOptions{
142+
PullImages: !noPull,
143+
IncludeStopped: includeStopped,
144+
ReviveStopped: reviveStopped,
145+
RemoveVolumes: removeVolumes,
146+
IncludeRestarting: includeRestarting,
147+
WarnOnHeadFailed: container.WarningStrategy(warnOnHeadPullFailed),
148+
})
150149

151150
notifier = notifications.NewNotifier(cmd)
152151
}
@@ -293,7 +292,7 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {
293292
startupLog.Info("Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST"))
294293
startupLog.Info("Note that the first check will be performed in " + until)
295294
} else if runOnce, _ := c.PersistentFlags().GetBool("run-once"); runOnce {
296-
startupLog.Info("Running a one time update.")
295+
startupLog.Info("Running a one time update.")
297296
} else {
298297
startupLog.Info("Periodic runs are not enabled.")
299298
}

pkg/container/client.go

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,39 +42,51 @@ type Client interface {
4242
// * DOCKER_HOST the docker-engine host to send api requests to
4343
// * DOCKER_TLS_VERIFY whether to verify tls certificates
4444
// * DOCKER_API_VERSION the minimum docker api version to work with
45-
func NewClient(pullImages, includeStopped, reviveStopped, removeVolumes, includeRestarting bool, warnOnHeadFailed string) Client {
45+
func NewClient(opts ClientOptions) Client {
4646
cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv)
4747

4848
if err != nil {
4949
log.Fatalf("Error instantiating Docker client: %s", err)
5050
}
5151

5252
return dockerClient{
53-
api: cli,
54-
pullImages: pullImages,
55-
removeVolumes: removeVolumes,
56-
includeStopped: includeStopped,
57-
reviveStopped: reviveStopped,
58-
includeRestarting: includeRestarting,
59-
warnOnHeadFailed: warnOnHeadFailed,
53+
api: cli,
54+
ClientOptions: opts,
6055
}
6156
}
6257

58+
// ClientOptions contains the options for how the docker client wrapper should behave
59+
type ClientOptions struct {
60+
PullImages bool
61+
RemoveVolumes bool
62+
IncludeStopped bool
63+
ReviveStopped bool
64+
IncludeRestarting bool
65+
WarnOnHeadFailed WarningStrategy
66+
}
67+
68+
// WarningStrategy is a value determining when to show warnings
69+
type WarningStrategy string
70+
71+
const (
72+
// WarnAlways warns whenever the problem occurs
73+
WarnAlways WarningStrategy = "always"
74+
// WarnNever never warns when the problem occurs
75+
WarnNever WarningStrategy = "never"
76+
// WarnAuto skips warning when the problem was expected
77+
WarnAuto WarningStrategy = "auto"
78+
)
79+
6380
type dockerClient struct {
64-
api sdkClient.CommonAPIClient
65-
pullImages bool
66-
removeVolumes bool
67-
includeStopped bool
68-
reviveStopped bool
69-
includeRestarting bool
70-
warnOnHeadFailed string
81+
api sdkClient.CommonAPIClient
82+
ClientOptions
7183
}
7284

7385
func (client dockerClient) WarnOnHeadPullFailed(container Container) bool {
74-
if client.warnOnHeadFailed == "always" {
86+
if client.WarnOnHeadFailed == WarnAlways {
7587
return true
7688
}
77-
if client.warnOnHeadFailed == "never" {
89+
if client.WarnOnHeadFailed == WarnNever {
7890
return false
7991
}
8092

@@ -85,11 +97,11 @@ func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) {
8597
cs := []Container{}
8698
bg := context.Background()
8799

88-
if client.includeStopped && client.includeRestarting {
100+
if client.IncludeStopped && client.IncludeRestarting {
89101
log.Debug("Retrieving running, stopped, restarting and exited containers")
90-
} else if client.includeStopped {
102+
} else if client.IncludeStopped {
91103
log.Debug("Retrieving running, stopped and exited containers")
92-
} else if client.includeRestarting {
104+
} else if client.IncludeRestarting {
93105
log.Debug("Retrieving running and restarting containers")
94106
} else {
95107
log.Debug("Retrieving running containers")
@@ -125,12 +137,12 @@ func (client dockerClient) createListFilter() filters.Args {
125137
filterArgs := filters.NewArgs()
126138
filterArgs.Add("status", "running")
127139

128-
if client.includeStopped {
140+
if client.IncludeStopped {
129141
filterArgs.Add("status", "created")
130142
filterArgs.Add("status", "exited")
131143
}
132144

133-
if client.includeRestarting {
145+
if client.IncludeRestarting {
134146
filterArgs.Add("status", "restarting")
135147
}
136148

@@ -179,7 +191,7 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
179191
} else {
180192
log.Debugf("Removing container %s", shortID)
181193

182-
if err := client.api.ContainerRemove(bg, idStr, types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.removeVolumes}); err != nil {
194+
if err := client.api.ContainerRemove(bg, idStr, types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.RemoveVolumes}); err != nil {
183195
return err
184196
}
185197
}
@@ -236,7 +248,7 @@ func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) {
236248
}
237249

238250
createdContainerID := t.ContainerID(createdContainer.ID)
239-
if !c.IsRunning() && !client.reviveStopped {
251+
if !c.IsRunning() && !client.ReviveStopped {
240252
return createdContainerID, nil
241253
}
242254

@@ -264,7 +276,7 @@ func (client dockerClient) RenameContainer(c Container, newName string) error {
264276
func (client dockerClient) IsContainerStale(container Container) (stale bool, latestImage t.ImageID, err error) {
265277
ctx := context.Background()
266278

267-
if !client.pullImages {
279+
if !client.PullImages {
268280
log.Debugf("Skipping image pull.")
269281
} else if err := client.PullImage(ctx, container); err != nil {
270282
return false, container.SafeImageID(), err
@@ -303,6 +315,10 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e
303315
"container": containerName,
304316
}
305317

318+
if strings.HasPrefix(imageName, "sha256:") {
319+
return fmt.Errorf("container uses a pinned image, and cannot be updated by watchtower")
320+
}
321+
306322
log.WithFields(fields).Debugf("Trying to load authentication credentials.")
307323
opts, err := registry.GetPullOptions(imageName)
308324
if err != nil {

pkg/container/client_test.go

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
. "github.com/onsi/gomega"
1717
. "github.com/onsi/gomega/types"
1818

19+
"context"
1920
"net/http"
2021
)
2122

@@ -35,38 +36,47 @@ var _ = Describe("the client", func() {
3536
containerUnknown := *mockContainerWithImageName("unknown.repo/prefix/imagename:latest")
3637
containerKnown := *mockContainerWithImageName("docker.io/prefix/imagename:latest")
3738

38-
When("warn on head failure is set to \"always\"", func() {
39-
c := newClientNoAPI(false, false, false, false, false, "always")
39+
When(`warn on head failure is set to "always"`, func() {
40+
c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}}
4041
It("should always return true", func() {
4142
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeTrue())
4243
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue())
4344
})
4445
})
45-
When("warn on head failure is set to \"auto\"", func() {
46-
c := newClientNoAPI(false, false, false, false, false, "auto")
47-
It("should always return true", func() {
46+
When(`warn on head failure is set to "auto"`, func() {
47+
c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAuto}}
48+
It("should return false for unknown repos", func() {
4849
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
4950
})
50-
It("should", func() {
51+
It("should return true for known repos", func() {
5152
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue())
5253
})
5354
})
54-
When("warn on head failure is set to \"never\"", func() {
55-
c := newClientNoAPI(false, false, false, false, false, "never")
55+
When(`warn on head failure is set to "never"`, func() {
56+
c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnNever}}
5657
It("should never return true", func() {
5758
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
5859
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeFalse())
5960
})
6061
})
6162
})
63+
When("pulling the latest image", func() {
64+
When("the image consist of a pinned hash", func() {
65+
It("should gracefully fail with a useful message", func() {
66+
c := dockerClient{}
67+
pinnedContainer := *mockContainerWithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b")
68+
c.PullImage(context.Background(), pinnedContainer)
69+
})
70+
})
71+
})
6272
When("listing containers", func() {
6373
When("no filter is provided", func() {
6474
It("should return all available containers", func() {
6575
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
6676
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
6777
client := dockerClient{
68-
api: docker,
69-
pullImages: false,
78+
api: docker,
79+
ClientOptions: ClientOptions{PullImages: false},
7080
}
7181
containers, err := client.ListContainers(filters.NoFilter)
7282
Expect(err).NotTo(HaveOccurred())
@@ -79,8 +89,8 @@ var _ = Describe("the client", func() {
7989
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
8090
filter := filters.FilterByNames([]string{"lollercoaster"}, filters.NoFilter)
8191
client := dockerClient{
82-
api: docker,
83-
pullImages: false,
92+
api: docker,
93+
ClientOptions: ClientOptions{PullImages: false},
8494
}
8595
containers, err := client.ListContainers(filter)
8696
Expect(err).NotTo(HaveOccurred())
@@ -92,8 +102,8 @@ var _ = Describe("the client", func() {
92102
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
93103
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
94104
client := dockerClient{
95-
api: docker,
96-
pullImages: false,
105+
api: docker,
106+
ClientOptions: ClientOptions{PullImages: false},
97107
}
98108
containers, err := client.ListContainers(filters.WatchtowerContainersFilter)
99109
Expect(err).NotTo(HaveOccurred())
@@ -105,9 +115,8 @@ var _ = Describe("the client", func() {
105115
mockServer.AppendHandlers(mocks.ListContainersHandler("running", "exited", "created"))
106116
mockServer.AppendHandlers(mocks.GetContainerHandlers("stopped", "watchtower", "running")...)
107117
client := dockerClient{
108-
api: docker,
109-
pullImages: false,
110-
includeStopped: true,
118+
api: docker,
119+
ClientOptions: ClientOptions{PullImages: false, IncludeStopped: true},
111120
}
112121
containers, err := client.ListContainers(filters.NoFilter)
113122
Expect(err).NotTo(HaveOccurred())
@@ -119,9 +128,8 @@ var _ = Describe("the client", func() {
119128
mockServer.AppendHandlers(mocks.ListContainersHandler("running", "restarting"))
120129
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running", "restarting")...)
121130
client := dockerClient{
122-
api: docker,
123-
pullImages: false,
124-
includeRestarting: true,
131+
api: docker,
132+
ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: true},
125133
}
126134
containers, err := client.ListContainers(filters.NoFilter)
127135
Expect(err).NotTo(HaveOccurred())
@@ -133,9 +141,8 @@ var _ = Describe("the client", func() {
133141
mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
134142
mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...)
135143
client := dockerClient{
136-
api: docker,
137-
pullImages: false,
138-
includeRestarting: false,
144+
api: docker,
145+
ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: false},
139146
}
140147
containers, err := client.ListContainers(filters.NoFilter)
141148
Expect(err).NotTo(HaveOccurred())
@@ -147,8 +154,8 @@ var _ = Describe("the client", func() {
147154
When(`logging`, func() {
148155
It("should include container id field", func() {
149156
client := dockerClient{
150-
api: docker,
151-
pullImages: false,
157+
api: docker,
158+
ClientOptions: ClientOptions{PullImages: false},
152159
}
153160

154161
// Capture logrus output in buffer

pkg/container/container_test.go

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -216,20 +216,20 @@ var _ = Describe("the container", func() {
216216
})
217217
})
218218
})
219-
219+
220220
When("there is a pre or post update timeout", func() {
221-
It("should return minute values", func() {
222-
c = mockContainerWithLabels(map[string]string{
223-
"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "3",
224-
"com.centurylinklabs.watchtower.lifecycle.post-update-timeout": "5",
225-
})
226-
preTimeout := c.PreUpdateTimeout()
227-
Expect(preTimeout).To(Equal(3))
228-
postTimeout := c.PostUpdateTimeout()
229-
Expect(postTimeout).To(Equal(5))
230-
})
231-
})
232-
221+
It("should return minute values", func() {
222+
c = mockContainerWithLabels(map[string]string{
223+
"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "3",
224+
"com.centurylinklabs.watchtower.lifecycle.post-update-timeout": "5",
225+
})
226+
preTimeout := c.PreUpdateTimeout()
227+
Expect(preTimeout).To(Equal(3))
228+
postTimeout := c.PostUpdateTimeout()
229+
Expect(postTimeout).To(Equal(5))
230+
})
231+
})
232+
233233
})
234234
})
235235

@@ -282,15 +282,3 @@ func mockContainerWithLabels(labels map[string]string) *Container {
282282
}
283283
return NewContainer(&content, nil)
284284
}
285-
286-
func newClientNoAPI(pullImages, includeStopped, reviveStopped, removeVolumes, includeRestarting bool, warnOnHeadFailed string) Client {
287-
return dockerClient{
288-
api: nil,
289-
pullImages: pullImages,
290-
removeVolumes: removeVolumes,
291-
includeStopped: includeStopped,
292-
reviveStopped: reviveStopped,
293-
includeRestarting: includeRestarting,
294-
warnOnHeadFailed: warnOnHeadFailed,
295-
}
296-
}

0 commit comments

Comments
 (0)