diff --git a/go.mod b/go.mod index 29724e71df9..445add4dc37 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/buger/goterm v1.0.4 - github.com/compose-spec/compose-go/v2 v2.4.8 + github.com/compose-spec/compose-go/v2 v2.4.9-0.20250225151507-331db8fefcb7 github.com/containerd/containerd/v2 v2.0.2 github.com/containerd/platforms v1.0.0-rc.1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc @@ -168,6 +168,7 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/zclconf/go-cty v1.16.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect diff --git a/go.sum b/go.sum index 6fe7eb2be55..ef86f85124e 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= -github.com/compose-spec/compose-go/v2 v2.4.8 h1:7Myl8wDRl/4mRz77S+eyDJymGGEHu0diQdGSSeyq90A= -github.com/compose-spec/compose-go/v2 v2.4.8/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= +github.com/compose-spec/compose-go/v2 v2.4.9-0.20250225151507-331db8fefcb7 h1:7NlxAsQcWvLpFlEHsBo80sJ1UMMs84kkf0yXGs6de2k= +github.com/compose-spec/compose-go/v2 v2.4.9-0.20250225151507-331db8fefcb7/go.mod h1:6k5l/0TxCg0/2uLEhRVEsoBWBprS2uvZi32J7xub3lo= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= @@ -494,6 +494,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/pkg/api/api.go b/pkg/api/api.go index e73bfcc9ef1..4f59a30fb42 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -532,6 +532,7 @@ type ImageSummary struct { Repository string Tag string Size int64 + LastTagTime time.Time } // ServiceStatus hold status about a service diff --git a/pkg/compose/build.go b/pkg/compose/build.go index cc617984d0e..7b47ce62417 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -23,6 +23,7 @@ import ( "os" "strings" "sync" + "time" "github.com/compose-spec/compose-go/v2/types" "github.com/containerd/platforms" @@ -70,7 +71,7 @@ const bakeSuggest = "Compose now can delegate build to bake for better performan var suggest sync.Once //nolint:gocyclo -func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions, localImages map[string]string) (map[string]string, error) { +func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions, localImages map[string]api.ImageSummary) (map[string]string, error) { imageIDs := map[string]string{} serviceToBuild := types.Services{} @@ -287,7 +288,11 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types. } for name, digest := range builtImages { - images[name] = digest + images[name] = api.ImageSummary{ + Repository: name, + ID: digest, + LastTagTime: time.Now(), + } } return nil }, @@ -300,19 +305,16 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types. // set digest as com.docker.compose.image label so we can detect outdated containers for name, service := range project.Services { image := api.GetImageNameOrDefault(service, project.Name) - digest, ok := images[image] + img, ok := images[image] if ok { - if service.Labels == nil { - service.Labels = types.Labels{} - } - service.CustomLabels.Add(api.ImageDigestLabel, digest) + service.CustomLabels.Add(api.ImageDigestLabel, img.ID) } project.Services[name] = service } return nil } -func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) { +func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]api.ImageSummary, error) { var imageNames []string for _, s := range project.Services { imgName := api.GetImageNameOrDefault(s, project.Name) @@ -324,14 +326,10 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ if err != nil { return nil, err } - images := map[string]string{} - for name, info := range imgs { - images[name] = info.ID - } for i, service := range project.Services { imgName := api.GetImageNameOrDefault(service, project.Name) - digest, ok := images[imgName] + img, ok := imgs[imgName] if !ok { continue } @@ -340,7 +338,7 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ if err != nil { return nil, err } - inspect, err := s.apiClient().ImageInspect(ctx, digest) + inspect, err := s.apiClient().ImageInspect(ctx, img.ID) if err != nil { return nil, err } @@ -353,15 +351,15 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ // there is a local image, but it's for the wrong platform, so // pretend it doesn't exist so that we can pull/build an image // for the correct platform instead - delete(images, imgName) + delete(imgs, imgName) } } - project.Services[i].CustomLabels.Add(api.ImageDigestLabel, digest) + project.Services[i].CustomLabels.Add(api.ImageDigestLabel, img.ID) } - return images, nil + return imgs, nil } // resolveAndMergeBuildArgs returns the final set of build arguments to use for the service image build. diff --git a/pkg/compose/images.go b/pkg/compose/images.go index e344666a8bf..c7bbaff4145 100644 --- a/pkg/compose/images.go +++ b/pkg/compose/images.go @@ -101,10 +101,11 @@ func (s *composeService) getImageSummaries(ctx context.Context, repoTags []strin } l.Lock() summary[repoTag] = api.ImageSummary{ - ID: inspect.ID, - Repository: repository, - Tag: tag, - Size: inspect.Size, + ID: inspect.ID, + Repository: repository, + Tag: tag, + Size: inspect.Size, + LastTagTime: inspect.Metadata.LastTagTime, } l.Unlock() return nil diff --git a/pkg/compose/pull.go b/pkg/compose/pull.go index 992fa1bfea0..c15fdbdb517 100644 --- a/pkg/compose/pull.go +++ b/pkg/compose/pull.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "strings" + "time" "github.com/compose-spec/compose-go/v2/types" "github.com/distribution/reference" @@ -153,7 +154,7 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts return multierror.Append(nil, pullErrors...).ErrorOrNil() } -func imageAlreadyPresent(serviceImage string, localImages map[string]string) bool { +func imageAlreadyPresent(serviceImage string, localImages map[string]api.ImageSummary) bool { normalizedImage, err := reference.ParseDockerRef(serviceImage) if err != nil { return false @@ -288,23 +289,16 @@ func encodedAuth(ref reference.Named, configFile driver.Auth) (string, error) { return base64.URLEncoding.EncodeToString(buf), nil } -func (s *composeService) pullRequiredImages(ctx context.Context, project *types.Project, images map[string]string, quietPull bool) error { +func (s *composeService) pullRequiredImages(ctx context.Context, project *types.Project, images map[string]api.ImageSummary, quietPull bool) error { var needPull []types.ServiceConfig for _, service := range project.Services { - if service.Image == "" { - continue + pull, err := mustPull(service, images) + if err != nil { + return err } - switch service.PullPolicy { - case "", types.PullPolicyMissing, types.PullPolicyIfNotPresent: - if _, ok := images[service.Image]; ok { - continue - } - case types.PullPolicyNever, types.PullPolicyBuild: - continue - case types.PullPolicyAlways: - // force pull + if pull { + needPull = append(needPull, service) } - needPull = append(needPull, service) } if len(needPull) == 0 { return nil @@ -314,11 +308,15 @@ func (s *composeService) pullRequiredImages(ctx context.Context, project *types. w := progress.ContextWriter(ctx) eg, ctx := errgroup.WithContext(ctx) eg.SetLimit(s.maxConcurrency) - pulledImages := make([]string, len(needPull)) + pulledImages := make([]api.ImageSummary, len(needPull)) for i, service := range needPull { eg.Go(func() error { id, err := s.pullServiceImage(ctx, service, s.configFile(), w, quietPull, project.Environment["DOCKER_DEFAULT_PLATFORM"]) - pulledImages[i] = id + pulledImages[i] = api.ImageSummary{ + ID: id, + Repository: service.Image, + LastTagTime: time.Now(), + } if err != nil && isServiceImageToBuild(service, project.Services) { // image can be built, so we can ignore pull failure return nil @@ -328,7 +326,7 @@ func (s *composeService) pullRequiredImages(ctx context.Context, project *types. } err := eg.Wait() for i, service := range needPull { - if pulledImages[i] != "" { + if pulledImages[i].ID != "" { images[service.Image] = pulledImages[i] } } @@ -336,6 +334,32 @@ func (s *composeService) pullRequiredImages(ctx context.Context, project *types. }, s.stdinfo()) } +func mustPull(service types.ServiceConfig, images map[string]api.ImageSummary) (bool, error) { + if service.Image == "" { + return false, nil + } + policy, duration, err := service.GetPullPolicy() + if err != nil { + return false, err + } + switch policy { + case types.PullPolicyAlways: + // force pull + return true, nil + case types.PullPolicyNever, types.PullPolicyBuild: + return false, nil + case types.PullPolicyRefresh: + img, ok := images[service.Image] + if !ok { + return true, nil + } + return time.Now().After(img.LastTagTime.Add(duration)), nil + default: // Pull if missing + _, ok := images[service.Image] + return !ok, nil + } +} + func isServiceImageToBuild(service types.ServiceConfig, services types.Services) bool { if service.Build != nil { return true