-
Notifications
You must be signed in to change notification settings - Fork 144
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Add GRPC executor which uses grpcurl to do requests to grpc endpoints. Fixes: #187 Signed-off-by: Matous Dzivjak <matous.dzivjak@kiwi.com>
- Loading branch information
Showing
7 changed files
with
334 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
# Venom - Executor Grpc | ||
|
||
Step for execute GRPC Request | ||
|
||
Based on `grpcurl`, see [grpcurl](https://github.com/fullstorydev/grpcurl) for more information. | ||
This executor relies on the gRPC server reflection, which should be enabled on the server as described | ||
[here](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md). | ||
gRPC server reflection is not enabled by default and not implemented for every gRPC library, | ||
make sure your library of choice supports reflection before implementing tests using this executor. | ||
gRPC server reflection also does not properly work with `gogo/protobuf`: grpc/grpc-go#1873 | ||
|
||
## Tests | ||
|
||
Results of test are parsed as json and saved in `bodyjson`. Status codes correspond | ||
to the official status codes of gRPC. | ||
You can find what individual return codes mean [here](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md). | ||
|
||
## Input | ||
|
||
In your yaml file, you can use: | ||
|
||
```yaml | ||
- url mandatory | ||
- service mandatory: service to call | ||
- method mandatory: list, describe, or method of the endpoint | ||
- plaintext optional: use plaintext protocol instead of TLS | ||
- data optional: data to marshal to json and send as a request | ||
- headers optional: data to send as additional headers | ||
- connect_timeout optional: The maximum time, in seconds, to wait for connection to be established. Defaults to 10 seconds. | ||
``` | ||
Example: | ||
```yaml | ||
|
||
name: Title of TestSuite | ||
testcases: | ||
|
||
- name: request GRPC | ||
steps: | ||
- type: grpc | ||
url: serverUrlWithoutHttp:8090 | ||
plaintext: true # skip TLS | ||
data: | ||
foo: bar | ||
service: coolService.api | ||
method: GetAllFoos | ||
assertions: | ||
- result.code ShouldEqual 0 | ||
- result.bodyjson.foo ShouldEqual bar | ||
|
||
``` | ||
|
||
Multiline script: | ||
|
||
```yaml | ||
name: Title of TestSuite | ||
testcases: | ||
- name: multiline script | ||
steps: | ||
- script: | | ||
echo "Foo" \ | ||
echo "Bar" | ||
``` | ||
## Output | ||
```yaml | ||
executor | ||
systemout | ||
systemerr | ||
err | ||
code | ||
timeseconds | ||
timehuman | ||
``` | ||
|
||
- result.timeseconds & result.timehuman: time of execution | ||
- result.executor.executor.script: script executed | ||
- result.err: if exists, this field contains error | ||
- result.systemout: Standard Output of executed script | ||
- result.systemerr: Error Output of executed script | ||
- result.code: Exit Code |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
package grpc | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"strconv" | ||
"time" | ||
|
||
"github.com/fullstorydev/grpcurl" | ||
"github.com/golang/protobuf/proto" | ||
"github.com/jhump/protoreflect/desc" | ||
"github.com/jhump/protoreflect/grpcreflect" | ||
"github.com/mitchellh/mapstructure" | ||
"google.golang.org/grpc" | ||
"google.golang.org/grpc/credentials" | ||
"google.golang.org/grpc/metadata" | ||
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" | ||
"google.golang.org/grpc/status" | ||
|
||
"github.com/ovh/venom" | ||
"github.com/ovh/venom/executors" | ||
) | ||
|
||
// Name for test exec | ||
const Name = "grpc" | ||
|
||
// New returns a new Test Exec | ||
func New() venom.Executor { | ||
return &Executor{} | ||
} | ||
|
||
// Executor represents a Test Exec | ||
type Executor struct { | ||
Url string `json:"url" yaml:"url"` | ||
Service string `json:"service" yaml:"service"` | ||
Method string `json:"method" yaml:"method"` | ||
Plaintext bool `json:"plaintext,omitempty" yaml:"plaintext,omitempty"` | ||
JsonDefaultFields bool `json:"default_field" yaml:"default_field"` | ||
IncludeTextSeparator bool `json:"include_test_separator" yaml:"include_test_separator"` | ||
Data map[string]interface{} `json:"data" yaml:"data"` | ||
Headers map[string]string `json:"headers" yaml:"headers"` | ||
ConnectTimeout *int64 `json:"connect_timeout" yaml:"connect_timeout"` | ||
} | ||
|
||
// Result represents a step result | ||
type Result struct { | ||
Executor Executor `json:"executor,omitempty" yaml:"executor,omitempty"` | ||
Systemout string `json:"systemout,omitempty" yaml:"systemout,omitempty"` | ||
SystemoutJSON interface{} `json:"systemoutjson,omitempty" yaml:"systemoutjson,omitempty"` | ||
Systemerr string `json:"systemerr,omitempty" yaml:"systemerr,omitempty"` | ||
SystemerrJSON interface{} `json:"systemerrjson,omitempty" yaml:"systemerrjson,omitempty"` | ||
Err string `json:"err,omitempty" yaml:"err,omitempty"` | ||
Code string `json:"code,omitempty" yaml:"code,omitempty"` | ||
TimeSeconds float64 `json:"timeseconds,omitempty" yaml:"timeseconds,omitempty"` | ||
TimeHuman string `json:"timehuman,omitempty" yaml:"timehuman,omitempty"` | ||
} | ||
|
||
type customHandler struct { | ||
formatter grpcurl.Formatter | ||
target *Result | ||
err error | ||
} | ||
|
||
// OnResolveMethod is called with a descriptor of the method that is being invoked. | ||
func (*customHandler) OnResolveMethod(m *desc.MethodDescriptor) {} | ||
|
||
// OnSendHeaders is called with the request metadata that is being sent. | ||
func (*customHandler) OnSendHeaders(metadata.MD) {} | ||
|
||
// OnReceiveHeaders is called when response headers have been received. | ||
func (*customHandler) OnReceiveHeaders(m metadata.MD) {} | ||
|
||
// OnReceiveResponse is called for each response message received. | ||
func (c *customHandler) OnReceiveResponse(msg proto.Message) { | ||
res, err := c.formatter(msg) | ||
if err != nil || c.err != nil { | ||
c.err = err | ||
return | ||
} | ||
c.target.Systemout = res | ||
} | ||
|
||
// OnReceiveTrailers is called when response trailers and final RPC status have been received. | ||
func (c *customHandler) OnReceiveTrailers(stat *status.Status, met metadata.MD) { | ||
if err := stat.Err(); err != nil { | ||
c.target.Systemerr = err.Error() | ||
} | ||
c.target.Code = strconv.Itoa(int(uint32(stat.Code()))) | ||
} | ||
|
||
// ZeroValueResult return an empty implemtation of this executor result | ||
func (Executor) ZeroValueResult() venom.ExecutorResult { | ||
r, _ := executors.Dump(Result{}) | ||
return r | ||
} | ||
|
||
// GetDefaultAssertions return default assertions for type exec | ||
func (Executor) GetDefaultAssertions() *venom.StepAssertions { | ||
return &venom.StepAssertions{Assertions: []string{"result.code ShouldEqual 0"}} | ||
} | ||
|
||
// Run execute TestStep of type exec | ||
func (Executor) Run(testCaseContext venom.TestCaseContext, l venom.Logger, step venom.TestStep, workdir string) (venom.ExecutorResult, error) { | ||
// decode test | ||
var e Executor | ||
if err := mapstructure.Decode(step, &e); err != nil { | ||
return nil, err | ||
} | ||
|
||
// prepare headers | ||
headers := make([]string, len(e.Headers)) | ||
for k, v := range e.Headers { | ||
headers = append(headers, fmt.Sprintf("%s: %s", k, v)) | ||
} | ||
|
||
// prepare data | ||
data, err := json.Marshal(e.Data) | ||
if err != nil { | ||
return nil, fmt.Errorf("runGrpcurl: Cannot marshal request data: %s\n", err) | ||
} | ||
|
||
result := Result{Executor: e} | ||
start := time.Now() | ||
|
||
ctx := context.Background() | ||
|
||
// prepare dial function | ||
dial := func() *grpc.ClientConn { | ||
dialTime := 10 * time.Second | ||
if e.ConnectTimeout != nil && *e.ConnectTimeout > 0 { | ||
dialTime = time.Duration(*e.ConnectTimeout * int64(time.Second)) | ||
} | ||
ctx, cancel := context.WithTimeout(ctx, dialTime) | ||
defer cancel() | ||
var creds credentials.TransportCredentials | ||
cc, err := grpcurl.BlockingDial(ctx, "tcp", e.Url, creds) | ||
if err != nil { | ||
return nil | ||
} | ||
return cc | ||
} | ||
|
||
var cc *grpc.ClientConn | ||
var descSource grpcurl.DescriptorSource | ||
var refClient *grpcreflect.Client | ||
md := grpcurl.MetadataFromHeaders(headers) | ||
refCtx := metadata.NewOutgoingContext(ctx, md) | ||
cc = dial() | ||
refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(cc)) | ||
descSource = grpcurl.DescriptorSourceFromServer(ctx, refClient) | ||
|
||
// arrange for the RPCs to be cleanly shutdown | ||
defer func() { | ||
if refClient != nil { | ||
refClient.Reset() | ||
refClient = nil | ||
} | ||
if cc != nil { | ||
_ = cc.Close() | ||
cc = nil | ||
} | ||
}() | ||
|
||
// Invoke an RPC | ||
if cc == nil { | ||
cc = dial() | ||
} | ||
|
||
// prepare request and send | ||
in := bytes.NewReader(data) | ||
rf, formatter, err := grpcurl.RequestParserAndFormatterFor( | ||
grpcurl.FormatJSON, | ||
descSource, | ||
e.JsonDefaultFields, | ||
e.IncludeTextSeparator, | ||
in, | ||
) | ||
if err != nil { | ||
return nil, fmt.Errorf("dailed to construct request parser and formatter %s", err) | ||
} | ||
|
||
// prepare custom handler to handle response | ||
handle := customHandler{ | ||
formatter, | ||
&result, | ||
nil, | ||
} | ||
|
||
// invoke the gRPC | ||
err = grpcurl.InvokeRPC(ctx, descSource, cc, e.Service+"/"+e.Method, append(headers), &handle, rf.Next) | ||
if err != nil { | ||
return nil, fmt.Errorf("error invoking method %s", err) | ||
} | ||
|
||
elapsed := time.Since(start) | ||
result.TimeSeconds = elapsed.Seconds() | ||
result.TimeHuman = elapsed.String() | ||
|
||
if handle.err != nil { | ||
result.Err = handle.err.Error() | ||
} | ||
|
||
// parse stdout as JSON | ||
var outJSONArray []interface{} | ||
if err := json.Unmarshal([]byte(result.Systemout), &outJSONArray); err != nil { | ||
outJSONMap := map[string]interface{}{} | ||
if err2 := json.Unmarshal([]byte(result.Systemout), &outJSONMap); err2 == nil { | ||
result.SystemoutJSON = outJSONMap | ||
} | ||
} else { | ||
result.SystemoutJSON = outJSONArray | ||
} | ||
|
||
// parse stderr output as JSON | ||
var errJSONArray []interface{} | ||
if err := json.Unmarshal([]byte(result.Systemout), &errJSONArray); err != nil { | ||
errJSONMap := map[string]interface{}{} | ||
if err2 := json.Unmarshal([]byte(result.Systemout), &errJSONMap); err2 == nil { | ||
result.SystemoutJSON = errJSONMap | ||
} | ||
} else { | ||
result.SystemoutJSON = errJSONArray | ||
} | ||
|
||
return executors.Dump(result) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
name: TestSuite GRPC | ||
testcases: | ||
- name: GRPC testcase | ||
steps: | ||
- type: grpc | ||
url: "{{.grpcUrl}}" | ||
service: "{{.grpcService}}" | ||
method: "{{.grpcMethod}}" | ||
plaintext: true | ||
assertions: | ||
- result.code ShouldEqual 0 | ||
- result.systemoutjson.foo ShouldEqual bar |