From 792f8fdcdf7d6939c3b2fe755a65ddf567458b27 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 29 Jun 2023 15:09:10 +0200 Subject: [PATCH] Add experimental test manage func (#704) * add test manage func * flesh out * fix linter * adjust test to use experimental package * regenerate * delete dir * Experimental: OAuth Token Retriever (#702) Co-authored-by: Mihaly Gyongyosi * fix golden file * update package name * fix test * fix golden file * undo commit * fix comment * shutdown for plugin --------- Co-authored-by: Andres Martinez Gotor Co-authored-by: Mihaly Gyongyosi --- backend/serve.go | 57 +++++++ experimental/datasourcetest/client.go | 96 +++++++++++ experimental/datasourcetest/manage.go | 51 ++++++ experimental/datasourcetest/server.go | 16 ++ experimental/oauthtokenretriever/sign.go | 57 ------- experimental/oauthtokenretriever/sign_test.go | 51 ------ experimental/oauthtokenretriever/token.go | 107 ------------ .../oauthtokenretriever/token_test.go | 64 -------- experimental/testdata/folder.golden.txt | 34 ++-- go.mod | 2 - go.sum | 4 - internal/tenant/tenanttest/tenant_test.go | 154 +++--------------- 12 files changed, 258 insertions(+), 435 deletions(-) create mode 100644 experimental/datasourcetest/client.go create mode 100644 experimental/datasourcetest/manage.go create mode 100644 experimental/datasourcetest/server.go delete mode 100644 experimental/oauthtokenretriever/sign.go delete mode 100644 experimental/oauthtokenretriever/sign_test.go delete mode 100644 experimental/oauthtokenretriever/token.go delete mode 100644 experimental/oauthtokenretriever/token_test.go diff --git a/backend/serve.go b/backend/serve.go index ca4a4fbb3..860869d0d 100644 --- a/backend/serve.go +++ b/backend/serve.go @@ -251,3 +251,60 @@ func Manage(pluginID string, serveOpts ServeOpts) error { // The default/normal hashicorp path. return Serve(serveOpts) } + +// TestStandaloneServe starts a gRPC server that is not managed by hashicorp. +// The function returns the gRPC server which should be closed by the consumer. +func TestStandaloneServe(opts ServeOpts, address string) (*grpc.Server, error) { + pluginOpts := asGRPCServeOpts(opts) + if pluginOpts.GRPCServer == nil { + pluginOpts.GRPCServer = func(grpcOptions []grpc.ServerOption) *grpc.Server { + return grpc.NewServer(append(defaultGRPCMiddlewares(opts), grpcOptions...)...) + } + } + + server := pluginOpts.GRPCServer(nil) + + var plugKeys []string + if pluginOpts.DiagnosticsServer != nil { + pluginv2.RegisterDiagnosticsServer(server, pluginOpts.DiagnosticsServer) + plugKeys = append(plugKeys, "diagnostics") + } + + if pluginOpts.ResourceServer != nil { + pluginv2.RegisterResourceServer(server, pluginOpts.ResourceServer) + plugKeys = append(plugKeys, "resources") + } + + if pluginOpts.DataServer != nil { + pluginv2.RegisterDataServer(server, pluginOpts.DataServer) + plugKeys = append(plugKeys, "data") + } + + if pluginOpts.StreamServer != nil { + pluginv2.RegisterStreamServer(server, pluginOpts.StreamServer) + plugKeys = append(plugKeys, "stream") + } + + // Start the GRPC server and handle graceful shutdown to ensure we execute deferred functions correctly + log.DefaultLogger.Info("Standalone plugin server", "capabilities", plugKeys) + listener, err := net.Listen("tcp", address) + if err != nil { + return nil, err + } + + serverErrChan := make(chan error, 1) + // Start GRPC server in a separate goroutine + go func() { + serverErrChan <- server.Serve(listener) + }() + + // Wait until signal or GRPC server termination in a separate goroutine + go func() { + err := <-serverErrChan + if err != nil { + log.DefaultLogger.Error("Server experienced an error", "error", err) + } + }() + + return server, nil +} diff --git a/experimental/datasourcetest/client.go b/experimental/datasourcetest/client.go new file mode 100644 index 000000000..78c7f9366 --- /dev/null +++ b/experimental/datasourcetest/client.go @@ -0,0 +1,96 @@ +package datasourcetest + +import ( + "context" + "errors" + "io" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2" +) + +type TestPluginClient struct { + DataClient pluginv2.DataClient + DiagnosticsClient pluginv2.DiagnosticsClient + ResourceClient pluginv2.ResourceClient + + conn *grpc.ClientConn +} + +func newTestPluginClient(addr string) (*TestPluginClient, error) { + c, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) + if err != nil { + return nil, err + } + + return &TestPluginClient{ + conn: c, + DiagnosticsClient: pluginv2.NewDiagnosticsClient(c), + DataClient: pluginv2.NewDataClient(c), + ResourceClient: pluginv2.NewResourceClient(c), + }, nil +} + +func (p *TestPluginClient) QueryData(ctx context.Context, r *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + req := backend.ToProto().QueryDataRequest(r) + + resp, err := p.DataClient.QueryData(ctx, req) + if err != nil { + return nil, err + } + + return backend.FromProto().QueryDataResponse(resp) +} + +func (p *TestPluginClient) CheckHealth(ctx context.Context, r *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + req := &pluginv2.CheckHealthRequest{ + PluginContext: backend.ToProto().PluginContext(r.PluginContext), + } + + resp, err := p.DiagnosticsClient.CheckHealth(ctx, req) + if err != nil { + return nil, err + } + + return backend.FromProto().CheckHealthResponse(resp), nil +} + +func (p *TestPluginClient) CallResource(ctx context.Context, r *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + protoReq := backend.ToProto().CallResourceRequest(r) + protoStream, err := p.ResourceClient.CallResource(ctx, protoReq) + if err != nil { + if status.Code(err) == codes.Unimplemented { + return errors.New("method not implemented") + } + + return err + } + + for { + protoResp, err := protoStream.Recv() + if err != nil { + if status.Code(err) == codes.Unimplemented { + return errors.New("method not implemented") + } + + if errors.Is(err, io.EOF) { + return nil + } + + return err + } + + if err = sender.Send(backend.FromProto().CallResourceResponse(protoResp)); err != nil { + return err + } + } +} + +func (p *TestPluginClient) shutdown() error { + return p.conn.Close() +} diff --git a/experimental/datasourcetest/manage.go b/experimental/datasourcetest/manage.go new file mode 100644 index 000000000..20b55a970 --- /dev/null +++ b/experimental/datasourcetest/manage.go @@ -0,0 +1,51 @@ +package datasourcetest + +import ( + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" + "github.com/grafana/grafana-plugin-sdk-go/internal/automanagement" +) + +type ManageOpts struct { + Address string +} + +type TestPlugin struct { + Client *TestPluginClient + Server *TestPluginServer +} + +func (p *TestPlugin) Shutdown() error { + if p.Server != nil { + p.Server.shutdown() + } + + if p.Client != nil { + return p.Client.shutdown() + } + return nil +} + +func Manage(instanceFactory datasource.InstanceFactoryFunc, opts ManageOpts) (TestPlugin, error) { + handler := automanagement.NewManager(datasource.NewInstanceManager(instanceFactory)) + s, err := backend.TestStandaloneServe(backend.ServeOpts{ + CheckHealthHandler: handler, + CallResourceHandler: handler, + QueryDataHandler: handler, + StreamHandler: handler, + }, opts.Address) + + if err != nil { + return TestPlugin{}, err + } + + c, err := newTestPluginClient(opts.Address) + if err != nil { + return TestPlugin{}, err + } + + return TestPlugin{ + Client: c, + Server: newTestPluginServer(s), + }, nil +} diff --git a/experimental/datasourcetest/server.go b/experimental/datasourcetest/server.go new file mode 100644 index 000000000..192bb0f59 --- /dev/null +++ b/experimental/datasourcetest/server.go @@ -0,0 +1,16 @@ +package datasourcetest + +import "google.golang.org/grpc" + +type TestPluginServer struct { + srv *grpc.Server +} + +func newTestPluginServer(s *grpc.Server) *TestPluginServer { + return &TestPluginServer{ + srv: s, + } +} +func (s *TestPluginServer) shutdown() { + s.srv.Stop() +} diff --git a/experimental/oauthtokenretriever/sign.go b/experimental/oauthtokenretriever/sign.go deleted file mode 100644 index 5db854798..000000000 --- a/experimental/oauthtokenretriever/sign.go +++ /dev/null @@ -1,57 +0,0 @@ -package oauthtokenretriever - -import ( - "crypto/x509" - "encoding/pem" - "errors" - "fmt" - - "github.com/go-jose/go-jose/v3" - "github.com/go-jose/go-jose/v3/jwt" -) - -type signer interface { - sign(payload interface{}) (string, error) -} - -type jwtSigner struct { - signer jose.Signer -} - -// parsePrivateKey parses a PEM encoded private key. -func parsePrivateKey(pemBytes []byte) (signer, error) { - block, _ := pem.Decode(pemBytes) - if block == nil { - return nil, errors.New("crypto: no key found") - } - - var rawkey interface{} - var alg jose.SignatureAlgorithm - switch block.Type { - case "RSA PRIVATE KEY": - alg = jose.RS256 - rsa, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, err - } - rawkey = rsa - case "PRIVATE KEY": - alg = jose.ES256 - ecdsa, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return nil, err - } - rawkey = ecdsa - default: - return nil, fmt.Errorf("crypto: unsupported private key type %q", block.Type) - } - s, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: rawkey}, &jose.SignerOptions{}) - if err != nil { - return nil, err - } - return &jwtSigner{signer: s}, nil -} - -func (s *jwtSigner) sign(payload interface{}) (string, error) { - return jwt.Signed(s.signer).Claims(payload).CompactSerialize() -} diff --git a/experimental/oauthtokenretriever/sign_test.go b/experimental/oauthtokenretriever/sign_test.go deleted file mode 100644 index 64b12a951..000000000 --- a/experimental/oauthtokenretriever/sign_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package oauthtokenretriever - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -const ( - testRSAKey = `-----BEGIN RSA PRIVATE KEY----- -MIICWwIBAAKBgQC35vznv35Kaby20gu+RQBDj/kHhPd64b6p9TKKxqiAs8kukNFj -Q8keR6MOO41Md0Jh4b/ZSo1O3C3K3K587NORJDWz0H2wVyTWDvSMI36nI/EnGDhh -4fImv5E/9jIvhOxCJ3Dej57//tMt8TEG1ZETrAKzUvB7EfCfsnazGraMQwIDAQAB -AoGAfbFh4B+w+LlGY4oyvow4vvTTV4FZCOLsRwuwzMs09iprcelHQ9pbxtddqeeo -DsBgXbhHQQPEi0bQAZxNolLX0m4nQ8n9H6by42qOJlwywYZIl7Di3aWYiOiT56v7 -PfqCsShSqsvWH8Ok4Jy6/Vcc4QcO4mGi8y8EZdSqfytGvkkCQQDhO+1Y4x36ETAh -NOQx1E/psPuSH8H6YeDoWYeap5z1KXzN4eTo01p8ckPSD93uXIig7LmfIWPMqlGV -yOBSyqD/AkEA0QXBLeDksi8hX8B2XOMfY9hWOBwBRXrlKX6TVF/9Kw+ulJpe3sU5 -lc53oytpk1VwXAfJrjNRqyIIIRnFyTJQvQJAMBgFxFcqzXziFBUhLOqy7amW7krN -ttMznSmQ5RspTsg/GA9GO9j1l2EmzjIJJ56mpgYmVK5iiw9LQHqWO9d8rQJASUDz -CtkeTTQnRh91W+hdP+i5jsCB0Y/YcEpj59YcK9M7I+lWBkyoec/6Lb0xKuluj1JL -ZDmoDYnHv5IAtxpjIQJASxC/V51AHfuQ+rWvbZ6jzoHW6owbFpC2RbZPtFanOlda -ozjy/YI5hvWLr/bre/wZ3N81pLA9lPgEpJiOPYem3Q== ------END RSA PRIVATE KEY----- -` - testECDSAKey = `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYH3q1su2TRDIr4RB -2okegCNvfhn/Q9CycAXtPnfYsZehRANCAARSs6LcDI314KqKqGHbv2FLGoMXjm6B -p6/mP7VLRqyPpiGmhCEKXD5R/695X5JYQRBF34hn2XZpMCW2z2Lr+d6s ------END PRIVATE KEY----- -` -) - -func Test_Sign(t *testing.T) { - for _, test := range []struct { - name string - key string - length int - }{ - {"RSA", testRSAKey, 196}, - {"ECDSA", testECDSAKey, 111}, - } { - t.Run(test.name, func(t *testing.T) { - signer, err := parsePrivateKey([]byte(test.key)) - assert.NoError(t, err) - signed, err := signer.sign(map[string]interface{}{}) - assert.NoError(t, err) - assert.Equal(t, test.length, len(signed)) - }) - } -} diff --git a/experimental/oauthtokenretriever/token.go b/experimental/oauthtokenretriever/token.go deleted file mode 100644 index 37f263592..000000000 --- a/experimental/oauthtokenretriever/token.go +++ /dev/null @@ -1,107 +0,0 @@ -package oauthtokenretriever - -import ( - "context" - "fmt" - "net/url" - "os" - "strings" - "time" - - "github.com/google/uuid" - "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" -) - -type TokenRetriever interface { - OnBehalfOfUser(ctx context.Context, userID string) (string, error) - Self(ctx context.Context) (string, error) -} - -type tokenRetriever struct { - signer signer - conf *clientcredentials.Config -} - -// tokenPayload returns a JWT payload for the given user ID, client ID, and host. -func (t *tokenRetriever) tokenPayload(userID string) map[string]interface{} { - iat := time.Now().Unix() - exp := iat + 1800 - u := uuid.New() - payload := map[string]interface{}{ - "iss": t.conf.ClientID, - "sub": fmt.Sprintf("user:id:%s", userID), - "aud": t.conf.TokenURL, - "exp": exp, - "iat": iat, - "jti": u.String(), - } - return payload -} - -func (t *tokenRetriever) Self(ctx context.Context) (string, error) { - t.conf.EndpointParams = url.Values{} - tok, err := t.conf.TokenSource(ctx).Token() - if err != nil { - return "", err - } - return tok.AccessToken, nil -} - -func (t *tokenRetriever) OnBehalfOfUser(ctx context.Context, userID string) (string, error) { - signed, err := t.signer.sign(t.tokenPayload(userID)) - if err != nil { - return "", err - } - - t.conf.EndpointParams = url.Values{ - "grant_type": {"urn:ietf:params:oauth:grant-type:jwt-bearer"}, - "assertion": {signed}, - } - tok, err := t.conf.TokenSource(ctx).Token() - if err != nil { - return "", err - } - - return tok.AccessToken, nil -} - -func New() (TokenRetriever, error) { - // The Grafana URL is required to obtain tokens later on - grafanaAppURL := strings.TrimRight(os.Getenv("GF_APP_URL"), "/") - if grafanaAppURL == "" { - // For debugging purposes only - grafanaAppURL = "http://localhost:3000" - } - - clientID := os.Getenv("GF_PLUGIN_APP_CLIENT_ID") - if clientID == "" { - return nil, fmt.Errorf("GF_PLUGIN_APP_CLIENT_ID is required") - } - - clientSecret := os.Getenv("GF_PLUGIN_APP_CLIENT_SECRET") - if clientSecret == "" { - return nil, fmt.Errorf("GF_PLUGIN_APP_CLIENT_SECRET is required") - } - - privateKey := os.Getenv("GF_PLUGIN_APP_PRIVATE_KEY") - if privateKey == "" { - return nil, fmt.Errorf("GF_PLUGIN_APP_PRIVATE_KEY is required") - } - - signer, err := parsePrivateKey([]byte(privateKey)) - if err != nil { - return nil, err - } - - return &tokenRetriever{ - signer: signer, - conf: &clientcredentials.Config{ - ClientID: clientID, - ClientSecret: clientSecret, - TokenURL: grafanaAppURL + "/oauth2/token", - AuthStyle: oauth2.AuthStyleInParams, - Scopes: []string{"profile", "email", "entitlements"}, - }, - }, nil -} diff --git a/experimental/oauthtokenretriever/token_test.go b/experimental/oauthtokenretriever/token_test.go deleted file mode 100644 index 7cbd66dd5..000000000 --- a/experimental/oauthtokenretriever/token_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package oauthtokenretriever - -import ( - "context" - "io" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_GetExternalServiceToken(t *testing.T) { - for _, test := range []struct { - name string - userID string - }{ - {"On Behalf Of", "1"}, - {"Service account", ""}, - } { - t.Run(test.name, func(t *testing.T) { - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b, err := io.ReadAll(r.Body) - assert.NoError(t, err) - if test.userID != "" { - assert.Contains(t, string(b), "assertion=") - assert.Contains(t, string(b), "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer") - } else { - assert.NotContains(t, string(b), "assertion=") - assert.Contains(t, string(b), "grant_type=client_credentials") - } - assert.Contains(t, string(b), "client_id=test_client_id") - assert.Contains(t, string(b), "client_secret=test_client_secret") - - w.Header().Set("Content-Type", "application/json") - _, err = w.Write([]byte(`{"access_token":"test_token"}`)) - assert.NoError(t, err) - })) - defer s.Close() - - os.Setenv("GF_APP_URL", s.URL) - defer os.Unsetenv("GF_APP_URL") - os.Setenv("GF_PLUGIN_APP_CLIENT_ID", "test_client_id") - defer os.Unsetenv("GF_PLUGIN_APP_CLIENT_ID") - os.Setenv("GF_PLUGIN_APP_CLIENT_SECRET", "test_client_secret") - defer os.Unsetenv("GF_PLUGIN_APP_CLIENT_SECRET") - os.Setenv("GF_PLUGIN_APP_PRIVATE_KEY", testECDSAKey) - defer os.Unsetenv("GF_PLUGIN_APP_PRIVATE_KEY") - - ss, err := New() - assert.NoError(t, err) - - var token string - if test.userID != "" { - token, err = ss.OnBehalfOfUser(context.Background(), test.userID) - } else { - token, err = ss.Self(context.Background()) - } - assert.NoError(t, err) - assert.Equal(t, "test_token", token) - }) - } -} diff --git a/experimental/testdata/folder.golden.txt b/experimental/testdata/folder.golden.txt index b0ee95e2c..febb5b63a 100644 --- a/experimental/testdata/folder.golden.txt +++ b/experimental/testdata/folder.golden.txt @@ -10,23 +10,23 @@ Frame[0] { } Name: Dimensions: 2 Fields by 16 Rows -+----------------------------+------------------+ -| Name: name | Name: media-type | -| Labels: | Labels: | -| Type: []string | Type: []string | -+----------------------------+------------------+ -| README.md | | -| actions | directory | -| authclient | directory | -| e2e | directory | -| fileinfo.go | | -| fileinfo_test.go | | -| frame_sorter.go | | -| frame_sorter_test.go | | -| golden_response_checker.go | | -| ... | ... | -+----------------------------+------------------+ ++----------------------+------------------+ +| Name: name | Name: media-type | +| Labels: | Labels: | +| Type: []string | Type: []string | ++----------------------+------------------+ +| README.md | | +| actions | directory | +| authclient | directory | +| datasourcetest | directory | +| e2e | directory | +| fileinfo.go | | +| fileinfo_test.go | | +| frame_sorter.go | | +| frame_sorter_test.go | | +| ... | ... | ++----------------------+------------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/go.mod b/go.mod index dae1b9b04..2cf877dd4 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac // indirect github.com/fatih/color v1.15.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -87,7 +86,6 @@ require ( go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect go.opentelemetry.io/otel/metric v0.37.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect diff --git a/go.sum b/go.sum index 17f10ff88..b7d4e3e95 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,6 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= -github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -370,9 +368,7 @@ go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= diff --git a/internal/tenant/tenanttest/tenant_test.go b/internal/tenant/tenanttest/tenant_test.go index 1977adb58..8f30a7b8f 100644 --- a/internal/tenant/tenanttest/tenant_test.go +++ b/internal/tenant/tenanttest/tenant_test.go @@ -2,23 +2,15 @@ package tenanttest import ( "context" - "errors" - "io" "testing" "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" - "github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2" - "github.com/grafana/grafana-plugin-sdk-go/internal/automanagement" - "github.com/grafana/grafana-plugin-sdk-go/internal/standalone" + experimentalDS "github.com/grafana/grafana-plugin-sdk-go/experimental/datasourcetest" "github.com/grafana/grafana-plugin-sdk-go/internal/tenant" ) @@ -33,88 +25,72 @@ func TestTenantWithPluginInstanceManagement(t *testing.T) { factoryInvocations := 0 factory := datasource.InstanceFactoryFunc(func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { factoryInvocations++ - return &testPlugin{ - settings: dataSourceSettings{}, - }, nil + return &testPlugin{}, nil }) - instancePrvdr := datasource.NewInstanceProvider(factory) - instanceMgr := instancemgmt.New(instancePrvdr) - handler := automanagement.NewManager(instanceMgr) - - pCtx := backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}} - qdr := &backend.QueryDataRequest{PluginContext: pCtx} - crr := &backend.CallResourceRequest{PluginContext: pCtx} - chr := &backend.CheckHealthRequest{PluginContext: pCtx} - responseSender := newTestCallResourceResponseSender() - - go func() { - err := backend.GracefulStandaloneServe(backend.ServeOpts{ - QueryDataHandler: handler, - CallResourceHandler: handler, - StreamHandler: handler, - CheckHealthHandler: handler, - }, standalone.NewServerSettings(addr, t.TempDir())) - require.NoError(t, err) - }() - - pc, shutdown, err := newPluginClient(addr) + tp, err := experimentalDS.Manage(factory, experimentalDS.ManageOpts{Address: addr}) require.NoError(t, err) defer func() { - err = shutdown() - require.NoError(t, err) + err = tp.Shutdown() + t.Log("plugin shutdown error", err) }() t.Run("Request without tenant information creates an instance", func(t *testing.T) { + pCtx := backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}} + qdr := &backend.QueryDataRequest{PluginContext: pCtx} + crr := &backend.CallResourceRequest{PluginContext: pCtx} + chr := &backend.CheckHealthRequest{PluginContext: pCtx} + responseSender := newTestCallResourceResponseSender() + ctx := context.Background() - resp, err := pc.QueryData(ctx, qdr) + resp, err := tp.Client.QueryData(ctx, qdr) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, 1, factoryInvocations) - err = pc.CallResource(ctx, crr, responseSender) + err = tp.Client.CallResource(ctx, crr, responseSender) require.NoError(t, err) require.Equal(t, 1, factoryInvocations) t.Run("Request from tenant #1 creates new instance", func(t *testing.T) { ctx = metadata.AppendToOutgoingContext(context.Background(), tenant.CtxKey, tenantID1) - resp, err = pc.QueryData(ctx, qdr) + resp, err = tp.Client.QueryData(ctx, qdr) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, 2, factoryInvocations) // subsequent requests from tenantID1 with same settings will reuse instance - resp, err = pc.QueryData(ctx, qdr) + resp, err = tp.Client.QueryData(ctx, qdr) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, 2, factoryInvocations) var chRes *backend.CheckHealthResult - chRes, err = pc.CheckHealth(ctx, chr) + chRes, err = tp.Client.CheckHealth(ctx, chr) require.NoError(t, err) require.NotNil(t, chRes) require.Equal(t, 2, factoryInvocations) t.Run("Request from tenant #2 creates new instance", func(t *testing.T) { ctx = metadata.AppendToOutgoingContext(context.Background(), tenant.CtxKey, tenantID2) - resp, err = pc.QueryData(ctx, qdr) + resp, err = tp.Client.QueryData(ctx, qdr) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, 3, factoryInvocations) // subsequent requests from tenantID2 with same settings will reuse instance - err = pc.CallResource(ctx, crr, responseSender) + err = tp.Client.CallResource(ctx, crr, responseSender) require.NoError(t, err) require.Equal(t, 3, factoryInvocations) }) // subsequent requests from tenantID1 with same settings will reuse instance ctx = metadata.AppendToOutgoingContext(context.Background(), tenant.CtxKey, tenantID1) - resp, err = pc.QueryData(ctx, qdr) + resp, err = tp.Client.QueryData(ctx, qdr) require.NoError(t, err) require.NotNil(t, resp) require.Equal(t, 3, factoryInvocations) - chRes, err = pc.CheckHealth(ctx, chr) + chRes, err = tp.Client.CheckHealth(ctx, chr) require.NoError(t, err) require.NotNil(t, chRes) require.Equal(t, 3, factoryInvocations) @@ -122,11 +98,7 @@ func TestTenantWithPluginInstanceManagement(t *testing.T) { }) } -type testPlugin struct { - settings dataSourceSettings -} - -type dataSourceSettings struct{} +type testPlugin struct{} func (p *testPlugin) QueryData(_ context.Context, _ *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { return backend.NewQueryDataResponse(), nil @@ -140,90 +112,6 @@ func (p *testPlugin) CheckHealth(_ context.Context, _ *backend.CheckHealthReques return &backend.CheckHealthResult{}, nil } -type testPluginClient struct { - dataClient pluginv2.DataClient - diagnosticsClient pluginv2.DiagnosticsClient - resourceClient pluginv2.ResourceClient -} - -type shutdownFunc func() error - -var noShutdown = shutdownFunc(func() error { - return nil -}) - -func newPluginClient(addr string) (*testPluginClient, shutdownFunc, error) { - conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) - if err != nil { - return nil, noShutdown, err - } - - plugin := &testPluginClient{ - diagnosticsClient: pluginv2.NewDiagnosticsClient(conn), - dataClient: pluginv2.NewDataClient(conn), - resourceClient: pluginv2.NewResourceClient(conn), - } - - return plugin, func() error { - return conn.Close() - }, nil -} - -func (p *testPluginClient) CheckHealth(ctx context.Context, r *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - req := &pluginv2.CheckHealthRequest{ - PluginContext: backend.ToProto().PluginContext(r.PluginContext), - } - - resp, err := p.diagnosticsClient.CheckHealth(ctx, req) - if err != nil { - return nil, err - } - - return backend.FromProto().CheckHealthResponse(resp), nil -} - -func (p *testPluginClient) CallResource(ctx context.Context, r *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - protoReq := backend.ToProto().CallResourceRequest(r) - protoStream, err := p.resourceClient.CallResource(ctx, protoReq) - if err != nil { - if status.Code(err) == codes.Unimplemented { - return errors.New("method not implemented") - } - - return err - } - - for { - protoResp, err := protoStream.Recv() - if err != nil { - if status.Code(err) == codes.Unimplemented { - return errors.New("method not implemented") - } - - if errors.Is(err, io.EOF) { - return nil - } - - return err - } - - if err = sender.Send(backend.FromProto().CallResourceResponse(protoResp)); err != nil { - return err - } - } -} - -func (p *testPluginClient) QueryData(ctx context.Context, r *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - req := backend.ToProto().QueryDataRequest(r) - - resp, err := p.dataClient.QueryData(ctx, req) - if err != nil { - return nil, err - } - - return backend.FromProto().QueryDataResponse(resp) -} - type testCallResourceResponseSender struct{} func newTestCallResourceResponseSender() *testCallResourceResponseSender {