Skip to content

Commit

Permalink
cli: add gRPC hooks (#316)
Browse files Browse the repository at this point in the history
* add grpc hook

* add retry/backoff params
make streaming RPC call

* Update cmd/tusd/cli/flags.go

Co-Authored-By: Márk Sági-Kazár <sagikazarmark@users.noreply.github.com>

* move one time grpc configuration to `Setup`

* remove stream grpc

Co-authored-by: Márk Sági-Kazár <sagikazarmark@users.noreply.github.com>
  • Loading branch information
inigohu and sagikazarmark authored Feb 6, 2020
1 parent 9c0e0c8 commit 8ef7648
Show file tree
Hide file tree
Showing 8 changed files with 717 additions and 1 deletion.
6 changes: 6 additions & 0 deletions cmd/tusd/cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ var Flags struct {
HttpHooksEndpoint string
HttpHooksRetry int
HttpHooksBackoff int
GrpcHooksEndpoint string
GrpcHooksRetry int
GrpcHooksBackoff int
HooksStopUploadCode int
PluginHookPath string
EnabledHooks []hooks.HookType
Expand Down Expand Up @@ -54,6 +57,9 @@ func ParseFlags() {
flag.StringVar(&Flags.HttpHooksEndpoint, "hooks-http", "", "An HTTP endpoint to which hook events will be sent to")
flag.IntVar(&Flags.HttpHooksRetry, "hooks-http-retry", 3, "Number of times to retry on a 500 or network timeout")
flag.IntVar(&Flags.HttpHooksBackoff, "hooks-http-backoff", 1, "Number of seconds to wait before retrying each retry")
flag.StringVar(&Flags.GrpcHooksEndpoint, "hooks-grpc", "", "An gRPC endpoint to which hook events will be sent to")
flag.IntVar(&Flags.GrpcHooksRetry, "hooks-grpc-retry", 3, "Number of times to retry on a server error or network timeout")
flag.IntVar(&Flags.GrpcHooksBackoff, "hooks-grpc-backoff", 1, "Number of seconds to wait before retrying each retry")
flag.IntVar(&Flags.HooksStopUploadCode, "hooks-stop-code", 0, "Return code from post-receive hook which causes tusd to stop and delete the current upload. A zero value means that no uploads will be stopped")
flag.StringVar(&Flags.PluginHookPath, "hooks-plugin", "", "Path to a Go plugin for loading hook functions (only supported on Linux and macOS; highly EXPERIMENTAL and may BREAK in the future)")
flag.BoolVar(&Flags.ShowVersion, "version", false, "Print tusd version information")
Expand Down
8 changes: 8 additions & 0 deletions cmd/tusd/cli/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ func SetupPreHooks(config *handler.Config) error {
MaxRetries: Flags.HttpHooksRetry,
Backoff: Flags.HttpHooksBackoff,
}
} else if Flags.GrpcHooksEndpoint != "" {
stdout.Printf("Using '%s' as the endpoint for gRPC hooks", Flags.GrpcHooksEndpoint)

hookHandler = &hooks.GrpcHook{
Endpoint: Flags.GrpcHooksEndpoint,
MaxRetries: Flags.GrpcHooksRetry,
Backoff: Flags.GrpcHooksBackoff,
}
} else if Flags.PluginHookPath != "" {
stdout.Printf("Using '%s' to load plugin for hooks", Flags.PluginHookPath)

Expand Down
73 changes: 73 additions & 0 deletions cmd/tusd/cli/hooks/grpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package hooks

import (
"context"
"time"

grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
"github.com/tus/tusd/pkg/handler"
pb "github.com/tus/tusd/pkg/proto/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)

type GrpcHook struct {
Endpoint string
MaxRetries int
Backoff int
Client pb.HookServiceClient
}

func (g GrpcHook) Setup() error {
opts := []grpc_retry.CallOption{
grpc_retry.WithBackoff(grpc_retry.BackoffLinear(time.Duration(g.Backoff) * time.Second)),
grpc_retry.WithMax(uint(g.MaxRetries)),
}
grpcOpts := []grpc.DialOption{
grpc.WithInsecure(),
grpc.WithUnaryInterceptor(grpc_retry.UnaryClientInterceptor(opts...)),
}
conn, err := grpc.Dial(g.Endpoint, grpcOpts...)
if err != nil {
return err
}
g.Client = pb.NewHookServiceClient(conn)
return nil
}

func (g GrpcHook) InvokeHook(typ HookType, info handler.HookEvent, captureOutput bool) ([]byte, int, error) {
ctx := context.Background()
req := &pb.SendRequest{Hook: marshal(info)}
resp, err := g.Client.Send(ctx, req)
if err != nil {
if e, ok := status.FromError(err); ok {
return nil, int(e.Code()), err
}
return nil, 2, err
}
if captureOutput {
return resp.Response.GetValue(), 0, err
}
return nil, 0, err
}

func marshal(info handler.HookEvent) *pb.Hook {
return &pb.Hook{
Upload: &pb.Upload{
Id: info.Upload.ID,
Size: info.Upload.Size,
SizeIsDeferred: info.Upload.SizeIsDeferred,
Offset: info.Upload.Offset,
MetaData: info.Upload.MetaData,
IsPartial: info.Upload.IsPartial,
IsFinal: info.Upload.IsFinal,
PartialUploads: info.Upload.PartialUploads,
Storage: info.Upload.Storage,
},
HttpRequest: &pb.HTTPRequest{
Method: info.HTTPRequest.Method,
Uri: info.HTTPRequest.URI,
RemoteAddr: info.HTTPRequest.RemoteAddr,
},
}
}
68 changes: 68 additions & 0 deletions cmd/tusd/cli/hooks/proto/v1/hook.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
syntax = "proto3";
package v1;

import "google/protobuf/any.proto";

// Uploaded data
message Upload {
// Unique integer identifier of the uploaded file
string id = 1;
// Total file size in bytes specified in the NewUpload call
int64 Size = 2;
// Indicates whether the total file size is deferred until later
bool SizeIsDeferred = 3;
// Offset in bytes (zero-based)
int64 Offset = 4;
map<string, string> metaData = 5;
// Indicates that this is a partial upload which will later be used to form
// a final upload by concatenation. Partial uploads should not be processed
// when they are finished since they are only incomplete chunks of files.
bool isPartial = 6;
// Indicates that this is a final upload
bool isFinal = 7;
// If the upload is a final one (see IsFinal) this will be a non-empty
// ordered slice containing the ids of the uploads of which the final upload
// will consist after concatenation.
repeated string partialUploads = 8;
// Storage contains information about where the data storage saves the upload,
// for example a file path. The available values vary depending on what data
// store is used. This map may also be nil.
map <string, string> storage = 9;
}

message HTTPRequest {
// Method is the HTTP method, e.g. POST or PATCH
string method = 1;
// URI is the full HTTP request URI, e.g. /files/fooo
string uri = 2;
// RemoteAddr contains the network address that sent the request
string remoteAddr = 3;
}

// Hook's data
message Hook {
// Upload contains information about the upload that caused this hook
// to be fired.
Upload upload = 1;
// HTTPRequest contains details about the HTTP request that reached
// tusd.
HTTPRequest httpRequest = 2;
}

// Request data to send hook
message SendRequest {
// The hook data
Hook hook = 1;
}

// Response that contains data for sended hook
message SendResponse {
// The response of the hook.
google.protobuf.Any response = 1;
}

// The hook service definition.
service HookService {
// Sends a hook
rpc Send (SendRequest) returns (SendResponse) {}
}
73 changes: 73 additions & 0 deletions docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,76 @@ Tusd uses the [Pester library](https://github.com/sethgrid/pester) to issue requ
$ # Retrying 5 times with a 2 second backoff
$ tusd --hooks-http http://localhost:8081/write --hooks-http-retry 5 --hooks-http-backoff 2
```

## GRPC Hooks

GRPC Hooks are the third type of hooks supported by tusd. Like the others hooks, it is disabled by default. To enable it, pass the `--hooks-grpc option to the tusd binary. The flag's value will be a gRPC endpoint, which the tusd binary will be sent to:

```bash
$ tusd --hooks-grpc localhost:8080

[tusd] Using 'localhost:8080' as the endpoint for gRPC hooks
[tusd] Using './data' as directory storage.
...
```

### Usage

Tusd will issue a `gRPC` request to the specified endpoint, specifying the hook name, such as pre-create or post-finish, in the `Hook-Name` header and following body:

```js
{
// The upload object contains the upload's details
"Upload": {
// The upload's ID. Will be empty during the pre-create event
"ID": "14b1c4c77771671a8479bc0444bbc5ce",
// The upload's total size in bytes.
"Size": 46205,
// The upload's current offset in bytes.
"Offset": 1592,
// These properties will be set to true, if the upload as a final or partial
// one. See the Concatenation extension for details:
// http://tus.io/protocols/resumable-upload.html#concatenation
"IsFinal": false,
"IsPartial": false,
// If the upload is a final one, this value will be an array of upload IDs
// which are concatenated to produce the upload.
"PartialUploads": null,
// The upload's meta data which can be supplied by the clients as it wishes.
// All keys and values in this object will be strings.
// Be aware that it may contain maliciously crafted values and you must not
// trust it without escaping it first!
"MetaData": {
"filename": "transloadit.png"
},
// Details about where the data store saved the uploaded file. The different
// availabl keys vary depending on the used data store.
"Storage": {
// For example, the filestore supplies the absolute file path:
"Type": "filestore",
"Path": "/my/upload/directory/14b1c4c77771671a8479bc0444bbc5ce",

// The S3Store and GCSStore supply the bucket name and object key:
"Type": "s3store",
"Bucket": "my-upload-bucket",
"Key": "my-prefix/14b1c4c77771671a8479bc0444bbc5ce"
}
},
// Details about the HTTP request which caused this hook to be fired.
// It can be used to record the client's IP address or inspect the headers.
"HTTPRequest": {
"Method": "PATCH",
"URI": "/files/14b1c4c77771671a8479bc0444bbc5ce",
"RemoteAddr": "1.2.3.4:47689",
}
}
```

### Configuration

By default, tusd will retry 3 times based on the gRPC status response or network error, with a 1 second backoff. This can be configured with the flags `--hooks-grpc-retry` and `--hooks-grpc-backoff`, like so:

```bash
$ # Retrying 5 times with a 2 second backoff
$ tusd --hooks-grpc localhost:8081/ --hooks-grpc-retry 5 --hooks-grpc-backoff 2
```
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ require (
github.com/aws/aws-sdk-go v1.20.1
github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40
github.com/golang/mock v1.3.1
github.com/golang/protobuf v1.3.2
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0
github.com/prometheus/client_golang v1.0.0
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0
github.com/stretchr/testify v1.3.0
github.com/stretchr/testify v1.4.0
github.com/vimeo/go-util v1.2.0
google.golang.org/api v0.6.0
google.golang.org/grpc v1.25.1
gopkg.in/Acconut/lockfile.v1 v1.1.0
gopkg.in/h2non/gock.v1 v1.0.14
)
Loading

0 comments on commit 8ef7648

Please sign in to comment.