diff --git a/cmd/invoke.go b/cmd/invoke.go index c8b009f59..3f350fd78 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -18,6 +18,7 @@ import ( "net/http" "os" "runtime" + "strings" "github.com/spf13/cobra" @@ -34,6 +35,7 @@ var ( invokeVerb string invokeDataFile string invokeSocket string + invokeHeaders = make([]string, 0) ) var InvokeCmd = &cobra.Command{ @@ -43,6 +45,9 @@ var InvokeCmd = &cobra.Command{ # Invoke a sample method on target app with POST Verb dapr invoke --app-id target --method sample --data '{"key":"value"} +# Invoke a sample method on target app with customized Header +dapr invoke --app-id target --method sample --data '{"key":"value"} --header Header1=Value1 --header Header2=Value2 + # Invoke a sample method on target app with GET Verb dapr invoke --app-id target --method sample --verb GET @@ -78,7 +83,25 @@ dapr invoke --unix-domain-socket --app-id target --method sample --verb GET } } - response, err := client.Invoke(invokeAppID, invokeAppMethod, bytePayload, invokeVerb, invokeSocket) + header := http.Header{} + for _, h := range invokeHeaders { + p := strings.Split(strings.TrimSpace(h), "=") + if len(p) != 2 { + print.FailureStatusEvent(os.Stderr, "Should one \"=\" in HTTP header.") + os.Exit(1) + } + + if p[0] == "" { + print.FailureStatusEvent(os.Stderr, "A header name is required.") + os.Exit(1) + } else if p[1] == "" { + print.FailureStatusEvent(os.Stderr, "Value for header name is required.") + os.Exit(1) + } + header.Add(p[0], p[1]) + } + + response, err := client.Invoke(invokeAppID, invokeAppMethod, bytePayload, invokeVerb, header, invokeSocket) if err != nil { err = fmt.Errorf("error invoking app %s: %w", invokeAppID, err) print.FailureStatusEvent(os.Stderr, err.Error()) @@ -98,6 +121,7 @@ func init() { InvokeCmd.Flags().StringVarP(&invokeData, "data", "d", "", "The JSON serialized data string (optional)") InvokeCmd.Flags().StringVarP(&invokeVerb, "verb", "v", defaultHTTPVerb, "The HTTP verb to use") InvokeCmd.Flags().StringVarP(&invokeDataFile, "data-file", "f", "", "A file containing the JSON serialized data (optional)") + InvokeCmd.Flags().StringArrayVarP(&invokeHeaders, "header", "H", []string{}, "HTTP headers to be used on invoke") InvokeCmd.Flags().BoolP("help", "h", false, "Print this help message") InvokeCmd.Flags().StringVarP(&invokeSocket, "unix-domain-socket", "u", "", "Path to a unix domain socket dir. If specified, Dapr API servers will use Unix Domain Sockets") InvokeCmd.MarkFlagRequired("app-id") diff --git a/go.mod b/go.mod index a903106b9..a632f87bf 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Pallinder/sillyname-go v0.0.0-20130730142914-97aeae9e6ba1 github.com/briandowns/spinner v1.19.0 github.com/dapr/dapr v1.11.0 - github.com/dapr/go-sdk v1.6.0 + github.com/dapr/go-sdk v1.8.0 github.com/docker/docker v20.10.21+incompatible github.com/fatih/color v1.15.0 github.com/gocarina/gocsv v0.0.0-20220927221512-ad3251f9fa25 @@ -38,6 +38,7 @@ require ( require ( github.com/Masterminds/semver/v3 v3.2.0 github.com/evanphx/json-patch v5.6.0+incompatible + google.golang.org/grpc v1.55.0 ) require ( @@ -88,6 +89,7 @@ require ( github.com/fasthttp/router v1.4.18 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect + github.com/go-chi/chi/v5 v5.0.8 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-gorp/gorp/v3 v3.0.2 // indirect github.com/go-kit/log v0.2.1 // indirect @@ -220,7 +222,6 @@ require ( gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.54.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index bf91ca344..5d19ba592 100644 --- a/go.sum +++ b/go.sum @@ -340,8 +340,8 @@ github.com/dapr/components-contrib v1.11.0-rc.11 h1:PFUTCCfZ+99BIorCNR+mB/CEGnGF github.com/dapr/components-contrib v1.11.0-rc.11/go.mod h1:prx2ATX6wFnR6Cp1xXGW3J9s5Gyz9AtOrG0xBf7QnHI= github.com/dapr/dapr v1.11.0 h1:sKDHKyehNm7xY2z6oXoUIcj2lXqKnlkAYPkvroldrwU= github.com/dapr/dapr v1.11.0/go.mod h1:++3iGjeJcpraxZxdZi1EblplcCXgu/Ycz0Q/u+MYvxw= -github.com/dapr/go-sdk v1.6.0 h1:jg5A2khSCHF8bGZsig5RWN/gD0jjitszc2V6Uq2pPdY= -github.com/dapr/go-sdk v1.6.0/go.mod h1:KLQBltoD9K0w5hKTihdcyg9Epob9gypwL5dYcQzPro4= +github.com/dapr/go-sdk v1.8.0 h1:OEleeL3zUTqXxIZ7Vkk3PClAeCh1g8sZ1yR2JFZKfXM= +github.com/dapr/go-sdk v1.8.0/go.mod h1:MBcTKXg8PmBc8A968tVWQg1Xt+DZtmeVR6zVVVGcmeA= github.com/dapr/kit v0.11.2 h1:4tJre4OWyOfBFDZeDvEIC+7nYgTRgqpCvo4/bfB2sN8= github.com/dapr/kit v0.11.2/go.mod h1:Iq5mKuZnmO+Lyu7MC8755YWv+mp3h7/nomRQcjZfN5k= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -430,6 +430,8 @@ github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2H github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew= github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -520,8 +522,8 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1767,8 +1769,8 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/pkg/standalone/client.go b/pkg/standalone/client.go index 3435492bf..b74efc112 100644 --- a/pkg/standalone/client.go +++ b/pkg/standalone/client.go @@ -13,6 +13,8 @@ limitations under the License. package standalone +import "net/http" + type DaprProcess interface { List() ([]ListOutput, error) } @@ -22,7 +24,7 @@ type daprProcess struct{} // Client is the interface the wraps all the methods exposed by the Dapr CLI. type Client interface { // Invoke is a command to invoke a remote or local dapr instance. - Invoke(appID, method string, data []byte, verb string, socket string) (string, error) + Invoke(appID, method string, data []byte, verb string, header http.Header, socket string) (string, error) // Publish is used to publish event to a topic in a pubsub for an app ID. Publish(publishAppID, pubsubName, topic string, payload []byte, socket string, metadata map[string]interface{}) error } diff --git a/pkg/standalone/invoke.go b/pkg/standalone/invoke.go index df059912f..34921e57c 100644 --- a/pkg/standalone/invoke.go +++ b/pkg/standalone/invoke.go @@ -26,7 +26,7 @@ import ( ) // Invoke is a command to invoke a remote or local dapr instance. -func (s *Standalone) Invoke(appID, method string, data []byte, verb string, path string) (string, error) { +func (s *Standalone) Invoke(appID, method string, data []byte, verb string, header http.Header, path string) (string, error) { list, err := s.process.List() if err != nil { return "", err @@ -39,7 +39,13 @@ func (s *Standalone) Invoke(appID, method string, data []byte, verb string, path if err != nil { return "", err } + req.Header.Set("Content-Type", "application/json") + for h, vs := range header { + for _, v := range vs { + req.Header.Add(h, v) + } + } var httpc http.Client diff --git a/pkg/standalone/invoke_test.go b/pkg/standalone/invoke_test.go index 0883df227..e0802d059 100644 --- a/pkg/standalone/invoke_test.go +++ b/pkg/standalone/invoke_test.go @@ -15,6 +15,7 @@ package standalone import ( "fmt" + "net/http" "os" "runtime" "testing" @@ -35,6 +36,7 @@ func TestInvoke(t *testing.T) { listErr error expectedPath string postResponse string + header http.Header resp string }{ { @@ -42,6 +44,7 @@ func TestInvoke(t *testing.T) { errorExpected: true, errString: assert.AnError.Error(), listErr: assert.AnError, + header: nil, }, { name: "appID not found", @@ -51,6 +54,7 @@ func TestInvoke(t *testing.T) { lo: ListOutput{ AppID: "testapp", }, + header: nil, }, { name: "appID found successful invoke empty response", @@ -59,6 +63,7 @@ func TestInvoke(t *testing.T) { lo: ListOutput{ AppID: "testapp", }, + header: nil, }, { name: "appID found successful invoke", @@ -69,8 +74,23 @@ func TestInvoke(t *testing.T) { }, expectedPath: "/v1.0/invoke/testapp/method/test", postResponse: "test payload", + header: nil, resp: "successful invoke", }, + { + name: "appID found successful invoke with customized header", + appID: "testapp", + method: "test", + lo: ListOutput{ + AppID: "testapp", + }, + expectedPath: "/v1.0/invoke/testapp/method/test", + postResponse: "test payload", + resp: "successful invoke", + header: http.Header{ + "Customized-Header": []string{"Value"}, + }, + }, } for _, socket := range []string{"", "/tmp"} { @@ -105,7 +125,7 @@ func TestInvoke(t *testing.T) { }, } - res, err := client.Invoke(tc.appID, tc.method, []byte(tc.resp), "GET", socket) + res, err := client.Invoke(tc.appID, tc.method, []byte(tc.resp), "GET", tc.header, socket) if tc.errorExpected { assert.Error(t, err, "expected an error") assert.Equal(t, tc.errString, err.Error(), "expected error strings to match") @@ -137,7 +157,7 @@ func TestInvoke(t *testing.T) { Err: tc.listErr, }, } - res, err := client.Invoke(tc.appID, tc.method, []byte(tc.resp), "POST", socket) + res, err := client.Invoke(tc.appID, tc.method, []byte(tc.resp), "POST", tc.header, socket) if tc.errorExpected { assert.Error(t, err, "expected an error") assert.Equal(t, tc.errString, err.Error(), "expected error strings to match") @@ -169,7 +189,7 @@ func TestInvoke(t *testing.T) { Err: tc.listErr, }, } - res, err := client.Invoke(tc.appID, tc.method, []byte(tc.resp), "DELETE", socket) + res, err := client.Invoke(tc.appID, tc.method, []byte(tc.resp), "DELETE", tc.header, socket) if tc.errorExpected { assert.Error(t, err, "expected an error") assert.Equal(t, tc.errString, err.Error(), "expected error strings to match") @@ -202,7 +222,7 @@ func TestInvoke(t *testing.T) { Err: tc.listErr, }, } - res, err := client.Invoke(tc.appID, tc.method, []byte(tc.resp), "PUT", socket) + res, err := client.Invoke(tc.appID, tc.method, []byte(tc.resp), "PUT", tc.header, socket) if tc.errorExpected { assert.Error(t, err, "expected an error") assert.Equal(t, tc.errString, err.Error(), "expected error strings to match") diff --git a/tests/e2e/standalone/invoke_test.go b/tests/e2e/standalone/invoke_test.go index 812d0ea4b..b00e2ee3a 100644 --- a/tests/e2e/standalone/invoke_test.go +++ b/tests/e2e/standalone/invoke_test.go @@ -27,6 +27,7 @@ import ( daprHttp "github.com/dapr/go-sdk/service/http" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" ) func StartTestService(t *testing.T, port int) common.Service { @@ -38,6 +39,18 @@ func StartTestService(t *testing.T, port int) common.Service { ContentType: e.ContentType, DataTypeURL: e.DataTypeURL, } + + md, ok := metadata.FromIncomingContext(ctx) + if ok { + data := []byte{} + for _, header := range []string{"Header1", "Header2"} { + values := md.Get(header) + if len(values) > 0 { + data = append(data, []byte(values[0])...) + } + } + val.Data = data + } return val, nil }) @@ -108,6 +121,14 @@ func TestStandaloneInvoke(t *testing.T) { assert.Contains(t, output, "error invoking app invoke_e2e: 404 Not Found") }) + t.Run(fmt.Sprintf("invoke mehod %s with http headers", path), func(t *testing.T) { + output, err := cmdInvoke("invoke_e2e", "test", path, "--header", "Header1=aValue1", "--header", "Header2=aValue2") + t.Log(output) + assert.NoError(t, err, "") + assert.Contains(t, output, "aValue1") + assert.Contains(t, output, "aValue2") + }) + output, err := cmdStopWithAppID("invoke_e2e") t.Log(output) require.NoError(t, err, "dapr stop failed")