mirror of
https://github.com/docker/compose.git
synced 2026-08-04 06:38:45 +00:00
fix: route OCI artifact pulls through Docker Desktop HTTP proxy
The compose process performs OCI artifact fetches in-process via containerd's docker resolver, whose default transport only honors HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars. Users behind PAC-only corporate proxies hit i/o timeouts on `oci://` includes and on `compose publish`. When Docker Desktop is the active engine and exposes httpproxy.sock, route the resolver through it (PAC-aware). Falls back to the default transport when DD is unavailable or the socket is missing. Modeled on docker/mcp-gateway PR #354. Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
This commit is contained in:
parent
659b269e52
commit
66c21c3b6c
7 changed files with 346 additions and 46 deletions
115
internal/desktop/proxy.go
Normal file
115
internal/desktop/proxy.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
Copyright 2026 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 desktop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/docker/compose/v5/internal/memnet"
|
||||
)
|
||||
|
||||
// Endpoint returns the Docker Desktop API socket endpoint advertised via the
|
||||
// engine info labels, or "" when the active engine is not Docker Desktop.
|
||||
func Endpoint(ctx context.Context, apiClient client.APIClient) (string, error) {
|
||||
res, err := apiClient.Info(ctx, client.InfoOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, l := range res.Info.Labels {
|
||||
if k, v, ok := strings.Cut(l, "="); ok && k == EngineLabel {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// httpProxySocketEndpoint derives Docker Desktop's HTTP proxy socket endpoint
|
||||
// from a Docker Desktop socket endpoint in the same directory. Returns ""
|
||||
// when the input is not a recognized form or when the derived unix socket
|
||||
// does not exist (older DD versions or non-DD installs).
|
||||
//
|
||||
// On macOS/Linux: unix:///path/to/Data/docker-cli.sock → unix:///path/to/Data/httpproxy.sock
|
||||
// On Windows: npipe://./pipe/dockerDesktopLinuxEngine → npipe://./pipe/dockerHttpProxy
|
||||
func httpProxySocketEndpoint(endpoint string) string {
|
||||
if sockPath, ok := strings.CutPrefix(endpoint, "unix://"); ok {
|
||||
proxyPath := filepath.Join(filepath.Dir(sockPath), "httpproxy.sock")
|
||||
if _, err := os.Stat(proxyPath); err != nil {
|
||||
return ""
|
||||
}
|
||||
return "unix://" + proxyPath
|
||||
}
|
||||
if strings.HasPrefix(endpoint, "npipe://") {
|
||||
return "npipe://./pipe/dockerHttpProxy"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ProxyTransport returns an http.RoundTripper that routes traffic through
|
||||
// Docker Desktop's PAC-aware HTTP proxy when DD exposes the proxy socket,
|
||||
// or nil when no override is needed (callers should use their own default
|
||||
// transport in that case — for the OCI resolver this means containerd's
|
||||
// built-in transport). Pass "" for endpoint when DD is not the active
|
||||
// engine.
|
||||
//
|
||||
// When DD is available, the returned transport is a clone of
|
||||
// http.DefaultTransport with only Proxy and DialContext overridden, so it
|
||||
// preserves stdlib timeout, pooling, and HTTP/2 defaults.
|
||||
func ProxyTransport(endpoint string) http.RoundTripper {
|
||||
proxyEndpoint := httpProxySocketEndpoint(endpoint)
|
||||
if proxyEndpoint == "" {
|
||||
logrus.Debug("Docker Desktop HTTP proxy not available; deferring to caller's default transport")
|
||||
return nil
|
||||
}
|
||||
logrus.Debugf("routing OCI traffic through Docker Desktop HTTP proxy at %s", proxyEndpoint)
|
||||
// Clone http.DefaultTransport to inherit stdlib timeout, pool, and
|
||||
// HTTP/2 defaults. Type-assertion is guarded since a process may have
|
||||
// replaced http.DefaultTransport with a wrapping RoundTripper (e.g.
|
||||
// instrumentation libraries); fall back to a fresh transport in that
|
||||
// case rather than panicking.
|
||||
var tr *http.Transport
|
||||
if defaultTr, ok := http.DefaultTransport.(*http.Transport); ok {
|
||||
tr = defaultTr.Clone()
|
||||
} else {
|
||||
tr = &http.Transport{}
|
||||
}
|
||||
tr.Proxy = http.ProxyURL(&url.URL{Scheme: "http"})
|
||||
tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
return memnet.DialEndpoint(ctx, proxyEndpoint)
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
// ProxyTransportFor discovers the Docker Desktop endpoint via apiClient and
|
||||
// returns the matching transport, or nil when DD is not active or discovery
|
||||
// fails (so callers fall back to their own default transport).
|
||||
func ProxyTransportFor(ctx context.Context, apiClient client.APIClient) http.RoundTripper {
|
||||
endpoint, err := Endpoint(ctx, apiClient)
|
||||
if err != nil {
|
||||
logrus.Debugf("could not detect Docker Desktop endpoint, deferring to caller's default transport: %v", err)
|
||||
return nil
|
||||
}
|
||||
return ProxyTransport(endpoint)
|
||||
}
|
||||
107
internal/desktop/proxy_test.go
Normal file
107
internal/desktop/proxy_test.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
Copyright 2026 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 desktop
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestHTTPProxySocketEndpoint_UnixSocketExists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cliSock := filepath.Join(dir, "docker-cli.sock")
|
||||
proxySock := filepath.Join(dir, "httpproxy.sock")
|
||||
mustTouch(t, cliSock)
|
||||
mustTouch(t, proxySock)
|
||||
|
||||
got := httpProxySocketEndpoint("unix://" + cliSock)
|
||||
assert.Equal(t, got, "unix://"+proxySock)
|
||||
}
|
||||
|
||||
func TestHTTPProxySocketEndpoint_UnixSocketMissing(t *testing.T) {
|
||||
// httpproxy.sock deliberately not created — older DD or partial install.
|
||||
dir := t.TempDir()
|
||||
cliSock := filepath.Join(dir, "docker-cli.sock")
|
||||
mustTouch(t, cliSock)
|
||||
|
||||
got := httpProxySocketEndpoint("unix://" + cliSock)
|
||||
assert.Equal(t, got, "", "stat miss must fall back so callers do not dial a non-existent socket")
|
||||
}
|
||||
|
||||
func TestHTTPProxySocketEndpoint_WindowsNamedPipe(t *testing.T) {
|
||||
got := httpProxySocketEndpoint("npipe://./pipe/dockerCli")
|
||||
assert.Equal(t, got, "npipe://./pipe/dockerHttpProxy")
|
||||
}
|
||||
|
||||
func TestHTTPProxySocketEndpoint_EmptyOrUnknown(t *testing.T) {
|
||||
assert.Equal(t, httpProxySocketEndpoint(""), "")
|
||||
assert.Equal(t, httpProxySocketEndpoint("tcp://localhost:1234"), "")
|
||||
}
|
||||
|
||||
func TestProxyTransport_NilWhenNoDockerDesktop(t *testing.T) {
|
||||
assert.Assert(t, ProxyTransport("") == nil,
|
||||
"must return nil so callers fall back to their own (e.g. containerd's) default transport")
|
||||
}
|
||||
|
||||
func TestProxyTransport_NilWhenSocketMissing(t *testing.T) {
|
||||
// no httpproxy.sock created
|
||||
dir := t.TempDir()
|
||||
cliSock := filepath.Join(dir, "docker-cli.sock")
|
||||
mustTouch(t, cliSock)
|
||||
|
||||
assert.Assert(t, ProxyTransport("unix://"+cliSock) == nil,
|
||||
"must return nil when DD endpoint is set but proxy socket is missing, not a transport that would dial a dead socket")
|
||||
}
|
||||
|
||||
func TestProxyTransport_RoutesThroughDockerDesktop(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("unix sockets test path; Windows uses named pipes which os.Stat handles differently")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
cliSock := filepath.Join(dir, "docker-cli.sock")
|
||||
proxySock := filepath.Join(dir, "httpproxy.sock")
|
||||
mustTouch(t, cliSock)
|
||||
mustTouch(t, proxySock)
|
||||
|
||||
got := ProxyTransport("unix://" + cliSock)
|
||||
tr, ok := got.(*http.Transport)
|
||||
assert.Assert(t, ok, "expected *http.Transport when DD endpoint is set and socket exists")
|
||||
assert.Assert(t, tr != http.DefaultTransport, "must be a clone, not DefaultTransport itself")
|
||||
|
||||
// Verify the clone preserved http.DefaultTransport's production
|
||||
// settings (timeouts, idle pool, HTTP/2). Compare to the source
|
||||
// fields rather than asserting fixed values so this test follows
|
||||
// stdlib changes.
|
||||
src := http.DefaultTransport.(*http.Transport)
|
||||
assert.Equal(t, tr.MaxIdleConns, src.MaxIdleConns)
|
||||
assert.Equal(t, tr.IdleConnTimeout, src.IdleConnTimeout)
|
||||
assert.Equal(t, tr.TLSHandshakeTimeout, src.TLSHandshakeTimeout)
|
||||
assert.Equal(t, tr.ExpectContinueTimeout, src.ExpectContinueTimeout)
|
||||
assert.Equal(t, tr.ForceAttemptHTTP2, src.ForceAttemptHTTP2)
|
||||
}
|
||||
|
||||
func mustTouch(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
f, err := os.Create(path)
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, f.Close())
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ package oci
|
|||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
|
@ -35,28 +36,35 @@ import (
|
|||
"github.com/docker/compose/v5/internal/registry"
|
||||
)
|
||||
|
||||
// NewResolver setup an OCI Resolver based on docker/cli config to provide registry credentials
|
||||
func NewResolver(config *configfile.ConfigFile, insecureRegistries ...string) remotes.Resolver {
|
||||
return docker.NewResolver(docker.ResolverOptions{
|
||||
Hosts: docker.ConfigureDefaultRegistries(
|
||||
docker.WithAuthorizer(docker.NewDockerAuthorizer(
|
||||
docker.WithAuthCreds(func(host string) (string, string, error) {
|
||||
host = registry.GetAuthConfigKey(host)
|
||||
auth, err := config.GetAuthConfig(host)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if auth.IdentityToken != "" {
|
||||
return "", auth.IdentityToken, nil
|
||||
}
|
||||
return auth.Username, auth.Password, nil
|
||||
}),
|
||||
)),
|
||||
docker.WithPlainHTTP(func(domain string) (bool, error) {
|
||||
// Should be used for testing **only**
|
||||
return slices.Contains(insecureRegistries, domain), nil
|
||||
// NewResolver sets up an OCI Resolver based on docker/cli config to provide
|
||||
// registry credentials. When transport is non-nil it is used as the HTTP
|
||||
// transport for all registry calls (e.g. to route through Docker Desktop's
|
||||
// PAC-aware proxy); nil falls back to containerd's default transport.
|
||||
func NewResolver(config *configfile.ConfigFile, transport http.RoundTripper, insecureRegistries ...string) remotes.Resolver {
|
||||
opts := []docker.RegistryOpt{
|
||||
docker.WithAuthorizer(docker.NewDockerAuthorizer(
|
||||
docker.WithAuthCreds(func(host string) (string, string, error) {
|
||||
host = registry.GetAuthConfigKey(host)
|
||||
auth, err := config.GetAuthConfig(host)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if auth.IdentityToken != "" {
|
||||
return "", auth.IdentityToken, nil
|
||||
}
|
||||
return auth.Username, auth.Password, nil
|
||||
}),
|
||||
),
|
||||
)),
|
||||
docker.WithPlainHTTP(func(domain string) (bool, error) {
|
||||
// Should be used for testing **only**
|
||||
return slices.Contains(insecureRegistries, domain), nil
|
||||
}),
|
||||
}
|
||||
if transport != nil {
|
||||
opts = append(opts, docker.WithClient(&http.Client{Transport: transport}))
|
||||
}
|
||||
return docker.NewResolver(docker.ResolverOptions{
|
||||
Hosts: docker.ConfigureDefaultRegistries(opts...),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
70
internal/oci/resolver_test.go
Normal file
70
internal/oci/resolver_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Copyright 2026 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 oci
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
// recordingRoundTripper counts RoundTrip invocations on a delegate so tests
|
||||
// can verify a supplied transport is actually used by the resolver.
|
||||
type recordingRoundTripper struct {
|
||||
delegate http.RoundTripper
|
||||
calls atomic.Int32
|
||||
}
|
||||
|
||||
func (r *recordingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
r.calls.Add(1)
|
||||
return r.delegate.RoundTrip(req)
|
||||
}
|
||||
|
||||
// TestNewResolver_UsesProvidedTransport guards that the transport passed to
|
||||
// NewResolver actually carries OCI traffic. The httptest server returns 401
|
||||
// so the resolver fails fast without real network access.
|
||||
func TestNewResolver_UsesProvidedTransport(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
host := server.Listener.Addr().String()
|
||||
// Bare *http.Transport (Proxy: nil) keeps the test hermetic — delegating
|
||||
// to http.DefaultTransport would honor HTTP[S]_PROXY env vars in CI or
|
||||
// dev shells and route requests away from our local httptest server.
|
||||
rec := &recordingRoundTripper{delegate: &http.Transport{}}
|
||||
|
||||
// Mark the test host insecure so the resolver uses HTTP scheme; this
|
||||
// avoids needing a TLS cert chain just to exercise plumbing.
|
||||
resolver := NewResolver(&configfile.ConfigFile{}, rec, host)
|
||||
|
||||
// We expect 401, but only care that the request reached our transport.
|
||||
_, _, _ = resolver.Resolve(t.Context(), host+"/test/image:latest")
|
||||
|
||||
assert.Assert(t, rec.calls.Load() > 0,
|
||||
"resolver did not invoke the supplied transport — wiring is broken")
|
||||
}
|
||||
|
||||
func TestNewResolver_NilTransportIsValid(t *testing.T) {
|
||||
resolver := NewResolver(&configfile.ConfigFile{}, nil)
|
||||
assert.Assert(t, resolver != nil, "NewResolver must return a non-nil resolver when transport is nil")
|
||||
}
|
||||
|
|
@ -18,28 +18,12 @@ package compose
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
|
||||
"github.com/docker/compose/v5/internal/desktop"
|
||||
)
|
||||
|
||||
// desktopEndpoint returns the Docker Desktop API socket address discovered
|
||||
// from the Docker engine info labels. It returns "" when the active engine
|
||||
// is not a Docker Desktop instance.
|
||||
func (s *composeService) desktopEndpoint(ctx context.Context) (string, error) {
|
||||
res, err := s.apiClient().Info(ctx, client.InfoOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, l := range res.Info.Labels {
|
||||
k, v, ok := strings.Cut(l, "=")
|
||||
if ok && k == desktop.EngineLabel {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
return desktop.Endpoint(ctx, s.apiClient())
|
||||
}
|
||||
|
||||
// isDesktopIntegrationActive returns true when Docker Desktop is the active engine.
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import (
|
|||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/docker/compose/v5/internal/desktop"
|
||||
"github.com/docker/compose/v5/internal/oci"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
"github.com/docker/compose/v5/pkg/compose/transform"
|
||||
|
|
@ -94,7 +95,7 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
|
|||
insecureRegistries = append(insecureRegistries, reference.Domain(named))
|
||||
}
|
||||
|
||||
resolver := oci.NewResolver(s.configFile(), insecureRegistries...)
|
||||
resolver := oci.NewResolver(s.configFile(), desktop.ProxyTransportFor(ctx, s.apiClient()), insecureRegistries...)
|
||||
|
||||
descriptor, err := oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/loader"
|
||||
"github.com/containerd/containerd/v2/core/images"
|
||||
|
|
@ -32,6 +34,7 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
spec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"github.com/docker/compose/v5/internal/desktop"
|
||||
"github.com/docker/compose/v5/internal/oci"
|
||||
"github.com/docker/compose/v5/pkg/api"
|
||||
)
|
||||
|
|
@ -79,7 +82,7 @@ func ociRemoteLoaderEnabled() (bool, error) {
|
|||
}
|
||||
|
||||
func NewOCIRemoteLoader(dockerCli command.Cli, offline bool, options api.OCIOptions) loader.ResourceLoader {
|
||||
return ociRemoteLoader{
|
||||
return &ociRemoteLoader{
|
||||
dockerCli: dockerCli,
|
||||
offline: offline,
|
||||
known: map[string]string{},
|
||||
|
|
@ -92,14 +95,26 @@ type ociRemoteLoader struct {
|
|||
offline bool
|
||||
known map[string]string
|
||||
insecureRegistries []string
|
||||
|
||||
// HTTP transport for the OCI resolver, initialized lazily so DD
|
||||
// detection happens once per loader rather than per Load() call.
|
||||
transportOnce sync.Once
|
||||
transport http.RoundTripper
|
||||
}
|
||||
|
||||
func (g ociRemoteLoader) Accept(path string) bool {
|
||||
func (g *ociRemoteLoader) httpTransport(ctx context.Context) http.RoundTripper {
|
||||
g.transportOnce.Do(func() {
|
||||
g.transport = desktop.ProxyTransportFor(ctx, g.dockerCli.Client())
|
||||
})
|
||||
return g.transport
|
||||
}
|
||||
|
||||
func (g *ociRemoteLoader) Accept(path string) bool {
|
||||
return strings.HasPrefix(path, OciPrefix)
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error) {
|
||||
func (g *ociRemoteLoader) Load(ctx context.Context, path string) (string, error) {
|
||||
enabled, err := ociRemoteLoaderEnabled()
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
@ -119,7 +134,7 @@ func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error)
|
|||
return "", err
|
||||
}
|
||||
|
||||
resolver := oci.NewResolver(g.dockerCli.ConfigFile(), g.insecureRegistries...)
|
||||
resolver := oci.NewResolver(g.dockerCli.ConfigFile(), g.httpTransport(ctx), g.insecureRegistries...)
|
||||
|
||||
descriptor, content, err := oci.Get(ctx, resolver, ref)
|
||||
if err != nil {
|
||||
|
|
@ -179,11 +194,11 @@ func (g ociRemoteLoader) Load(ctx context.Context, path string) (string, error)
|
|||
return filepath.Join(local, "compose.yaml"), nil
|
||||
}
|
||||
|
||||
func (g ociRemoteLoader) Dir(path string) string {
|
||||
func (g *ociRemoteLoader) Dir(path string) string {
|
||||
return g.known[path]
|
||||
}
|
||||
|
||||
func (g ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, manifest spec.Manifest, ref reference.Named, resolver remotes.Resolver) error {
|
||||
func (g *ociRemoteLoader) pullComposeFiles(ctx context.Context, local string, manifest spec.Manifest, ref reference.Named, resolver remotes.Resolver) error {
|
||||
err := os.MkdirAll(local, 0o700)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -259,4 +274,4 @@ func writeEnvFile(layer spec.Descriptor, local string, content []byte) error {
|
|||
return err
|
||||
}
|
||||
|
||||
var _ loader.ResourceLoader = ociRemoteLoader{}
|
||||
var _ loader.ResourceLoader = (*ociRemoteLoader)(nil)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue