Skip to content

Commit 1246771

Browse files
authored
feat: check container config before update (#925)
* feat: check container config before restart * fix: only skip when hostconfig and config differ * fix: update test mocks to not fail tests * test: add verify config tests
1 parent fdf6e46 commit 1246771

File tree

6 files changed

+151
-13
lines changed

6 files changed

+151
-13
lines changed

cmd/root.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {
269269
}
270270

271271
log.Info("Watchtower ", version, "\n", notifs, "\n", filtering, "\n", schedMessage)
272+
if log.IsLevelEnabled(log.TraceLevel) {
273+
log.Warn("trace level enabled: log will include sensitive information as credentials and tokens")
274+
}
272275
}
273276
}
274277

@@ -330,8 +333,10 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
330333
}
331334
metricResults, err := actions.Update(client, updateParams)
332335
if err != nil {
333-
log.Println(err)
336+
log.Error(err)
334337
}
335338
notifier.SendNotification()
339+
log.Debugf("Session done: %v scanned, %v updated, %v failed",
340+
metricResults.Scanned, metricResults.Updated, metricResults.Failed)
336341
return metricResults
337342
}

internal/actions/mocks/container.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ func CreateMockContainer(id string, name string, image string, created time.Time
1616
Image: image,
1717
Name: name,
1818
Created: created.String(),
19+
HostConfig: &container2.HostConfig{
20+
PortBindings: map[nat.Port][]nat.PortBinding{},
21+
},
1922
},
2023
Config: &container2.Config{
21-
Image: image,
22-
Labels: make(map[string]string),
24+
Image: image,
25+
Labels: make(map[string]string),
2326
ExposedPorts: map[nat.Port]struct{}{},
2427
},
2528
}

internal/actions/update.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package actions
22

33
import (
4-
"errors"
54
"github.com/containrrr/watchtower/internal/util"
65
"github.com/containrrr/watchtower/pkg/container"
76
"github.com/containrrr/watchtower/pkg/lifecycle"
@@ -33,11 +32,23 @@ func Update(client container.Client, params types.UpdateParams) (*metrics2.Metri
3332

3433
for i, targetContainer := range containers {
3534
stale, err := client.IsContainerStale(targetContainer)
36-
if stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly() && !targetContainer.HasImageInfo() {
37-
err = errors.New("no available image info")
35+
shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly()
36+
if err == nil && shouldUpdate {
37+
// Check to make sure we have all the necessary information for recreating the container
38+
err = targetContainer.VerifyConfiguration()
39+
// If the image information is incomplete and trace logging is enabled, log it for further diagnosis
40+
if err != nil && log.IsLevelEnabled(log.TraceLevel) {
41+
imageInfo := targetContainer.ImageInfo()
42+
log.Tracef("Image info: %#v", imageInfo)
43+
log.Tracef("Container info: %#v", targetContainer.ContainerInfo())
44+
if imageInfo != nil {
45+
log.Tracef("Image config: %#v", imageInfo.Config)
46+
}
47+
}
3848
}
49+
3950
if err != nil {
40-
log.Infof("Unable to update container %q: %v. Proceeding to next.", containers[i].Name(), err)
51+
log.Infof("Unable to update container %q: %v. Proceeding to next.", targetContainer.Name(), err)
4152
stale = false
4253
staleCheckFailed++
4354
metric.Failed++

pkg/container/container.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,32 @@ func (c Container) HasImageInfo() bool {
258258
func (c Container) ImageInfo() *types.ImageInspect {
259259
return c.imageInfo
260260
}
261+
262+
// VerifyConfiguration checks the container and image configurations for nil references to make sure
263+
// that the container can be recreated once deleted
264+
func (c Container) VerifyConfiguration() error {
265+
if c.imageInfo == nil {
266+
return errorNoImageInfo
267+
}
268+
269+
containerInfo := c.ContainerInfo()
270+
if containerInfo == nil {
271+
return errorInvalidConfig
272+
}
273+
274+
containerConfig := containerInfo.Config
275+
if containerConfig == nil {
276+
return errorInvalidConfig
277+
}
278+
279+
hostConfig := containerInfo.HostConfig
280+
if hostConfig == nil {
281+
return errorInvalidConfig
282+
}
283+
284+
if len(hostConfig.PortBindings) > 0 && containerConfig.ExposedPorts == nil {
285+
return errorNoExposedPorts
286+
}
287+
288+
return nil
289+
}

pkg/container/container_test.go

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/docker/docker/api/types"
77
"github.com/docker/docker/api/types/container"
88
cli "github.com/docker/docker/client"
9+
"github.com/docker/go-connections/nat"
910
. "github.com/onsi/ginkgo"
1011
. "github.com/onsi/gomega"
1112
)
@@ -32,14 +33,14 @@ var _ = Describe("the container", func() {
3233
containerKnown := *mockContainerWithImageName("docker.io/prefix/imagename:latest")
3334

3435
When("warn on head failure is set to \"always\"", func() {
35-
c := NewClient(false, false, false, false, false, "always")
36+
c := newClientNoAPI(false, false, false, false, false, "always")
3637
It("should always return true", func() {
3738
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeTrue())
3839
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue())
3940
})
4041
})
4142
When("warn on head failure is set to \"auto\"", func() {
42-
c := NewClient(false, false, false, false, false, "auto")
43+
c := newClientNoAPI(false, false, false, false, false, "auto")
4344
It("should always return true", func() {
4445
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
4546
})
@@ -48,7 +49,7 @@ var _ = Describe("the container", func() {
4849
})
4950
})
5051
When("warn on head failure is set to \"never\"", func() {
51-
c := NewClient(false, false, false, false, false, "never")
52+
c := newClientNoAPI(false, false, false, false, false, "never")
5253
It("should never return true", func() {
5354
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
5455
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeFalse())
@@ -130,6 +131,63 @@ var _ = Describe("the container", func() {
130131
})
131132
})
132133
})
134+
Describe("VerifyConfiguration", func() {
135+
When("verifying a container with no image info", func() {
136+
It("should return an error", func() {
137+
c := mockContainerWithPortBindings()
138+
c.imageInfo = nil
139+
err := c.VerifyConfiguration()
140+
Expect(err).To(Equal(errorNoImageInfo))
141+
})
142+
})
143+
When("verifying a container with no container info", func() {
144+
It("should return an error", func() {
145+
c := mockContainerWithPortBindings()
146+
c.containerInfo = nil
147+
err := c.VerifyConfiguration()
148+
Expect(err).To(Equal(errorInvalidConfig))
149+
})
150+
})
151+
When("verifying a container with no config", func() {
152+
It("should return an error", func() {
153+
c := mockContainerWithPortBindings()
154+
c.containerInfo.Config = nil
155+
err := c.VerifyConfiguration()
156+
Expect(err).To(Equal(errorInvalidConfig))
157+
})
158+
})
159+
When("verifying a container with no host config", func() {
160+
It("should return an error", func() {
161+
c := mockContainerWithPortBindings()
162+
c.containerInfo.HostConfig = nil
163+
err := c.VerifyConfiguration()
164+
Expect(err).To(Equal(errorInvalidConfig))
165+
})
166+
})
167+
When("verifying a container with no port bindings", func() {
168+
It("should not return an error", func() {
169+
c := mockContainerWithPortBindings()
170+
err := c.VerifyConfiguration()
171+
Expect(err).ToNot(HaveOccurred())
172+
})
173+
})
174+
When("verifying a container with port bindings, but no exposed ports", func() {
175+
It("should return an error", func() {
176+
c := mockContainerWithPortBindings("80/tcp")
177+
c.containerInfo.Config.ExposedPorts = nil
178+
err := c.VerifyConfiguration()
179+
Expect(err).To(Equal(errorNoExposedPorts))
180+
})
181+
})
182+
When("verifying a container with port bindings and exposed ports is non-nil", func() {
183+
It("should return an error", func() {
184+
c := mockContainerWithPortBindings("80/tcp")
185+
c.containerInfo.Config.ExposedPorts = map[nat.Port]struct{}{"80/tcp": {}}
186+
err := c.VerifyConfiguration()
187+
Expect(err).ToNot(HaveOccurred())
188+
})
189+
})
190+
})
133191
When("asked for metadata", func() {
134192
var c *Container
135193
BeforeEach(func() {
@@ -281,10 +339,23 @@ var _ = Describe("the container", func() {
281339
})
282340
})
283341

342+
func mockContainerWithPortBindings(portBindingSources ...string) *Container {
343+
mockContainer := mockContainerWithLabels(nil)
344+
mockContainer.imageInfo = &types.ImageInspect{}
345+
hostConfig := &container.HostConfig{
346+
PortBindings: nat.PortMap{},
347+
}
348+
for _, pbs := range portBindingSources {
349+
hostConfig.PortBindings[nat.Port(pbs)] = []nat.PortBinding{}
350+
}
351+
mockContainer.containerInfo.HostConfig = hostConfig
352+
return mockContainer
353+
}
354+
284355
func mockContainerWithImageName(name string) *Container {
285-
container := mockContainerWithLabels(nil)
286-
container.containerInfo.Config.Image = name
287-
return container
356+
mockContainer := mockContainerWithLabels(nil)
357+
mockContainer.containerInfo.Config.Image = name
358+
return mockContainer
288359
}
289360

290361
func mockContainerWithLinks(links []string) *Container {
@@ -317,3 +388,15 @@ func mockContainerWithLabels(labels map[string]string) *Container {
317388
}
318389
return NewContainer(&content, nil)
319390
}
391+
392+
func newClientNoAPI(pullImages, includeStopped, reviveStopped, removeVolumes, includeRestarting bool, warnOnHeadFailed string) Client {
393+
return dockerClient{
394+
api: nil,
395+
pullImages: pullImages,
396+
removeVolumes: removeVolumes,
397+
includeStopped: includeStopped,
398+
reviveStopped: reviveStopped,
399+
includeRestarting: includeRestarting,
400+
warnOnHeadFailed: warnOnHeadFailed,
401+
}
402+
}

pkg/container/errors.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package container
2+
3+
import "errors"
4+
5+
var errorNoImageInfo = errors.New("no available image info")
6+
var errorNoExposedPorts = errors.New("exposed ports does not match port bindings")
7+
var errorInvalidConfig = errors.New("container configuration missing or invalid")

0 commit comments

Comments
 (0)