Merge component 'engine' from git@github.com:moby/moby master

This commit is contained in:
GordonTheTurtle
2018-02-21 17:06:03 +00:00
61 changed files with 1037 additions and 828 deletions

View File

@ -372,6 +372,10 @@ func (sr *swarmRouter) createSecret(ctx context.Context, w http.ResponseWriter,
if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
return err
}
version := httputils.VersionFromContext(ctx)
if secret.Templating != nil && versions.LessThan(version, "1.36") {
return errdefs.InvalidParameter(errors.Errorf("secret templating is not supported on the specified API version: %s", version))
}
id, err := sr.backend.CreateSecret(secret)
if err != nil {
@ -440,6 +444,11 @@ func (sr *swarmRouter) createConfig(ctx context.Context, w http.ResponseWriter,
return err
}
version := httputils.VersionFromContext(ctx)
if config.Templating != nil && versions.LessThan(version, "1.36") {
return errdefs.InvalidParameter(errors.Errorf("config templating is not supported on the specified API version: %s", version))
}
id, err := sr.backend.CreateConfig(config)
if err != nil {
return err

View File

@ -3339,6 +3339,13 @@ definitions:
Driver:
description: "Name of the secrets driver used to fetch the secret's value from an external secret store"
$ref: "#/definitions/Driver"
Templating:
description: |
Templating driver, if applicable
Templating controls whether and how to evaluate the config payload as
a template. If no driver is set, no templating is used.
$ref: "#/definitions/Driver"
Secret:
type: "object"
@ -3375,6 +3382,13 @@ definitions:
Base64-url-safe-encoded ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-3.2))
config data.
type: "string"
Templating:
description: |
Templating driver, if applicable
Templating controls whether and how to evaluate the config payload as
a template. If no driver is set, no templating is used.
$ref: "#/definitions/Driver"
Config:
type: "object"

View File

@ -13,6 +13,10 @@ type Config struct {
type ConfigSpec struct {
Annotations
Data []byte `json:",omitempty"`
// Templating controls whether and how to evaluate the config payload as
// a template. If it is not set, no templating is used.
Templating *Driver `json:",omitempty"`
}
// ConfigReferenceFileTarget is a file target in a config reference

View File

@ -14,6 +14,10 @@ type SecretSpec struct {
Annotations
Data []byte `json:",omitempty"`
Driver *Driver `json:",omitempty"` // name of the secrets driver used to fetch the secret's value from an external secret store
// Templating controls whether and how to evaluate the secret payload as
// a template. If it is not set, no templating is used.
Templating *Driver `json:",omitempty"`
}
// SecretReferenceFileTarget is a file target in a secret reference

View File

@ -42,8 +42,8 @@ For example, to list running containers (the equivalent of "docker ps"):
package client // import "github.com/docker/docker/client"
import (
"errors"
"fmt"
"net"
"net/http"
"net/url"
"os"
@ -56,6 +56,7 @@ import (
"github.com/docker/docker/api/types/versions"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
@ -103,18 +104,21 @@ func CheckRedirect(req *http.Request, via []*http.Request) error {
}
// NewEnvClient initializes a new API client based on environment variables.
// Use DOCKER_HOST to set the url to the docker server.
// Use DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
// Use DOCKER_CERT_PATH to load the TLS certificates from.
// Use DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default.
// deprecated: use NewClientWithOpts(FromEnv)
// See FromEnv for a list of support environment variables.
//
// Deprecated: use NewClientWithOpts(FromEnv)
func NewEnvClient() (*Client, error) {
return NewClientWithOpts(FromEnv)
}
// FromEnv enhance the default client with values from environment variables
// FromEnv configures the client with values from environment variables.
//
// Supported environment variables:
// DOCKER_HOST to set the url to the docker server.
// DOCKER_API_VERSION to set the version of the API to reach, leave empty for latest.
// DOCKER_CERT_PATH to load the TLS certificates from.
// DOCKER_TLS_VERIFY to enable or disable TLS verification, off by default.
func FromEnv(c *Client) error {
var httpClient *http.Client
if dockerCertPath := os.Getenv("DOCKER_CERT_PATH"); dockerCertPath != "" {
options := tlsconfig.Options{
CAFile: filepath.Join(dockerCertPath, "ca.pem"),
@ -127,30 +131,58 @@ func FromEnv(c *Client) error {
return err
}
httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsc,
},
c.client = &http.Client{
Transport: &http.Transport{TLSClientConfig: tlsc},
CheckRedirect: CheckRedirect,
}
WithHTTPClient(httpClient)(c)
}
host := os.Getenv("DOCKER_HOST")
if host != "" {
// WithHost will create an API client if it doesn't exist
if host := os.Getenv("DOCKER_HOST"); host != "" {
if err := WithHost(host)(c); err != nil {
return err
}
}
version := os.Getenv("DOCKER_API_VERSION")
if version != "" {
if version := os.Getenv("DOCKER_API_VERSION"); version != "" {
c.version = version
c.manualOverride = true
}
return nil
}
// WithTLSClientConfig applies a tls config to the client transport.
func WithTLSClientConfig(cacertPath, certPath, keyPath string) func(*Client) error {
return func(c *Client) error {
opts := tlsconfig.Options{
CAFile: cacertPath,
CertFile: certPath,
KeyFile: keyPath,
ExclusiveRootPools: true,
}
config, err := tlsconfig.Client(opts)
if err != nil {
return errors.Wrap(err, "failed to create tls config")
}
if transport, ok := c.client.Transport.(*http.Transport); ok {
transport.TLSClientConfig = config
return nil
}
return errors.Errorf("cannot apply tls config to transport: %T", c.client.Transport)
}
}
// WithDialer applies the dialer.DialContext to the client transport. This can be
// used to set the Timeout and KeepAlive settings of the client.
func WithDialer(dialer *net.Dialer) func(*Client) error {
return func(c *Client) error {
if transport, ok := c.client.Transport.(*http.Transport); ok {
transport.DialContext = dialer.DialContext
return nil
}
return errors.Errorf("cannot apply dialer to transport: %T", c.client.Transport)
}
}
// WithVersion overrides the client version with the specified one
func WithVersion(version string) func(*Client) error {
return func(c *Client) error {
@ -159,8 +191,7 @@ func WithVersion(version string) func(*Client) error {
}
}
// WithHost overrides the client host with the specified one, creating a new
// http client if one doesn't exist
// WithHost overrides the client host with the specified one.
func WithHost(host string) func(*Client) error {
return func(c *Client) error {
hostURL, err := ParseHostURL(host)
@ -171,17 +202,10 @@ func WithHost(host string) func(*Client) error {
c.proto = hostURL.Scheme
c.addr = hostURL.Host
c.basePath = hostURL.Path
if c.client == nil {
client, err := defaultHTTPClient(host)
if err != nil {
return err
}
return WithHTTPClient(client)(c)
}
if transport, ok := c.client.Transport.(*http.Transport); ok {
return sockets.ConfigureTransport(transport, c.proto, c.addr)
}
return fmt.Errorf("cannot apply host to http transport")
return errors.Errorf("cannot apply host to transport: %T", c.client.Transport)
}
}
@ -266,7 +290,7 @@ func defaultHTTPClient(host string) (*http.Client, error) {
// It won't send any version information if the version number is empty. It is
// highly recommended that you set a version or your client may break if the
// server is upgraded.
// deprecated: use NewClientWithOpts
// Deprecated: use NewClientWithOpts
func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string) (*Client, error) {
return NewClientWithOpts(WithHost(host), WithVersion(version), WithHTTPClient(client), WithHTTPHeaders(httpHeaders))
}
@ -332,17 +356,6 @@ func (cli *Client) DaemonHost() string {
return cli.host
}
// ParseHost parses a url string, validates the strings is a host url, and returns
// the parsed host as: protocol, address, and base path
// Deprecated: use ParseHostURL
func ParseHost(host string) (string, string, string, error) {
hostURL, err := ParseHostURL(host)
if err != nil {
return "", "", "", err
}
return hostURL.Scheme, hostURL.Host, hostURL.Path, nil
}
// ParseHostURL parses a url string, validates the string is a host url, and
// returns the parsed URL
func ParseHostURL(host string) (*url.URL, error) {
@ -378,6 +391,7 @@ func (cli *Client) CustomHTTPHeaders() map[string]string {
}
// SetCustomHTTPHeaders that will be set on every HTTP request made by the client.
// Deprecated: use WithHTTPHeaders when creating the client.
func (cli *Client) SetCustomHTTPHeaders(headers map[string]string) {
cli.customHTTPHeaders = headers
}

View File

@ -6,18 +6,19 @@ import (
"net/url"
"os"
"runtime"
"strings"
"testing"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/internal/testutil"
"github.com/gotestyourself/gotestyourself/env"
"github.com/gotestyourself/gotestyourself/skip"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewEnvClient(t *testing.T) {
skip.IfCondition(t, runtime.GOOS == "windows")
skip.If(t, runtime.GOOS == "windows")
testcases := []struct {
doc string
@ -83,10 +84,9 @@ func TestNewEnvClient(t *testing.T) {
},
}
env := envToMap()
defer mapToEnv(env)
defer env.PatchAll(t, nil)()
for _, c := range testcases {
mapToEnv(c.envs)
env.PatchAll(t, c.envs)
apiclient, err := NewEnvClient()
if c.expectedError != "" {
assert.Error(t, err, c.doc)
@ -132,32 +132,6 @@ func TestGetAPIPath(t *testing.T) {
}
}
func TestParseHost(t *testing.T) {
cases := []struct {
host string
proto string
addr string
base string
err bool
}{
{"", "", "", "", true},
{"foobar", "", "", "", true},
{"foo://bar", "foo", "bar", "", false},
{"tcp://localhost:2476", "tcp", "localhost:2476", "", false},
{"tcp://localhost:2476/path", "tcp", "localhost:2476", "/path", false},
}
for _, cs := range cases {
p, a, b, e := ParseHost(cs.host)
if cs.err {
assert.Error(t, e)
}
assert.Equal(t, cs.proto, p)
assert.Equal(t, cs.addr, a)
assert.Equal(t, cs.base, b)
}
}
func TestParseHostURL(t *testing.T) {
testcases := []struct {
host string
@ -196,16 +170,12 @@ func TestParseHostURL(t *testing.T) {
}
func TestNewEnvClientSetsDefaultVersion(t *testing.T) {
env := envToMap()
defer mapToEnv(env)
envMap := map[string]string{
defer env.PatchAll(t, map[string]string{
"DOCKER_HOST": "",
"DOCKER_API_VERSION": "",
"DOCKER_TLS_VERIFY": "",
"DOCKER_CERT_PATH": "",
}
mapToEnv(envMap)
})()
client, err := NewEnvClient()
if err != nil {
@ -225,18 +195,10 @@ func TestNewEnvClientSetsDefaultVersion(t *testing.T) {
// TestNegotiateAPIVersionEmpty asserts that client.Client can
// negotiate a compatible APIVersion when omitted
func TestNegotiateAPIVersionEmpty(t *testing.T) {
env := envToMap()
defer mapToEnv(env)
envMap := map[string]string{
"DOCKER_API_VERSION": "",
}
mapToEnv(envMap)
defer env.PatchAll(t, map[string]string{"DOCKER_API_VERSION": ""})
client, err := NewEnvClient()
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
ping := types.Ping{
APIVersion: "",
@ -260,12 +222,9 @@ func TestNegotiateAPIVersionEmpty(t *testing.T) {
// negotiate a compatible APIVersion with the server
func TestNegotiateAPIVersion(t *testing.T) {
client, err := NewEnvClient()
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
expected := "1.21"
ping := types.Ping{
APIVersion: expected,
OSType: "linux",
@ -291,18 +250,11 @@ func TestNegotiateAPIVersion(t *testing.T) {
// TestNegotiateAPIVersionOverride asserts that we honor
// the environment variable DOCKER_API_VERSION when negotianing versions
func TestNegotiateAPVersionOverride(t *testing.T) {
env := envToMap()
defer mapToEnv(env)
envMap := map[string]string{
"DOCKER_API_VERSION": "9.99",
}
mapToEnv(envMap)
expected := "9.99"
defer env.PatchAll(t, map[string]string{"DOCKER_API_VERSION": expected})()
client, err := NewEnvClient()
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
ping := types.Ping{
APIVersion: "1.24",
@ -310,31 +262,11 @@ func TestNegotiateAPVersionOverride(t *testing.T) {
Experimental: false,
}
expected := envMap["DOCKER_API_VERSION"]
// test that we honored the env var
client.NegotiateAPIVersionPing(ping)
assert.Equal(t, expected, client.version)
}
// mapToEnv takes a map of environment variables and sets them
func mapToEnv(env map[string]string) {
for k, v := range env {
os.Setenv(k, v)
}
}
// envToMap returns a map of environment variables
func envToMap() map[string]string {
env := make(map[string]string)
for _, e := range os.Environ() {
kv := strings.SplitAfterN(e, "=", 2)
env[kv[0]] = kv[1]
}
return env
}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (rtf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {

View File

@ -37,6 +37,7 @@ type CommonAPIClient interface {
NegotiateAPIVersion(ctx context.Context)
NegotiateAPIVersionPing(types.Ping)
DialSession(ctx context.Context, proto string, meta map[string][]string) (net.Conn, error)
Close() error
}
// ContainerAPIClient defines API client methods for the containers

View File

@ -123,10 +123,7 @@ func (cli *Client) sendRequest(ctx context.Context, method, path string, query u
if err != nil {
return resp, err
}
if err := cli.checkResponseErr(resp); err != nil {
return resp, err
}
return resp, nil
return resp, cli.checkResponseErr(resp)
}
func (cli *Client) doRequest(ctx context.Context, req *http.Request) (serverResponse, error) {

View File

@ -9,6 +9,7 @@ import (
"testing"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
@ -44,10 +45,8 @@ func TestSetHostHeader(t *testing.T) {
}
for c, test := range testCases {
proto, addr, basePath, err := ParseHost(test.host)
if err != nil {
t.Fatal(err)
}
hostURL, err := ParseHostURL(test.host)
require.NoError(t, err)
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
@ -66,15 +65,13 @@ func TestSetHostHeader(t *testing.T) {
}, nil
}),
proto: proto,
addr: addr,
basePath: basePath,
proto: hostURL.Scheme,
addr: hostURL.Host,
basePath: hostURL.Path,
}
_, err = client.sendRequest(context.Background(), "GET", testURL, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
require.NoError(t, err)
}
}

View File

@ -1049,21 +1049,6 @@ func getSecretTargetPath(r *swarmtypes.SecretReference) string {
return filepath.Join(containerSecretMountPath, r.File.Name)
}
// ConfigsDirPath returns the path to the directory where configs are stored on
// disk.
func (container *Container) ConfigsDirPath() (string, error) {
return container.GetRootResourcePath("configs")
}
// ConfigFilePath returns the path to the on-disk location of a config.
func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) (string, error) {
configs, err := container.ConfigsDirPath()
if err != nil {
return "", err
}
return filepath.Join(configs, configRef.ConfigID), nil
}
// CreateDaemonEnvironment creates a new environment variable slice for this container.
func (container *Container) CreateDaemonEnvironment(tty bool, linkedEnv []string) []string {
// Setup environment

View File

@ -5,11 +5,13 @@ package container // import "github.com/docker/docker/container"
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/containerd/continuity/fs"
"github.com/docker/docker/api/types"
containertypes "github.com/docker/docker/api/types/container"
mounttypes "github.com/docker/docker/api/types/mount"
swarmtypes "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/mount"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/volume"
@ -233,6 +235,17 @@ func (container *Container) SecretMounts() ([]Mount, error) {
Writable: false,
})
}
for _, r := range container.ConfigReferences {
fPath, err := container.ConfigFilePath(*r)
if err != nil {
return nil, err
}
mounts = append(mounts, Mount{
Source: fPath,
Destination: r.File.Name,
Writable: false,
})
}
return mounts, nil
}
@ -253,27 +266,6 @@ func (container *Container) UnmountSecrets() error {
return mount.RecursiveUnmount(p)
}
// ConfigMounts returns the mounts for configs.
func (container *Container) ConfigMounts() ([]Mount, error) {
var mounts []Mount
for _, configRef := range container.ConfigReferences {
if configRef.File == nil {
continue
}
src, err := container.ConfigFilePath(*configRef)
if err != nil {
return nil, err
}
mounts = append(mounts, Mount{
Source: src,
Destination: configRef.File.Name,
Writable: false,
})
}
return mounts, nil
}
type conflictingUpdateOptions string
func (e conflictingUpdateOptions) Error() string {
@ -457,3 +449,13 @@ func (container *Container) GetMountPoints() []types.MountPoint {
}
return mountPoints
}
// ConfigFilePath returns the path to the on-disk location of a config.
// On unix, configs are always considered secret
func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) (string, error) {
mounts, err := container.SecretMountPath()
if err != nil {
return "", err
}
return filepath.Join(mounts, configRef.ConfigID), nil
}

View File

@ -7,6 +7,7 @@ import (
"github.com/docker/docker/api/types"
containertypes "github.com/docker/docker/api/types/container"
swarmtypes "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/system"
)
@ -102,23 +103,20 @@ func (container *Container) CreateConfigSymlinks() error {
}
// ConfigMounts returns the mount for configs.
// All configs are stored in a single mount on Windows. Target symlinks are
// created for each config, pointing to the files in this mount.
func (container *Container) ConfigMounts() ([]Mount, error) {
// TODO: Right now Windows doesn't really have a "secure" storage for secrets,
// however some configs may contain secrets. Once secure storage is worked out,
// configs and secret handling should be merged.
func (container *Container) ConfigMounts() []Mount {
var mounts []Mount
if len(container.ConfigReferences) > 0 {
src, err := container.ConfigsDirPath()
if err != nil {
return nil, err
}
mounts = append(mounts, Mount{
Source: src,
Source: container.ConfigsDirPath(),
Destination: containerInternalConfigsDirPath,
Writable: false,
})
}
return mounts, nil
return mounts
}
// DetachAndUnmount unmounts all volumes.
@ -204,3 +202,12 @@ func (container *Container) GetMountPoints() []types.MountPoint {
}
return mountPoints
}
func (container *Container) ConfigsDirPath() string {
return filepath.Join(container.Root, "configs")
}
// ConfigFilePath returns the path to the on-disk location of a config.
func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) string {
return filepath.Join(container.ConfigsDirPath(), configRef.ConfigID)
}

View File

@ -2,6 +2,7 @@ package convert // import "github.com/docker/docker/daemon/cluster/convert"
import (
swarmtypes "github.com/docker/docker/api/types/swarm"
types "github.com/docker/docker/api/types/swarm"
swarmapi "github.com/docker/swarmkit/api"
gogotypes "github.com/gogo/protobuf/types"
)
@ -21,18 +22,34 @@ func ConfigFromGRPC(s *swarmapi.Config) swarmtypes.Config {
config.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt)
config.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt)
if s.Spec.Templating != nil {
config.Spec.Templating = &types.Driver{
Name: s.Spec.Templating.Name,
Options: s.Spec.Templating.Options,
}
}
return config
}
// ConfigSpecToGRPC converts Config to a grpc Config.
func ConfigSpecToGRPC(s swarmtypes.ConfigSpec) swarmapi.ConfigSpec {
return swarmapi.ConfigSpec{
spec := swarmapi.ConfigSpec{
Annotations: swarmapi.Annotations{
Name: s.Name,
Labels: s.Labels,
},
Data: s.Data,
}
if s.Templating != nil {
spec.Templating = &swarmapi.Driver{
Name: s.Templating.Name,
Options: s.Templating.Options,
}
}
return spec
}
// ConfigReferencesFromGRPC converts a slice of grpc ConfigReference to ConfigReference

View File

@ -2,6 +2,7 @@ package convert // import "github.com/docker/docker/daemon/cluster/convert"
import (
swarmtypes "github.com/docker/docker/api/types/swarm"
types "github.com/docker/docker/api/types/swarm"
swarmapi "github.com/docker/swarmkit/api"
gogotypes "github.com/gogo/protobuf/types"
)
@ -22,12 +23,19 @@ func SecretFromGRPC(s *swarmapi.Secret) swarmtypes.Secret {
secret.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt)
secret.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt)
if s.Spec.Templating != nil {
secret.Spec.Templating = &types.Driver{
Name: s.Spec.Templating.Name,
Options: s.Spec.Templating.Options,
}
}
return secret
}
// SecretSpecToGRPC converts Secret to a grpc Secret.
func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec {
return swarmapi.SecretSpec{
spec := swarmapi.SecretSpec{
Annotations: swarmapi.Annotations{
Name: s.Name,
Labels: s.Labels,
@ -35,6 +43,15 @@ func SecretSpecToGRPC(s swarmtypes.SecretSpec) swarmapi.SecretSpec {
Data: s.Data,
Driver: driverToGRPC(s.Driver),
}
if s.Templating != nil {
spec.Templating = &swarmapi.Driver{
Name: s.Templating.Name,
Options: s.Templating.Options,
}
}
return spec
}
// SecretReferencesFromGRPC converts a slice of grpc SecretReference to SecretReference

View File

@ -19,6 +19,7 @@ import (
"github.com/docker/swarmkit/agent/exec"
"github.com/docker/swarmkit/api"
"github.com/docker/swarmkit/api/naming"
"github.com/docker/swarmkit/template"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
@ -191,7 +192,7 @@ func (e *executor) Configure(ctx context.Context, node *api.Node) error {
// Controller returns a docker container runner.
func (e *executor) Controller(t *api.Task) (exec.Controller, error) {
dependencyGetter := agent.Restrict(e.dependencies, t)
dependencyGetter := template.NewTemplatedDependencyGetter(agent.Restrict(e.dependencies, t), t, nil)
// Get the node description from the executor field
e.mutex.Lock()

View File

@ -16,8 +16,6 @@ func (daemon *Daemon) SetContainerConfigReferences(name string, refs []*swarmtyp
if err != nil {
return err
}
c.ConfigReferences = refs
c.ConfigReferences = append(c.ConfigReferences, refs...)
return nil
}

View File

@ -161,43 +161,26 @@ func (daemon *Daemon) setupIpcDirs(c *container.Container) error {
}
func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
if len(c.SecretReferences) == 0 {
if len(c.SecretReferences) == 0 && len(c.ConfigReferences) == 0 {
return nil
}
localMountPath, err := c.SecretMountPath()
if err != nil {
return errors.Wrap(err, "error getting secrets mount dir")
if err := daemon.createSecretsDir(c); err != nil {
return err
}
logrus.Debugf("secrets: setting up secret dir: %s", localMountPath)
// retrieve possible remapped range start for root UID, GID
rootIDs := daemon.idMappings.RootPair()
// create tmpfs
if err := idtools.MkdirAllAndChown(localMountPath, 0700, rootIDs); err != nil {
return errors.Wrap(err, "error creating secret local mount path")
}
defer func() {
if setupErr != nil {
// cleanup
_ = detachMounted(localMountPath)
if err := os.RemoveAll(localMountPath); err != nil {
logrus.Errorf("error cleaning up secret mount: %s", err)
}
daemon.cleanupSecretDir(c)
}
}()
tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootIDs.UID, rootIDs.GID)
if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "nodev,nosuid,noexec,"+tmpfsOwnership); err != nil {
return errors.Wrap(err, "unable to setup secret mount")
}
if c.DependencyStore == nil {
return fmt.Errorf("secret store is not initialized")
}
// retrieve possible remapped range start for root UID, GID
rootIDs := daemon.idMappings.RootPair()
for _, s := range c.SecretReferences {
// TODO (ehazlett): use type switch when more are supported
if s.File == nil {
@ -244,78 +227,38 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) {
}
}
label.Relabel(localMountPath, c.MountLabel, false)
// remount secrets ro
if err := mount.Mount("tmpfs", localMountPath, "tmpfs", "remount,ro,"+tmpfsOwnership); err != nil {
return errors.Wrap(err, "unable to remount secret dir as readonly")
}
return nil
}
func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
if len(c.ConfigReferences) == 0 {
return nil
}
localPath, err := c.ConfigsDirPath()
if err != nil {
return err
}
logrus.Debugf("configs: setting up config dir: %s", localPath)
// retrieve possible remapped range start for root UID, GID
rootIDs := daemon.idMappings.RootPair()
// create tmpfs
if err := idtools.MkdirAllAndChown(localPath, 0700, rootIDs); err != nil {
return errors.Wrap(err, "error creating config dir")
}
defer func() {
if setupErr != nil {
if err := os.RemoveAll(localPath); err != nil {
logrus.Errorf("error cleaning up config dir: %s", err)
}
}
}()
if c.DependencyStore == nil {
return fmt.Errorf("config store is not initialized")
}
for _, configRef := range c.ConfigReferences {
for _, ref := range c.ConfigReferences {
// TODO (ehazlett): use type switch when more are supported
if configRef.File == nil {
if ref.File == nil {
logrus.Error("config target type is not a file target")
continue
}
fPath, err := c.ConfigFilePath(*configRef)
fPath, err := c.ConfigFilePath(*ref)
if err != nil {
return err
return errors.Wrap(err, "error getting config file path for container")
}
log := logrus.WithFields(logrus.Fields{"name": configRef.File.Name, "path": fPath})
if err := idtools.MkdirAllAndChown(filepath.Dir(fPath), 0700, rootIDs); err != nil {
return errors.Wrap(err, "error creating config path")
return errors.Wrap(err, "error creating config mount path")
}
log.Debug("injecting config")
config, err := c.DependencyStore.Configs().Get(configRef.ConfigID)
logrus.WithFields(logrus.Fields{
"name": ref.File.Name,
"path": fPath,
}).Debug("injecting config")
config, err := c.DependencyStore.Configs().Get(ref.ConfigID)
if err != nil {
return errors.Wrap(err, "unable to get config from config store")
}
if err := ioutil.WriteFile(fPath, config.Spec.Data, configRef.File.Mode); err != nil {
if err := ioutil.WriteFile(fPath, config.Spec.Data, ref.File.Mode); err != nil {
return errors.Wrap(err, "error injecting config")
}
uid, err := strconv.Atoi(configRef.File.UID)
uid, err := strconv.Atoi(ref.File.UID)
if err != nil {
return err
}
gid, err := strconv.Atoi(configRef.File.GID)
gid, err := strconv.Atoi(ref.File.GID)
if err != nil {
return err
}
@ -323,16 +266,69 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
if err := os.Chown(fPath, rootIDs.UID+uid, rootIDs.GID+gid); err != nil {
return errors.Wrap(err, "error setting ownership for config")
}
if err := os.Chmod(fPath, configRef.File.Mode); err != nil {
if err := os.Chmod(fPath, ref.File.Mode); err != nil {
return errors.Wrap(err, "error setting file mode for config")
}
}
label.Relabel(fPath, c.MountLabel, false)
return daemon.remountSecretDir(c)
}
// createSecretsDir is used to create a dir suitable for storing container secrets.
// In practice this is using a tmpfs mount and is used for both "configs" and "secrets"
func (daemon *Daemon) createSecretsDir(c *container.Container) error {
// retrieve possible remapped range start for root UID, GID
rootIDs := daemon.idMappings.RootPair()
dir, err := c.SecretMountPath()
if err != nil {
return errors.Wrap(err, "error getting container secrets dir")
}
// create tmpfs
if err := idtools.MkdirAllAndChown(dir, 0700, rootIDs); err != nil {
return errors.Wrap(err, "error creating secret local mount path")
}
tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootIDs.UID, rootIDs.GID)
if err := mount.Mount("tmpfs", dir, "tmpfs", "nodev,nosuid,noexec,"+tmpfsOwnership); err != nil {
return errors.Wrap(err, "unable to setup secret mount")
}
return nil
}
func (daemon *Daemon) remountSecretDir(c *container.Container) error {
dir, err := c.SecretMountPath()
if err != nil {
return errors.Wrap(err, "error getting container secrets path")
}
if err := label.Relabel(dir, c.MountLabel, false); err != nil {
logrus.WithError(err).WithField("dir", dir).Warn("Error while attempting to set selinux label")
}
rootIDs := daemon.idMappings.RootPair()
tmpfsOwnership := fmt.Sprintf("uid=%d,gid=%d", rootIDs.UID, rootIDs.GID)
// remount secrets ro
if err := mount.Mount("tmpfs", dir, "tmpfs", "remount,ro,"+tmpfsOwnership); err != nil {
return errors.Wrap(err, "unable to remount dir as readonly")
}
return nil
}
func (daemon *Daemon) cleanupSecretDir(c *container.Container) {
dir, err := c.SecretMountPath()
if err != nil {
logrus.WithError(err).WithField("container", c.ID).Warn("error getting secrets mount path for container")
}
if err := mount.RecursiveUnmount(dir); err != nil {
logrus.WithField("dir", dir).WithError(err).Warn("Error while attmepting to unmount dir, this may prevent removal of container.")
}
if err := os.RemoveAll(dir); err != nil && !os.IsNotExist(err) {
logrus.WithField("dir", dir).WithError(err).Error("Error removing dir.")
}
}
func killProcessDirectly(cntr *container.Container) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

View File

@ -21,10 +21,7 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
return nil
}
localPath, err := c.ConfigsDirPath()
if err != nil {
return err
}
localPath := c.ConfigsDirPath()
logrus.Debugf("configs: setting up config dir: %s", localPath)
// create local config root
@ -51,11 +48,7 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) {
continue
}
fPath, err := c.ConfigFilePath(*configRef)
if err != nil {
return err
}
fPath := c.ConfigFilePath(*configRef)
log := logrus.WithFields(logrus.Fields{"name": configRef.File.Name, "path": fPath})
log.Debug("injecting config")

View File

@ -23,7 +23,7 @@ func newPluginDriver(name string, pl plugingetter.CompatPlugin, config Options)
home := config.Root
if !pl.IsV1() {
if p, ok := pl.(*v2.Plugin); ok {
if p.PropagatedMount != "" {
if p.PluginObj.Config.PropagatedMount != "" {
home = p.PluginObj.Config.PropagatedMount
}
}

View File

@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"io"
"path/filepath"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/containerfs"
@ -143,7 +142,7 @@ func (d *graphDriverProxy) Get(id, mountLabel string) (containerfs.ContainerFS,
if ret.Err != "" {
err = errors.New(ret.Err)
}
return containerfs.NewLocalContainerFS(filepath.Join(d.p.BasePath(), ret.Dir)), err
return containerfs.NewLocalContainerFS(d.p.ScopedPath(ret.Dir)), err
}
func (d *graphDriverProxy) Put(id string) error {

View File

@ -3,7 +3,7 @@ package logger // import "github.com/docker/docker/daemon/logger"
import (
"io"
"os"
"strings"
"path/filepath"
"sync"
"time"
@ -19,7 +19,6 @@ type pluginAdapter struct {
driverName string
id string
plugin logPlugin
basePath string
fifoPath string
capabilities Capability
logInfo Info
@ -58,7 +57,7 @@ func (a *pluginAdapter) Close() error {
a.mu.Lock()
defer a.mu.Unlock()
if err := a.plugin.StopLogging(strings.TrimPrefix(a.fifoPath, a.basePath)); err != nil {
if err := a.plugin.StopLogging(filepath.Join("/", "run", "docker", "logging", a.id)); err != nil {
return err
}

View File

@ -5,7 +5,6 @@ import (
"io"
"os"
"path/filepath"
"strings"
"github.com/docker/docker/api/types/plugins/logdriver"
getter "github.com/docker/docker/pkg/plugingetter"
@ -39,18 +38,20 @@ func getPlugin(name string, mode int) (Creator, error) {
}
d := &logPluginProxy{p.Client()}
return makePluginCreator(name, d, p.BasePath()), nil
return makePluginCreator(name, d, p.ScopedPath), nil
}
func makePluginCreator(name string, l *logPluginProxy, basePath string) Creator {
func makePluginCreator(name string, l *logPluginProxy, scopePath func(s string) string) Creator {
return func(logCtx Info) (logger Logger, err error) {
defer func() {
if err != nil {
pluginGetter.Get(name, extName, getter.Release)
}
}()
root := filepath.Join(basePath, "run", "docker", "logging")
if err := os.MkdirAll(root, 0700); err != nil {
unscopedPath := filepath.Join("/", "run", "docker", "logging")
logRoot := scopePath(unscopedPath)
if err := os.MkdirAll(logRoot, 0700); err != nil {
return nil, err
}
@ -59,8 +60,7 @@ func makePluginCreator(name string, l *logPluginProxy, basePath string) Creator
driverName: name,
id: id,
plugin: l,
basePath: basePath,
fifoPath: filepath.Join(root, id),
fifoPath: filepath.Join(logRoot, id),
logInfo: logCtx,
}
@ -77,7 +77,7 @@ func makePluginCreator(name string, l *logPluginProxy, basePath string) Creator
a.stream = stream
a.enc = logdriver.NewLogEntryEncoder(a.stream)
if err := l.StartLogging(strings.TrimPrefix(a.fifoPath, basePath), logCtx); err != nil {
if err := l.StartLogging(filepath.Join(unscopedPath, id), logCtx); err != nil {
return nil, errors.Wrapf(err, "error creating logger")
}

View File

@ -1,10 +1,8 @@
package daemon // import "github.com/docker/docker/daemon"
import (
"path/filepath"
"sync"
"github.com/docker/docker/pkg/mount"
"github.com/docker/docker/pkg/plugingetter"
metrics "github.com/docker/go-metrics"
"github.com/pkg/errors"
@ -132,18 +130,6 @@ func (d *Daemon) cleanupMetricsPlugins() {
}
}
type metricsPlugin struct {
plugingetter.CompatPlugin
}
func (p metricsPlugin) sock() string {
return "metrics.sock"
}
func (p metricsPlugin) sockBase() string {
return filepath.Join(p.BasePath(), "run", "docker")
}
func pluginStartMetricsCollection(p plugingetter.CompatPlugin) error {
type metricsPluginResponse struct {
Err string
@ -162,12 +148,4 @@ func pluginStopMetricsCollection(p plugingetter.CompatPlugin) {
if err := p.Client().Call(metricsPluginType+".StopMetrics", nil, nil); err != nil {
logrus.WithError(err).WithField("name", p.Name()).Error("error stopping metrics collector")
}
mp := metricsPlugin{p}
sockPath := filepath.Join(mp.sockBase(), mp.sock())
if err := mount.Unmount(sockPath); err != nil {
if mounted, _ := mount.Mounted(sockPath); mounted {
logrus.WithError(err).WithField("name", p.Name()).WithField("socket", sockPath).Error("error unmounting metrics socket for plugin")
}
}
}

View File

@ -5,13 +5,13 @@ package daemon // import "github.com/docker/docker/daemon"
import (
"net"
"net/http"
"os"
"path/filepath"
"github.com/docker/docker/pkg/mount"
"github.com/docker/docker/pkg/plugingetter"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/plugin"
metrics "github.com/docker/go-metrics"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
@ -34,52 +34,22 @@ func (daemon *Daemon) listenMetricsSock() (string, error) {
return path, nil
}
func registerMetricsPluginCallback(getter plugingetter.PluginGetter, sockPath string) {
getter.Handle(metricsPluginType, func(name string, client *plugins.Client) {
func registerMetricsPluginCallback(store *plugin.Store, sockPath string) {
store.RegisterRuntimeOpt(metricsPluginType, func(s *specs.Spec) {
f := plugin.WithSpecMounts([]specs.Mount{
{Type: "bind", Source: sockPath, Destination: "/run/docker/metrics.sock", Options: []string{"bind", "ro"}},
})
f(s)
})
store.Handle(metricsPluginType, func(name string, client *plugins.Client) {
// Use lookup since nothing in the system can really reference it, no need
// to protect against removal
p, err := getter.Get(name, metricsPluginType, plugingetter.Lookup)
p, err := store.Get(name, metricsPluginType, plugingetter.Lookup)
if err != nil {
return
}
mp := metricsPlugin{p}
sockBase := mp.sockBase()
if err := os.MkdirAll(sockBase, 0755); err != nil {
logrus.WithError(err).WithField("name", name).WithField("path", sockBase).Error("error creating metrics plugin base path")
return
}
defer func() {
if err != nil {
os.RemoveAll(sockBase)
}
}()
pluginSockPath := filepath.Join(sockBase, mp.sock())
_, err = os.Stat(pluginSockPath)
if err == nil {
mount.Unmount(pluginSockPath)
} else {
logrus.WithField("path", pluginSockPath).Debugf("creating plugin socket")
f, err := os.OpenFile(pluginSockPath, os.O_CREATE, 0600)
if err != nil {
return
}
f.Close()
}
if err := mount.Mount(sockPath, pluginSockPath, "none", "bind,ro"); err != nil {
logrus.WithError(err).WithField("name", name).Error("could not mount metrics socket to plugin")
return
}
if err := pluginStartMetricsCollection(p); err != nil {
if err := mount.Unmount(pluginSockPath); err != nil {
if mounted, _ := mount.Mounted(pluginSockPath); mounted {
logrus.WithError(err).WithField("sock_path", pluginSockPath).Error("error unmounting metrics socket from plugin during cleanup")
}
}
logrus.WithError(err).WithField("name", name).Error("error while initializing metrics plugin")
}
})

View File

@ -755,7 +755,7 @@ func (daemon *Daemon) populateCommonSpec(s *specs.Spec, c *container.Container)
return nil
}
func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
func (daemon *Daemon) createSpec(c *container.Container) (retSpec *specs.Spec, err error) {
s := oci.DefaultSpec()
if err := daemon.populateCommonSpec(&s, c); err != nil {
return nil, err
@ -837,11 +837,13 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
return nil, err
}
if err := daemon.setupSecretDir(c); err != nil {
return nil, err
}
defer func() {
if err != nil {
daemon.cleanupSecretDir(c)
}
}()
if err := daemon.setupConfigDir(c); err != nil {
if err := daemon.setupSecretDir(c); err != nil {
return nil, err
}
@ -866,12 +868,6 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
}
ms = append(ms, secretMounts...)
configMounts, err := c.ConfigMounts()
if err != nil {
return nil, err
}
ms = append(ms, configMounts...)
sort.Sort(mounts(ms))
if err := setMounts(daemon, &s, c, ms); err != nil {
return nil, fmt.Errorf("linux mounts: %v", err)

View File

@ -102,10 +102,7 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
mounts = append(mounts, secretMounts...)
}
configMounts, err := c.ConfigMounts()
if err != nil {
return nil, err
}
configMounts := c.ConfigMounts()
if configMounts != nil {
mounts = append(mounts, configMounts...)
}

View File

@ -131,7 +131,7 @@ can take over 15 minutes to complete.
Successfully built 3d872560918e
Successfully tagged docker-dev:dry-run-test
docker run --rm -i --privileged -e BUILDFLAGS -e KEEPBUNDLE -e DOCKER_BUILD_GOGC -e DOCKER_BUILD_PKGS -e DOCKER_CLIENTONLY -e DOCKER_DEBUG -e DOCKER_EXPERIMENTAL -e DOCKER_GITCOMMIT -e DOCKER_GRAPHDRIVER=devicemapper -e DOCKER_INCREMENTAL_BINARY -e DOCKER_REMAP_ROOT -e DOCKER_STORAGE_OPTS -e DOCKER_USERLANDPROXY -e TESTDIRS -e TESTFLAGS -e TIMEOUT -v "home/ubuntu/repos/docker/bundles:/go/src/github.com/docker/docker/bundles" -t "docker-dev:dry-run-test" bash
root@f31fa223770f:/go/src/github.com/docker/docker#
#
```
At this point, your prompt reflects the container's BASH shell.
@ -146,7 +146,7 @@ can take over 15 minutes to complete.
6. Make a `dockerd` binary.
```none
root@a8b2885ab900:/go/src/github.com/docker/docker# hack/make.sh binary
# hack/make.sh binary
Removing bundles/
---> Making bundle: binary (in bundles/binary)
@ -160,13 +160,13 @@ can take over 15 minutes to complete.
`/usr/local/bin/` directory.
```none
root@a8b2885ab900:/go/src/github.com/docker/docker# make install
# make install
```
8. Start the Engine daemon running in the background.
```none
root@a8b2885ab900:/go/src/github.com/docker/docker# dockerd -D &
# dockerd -D &
...output snipped...
DEBU[0001] Registering POST, /networks/{id:.*}/connect
DEBU[0001] Registering POST, /networks/{id:.*}/disconnect
@ -252,13 +252,13 @@ can take over 15 minutes to complete.
10. Run the `hello-world` image.
```none
root@5f8630b873fe:/go/src/github.com/docker/docker# docker run hello-world
# docker run hello-world
```
11. List the image you just downloaded.
```none
root@5f8630b873fe:/go/src/github.com/docker/docker# docker images
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest c54a2cc56cbb 3 months ago 1.85 kB
```
@ -347,7 +347,7 @@ example, you'll edit the help for the `attach` subcommand.
10. To view your change, run the `dockerd --help` command in the docker development container shell.
```bash
root@b0cb4f22715d:/go/src/github.com/docker/docker# dockerd --help
# dockerd --help
Usage: dockerd COMMAND

View File

@ -29,7 +29,10 @@ Depending on your contribution, you may need to add _integration tests_. These
are tests that combine two or more work units into one component. These work
units each have unit tests and then, together, integration tests that test the
interface between the components. The `integration` and `integration-cli`
directories in the Docker repository contain integration test code.
directories in the Docker repository contain integration test code. Note that
`integration-cli` tests are now deprecated in the Moby project, and new tests
cannot be added to this suite - add `integration` tests instead using the API
client.
Testing is its own specialty. If you aren't familiar with testing techniques,
there is a lot of information available to you on the Web. For now, you should
@ -93,7 +96,8 @@ hour. To run the test suite, do the following:
## Run targets inside a development container
If you are working inside a development container, you use the
`hack/make.sh` script to run tests. The `hack/make.sh` script doesn't
`hack/test/unit` script to run unit-tests, and `hack/make.sh` script to run
integration and other tests. The `hack/make.sh` script doesn't
have a single target that runs all the tests. Instead, you provide a single
command line with multiple targets that does the same thing.
@ -110,19 +114,25 @@ Try this now.
$ docker run --privileged --rm -ti -v `pwd`:/go/src/github.com/docker/docker dry-run-test /bin/bash
```
3. Run the tests using the `hack/make.sh` script.
3. Run the unit tests using the `hack/test/unit` script.
```bash
root@5f8630b873fe:/go/src/github.com/docker/docker# hack/make.sh dynbinary binary cross test-unit test-integration test-docker-py
# hack/test/unit
```
4. Run the tests using the `hack/make.sh` script.
```bash
# hack/make.sh dynbinary binary cross test-integration test-docker-py
```
The tests run just as they did within your local host.
Of course, you can also run a subset of these targets too. For example, to run
just the unit tests:
just the integration tests:
```bash
root@5f8630b873fe:/go/src/github.com/docker/docker# hack/make.sh dynbinary binary cross test-unit
# hack/make.sh dynbinary binary cross test-integration
```
Most test targets require that you build these precursor targets first:
@ -170,7 +180,7 @@ $ TESTFLAGS='-check.f DockerSuite.TestBuild*' make test-integration
To run the same test inside your Docker development container, you do this:
```bash
root@5f8630b873fe:/go/src/github.com/docker/docker# TESTFLAGS='-check.f TestBuild*' hack/make.sh binary test-integration
# TESTFLAGS='-check.f TestBuild*' hack/make.sh binary test-integration
```
## Test the Windows binary against a Linux daemon

View File

@ -7,10 +7,6 @@ export PATH="$base/binary-daemon:$base/dynbinary-daemon:$PATH"
export TEST_CLIENT_BINARY=docker
# Do not bump this version! Integration tests should no longer rely on the docker cli, they should be
# API tests instead. For the existing tests the scripts will use a frozen version of the docker cli
# with a DOCKER_API_VERSION frozen to 1.30, which should ensure that the CI remains green at all times.
export DOCKER_API_VERSION=1.30
if [ -n "$DOCKER_CLI_PATH" ]; then
export TEST_CLIENT_BINARY=/usr/local/cli/$(basename "$DOCKER_CLI_PATH")
fi

View File

@ -1,7 +1,6 @@
package daemon // import "github.com/docker/docker/integration-cli/daemon"
import (
"bytes"
"encoding/json"
"fmt"
"io"
@ -14,7 +13,6 @@ import (
"strings"
"time"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/client"
@ -596,28 +594,6 @@ func (d *Daemon) PrependHostArg(args []string) []string {
return append([]string{"--host", d.Sock()}, args...)
}
// SockRequest executes a socket request on a daemon and returns statuscode and output.
func (d *Daemon) SockRequest(method, endpoint string, data interface{}) (int, []byte, error) {
jsonData := bytes.NewBuffer(nil)
if err := json.NewEncoder(jsonData).Encode(data); err != nil {
return -1, nil, err
}
res, body, err := d.SockRequestRaw(method, endpoint, jsonData, "application/json")
if err != nil {
return -1, nil, err
}
b, err := request.ReadBody(body)
return res.StatusCode, b, err
}
// SockRequestRaw executes a socket request on a daemon and returns an http
// response and a reader for the output data.
// Deprecated: use request package instead
func (d *Daemon) SockRequestRaw(method, endpoint string, data io.Reader, ct string) (*http.Response, io.ReadCloser, error) {
return request.SockRequestRaw(method, endpoint, data, ct, d.Sock())
}
// LogFileName returns the path the daemon's log file
func (d *Daemon) LogFileName() string {
return d.logFile.Name()
@ -746,12 +722,9 @@ func (d *Daemon) ReloadConfig() error {
// NewClient creates new client based on daemon's socket path
func (d *Daemon) NewClient() (*client.Client, error) {
httpClient, err := request.NewHTTPClient(d.Sock())
if err != nil {
return nil, err
}
return client.NewClient(d.Sock(), api.DefaultVersion, httpClient, nil)
return client.NewClientWithOpts(
client.FromEnv,
client.WithHost(d.Sock()))
}
// WaitInspectWithArgs waits for the specified expression to be equals to the specified expected string in the given time.

View File

@ -1,18 +1,18 @@
package daemon // import "github.com/docker/docker/integration-cli/daemon"
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/integration-cli/checker"
"github.com/go-check/check"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
@ -234,31 +234,28 @@ func (d *Swarm) CheckServiceUpdateState(service string) func(*check.C) (interfac
// CheckPluginRunning returns the runtime state of the plugin
func (d *Swarm) CheckPluginRunning(plugin string) func(c *check.C) (interface{}, check.CommentInterface) {
return func(c *check.C) (interface{}, check.CommentInterface) {
status, out, err := d.SockRequest("GET", "/plugins/"+plugin+"/json", nil)
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
if status != http.StatusOK {
return false, nil
apiclient, err := d.NewClient()
require.NoError(c, err)
resp, _, err := apiclient.PluginInspectWithRaw(context.Background(), plugin)
if client.IsErrNotFound(err) {
return false, check.Commentf("%v", err)
}
var p types.Plugin
c.Assert(json.Unmarshal(out, &p), checker.IsNil, check.Commentf(string(out)))
return p.Enabled, check.Commentf("%+v", p)
require.NoError(c, err)
return resp.Enabled, check.Commentf("%+v", resp)
}
}
// CheckPluginImage returns the runtime state of the plugin
func (d *Swarm) CheckPluginImage(plugin string) func(c *check.C) (interface{}, check.CommentInterface) {
return func(c *check.C) (interface{}, check.CommentInterface) {
status, out, err := d.SockRequest("GET", "/plugins/"+plugin+"/json", nil)
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
if status != http.StatusOK {
return false, nil
apiclient, err := d.NewClient()
require.NoError(c, err)
resp, _, err := apiclient.PluginInspectWithRaw(context.Background(), plugin)
if client.IsErrNotFound(err) {
return false, check.Commentf("%v", err)
}
var p types.Plugin
c.Assert(json.Unmarshal(out, &p), checker.IsNil, check.Commentf(string(out)))
return p.PluginReference, check.Commentf("%+v", p)
require.NoError(c, err)
return resp.PluginReference, check.Commentf("%+v", resp)
}
}

View File

@ -4,9 +4,11 @@ import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"strings"
"time"
@ -73,10 +75,8 @@ func (s *DockerSuite) TestGetContainersAttachWebsocket(c *check.C) {
// regression gh14320
func (s *DockerSuite) TestPostContainersAttachContainerNotFound(c *check.C) {
client, err := request.NewHTTPClient(daemonHost())
resp, _, err := request.Post("/containers/doesnotexist/attach")
c.Assert(err, checker.IsNil)
req, err := request.New(daemonHost(), "/containers/doesnotexist/attach", request.Method(http.MethodPost))
resp, err := client.Do(req)
// connection will shutdown, err should be "persistent connection closed"
c.Assert(resp.StatusCode, checker.Equals, http.StatusNotFound)
content, err := request.ReadBody(resp.Body)
@ -140,12 +140,12 @@ func (s *DockerSuite) TestPostContainersAttach(c *check.C) {
cid, _ := dockerCmd(c, "run", "-di", "busybox", "cat")
cid = strings.TrimSpace(cid)
// Attach to the container's stdout stream.
conn, br, err := request.SockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", daemonHost())
conn, br, err := sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", daemonHost())
c.Assert(err, checker.IsNil)
// Check if the data from stdout can be received.
expectSuccess(conn, br, "stdout", false)
// Attach to the container's stderr stream.
conn, br, err = request.SockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain", daemonHost())
conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain", daemonHost())
c.Assert(err, checker.IsNil)
// Since the container only emits stdout, attaching to stderr should return nothing.
expectTimeout(conn, br, "stdout")
@ -153,10 +153,10 @@ func (s *DockerSuite) TestPostContainersAttach(c *check.C) {
// Test the similar functions of the stderr stream.
cid, _ = dockerCmd(c, "run", "-di", "busybox", "/bin/sh", "-c", "cat >&2")
cid = strings.TrimSpace(cid)
conn, br, err = request.SockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain", daemonHost())
conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain", daemonHost())
c.Assert(err, checker.IsNil)
expectSuccess(conn, br, "stderr", false)
conn, br, err = request.SockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", daemonHost())
conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", daemonHost())
c.Assert(err, checker.IsNil)
expectTimeout(conn, br, "stderr")
@ -164,12 +164,12 @@ func (s *DockerSuite) TestPostContainersAttach(c *check.C) {
cid, _ = dockerCmd(c, "run", "-dit", "busybox", "/bin/sh", "-c", "cat >&2")
cid = strings.TrimSpace(cid)
// Attach to stdout only.
conn, br, err = request.SockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", daemonHost())
conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stdout=1", nil, "text/plain", daemonHost())
c.Assert(err, checker.IsNil)
expectSuccess(conn, br, "stdout", true)
// Attach without stdout stream.
conn, br, err = request.SockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain", daemonHost())
conn, br, err = sockRequestHijack("POST", "/containers/"+cid+"/attach?stream=1&stdin=1&stderr=1", nil, "text/plain", daemonHost())
c.Assert(err, checker.IsNil)
// Nothing should be received because both the stdout and stderr of the container will be
// sent to the client as stdout when tty is enabled.
@ -210,3 +210,43 @@ func (s *DockerSuite) TestPostContainersAttach(c *check.C) {
stdcopy.StdCopy(actualStdout, actualStderr, resp.Reader)
c.Assert(actualStdout.Bytes(), checker.DeepEquals, []byte("hello\nsuccess"), check.Commentf("Attach didn't return the expected data from stdout"))
}
// SockRequestHijack creates a connection to specified host (with method, contenttype, …) and returns a hijacked connection
// and the output as a `bufio.Reader`
func sockRequestHijack(method, endpoint string, data io.Reader, ct string, daemon string, modifiers ...func(*http.Request)) (net.Conn, *bufio.Reader, error) {
req, client, err := newRequestClient(method, endpoint, data, ct, daemon, modifiers...)
if err != nil {
return nil, nil, err
}
client.Do(req)
conn, br := client.Hijack()
return conn, br, nil
}
// FIXME(vdemeester) httputil.ClientConn is deprecated, use http.Client instead (closer to actual client)
// Deprecated: Use New instead of NewRequestClient
// Deprecated: use request.Do (or Get, Delete, Post) instead
func newRequestClient(method, endpoint string, data io.Reader, ct, daemon string, modifiers ...func(*http.Request)) (*http.Request, *httputil.ClientConn, error) {
c, err := request.SockConn(time.Duration(10*time.Second), daemon)
if err != nil {
return nil, nil, fmt.Errorf("could not dial docker daemon: %v", err)
}
client := httputil.NewClientConn(c, nil)
req, err := http.NewRequest(method, endpoint, data)
if err != nil {
client.Close()
return nil, nil, fmt.Errorf("could not create new request: %v", err)
}
for _, opt := range modifiers {
opt(req)
}
if ct != "" {
req.Header.Set("Content-Type", ct)
}
return req, client, nil
}

View File

@ -319,8 +319,7 @@ func (s *DockerSuite) TestBuildOnBuildCache(c *check.C) {
assert.Len(c, imageIDs, 2)
parentID, childID := imageIDs[0], imageIDs[1]
client, err := request.NewClient()
require.NoError(c, err)
client := testEnv.APIClient()
// check parentID is correct
image, _, err := client.ImageInspectWithRaw(context.Background(), childID)
@ -329,12 +328,11 @@ func (s *DockerSuite) TestBuildOnBuildCache(c *check.C) {
}
func (s *DockerRegistrySuite) TestBuildCopyFromForcePull(c *check.C) {
client, err := request.NewClient()
require.NoError(c, err)
client := testEnv.APIClient()
repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
// tag the image to upload it to the private registry
err = client.ImageTag(context.TODO(), "busybox", repoName)
err := client.ImageTag(context.TODO(), "busybox", repoName)
assert.Nil(c, err)
// push the image to the registry
rc, err := client.ImagePush(context.TODO(), repoName, types.ImagePushOptions{RegistryAuth: "{}"})
@ -545,9 +543,7 @@ func (s *DockerSuite) TestBuildWithSession(c *check.C) {
assert.Equal(c, strings.Count(out, "Using cache"), 2)
assert.Contains(c, out, "contentcontent")
client, err := request.NewClient()
require.NoError(c, err)
client := testEnv.APIClient()
du, err := client.DiskUsage(context.TODO())
assert.Nil(c, err)
assert.True(c, du.BuilderSize > 10)
@ -582,9 +578,7 @@ func (s *DockerSuite) TestBuildWithSession(c *check.C) {
}
func testBuildWithSession(c *check.C, dir, dockerfile string) (outStr string) {
client, err := request.NewClient()
require.NoError(c, err)
client := testEnv.APIClient()
sess, err := session.NewSession("foo1", "foo")
assert.Nil(c, err)

View File

@ -6,13 +6,16 @@ import (
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"strings"
winio "github.com/Microsoft/go-winio"
"github.com/docker/docker/integration-cli/checker"
"github.com/docker/docker/integration-cli/request"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/go-check/check"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func (s *DockerSuite) TestContainersAPICreateMountsBindNamedPipe(c *check.C) {
@ -44,28 +47,30 @@ func (s *DockerSuite) TestContainersAPICreateMountsBindNamedPipe(c *check.C) {
containerPipeName := `\\.\pipe\docker-cli-test-pipe`
text := "hello from a pipe"
cmd := fmt.Sprintf("echo %s > %s", text, containerPipeName)
name := "test-bind-npipe"
data := map[string]interface{}{
"Image": testEnv.PlatformDefaults.BaseImage,
"Cmd": []string{"cmd", "/c", cmd},
"HostConfig": map[string]interface{}{"Mounts": []map[string]interface{}{{"Type": "npipe", "Source": hostPipeName, "Target": containerPipeName}}},
}
status, resp, err := request.SockRequest("POST", "/containers/create?name="+name, data, daemonHost())
c.Assert(err, checker.IsNil, check.Commentf(string(resp)))
c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf(string(resp)))
ctx := context.Background()
client := testEnv.APIClient()
_, err = client.ContainerCreate(ctx,
&container.Config{
Image: testEnv.PlatformDefaults.BaseImage,
Cmd: []string{"cmd", "/c", cmd},
}, &container.HostConfig{
Mounts: []mount.Mount{
{
Type: "npipe",
Source: hostPipeName,
Target: containerPipeName,
},
},
},
nil, name)
require.NoError(c, err)
status, _, err = request.SockRequest("POST", "/containers/"+name+"/start", nil, daemonHost())
c.Assert(err, checker.IsNil)
c.Assert(status, checker.Equals, http.StatusNoContent)
err = client.ContainerStart(ctx, name, types.ContainerStartOptions{})
require.NoError(c, err)
err = <-ch
if err != nil {
c.Fatal(err)
}
result := strings.TrimSpace(string(b))
if result != text {
c.Errorf("expected pipe to contain %s, got %s", text, result)
}
require.NoError(c, err)
assert.Equal(c, text, strings.TrimSpace(string(b)))
}

View File

@ -59,7 +59,7 @@ func (s *DockerSuite) TestExecResizeImmediatelyAfterExecStart(c *check.C) {
}
payload := bytes.NewBufferString(`{"Tty":true}`)
conn, _, err := request.SockRequestHijack("POST", fmt.Sprintf("/exec/%s/start", execID), payload, "application/json", daemonHost())
conn, _, err := sockRequestHijack("POST", fmt.Sprintf("/exec/%s/start", execID), payload, "application/json", daemonHost())
if err != nil {
return fmt.Errorf("Failed to start the exec: %q", err.Error())
}

View File

@ -157,33 +157,21 @@ func (s *DockerSuite) TestAPIImagesSearchJSONContentType(c *check.C) {
// Test case for 30027: image size reported as -1 in v1.12 client against v1.13 daemon.
// This test checks to make sure both v1.12 and v1.13 client against v1.13 daemon get correct `Size` after the fix.
func (s *DockerSuite) TestAPIImagesSizeCompatibility(c *check.C) {
cli, err := client.NewEnvClient()
c.Assert(err, checker.IsNil)
defer cli.Close()
apiclient := testEnv.APIClient()
defer apiclient.Close()
images, err := cli.ImageList(context.Background(), types.ImageListOptions{})
images, err := apiclient.ImageList(context.Background(), types.ImageListOptions{})
c.Assert(err, checker.IsNil)
c.Assert(len(images), checker.Not(checker.Equals), 0)
for _, image := range images {
c.Assert(image.Size, checker.Not(checker.Equals), int64(-1))
}
type v124Image struct {
ID string `json:"Id"`
ParentID string `json:"ParentId"`
RepoTags []string
RepoDigests []string
Created int64
Size int64
VirtualSize int64
Labels map[string]string
}
cli, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("v1.24"))
apiclient, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("v1.24"))
c.Assert(err, checker.IsNil)
defer cli.Close()
defer apiclient.Close()
v124Images, err := cli.ImageList(context.Background(), types.ImageListOptions{})
v124Images, err := apiclient.ImageList(context.Background(), types.ImageListOptions{})
c.Assert(err, checker.IsNil)
c.Assert(len(v124Images), checker.Not(checker.Equals), 0)
for _, image := range v124Images {

View File

@ -12,7 +12,6 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/integration-cli/checker"
"github.com/docker/docker/integration-cli/cli"
"github.com/docker/docker/integration-cli/request"
"github.com/go-check/check"
"golang.org/x/net/context"
)
@ -59,8 +58,7 @@ func testIpcNonePrivateShareable(c *check.C, mode string, mustBeMounted bool, mu
}
ctx := context.Background()
client, err := request.NewClient()
c.Assert(err, checker.IsNil)
client := testEnv.APIClient()
resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, "")
c.Assert(err, checker.IsNil)
@ -125,8 +123,7 @@ func testIpcContainer(s *DockerSuite, c *check.C, donorMode string, mustWork boo
}
ctx := context.Background()
client, err := request.NewClient()
c.Assert(err, checker.IsNil)
client := testEnv.APIClient()
// create and start the "donor" container
resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, "")
@ -195,9 +192,7 @@ func (s *DockerSuite) TestAPIIpcModeHost(c *check.C) {
}
ctx := context.Background()
client, err := request.NewClient()
c.Assert(err, checker.IsNil)
client := testEnv.APIClient()
resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, "")
c.Assert(err, checker.IsNil)
c.Assert(len(resp.Warnings), checker.Equals, 0)

View File

@ -3,12 +3,10 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
@ -21,11 +19,14 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/integration-cli/checker"
"github.com/docker/docker/integration-cli/daemon"
"github.com/docker/docker/integration-cli/request"
"github.com/docker/swarmkit/ca"
"github.com/go-check/check"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
@ -1006,32 +1007,19 @@ func (s *DockerSwarmSuite) TestSwarmRepeatedRootRotation(c *check.C) {
func (s *DockerSwarmSuite) TestAPINetworkInspectWithScope(c *check.C) {
d := s.AddDaemon(c, true, true)
name := "foo"
networkCreateRequest := types.NetworkCreateRequest{
Name: name,
}
name := "test-scoped-network"
ctx := context.Background()
apiclient, err := d.NewClient()
require.NoError(c, err)
var n types.NetworkCreateResponse
networkCreateRequest.NetworkCreate.Driver = "overlay"
resp, err := apiclient.NetworkCreate(ctx, name, types.NetworkCreate{Driver: "overlay"})
require.NoError(c, err)
status, out, err := d.SockRequest("POST", "/networks/create", networkCreateRequest)
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
c.Assert(status, checker.Equals, http.StatusCreated, check.Commentf(string(out)))
c.Assert(json.Unmarshal(out, &n), checker.IsNil)
network, err := apiclient.NetworkInspect(ctx, name, types.NetworkInspectOptions{})
require.NoError(c, err)
assert.Equal(c, "swarm", network.Scope)
assert.Equal(c, resp.ID, network.ID)
var r types.NetworkResource
status, body, err := d.SockRequest("GET", "/networks/"+name, nil)
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
c.Assert(status, checker.Equals, http.StatusOK, check.Commentf(string(out)))
c.Assert(json.Unmarshal(body, &r), checker.IsNil)
c.Assert(r.Scope, checker.Equals, "swarm")
c.Assert(r.ID, checker.Equals, n.ID)
v := url.Values{}
v.Set("scope", "local")
status, _, err = d.SockRequest("GET", "/networks/"+name+"?"+v.Encode(), nil)
c.Assert(err, checker.IsNil, check.Commentf(string(out)))
c.Assert(status, checker.Equals, http.StatusNotFound, check.Commentf(string(out)))
_, err = apiclient.NetworkInspect(ctx, name, types.NetworkInspectOptions{Scope: "local"})
assert.True(c, client.IsErrNotFound(err))
}

View File

@ -3,8 +3,6 @@
package main
import (
"os"
"path/filepath"
"strings"
"github.com/docker/docker/integration-cli/checker"
@ -199,12 +197,6 @@ func (s *DockerDaemonSuite) TestVolumePlugin(c *check.C) {
if err != nil {
c.Fatalf("Could not install plugin: %v %s", err, out)
}
pluginID, err := s.d.Cmd("plugin", "inspect", "-f", "{{.Id}}", pName)
pluginID = strings.TrimSpace(pluginID)
if err != nil {
c.Fatalf("Could not retrieve plugin ID: %v %s", err, pluginID)
}
mountpointPrefix := filepath.Join(s.d.RootDir(), "plugins", pluginID, "rootfs")
defer func() {
if out, err := s.d.Cmd("plugin", "disable", pName); err != nil {
c.Fatalf("Could not disable plugin: %v %s", err, out)
@ -213,11 +205,6 @@ func (s *DockerDaemonSuite) TestVolumePlugin(c *check.C) {
if out, err := s.d.Cmd("plugin", "remove", pName); err != nil {
c.Fatalf("Could not remove plugin: %v %s", err, out)
}
exists, err := existsMountpointWithPrefix(mountpointPrefix)
c.Assert(err, checker.IsNil)
c.Assert(exists, checker.Equals, false)
}()
out, err = s.d.Cmd("volume", "create", "-d", pName, volName)
@ -237,21 +224,11 @@ func (s *DockerDaemonSuite) TestVolumePlugin(c *check.C) {
c.Assert(out, checker.Contains, volName)
c.Assert(out, checker.Contains, pName)
mountPoint, err := s.d.Cmd("volume", "inspect", volName, "--format", "{{.Mountpoint}}")
if err != nil {
c.Fatalf("Could not inspect volume: %v %s", err, mountPoint)
}
mountPoint = strings.TrimSpace(mountPoint)
out, err = s.d.Cmd("run", "--rm", "-v", volName+":"+destDir, "busybox", "touch", destDir+destFile)
c.Assert(err, checker.IsNil, check.Commentf(out))
path := filepath.Join(s.d.RootDir(), "plugins", pluginID, "rootfs", mountPoint, destFile)
_, err = os.Lstat(path)
c.Assert(err, checker.IsNil)
exists, err := existsMountpointWithPrefix(mountpointPrefix)
c.Assert(err, checker.IsNil)
c.Assert(exists, checker.Equals, true)
out, err = s.d.Cmd("run", "--rm", "-v", volName+":"+destDir, "busybox", "ls", destDir+destFile)
c.Assert(err, checker.IsNil, check.Commentf(out))
}
func (s *DockerDaemonSuite) TestGraphdriverPlugin(c *check.C) {

View File

@ -15,7 +15,6 @@ import (
"github.com/docker/docker/integration-cli/cli"
"github.com/docker/docker/integration-cli/daemon"
"github.com/docker/docker/integration-cli/fixtures/plugin"
"github.com/docker/docker/integration-cli/request"
"github.com/go-check/check"
"github.com/gotestyourself/gotestyourself/icmd"
"golang.org/x/net/context"
@ -159,9 +158,7 @@ func (s *DockerSuite) TestPluginInstallDisableVolumeLs(c *check.C) {
}
func (ps *DockerPluginSuite) TestPluginSet(c *check.C) {
// Create a new plugin with extra settings
client, err := request.NewClient()
c.Assert(err, checker.IsNil, check.Commentf("failed to create test client"))
client := testEnv.APIClient()
name := "test"
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
@ -171,7 +168,8 @@ func (ps *DockerPluginSuite) TestPluginSet(c *check.C) {
mntSrc := "foo"
devPath := "/dev/bar"
err = plugin.Create(ctx, client, name, func(cfg *plugin.Config) {
// Create a new plugin with extra settings
err := plugin.Create(ctx, client, name, func(cfg *plugin.Config) {
cfg.Env = []types.PluginEnv{{Name: "DEBUG", Value: &initialValue, Settable: []string{"value"}}}
cfg.Mounts = []types.PluginMount{
{Name: "pmount1", Settable: []string{"source"}, Type: "none", Source: &mntSrc},
@ -401,12 +399,11 @@ func (s *DockerTrustSuite) TestPluginUntrustedInstall(c *check.C) {
func (ps *DockerPluginSuite) TestPluginIDPrefix(c *check.C) {
name := "test"
client, err := request.NewClient()
c.Assert(err, checker.IsNil, check.Commentf("error creating test client"))
client := testEnv.APIClient()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
initialValue := "0"
err = plugin.Create(ctx, client, name, func(cfg *plugin.Config) {
err := plugin.Create(ctx, client, name, func(cfg *plugin.Config) {
cfg.Env = []types.PluginEnv{{Name: "DEBUG", Value: &initialValue, Settable: []string{"value"}}}
})
cancel()
@ -466,8 +463,7 @@ func (ps *DockerPluginSuite) TestPluginListDefaultFormat(c *check.C) {
c.Assert(err, check.IsNil)
name := "test:latest"
client, err := request.NewClient()
c.Assert(err, checker.IsNil, check.Commentf("error creating test client"))
client := testEnv.APIClient()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

View File

@ -1,16 +1,13 @@
package request // import "github.com/docker/docker/integration-cli/request"
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
@ -95,11 +92,11 @@ func Do(endpoint string, modifiers ...func(*http.Request) error) (*http.Response
// DoOnHost creates and execute a request on the specified host and endpoint, with the specified request modifiers
func DoOnHost(host, endpoint string, modifiers ...func(*http.Request) error) (*http.Response, io.ReadCloser, error) {
req, err := New(host, endpoint, modifiers...)
req, err := newRequest(host, endpoint, modifiers...)
if err != nil {
return nil, nil, err
}
client, err := NewHTTPClient(host)
client, err := newHTTPClient(host)
if err != nil {
return nil, nil, err
}
@ -114,18 +111,15 @@ func DoOnHost(host, endpoint string, modifiers ...func(*http.Request) error) (*h
return resp, body, err
}
// New creates a new http Request to the specified host and endpoint, with the specified request modifiers
func New(host, endpoint string, modifiers ...func(*http.Request) error) (*http.Request, error) {
_, addr, _, err := dclient.ParseHost(host)
// newRequest creates a new http Request to the specified host and endpoint, with the specified request modifiers
func newRequest(host, endpoint string, modifiers ...func(*http.Request) error) (*http.Request, error) {
hostUrl, err := dclient.ParseHostURL(host)
if err != nil {
return nil, err
}
if err != nil {
return nil, errors.Wrapf(err, "could not parse url %q", host)
return nil, errors.Wrapf(err, "failed parsing url %q", host)
}
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return nil, fmt.Errorf("could not create new request: %v", err)
return nil, errors.Wrap(err, "failed to create request")
}
if os.Getenv("DOCKER_TLS_VERIFY") != "" {
@ -133,7 +127,7 @@ func New(host, endpoint string, modifiers ...func(*http.Request) error) (*http.R
} else {
req.URL.Scheme = "http"
}
req.URL.Host = addr
req.URL.Host = hostUrl.Host
for _, config := range modifiers {
if err := config(req); err != nil {
@ -143,15 +137,16 @@ func New(host, endpoint string, modifiers ...func(*http.Request) error) (*http.R
return req, nil
}
// NewHTTPClient creates an http client for the specific host
func NewHTTPClient(host string) (*http.Client, error) {
// newHTTPClient creates an http client for the specific host
// TODO: Share more code with client.defaultHTTPClient
func newHTTPClient(host string) (*http.Client, error) {
// FIXME(vdemeester) 10*time.Second timeout of SockRequest… ?
proto, addr, _, err := dclient.ParseHost(host)
hostUrl, err := dclient.ParseHostURL(host)
if err != nil {
return nil, err
}
transport := new(http.Transport)
if proto == "tcp" && os.Getenv("DOCKER_TLS_VERIFY") != "" {
if hostUrl.Scheme == "tcp" && os.Getenv("DOCKER_TLS_VERIFY") != "" {
// Setup the socket TLS configuration.
tlsConfig, err := getTLSConfig()
if err != nil {
@ -160,102 +155,22 @@ func NewHTTPClient(host string) (*http.Client, error) {
transport = &http.Transport{TLSClientConfig: tlsConfig}
}
transport.DisableKeepAlives = true
err = sockets.ConfigureTransport(transport, proto, addr)
return &http.Client{
Transport: transport,
}, err
err = sockets.ConfigureTransport(transport, hostUrl.Scheme, hostUrl.Host)
return &http.Client{Transport: transport}, err
}
// NewClient returns a new Docker API client
// Deprecated: Use Execution.APIClient()
func NewClient() (dclient.APIClient, error) {
return dclient.NewClientWithOpts(dclient.WithHost(DaemonHost()))
}
// FIXME(vdemeester) httputil.ClientConn is deprecated, use http.Client instead (closer to actual client)
// Deprecated: Use New instead of NewRequestClient
// Deprecated: use request.Do (or Get, Delete, Post) instead
func newRequestClient(method, endpoint string, data io.Reader, ct, daemon string, modifiers ...func(*http.Request)) (*http.Request, *httputil.ClientConn, error) {
c, err := SockConn(time.Duration(10*time.Second), daemon)
if err != nil {
return nil, nil, fmt.Errorf("could not dial docker daemon: %v", err)
}
client := httputil.NewClientConn(c, nil)
req, err := http.NewRequest(method, endpoint, data)
if err != nil {
client.Close()
return nil, nil, fmt.Errorf("could not create new request: %v", err)
}
for _, opt := range modifiers {
opt(req)
}
if ct != "" {
req.Header.Set("Content-Type", ct)
}
return req, client, nil
}
// SockRequest create a request against the specified host (with method, endpoint and other request modifier) and
// returns the status code, and the content as an byte slice
// Deprecated: use request.Do instead
func SockRequest(method, endpoint string, data interface{}, daemon string, modifiers ...func(*http.Request)) (int, []byte, error) {
jsonData := bytes.NewBuffer(nil)
if err := json.NewEncoder(jsonData).Encode(data); err != nil {
return -1, nil, err
}
res, body, err := SockRequestRaw(method, endpoint, jsonData, "application/json", daemon, modifiers...)
if err != nil {
return -1, nil, err
}
b, err := ReadBody(body)
return res.StatusCode, b, err
}
// ReadBody read the specified ReadCloser content and returns it
func ReadBody(b io.ReadCloser) ([]byte, error) {
defer b.Close()
return ioutil.ReadAll(b)
}
// SockRequestRaw create a request against the specified host (with method, endpoint and other request modifier) and
// returns the http response, the output as a io.ReadCloser
// Deprecated: use request.Do (or Get, Delete, Post) instead
func SockRequestRaw(method, endpoint string, data io.Reader, ct, daemon string, modifiers ...func(*http.Request)) (*http.Response, io.ReadCloser, error) {
req, client, err := newRequestClient(method, endpoint, data, ct, daemon, modifiers...)
if err != nil {
return nil, nil, err
}
resp, err := client.Do(req)
if err != nil {
client.Close()
return resp, nil, err
}
body := ioutils.NewReadCloserWrapper(resp.Body, func() error {
defer resp.Body.Close()
return client.Close()
})
return resp, body, err
}
// SockRequestHijack creates a connection to specified host (with method, contenttype, …) and returns a hijacked connection
// and the output as a `bufio.Reader`
func SockRequestHijack(method, endpoint string, data io.Reader, ct string, daemon string, modifiers ...func(*http.Request)) (net.Conn, *bufio.Reader, error) {
req, client, err := newRequestClient(method, endpoint, data, ct, daemon, modifiers...)
if err != nil {
return nil, nil, err
}
client.Do(req)
conn, br := client.Hijack()
return conn, br, nil
}
// SockConn opens a connection on the specified socket
func SockConn(timeout time.Duration, daemon string) (net.Conn, error) {
daemonURL, err := url.Parse(daemon)

View File

@ -17,7 +17,6 @@ import (
"github.com/docker/docker/integration-cli/checker"
"github.com/docker/docker/integration-cli/cli"
"github.com/docker/docker/integration-cli/fixtures/plugin"
"github.com/docker/docker/integration-cli/request"
"github.com/docker/go-connections/tlsconfig"
"github.com/go-check/check"
"github.com/gotestyourself/gotestyourself/icmd"
@ -230,11 +229,10 @@ func (s *DockerTrustSuite) setupTrustedImage(c *check.C, name string) string {
func (s *DockerTrustSuite) setupTrustedplugin(c *check.C, source, name string) string {
repoName := fmt.Sprintf("%v/dockercli/%s:latest", privateRegistryURL, name)
client, err := request.NewClient()
c.Assert(err, checker.IsNil, check.Commentf("could not create test client"))
client := testEnv.APIClient()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
err = plugin.Create(ctx, client, repoName)
err := plugin.Create(ctx, client, repoName)
cancel()
c.Assert(err, checker.IsNil, check.Commentf("could not create test plugin"))

View File

@ -1,8 +1,10 @@
package config
import (
"bytes"
"sort"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@ -10,6 +12,7 @@ import (
"github.com/docker/docker/client"
"github.com/docker/docker/integration/internal/swarm"
"github.com/docker/docker/internal/testutil"
"github.com/docker/docker/pkg/stdcopy"
"github.com/gotestyourself/gotestyourself/skip"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -188,3 +191,139 @@ func TestConfigsUpdate(t *testing.T) {
err = client.ConfigUpdate(ctx, configID, insp.Version, insp.Spec)
testutil.ErrorContains(t, err, "only updates to Labels are allowed")
}
func TestTemplatedConfig(t *testing.T) {
d := swarm.NewSwarm(t, testEnv)
defer d.Stop(t)
ctx := context.Background()
client := swarm.GetClient(t, d)
referencedSecretSpec := swarmtypes.SecretSpec{
Annotations: swarmtypes.Annotations{
Name: "referencedsecret",
},
Data: []byte("this is a secret"),
}
referencedSecret, err := client.SecretCreate(ctx, referencedSecretSpec)
assert.NoError(t, err)
referencedConfigSpec := swarmtypes.ConfigSpec{
Annotations: swarmtypes.Annotations{
Name: "referencedconfig",
},
Data: []byte("this is a config"),
}
referencedConfig, err := client.ConfigCreate(ctx, referencedConfigSpec)
assert.NoError(t, err)
configSpec := swarmtypes.ConfigSpec{
Annotations: swarmtypes.Annotations{
Name: "templated_config",
},
Templating: &swarmtypes.Driver{
Name: "golang",
},
Data: []byte("SERVICE_NAME={{.Service.Name}}\n" +
"{{secret \"referencedsecrettarget\"}}\n" +
"{{config \"referencedconfigtarget\"}}\n"),
}
templatedConfig, err := client.ConfigCreate(ctx, configSpec)
assert.NoError(t, err)
serviceID := swarm.CreateService(t, d,
swarm.ServiceWithConfig(
&swarmtypes.ConfigReference{
File: &swarmtypes.ConfigReferenceFileTarget{
Name: "/templated_config",
UID: "0",
GID: "0",
Mode: 0600,
},
ConfigID: templatedConfig.ID,
ConfigName: "templated_config",
},
),
swarm.ServiceWithConfig(
&swarmtypes.ConfigReference{
File: &swarmtypes.ConfigReferenceFileTarget{
Name: "referencedconfigtarget",
UID: "0",
GID: "0",
Mode: 0600,
},
ConfigID: referencedConfig.ID,
ConfigName: "referencedconfig",
},
),
swarm.ServiceWithSecret(
&swarmtypes.SecretReference{
File: &swarmtypes.SecretReferenceFileTarget{
Name: "referencedsecrettarget",
UID: "0",
GID: "0",
Mode: 0600,
},
SecretID: referencedSecret.ID,
SecretName: "referencedsecret",
},
),
swarm.ServiceWithName("svc"),
)
var tasks []swarmtypes.Task
waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
tasks = swarm.GetRunningTasks(t, d, serviceID)
return len(tasks) > 0
})
task := tasks[0]
waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
if task.NodeID == "" || (task.Status.ContainerStatus == nil || task.Status.ContainerStatus.ContainerID == "") {
task, _, _ = client.TaskInspectWithRaw(context.Background(), task.ID)
}
return task.NodeID != "" && task.Status.ContainerStatus != nil && task.Status.ContainerStatus.ContainerID != ""
})
attach := swarm.ExecTask(t, d, task, types.ExecConfig{
Cmd: []string{"/bin/cat", "/templated_config"},
AttachStdout: true,
AttachStderr: true,
})
expect := "SERVICE_NAME=svc\n" +
"this is a secret\n" +
"this is a config\n"
assertAttachedStream(t, attach, expect)
attach = swarm.ExecTask(t, d, task, types.ExecConfig{
Cmd: []string{"mount"},
AttachStdout: true,
AttachStderr: true,
})
assertAttachedStream(t, attach, "tmpfs on /templated_config type tmpfs")
}
func assertAttachedStream(t *testing.T, attach types.HijackedResponse, expect string) {
buf := bytes.NewBuffer(nil)
_, err := stdcopy.StdCopy(buf, buf, attach.Reader)
require.NoError(t, err)
assert.Contains(t, buf.String(), expect)
}
func waitAndAssert(t *testing.T, timeout time.Duration, f func(*testing.T) bool) {
t.Helper()
after := time.After(timeout)
for {
select {
case <-after:
t.Fatalf("timed out waiting for condition")
default:
}
if f(t) {
return
}
time.Sleep(100 * time.Millisecond)
}
}

View File

@ -35,7 +35,7 @@ func TestUpdateMemory(t *testing.T) {
const (
setMemory int64 = 314572800
setMemorySwap = 524288000
setMemorySwap int64 = 524288000
)
_, err := client.ContainerUpdate(ctx, cID, containertypes.UpdateConfig{
@ -66,7 +66,7 @@ func TestUpdateMemory(t *testing.T) {
assert.Equal(t, strconv.FormatInt(setMemorySwap, 10), strings.TrimSpace(res.Stdout()))
}
func TestUpdateCPUQUota(t *testing.T) {
func TestUpdateCPUQuota(t *testing.T) {
t.Parallel()
defer setupTest(t)()

View File

@ -2,8 +2,6 @@ package request // import "github.com/docker/docker/integration/internal/request
import (
"fmt"
"net"
"net/http"
"testing"
"time"
@ -11,8 +9,6 @@ import (
"github.com/docker/docker/client"
"github.com/docker/docker/internal/test/environment"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
"github.com/stretchr/testify/require"
)
@ -24,36 +20,6 @@ func NewAPIClient(t *testing.T, ops ...func(*client.Client) error) client.APICli
return clt
}
// NewTLSAPIClient returns a docker API client configured with the
// provided TLS settings
func NewTLSAPIClient(t *testing.T, host, cacertPath, certPath, keyPath string) (client.APIClient, error) {
opts := tlsconfig.Options{
CAFile: cacertPath,
CertFile: certPath,
KeyFile: keyPath,
ExclusiveRootPools: true,
}
config, err := tlsconfig.Client(opts)
require.Nil(t, err)
tr := &http.Transport{
TLSClientConfig: config,
DialContext: (&net.Dialer{
KeepAlive: 30 * time.Second,
Timeout: 30 * time.Second,
}).DialContext,
}
proto, addr, _, err := client.ParseHost(host)
require.Nil(t, err)
sockets.ConfigureTransport(tr, proto, addr)
httpClient := &http.Client{
Transport: tr,
CheckRedirect: client.CheckRedirect,
}
return client.NewClientWithOpts(client.WithHost(host), client.WithHTTPClient(httpClient))
}
// daemonTime provides the current time on the daemon host
func daemonTime(ctx context.Context, t *testing.T, client client.APIClient, testEnv *environment.Execution) time.Time {
if testEnv.IsLocalDaemon() {

View File

@ -1,10 +1,14 @@
package swarm
import (
"context"
"fmt"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
swarmtypes "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/integration-cli/daemon"
"github.com/docker/docker/internal/test/environment"
"github.com/stretchr/testify/require"
@ -34,3 +38,121 @@ func NewSwarm(t *testing.T, testEnv *environment.Execution) *daemon.Swarm {
require.NoError(t, d.Init(swarmtypes.InitRequest{}))
return d
}
// ServiceSpecOpt is used with `CreateService` to pass in service spec modifiers
type ServiceSpecOpt func(*swarmtypes.ServiceSpec)
// CreateService creates a service on the passed in swarm daemon.
func CreateService(t *testing.T, d *daemon.Swarm, opts ...ServiceSpecOpt) string {
spec := defaultServiceSpec()
for _, o := range opts {
o(&spec)
}
client := GetClient(t, d)
resp, err := client.ServiceCreate(context.Background(), spec, types.ServiceCreateOptions{})
require.NoError(t, err, "error creating service")
return resp.ID
}
func defaultServiceSpec() swarmtypes.ServiceSpec {
var spec swarmtypes.ServiceSpec
ServiceWithImage("busybox:latest")(&spec)
ServiceWithCommand([]string{"/bin/top"})(&spec)
ServiceWithReplicas(1)(&spec)
return spec
}
// ServiceWithImage sets the image to use for the service
func ServiceWithImage(image string) func(*swarmtypes.ServiceSpec) {
return func(spec *swarmtypes.ServiceSpec) {
ensureContainerSpec(spec)
spec.TaskTemplate.ContainerSpec.Image = image
}
}
// ServiceWithCommand sets the command to use for the service
func ServiceWithCommand(cmd []string) ServiceSpecOpt {
return func(spec *swarmtypes.ServiceSpec) {
ensureContainerSpec(spec)
spec.TaskTemplate.ContainerSpec.Command = cmd
}
}
// ServiceWithConfig adds the config reference to the service
func ServiceWithConfig(configRef *swarmtypes.ConfigReference) ServiceSpecOpt {
return func(spec *swarmtypes.ServiceSpec) {
ensureContainerSpec(spec)
spec.TaskTemplate.ContainerSpec.Configs = append(spec.TaskTemplate.ContainerSpec.Configs, configRef)
}
}
// ServiceWithSecret adds the secret reference to the service
func ServiceWithSecret(secretRef *swarmtypes.SecretReference) ServiceSpecOpt {
return func(spec *swarmtypes.ServiceSpec) {
ensureContainerSpec(spec)
spec.TaskTemplate.ContainerSpec.Secrets = append(spec.TaskTemplate.ContainerSpec.Secrets, secretRef)
}
}
// ServiceWithReplicas sets the replicas for the service
func ServiceWithReplicas(n uint64) ServiceSpecOpt {
return func(spec *swarmtypes.ServiceSpec) {
spec.Mode = swarmtypes.ServiceMode{
Replicated: &swarmtypes.ReplicatedService{
Replicas: &n,
},
}
}
}
// ServiceWithName sets the name of the service
func ServiceWithName(name string) ServiceSpecOpt {
return func(spec *swarmtypes.ServiceSpec) {
spec.Annotations.Name = name
}
}
// GetRunningTasks gets the list of running tasks for a service
func GetRunningTasks(t *testing.T, d *daemon.Swarm, serviceID string) []swarmtypes.Task {
client := GetClient(t, d)
filterArgs := filters.NewArgs()
filterArgs.Add("desired-state", "running")
filterArgs.Add("service", serviceID)
options := types.TaskListOptions{
Filters: filterArgs,
}
tasks, err := client.TaskList(context.Background(), options)
require.NoError(t, err)
return tasks
}
// ExecTask runs the passed in exec config on the given task
func ExecTask(t *testing.T, d *daemon.Swarm, task swarmtypes.Task, config types.ExecConfig) types.HijackedResponse {
client := GetClient(t, d)
ctx := context.Background()
resp, err := client.ContainerExecCreate(ctx, task.Status.ContainerStatus.ContainerID, config)
require.NoError(t, err, "error creating exec")
startCheck := types.ExecStartCheck{}
attach, err := client.ContainerExecAttach(ctx, resp.ID, startCheck)
require.NoError(t, err, "error attaching to exec")
return attach
}
func ensureContainerSpec(spec *swarmtypes.ServiceSpec) {
if spec.TaskTemplate.ContainerSpec == nil {
spec.TaskTemplate.ContainerSpec = &swarmtypes.ContainerSpec{}
}
}
// GetClient creates a new client for the passed in swarm daemon.
func GetClient(t *testing.T, d *daemon.Swarm) client.APIClient {
client, err := client.NewClientWithOpts(client.WithHost((d.Sock())))
require.NoError(t, err)
return client
}

View File

@ -22,7 +22,6 @@ import (
eventtypes "github.com/docker/docker/api/types/events"
"github.com/docker/docker/client"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/integration/internal/request"
"github.com/docker/docker/internal/test/environment"
"github.com/docker/docker/pkg/authorization"
"github.com/gotestyourself/gotestyourself/skip"
@ -126,7 +125,7 @@ func TestAuthZPluginTLS(t *testing.T) {
ctrl.reqRes.Allow = true
ctrl.resRes.Allow = true
client, err := request.NewTLSAPIClient(t, testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath)
client, err := newTLSAPIClient(testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath)
require.Nil(t, err)
_, err = client.ServerVersion(context.Background())
@ -136,6 +135,17 @@ func TestAuthZPluginTLS(t *testing.T) {
require.Equal(t, "client", ctrl.resUser)
}
func newTLSAPIClient(host, cacertPath, certPath, keyPath string) (client.APIClient, error) {
dialer := &net.Dialer{
KeepAlive: 30 * time.Second,
Timeout: 30 * time.Second,
}
return client.NewClientWithOpts(
client.WithTLSClientConfig(cacertPath, certPath, keyPath),
client.WithDialer(dialer),
client.WithHost(host))
}
func TestAuthZPluginDenyRequest(t *testing.T) {
defer setupTestV1(t)()
d.Start(t, "--authorization-plugin="+testAuthZPlugin)

View File

@ -1,8 +1,10 @@
package secret
import (
"bytes"
"sort"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@ -10,6 +12,7 @@ import (
"github.com/docker/docker/client"
"github.com/docker/docker/integration/internal/swarm"
"github.com/docker/docker/internal/testutil"
"github.com/docker/docker/pkg/stdcopy"
"github.com/gotestyourself/gotestyourself/skip"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -232,3 +235,139 @@ func TestSecretsUpdate(t *testing.T) {
err = client.SecretUpdate(ctx, secretID, insp.Version, insp.Spec)
testutil.ErrorContains(t, err, "only updates to Labels are allowed")
}
func TestTemplatedSecret(t *testing.T) {
d := swarm.NewSwarm(t, testEnv)
defer d.Stop(t)
ctx := context.Background()
client := swarm.GetClient(t, d)
referencedSecretSpec := swarmtypes.SecretSpec{
Annotations: swarmtypes.Annotations{
Name: "referencedsecret",
},
Data: []byte("this is a secret"),
}
referencedSecret, err := client.SecretCreate(ctx, referencedSecretSpec)
assert.NoError(t, err)
referencedConfigSpec := swarmtypes.ConfigSpec{
Annotations: swarmtypes.Annotations{
Name: "referencedconfig",
},
Data: []byte("this is a config"),
}
referencedConfig, err := client.ConfigCreate(ctx, referencedConfigSpec)
assert.NoError(t, err)
secretSpec := swarmtypes.SecretSpec{
Annotations: swarmtypes.Annotations{
Name: "templated_secret",
},
Templating: &swarmtypes.Driver{
Name: "golang",
},
Data: []byte("SERVICE_NAME={{.Service.Name}}\n" +
"{{secret \"referencedsecrettarget\"}}\n" +
"{{config \"referencedconfigtarget\"}}\n"),
}
templatedSecret, err := client.SecretCreate(ctx, secretSpec)
assert.NoError(t, err)
serviceID := swarm.CreateService(t, d,
swarm.ServiceWithSecret(
&swarmtypes.SecretReference{
File: &swarmtypes.SecretReferenceFileTarget{
Name: "templated_secret",
UID: "0",
GID: "0",
Mode: 0600,
},
SecretID: templatedSecret.ID,
SecretName: "templated_secret",
},
),
swarm.ServiceWithConfig(
&swarmtypes.ConfigReference{
File: &swarmtypes.ConfigReferenceFileTarget{
Name: "referencedconfigtarget",
UID: "0",
GID: "0",
Mode: 0600,
},
ConfigID: referencedConfig.ID,
ConfigName: "referencedconfig",
},
),
swarm.ServiceWithSecret(
&swarmtypes.SecretReference{
File: &swarmtypes.SecretReferenceFileTarget{
Name: "referencedsecrettarget",
UID: "0",
GID: "0",
Mode: 0600,
},
SecretID: referencedSecret.ID,
SecretName: "referencedsecret",
},
),
swarm.ServiceWithName("svc"),
)
var tasks []swarmtypes.Task
waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
tasks = swarm.GetRunningTasks(t, d, serviceID)
return len(tasks) > 0
})
task := tasks[0]
waitAndAssert(t, 60*time.Second, func(t *testing.T) bool {
if task.NodeID == "" || (task.Status.ContainerStatus == nil || task.Status.ContainerStatus.ContainerID == "") {
task, _, _ = client.TaskInspectWithRaw(context.Background(), task.ID)
}
return task.NodeID != "" && task.Status.ContainerStatus != nil && task.Status.ContainerStatus.ContainerID != ""
})
attach := swarm.ExecTask(t, d, task, types.ExecConfig{
Cmd: []string{"/bin/cat", "/run/secrets/templated_secret"},
AttachStdout: true,
AttachStderr: true,
})
expect := "SERVICE_NAME=svc\n" +
"this is a secret\n" +
"this is a config\n"
assertAttachedStream(t, attach, expect)
attach = swarm.ExecTask(t, d, task, types.ExecConfig{
Cmd: []string{"mount"},
AttachStdout: true,
AttachStderr: true,
})
assertAttachedStream(t, attach, "tmpfs on /run/secrets/templated_secret type tmpfs")
}
func assertAttachedStream(t *testing.T, attach types.HijackedResponse, expect string) {
buf := bytes.NewBuffer(nil)
_, err := stdcopy.StdCopy(buf, buf, attach.Reader)
require.NoError(t, err)
assert.Contains(t, buf.String(), expect)
}
func waitAndAssert(t *testing.T, timeout time.Duration, f func(*testing.T) bool) {
t.Helper()
after := time.After(timeout)
for {
select {
case <-after:
t.Fatalf("timed out waiting for condition")
default:
}
if f(t) {
return
}
time.Sleep(100 * time.Millisecond)
}
}

View File

@ -17,7 +17,7 @@ const (
type CompatPlugin interface {
Client() *plugins.Client
Name() string
BasePath() string
ScopedPath(string) string
IsV1() bool
}

View File

@ -2,8 +2,8 @@
package plugins // import "github.com/docker/docker/pkg/plugins"
// BasePath returns the path to which all paths returned by the plugin are relative to.
// For v1 plugins, this always returns the host's root directory.
func (p *Plugin) BasePath() string {
return "/"
// ScopedPath returns the path scoped to the plugin's rootfs.
// For v1 plugins, this always returns the path unchanged as v1 plugins run directly on the host.
func (p *Plugin) ScopedPath(s string) string {
return s
}

View File

@ -1,8 +1,7 @@
package plugins // import "github.com/docker/docker/pkg/plugins"
// BasePath returns the path to which all paths returned by the plugin are relative to.
// For Windows v1 plugins, this returns an empty string, since the plugin is already aware
// of the absolute path of the mount.
func (p *Plugin) BasePath() string {
return ""
// ScopedPath returns the path scoped to the plugin's rootfs.
// For v1 plugins, this always returns the path unchanged as v1 plugins run directly on the host.
func (p *Plugin) ScopedPath(s string) string {
return s
}

View File

@ -5,12 +5,14 @@ import (
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/plugin/v2"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// Store manages the plugin inventory in memory and on-disk
type Store struct {
sync.RWMutex
plugins map[string]*v2.Plugin
plugins map[string]*v2.Plugin
specOpts map[string][]SpecOpt
/* handlers are necessary for transition path of legacy plugins
* to the new model. Legacy plugins use Handle() for registering an
* activation callback.*/
@ -21,10 +23,14 @@ type Store struct {
func NewStore() *Store {
return &Store{
plugins: make(map[string]*v2.Plugin),
specOpts: make(map[string][]SpecOpt),
handlers: make(map[string][]func(string, *plugins.Client)),
}
}
// SpecOpt is used for subsystems that need to modify the runtime spec of a plugin
type SpecOpt func(*specs.Spec)
// CreateOpt is used to configure specific plugin details when created
type CreateOpt func(p *v2.Plugin)
@ -35,3 +41,10 @@ func WithSwarmService(id string) CreateOpt {
p.SwarmServiceID = id
}
}
// WithSpecMounts is a SpecOpt which appends the provided mounts to the runtime spec
func WithSpecMounts(mounts []specs.Mount) SpecOpt {
return func(s *specs.Spec) {
s.Mounts = append(s.Mounts, mounts...)
}
}

View File

@ -112,11 +112,6 @@ func NewManager(config ManagerConfig) (*Manager, error) {
return nil, errors.Wrapf(err, "failed to mkdir %v", dirName)
}
}
if err := setupRoot(manager.config.Root); err != nil {
return nil, err
}
var err error
manager.executor, err = config.CreateExecutor(manager)
if err != nil {
@ -151,16 +146,6 @@ func (pm *Manager) HandleExitEvent(id string) error {
os.RemoveAll(filepath.Join(pm.config.ExecRoot, id))
if p.PropagatedMount != "" {
if err := mount.Unmount(p.PropagatedMount); err != nil {
logrus.Warnf("Could not unmount %s: %v", p.PropagatedMount, err)
}
propRoot := filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
if err := mount.Unmount(propRoot); err != nil {
logrus.Warn("Could not unmount %s: %v", propRoot, err)
}
}
pm.mu.RLock()
c := pm.cMap[p]
if c.exitChan != nil {
@ -171,6 +156,10 @@ func (pm *Manager) HandleExitEvent(id string) error {
if restart {
pm.enable(p, c, true)
} else {
if err := mount.RecursiveUnmount(filepath.Join(pm.config.Root, id)); err != nil {
return errors.Wrap(err, "error cleaning up plugin mounts")
}
}
return nil
}
@ -239,28 +228,17 @@ func (pm *Manager) reload() error { // todo: restore
// check if we need to migrate an older propagated mount from before
// these mounts were stored outside the plugin rootfs
if _, err := os.Stat(propRoot); os.IsNotExist(err) {
if _, err := os.Stat(p.PropagatedMount); err == nil {
// make sure nothing is mounted here
// don't care about errors
mount.Unmount(p.PropagatedMount)
if err := os.Rename(p.PropagatedMount, propRoot); err != nil {
rootfsProp := filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount)
if _, err := os.Stat(rootfsProp); err == nil {
if err := os.Rename(rootfsProp, propRoot); err != nil {
logrus.WithError(err).WithField("dir", propRoot).Error("error migrating propagated mount storage")
}
if err := os.MkdirAll(p.PropagatedMount, 0755); err != nil {
logrus.WithError(err).WithField("dir", p.PropagatedMount).Error("error migrating propagated mount storage")
}
}
}
if err := os.MkdirAll(propRoot, 0755); err != nil {
logrus.Errorf("failed to create PropagatedMount directory at %s: %v", propRoot, err)
}
// TODO: sanitize PropagatedMount and prevent breakout
p.PropagatedMount = filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount)
if err := os.MkdirAll(p.PropagatedMount, 0755); err != nil {
logrus.Errorf("failed to create PropagatedMount directory at %s: %v", p.PropagatedMount, err)
return
}
}
}
}

View File

@ -22,7 +22,7 @@ import (
"golang.org/x/sys/unix"
)
func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) (err error) {
func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
p.Rootfs = filepath.Join(pm.config.Root, p.PluginObj.ID, "rootfs")
if p.IsEnabled() && !force {
return errors.Wrap(enabledError(p.Name()), "plugin already enabled")
@ -40,20 +40,16 @@ func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) (err error) {
pm.mu.Unlock()
var propRoot string
if p.PropagatedMount != "" {
if p.PluginObj.Config.PropagatedMount != "" {
propRoot = filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
if err = os.MkdirAll(propRoot, 0755); err != nil {
if err := os.MkdirAll(propRoot, 0755); err != nil {
logrus.Errorf("failed to create PropagatedMount directory at %s: %v", propRoot, err)
}
if err = mount.MakeRShared(propRoot); err != nil {
if err := mount.MakeRShared(propRoot); err != nil {
return errors.Wrap(err, "error setting up propagated mount dir")
}
if err = mount.Mount(propRoot, p.PropagatedMount, "none", "rbind"); err != nil {
return errors.Wrap(err, "error creating mount for propagated mount")
}
}
rootFS := containerfs.NewLocalContainerFS(filepath.Join(pm.config.Root, p.PluginObj.ID, rootFSFileName))
@ -63,16 +59,12 @@ func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) (err error) {
stdout, stderr := makeLoggerStreams(p.GetID())
if err := pm.executor.Create(p.GetID(), *spec, stdout, stderr); err != nil {
if p.PropagatedMount != "" {
if err := mount.Unmount(p.PropagatedMount); err != nil {
logrus.Warnf("Could not unmount %s: %v", p.PropagatedMount, err)
}
if p.PluginObj.Config.PropagatedMount != "" {
if err := mount.Unmount(propRoot); err != nil {
logrus.Warnf("Could not unmount %s: %v", propRoot, err)
}
}
}
return pm.pluginPostStart(p, c)
}
@ -167,13 +159,6 @@ func shutdownPlugin(p *v2.Plugin, c *controller, executor Executor) {
}
}
func setupRoot(root string) error {
if err := mount.MakePrivate(root); err != nil {
return errors.Wrap(err, "error setting plugin manager root to private")
}
return nil
}
func (pm *Manager) disable(p *v2.Plugin, c *controller) error {
if !p.IsEnabled() {
return errors.Wrap(errDisabled(p.Name()), "plugin is already disabled")
@ -202,7 +187,9 @@ func (pm *Manager) Shutdown() {
shutdownPlugin(p, c, pm.executor)
}
}
mount.Unmount(pm.config.Root)
if err := mount.RecursiveUnmount(pm.config.Root); err != nil {
logrus.WithError(err).Warn("error cleaning up plugin mounts")
}
}
func (pm *Manager) upgradePlugin(p *v2.Plugin, configDigest digest.Digest, blobsums []digest.Digest, tmpRootFSDir string, privileges *types.PluginPrivileges) (err error) {

View File

@ -26,5 +26,3 @@ func (pm *Manager) restore(p *v2.Plugin) error {
// Shutdown plugins
func (pm *Manager) Shutdown() {
}
func setupRoot(root string) error { return nil }

View File

@ -9,6 +9,7 @@ import (
"github.com/docker/docker/pkg/plugingetter"
"github.com/docker/docker/pkg/plugins"
"github.com/docker/docker/plugin/v2"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@ -64,6 +65,10 @@ func (ps *Store) GetAll() map[string]*v2.Plugin {
func (ps *Store) SetAll(plugins map[string]*v2.Plugin) {
ps.Lock()
defer ps.Unlock()
for _, p := range plugins {
ps.setSpecOpts(p)
}
ps.plugins = plugins
}
@ -90,6 +95,22 @@ func (ps *Store) SetState(p *v2.Plugin, state bool) {
p.PluginObj.Enabled = state
}
func (ps *Store) setSpecOpts(p *v2.Plugin) {
var specOpts []SpecOpt
for _, typ := range p.GetTypes() {
opts, ok := ps.specOpts[typ.String()]
if ok {
specOpts = append(specOpts, opts...)
}
}
p.SetSpecOptModifier(func(s *specs.Spec) {
for _, o := range specOpts {
o(s)
}
})
}
// Add adds a plugin to memory and plugindb.
// An error will be returned if there is a collision.
func (ps *Store) Add(p *v2.Plugin) error {
@ -99,6 +120,9 @@ func (ps *Store) Add(p *v2.Plugin) error {
if v, exist := ps.plugins[p.GetID()]; exist {
return fmt.Errorf("plugin %q has the same ID %s as %q", p.Name(), p.GetID(), v.Name())
}
ps.setSpecOpts(p)
ps.plugins[p.GetID()] = p
return nil
}
@ -182,20 +206,24 @@ func (ps *Store) GetAllByCap(capability string) ([]plugingetter.CompatPlugin, er
return result, nil
}
func pluginType(cap string) string {
return fmt.Sprintf("docker.%s/%s", strings.ToLower(cap), defaultAPIVersion)
}
// Handle sets a callback for a given capability. It is only used by network
// and ipam drivers during plugin registration. The callback registers the
// driver with the subsystem (network, ipam).
func (ps *Store) Handle(capability string, callback func(string, *plugins.Client)) {
pluginType := fmt.Sprintf("docker.%s/%s", strings.ToLower(capability), defaultAPIVersion)
typ := pluginType(capability)
// Register callback with new plugin model.
ps.Lock()
handlers, ok := ps.handlers[pluginType]
handlers, ok := ps.handlers[typ]
if !ok {
handlers = []func(string, *plugins.Client){}
}
handlers = append(handlers, callback)
ps.handlers[pluginType] = handlers
ps.handlers[typ] = handlers
ps.Unlock()
// Register callback with legacy plugin model.
@ -204,6 +232,15 @@ func (ps *Store) Handle(capability string, callback func(string, *plugins.Client
}
}
// RegisterRuntimeOpt stores a list of SpecOpts for the provided capability.
// These options are applied to the runtime spec before a plugin is started for the specified capability.
func (ps *Store) RegisterRuntimeOpt(cap string, opts ...SpecOpt) {
ps.Lock()
defer ps.Unlock()
typ := pluginType(cap)
ps.specOpts[typ] = append(ps.specOpts[typ], opts...)
}
// CallHandler calls the registered callback. It is invoked during plugin enable.
func (ps *Store) CallHandler(p *v2.Plugin) {
for _, typ := range p.GetTypes() {

View File

@ -2,6 +2,7 @@ package v2 // import "github.com/docker/docker/plugin/v2"
import (
"fmt"
"path/filepath"
"strings"
"sync"
@ -9,20 +10,22 @@ import (
"github.com/docker/docker/pkg/plugingetter"
"github.com/docker/docker/pkg/plugins"
"github.com/opencontainers/go-digest"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// Plugin represents an individual plugin.
type Plugin struct {
mu sync.RWMutex
PluginObj types.Plugin `json:"plugin"` // todo: embed struct
pClient *plugins.Client
refCount int
PropagatedMount string // TODO: make private
Rootfs string // TODO: make private
mu sync.RWMutex
PluginObj types.Plugin `json:"plugin"` // todo: embed struct
pClient *plugins.Client
refCount int
Rootfs string // TODO: make private
Config digest.Digest
Blobsums []digest.Digest
modifyRuntimeSpec func(*specs.Spec)
SwarmServiceID string
}
@ -37,10 +40,13 @@ func (e ErrInadequateCapability) Error() string {
return fmt.Sprintf("plugin does not provide %q capability", e.cap)
}
// BasePath returns the path to which all paths returned by the plugin are relative to.
// For Plugin objects this returns the host path of the plugin container's rootfs.
func (p *Plugin) BasePath() string {
return p.Rootfs
// ScopedPath returns the path scoped to the plugin rootfs
func (p *Plugin) ScopedPath(s string) string {
if p.PluginObj.Config.PropagatedMount != "" && strings.HasPrefix(s, p.PluginObj.Config.PropagatedMount) {
// re-scope to the propagated mount path on the host
return filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount", strings.TrimPrefix(s, p.PluginObj.Config.PropagatedMount))
}
return filepath.Join(p.Rootfs, s)
}
// Client returns the plugin client.
@ -250,3 +256,11 @@ func (p *Plugin) Acquire() {
func (p *Plugin) Release() {
p.AddRefCount(plugingetter.Release)
}
// SetSpecOptModifier sets the function to use to modify the the generated
// runtime spec.
func (p *Plugin) SetSpecOptModifier(f func(*specs.Spec)) {
p.mu.Lock()
p.modifyRuntimeSpec = f
p.mu.Unlock()
}

View File

@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"github.com/docker/docker/api/types"
@ -16,6 +17,7 @@ import (
// InitSpec creates an OCI spec from the plugin's config.
func (p *Plugin) InitSpec(execRoot string) (*specs.Spec, error) {
s := oci.DefaultSpec()
s.Root = &specs.Root{
Path: p.Rootfs,
Readonly: false, // TODO: all plugins should be readonly? settable in config?
@ -31,6 +33,17 @@ func (p *Plugin) InitSpec(execRoot string) (*specs.Spec, error) {
return nil, errors.WithStack(err)
}
if p.PluginObj.Config.PropagatedMount != "" {
pRoot := filepath.Join(filepath.Dir(p.Rootfs), "propagated-mount")
s.Mounts = append(s.Mounts, specs.Mount{
Source: pRoot,
Destination: p.PluginObj.Config.PropagatedMount,
Type: "bind",
Options: []string{"rbind", "rw", "rshared"},
})
s.Linux.RootfsPropagation = "rshared"
}
mounts := append(p.PluginObj.Config.Mounts, types.PluginMount{
Source: &execRoot,
Destination: defaultPluginRuntimeDestination,
@ -88,11 +101,6 @@ func (p *Plugin) InitSpec(execRoot string) (*specs.Spec, error) {
}
}
if p.PluginObj.Config.PropagatedMount != "" {
p.PropagatedMount = filepath.Join(p.Rootfs, p.PluginObj.Config.PropagatedMount)
s.Linux.RootfsPropagation = "rshared"
}
if p.PluginObj.Config.Linux.AllowAllDevices {
s.Linux.Resources.Devices = []specs.LinuxDeviceCgroup{{Allow: true, Access: "rwm"}}
}
@ -126,5 +134,13 @@ func (p *Plugin) InitSpec(execRoot string) (*specs.Spec, error) {
caps.Inheritable = append(caps.Inheritable, p.PluginObj.Config.Linux.Capabilities...)
caps.Effective = append(caps.Effective, p.PluginObj.Config.Linux.Capabilities...)
if p.modifyRuntimeSpec != nil {
p.modifyRuntimeSpec(&s)
}
sort.Slice(s.Mounts, func(i, j int) bool {
return s.Mounts[i].Destination < s.Mounts[j].Destination
})
return &s, nil
}

View File

@ -2,7 +2,6 @@ package volumedrivers // import "github.com/docker/docker/volume/drivers"
import (
"errors"
"path/filepath"
"strings"
"time"
@ -16,7 +15,7 @@ var (
type volumeDriverAdapter struct {
name string
baseHostPath string
scopePath func(s string) string
capabilities *volume.Capability
proxy *volumeDriverProxy
}
@ -30,10 +29,10 @@ func (a *volumeDriverAdapter) Create(name string, opts map[string]string) (volum
return nil, err
}
return &volumeAdapter{
proxy: a.proxy,
name: name,
driverName: a.name,
baseHostPath: a.baseHostPath,
proxy: a.proxy,
name: name,
driverName: a.name,
scopePath: a.scopePath,
}, nil
}
@ -41,13 +40,6 @@ func (a *volumeDriverAdapter) Remove(v volume.Volume) error {
return a.proxy.Remove(v.Name())
}
func hostPath(baseHostPath, path string) string {
if baseHostPath != "" {
path = filepath.Join(baseHostPath, path)
}
return path
}
func (a *volumeDriverAdapter) List() ([]volume.Volume, error) {
ls, err := a.proxy.List()
if err != nil {
@ -57,11 +49,11 @@ func (a *volumeDriverAdapter) List() ([]volume.Volume, error) {
var out []volume.Volume
for _, vp := range ls {
out = append(out, &volumeAdapter{
proxy: a.proxy,
name: vp.Name,
baseHostPath: a.baseHostPath,
driverName: a.name,
eMount: hostPath(a.baseHostPath, vp.Mountpoint),
proxy: a.proxy,
name: vp.Name,
scopePath: a.scopePath,
driverName: a.name,
eMount: a.scopePath(vp.Mountpoint),
})
}
return out, nil
@ -79,13 +71,13 @@ func (a *volumeDriverAdapter) Get(name string) (volume.Volume, error) {
}
return &volumeAdapter{
proxy: a.proxy,
name: v.Name,
driverName: a.Name(),
eMount: v.Mountpoint,
createdAt: v.CreatedAt,
status: v.Status,
baseHostPath: a.baseHostPath,
proxy: a.proxy,
name: v.Name,
driverName: a.Name(),
eMount: v.Mountpoint,
createdAt: v.CreatedAt,
status: v.Status,
scopePath: a.scopePath,
}, nil
}
@ -122,13 +114,13 @@ func (a *volumeDriverAdapter) getCapabilities() volume.Capability {
}
type volumeAdapter struct {
proxy *volumeDriverProxy
name string
baseHostPath string
driverName string
eMount string // ephemeral host volume path
createdAt time.Time // time the directory was created
status map[string]interface{}
proxy *volumeDriverProxy
name string
scopePath func(string) string
driverName string
eMount string // ephemeral host volume path
createdAt time.Time // time the directory was created
status map[string]interface{}
}
type proxyVolume struct {
@ -149,7 +141,7 @@ func (a *volumeAdapter) DriverName() string {
func (a *volumeAdapter) Path() string {
if len(a.eMount) == 0 {
mountpoint, _ := a.proxy.Path(a.name)
a.eMount = hostPath(a.baseHostPath, mountpoint)
a.eMount = a.scopePath(mountpoint)
}
return a.eMount
}
@ -160,7 +152,7 @@ func (a *volumeAdapter) CachedPath() string {
func (a *volumeAdapter) Mount(id string) (string, error) {
mountpoint, err := a.proxy.Mount(a.name, id)
a.eMount = hostPath(a.baseHostPath, mountpoint)
a.eMount = a.scopePath(mountpoint)
return a.eMount, err
}

View File

@ -25,9 +25,9 @@ var drivers = &driverExtpoint{
const extName = "VolumeDriver"
// NewVolumeDriver returns a driver has the given name mapped on the given client.
func NewVolumeDriver(name string, baseHostPath string, c client) volume.Driver {
func NewVolumeDriver(name string, scopePath func(string) string, c client) volume.Driver {
proxy := &volumeDriverProxy{c}
return &volumeDriverAdapter{name: name, baseHostPath: baseHostPath, proxy: proxy}
return &volumeDriverAdapter{name: name, scopePath: scopePath, proxy: proxy}
}
// volumeDriver defines the available functions that volume plugins must implement.
@ -129,7 +129,7 @@ func lookup(name string, mode int) (volume.Driver, error) {
return nil, errors.Wrap(err, "error looking up volume plugin "+name)
}
d := NewVolumeDriver(p.Name(), p.BasePath(), p.Client())
d := NewVolumeDriver(p.Name(), p.ScopedPath, p.Client())
if err := validateDriver(d); err != nil {
if mode > 0 {
// Undo any reference count changes from the initial `Get`
@ -224,7 +224,7 @@ func GetAllDrivers() ([]volume.Driver, error) {
continue
}
ext := NewVolumeDriver(name, p.BasePath(), p.Client())
ext := NewVolumeDriver(name, p.ScopedPath, p.Client())
if p.IsV1() {
drivers.extensions[name] = ext
}

View File

@ -178,8 +178,8 @@ func (p *fakePlugin) IsV1() bool {
return false
}
func (p *fakePlugin) BasePath() string {
return ""
func (p *fakePlugin) ScopedPath(s string) string {
return s
}
type fakePluginGetter struct {