pull.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package compose
  14. import (
  15. "context"
  16. "encoding/base64"
  17. "encoding/json"
  18. "errors"
  19. "fmt"
  20. "io"
  21. "strings"
  22. "github.com/compose-spec/compose-go/types"
  23. "github.com/distribution/distribution/v3/reference"
  24. "github.com/docker/buildx/driver"
  25. moby "github.com/docker/docker/api/types"
  26. "github.com/docker/docker/pkg/jsonmessage"
  27. "github.com/docker/docker/registry"
  28. "github.com/hashicorp/go-multierror"
  29. "golang.org/x/sync/errgroup"
  30. "github.com/docker/compose/v2/pkg/api"
  31. "github.com/docker/compose/v2/pkg/progress"
  32. )
  33. func (s *composeService) Pull(ctx context.Context, project *types.Project, options api.PullOptions) error {
  34. if options.Quiet {
  35. return s.pull(ctx, project, options)
  36. }
  37. return progress.Run(ctx, func(ctx context.Context) error {
  38. return s.pull(ctx, project, options)
  39. })
  40. }
  41. func (s *composeService) pull(ctx context.Context, project *types.Project, opts api.PullOptions) error { //nolint:gocyclo
  42. info, err := s.apiClient().Info(ctx)
  43. if err != nil {
  44. return err
  45. }
  46. if info.IndexServerAddress == "" {
  47. info.IndexServerAddress = registry.IndexServer
  48. }
  49. images, err := s.getLocalImagesDigests(ctx, project)
  50. if err != nil {
  51. return err
  52. }
  53. w := progress.ContextWriter(ctx)
  54. eg, ctx := errgroup.WithContext(ctx)
  55. eg.SetLimit(s.maxConcurrency)
  56. var (
  57. mustBuild []string
  58. pullErrors = make([]error, len(project.Services))
  59. imagesBeingPulled = map[string]string{}
  60. )
  61. for i, service := range project.Services {
  62. i, service := i, service
  63. if service.Image == "" {
  64. w.Event(progress.Event{
  65. ID: service.Name,
  66. Status: progress.Done,
  67. Text: "Skipped - No image to be pulled",
  68. })
  69. continue
  70. }
  71. switch service.PullPolicy {
  72. case types.PullPolicyNever, types.PullPolicyBuild:
  73. w.Event(progress.Event{
  74. ID: service.Name,
  75. Status: progress.Done,
  76. Text: "Skipped",
  77. })
  78. continue
  79. case types.PullPolicyMissing, types.PullPolicyIfNotPresent:
  80. if imageAlreadyPresent(service.Image, images) {
  81. w.Event(progress.Event{
  82. ID: service.Name,
  83. Status: progress.Done,
  84. Text: "Skipped - Image is already present locally",
  85. })
  86. continue
  87. }
  88. }
  89. if service.Build != nil && opts.IgnoreBuildable {
  90. w.Event(progress.Event{
  91. ID: service.Name,
  92. Status: progress.Done,
  93. Text: "Skipped - Image can be built",
  94. })
  95. continue
  96. }
  97. if s, ok := imagesBeingPulled[service.Image]; ok {
  98. w.Event(progress.Event{
  99. ID: service.Name,
  100. Status: progress.Done,
  101. Text: fmt.Sprintf("Skipped - Image is already being pulled by %v", s),
  102. })
  103. continue
  104. }
  105. imagesBeingPulled[service.Image] = service.Name
  106. eg.Go(func() error {
  107. _, err := s.pullServiceImage(ctx, service, info, s.configFile(), w, false, project.Environment["DOCKER_DEFAULT_PLATFORM"])
  108. if err != nil {
  109. pullErrors[i] = err
  110. if service.Build != nil {
  111. mustBuild = append(mustBuild, service.Name)
  112. }
  113. if !opts.IgnoreFailures && service.Build == nil {
  114. if s.dryRun {
  115. w.Event(progress.Event{
  116. ID: service.Name,
  117. Status: progress.Error,
  118. Text: fmt.Sprintf(" - Pull error for image: %s", service.Image),
  119. })
  120. }
  121. // fail fast if image can't be pulled nor built
  122. return err
  123. }
  124. }
  125. return nil
  126. })
  127. }
  128. err = eg.Wait()
  129. if len(mustBuild) > 0 {
  130. w.TailMsgf("WARNING: Some service image(s) must be built from source by running:\n docker compose build %s", strings.Join(mustBuild, " "))
  131. }
  132. if err != nil {
  133. return err
  134. }
  135. if opts.IgnoreFailures {
  136. return nil
  137. }
  138. return multierror.Append(nil, pullErrors...).ErrorOrNil()
  139. }
  140. func imageAlreadyPresent(serviceImage string, localImages map[string]string) bool {
  141. normalizedImage, err := reference.ParseDockerRef(serviceImage)
  142. if err != nil {
  143. return false
  144. }
  145. tagged, ok := normalizedImage.(reference.NamedTagged)
  146. if !ok {
  147. return false
  148. }
  149. _, ok = localImages[serviceImage]
  150. return ok && tagged.Tag() != "latest"
  151. }
  152. func (s *composeService) pullServiceImage(ctx context.Context, service types.ServiceConfig, info moby.Info,
  153. configFile driver.Auth, w progress.Writer, quietPull bool, defaultPlatform string) (string, error) {
  154. w.Event(progress.Event{
  155. ID: service.Name,
  156. Status: progress.Working,
  157. Text: "Pulling",
  158. })
  159. ref, err := reference.ParseNormalizedNamed(service.Image)
  160. if err != nil {
  161. return "", err
  162. }
  163. encodedAuth, err := encodedAuth(ref, info, configFile)
  164. if err != nil {
  165. return "", err
  166. }
  167. platform := service.Platform
  168. if platform == "" {
  169. platform = defaultPlatform
  170. }
  171. stream, err := s.apiClient().ImagePull(ctx, service.Image, moby.ImagePullOptions{
  172. RegistryAuth: encodedAuth,
  173. Platform: platform,
  174. })
  175. // check if has error and the service has a build section
  176. // then the status should be warning instead of error
  177. if err != nil && service.Build != nil {
  178. w.Event(progress.Event{
  179. ID: service.Name,
  180. Status: progress.Warning,
  181. Text: "Warning",
  182. })
  183. return "", WrapCategorisedComposeError(err, PullFailure)
  184. }
  185. if err != nil {
  186. w.Event(progress.Event{
  187. ID: service.Name,
  188. Status: progress.Error,
  189. Text: "Error",
  190. })
  191. return "", WrapCategorisedComposeError(err, PullFailure)
  192. }
  193. dec := json.NewDecoder(stream)
  194. for {
  195. var jm jsonmessage.JSONMessage
  196. if err := dec.Decode(&jm); err != nil {
  197. if err == io.EOF {
  198. break
  199. }
  200. return "", WrapCategorisedComposeError(err, PullFailure)
  201. }
  202. if jm.Error != nil {
  203. return "", WrapCategorisedComposeError(errors.New(jm.Error.Message), PullFailure)
  204. }
  205. if !quietPull {
  206. toPullProgressEvent(service.Name, jm, w)
  207. }
  208. }
  209. w.Event(progress.Event{
  210. ID: service.Name,
  211. Status: progress.Done,
  212. Text: "Pulled",
  213. })
  214. inspected, _, err := s.apiClient().ImageInspectWithRaw(ctx, service.Image)
  215. if err != nil {
  216. return "", err
  217. }
  218. return inspected.ID, nil
  219. }
  220. func encodedAuth(ref reference.Named, info moby.Info, configFile driver.Auth) (string, error) {
  221. repoInfo, err := registry.ParseRepositoryInfo(ref)
  222. if err != nil {
  223. return "", err
  224. }
  225. key := repoInfo.Index.Name
  226. if repoInfo.Index.Official {
  227. key = info.IndexServerAddress
  228. }
  229. authConfig, err := configFile.GetAuthConfig(key)
  230. if err != nil {
  231. return "", err
  232. }
  233. buf, err := json.Marshal(authConfig)
  234. if err != nil {
  235. return "", err
  236. }
  237. return base64.URLEncoding.EncodeToString(buf), nil
  238. }
  239. func (s *composeService) pullRequiredImages(ctx context.Context, project *types.Project, images map[string]string, quietPull bool) error {
  240. info, err := s.apiClient().Info(ctx)
  241. if err != nil {
  242. return err
  243. }
  244. if info.IndexServerAddress == "" {
  245. info.IndexServerAddress = registry.IndexServer
  246. }
  247. var needPull []types.ServiceConfig
  248. for _, service := range project.Services {
  249. if service.Image == "" {
  250. continue
  251. }
  252. switch service.PullPolicy {
  253. case "", types.PullPolicyMissing, types.PullPolicyIfNotPresent:
  254. if _, ok := images[service.Image]; ok {
  255. continue
  256. }
  257. case types.PullPolicyNever, types.PullPolicyBuild:
  258. continue
  259. case types.PullPolicyAlways:
  260. // force pull
  261. }
  262. needPull = append(needPull, service)
  263. }
  264. if len(needPull) == 0 {
  265. return nil
  266. }
  267. return progress.Run(ctx, func(ctx context.Context) error {
  268. w := progress.ContextWriter(ctx)
  269. eg, ctx := errgroup.WithContext(ctx)
  270. eg.SetLimit(s.maxConcurrency)
  271. pulledImages := make([]string, len(needPull))
  272. for i, service := range needPull {
  273. i, service := i, service
  274. eg.Go(func() error {
  275. id, err := s.pullServiceImage(ctx, service, info, s.configFile(), w, quietPull, project.Environment["DOCKER_DEFAULT_PLATFORM"])
  276. pulledImages[i] = id
  277. if err != nil && isServiceImageToBuild(service, project.Services) {
  278. // image can be built, so we can ignore pull failure
  279. return nil
  280. }
  281. return err
  282. })
  283. }
  284. err := eg.Wait()
  285. for i, service := range needPull {
  286. if pulledImages[i] != "" {
  287. images[service.Image] = pulledImages[i]
  288. }
  289. }
  290. return err
  291. })
  292. }
  293. func isServiceImageToBuild(service types.ServiceConfig, services []types.ServiceConfig) bool {
  294. if service.Build != nil {
  295. return true
  296. }
  297. for _, depService := range services {
  298. if depService.Image == service.Image && depService.Build != nil {
  299. return true
  300. }
  301. }
  302. return false
  303. }
  304. const (
  305. PreparingPhase = "Preparing"
  306. WaitingPhase = "Waiting"
  307. PullingFsPhase = "Pulling fs layer"
  308. DownloadingPhase = "Downloading"
  309. DownloadCompletePhase = "Download complete"
  310. ExtractingPhase = "Extracting"
  311. VerifyingChecksumPhase = "Verifying Checksum"
  312. AlreadyExistsPhase = "Already exists"
  313. PullCompletePhase = "Pull complete"
  314. )
  315. func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, w progress.Writer) {
  316. if jm.ID == "" || jm.Progress == nil {
  317. return
  318. }
  319. var (
  320. text string
  321. total int64
  322. percent int
  323. current int64
  324. status = progress.Working
  325. )
  326. text = jm.Progress.String()
  327. switch jm.Status {
  328. case PreparingPhase, WaitingPhase, PullingFsPhase:
  329. percent = 0
  330. case DownloadingPhase, ExtractingPhase, VerifyingChecksumPhase:
  331. if jm.Progress != nil {
  332. current = jm.Progress.Current
  333. total = jm.Progress.Total
  334. if jm.Progress.Total > 0 {
  335. percent = int(jm.Progress.Current * 100 / jm.Progress.Total)
  336. }
  337. }
  338. case DownloadCompletePhase, AlreadyExistsPhase, PullCompletePhase:
  339. status = progress.Done
  340. percent = 100
  341. }
  342. if strings.Contains(jm.Status, "Image is up to date") ||
  343. strings.Contains(jm.Status, "Downloaded newer image") {
  344. status = progress.Done
  345. percent = 100
  346. }
  347. if jm.Error != nil {
  348. status = progress.Error
  349. text = jm.Error.Message
  350. }
  351. w.Event(progress.Event{
  352. ID: jm.ID,
  353. ParentID: parent,
  354. Current: current,
  355. Total: total,
  356. Percent: percent,
  357. Text: jm.Status,
  358. Status: status,
  359. StatusText: text,
  360. })
  361. }