push.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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/v2/types"
  23. "github.com/distribution/reference"
  24. "github.com/docker/buildx/driver"
  25. "github.com/docker/docker/api/types/image"
  26. "github.com/docker/docker/api/types/system"
  27. "github.com/docker/docker/pkg/jsonmessage"
  28. "github.com/docker/docker/registry"
  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) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
  34. if options.Quiet {
  35. return s.push(ctx, project, options)
  36. }
  37. return progress.RunWithTitle(ctx, func(ctx context.Context) error {
  38. return s.push(ctx, project, options)
  39. }, s.stdinfo(), "Pushing")
  40. }
  41. func (s *composeService) push(ctx context.Context, project *types.Project, options api.PushOptions) error {
  42. eg, ctx := errgroup.WithContext(ctx)
  43. eg.SetLimit(s.maxConcurrency)
  44. info, err := s.apiClient().Info(ctx)
  45. if err != nil {
  46. return err
  47. }
  48. if info.IndexServerAddress == "" {
  49. info.IndexServerAddress = registry.IndexServer
  50. }
  51. w := progress.ContextWriter(ctx)
  52. for _, service := range project.Services {
  53. if service.Build == nil || service.Image == "" {
  54. if options.ImageMandatory && service.Image == "" {
  55. return fmt.Errorf("%q attribute is mandatory to push an image for service %q", "service.image", service.Name)
  56. }
  57. w.Event(progress.Event{
  58. ID: service.Name,
  59. Status: progress.Done,
  60. Text: "Skipped",
  61. })
  62. continue
  63. }
  64. service := service
  65. tags := []string{service.Image}
  66. if service.Build != nil {
  67. tags = append(tags, service.Build.Tags...)
  68. }
  69. for _, tag := range tags {
  70. tag := tag
  71. eg.Go(func() error {
  72. err := s.pushServiceImage(ctx, tag, info, s.configFile(), w, options.Quiet)
  73. if err != nil {
  74. if !options.IgnoreFailures {
  75. return err
  76. }
  77. w.TailMsgf("Pushing %s: %s", service.Name, err.Error())
  78. }
  79. return nil
  80. })
  81. }
  82. }
  83. return eg.Wait()
  84. }
  85. func (s *composeService) pushServiceImage(ctx context.Context, tag string, info system.Info, configFile driver.Auth, w progress.Writer, quietPush bool) error {
  86. ref, err := reference.ParseNormalizedNamed(tag)
  87. if err != nil {
  88. return err
  89. }
  90. repoInfo, err := registry.ParseRepositoryInfo(ref)
  91. if err != nil {
  92. return err
  93. }
  94. key := repoInfo.Index.Name
  95. if repoInfo.Index.Official {
  96. key = info.IndexServerAddress
  97. }
  98. authConfig, err := configFile.GetAuthConfig(key)
  99. if err != nil {
  100. return err
  101. }
  102. buf, err := json.Marshal(authConfig)
  103. if err != nil {
  104. return err
  105. }
  106. stream, err := s.apiClient().ImagePush(ctx, tag, image.PushOptions{
  107. RegistryAuth: base64.URLEncoding.EncodeToString(buf),
  108. })
  109. if err != nil {
  110. return err
  111. }
  112. dec := json.NewDecoder(stream)
  113. for {
  114. var jm jsonmessage.JSONMessage
  115. if err := dec.Decode(&jm); err != nil {
  116. if errors.Is(err, io.EOF) {
  117. break
  118. }
  119. return err
  120. }
  121. if jm.Error != nil {
  122. return errors.New(jm.Error.Message)
  123. }
  124. if !quietPush {
  125. toPushProgressEvent(tag, jm, w)
  126. }
  127. }
  128. return nil
  129. }
  130. func toPushProgressEvent(prefix string, jm jsonmessage.JSONMessage, w progress.Writer) {
  131. if jm.ID == "" {
  132. // skipped
  133. return
  134. }
  135. var (
  136. text string
  137. status = progress.Working
  138. total int64
  139. current int64
  140. percent int
  141. )
  142. if isDone(jm) {
  143. status = progress.Done
  144. percent = 100
  145. }
  146. if jm.Error != nil {
  147. status = progress.Error
  148. text = jm.Error.Message
  149. }
  150. if jm.Progress != nil {
  151. text = jm.Progress.String()
  152. if jm.Progress.Total != 0 {
  153. current = jm.Progress.Current
  154. total = jm.Progress.Total
  155. if jm.Progress.Total > 0 {
  156. percent = int(jm.Progress.Current * 100 / jm.Progress.Total)
  157. }
  158. }
  159. }
  160. w.Event(progress.Event{
  161. ID: fmt.Sprintf("Pushing %s: %s", prefix, jm.ID),
  162. Text: jm.Status,
  163. Status: status,
  164. Current: current,
  165. Total: total,
  166. Percent: percent,
  167. StatusText: text,
  168. })
  169. }
  170. func isDone(msg jsonmessage.JSONMessage) bool {
  171. // TODO there should be a better way to detect push is done than such a status message check
  172. switch strings.ToLower(msg.Status) {
  173. case "pushed", "layer already exists":
  174. return true
  175. default:
  176. return false
  177. }
  178. }