package plugins // import "github.com/docker/docker/pkg/plugins" import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "strings" "testing" "time" "github.com/docker/docker/pkg/plugins/transport" "github.com/docker/go-connections/tlsconfig" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func setupRemotePluginServer(t *testing.T) (mux *http.ServeMux, addr string) { t.Helper() mux = http.NewServeMux() server := httptest.NewServer(mux) t.Logf("started remote plugin server listening on: %s", server.URL) t.Cleanup(func() { server.Close() }) return mux, server.URL } func TestFailedConnection(t *testing.T) { t.Parallel() c, _ := NewClient("tcp://127.0.0.1:1", &tlsconfig.Options{InsecureSkipVerify: true}) _, err := c.callWithRetry("Service.Method", nil, false) if err == nil { t.Fatal("Unexpected successful connection") } } func TestFailOnce(t *testing.T) { t.Parallel() mux, addr := setupRemotePluginServer(t) failed := false mux.HandleFunc("/Test.FailOnce", func(w http.ResponseWriter, r *http.Request) { if !failed { failed = true panic("Plugin not ready (intentional panic for test)") } }) c, _ := NewClient(addr, &tlsconfig.Options{InsecureSkipVerify: true}) b := strings.NewReader("body") _, err := c.callWithRetry("Test.FailOnce", b, true) if err != nil { t.Fatal(err) } } func TestEchoInputOutput(t *testing.T) { t.Parallel() mux, addr := setupRemotePluginServer(t) m := Manifest{[]string{"VolumeDriver", "NetworkDriver"}} mux.HandleFunc("/Test.Echo", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Fatalf("Expected POST, got %s\n", r.Method) } header := w.Header() header.Set("Content-Type", transport.VersionMimetype) io.Copy(w, r.Body) }) c, _ := NewClient(addr, &tlsconfig.Options{InsecureSkipVerify: true}) var output Manifest err := c.Call("Test.Echo", m, &output) if err != nil { t.Fatal(err) } assert.Check(t, is.DeepEqual(m, output)) err = c.Call("Test.Echo", nil, nil) if err != nil { t.Fatal(err) } } func TestBackoff(t *testing.T) { t.Parallel() cases := []struct { retries int expTimeOff time.Duration }{ {expTimeOff: time.Duration(1)}, {retries: 1, expTimeOff: time.Duration(2)}, {retries: 2, expTimeOff: time.Duration(4)}, {retries: 4, expTimeOff: time.Duration(16)}, {retries: 6, expTimeOff: time.Duration(30)}, {retries: 10, expTimeOff: time.Duration(30)}, } for _, tc := range cases { tc := tc t.Run(fmt.Sprintf("retries: %v", tc.retries), func(t *testing.T) { s := tc.expTimeOff * time.Second if d := backoff(tc.retries); d != s { t.Fatalf("Retry %v, expected %v, was %v\n", tc.retries, s, d) } }) } } func TestAbortRetry(t *testing.T) { t.Parallel() cases := []struct { timeOff time.Duration expAbort bool }{ {timeOff: time.Duration(1)}, {timeOff: time.Duration(2)}, {timeOff: time.Duration(10)}, {timeOff: time.Duration(30), expAbort: true}, {timeOff: time.Duration(40), expAbort: true}, } for _, tc := range cases { tc := tc t.Run(fmt.Sprintf("duration: %v", tc.timeOff), func(t *testing.T) { s := tc.timeOff * time.Second if a := abort(time.Now(), s, 0); a != tc.expAbort { t.Fatalf("Duration %v, expected %v, was %v\n", tc.timeOff, s, a) } }) } } func TestClientScheme(t *testing.T) { t.Parallel() cases := map[string]string{ "tcp://127.0.0.1:8080": "http", "unix:///usr/local/plugins/foo": "http", "http://127.0.0.1:8080": "http", "https://127.0.0.1:8080": "https", } for addr, scheme := range cases { u, err := url.Parse(addr) if err != nil { t.Error(err) } s := httpScheme(u) if s != scheme { t.Fatalf("URL scheme mismatch, expected %s, got %s", scheme, s) } } } func TestNewClientWithTimeout(t *testing.T) { t.Parallel() mux, addr := setupRemotePluginServer(t) m := Manifest{[]string{"VolumeDriver", "NetworkDriver"}} mux.HandleFunc("/Test.Echo", func(w http.ResponseWriter, r *http.Request) { time.Sleep(20 * time.Millisecond) io.Copy(w, r.Body) }) timeout := 10 * time.Millisecond c, _ := NewClientWithTimeout(addr, &tlsconfig.Options{InsecureSkipVerify: true}, timeout) var output Manifest err := c.CallWithOptions("Test.Echo", m, &output, func(opts *RequestOpts) { opts.testTimeOut = 1 }) assert.ErrorType(t, err, os.IsTimeout) } func TestClientStream(t *testing.T) { t.Parallel() mux, addr := setupRemotePluginServer(t) m := Manifest{[]string{"VolumeDriver", "NetworkDriver"}} var output Manifest mux.HandleFunc("/Test.Echo", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Fatalf("Expected POST, got %s", r.Method) } header := w.Header() header.Set("Content-Type", transport.VersionMimetype) io.Copy(w, r.Body) }) c, _ := NewClient(addr, &tlsconfig.Options{InsecureSkipVerify: true}) body, err := c.Stream("Test.Echo", m) if err != nil { t.Fatal(err) } defer body.Close() if err := json.NewDecoder(body).Decode(&output); err != nil { t.Fatalf("Test.Echo: error reading plugin resp: %v", err) } assert.Check(t, is.DeepEqual(m, output)) } func TestClientSendFile(t *testing.T) { t.Parallel() mux, addr := setupRemotePluginServer(t) m := Manifest{[]string{"VolumeDriver", "NetworkDriver"}} var output Manifest var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(m); err != nil { t.Fatal(err) } mux.HandleFunc("/Test.Echo", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Fatalf("Expected POST, got %s\n", r.Method) } header := w.Header() header.Set("Content-Type", transport.VersionMimetype) io.Copy(w, r.Body) }) c, _ := NewClient(addr, &tlsconfig.Options{InsecureSkipVerify: true}) if err := c.SendFile("Test.Echo", &buf, &output); err != nil { t.Fatal(err) } assert.Check(t, is.DeepEqual(m, output)) } func TestClientWithRequestTimeout(t *testing.T) { t.Parallel() type timeoutError interface { Timeout() bool } unblock := make(chan struct{}) testHandler := func(w http.ResponseWriter, r *http.Request) { select { case <-unblock: case <-r.Context().Done(): } w.WriteHeader(http.StatusOK) } srv := httptest.NewServer(http.HandlerFunc(testHandler)) defer func() { close(unblock) srv.Close() }() client := &Client{http: srv.Client(), requestFactory: &testRequestWrapper{srv}} errCh := make(chan error, 1) go func() { _, err := client.callWithRetry("/Plugin.Hello", nil, false, WithRequestTimeout(time.Millisecond)) errCh <- err }() timer := time.NewTimer(5 * time.Second) defer timer.Stop() select { case err := <-errCh: var tErr timeoutError if assert.Check(t, errors.As(err, &tErr), "want timeout error, got %T", err) { assert.Check(t, tErr.Timeout()) } case <-timer.C: t.Fatal("client request did not time out in time") } } type testRequestWrapper struct { *httptest.Server } func (w *testRequestWrapper) NewRequest(path string, data io.Reader) (*http.Request, error) { req, err := http.NewRequest(http.MethodPost, path, data) if err != nil { return nil, err } u, err := url.Parse(w.Server.URL) if err != nil { return nil, err } req.URL = u return req, nil }