mirror of
https://github.com/containrrr/watchtower.git
synced 2025-09-22 05:40:50 +02:00
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
This commit is contained in:
parent
fdf6e46e7b
commit
12467712a1
6 changed files with 151 additions and 13 deletions
|
@ -269,6 +269,9 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Watchtower ", version, "\n", notifs, "\n", filtering, "\n", schedMessage)
|
log.Info("Watchtower ", version, "\n", notifs, "\n", filtering, "\n", schedMessage)
|
||||||
|
if log.IsLevelEnabled(log.TraceLevel) {
|
||||||
|
log.Warn("trace level enabled: log will include sensitive information as credentials and tokens")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -330,8 +333,10 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
|
||||||
}
|
}
|
||||||
metricResults, err := actions.Update(client, updateParams)
|
metricResults, err := actions.Update(client, updateParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
notifier.SendNotification()
|
notifier.SendNotification()
|
||||||
|
log.Debugf("Session done: %v scanned, %v updated, %v failed",
|
||||||
|
metricResults.Scanned, metricResults.Updated, metricResults.Failed)
|
||||||
return metricResults
|
return metricResults
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,13 @@ func CreateMockContainer(id string, name string, image string, created time.Time
|
||||||
Image: image,
|
Image: image,
|
||||||
Name: name,
|
Name: name,
|
||||||
Created: created.String(),
|
Created: created.String(),
|
||||||
|
HostConfig: &container2.HostConfig{
|
||||||
|
PortBindings: map[nat.Port][]nat.PortBinding{},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Config: &container2.Config{
|
Config: &container2.Config{
|
||||||
Image: image,
|
Image: image,
|
||||||
Labels: make(map[string]string),
|
Labels: make(map[string]string),
|
||||||
ExposedPorts: map[nat.Port]struct{}{},
|
ExposedPorts: map[nat.Port]struct{}{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"github.com/containrrr/watchtower/internal/util"
|
"github.com/containrrr/watchtower/internal/util"
|
||||||
"github.com/containrrr/watchtower/pkg/container"
|
"github.com/containrrr/watchtower/pkg/container"
|
||||||
"github.com/containrrr/watchtower/pkg/lifecycle"
|
"github.com/containrrr/watchtower/pkg/lifecycle"
|
||||||
|
@ -33,11 +32,23 @@ func Update(client container.Client, params types.UpdateParams) (*metrics2.Metri
|
||||||
|
|
||||||
for i, targetContainer := range containers {
|
for i, targetContainer := range containers {
|
||||||
stale, err := client.IsContainerStale(targetContainer)
|
stale, err := client.IsContainerStale(targetContainer)
|
||||||
if stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly() && !targetContainer.HasImageInfo() {
|
shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly()
|
||||||
err = errors.New("no available image info")
|
if err == nil && shouldUpdate {
|
||||||
|
// Check to make sure we have all the necessary information for recreating the container
|
||||||
|
err = targetContainer.VerifyConfiguration()
|
||||||
|
// If the image information is incomplete and trace logging is enabled, log it for further diagnosis
|
||||||
|
if err != nil && log.IsLevelEnabled(log.TraceLevel) {
|
||||||
|
imageInfo := targetContainer.ImageInfo()
|
||||||
|
log.Tracef("Image info: %#v", imageInfo)
|
||||||
|
log.Tracef("Container info: %#v", targetContainer.ContainerInfo())
|
||||||
|
if imageInfo != nil {
|
||||||
|
log.Tracef("Image config: %#v", imageInfo.Config)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Infof("Unable to update container %q: %v. Proceeding to next.", containers[i].Name(), err)
|
log.Infof("Unable to update container %q: %v. Proceeding to next.", targetContainer.Name(), err)
|
||||||
stale = false
|
stale = false
|
||||||
staleCheckFailed++
|
staleCheckFailed++
|
||||||
metric.Failed++
|
metric.Failed++
|
||||||
|
|
|
@ -258,3 +258,32 @@ func (c Container) HasImageInfo() bool {
|
||||||
func (c Container) ImageInfo() *types.ImageInspect {
|
func (c Container) ImageInfo() *types.ImageInspect {
|
||||||
return c.imageInfo
|
return c.imageInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyConfiguration checks the container and image configurations for nil references to make sure
|
||||||
|
// that the container can be recreated once deleted
|
||||||
|
func (c Container) VerifyConfiguration() error {
|
||||||
|
if c.imageInfo == nil {
|
||||||
|
return errorNoImageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
containerInfo := c.ContainerInfo()
|
||||||
|
if containerInfo == nil {
|
||||||
|
return errorInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
containerConfig := containerInfo.Config
|
||||||
|
if containerConfig == nil {
|
||||||
|
return errorInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
hostConfig := containerInfo.HostConfig
|
||||||
|
if hostConfig == nil {
|
||||||
|
return errorInvalidConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hostConfig.PortBindings) > 0 && containerConfig.ExposedPorts == nil {
|
||||||
|
return errorNoExposedPorts
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
cli "github.com/docker/docker/client"
|
cli "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
|
@ -32,14 +33,14 @@ var _ = Describe("the container", func() {
|
||||||
containerKnown := *mockContainerWithImageName("docker.io/prefix/imagename:latest")
|
containerKnown := *mockContainerWithImageName("docker.io/prefix/imagename:latest")
|
||||||
|
|
||||||
When("warn on head failure is set to \"always\"", func() {
|
When("warn on head failure is set to \"always\"", func() {
|
||||||
c := NewClient(false, false, false, false, false, "always")
|
c := newClientNoAPI(false, false, false, false, false, "always")
|
||||||
It("should always return true", func() {
|
It("should always return true", func() {
|
||||||
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeTrue())
|
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeTrue())
|
||||||
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue())
|
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
When("warn on head failure is set to \"auto\"", func() {
|
When("warn on head failure is set to \"auto\"", func() {
|
||||||
c := NewClient(false, false, false, false, false, "auto")
|
c := newClientNoAPI(false, false, false, false, false, "auto")
|
||||||
It("should always return true", func() {
|
It("should always return true", func() {
|
||||||
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
|
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
|
||||||
})
|
})
|
||||||
|
@ -48,7 +49,7 @@ var _ = Describe("the container", func() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
When("warn on head failure is set to \"never\"", func() {
|
When("warn on head failure is set to \"never\"", func() {
|
||||||
c := NewClient(false, false, false, false, false, "never")
|
c := newClientNoAPI(false, false, false, false, false, "never")
|
||||||
It("should never return true", func() {
|
It("should never return true", func() {
|
||||||
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
|
Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
|
||||||
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeFalse())
|
Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeFalse())
|
||||||
|
@ -130,6 +131,63 @@ var _ = Describe("the container", func() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Describe("VerifyConfiguration", func() {
|
||||||
|
When("verifying a container with no image info", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
c := mockContainerWithPortBindings()
|
||||||
|
c.imageInfo = nil
|
||||||
|
err := c.VerifyConfiguration()
|
||||||
|
Expect(err).To(Equal(errorNoImageInfo))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
When("verifying a container with no container info", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
c := mockContainerWithPortBindings()
|
||||||
|
c.containerInfo = nil
|
||||||
|
err := c.VerifyConfiguration()
|
||||||
|
Expect(err).To(Equal(errorInvalidConfig))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
When("verifying a container with no config", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
c := mockContainerWithPortBindings()
|
||||||
|
c.containerInfo.Config = nil
|
||||||
|
err := c.VerifyConfiguration()
|
||||||
|
Expect(err).To(Equal(errorInvalidConfig))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
When("verifying a container with no host config", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
c := mockContainerWithPortBindings()
|
||||||
|
c.containerInfo.HostConfig = nil
|
||||||
|
err := c.VerifyConfiguration()
|
||||||
|
Expect(err).To(Equal(errorInvalidConfig))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
When("verifying a container with no port bindings", func() {
|
||||||
|
It("should not return an error", func() {
|
||||||
|
c := mockContainerWithPortBindings()
|
||||||
|
err := c.VerifyConfiguration()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
When("verifying a container with port bindings, but no exposed ports", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
c := mockContainerWithPortBindings("80/tcp")
|
||||||
|
c.containerInfo.Config.ExposedPorts = nil
|
||||||
|
err := c.VerifyConfiguration()
|
||||||
|
Expect(err).To(Equal(errorNoExposedPorts))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
When("verifying a container with port bindings and exposed ports is non-nil", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
c := mockContainerWithPortBindings("80/tcp")
|
||||||
|
c.containerInfo.Config.ExposedPorts = map[nat.Port]struct{}{"80/tcp": {}}
|
||||||
|
err := c.VerifyConfiguration()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
When("asked for metadata", func() {
|
When("asked for metadata", func() {
|
||||||
var c *Container
|
var c *Container
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
|
@ -281,10 +339,23 @@ var _ = Describe("the container", func() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
func mockContainerWithPortBindings(portBindingSources ...string) *Container {
|
||||||
|
mockContainer := mockContainerWithLabels(nil)
|
||||||
|
mockContainer.imageInfo = &types.ImageInspect{}
|
||||||
|
hostConfig := &container.HostConfig{
|
||||||
|
PortBindings: nat.PortMap{},
|
||||||
|
}
|
||||||
|
for _, pbs := range portBindingSources {
|
||||||
|
hostConfig.PortBindings[nat.Port(pbs)] = []nat.PortBinding{}
|
||||||
|
}
|
||||||
|
mockContainer.containerInfo.HostConfig = hostConfig
|
||||||
|
return mockContainer
|
||||||
|
}
|
||||||
|
|
||||||
func mockContainerWithImageName(name string) *Container {
|
func mockContainerWithImageName(name string) *Container {
|
||||||
container := mockContainerWithLabels(nil)
|
mockContainer := mockContainerWithLabels(nil)
|
||||||
container.containerInfo.Config.Image = name
|
mockContainer.containerInfo.Config.Image = name
|
||||||
return container
|
return mockContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func mockContainerWithLinks(links []string) *Container {
|
func mockContainerWithLinks(links []string) *Container {
|
||||||
|
@ -317,3 +388,15 @@ func mockContainerWithLabels(labels map[string]string) *Container {
|
||||||
}
|
}
|
||||||
return NewContainer(&content, nil)
|
return NewContainer(&content, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newClientNoAPI(pullImages, includeStopped, reviveStopped, removeVolumes, includeRestarting bool, warnOnHeadFailed string) Client {
|
||||||
|
return dockerClient{
|
||||||
|
api: nil,
|
||||||
|
pullImages: pullImages,
|
||||||
|
removeVolumes: removeVolumes,
|
||||||
|
includeStopped: includeStopped,
|
||||||
|
reviveStopped: reviveStopped,
|
||||||
|
includeRestarting: includeRestarting,
|
||||||
|
warnOnHeadFailed: warnOnHeadFailed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
7
pkg/container/errors.go
Normal file
7
pkg/container/errors.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var errorNoImageInfo = errors.New("no available image info")
|
||||||
|
var errorNoExposedPorts = errors.New("exposed ports does not match port bindings")
|
||||||
|
var errorInvalidConfig = errors.New("container configuration missing or invalid")
|
Loading…
Add table
Add a link
Reference in a new issue