push.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  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. "bytes"
  16. "context"
  17. "encoding/base64"
  18. "encoding/json"
  19. "fmt"
  20. "io"
  21. "github.com/compose-spec/compose-go/types"
  22. "github.com/distribution/distribution/v3/reference"
  23. "github.com/docker/buildx/driver"
  24. "github.com/docker/compose/v2/pkg/api"
  25. "github.com/docker/compose/v2/pkg/progress"
  26. moby "github.com/docker/docker/api/types"
  27. "github.com/docker/docker/pkg/jsonmessage"
  28. "github.com/docker/docker/registry"
  29. "github.com/opencontainers/go-digest"
  30. v1 "github.com/opencontainers/image-spec/specs-go/v1"
  31. "github.com/pkg/errors"
  32. "golang.org/x/sync/errgroup"
  33. "oras.land/oras-go/v2/content"
  34. "oras.land/oras-go/v2/registry/remote"
  35. )
  36. func (s *composeService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
  37. if options.Quiet {
  38. return s.push(ctx, project, options)
  39. }
  40. return progress.RunWithTitle(ctx, func(ctx context.Context) error {
  41. return s.push(ctx, project, options)
  42. }, s.stdinfo(), "Pushing")
  43. }
  44. func (s *composeService) push(upctx context.Context, project *types.Project, options api.PushOptions) error {
  45. eg, ctx := errgroup.WithContext(upctx)
  46. eg.SetLimit(s.maxConcurrency)
  47. info, err := s.apiClient().Info(ctx)
  48. if err != nil {
  49. return err
  50. }
  51. if info.IndexServerAddress == "" {
  52. info.IndexServerAddress = registry.IndexServer
  53. }
  54. w := progress.ContextWriter(ctx)
  55. for _, service := range project.Services {
  56. if service.Build == nil || service.Image == "" {
  57. w.Event(progress.Event{
  58. ID: service.Name,
  59. Status: progress.Done,
  60. Text: "Skipped",
  61. })
  62. continue
  63. }
  64. service := service
  65. eg.Go(func() error {
  66. err := s.pushServiceImage(ctx, service, info, s.configFile(), w, options.Quiet)
  67. if err != nil {
  68. if !options.IgnoreFailures {
  69. return err
  70. }
  71. w.TailMsgf("Pushing %s: %s", service.Name, err.Error())
  72. }
  73. return nil
  74. })
  75. }
  76. err = eg.Wait()
  77. if err != nil {
  78. return err
  79. }
  80. ctx = upctx
  81. if options.Repository != "" {
  82. repository, err := remote.NewRepository(options.Repository)
  83. if err != nil {
  84. return err
  85. }
  86. yaml, err := project.MarshalYAML()
  87. if err != nil {
  88. return err
  89. }
  90. manifests := []v1.Descriptor{
  91. {
  92. MediaType: "application/vnd.oci.artifact.manifest.v1+json",
  93. Digest: digest.FromBytes(yaml),
  94. Size: int64(len(yaml)),
  95. Data: yaml,
  96. ArtifactType: "application/vnd.docker.compose.yaml",
  97. },
  98. }
  99. for _, service := range project.Services {
  100. inspected, _, err := s.dockerCli.Client().ImageInspectWithRaw(ctx, service.Image)
  101. if err != nil {
  102. return err
  103. }
  104. manifests = append(manifests, v1.Descriptor{
  105. MediaType: v1.MediaTypeImageIndex,
  106. Digest: digest.Digest(inspected.RepoDigests[0]),
  107. Size: inspected.Size,
  108. Annotations: map[string]string{
  109. "com.docker.compose.service": service.Name,
  110. },
  111. })
  112. }
  113. manifest := v1.Index{
  114. MediaType: v1.MediaTypeImageIndex,
  115. Manifests: manifests,
  116. Annotations: map[string]string{
  117. "com.docker.compose": api.ComposeVersion,
  118. },
  119. }
  120. manifestContent, err := json.Marshal(manifest)
  121. if err != nil {
  122. return err
  123. }
  124. manifestDescriptor := content.NewDescriptorFromBytes(v1.MediaTypeImageIndex, manifestContent)
  125. err = repository.Push(ctx, manifestDescriptor, bytes.NewReader(manifestContent))
  126. if err != nil {
  127. return err
  128. }
  129. }
  130. return nil
  131. }
  132. func (s *composeService) pushServiceImage(ctx context.Context, service types.ServiceConfig, info moby.Info, configFile driver.Auth, w progress.Writer, quietPush bool) error {
  133. ref, err := reference.ParseNormalizedNamed(service.Image)
  134. if err != nil {
  135. return err
  136. }
  137. repoInfo, err := registry.ParseRepositoryInfo(ref)
  138. if err != nil {
  139. return err
  140. }
  141. key := repoInfo.Index.Name
  142. if repoInfo.Index.Official {
  143. key = info.IndexServerAddress
  144. }
  145. authConfig, err := configFile.GetAuthConfig(key)
  146. if err != nil {
  147. return err
  148. }
  149. buf, err := json.Marshal(authConfig)
  150. if err != nil {
  151. return err
  152. }
  153. stream, err := s.apiClient().ImagePush(ctx, service.Image, moby.ImagePushOptions{
  154. RegistryAuth: base64.URLEncoding.EncodeToString(buf),
  155. })
  156. if err != nil {
  157. return err
  158. }
  159. dec := json.NewDecoder(stream)
  160. for {
  161. var jm jsonmessage.JSONMessage
  162. if err := dec.Decode(&jm); err != nil {
  163. if err == io.EOF {
  164. break
  165. }
  166. return err
  167. }
  168. if jm.Error != nil {
  169. return errors.New(jm.Error.Message)
  170. }
  171. if !quietPush {
  172. toPushProgressEvent(service.Name, jm, w)
  173. }
  174. }
  175. return nil
  176. }
  177. func toPushProgressEvent(prefix string, jm jsonmessage.JSONMessage, w progress.Writer) {
  178. if jm.ID == "" {
  179. // skipped
  180. return
  181. }
  182. var (
  183. text string
  184. status = progress.Working
  185. total int64
  186. current int64
  187. percent int
  188. )
  189. if jm.Status == "Pushed" || jm.Status == "Already exists" {
  190. status = progress.Done
  191. percent = 100
  192. }
  193. if jm.Error != nil {
  194. status = progress.Error
  195. text = jm.Error.Message
  196. }
  197. if jm.Progress != nil {
  198. text = jm.Progress.String()
  199. if jm.Progress.Total != 0 {
  200. current = jm.Progress.Current
  201. total = jm.Progress.Total
  202. if jm.Progress.Total > 0 {
  203. percent = int(jm.Progress.Current * 100 / jm.Progress.Total)
  204. }
  205. }
  206. }
  207. w.Event(progress.Event{
  208. ID: fmt.Sprintf("Pushing %s: %s", prefix, jm.ID),
  209. Text: jm.Status,
  210. Status: status,
  211. Current: current,
  212. Total: total,
  213. Percent: percent,
  214. StatusText: text,
  215. })
  216. }