github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/integration-cli/docker_cli_plugins_test.go (about) 1 package main 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "os" 9 "path" 10 "path/filepath" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/docker/docker/api/types" 16 "github.com/docker/docker/integration-cli/cli" 17 "github.com/docker/docker/integration-cli/daemon" 18 "github.com/docker/docker/testutil" 19 "github.com/docker/docker/testutil/fixtures/plugin" 20 "gotest.tools/v3/assert" 21 is "gotest.tools/v3/assert/cmp" 22 "gotest.tools/v3/skip" 23 ) 24 25 const ( 26 pluginProcessName = "sample-volume-plugin" 27 pName = "tiborvass/sample-volume-plugin" 28 npName = "tiborvass/test-docker-netplugin" 29 pTag = "latest" 30 pNameWithTag = pName + ":" + pTag 31 npNameWithTag = npName + ":" + pTag 32 ) 33 34 type DockerCLIPluginsSuite struct { 35 ds *DockerSuite 36 } 37 38 func (s *DockerCLIPluginsSuite) TearDownTest(ctx context.Context, c *testing.T) { 39 s.ds.TearDownTest(ctx, c) 40 } 41 42 func (s *DockerCLIPluginsSuite) OnTimeout(c *testing.T) { 43 s.ds.OnTimeout(c) 44 } 45 46 func (ps *DockerPluginSuite) TestPluginBasicOps(c *testing.T) { 47 pluginName := ps.getPluginRepoWithTag() 48 _, _, err := dockerCmdWithError("plugin", "install", "--grant-all-permissions", pluginName) 49 assert.NilError(c, err) 50 51 out, _, err := dockerCmdWithError("plugin", "ls") 52 assert.NilError(c, err) 53 assert.Check(c, is.Contains(out, pluginName)) 54 assert.Check(c, is.Contains(out, "true")) 55 id, _, err := dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", pluginName) 56 id = strings.TrimSpace(id) 57 assert.NilError(c, err) 58 59 out, _, err = dockerCmdWithError("plugin", "remove", pluginName) 60 assert.ErrorContains(c, err, "") 61 assert.Check(c, is.Contains(out, "is enabled")) 62 _, _, err = dockerCmdWithError("plugin", "disable", pluginName) 63 assert.NilError(c, err) 64 65 out, _, err = dockerCmdWithError("plugin", "remove", pluginName) 66 assert.NilError(c, err) 67 assert.Check(c, is.Contains(out, pluginName)) 68 _, err = os.Stat(filepath.Join(testEnv.DaemonInfo.DockerRootDir, "plugins", id)) 69 if !os.IsNotExist(err) { 70 c.Fatal(err) 71 } 72 } 73 74 func (ps *DockerPluginSuite) TestPluginForceRemove(c *testing.T) { 75 pluginName := ps.getPluginRepoWithTag() 76 77 _, _, err := dockerCmdWithError("plugin", "install", "--grant-all-permissions", pluginName) 78 assert.NilError(c, err) 79 80 out, _, _ := dockerCmdWithError("plugin", "remove", pluginName) 81 assert.Check(c, is.Contains(out, "is enabled")) 82 out, _, err = dockerCmdWithError("plugin", "remove", "--force", pluginName) 83 assert.NilError(c, err) 84 assert.Check(c, is.Contains(out, pluginName)) 85 } 86 87 func (s *DockerCLIPluginsSuite) TestPluginActive(c *testing.T) { 88 testRequires(c, DaemonIsLinux, IsAmd64, Network) 89 90 _, _, err := dockerCmdWithError("plugin", "install", "--grant-all-permissions", pNameWithTag) 91 assert.NilError(c, err) 92 93 _, _, err = dockerCmdWithError("volume", "create", "-d", pNameWithTag, "--name", "testvol1") 94 assert.NilError(c, err) 95 96 out, _, _ := dockerCmdWithError("plugin", "disable", pNameWithTag) 97 assert.Check(c, is.Contains(out, "in use")) 98 _, _, err = dockerCmdWithError("volume", "rm", "testvol1") 99 assert.NilError(c, err) 100 101 _, _, err = dockerCmdWithError("plugin", "disable", pNameWithTag) 102 assert.NilError(c, err) 103 104 out, _, err = dockerCmdWithError("plugin", "remove", pNameWithTag) 105 assert.NilError(c, err) 106 assert.Check(c, is.Contains(out, pNameWithTag)) 107 } 108 109 func (s *DockerCLIPluginsSuite) TestPluginActiveNetwork(c *testing.T) { 110 testRequires(c, DaemonIsLinux, IsAmd64, Network) 111 _, _, err := dockerCmdWithError("plugin", "install", "--grant-all-permissions", npNameWithTag) 112 assert.NilError(c, err) 113 114 out, _, err := dockerCmdWithError("network", "create", "-d", npNameWithTag, "test") 115 assert.NilError(c, err) 116 117 nID := strings.TrimSpace(out) 118 119 out, _, _ = dockerCmdWithError("plugin", "remove", npNameWithTag) 120 assert.Check(c, is.Contains(out, "is in use")) 121 _, _, err = dockerCmdWithError("network", "rm", nID) 122 assert.NilError(c, err) 123 124 out, _, _ = dockerCmdWithError("plugin", "remove", npNameWithTag) 125 assert.Check(c, is.Contains(out, "is enabled")) 126 _, _, err = dockerCmdWithError("plugin", "disable", npNameWithTag) 127 assert.NilError(c, err) 128 129 out, _, err = dockerCmdWithError("plugin", "remove", npNameWithTag) 130 assert.NilError(c, err) 131 assert.Check(c, is.Contains(out, npNameWithTag)) 132 } 133 134 func (ps *DockerPluginSuite) TestPluginInstallDisable(c *testing.T) { 135 pluginName := ps.getPluginRepoWithTag() 136 137 out, _, err := dockerCmdWithError("plugin", "install", "--grant-all-permissions", "--disable", pluginName) 138 assert.NilError(c, err) 139 assert.Check(c, is.Contains(out, pluginName)) 140 out, _, err = dockerCmdWithError("plugin", "ls") 141 assert.NilError(c, err) 142 assert.Check(c, is.Contains(out, "false")) 143 out, _, err = dockerCmdWithError("plugin", "enable", pluginName) 144 assert.NilError(c, err) 145 assert.Check(c, is.Contains(out, pluginName)) 146 out, _, err = dockerCmdWithError("plugin", "disable", pluginName) 147 assert.NilError(c, err) 148 assert.Check(c, is.Contains(out, pluginName)) 149 out, _, err = dockerCmdWithError("plugin", "remove", pluginName) 150 assert.NilError(c, err) 151 assert.Check(c, is.Contains(out, pluginName)) 152 } 153 154 func (s *DockerCLIPluginsSuite) TestPluginInstallDisableVolumeLs(c *testing.T) { 155 testRequires(c, DaemonIsLinux, IsAmd64, Network) 156 out, _, err := dockerCmdWithError("plugin", "install", "--grant-all-permissions", "--disable", pName) 157 assert.NilError(c, err) 158 assert.Check(c, is.Contains(out, pName)) 159 cli.DockerCmd(c, "volume", "ls") 160 } 161 162 func (ps *DockerPluginSuite) TestPluginSet(c *testing.T) { 163 client := testEnv.APIClient() 164 165 name := "test" 166 ctx, cancel := context.WithTimeout(testutil.GetContext(c), 60*time.Second) 167 defer cancel() 168 169 initialValue := "0" 170 mntSrc := "foo" 171 devPath := "/dev/bar" 172 173 // Create a new plugin with extra settings 174 err := plugin.Create(ctx, client, name, func(cfg *plugin.Config) { 175 cfg.Env = []types.PluginEnv{{Name: "DEBUG", Value: &initialValue, Settable: []string{"value"}}} 176 cfg.Mounts = []types.PluginMount{ 177 {Name: "pmount1", Settable: []string{"source"}, Type: "none", Source: &mntSrc}, 178 {Name: "pmount2", Settable: []string{"source"}, Type: "none"}, // Mount without source is invalid. 179 } 180 cfg.Linux.Devices = []types.PluginDevice{ 181 {Name: "pdev1", Path: &devPath, Settable: []string{"path"}}, 182 {Name: "pdev2", Settable: []string{"path"}}, // Device without Path is invalid. 183 } 184 }) 185 assert.Assert(c, err == nil, "failed to create test plugin") 186 187 env := cli.DockerCmd(c, "plugin", "inspect", "-f", "{{.Settings.Env}}", name).Stdout() 188 assert.Check(c, is.Equal(strings.TrimSpace(env), "[DEBUG=0]")) 189 190 cli.DockerCmd(c, "plugin", "set", name, "DEBUG=1") 191 192 env = cli.DockerCmd(c, "plugin", "inspect", "-f", "{{.Settings.Env}}", name).Stdout() 193 assert.Check(c, is.Equal(strings.TrimSpace(env), "[DEBUG=1]")) 194 195 mounts := cli.DockerCmd(c, "plugin", "inspect", "-f", "{{with $mount := index .Settings.Mounts 0}}{{$mount.Source}}{{end}}", name).Stdout() 196 assert.Check(c, is.Contains(mounts, mntSrc)) 197 cli.DockerCmd(c, "plugin", "set", name, "pmount1.source=bar") 198 199 mounts = cli.DockerCmd(c, "plugin", "inspect", "-f", "{{with $mount := index .Settings.Mounts 0}}{{$mount.Source}}{{end}}", name).Stdout() 200 assert.Check(c, is.Contains(mounts, "bar")) 201 out, _, err := dockerCmdWithError("plugin", "set", name, "pmount2.source=bar2") 202 assert.ErrorContains(c, err, "") 203 assert.Check(c, is.Contains(out, "Plugin config has no mount source")) 204 out, _, err = dockerCmdWithError("plugin", "set", name, "pdev2.path=/dev/bar2") 205 assert.ErrorContains(c, err, "") 206 assert.Check(c, is.Contains(out, "Plugin config has no device path")) 207 } 208 209 func (ps *DockerPluginSuite) TestPluginInstallArgs(c *testing.T) { 210 pluginName := path.Join(ps.registryHost(), "plugin", "testplugininstallwithargs") 211 ctx, cancel := context.WithTimeout(testutil.GetContext(c), 60*time.Second) 212 defer cancel() 213 214 plugin.CreateInRegistry(ctx, pluginName, nil, func(cfg *plugin.Config) { 215 cfg.Env = []types.PluginEnv{{Name: "DEBUG", Settable: []string{"value"}}} 216 }) 217 218 out := cli.DockerCmd(c, "plugin", "install", "--grant-all-permissions", "--disable", pluginName, "DEBUG=1").Stdout() 219 assert.Check(c, is.Contains(out, pluginName)) 220 env := cli.DockerCmd(c, "plugin", "inspect", "-f", "{{.Settings.Env}}", pluginName).Stdout() 221 assert.Check(c, is.Equal(strings.TrimSpace(env), "[DEBUG=1]")) 222 } 223 224 func (ps *DockerPluginSuite) TestPluginInstallImage(c *testing.T) { 225 testRequires(c, IsAmd64) 226 skip.If(c, GitHubActions, "FIXME: https://github.com/moby/moby/issues/43996") 227 228 const imgRepo = privateRegistryURL + "/dockercli/busybox" 229 // tag the image to upload it to the private registry 230 cli.DockerCmd(c, "tag", "busybox", imgRepo) 231 // push the image to the registry 232 cli.DockerCmd(c, "push", imgRepo) 233 234 out, _, err := dockerCmdWithError("plugin", "install", imgRepo) 235 assert.ErrorContains(c, err, "") 236 assert.Check(c, is.Contains(out, `Encountered remote "application/vnd.docker.container.image.v1+json"(image) when fetching`)) 237 } 238 239 func (ps *DockerPluginSuite) TestPluginEnableDisableNegative(c *testing.T) { 240 pluginName := ps.getPluginRepoWithTag() 241 242 out, _, err := dockerCmdWithError("plugin", "install", "--grant-all-permissions", pluginName) 243 assert.NilError(c, err) 244 assert.Check(c, is.Contains(out, pluginName)) 245 out, _, err = dockerCmdWithError("plugin", "enable", pluginName) 246 assert.ErrorContains(c, err, "") 247 assert.Check(c, is.Contains(out, "already enabled")) 248 _, _, err = dockerCmdWithError("plugin", "disable", pluginName) 249 assert.NilError(c, err) 250 251 out, _, err = dockerCmdWithError("plugin", "disable", pluginName) 252 assert.ErrorContains(c, err, "") 253 assert.Check(c, is.Contains(out, "already disabled")) 254 _, _, err = dockerCmdWithError("plugin", "remove", pluginName) 255 assert.NilError(c, err) 256 } 257 258 func (ps *DockerPluginSuite) TestPluginCreate(c *testing.T) { 259 name := "foo/bar-driver" 260 temp, err := os.MkdirTemp("", "foo") 261 assert.NilError(c, err) 262 defer os.RemoveAll(temp) 263 264 data := `{"description": "foo plugin"}` 265 err = os.WriteFile(filepath.Join(temp, "config.json"), []byte(data), 0o644) 266 assert.NilError(c, err) 267 268 err = os.MkdirAll(filepath.Join(temp, "rootfs"), 0o700) 269 assert.NilError(c, err) 270 271 out, _, err := dockerCmdWithError("plugin", "create", name, temp) 272 assert.NilError(c, err) 273 assert.Check(c, is.Contains(out, name)) 274 out, _, err = dockerCmdWithError("plugin", "ls") 275 assert.NilError(c, err) 276 assert.Check(c, is.Contains(out, name)) 277 out, _, err = dockerCmdWithError("plugin", "create", name, temp) 278 assert.ErrorContains(c, err, "") 279 assert.Check(c, is.Contains(out, "already exist")) 280 out, _, err = dockerCmdWithError("plugin", "ls") 281 assert.NilError(c, err) 282 assert.Check(c, is.Contains(out, name)) 283 // The output will consists of one HEADER line and one line of foo/bar-driver 284 assert.Equal(c, len(strings.Split(strings.TrimSpace(out), "\n")), 2) 285 } 286 287 func (ps *DockerPluginSuite) TestPluginInspect(c *testing.T) { 288 pluginName := ps.getPluginRepoWithTag() 289 290 _, _, err := dockerCmdWithError("plugin", "install", "--grant-all-permissions", pluginName) 291 assert.NilError(c, err) 292 293 out, _, err := dockerCmdWithError("plugin", "ls") 294 assert.NilError(c, err) 295 assert.Check(c, is.Contains(out, pluginName)) 296 assert.Check(c, is.Contains(out, "true")) 297 // Find the ID first 298 out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", pluginName) 299 assert.NilError(c, err) 300 id := strings.TrimSpace(out) 301 assert.Assert(c, id != "") 302 303 // Long form 304 out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", id) 305 assert.NilError(c, err) 306 assert.Check(c, is.Equal(strings.TrimSpace(out), id)) 307 308 // Short form 309 out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", id[:5]) 310 assert.NilError(c, err) 311 assert.Check(c, is.Equal(strings.TrimSpace(out), id)) 312 313 // Name with tag form 314 out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", pluginName) 315 assert.NilError(c, err) 316 assert.Check(c, is.Equal(strings.TrimSpace(out), id)) 317 318 // Name without tag form 319 out, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", ps.getPluginRepo()) 320 assert.NilError(c, err) 321 assert.Check(c, is.Equal(strings.TrimSpace(out), id)) 322 323 _, _, err = dockerCmdWithError("plugin", "disable", pluginName) 324 assert.NilError(c, err) 325 326 out, _, err = dockerCmdWithError("plugin", "remove", pluginName) 327 assert.NilError(c, err) 328 assert.Check(c, is.Contains(out, pluginName)) 329 // After remove nothing should be found 330 _, _, err = dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", id[:5]) 331 assert.ErrorContains(c, err, "") 332 } 333 334 // Test case for https://github.com/docker/docker/pull/29186#discussion_r91277345 335 func (s *DockerCLIPluginsSuite) TestPluginInspectOnWindows(c *testing.T) { 336 // This test should work on Windows only 337 testRequires(c, DaemonIsWindows) 338 339 out, _, err := dockerCmdWithError("plugin", "inspect", "foobar") 340 assert.ErrorContains(c, err, "") 341 assert.Check(c, is.Contains(out, "plugins are not supported on this platform")) 342 assert.ErrorContains(c, err, "plugins are not supported on this platform") 343 } 344 345 func (ps *DockerPluginSuite) TestPluginIDPrefix(c *testing.T) { 346 name := "test" 347 client := testEnv.APIClient() 348 349 ctx, cancel := context.WithTimeout(testutil.GetContext(c), 60*time.Second) 350 initialValue := "0" 351 err := plugin.Create(ctx, client, name, func(cfg *plugin.Config) { 352 cfg.Env = []types.PluginEnv{{Name: "DEBUG", Value: &initialValue, Settable: []string{"value"}}} 353 }) 354 cancel() 355 356 assert.Assert(c, err == nil, "failed to create test plugin") 357 358 // Find ID first 359 id, _, err := dockerCmdWithError("plugin", "inspect", "-f", "{{.Id}}", name) 360 id = strings.TrimSpace(id) 361 assert.NilError(c, err) 362 363 // List current state 364 out, _, err := dockerCmdWithError("plugin", "ls") 365 assert.NilError(c, err) 366 assert.Check(c, is.Contains(out, name)) 367 assert.Check(c, is.Contains(out, "false")) 368 env := cli.DockerCmd(c, "plugin", "inspect", "-f", "{{.Settings.Env}}", id[:5]).Stdout() 369 assert.Check(c, is.Equal(strings.TrimSpace(env), "[DEBUG=0]")) 370 371 cli.DockerCmd(c, "plugin", "set", id[:5], "DEBUG=1") 372 373 env = cli.DockerCmd(c, "plugin", "inspect", "-f", "{{.Settings.Env}}", id[:5]).Stdout() 374 assert.Check(c, is.Equal(strings.TrimSpace(env), "[DEBUG=1]")) 375 376 // Enable 377 _, _, err = dockerCmdWithError("plugin", "enable", id[:5]) 378 assert.NilError(c, err) 379 out, _, err = dockerCmdWithError("plugin", "ls") 380 assert.NilError(c, err) 381 assert.Check(c, is.Contains(out, name)) 382 assert.Check(c, is.Contains(out, "true")) 383 // Disable 384 _, _, err = dockerCmdWithError("plugin", "disable", id[:5]) 385 assert.NilError(c, err) 386 out, _, err = dockerCmdWithError("plugin", "ls") 387 assert.NilError(c, err) 388 assert.Check(c, is.Contains(out, name)) 389 assert.Check(c, is.Contains(out, "false")) 390 // Remove 391 _, _, err = dockerCmdWithError("plugin", "remove", id[:5]) 392 assert.NilError(c, err) 393 // List returns none 394 out, _, err = dockerCmdWithError("plugin", "ls") 395 assert.NilError(c, err) 396 assert.Assert(c, !strings.Contains(out, name)) 397 } 398 399 func (ps *DockerPluginSuite) TestPluginListDefaultFormat(c *testing.T) { 400 config, err := os.MkdirTemp("", "config-file-") 401 assert.NilError(c, err) 402 defer os.RemoveAll(config) 403 404 err = os.WriteFile(filepath.Join(config, "config.json"), []byte(`{"pluginsFormat": "raw"}`), 0o644) 405 assert.NilError(c, err) 406 407 name := "test:latest" 408 client := testEnv.APIClient() 409 410 ctx, cancel := context.WithTimeout(testutil.GetContext(c), 60*time.Second) 411 defer cancel() 412 err = plugin.Create(ctx, client, name, func(cfg *plugin.Config) { 413 cfg.Description = "test plugin" 414 }) 415 assert.Assert(c, err == nil, "failed to create test plugin") 416 417 id := cli.DockerCmd(c, "plugin", "inspect", "--format", "{{.ID}}", name).Stdout() 418 id = strings.TrimSpace(id) 419 420 // We expect the format to be in `raw + --no-trunc` 421 expectedOutput := fmt.Sprintf(`plugin_id: %s 422 name: %s 423 description: test plugin 424 enabled: false`, id, name) 425 426 out := cli.DockerCmd(c, "--config", config, "plugin", "ls", "--no-trunc").Combined() 427 assert.Check(c, is.Contains(out, expectedOutput)) 428 } 429 430 func (s *DockerCLIPluginsSuite) TestPluginUpgrade(c *testing.T) { 431 testRequires(c, DaemonIsLinux, Network, testEnv.IsLocalDaemon, IsAmd64, NotUserNamespace) 432 const pluginName = "cpuguy83/docker-volume-driver-plugin-local:latest" 433 const pluginV2 = "cpuguy83/docker-volume-driver-plugin-local:v2" 434 435 cli.DockerCmd(c, "plugin", "install", "--grant-all-permissions", pluginName) 436 cli.DockerCmd(c, "volume", "create", "--driver", pluginName, "bananas") 437 cli.DockerCmd(c, "run", "--rm", "-v", "bananas:/apple", "busybox", "sh", "-c", "touch /apple/core") 438 439 out, _, err := dockerCmdWithError("plugin", "upgrade", "--grant-all-permissions", pluginName, pluginV2) 440 assert.ErrorContains(c, err, "", out) 441 assert.Check(c, is.Contains(out, "disabled before upgrading")) 442 id := cli.DockerCmd(c, "plugin", "inspect", "--format={{.ID}}", pluginName).Stdout() 443 id = strings.TrimSpace(id) 444 445 // make sure "v2" does not exists 446 _, err = os.Stat(filepath.Join(testEnv.DaemonInfo.DockerRootDir, "plugins", id, "rootfs", "v2")) 447 assert.Assert(c, os.IsNotExist(err), out) 448 449 cli.DockerCmd(c, "plugin", "disable", "-f", pluginName) 450 cli.DockerCmd(c, "plugin", "upgrade", "--grant-all-permissions", "--skip-remote-check", pluginName, pluginV2) 451 452 // make sure "v2" file exists 453 _, err = os.Stat(filepath.Join(testEnv.DaemonInfo.DockerRootDir, "plugins", id, "rootfs", "v2")) 454 assert.NilError(c, err) 455 456 cli.DockerCmd(c, "plugin", "enable", pluginName) 457 cli.DockerCmd(c, "volume", "inspect", "bananas") 458 cli.DockerCmd(c, "run", "--rm", "-v", "bananas:/apple", "busybox", "sh", "-c", "ls -lh /apple/core") 459 } 460 461 func (s *DockerCLIPluginsSuite) TestPluginMetricsCollector(c *testing.T) { 462 testRequires(c, DaemonIsLinux, Network, testEnv.IsLocalDaemon, IsAmd64) 463 d := daemon.New(c, dockerBinary, dockerdBinary) 464 d.Start(c) 465 defer d.Stop(c) 466 467 name := "cpuguy83/docker-metrics-plugin-test:latest" 468 r := cli.Docker(cli.Args("plugin", "install", "--grant-all-permissions", name), cli.Daemon(d)) 469 assert.Assert(c, r.Error == nil, r.Combined()) 470 471 // plugin lisens on localhost:19393 and proxies the metrics 472 resp, err := http.Get("http://localhost:19393/metrics") 473 assert.NilError(c, err) 474 defer resp.Body.Close() 475 476 b, err := io.ReadAll(resp.Body) 477 assert.NilError(c, err) 478 // check that a known metric is there... don't expect this metric to change over time.. probably safe 479 assert.Check(c, is.Contains(string(b), "container_actions")) 480 }