diff --git a/Gopkg.lock b/Gopkg.lock index dec93774..68fafcec 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -67,6 +67,20 @@ pruneopts = "UT" revision = "316fb6d3f031ae8f4d457c6c5186b9e3ded70435" +[[projects]] + digest = "1:bf40199583e5143d1472fc34d10d6f4b69d97572142acf343b3e43136da40823" + name = "github.com/google/go-cmp" + packages = [ + "cmp", + "cmp/internal/diff", + "cmp/internal/flags", + "cmp/internal/function", + "cmp/internal/value", + ] + pruneopts = "UT" + revision = "6f77996f0c42f7b84e5a2b252227263f93432e9b" + version = "v0.3.0" + [[projects]] branch = "master" digest = "1:3ee90c0d94da31b442dde97c99635aaafec68d0b8a3c12ee2075c6bdabeec6bb" @@ -87,6 +101,14 @@ revision = "ee43cbb60db7bd22502942cccbc39059117352ab" version = "v0.1.0" +[[projects]] + digest = "1:3af6be4fee7c08f81f13d36f04ffb63ad4b6b5aaba12cce96095c7c2863d4912" + name = "github.com/gorilla/mux" + packages = ["."] + pruneopts = "UT" + revision = "ed099d42384823742bba0bf9a72b53b55c9e2e38" + version = "v1.7.2" + [[projects]] branch = "master" digest = "1:cfca58687d969bf8184ddbe5058cbabc2ecf26b371ef56f9e57791ab151b16fb" @@ -472,7 +494,8 @@ analyzer-version = 1 input-imports = [ "github.com/golang/glog", - "github.com/golang/protobuf/proto", + "github.com/google/go-cmp/cmp", + "github.com/gorilla/mux", "github.com/grpc-ecosystem/grpc-gateway/runtime", "github.com/jhump/protoreflect/desc", "github.com/jhump/protoreflect/desc/protoparse", diff --git a/README.md b/README.md index b89e5ff3..0da177f4 100644 --- a/README.md +++ b/README.md @@ -93,3 +93,5 @@ If you don't see anything listed, feel free to open an issue or pull request to ### Special Thanks Huge props to [jhump](https://github.com/jhump) for his [protoreflect](https://github.com/jhump/protoreflect) package, which gURL makes heavy use of. + + diff --git a/cmd/call/call.go b/cmd/call/call.go index 6036d05f..a0f6d786 100644 --- a/cmd/call/call.go +++ b/cmd/call/call.go @@ -15,7 +15,6 @@ import ( "github.com/wearefair/gurl/pkg/options" "github.com/wearefair/gurl/pkg/util" "google.golang.org/grpc/metadata" - "k8s.io/client-go/tools/clientcmd" ) var ( @@ -38,10 +37,12 @@ var CallCmd = &cobra.Command{ } func init() { - flags := CallCmd.Flags() // Add any flags that were registered on the built-in flag package. - flags.AddGoFlagSet(flag.CommandLine) + // This is specifically for configuring glog. We want this to be a persistent + // flag since we want to be able to handle log configuration for subcommands as well. + CallCmd.PersistentFlags().AddGoFlagSet(flag.CommandLine) + flags := CallCmd.Flags() flags.StringVarP(&uri, "uri", "u", "", "gRPC URI in the form of host:port/service_name/method_name") flags.StringVarP(&data, "data", "d", "", "Data, as JSON string, to send to the gRPC service") CallCmd.MarkFlagRequired("uri") @@ -68,33 +69,44 @@ func runCall(cmd *cobra.Command, args []string) error { } log.Infof("Parsed URI: %#v", parsedURI) - address := fmt.Sprintf("%s:%s", parsedURI.Host, parsedURI.Port) + // Set up connector + var connector jsonpb.Connector if parsedURI.Protocol == util.K8Protocol { // Set up port forward, then send request - req := uriToPortForwardRequest(parsedURI) - pf, err := k8.StartPortForward(k8Config(), req) - if err != nil { - return err + req := k8.PortForwardRequest{ + Context: parsedURI.Context, + // TODO: Make this namespace configurable via URI + Namespace: "default", + Service: parsedURI.Host, + Port: parsedURI.Port, } - defer pf.Close() - address = fmt.Sprintf("localhost:%s", pf.LocalPort()) + connector = jsonpb.NewK8Connector(k8.DefaultConfig(), req) } + // Set up the JSONPB client cfg := &jsonpb.Config{ - Address: address, DialOptions: callOptions.DialOptions(), ImportPaths: config.Instance().Local.ImportPaths, ServicePaths: config.Instance().Local.ServicePaths, } - client, err := jsonpb.NewClient(cfg) if err != nil { return log.LogAndReturn(err) } // Send request and get response - response, err := client.Call(callOptions.ContextWithOptions(context.Background()), parsedURI.Service, parsedURI.RPC, []byte(data)) + address := fmt.Sprintf("%s:%s", parsedURI.Host, parsedURI.Port) + req := &jsonpb.Request{ + Connector: connector, + Address: address, + DialOptions: callOptions.DialOptions(), + Service: parsedURI.Service, + RPC: parsedURI.RPC, + Message: []byte(data), + } + + response, err := client.Invoke(callOptions.ContextWithOptions(context.Background()), req) if err != nil { return log.LogAndReturn(err) } @@ -108,24 +120,3 @@ func runCall(cmd *cobra.Command, args []string) error { fmt.Printf("Response:\n%s\n", prettyResponse.String()) return nil } - -// Reads K8 config from default location, which is $HOME/.kube/config -func k8Config() clientcmd.ClientConfig { - // if you want to change the loading rules (which files in which order), you can do so here - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - - // if you want to change override values or bind them to flags, there are methods to help you - configOverrides := &clientcmd.ConfigOverrides{} - - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) -} - -func uriToPortForwardRequest(uri *util.URI) k8.PortForwardRequest { - return k8.PortForwardRequest{ - Context: uri.Context, - // TODO: Make this namespace configurable via URI - Namespace: "default", - Service: uri.Host, - Port: uri.Port, - } -} diff --git a/cmd/proxy/proxy.go b/cmd/proxy/proxy.go new file mode 100644 index 00000000..32d118f1 --- /dev/null +++ b/cmd/proxy/proxy.go @@ -0,0 +1,45 @@ +package proxy + +import ( + "github.com/spf13/cobra" + "github.com/wearefair/gurl/pkg/config" + "github.com/wearefair/gurl/pkg/jsonpb" + "github.com/wearefair/gurl/pkg/log" + "github.com/wearefair/gurl/pkg/proxy" +) + +var ( + port int +) + +var ProxyCmd = &cobra.Command{ + Use: "proxy", + Short: "Run the gURL proxy", + RunE: runProxy, +} + +func init() { + ProxyCmd.Flags().IntVarP(&port, "port", "p", 3030, "Port for the proxy to run on") +} + +func runProxy(cmd *cobra.Command, args []string) error { + cfg := proxy.DefaultConfig() + // cfg.ImportPaths = config.Instance().Local.ImportPaths + // cfg.ServicePaths = config.Instance().Local.ServicePaths + + jsonpbCfg := &jsonpb.Config{ + ImportPaths: config.Instance().Local.ImportPaths, + ServicePaths: config.Instance().Local.ServicePaths, + } + + client, err := jsonpb.NewClient(jsonpbCfg) + if err != nil { + return err + } + + cfg.Caller = client + + proxySrv := proxy.New(cfg) + log.Infof("Starting server at %d\n", port) + return proxySrv.Run() +} diff --git a/cmd/root.go b/cmd/root.go index 27bcf4f1..3cc7c858 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( "github.com/wearefair/gurl/cmd/call" configcmd "github.com/wearefair/gurl/cmd/config" "github.com/wearefair/gurl/cmd/list" + proxycmd "github.com/wearefair/gurl/cmd/proxy" "github.com/wearefair/gurl/pkg/config" ) @@ -19,6 +20,7 @@ func init() { cobra.OnInitialize(initConfig) call.CallCmd.AddCommand(list.ListServicesCmd) call.CallCmd.AddCommand(configcmd.ConfigCmd) + call.CallCmd.AddCommand(proxycmd.ProxyCmd) } func initConfig() { diff --git a/pkg/jsonpb/client.go b/pkg/jsonpb/client.go index cd40a5c2..4462ef5b 100644 --- a/pkg/jsonpb/client.go +++ b/pkg/jsonpb/client.go @@ -7,22 +7,15 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/jhump/protoreflect/dynamic/grpcdynamic" "github.com/wearefair/gurl/pkg/protobuf" - "google.golang.org/grpc" ) // Client handles constructing and dialing a gRPC service type Client struct { - stub grpcdynamic.Stub - // TODO: Might want to turn this into an interface? collector *protobuf.Collector } // NewClient creates a client with a Stub func NewClient(cfg *Config) (*Client, error) { - conn, err := grpc.Dial(cfg.Address, cfg.DialOptions...) - if err != nil { - return nil, err - } // Walks the proto import and service paths defined in the config and returns all descriptors descriptors, err := protobuf.Collect(cfg.ImportPaths, cfg.ServicePaths) if err != nil { @@ -30,23 +23,30 @@ func NewClient(cfg *Config) (*Client, error) { } return &Client{ - stub: grpcdynamic.NewStub(conn), collector: protobuf.NewCollector(descriptors), }, nil } -// Call takes in a context, service, RPC, and message as JSON string to convert to protobuf and -// send across the wire. -func (c *Client) Call(ctx context.Context, service, rpc string, rawMsg []byte) ([]byte, error) { - serviceDescriptor, err := c.collector.GetService(service) +// Invoke makes a unary call across the wire. +func (c *Client) Invoke(ctx context.Context, req *Request) ([]byte, error) { + // Run any connection logic + conn, err := req.Connect() + if err != nil { + return nil, err + } + defer req.Close() + + stub := grpcdynamic.NewStub(conn) + + serviceDescriptor, err := c.collector.GetService(req.Service) if err != nil { return nil, err } // Find the RPC attached to the service via the URI - methodDescriptor := serviceDescriptor.FindMethodByName(rpc) + methodDescriptor := serviceDescriptor.FindMethodByName(req.RPC) if methodDescriptor == nil { - err := fmt.Errorf("No method %s found", service) + err := fmt.Errorf("No method %s found", req.Service) return nil, err } @@ -58,7 +58,7 @@ func (c *Client) Call(ctx context.Context, service, rpc string, rawMsg []byte) ( return nil, err } - message, err := protobuf.Construct(messageDescriptor, rawMsg) + message, err := protobuf.Construct(messageDescriptor, req.Message) if err != nil { return nil, err } @@ -69,7 +69,7 @@ func (c *Client) Call(ctx context.Context, service, rpc string, rawMsg []byte) ( methodProto.ClientStreaming = &disableStreaming methodProto.ServerStreaming = &disableStreaming - response, err := c.stub.InvokeRpc(ctx, methodDescriptor, message) + response, err := stub.InvokeRpc(ctx, methodDescriptor, message) if err != nil { return nil, err } diff --git a/pkg/jsonpb/config.go b/pkg/jsonpb/config.go index e55e961b..ce4cd345 100644 --- a/pkg/jsonpb/config.go +++ b/pkg/jsonpb/config.go @@ -6,7 +6,6 @@ import ( // Config handles everything necessary to construct a Client type Config struct { - Address string DialOptions []grpc.DialOption ImportPaths []string ServicePaths []string diff --git a/pkg/jsonpb/connector.go b/pkg/jsonpb/connector.go new file mode 100644 index 00000000..73e8e13a --- /dev/null +++ b/pkg/jsonpb/connector.go @@ -0,0 +1,53 @@ +package jsonpb + +import ( + "fmt" + + "github.com/wearefair/gurl/pkg/k8" + "k8s.io/client-go/tools/clientcmd" +) + +// Connector wraps logic for connecting to a service, and then closing +// that connection. +type Connector interface { + // Connect connects sand then returns the address that the connector is expected + // to connect at, if any + Connect() (address string, err error) + Close() error +} + +// K8Connector implements the +type K8Connector struct { + k8Cfg clientcmd.ClientConfig + req k8.PortForwardRequest + pf *k8.PortForward +} + +// NewK8Connector opens up a new portforward request to K8 +func NewK8Connector(cfg clientcmd.ClientConfig, req k8.PortForwardRequest) *K8Connector { + return &K8Connector{ + k8Cfg: cfg, + req: req, + } +} + +// Connect on the K8Connector opens up a portforward, and then returns the portforward +// address to dial and connect to +func (c *K8Connector) Connect() (string, error) { + var address string + pf, err := k8.StartPortForward(c.k8Cfg, c.req) + if err != nil { + return address, err + } + c.pf = pf + + address = fmt.Sprintf("localhost:%s", pf.LocalPort()) + return address, nil +} + +// Close closest the portforward connection +func (c *K8Connector) Close() error { + c.pf.Close() + + return nil +} diff --git a/pkg/jsonpb/request.go b/pkg/jsonpb/request.go new file mode 100644 index 00000000..6427605e --- /dev/null +++ b/pkg/jsonpb/request.go @@ -0,0 +1,46 @@ +package jsonpb + +import ( + "google.golang.org/grpc" +) + +// Request encompasses all of the args required to make a request +type Request struct { + // Connector handles things like K8 portforward logic + Connector Connector + + // Address is where something will connect to + Address string + + // DialOptions to pass down to the gRPC client + DialOptions []grpc.DialOption + + // The FQDN of the gRPC service + Service string + // The method name of the RPC to target + RPC string + // The JSON message in bytes to send to the service + Message []byte +} + +func (r *Request) Connect() (*grpc.ClientConn, error) { + var err error + address := r.Address + + if r.Connector != nil { + address, err = r.Connector.Connect() + if err != nil { + return nil, err + } + } + + return grpc.Dial(address, r.DialOptions...) +} + +func (r *Request) Close() error { + if r.Connector == nil { + return nil + } + + return r.Connector.Close() +} diff --git a/pkg/k8/config.go b/pkg/k8/config.go new file mode 100644 index 00000000..38b849b4 --- /dev/null +++ b/pkg/k8/config.go @@ -0,0 +1,15 @@ +package k8 + +import "k8s.io/client-go/tools/clientcmd" + +// K8Config is just a helper function for getting the default K8 configs from +// $HOME/.kube/config +func DefaultConfig() clientcmd.ClientConfig { + // if you want to change the loading rules (which files in which order), you can do so here + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + + // if you want to change override values or bind them to flags, there are methods to help you + configOverrides := &clientcmd.ConfigOverrides{} + + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides) +} diff --git a/pkg/middleware/log/log.go b/pkg/middleware/log/log.go new file mode 100644 index 00000000..7379360f --- /dev/null +++ b/pkg/middleware/log/log.go @@ -0,0 +1,35 @@ +package log + +import ( + "net/http" + + "github.com/wearefair/gurl/pkg/log" +) + +// Middleware is logging middleware that logs the request/response info +func Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rw := &responseWrapper{ResponseWriter: w} + next.ServeHTTP(rw, r) + log.Infof("%s - [%d] - %s", r.Method, rw.status, r.URL) + }) +} + +// responseWrapper allows us to grab the info about a response and log it +type responseWrapper struct { + http.ResponseWriter + status int +} + +func (o *responseWrapper) Header() http.Header { + return o.ResponseWriter.Header() +} + +func (o *responseWrapper) Write(b []byte) (int, error) { + return o.ResponseWriter.Write(b) +} + +func (o *responseWrapper) WriteHeader(code int) { + o.status = code + o.ResponseWriter.WriteHeader(code) +} diff --git a/pkg/protobuf/cacher.go b/pkg/protobuf/cacher.go new file mode 100644 index 00000000..24cf7bb7 --- /dev/null +++ b/pkg/protobuf/cacher.go @@ -0,0 +1,16 @@ +package protobuf + +import "github.com/jhump/protoreflect/desc" + +// Cacher is the public interface for registering FileDescriptors and getting back +// Message and ServiceDescriptors. +type Cacher interface { + // AddDescriptors takes in a slice of file descriptors, walks them, and collects + // all message/service descriptors so that they can be returned in the GetMessage + // and GetService methods. + AddDescriptors(fileDescriptors []*desc.FileDescriptor) + // GetMessage takes a message descriptor's FQDN and returns the descriptor + GetMessage(fqdn string) (*desc.MessageDescriptor, error) + // GetService takes a service descriptor's FQDN and returns the descriptor + GetService(fqdn string) (*desc.ServiceDescriptor, error) +} diff --git a/pkg/proxy/caller.go b/pkg/proxy/caller.go new file mode 100644 index 00000000..4f21ff9f --- /dev/null +++ b/pkg/proxy/caller.go @@ -0,0 +1,12 @@ +package proxy + +import ( + "context" + + "github.com/wearefair/gurl/pkg/jsonpb" +) + +// Caller is the interface that wraps calling across the wire +type Caller interface { + Invoke(context.Context, *jsonpb.Request) ([]byte, error) +} diff --git a/pkg/proxy/config.go b/pkg/proxy/config.go new file mode 100644 index 00000000..6f6bab75 --- /dev/null +++ b/pkg/proxy/config.go @@ -0,0 +1,48 @@ +package proxy + +import ( + "net/http" + + "github.com/gorilla/mux" + logmw "github.com/wearefair/gurl/pkg/middleware/log" + "github.com/wearefair/gurl/pkg/options" + "google.golang.org/grpc/metadata" +) + +// Config wraps all configs for the proxy +type Config struct { + // Addr is the address that the proxy will run at + Addr string + // Options to pass down to the jsonpb client + Options *options.Options + // Caller is what's used to pull in the import and service paths for handling + // the JSON to protobuf behavior + Caller Caller + // // ImportPaths are the import paths to pass down to the jsonpb client + // ImportPaths []string + // // ServicePaths are the service paths to pass down to the jsonpb client + // ServicePaths []string + // Middlewares you want to register with the router + Middlewares []func(http.Handler) http.Handler + // You can override the ProxyTargetHeader to target calls at, if desired. + ProxyTargetHeader string + // You can override the default handler. This should only be passed in if + // you don't want to override the Router. + ProxyHandler http.HandlerFunc + // If you want to override the default mux.Router. All of the middleware + // passed in will be registered on the router on creation of the Proxy struct. + Router *mux.Router +} + +// DefaultConfig returns a sane base template for Proxy configs +func DefaultConfig() *Config { + return &Config{ + Addr: ":3030", + Middlewares: []func(http.Handler) http.Handler{ + logmw.Middleware, + }, + Options: &options.Options{Metadata: metadata.MD{}}, + Router: mux.NewRouter(), + ProxyTargetHeader: DefaultProxyTargetHeader, + } +} diff --git a/pkg/proxy/handler.go b/pkg/proxy/handler.go new file mode 100644 index 00000000..6b22ef34 --- /dev/null +++ b/pkg/proxy/handler.go @@ -0,0 +1,115 @@ +package proxy + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + + "github.com/gorilla/mux" + "github.com/wearefair/gurl/pkg/jsonpb" + "github.com/wearefair/gurl/pkg/k8" + "github.com/wearefair/gurl/pkg/log" + "github.com/wearefair/gurl/pkg/util" + "google.golang.org/grpc/metadata" +) + +const ( + ServiceKey = "service" + RpcKey = "rpc" +) + +// Handler takes in a route resembling the following +// format: ://. +// Ex: localhost:50051/fairapis.pubsub.v1.Publish/ListTopics +// It pulls the service and the RPC name off of the route, generates +// the proto message, and then routes it to the proper destination. +// The destination must be set in the headers under the proxy target header +// which defaults to x-gurl-proxy-target +func (p *Proxy) Handler(rw http.ResponseWriter, req *http.Request) { + // Pull the host off the headers + target := req.Header.Get(p.proxyTargetHeader) + + // If the header is not set... return a 422, because we really + // can't process this request. Where are we supposed to forward this to? + if target == "" { + log.Error("Proxy target header is empty!") + rw.WriteHeader(http.StatusUnprocessableEntity) + return + } + + defer req.Body.Close() + msg, err := ioutil.ReadAll(req.Body) + if err != nil { + log.Error(err.Error()) + rw.WriteHeader(http.StatusBadRequest) + return + } + + // Pull the variables off of the request + vars := mux.Vars(req) + + // TODO: Validation errors on either one of these if they're nil + service := vars[ServiceKey] + rpc := vars[RpcKey] + + if service == "" || rpc == "" { + log.Error(fmt.Errorf("Service or RPC cannot be blank, service: %s, rpc: %s\n", service, rpc)) + rw.WriteHeader(http.StatusBadRequest) + return + } + + parsedURI, err := util.ParseURI(target) + if err != nil { + log.Error(err) + rw.WriteHeader(http.StatusBadRequest) + return + } + + // TODO: This is duplicated + var connector jsonpb.Connector + if parsedURI.Protocol == util.K8Protocol { + // Set up port forward, then send request + req := k8.PortForwardRequest{ + Context: parsedURI.Context, + // TODO: Make this namespace configurable via URI + Namespace: "default", + Service: parsedURI.Host, + Port: parsedURI.Port, + } + + connector = jsonpb.NewK8Connector(k8.DefaultConfig(), req) + } + + jsonpbReq := &jsonpb.Request{ + Connector: connector, + Address: target, + DialOptions: p.opts.DialOptions(), + Service: service, + RPC: rpc, + Message: msg, + } + + // TODO: This ctx isn't used properly + outgoingMd := mergeHttpHeadersToMetadata(p.opts.Metadata, req.Header) + ctx := metadata.NewOutgoingContext(context.Background(), outgoingMd) + + response, err := p.caller.Invoke(ctx, jsonpbReq) + if err != nil { + log.Error(err) + rw.WriteHeader(http.StatusBadRequest) + return + } + + rw.WriteHeader(http.StatusOK) + rw.Write(response) +} + +func mergeHttpHeadersToMetadata(md metadata.MD, headers http.Header) metadata.MD { + mdCopy := md.Copy() + for key, vals := range headers { + mdCopy.Append(key, vals...) + } + + return mdCopy +} diff --git a/pkg/proxy/handler_test.go b/pkg/proxy/handler_test.go new file mode 100644 index 00000000..47068961 --- /dev/null +++ b/pkg/proxy/handler_test.go @@ -0,0 +1,43 @@ +package proxy + +import ( + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc/metadata" +) + +func TestMergeHttpHeadersToMetadata(t *testing.T) { + testCases := []struct { + Md metadata.MD + Headers http.Header + Expected metadata.MD + }{ + // Empty metadata merges with HTTP headers + { + Md: metadata.MD{}, + Headers: map[string][]string{"foo": []string{"bar"}}, + Expected: metadata.Pairs("foo", "bar"), + }, + // Headers with multiple values merge properly + { + Md: metadata.MD{}, + Headers: map[string][]string{"hello": []string{"world", "boo"}}, + Expected: metadata.Pairs("hello", "world", "hello", "boo"), + }, + // Original metadata is not touched if headers are empty + { + Md: metadata.Pairs("foo", "bar"), + Expected: metadata.Pairs("foo", "bar"), + }, + } + + for i, testCase := range testCases { + res := mergeHttpHeadersToMetadata(testCase.Md, testCase.Headers) + + if !cmp.Equal(res, testCase.Expected) { + t.Errorf("[%d] - Expected: %+v\nActual: %+v\n", i, testCase.Expected, res) + } + } +} diff --git a/pkg/proxy/header.go b/pkg/proxy/header.go new file mode 100644 index 00000000..a9bf9500 --- /dev/null +++ b/pkg/proxy/header.go @@ -0,0 +1,10 @@ +package proxy + +const ( + // This is the default proxy header for forwarding the request + DefaultProxyTargetHeader = "x-gurl-proxy-target" +) + +var ( + ProxyTargetHeader = DefaultProxyTargetHeader +) diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go new file mode 100644 index 00000000..9f312449 --- /dev/null +++ b/pkg/proxy/proxy.go @@ -0,0 +1,55 @@ +package proxy + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/wearefair/gurl/pkg/options" +) + +// Proxy encapsulates all gURL proxy server logic +type Proxy struct { + caller Caller + router *mux.Router + server *http.Server + opts *options.Options + proxyTargetHeader string + + importPaths []string + servicePaths []string +} + +// New returns an instance of Proxy +func New(cfg *Config) *Proxy { + configureMiddleware(cfg.Router, cfg.Middlewares) + + s := &http.Server{ + Addr: cfg.Addr, + Handler: cfg.Router, + } + + return &Proxy{ + caller: cfg.Caller, + opts: cfg.Options, + proxyTargetHeader: cfg.ProxyTargetHeader, + router: cfg.Router, + server: s, + } +} + +// Run the proxy server +func (p *Proxy) Run() error { + p.configure() + return p.server.ListenAndServe() +} + +// Proxy configure configures the handler and routes +func (p *Proxy) configure() { + p.router.HandleFunc("/{service}/{rpc}", p.Handler) +} + +func configureMiddleware(r *mux.Router, middlewares []func(http.Handler) http.Handler) { + for _, middleware := range middlewares { + r.Use(middleware) + } +} diff --git a/pkg/util/uri.go b/pkg/util/uri.go index b39eac44..e1abdb04 100644 --- a/pkg/util/uri.go +++ b/pkg/util/uri.go @@ -41,7 +41,7 @@ const ( // Examples of valid URI for gurl: // http://localhost:50051/hello.world.package.Foo/Bar // k8://fake-context/foo-service:50051/hello.world.package.Foo/Bar - uriRegex = `((?P[a-z0-9]{2,5})(?:\:\/\/)((?P[0-9a-z._-]+)(?:\/))?)?(?P[0-9a-z-_.]+)(?:\:)(?P[0-9]{2,5})(?:\/)(?P[0-9a-zA-Z._-]+)(?:\/)(?P[0-9a-zA-Z._-]+)` + uriRegex = `((?P[a-z0-9]{2,5})(?:\:\/\/)((?P[0-9a-z._-]+)(?:\/))?)?(?P[0-9a-z-_.]+)(?:\:)(?P[0-9]{2,5})((?:\/)(?P[0-9a-zA-Z._-]+)(?:\/)(?P[0-9a-zA-Z._-]+))?` ) var ( diff --git a/pkg/util/uri_test.go b/pkg/util/uri_test.go index eae19a7d..305db3af 100644 --- a/pkg/util/uri_test.go +++ b/pkg/util/uri_test.go @@ -59,6 +59,17 @@ func TestParseURI(t *testing.T) { }, Err: nil, }, + // Parse input without service or RPC + { + Input: "k8://fake-context/k8-service:3000/", + Expected: &URI{ + Protocol: "k8", + Host: "k8-service", + Port: "3000", + Context: "fake-context", + }, + Err: nil, + }, // Input that's a completely hot garbage returns error { Input: "fakeNews",