mirror of
https://github.com/containrrr/watchtower.git
synced 2026-01-26 18:56:09 +01:00
previously, this was only done when marking dependant containers for restart. but since the names should ALWAYS start with a slash, this check could be performed every time the container is iterated. the only downside to this is that it's less performant (as it needs to copy the strings instead of just having the slices point to the same source string), but it should not be noticable.
269 lines
8.3 KiB
Go
269 lines
8.3 KiB
Go
package actions
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/containrrr/watchtower/internal/util"
|
|
"github.com/containrrr/watchtower/pkg/container"
|
|
"github.com/containrrr/watchtower/pkg/lifecycle"
|
|
"github.com/containrrr/watchtower/pkg/session"
|
|
"github.com/containrrr/watchtower/pkg/sorter"
|
|
"github.com/containrrr/watchtower/pkg/types"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Update looks at the running Docker containers to see if any of the images
|
|
// used to start those containers have been updated. If a change is detected in
|
|
// any of the images, the associated containers are stopped and restarted with
|
|
// the new image.
|
|
func Update(client container.Client, params types.UpdateParams) (types.Report, error) {
|
|
log.Debug("Checking containers for updated images")
|
|
progress := &session.Progress{}
|
|
staleCount := 0
|
|
|
|
if params.LifecycleHooks {
|
|
lifecycle.ExecutePreChecks(client, params)
|
|
}
|
|
|
|
containers, err := client.ListContainers(params.Filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
staleCheckFailed := 0
|
|
|
|
for i, targetContainer := range containers {
|
|
stale, newestImage, err := client.IsContainerStale(targetContainer)
|
|
shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly()
|
|
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 {
|
|
log.Infof("Unable to update container %q: %v. Proceeding to next.", targetContainer.Name(), err)
|
|
stale = false
|
|
staleCheckFailed++
|
|
progress.AddSkipped(targetContainer, err)
|
|
} else {
|
|
progress.AddScanned(targetContainer, newestImage)
|
|
}
|
|
containers[i].Stale = stale
|
|
|
|
if stale {
|
|
staleCount++
|
|
}
|
|
}
|
|
|
|
containers, err = sorter.SortByDependencies(containers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
UpdateImplicitRestart(containers)
|
|
|
|
var containersToUpdate []container.Container
|
|
if !params.MonitorOnly {
|
|
for _, c := range containers {
|
|
if !c.IsMonitorOnly() {
|
|
containersToUpdate = append(containersToUpdate, c)
|
|
progress.MarkForUpdate(c.ID())
|
|
}
|
|
}
|
|
}
|
|
|
|
if params.RollingRestart {
|
|
progress.UpdateFailed(performRollingRestart(containersToUpdate, client, params))
|
|
} else {
|
|
failedStop, stoppedImages := stopContainersInReversedOrder(containersToUpdate, client, params)
|
|
progress.UpdateFailed(failedStop)
|
|
failedStart := restartContainersInSortedOrder(containersToUpdate, client, params, stoppedImages)
|
|
progress.UpdateFailed(failedStart)
|
|
}
|
|
|
|
if params.LifecycleHooks {
|
|
lifecycle.ExecutePostChecks(client, params)
|
|
}
|
|
return progress.Report(), nil
|
|
}
|
|
|
|
func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {
|
|
cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
|
|
failed := make(map[types.ContainerID]error, len(containers))
|
|
|
|
for i := len(containers) - 1; i >= 0; i-- {
|
|
if containers[i].ToRestart() {
|
|
err := stopStaleContainer(containers[i], client, params)
|
|
if err != nil {
|
|
failed[containers[i].ID()] = err
|
|
} else {
|
|
if err := restartStaleContainer(containers[i], client, params); err != nil {
|
|
failed[containers[i].ID()] = err
|
|
} else if containers[i].Stale {
|
|
// Only add (previously) stale containers' images to cleanup
|
|
cleanupImageIDs[containers[i].ImageID()] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if params.Cleanup {
|
|
cleanupImages(client, cleanupImageIDs)
|
|
}
|
|
return failed
|
|
}
|
|
|
|
func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) {
|
|
failed = make(map[types.ContainerID]error, len(containers))
|
|
stopped = make(map[types.ImageID]bool, len(containers))
|
|
for i := len(containers) - 1; i >= 0; i-- {
|
|
if err := stopStaleContainer(containers[i], client, params); err != nil {
|
|
failed[containers[i].ID()] = err
|
|
} else {
|
|
// NOTE: If a container is restarted due to a dependency this might be empty
|
|
stopped[containers[i].SafeImageID()] = true
|
|
}
|
|
|
|
}
|
|
return
|
|
}
|
|
|
|
func stopStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error {
|
|
if container.IsWatchtower() {
|
|
log.Debugf("This is the watchtower container %s", container.Name())
|
|
return nil
|
|
}
|
|
|
|
if !container.ToRestart() {
|
|
return nil
|
|
}
|
|
|
|
// Perform an additional check here to prevent us from stopping a linked container we cannot restart
|
|
if container.LinkedToRestarting {
|
|
if err := container.VerifyConfiguration(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if params.LifecycleHooks {
|
|
skipUpdate, err := lifecycle.ExecutePreUpdateCommand(client, container)
|
|
if err != nil {
|
|
log.Error(err)
|
|
log.Info("Skipping container as the pre-update command failed")
|
|
return err
|
|
}
|
|
if skipUpdate {
|
|
log.Debug("Skipping container as the pre-update command returned exit code 75 (EX_TEMPFAIL)")
|
|
return errors.New("skipping container as the pre-update command returned exit code 75 (EX_TEMPFAIL)")
|
|
}
|
|
}
|
|
|
|
if err := client.StopContainer(container, params.Timeout); err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error {
|
|
cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
|
|
failed := make(map[types.ContainerID]error, len(containers))
|
|
|
|
for _, c := range containers {
|
|
if !c.ToRestart() {
|
|
continue
|
|
}
|
|
if stoppedImages[c.SafeImageID()] {
|
|
if err := restartStaleContainer(c, client, params); err != nil {
|
|
failed[c.ID()] = err
|
|
} else if c.Stale {
|
|
// Only add (previously) stale containers' images to cleanup
|
|
cleanupImageIDs[c.ImageID()] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if params.Cleanup {
|
|
cleanupImages(client, cleanupImageIDs)
|
|
}
|
|
|
|
return failed
|
|
}
|
|
|
|
func cleanupImages(client container.Client, imageIDs map[types.ImageID]bool) {
|
|
for imageID := range imageIDs {
|
|
if imageID == "" {
|
|
continue
|
|
}
|
|
if err := client.RemoveImageByID(imageID); err != nil {
|
|
log.Error(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func restartStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error {
|
|
// Since we can't shutdown a watchtower container immediately, we need to
|
|
// start the new one while the old one is still running. This prevents us
|
|
// from re-using the same container name so we first rename the current
|
|
// instance so that the new one can adopt the old name.
|
|
if container.IsWatchtower() {
|
|
if err := client.RenameContainer(container, util.RandName()); err != nil {
|
|
log.Error(err)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if !params.NoRestart {
|
|
if newContainerID, err := client.StartContainer(container); err != nil {
|
|
log.Error(err)
|
|
return err
|
|
} else if container.ToRestart() && params.LifecycleHooks {
|
|
lifecycle.ExecutePostUpdateCommand(client, newContainerID)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateImplicitRestart iterates through the passed containers, setting the
|
|
// `LinkedToRestarting` flag if any of it's linked containers are marked for restart
|
|
func UpdateImplicitRestart(containers []container.Container) {
|
|
|
|
for ci, c := range containers {
|
|
if c.ToRestart() {
|
|
// The container is already marked for restart, no need to check
|
|
continue
|
|
}
|
|
|
|
if link := linkedContainerMarkedForRestart(c.Links(), containers); link != "" {
|
|
log.WithFields(log.Fields{
|
|
"restarting": link,
|
|
"linked": c.Name(),
|
|
}).Debug("container is linked to restarting")
|
|
// NOTE: To mutate the array, the `c` variable cannot be used as it's a copy
|
|
containers[ci].LinkedToRestarting = true
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// linkedContainerMarkedForRestart returns the name of the first link that matches a
|
|
// container marked for restart
|
|
func linkedContainerMarkedForRestart(links []string, containers []container.Container) string {
|
|
for _, linkName := range links {
|
|
for _, candidate := range containers {
|
|
if candidate.Name() == linkName && candidate.ToRestart() {
|
|
return linkName
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|