gopkg.in/docker/docker.v20@v20.10.27/testutil/fixtures/plugin/plugin.go (about)

     1  package plugin // import "github.com/docker/docker/testutil/fixtures/plugin"
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"time"
    11  
    12  	"github.com/docker/docker/api/types"
    13  	"github.com/docker/docker/pkg/archive"
    14  	"github.com/docker/docker/plugin"
    15  	"github.com/docker/docker/registry"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  // CreateOpt is passed used to change the default plugin config before
    20  // creating it
    21  type CreateOpt func(*Config)
    22  
    23  // Config wraps types.PluginConfig to provide some extra state for options
    24  // extra customizations on the plugin details, such as using a custom binary to
    25  // create the plugin with.
    26  type Config struct {
    27  	*types.PluginConfig
    28  	binPath        string
    29  	RegistryConfig registry.ServiceOptions
    30  }
    31  
    32  // WithInsecureRegistry specifies that the given registry can skip host-key checking as well as fall back to plain http
    33  func WithInsecureRegistry(url string) CreateOpt {
    34  	return func(cfg *Config) {
    35  		cfg.RegistryConfig.InsecureRegistries = append(cfg.RegistryConfig.InsecureRegistries, url)
    36  	}
    37  }
    38  
    39  // WithBinary is a CreateOpt to set an custom binary to create the plugin with.
    40  // This binary must be statically compiled.
    41  func WithBinary(bin string) CreateOpt {
    42  	return func(cfg *Config) {
    43  		cfg.binPath = bin
    44  	}
    45  }
    46  
    47  // CreateClient is the interface used for `BuildPlugin` to interact with the
    48  // daemon.
    49  type CreateClient interface {
    50  	PluginCreate(context.Context, io.Reader, types.PluginCreateOptions) error
    51  }
    52  
    53  // Create creates a new plugin with the specified name
    54  func Create(ctx context.Context, c CreateClient, name string, opts ...CreateOpt) error {
    55  	tmpDir, err := os.MkdirTemp("", "create-test-plugin")
    56  	if err != nil {
    57  		return err
    58  	}
    59  	defer os.RemoveAll(tmpDir)
    60  
    61  	tar, err := makePluginBundle(tmpDir, opts...)
    62  	if err != nil {
    63  		return err
    64  	}
    65  	defer tar.Close()
    66  
    67  	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    68  	defer cancel()
    69  
    70  	return c.PluginCreate(ctx, tar, types.PluginCreateOptions{RepoName: name})
    71  }
    72  
    73  // CreateInRegistry makes a plugin (locally) and pushes it to a registry.
    74  // This does not use a dockerd instance to create or push the plugin.
    75  // If you just want to create a plugin in some daemon, use `Create`.
    76  //
    77  // This can be useful when testing plugins on swarm where you don't really want
    78  // the plugin to exist on any of the daemons (immediately) and there needs to be
    79  // some way to distribute the plugin.
    80  func CreateInRegistry(ctx context.Context, repo string, auth *types.AuthConfig, opts ...CreateOpt) error {
    81  	tmpDir, err := os.MkdirTemp("", "create-test-plugin-local")
    82  	if err != nil {
    83  		return err
    84  	}
    85  	defer os.RemoveAll(tmpDir)
    86  
    87  	inPath := filepath.Join(tmpDir, "plugin")
    88  	if err := os.MkdirAll(inPath, 0755); err != nil {
    89  		return errors.Wrap(err, "error creating plugin root")
    90  	}
    91  
    92  	var cfg Config
    93  	cfg.PluginConfig = &types.PluginConfig{}
    94  	for _, o := range opts {
    95  		o(&cfg)
    96  	}
    97  
    98  	tar, err := makePluginBundle(inPath, opts...)
    99  	if err != nil {
   100  		return err
   101  	}
   102  	defer tar.Close()
   103  
   104  	dummyExec := func(m *plugin.Manager) (plugin.Executor, error) {
   105  		return nil, nil
   106  	}
   107  
   108  	regService, err := registry.NewService(cfg.RegistryConfig)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	managerConfig := plugin.ManagerConfig{
   114  		Store:           plugin.NewStore(),
   115  		RegistryService: regService,
   116  		Root:            filepath.Join(tmpDir, "root"),
   117  		ExecRoot:        "/run/docker", // manager init fails if not set
   118  		CreateExecutor:  dummyExec,
   119  		LogPluginEvent:  func(id, name, action string) {}, // panics when not set
   120  	}
   121  	manager, err := plugin.NewManager(managerConfig)
   122  	if err != nil {
   123  		return errors.Wrap(err, "error creating plugin manager")
   124  	}
   125  
   126  	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
   127  	defer cancel()
   128  	if err := manager.CreateFromContext(ctx, tar, &types.PluginCreateOptions{RepoName: repo}); err != nil {
   129  		return err
   130  	}
   131  
   132  	if auth == nil {
   133  		auth = &types.AuthConfig{}
   134  	}
   135  	err = manager.Push(ctx, repo, nil, auth, io.Discard)
   136  	return errors.Wrap(err, "error pushing plugin")
   137  }
   138  
   139  func makePluginBundle(inPath string, opts ...CreateOpt) (io.ReadCloser, error) {
   140  	p := &types.PluginConfig{
   141  		Interface: types.PluginConfigInterface{
   142  			Socket: "basic.sock",
   143  			Types:  []types.PluginInterfaceType{{Capability: "docker.dummy/1.0"}},
   144  		},
   145  		Entrypoint: []string{"/basic"},
   146  	}
   147  	cfg := &Config{
   148  		PluginConfig: p,
   149  	}
   150  	for _, o := range opts {
   151  		o(cfg)
   152  	}
   153  	if cfg.binPath == "" {
   154  		binPath, err := ensureBasicPluginBin()
   155  		if err != nil {
   156  			return nil, err
   157  		}
   158  		cfg.binPath = binPath
   159  	}
   160  
   161  	configJSON, err := json.Marshal(p)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	if err := os.WriteFile(filepath.Join(inPath, "config.json"), configJSON, 0644); err != nil {
   166  		return nil, err
   167  	}
   168  	if err := os.MkdirAll(filepath.Join(inPath, "rootfs", filepath.Dir(p.Entrypoint[0])), 0755); err != nil {
   169  		return nil, errors.Wrap(err, "error creating plugin rootfs dir")
   170  	}
   171  
   172  	// Ensure the mount target paths exist
   173  	for _, m := range p.Mounts {
   174  		var stat os.FileInfo
   175  		if m.Source != nil {
   176  			stat, err = os.Stat(*m.Source)
   177  			if err != nil && !os.IsNotExist(err) {
   178  				return nil, err
   179  			}
   180  		}
   181  
   182  		if stat == nil || stat.IsDir() {
   183  			var mode os.FileMode = 0755
   184  			if stat != nil {
   185  				mode = stat.Mode()
   186  			}
   187  			if err := os.MkdirAll(filepath.Join(inPath, "rootfs", m.Destination), mode); err != nil {
   188  				return nil, errors.Wrap(err, "error preparing plugin mount destination path")
   189  			}
   190  		} else {
   191  			if err := os.MkdirAll(filepath.Join(inPath, "rootfs", filepath.Dir(m.Destination)), 0755); err != nil {
   192  				return nil, errors.Wrap(err, "error preparing plugin mount destination dir")
   193  			}
   194  			f, err := os.Create(filepath.Join(inPath, "rootfs", m.Destination))
   195  			if err != nil && !os.IsExist(err) {
   196  				return nil, errors.Wrap(err, "error preparing plugin mount destination file")
   197  			}
   198  			if f != nil {
   199  				f.Close()
   200  			}
   201  		}
   202  	}
   203  	if err := archive.NewDefaultArchiver().CopyFileWithTar(cfg.binPath, filepath.Join(inPath, "rootfs", p.Entrypoint[0])); err != nil {
   204  		return nil, errors.Wrap(err, "error copying plugin binary to rootfs path")
   205  	}
   206  	tar, err := archive.Tar(inPath, archive.Uncompressed)
   207  	return tar, errors.Wrap(err, "error making plugin archive")
   208  }
   209  
   210  func ensureBasicPluginBin() (string, error) {
   211  	name := "docker-basic-plugin"
   212  	p, err := exec.LookPath(name)
   213  	if err == nil {
   214  		return p, nil
   215  	}
   216  
   217  	goBin, err := exec.LookPath("go")
   218  	if err != nil {
   219  		return "", err
   220  	}
   221  	installPath := filepath.Join(os.Getenv("GOPATH"), "bin", name)
   222  	sourcePath := filepath.Join("github.com", "docker", "docker", "testutil", "fixtures", "plugin", "basic")
   223  	cmd := exec.Command(goBin, "build", "-o", installPath, sourcePath)
   224  	cmd.Env = append(os.Environ(), "CGO_ENABLED=0", "GO111MODULE=off")
   225  	if out, err := cmd.CombinedOutput(); err != nil {
   226  		return "", errors.Wrapf(err, "error building basic plugin bin: %s", string(out))
   227  	}
   228  	return installPath, nil
   229  }