plugin.go 6.8 KB

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