| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- /*
- Copyright 2020 Docker Compose CLI authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package remote
- import (
- "context"
- "encoding/json"
- "fmt"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "github.com/adrg/xdg"
- "github.com/compose-spec/compose-go/loader"
- "github.com/distribution/reference"
- "github.com/docker/buildx/store/storeutil"
- "github.com/docker/buildx/util/imagetools"
- "github.com/docker/cli/cli/command"
- v1 "github.com/opencontainers/image-spec/specs-go/v1"
- )
- const OCI_REMOTE_ENABLED = "COMPOSE_EXPERIMENTAL_OCI_REMOTE"
- func ociRemoteLoaderEnabled() (bool, error) {
- if v := os.Getenv(OCI_REMOTE_ENABLED); v != "" {
- enabled, err := strconv.ParseBool(v)
- if err != nil {
- return false, fmt.Errorf("COMPOSE_EXPERIMENTAL_OCI_REMOTE environment variable expects boolean value: %w", err)
- }
- return enabled, err
- }
- return false, nil
- }
- func NewOCIRemoteLoader(dockerCli command.Cli, offline bool) (loader.ResourceLoader, error) {
- // xdg.CacheFile creates the parent directories for the target file path
- // and returns the fully qualified path, so use "git" as a filename and
- // then chop it off after, i.e. no ~/.cache/docker-compose/git file will
- // ever be created
- cache, err := xdg.CacheFile(filepath.Join("docker-compose", "oci"))
- if err != nil {
- return nil, fmt.Errorf("initializing git cache: %w", err)
- }
- cache = filepath.Dir(cache)
- return ociRemoteLoader{
- cache: cache,
- dockerCli: dockerCli,
- offline: offline,
- }, err
- }
- type ociRemoteLoader struct {
- cache string
- dockerCli command.Cli
- offline bool
- }
- const prefix = "oci://"
- func (g ociRemoteLoader) Accept(path string) bool {
- return strings.HasPrefix(path, prefix)
- }
- func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error) {
- enabled, err := ociRemoteLoaderEnabled()
- if err != nil {
- return "", err
- }
- if !enabled {
- return "", fmt.Errorf("experimental OCI remote resource is disabled. %q must be set", OCI_REMOTE_ENABLED)
- }
- if g.offline {
- return "", nil
- }
- ref, err := reference.ParseDockerRef(path[len(prefix):])
- if err != nil {
- return "", err
- }
- opt, err := storeutil.GetImageConfig(g.dockerCli, nil)
- if err != nil {
- return "", err
- }
- resolver := imagetools.New(opt)
- content, descriptor, err := resolver.Get(ctx, ref.String())
- if err != nil {
- return "", err
- }
- local := filepath.Join(g.cache, descriptor.Digest.Hex())
- composeFile := filepath.Join(local, "compose.yaml")
- if _, err = os.Stat(local); os.IsNotExist(err) {
- var manifest v1.Manifest
- err = json.Unmarshal(content, &manifest)
- if err != nil {
- return "", err
- }
- err2 := g.pullComposeFiles(ctx, local, composeFile, manifest, ref, resolver)
- if err2 != nil {
- return "", err2
- }
- }
- return composeFile, nil
- }
- func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, composeFile string, manifest v1.Manifest, ref reference.Named, resolver *imagetools.Resolver) error {
- err := os.MkdirAll(local, 0o700)
- if err != nil {
- return err
- }
- f, err := os.Create(composeFile)
- if err != nil {
- return err
- }
- defer f.Close() //nolint:errcheck
- if manifest.ArtifactType != "application/vnd.docker.compose.project" {
- return fmt.Errorf("%s is not a compose project OCI artifact, but %s", ref.String(), manifest.ArtifactType)
- }
- for i, layer := range manifest.Layers {
- digested, err := reference.WithDigest(ref, layer.Digest)
- if err != nil {
- return err
- }
- content, _, err := resolver.Get(ctx, digested.String())
- if err != nil {
- return err
- }
- if i > 0 {
- _, err = f.Write([]byte("\n---\n"))
- if err != nil {
- return err
- }
- }
- _, err = f.Write(content)
- if err != nil {
- return err
- }
- }
- return nil
- }
- var _ loader.ResourceLoader = ociRemoteLoader{}
|