publish.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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. "crypto/sha256"
  17. "errors"
  18. "fmt"
  19. "os"
  20. "github.com/compose-spec/compose-go/v2/loader"
  21. "github.com/compose-spec/compose-go/v2/types"
  22. "github.com/distribution/reference"
  23. "github.com/docker/buildx/util/imagetools"
  24. "github.com/docker/cli/cli/command"
  25. "github.com/docker/compose/v2/internal/ocipush"
  26. "github.com/docker/compose/v2/pkg/api"
  27. "github.com/docker/compose/v2/pkg/compose/transform"
  28. "github.com/docker/compose/v2/pkg/progress"
  29. "github.com/docker/compose/v2/pkg/prompt"
  30. )
  31. func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
  32. return progress.RunWithTitle(ctx, func(ctx context.Context) error {
  33. return s.publish(ctx, project, repository, options)
  34. }, s.stdinfo(), "Publishing")
  35. }
  36. func (s *composeService) publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
  37. accept, err := s.preChecks(project, options)
  38. if err != nil {
  39. return err
  40. }
  41. if !accept {
  42. return nil
  43. }
  44. err = s.Push(ctx, project, api.PushOptions{IgnoreFailures: true, ImageMandatory: true})
  45. if err != nil {
  46. return err
  47. }
  48. named, err := reference.ParseDockerRef(repository)
  49. if err != nil {
  50. return err
  51. }
  52. resolver := imagetools.New(imagetools.Opt{
  53. Auth: s.configFile(),
  54. })
  55. var layers []ocipush.Pushable
  56. extFiles := map[string]string{}
  57. for _, file := range project.ComposeFiles {
  58. data, err := processFile(ctx, file, project, extFiles)
  59. if err != nil {
  60. return err
  61. }
  62. layerDescriptor := ocipush.DescriptorForComposeFile(file, data)
  63. layers = append(layers, ocipush.Pushable{
  64. Descriptor: layerDescriptor,
  65. Data: data,
  66. })
  67. }
  68. extLayers, err := processExtends(ctx, project, extFiles)
  69. if err != nil {
  70. return err
  71. }
  72. layers = append(layers, extLayers...)
  73. if options.WithEnvironment {
  74. layers = append(layers, envFileLayers(project)...)
  75. }
  76. if options.ResolveImageDigests {
  77. yaml, err := s.generateImageDigestsOverride(ctx, project)
  78. if err != nil {
  79. return err
  80. }
  81. layerDescriptor := ocipush.DescriptorForComposeFile("image-digests.yaml", yaml)
  82. layers = append(layers, ocipush.Pushable{
  83. Descriptor: layerDescriptor,
  84. Data: yaml,
  85. })
  86. }
  87. w := progress.ContextWriter(ctx)
  88. w.Event(progress.Event{
  89. ID: repository,
  90. Text: "publishing",
  91. Status: progress.Working,
  92. })
  93. if !s.dryRun {
  94. err = ocipush.PushManifest(ctx, resolver, named, layers, options.OCIVersion)
  95. if err != nil {
  96. w.Event(progress.Event{
  97. ID: repository,
  98. Text: "publishing",
  99. Status: progress.Error,
  100. })
  101. return err
  102. }
  103. }
  104. w.Event(progress.Event{
  105. ID: repository,
  106. Text: "published",
  107. Status: progress.Done,
  108. })
  109. return nil
  110. }
  111. func processExtends(ctx context.Context, project *types.Project, extFiles map[string]string) ([]ocipush.Pushable, error) {
  112. var layers []ocipush.Pushable
  113. moreExtFiles := map[string]string{}
  114. for xf, hash := range extFiles {
  115. data, err := processFile(ctx, xf, project, moreExtFiles)
  116. if err != nil {
  117. return nil, err
  118. }
  119. layerDescriptor := ocipush.DescriptorForComposeFile(hash, data)
  120. layerDescriptor.Annotations["com.docker.compose.extends"] = "true"
  121. layers = append(layers, ocipush.Pushable{
  122. Descriptor: layerDescriptor,
  123. Data: data,
  124. })
  125. }
  126. for f, hash := range moreExtFiles {
  127. if _, ok := extFiles[f]; ok {
  128. delete(moreExtFiles, f)
  129. }
  130. extFiles[f] = hash
  131. }
  132. if len(moreExtFiles) > 0 {
  133. extLayers, err := processExtends(ctx, project, moreExtFiles)
  134. if err != nil {
  135. return nil, err
  136. }
  137. layers = append(layers, extLayers...)
  138. }
  139. return layers, nil
  140. }
  141. func processFile(ctx context.Context, file string, project *types.Project, extFiles map[string]string) ([]byte, error) {
  142. f, err := os.ReadFile(file)
  143. if err != nil {
  144. return nil, err
  145. }
  146. base, err := loader.LoadWithContext(ctx, types.ConfigDetails{
  147. WorkingDir: project.WorkingDir,
  148. Environment: project.Environment,
  149. ConfigFiles: []types.ConfigFile{
  150. {
  151. Filename: file,
  152. Content: f,
  153. },
  154. },
  155. }, func(options *loader.Options) {
  156. options.SkipValidation = true
  157. options.SkipExtends = true
  158. options.SkipConsistencyCheck = true
  159. options.ResolvePaths = true
  160. })
  161. if err != nil {
  162. return nil, err
  163. }
  164. for name, service := range base.Services {
  165. if service.Extends == nil {
  166. continue
  167. }
  168. xf := service.Extends.File
  169. if xf == "" {
  170. continue
  171. }
  172. if _, err = os.Stat(service.Extends.File); os.IsNotExist(err) {
  173. // No local file, while we loaded the project successfully: This is actually a remote resource
  174. continue
  175. }
  176. hash := fmt.Sprintf("%x.yaml", sha256.Sum256([]byte(xf)))
  177. extFiles[xf] = hash
  178. f, err = transform.ReplaceExtendsFile(f, name, hash)
  179. if err != nil {
  180. return nil, err
  181. }
  182. }
  183. return f, nil
  184. }
  185. func (s *composeService) generateImageDigestsOverride(ctx context.Context, project *types.Project) ([]byte, error) {
  186. project, err := project.WithProfiles([]string{"*"})
  187. if err != nil {
  188. return nil, err
  189. }
  190. project, err = project.WithImagesResolved(ImageDigestResolver(ctx, s.configFile(), s.apiClient()))
  191. if err != nil {
  192. return nil, err
  193. }
  194. override := types.Project{
  195. Services: types.Services{},
  196. }
  197. for name, service := range project.Services {
  198. override.Services[name] = types.ServiceConfig{
  199. Image: service.Image,
  200. }
  201. }
  202. return override.MarshalYAML()
  203. }
  204. func (s *composeService) preChecks(project *types.Project, options api.PublishOptions) (bool, error) {
  205. if ok, err := s.checkOnlyBuildSection(project); !ok {
  206. return false, err
  207. }
  208. envVariables, err := s.checkEnvironmentVariables(project, options)
  209. if err != nil {
  210. return false, err
  211. }
  212. if !options.AssumeYes && len(envVariables) > 0 {
  213. fmt.Println("you are about to publish environment variables within your OCI artifact.\n" +
  214. "please double check that you are not leaking sensitive data")
  215. for key, val := range envVariables {
  216. _, _ = fmt.Fprintln(s.dockerCli.Out(), "Service/Config ", key)
  217. for k, v := range val {
  218. _, _ = fmt.Fprintf(s.dockerCli.Out(), "%s=%v\n", k, *v)
  219. }
  220. }
  221. return acceptPublishEnvVariables(s.dockerCli)
  222. }
  223. for name, config := range project.Services {
  224. for _, volume := range config.Volumes {
  225. if volume.Type == types.VolumeTypeBind {
  226. return false, fmt.Errorf("cannot publish compose file: service %q relies on bind-mount. You should use volumes", name)
  227. }
  228. }
  229. }
  230. return true, nil
  231. }
  232. func (s *composeService) checkEnvironmentVariables(project *types.Project, options api.PublishOptions) (map[string]types.MappingWithEquals, error) {
  233. envVarList := map[string]types.MappingWithEquals{}
  234. errorList := map[string][]string{}
  235. for _, service := range project.Services {
  236. if len(service.EnvFiles) > 0 {
  237. errorList[service.Name] = append(errorList[service.Name], fmt.Sprintf("service %q has env_file declared.", service.Name))
  238. }
  239. if len(service.Environment) > 0 {
  240. errorList[service.Name] = append(errorList[service.Name], fmt.Sprintf("service %q has environment variable(s) declared.", service.Name))
  241. envVarList[service.Name] = service.Environment
  242. }
  243. }
  244. for _, config := range project.Configs {
  245. if config.Environment != "" {
  246. errorList[config.Name] = append(errorList[config.Name], fmt.Sprintf("config %q is declare as an environment variable.", config.Name))
  247. envVarList[config.Name] = types.NewMappingWithEquals([]string{fmt.Sprintf("%s=%s", config.Name, config.Environment)})
  248. }
  249. }
  250. if !options.WithEnvironment && len(errorList) > 0 {
  251. errorMsgSuffix := "To avoid leaking sensitive data, you must either explicitly allow the sending of environment variables by using the --with-env flag,\n" +
  252. "or remove sensitive data from your Compose configuration"
  253. errorMsg := ""
  254. for _, errors := range errorList {
  255. for _, err := range errors {
  256. errorMsg += fmt.Sprintf("%s\n", err)
  257. }
  258. }
  259. return nil, fmt.Errorf("%s%s", errorMsg, errorMsgSuffix)
  260. }
  261. return envVarList, nil
  262. }
  263. func acceptPublishEnvVariables(cli command.Cli) (bool, error) {
  264. msg := "Are you ok to publish these environment variables? [y/N]: "
  265. confirm, err := prompt.NewPrompt(cli.In(), cli.Out()).Confirm(msg, false)
  266. return confirm, err
  267. }
  268. func envFileLayers(project *types.Project) []ocipush.Pushable {
  269. var layers []ocipush.Pushable
  270. for _, service := range project.Services {
  271. for _, envFile := range service.EnvFiles {
  272. f, err := os.ReadFile(envFile.Path)
  273. if err != nil {
  274. // if we can't read the file, skip to the next one
  275. continue
  276. }
  277. layerDescriptor := ocipush.DescriptorForEnvFile(envFile.Path, f)
  278. layers = append(layers, ocipush.Pushable{
  279. Descriptor: layerDescriptor,
  280. Data: f,
  281. })
  282. }
  283. }
  284. return layers
  285. }
  286. func (s *composeService) checkOnlyBuildSection(project *types.Project) (bool, error) {
  287. errorList := []string{}
  288. for _, service := range project.Services {
  289. if service.Image == "" && service.Build != nil {
  290. errorList = append(errorList, service.Name)
  291. }
  292. }
  293. if len(errorList) > 0 {
  294. errMsg := "your Compose stack cannot be published as it only contains a build section for service(s):\n"
  295. for _, serviceInError := range errorList {
  296. errMsg += fmt.Sprintf("- %q\n", serviceInError)
  297. }
  298. return false, errors.New(errMsg)
  299. }
  300. return true, nil
  301. }