github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/integration/plugin/graphdriver/external_test.go (about) 1 package graphdriver 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "net/http/httptest" 10 "os" 11 "runtime" 12 "testing" 13 14 "github.com/docker/docker/api/types" 15 containertypes "github.com/docker/docker/api/types/container" 16 "github.com/docker/docker/client" 17 "github.com/docker/docker/daemon/graphdriver" 18 "github.com/docker/docker/daemon/graphdriver/vfs" 19 "github.com/docker/docker/integration/internal/container" 20 "github.com/docker/docker/integration/internal/requirement" 21 "github.com/docker/docker/pkg/archive" 22 "github.com/docker/docker/pkg/idtools" 23 "github.com/docker/docker/pkg/plugins" 24 "github.com/docker/docker/testutil/daemon" 25 "gotest.tools/v3/assert" 26 is "gotest.tools/v3/assert/cmp" 27 "gotest.tools/v3/skip" 28 ) 29 30 type graphEventsCounter struct { 31 activations int 32 creations int 33 removals int 34 gets int 35 puts int 36 stats int 37 cleanups int 38 exists int 39 init int 40 metadata int 41 diff int 42 applydiff int 43 changes int 44 diffsize int 45 } 46 47 func TestExternalGraphDriver(t *testing.T) { 48 skip.If(t, runtime.GOOS == "windows") 49 skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon") 50 skip.If(t, !requirement.HasHubConnectivity(t)) 51 skip.If(t, testEnv.IsRootless, "rootless mode doesn't support external graph driver") 52 53 // Setup plugin(s) 54 ec := make(map[string]*graphEventsCounter) 55 sserver := setupPluginViaSpecFile(t, ec) 56 jserver := setupPluginViaJSONFile(t, ec) 57 // Create daemon 58 d := daemon.New(t, daemon.WithExperimental()) 59 c := d.NewClientT(t) 60 61 for _, tc := range []struct { 62 name string 63 test func(client.APIClient, *daemon.Daemon) func(*testing.T) 64 }{ 65 { 66 name: "json", 67 test: testExternalGraphDriver("json", ec), 68 }, 69 { 70 name: "spec", 71 test: testExternalGraphDriver("spec", ec), 72 }, 73 { 74 name: "pull", 75 test: testGraphDriverPull, 76 }, 77 } { 78 t.Run(tc.name, tc.test(c, d)) 79 } 80 81 sserver.Close() 82 jserver.Close() 83 err := os.RemoveAll("/etc/docker/plugins") 84 assert.NilError(t, err) 85 } 86 87 func setupPluginViaSpecFile(t *testing.T, ec map[string]*graphEventsCounter) *httptest.Server { 88 mux := http.NewServeMux() 89 server := httptest.NewServer(mux) 90 91 setupPlugin(t, ec, "spec", mux, []byte(server.URL)) 92 93 return server 94 } 95 96 func setupPluginViaJSONFile(t *testing.T, ec map[string]*graphEventsCounter) *httptest.Server { 97 mux := http.NewServeMux() 98 server := httptest.NewServer(mux) 99 100 p := plugins.NewLocalPlugin("json-external-graph-driver", server.URL) 101 b, err := json.Marshal(p) 102 assert.NilError(t, err) 103 104 setupPlugin(t, ec, "json", mux, b) 105 106 return server 107 } 108 109 func setupPlugin(t *testing.T, ec map[string]*graphEventsCounter, ext string, mux *http.ServeMux, b []byte) { 110 name := fmt.Sprintf("%s-external-graph-driver", ext) 111 type graphDriverRequest struct { 112 ID string `json:",omitempty"` 113 Parent string `json:",omitempty"` 114 MountLabel string `json:",omitempty"` 115 ReadOnly bool `json:",omitempty"` 116 } 117 118 type graphDriverResponse struct { 119 Err error `json:",omitempty"` 120 Dir string `json:",omitempty"` 121 Exists bool `json:",omitempty"` 122 Status [][2]string `json:",omitempty"` 123 Metadata map[string]string `json:",omitempty"` 124 Changes []archive.Change `json:",omitempty"` 125 Size int64 `json:",omitempty"` 126 } 127 128 respond := func(w http.ResponseWriter, data interface{}) { 129 w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") 130 switch t := data.(type) { 131 case error: 132 fmt.Fprintf(w, "{\"Err\": %q}\n", t.Error()) 133 case string: 134 fmt.Fprintln(w, t) 135 default: 136 json.NewEncoder(w).Encode(&data) 137 } 138 } 139 140 decReq := func(b io.ReadCloser, out interface{}, w http.ResponseWriter) error { 141 defer b.Close() 142 if err := json.NewDecoder(b).Decode(&out); err != nil { 143 http.Error(w, fmt.Sprintf("error decoding json: %s", err.Error()), 500) 144 } 145 return nil 146 } 147 148 base, err := os.MkdirTemp("", name) 149 assert.NilError(t, err) 150 vfsProto, err := vfs.Init(base, []string{}, idtools.IdentityMapping{}) 151 assert.NilError(t, err, "error initializing graph driver") 152 driver := graphdriver.NewNaiveDiffDriver(vfsProto, idtools.IdentityMapping{}) 153 154 ec[ext] = &graphEventsCounter{} 155 mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { 156 ec[ext].activations++ 157 respond(w, `{"Implements": ["GraphDriver"]}`) 158 }) 159 160 mux.HandleFunc("/GraphDriver.Init", func(w http.ResponseWriter, r *http.Request) { 161 ec[ext].init++ 162 respond(w, "{}") 163 }) 164 165 mux.HandleFunc("/GraphDriver.CreateReadWrite", func(w http.ResponseWriter, r *http.Request) { 166 ec[ext].creations++ 167 168 var req graphDriverRequest 169 if err := decReq(r.Body, &req, w); err != nil { 170 return 171 } 172 if err := driver.CreateReadWrite(req.ID, req.Parent, nil); err != nil { 173 respond(w, err) 174 return 175 } 176 respond(w, "{}") 177 }) 178 179 mux.HandleFunc("/GraphDriver.Create", func(w http.ResponseWriter, r *http.Request) { 180 ec[ext].creations++ 181 182 var req graphDriverRequest 183 if err := decReq(r.Body, &req, w); err != nil { 184 return 185 } 186 if err := driver.Create(req.ID, req.Parent, nil); err != nil { 187 respond(w, err) 188 return 189 } 190 respond(w, "{}") 191 }) 192 193 mux.HandleFunc("/GraphDriver.Remove", func(w http.ResponseWriter, r *http.Request) { 194 ec[ext].removals++ 195 196 var req graphDriverRequest 197 if err := decReq(r.Body, &req, w); err != nil { 198 return 199 } 200 201 if err := driver.Remove(req.ID); err != nil { 202 respond(w, err) 203 return 204 } 205 respond(w, "{}") 206 }) 207 208 mux.HandleFunc("/GraphDriver.Get", func(w http.ResponseWriter, r *http.Request) { 209 ec[ext].gets++ 210 211 var req graphDriverRequest 212 if err := decReq(r.Body, &req, w); err != nil { 213 return 214 } 215 216 dir, err := driver.Get(req.ID, req.MountLabel) 217 if err != nil { 218 respond(w, err) 219 return 220 } 221 respond(w, &graphDriverResponse{Dir: dir}) 222 }) 223 224 mux.HandleFunc("/GraphDriver.Put", func(w http.ResponseWriter, r *http.Request) { 225 ec[ext].puts++ 226 227 var req graphDriverRequest 228 if err := decReq(r.Body, &req, w); err != nil { 229 return 230 } 231 232 if err := driver.Put(req.ID); err != nil { 233 respond(w, err) 234 return 235 } 236 respond(w, "{}") 237 }) 238 239 mux.HandleFunc("/GraphDriver.Exists", func(w http.ResponseWriter, r *http.Request) { 240 ec[ext].exists++ 241 242 var req graphDriverRequest 243 if err := decReq(r.Body, &req, w); err != nil { 244 return 245 } 246 respond(w, &graphDriverResponse{Exists: driver.Exists(req.ID)}) 247 }) 248 249 mux.HandleFunc("/GraphDriver.Status", func(w http.ResponseWriter, r *http.Request) { 250 ec[ext].stats++ 251 respond(w, &graphDriverResponse{Status: driver.Status()}) 252 }) 253 254 mux.HandleFunc("/GraphDriver.Cleanup", func(w http.ResponseWriter, r *http.Request) { 255 ec[ext].cleanups++ 256 err := driver.Cleanup() 257 if err != nil { 258 respond(w, err) 259 return 260 } 261 respond(w, `{}`) 262 }) 263 264 mux.HandleFunc("/GraphDriver.GetMetadata", func(w http.ResponseWriter, r *http.Request) { 265 ec[ext].metadata++ 266 267 var req graphDriverRequest 268 if err := decReq(r.Body, &req, w); err != nil { 269 return 270 } 271 272 data, err := driver.GetMetadata(req.ID) 273 if err != nil { 274 respond(w, err) 275 return 276 } 277 respond(w, &graphDriverResponse{Metadata: data}) 278 }) 279 280 mux.HandleFunc("/GraphDriver.Diff", func(w http.ResponseWriter, r *http.Request) { 281 ec[ext].diff++ 282 283 var req graphDriverRequest 284 if err := decReq(r.Body, &req, w); err != nil { 285 return 286 } 287 288 diff, err := driver.Diff(req.ID, req.Parent) 289 if err != nil { 290 respond(w, err) 291 return 292 } 293 io.Copy(w, diff) 294 }) 295 296 mux.HandleFunc("/GraphDriver.Changes", func(w http.ResponseWriter, r *http.Request) { 297 ec[ext].changes++ 298 var req graphDriverRequest 299 if err := decReq(r.Body, &req, w); err != nil { 300 return 301 } 302 303 changes, err := driver.Changes(req.ID, req.Parent) 304 if err != nil { 305 respond(w, err) 306 return 307 } 308 respond(w, &graphDriverResponse{Changes: changes}) 309 }) 310 311 mux.HandleFunc("/GraphDriver.ApplyDiff", func(w http.ResponseWriter, r *http.Request) { 312 ec[ext].applydiff++ 313 diff := r.Body 314 defer r.Body.Close() 315 316 id := r.URL.Query().Get("id") 317 parent := r.URL.Query().Get("parent") 318 319 if id == "" { 320 http.Error(w, "missing id", 409) 321 } 322 323 size, err := driver.ApplyDiff(id, parent, diff) 324 if err != nil { 325 respond(w, err) 326 return 327 } 328 respond(w, &graphDriverResponse{Size: size}) 329 }) 330 331 mux.HandleFunc("/GraphDriver.DiffSize", func(w http.ResponseWriter, r *http.Request) { 332 ec[ext].diffsize++ 333 334 var req graphDriverRequest 335 if err := decReq(r.Body, &req, w); err != nil { 336 return 337 } 338 339 size, err := driver.DiffSize(req.ID, req.Parent) 340 if err != nil { 341 respond(w, err) 342 return 343 } 344 respond(w, &graphDriverResponse{Size: size}) 345 }) 346 347 err = os.MkdirAll("/etc/docker/plugins", 0755) 348 assert.NilError(t, err) 349 350 specFile := "/etc/docker/plugins/" + name + "." + ext 351 err = os.WriteFile(specFile, b, 0644) 352 assert.NilError(t, err) 353 } 354 355 func testExternalGraphDriver(ext string, ec map[string]*graphEventsCounter) func(client.APIClient, *daemon.Daemon) func(*testing.T) { 356 return func(c client.APIClient, d *daemon.Daemon) func(*testing.T) { 357 return func(t *testing.T) { 358 driverName := fmt.Sprintf("%s-external-graph-driver", ext) 359 d.StartWithBusybox(t, "-s", driverName) 360 361 ctx := context.Background() 362 363 testGraphDriver(ctx, t, c, driverName, func(t *testing.T) { 364 d.Restart(t, "-s", driverName) 365 }) 366 367 _, err := c.Info(ctx) 368 assert.NilError(t, err) 369 370 d.Stop(t) 371 372 // Don't check ec.exists, because the daemon no longer calls the 373 // Exists function. 374 assert.Check(t, is.Equal(ec[ext].activations, 2)) 375 assert.Check(t, is.Equal(ec[ext].init, 2)) 376 assert.Check(t, ec[ext].creations >= 1) 377 assert.Check(t, ec[ext].removals >= 1) 378 assert.Check(t, ec[ext].gets >= 1) 379 assert.Check(t, ec[ext].puts >= 1) 380 assert.Check(t, is.Equal(ec[ext].stats, 5)) 381 assert.Check(t, is.Equal(ec[ext].cleanups, 2)) 382 assert.Check(t, ec[ext].applydiff >= 1) 383 assert.Check(t, is.Equal(ec[ext].changes, 1)) 384 assert.Check(t, is.Equal(ec[ext].diffsize, 0)) 385 assert.Check(t, is.Equal(ec[ext].diff, 0)) 386 assert.Check(t, is.Equal(ec[ext].metadata, 1)) 387 } 388 } 389 } 390 391 func testGraphDriverPull(c client.APIClient, d *daemon.Daemon) func(*testing.T) { 392 return func(t *testing.T) { 393 d.Start(t) 394 defer d.Stop(t) 395 ctx := context.Background() 396 397 r, err := c.ImagePull(ctx, "busybox:latest@sha256:95cf004f559831017cdf4628aaf1bb30133677be8702a8c5f2994629f637a209", types.ImagePullOptions{}) 398 assert.NilError(t, err) 399 _, err = io.Copy(io.Discard, r) 400 assert.NilError(t, err) 401 402 container.Run(ctx, t, c, container.WithImage("busybox:latest@sha256:95cf004f559831017cdf4628aaf1bb30133677be8702a8c5f2994629f637a209")) 403 } 404 } 405 406 func TestGraphdriverPluginV2(t *testing.T) { 407 skip.If(t, runtime.GOOS == "windows") 408 skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon") 409 skip.If(t, !requirement.HasHubConnectivity(t)) 410 skip.If(t, os.Getenv("DOCKER_ENGINE_GOARCH") != "amd64") 411 skip.If(t, !requirement.Overlay2Supported(testEnv.DaemonInfo.KernelVersion)) 412 413 d := daemon.New(t, daemon.WithExperimental()) 414 d.Start(t) 415 defer d.Stop(t) 416 417 client := d.NewClientT(t) 418 defer client.Close() 419 ctx := context.Background() 420 421 // install the plugin 422 plugin := "cpuguy83/docker-overlay2-graphdriver-plugin" 423 responseReader, err := client.PluginInstall(ctx, plugin, types.PluginInstallOptions{ 424 RemoteRef: plugin, 425 AcceptAllPermissions: true, 426 }) 427 assert.NilError(t, err) 428 defer responseReader.Close() 429 // ensure it's done by waiting for EOF on the response 430 _, err = io.Copy(io.Discard, responseReader) 431 assert.NilError(t, err) 432 433 // restart the daemon with the plugin set as the storage driver 434 d.Stop(t) 435 d.StartWithBusybox(t, "-s", plugin) 436 437 testGraphDriver(ctx, t, client, plugin, nil) 438 } 439 440 func testGraphDriver(ctx context.Context, t *testing.T, c client.APIClient, driverName string, afterContainerRunFn func(*testing.T)) { 441 id := container.Run(ctx, t, c, container.WithCmd("sh", "-c", "echo hello > /hello")) 442 443 if afterContainerRunFn != nil { 444 afterContainerRunFn(t) 445 } 446 447 i, err := c.ContainerInspect(ctx, id) 448 assert.NilError(t, err) 449 assert.Check(t, is.Equal(i.GraphDriver.Name, driverName)) 450 451 diffs, err := c.ContainerDiff(ctx, id) 452 assert.NilError(t, err) 453 assert.Check(t, is.Contains(diffs, containertypes.ContainerChangeResponseItem{ 454 Kind: archive.ChangeAdd, 455 Path: "/hello", 456 }), "diffs: %v", diffs) 457 458 err = c.ContainerRemove(ctx, id, types.ContainerRemoveOptions{ 459 Force: true, 460 }) 461 assert.NilError(t, err) 462 }