oci.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  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 remote
  14. import (
  15. "context"
  16. "encoding/json"
  17. "fmt"
  18. "os"
  19. "path/filepath"
  20. "strconv"
  21. "strings"
  22. "github.com/adrg/xdg"
  23. "github.com/distribution/reference"
  24. "github.com/docker/buildx/store/storeutil"
  25. "github.com/docker/buildx/util/imagetools"
  26. "github.com/docker/cli/cli/command"
  27. v1 "github.com/opencontainers/image-spec/specs-go/v1"
  28. "github.com/compose-spec/compose-go/loader"
  29. )
  30. func OCIRemoteLoaderEnabled() (bool, error) {
  31. if v := os.Getenv("COMPOSE_EXPERIMENTAL_OCI_REMOTE"); v != "" {
  32. enabled, err := strconv.ParseBool(v)
  33. if err != nil {
  34. return false, fmt.Errorf("COMPOSE_EXPERIMENTAL_OCI_REMOTE environment variable expects boolean value: %w", err)
  35. }
  36. return enabled, err
  37. }
  38. return false, nil
  39. }
  40. func NewOCIRemoteLoader(dockerCli command.Cli, offline bool) (loader.ResourceLoader, error) {
  41. // xdg.CacheFile creates the parent directories for the target file path
  42. // and returns the fully qualified path, so use "git" as a filename and
  43. // then chop it off after, i.e. no ~/.cache/docker-compose/git file will
  44. // ever be created
  45. cache, err := xdg.CacheFile(filepath.Join("docker-compose", "oci"))
  46. if err != nil {
  47. return nil, fmt.Errorf("initializing git cache: %w", err)
  48. }
  49. cache = filepath.Dir(cache)
  50. return ociRemoteLoader{
  51. cache: cache,
  52. dockerCli: dockerCli,
  53. offline: offline,
  54. }, err
  55. }
  56. type ociRemoteLoader struct {
  57. cache string
  58. dockerCli command.Cli
  59. offline bool
  60. }
  61. const prefix = "oci://"
  62. func (g ociRemoteLoader) Accept(path string) bool {
  63. return strings.HasPrefix(path, prefix)
  64. }
  65. func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error) {
  66. if g.offline {
  67. return "", nil
  68. }
  69. ref, err := reference.ParseDockerRef(path[len(prefix):])
  70. if err != nil {
  71. return "", err
  72. }
  73. opt, err := storeutil.GetImageConfig(g.dockerCli, nil)
  74. if err != nil {
  75. return "", err
  76. }
  77. resolver := imagetools.New(opt)
  78. content, descriptor, err := resolver.Get(ctx, ref.String())
  79. if err != nil {
  80. return "", err
  81. }
  82. local := filepath.Join(g.cache, descriptor.Digest.Hex())
  83. composeFile := filepath.Join(local, "compose.yaml")
  84. if _, err = os.Stat(local); os.IsNotExist(err) {
  85. err = os.MkdirAll(local, 0o700)
  86. if err != nil {
  87. return "", err
  88. }
  89. f, err := os.Create(composeFile)
  90. if err != nil {
  91. return "", err
  92. }
  93. defer f.Close() //nolint:errcheck
  94. var manifest v1.Manifest
  95. err = json.Unmarshal(content, &manifest)
  96. if err != nil {
  97. return "", err
  98. }
  99. if manifest.ArtifactType != "application/vnd.docker.compose.project" {
  100. return "", fmt.Errorf("%s is not a compose project OCI artifact, but %s", ref.String(), manifest.ArtifactType)
  101. }
  102. for i, layer := range manifest.Layers {
  103. digested, err := reference.WithDigest(ref, layer.Digest)
  104. if err != nil {
  105. return "", err
  106. }
  107. content, _, err := resolver.Get(ctx, digested.String())
  108. if err != nil {
  109. return "", err
  110. }
  111. if i > 0 {
  112. _, err = f.Write([]byte("\n---\n"))
  113. if err != nil {
  114. return "", err
  115. }
  116. }
  117. _, err = f.Write(content)
  118. if err != nil {
  119. return "", err
  120. }
  121. }
  122. }
  123. return composeFile, nil
  124. }
  125. var _ loader.ResourceLoader = ociRemoteLoader{}