2015-07-21 16:04:41 +00:00
package actions
2015-07-13 21:41:04 +00:00
import (
2021-06-24 00:36:33 +02:00
"errors"
2023-12-21 19:28:25 -05:00
"time"
2022-04-18 19:36:38 +02:00
2019-07-22 10:20:11 +02:00
"github.com/containrrr/watchtower/internal/util"
2019-07-21 20:14:28 +02:00
"github.com/containrrr/watchtower/pkg/container"
2020-01-11 23:35:25 +01:00
"github.com/containrrr/watchtower/pkg/lifecycle"
2021-06-27 09:05:01 +02:00
"github.com/containrrr/watchtower/pkg/session"
2020-01-11 23:35:25 +01:00
"github.com/containrrr/watchtower/pkg/sorter"
"github.com/containrrr/watchtower/pkg/types"
2019-04-04 23:08:44 +02:00
log "github.com/sirupsen/logrus"
2015-07-13 21:41:04 +00:00
)
2015-07-31 22:23:17 +00:00
// 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.
2021-06-27 09:05:01 +02:00
func Update ( client container . Client , params types . UpdateParams ) ( types . Report , error ) {
2017-04-12 18:32:00 +02:00
log . Debug ( "Checking containers for updated images" )
2021-06-27 09:05:01 +02:00
progress := & session . Progress { }
2021-01-06 22:28:32 +01:00
staleCount := 0
2015-07-22 21:58:16 +00:00
2020-01-11 23:35:25 +01:00
if params . LifecycleHooks {
lifecycle . ExecutePreChecks ( client , params )
}
2019-09-15 16:58:46 +02:00
2019-04-07 15:52:56 +02:00
containers , err := client . ListContainers ( params . Filter )
2015-07-13 21:41:04 +00:00
if err != nil {
2021-01-06 22:28:32 +01:00
return nil , err
2015-07-13 21:41:04 +00:00
}
2021-01-06 22:28:32 +01:00
staleCheckFailed := 0
2020-08-18 20:55:35 +02:00
for i , targetContainer := range containers {
2023-12-21 19:28:25 -05:00
// stale will be true if there is a more recent image than the current container is using
2023-09-16 17:13:41 +02:00
stale , newestImage , err := client . IsContainerStale ( targetContainer , params )
shouldUpdate := stale && ! params . NoRestart && ! targetContainer . IsMonitorOnly ( params )
2024-01-07 18:19:00 -06:00
imageUpdateDeferred := false
2023-12-21 19:28:25 -05:00
imageAgeDays := 0
2021-04-24 18:29:05 +02:00
if err == nil && shouldUpdate {
2023-12-21 19:28:25 -05:00
// Check to make sure we have all the necessary information for recreating the container, including ImageInfo
2021-04-24 18:29:05 +02:00
err = targetContainer . VerifyConfiguration ( )
2023-12-21 19:28:25 -05:00
if err == nil {
2024-01-07 18:01:59 -06:00
if params . DeferDays > 0 {
imageAgeDays , imageErr := getImageAgeDays ( targetContainer . ImageInfo ( ) . Created )
err = imageErr
2023-12-21 19:28:25 -05:00
if err == nil {
2024-01-07 18:19:00 -06:00
imageUpdateDeferred = imageAgeDays < params . DeferDays
2023-12-21 19:28:25 -05:00
}
}
} else if log . IsLevelEnabled ( log . TraceLevel ) {
// If the image information is incomplete and trace logging is enabled, log it for further diagnosis
2021-04-24 18:29:05 +02:00
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 )
}
}
2020-08-18 20:55:35 +02:00
}
2021-04-24 18:29:05 +02:00
2015-07-21 22:41:58 +00:00
if err != nil {
2021-04-24 18:29:05 +02:00
log . Infof ( "Unable to update container %q: %v. Proceeding to next." , targetContainer . Name ( ) , err )
2016-10-13 18:14:41 +01:00
stale = false
2021-01-06 22:28:32 +01:00
staleCheckFailed ++
2021-06-27 09:05:01 +02:00
progress . AddSkipped ( targetContainer , err )
2024-01-07 18:19:00 -06:00
} else if imageUpdateDeferred {
2024-01-07 18:01:59 -06:00
log . Infof ( "New image found for %s that was created %d day(s) ago but update deferred until %d day(s) after creation" , targetContainer . Name ( ) , imageAgeDays , params . DeferDays )
2023-12-21 19:28:25 -05:00
// technically the container is stale but we set it to false here because it is this stale flag that tells downstream methods whether to perform the update
stale = false
progress . AddScanned ( targetContainer , newestImage )
2024-01-07 18:01:59 -06:00
progress . MarkDeferred ( targetContainer . ID ( ) )
2021-06-27 09:05:01 +02:00
} else {
progress . AddScanned ( targetContainer , newestImage )
2015-07-14 20:51:12 +00:00
}
2023-04-12 17:36:01 +02:00
containers [ i ] . SetStale ( stale )
2021-01-06 22:28:32 +01:00
if stale {
staleCount ++
}
2015-07-14 20:51:12 +00:00
}
2015-07-13 21:41:04 +00:00
2020-01-11 23:35:25 +01:00
containers , err = sorter . SortByDependencies ( containers )
2015-07-14 20:51:12 +00:00
if err != nil {
2021-01-06 22:28:32 +01:00
return nil , err
2015-07-14 20:51:12 +00:00
}
2015-07-13 21:41:04 +00:00
2022-01-11 17:15:22 +01:00
UpdateImplicitRestart ( containers )
2015-07-13 21:41:04 +00:00
2023-12-21 19:28:25 -05:00
// containersToUpdate will contain all containers, not just those that need to be updated. The "stale" flag is checked via container.ToRestart()
2023-12-21 19:35:35 -05:00
// within stopContainersInReversedOrder and restartContainersInSortedOrder to skip containers with stale set to false (unless LinkedToRestarting set)
2024-01-07 18:01:59 -06:00
// NOTE: This logic is changing with latest PR on main repo
2023-04-12 17:36:01 +02:00
var containersToUpdate [ ] types . Container
2023-09-16 17:13:41 +02:00
for _ , c := range containers {
2024-01-07 18:01:59 -06:00
// pulling this change in from PR 1895 for now to avoid updating status incorrectly
if c . ToRestart ( ) && ! c . IsMonitorOnly ( params ) {
2023-09-16 17:13:41 +02:00
containersToUpdate = append ( containersToUpdate , c )
progress . MarkForUpdate ( c . ID ( ) )
2020-01-11 23:35:25 +01:00
}
2019-04-07 15:52:56 +02:00
}
2020-08-21 13:35:46 -07:00
if params . RollingRestart {
2021-06-27 09:05:01 +02:00
progress . UpdateFailed ( performRollingRestart ( containersToUpdate , client , params ) )
2020-08-21 13:35:46 -07:00
} else {
2021-06-27 09:05:01 +02:00
failedStop , stoppedImages := stopContainersInReversedOrder ( containersToUpdate , client , params )
progress . UpdateFailed ( failedStop )
failedStart := restartContainersInSortedOrder ( containersToUpdate , client , params , stoppedImages )
progress . UpdateFailed ( failedStart )
2020-08-21 13:35:46 -07:00
}
2021-01-06 22:28:32 +01:00
2020-01-11 23:35:25 +01:00
if params . LifecycleHooks {
lifecycle . ExecutePostChecks ( client , params )
}
2021-06-27 09:05:01 +02:00
return progress . Report ( ) , nil
2019-07-22 10:20:11 +02:00
}
2023-04-12 17:36:01 +02:00
func performRollingRestart ( containers [ ] types . Container , client container . Client , params types . UpdateParams ) map [ types . ContainerID ] error {
2021-06-27 09:05:01 +02:00
cleanupImageIDs := make ( map [ types . ImageID ] bool , len ( containers ) )
failed := make ( map [ types . ContainerID ] error , len ( containers ) )
2020-08-21 13:35:46 -07:00
for i := len ( containers ) - 1 ; i >= 0 ; i -- {
2021-04-18 18:37:35 +02:00
if containers [ i ] . ToRestart ( ) {
2021-06-24 00:36:33 +02:00
err := stopStaleContainer ( containers [ i ] , client , params )
if err != nil {
2021-06-27 09:05:01 +02:00
failed [ containers [ i ] . ID ( ) ] = err
2021-06-24 00:36:33 +02:00
} else {
if err := restartStaleContainer ( containers [ i ] , client , params ) ; err != nil {
2021-06-27 09:05:01 +02:00
failed [ containers [ i ] . ID ( ) ] = err
2023-04-12 17:36:01 +02:00
} else if containers [ i ] . IsStale ( ) {
2022-04-18 19:36:38 +02:00
// Only add (previously) stale containers' images to cleanup
cleanupImageIDs [ containers [ i ] . ImageID ( ) ] = true
2021-06-24 00:36:33 +02:00
}
2021-01-06 22:28:32 +01:00
}
2020-08-21 13:35:46 -07:00
}
}
if params . Cleanup {
cleanupImages ( client , cleanupImageIDs )
}
2021-01-06 22:28:32 +01:00
return failed
2020-08-21 13:35:46 -07:00
}
2023-04-12 17:36:01 +02:00
func stopContainersInReversedOrder ( containers [ ] types . Container , client container . Client , params types . UpdateParams ) ( failed map [ types . ContainerID ] error , stopped map [ types . ImageID ] bool ) {
2021-06-27 09:05:01 +02:00
failed = make ( map [ types . ContainerID ] error , len ( containers ) )
stopped = make ( map [ types . ImageID ] bool , len ( containers ) )
2015-07-14 20:51:12 +00:00
for i := len ( containers ) - 1 ; i >= 0 ; i -- {
2021-01-06 22:28:32 +01:00
if err := stopStaleContainer ( containers [ i ] , client , params ) ; err != nil {
2021-06-27 09:05:01 +02:00
failed [ containers [ i ] . ID ( ) ] = err
2021-06-24 00:36:33 +02:00
} else {
2022-04-18 19:36:38 +02:00
// NOTE: If a container is restarted due to a dependency this might be empty
stopped [ containers [ i ] . SafeImageID ( ) ] = true
2021-01-06 22:28:32 +01:00
}
2021-06-24 00:36:33 +02:00
2019-07-22 10:20:11 +02:00
}
2021-06-27 09:05:01 +02:00
return
2019-07-22 10:20:11 +02:00
}
2015-07-20 22:54:18 +00:00
2023-04-12 17:36:01 +02:00
func stopStaleContainer ( container types . Container , client container . Client , params types . UpdateParams ) error {
2019-07-22 10:20:11 +02:00
if container . IsWatchtower ( ) {
log . Debugf ( "This is the watchtower container %s" , container . Name ( ) )
2021-01-06 22:28:32 +01:00
return nil
2019-07-22 10:20:11 +02:00
}
2015-07-20 22:54:18 +00:00
2021-04-18 18:37:35 +02:00
if ! container . ToRestart ( ) {
2021-01-06 22:28:32 +01:00
return nil
2015-07-14 20:51:12 +00:00
}
2022-04-18 19:36:38 +02:00
// Perform an additional check here to prevent us from stopping a linked container we cannot restart
2023-04-12 17:36:01 +02:00
if container . IsLinkedToRestarting ( ) {
2022-04-18 19:36:38 +02:00
if err := container . VerifyConfiguration ( ) ; err != nil {
return err
}
}
2020-01-11 23:35:25 +01:00
if params . LifecycleHooks {
2021-06-27 09:05:01 +02:00
skipUpdate , err := lifecycle . ExecutePreUpdateCommand ( client , container )
2021-06-24 00:36:33 +02:00
if err != nil {
2020-01-03 18:51:32 +01:00
log . Error ( err )
log . Info ( "Skipping container as the pre-update command failed" )
2021-01-06 22:28:32 +01:00
return err
2020-01-03 18:51:32 +01:00
}
2021-06-27 09:05:01 +02:00
if skipUpdate {
2021-06-24 00:36:33 +02:00
log . Debug ( "Skipping container as the pre-update command returned exit code 75 (EX_TEMPFAIL)" )
2021-06-27 09:05:01 +02:00
return errors . New ( "skipping container as the pre-update command returned exit code 75 (EX_TEMPFAIL)" )
2021-06-24 00:36:33 +02:00
}
2020-01-11 23:35:25 +01:00
}
2020-04-24 13:45:24 +02:00
2019-07-27 01:37:16 +02:00
if err := client . StopContainer ( container , params . Timeout ) ; err != nil {
2019-07-22 10:20:11 +02:00
log . Error ( err )
2021-01-06 22:28:32 +01:00
return err
2019-07-22 10:20:11 +02:00
}
2021-01-06 22:28:32 +01:00
return nil
2019-07-22 10:20:11 +02:00
}
2023-04-12 17:36:01 +02:00
func restartContainersInSortedOrder ( containers [ ] types . Container , client container . Client , params types . UpdateParams , stoppedImages map [ types . ImageID ] bool ) map [ types . ContainerID ] error {
2021-06-27 09:05:01 +02:00
cleanupImageIDs := make ( map [ types . ImageID ] bool , len ( containers ) )
failed := make ( map [ types . ContainerID ] error , len ( containers ) )
2021-01-06 22:28:32 +01:00
for _ , c := range containers {
2021-04-18 18:37:35 +02:00
if ! c . ToRestart ( ) {
2019-07-22 10:20:11 +02:00
continue
}
2022-04-18 19:36:38 +02:00
if stoppedImages [ c . SafeImageID ( ) ] {
2021-06-24 00:36:33 +02:00
if err := restartStaleContainer ( c , client , params ) ; err != nil {
2021-06-27 09:05:01 +02:00
failed [ c . ID ( ) ] = err
2023-04-12 17:36:01 +02:00
} else if c . IsStale ( ) {
2022-04-18 19:36:38 +02:00
// Only add (previously) stale containers' images to cleanup
cleanupImageIDs [ c . ImageID ( ) ] = true
2021-06-24 00:36:33 +02:00
}
2021-01-06 22:28:32 +01:00
}
2019-10-13 14:46:06 -06:00
}
2020-08-21 13:35:46 -07:00
2019-10-13 14:46:06 -06:00
if params . Cleanup {
2021-06-27 09:05:01 +02:00
cleanupImages ( client , cleanupImageIDs )
2020-08-21 13:35:46 -07:00
}
2021-01-06 22:28:32 +01:00
return failed
2020-08-21 13:35:46 -07:00
}
2021-06-27 09:05:01 +02:00
func cleanupImages ( client container . Client , imageIDs map [ types . ImageID ] bool ) {
2020-08-21 13:35:46 -07:00
for imageID := range imageIDs {
2022-04-18 19:36:38 +02:00
if imageID == "" {
continue
}
2020-08-21 13:35:46 -07:00
if err := client . RemoveImageByID ( imageID ) ; err != nil {
log . Error ( err )
2019-10-13 14:46:06 -06:00
}
2019-07-22 10:20:11 +02:00
}
}
2015-07-20 22:54:18 +00:00
2023-04-12 17:36:01 +02:00
func restartStaleContainer ( container types . Container , client container . Client , params types . UpdateParams ) error {
2019-07-22 10:20:11 +02:00
// 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 )
2021-01-06 22:28:32 +01:00
return nil
2019-07-22 10:20:11 +02:00
}
}
2015-07-31 18:24:27 +00:00
2019-07-22 10:20:11 +02:00
if ! params . NoRestart {
2019-07-27 01:37:16 +02:00
if newContainerID , err := client . StartContainer ( container ) ; err != nil {
2019-07-22 10:20:11 +02:00
log . Error ( err )
2021-01-06 22:28:32 +01:00
return err
2021-04-18 18:37:35 +02:00
} else if container . ToRestart ( ) && params . LifecycleHooks {
2020-01-11 23:35:25 +01:00
lifecycle . ExecutePostUpdateCommand ( client , newContainerID )
2015-07-13 21:41:04 +00:00
}
}
2021-01-06 22:28:32 +01:00
return nil
2015-07-13 21:41:04 +00:00
}
2022-01-11 17:15:22 +01:00
// UpdateImplicitRestart iterates through the passed containers, setting the
// `LinkedToRestarting` flag if any of it's linked containers are marked for restart
2023-04-12 17:36:01 +02:00
func UpdateImplicitRestart ( containers [ ] types . Container ) {
2015-07-13 21:41:04 +00:00
2022-01-11 17:15:22 +01:00
for ci , c := range containers {
2021-04-18 18:37:35 +02:00
if c . ToRestart ( ) {
2022-01-11 17:15:22 +01:00
// The container is already marked for restart, no need to check
2015-07-14 20:51:12 +00:00
continue
}
2015-07-13 21:41:04 +00:00
2022-01-11 17:15:22 +01:00
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
2023-04-12 17:36:01 +02:00
containers [ ci ] . SetLinkedToRestarting ( true )
2022-01-11 17:15:22 +01:00
}
}
}
// linkedContainerMarkedForRestart returns the name of the first link that matches a
// container marked for restart
2023-04-12 17:36:01 +02:00
func linkedContainerMarkedForRestart ( links [ ] string , containers [ ] types . Container ) string {
2022-01-11 17:15:22 +01:00
for _ , linkName := range links {
for _ , candidate := range containers {
if candidate . Name ( ) == linkName && candidate . ToRestart ( ) {
return linkName
2015-07-14 20:51:12 +00:00
}
}
2015-07-13 21:41:04 +00:00
}
2022-01-11 17:15:22 +01:00
return ""
2020-01-03 18:51:32 +01:00
}
2023-12-21 19:28:25 -05:00
// Finds the difference between now and a given date, in full days. Input date is expected to originate
2024-01-07 18:01:59 -06:00
// from an image's Created attribute which will follow ISO 3339/8601 format.
// Reference: https://docs.docker.com/engine/api/v1.43/#tag/Image/operation/ImageInspect
2023-12-21 19:28:25 -05:00
func getImageAgeDays ( imageCreatedDateTime string ) ( int , error ) {
2024-01-07 18:01:59 -06:00
imageCreatedDate , error := time . Parse ( time . RFC3339Nano , imageCreatedDateTime )
2023-12-21 19:28:25 -05:00
if error != nil {
log . Errorf ( "Error parsing imageCreatedDateTime date (%s). Error: %s" , imageCreatedDateTime , error )
return - 1 , error
}
return int ( time . Since ( imageCreatedDate ) . Hours ( ) / 24 ) , nil
}