From 183298e28076848e42e8e8dc8238b75c034ae4c4 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Mon, 17 Jul 2023 13:04:31 -0700 Subject: [PATCH 1/6] init commit --- agent/http.go | 10 ++ internal/resource/http/http.go | 145 ++++++++++++++++++++++++++++ internal/resource/http/http_test.go | 77 +++++++++++++++ internal/resource/registry.go | 13 +++ 4 files changed, 245 insertions(+) create mode 100644 internal/resource/http/http.go create mode 100644 internal/resource/http/http_test.go diff --git a/agent/http.go b/agent/http.go index 32010c343a6..5c3429a8a73 100644 --- a/agent/http.go +++ b/agent/http.go @@ -36,6 +36,7 @@ import ( "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/uiserver" "github.com/hashicorp/consul/api" + resourcehttp "github.com/hashicorp/consul/internal/resource/http" "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/logging" "github.com/hashicorp/consul/proto/private/pbcommon" @@ -259,6 +260,15 @@ func (s *HTTPHandlers) handler() http.Handler { handlePProf("/debug/pprof/symbol", pprof.Symbol) handlePProf("/debug/pprof/trace", pprof.Trace) + mux.Handle("/api/", + http.StripPrefix("/api", + resourcehttp.NewHandler( + s.agent.delegate.ResourceServiceClient(), + s.agent.baseDeps.Registry, + ), + ), + ) + if s.IsUIEnabled() { // Note that we _don't_ support reloading ui_config.{enabled, content_dir, // content_path} since this only runs at initial startup. diff --git a/internal/resource/http/http.go b/internal/resource/http/http.go new file mode 100644 index 00000000000..f92b07a71cc --- /dev/null +++ b/internal/resource/http/http.go @@ -0,0 +1,145 @@ +package http + +import ( + "encoding/json" + "fmt" + "net/http" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/proto-public/pbresource" +) + +func NewHandler(client pbresource.ResourceServiceClient, registry resource.Registry) http.Handler { + mux := http.NewServeMux() + for _, t := range registry.Types() { + // Individual Resource Endpoints. + prefix := fmt.Sprintf("/%s/%s/%s/", t.Type.Group, t.Type.GroupVersion, t.Type.Kind) + fmt.Println("REGISTERED URLS: ", prefix) + mux.Handle(prefix, http.StripPrefix(prefix, &resourceHandler{t, client})) + } + + return mux +} + +type writeRequest struct { + // TODO: Owner. + Version string `json:"version"` + Metadata map[string]string `json:"metadata"` + Data json.RawMessage `json:"data"` +} + +type resourceHandler struct { + reg resource.Registration + client pbresource.ResourceServiceClient +} + +func (h *resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + h.handleRead(w, r) + case http.MethodPut: + h.handleWrite(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + return + } +} + +func (h *resourceHandler) handleRead(w http.ResponseWriter, r *http.Request) { + rsp, err := h.client.Read(r.Context(), &pbresource.ReadRequest{ + Id: &pbresource.ID{ + Type: h.reg.Type, + Tenancy: tenancy(r), + Name: r.URL.Path, + }, + }) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + output, err := jsonMarshal(rsp.Resource) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Write(output) +} + +func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request) { + // do we introduce logger in this server? + //logger := hclog.New(&hclog.LoggerOptions{Name: "xinyi"}) + //logger.Debug("DECODING ERROR", "error", err.Error()) + var req writeRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + data := h.reg.Proto.ProtoReflect().New().Interface() + if err := protojson.Unmarshal(req.Data, data); err != nil { + fmt.Println("UNMARSHAL REQUEST ERROR", err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + + a, err := anypb.New(data) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + rsp, err := h.client.Write(r.Context(), &pbresource.WriteRequest{ + Resource: &pbresource.Resource{ + Id: &pbresource.ID{ + Type: h.reg.Type, + Tenancy: tenancy(r), + Name: r.URL.Path, + }, + Version: req.Version, + Metadata: req.Metadata, + Data: a, + }, + }) + if err != nil { + fmt.Println("WRITE ERROR", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + output, err := jsonMarshal(rsp.Resource) + if err != nil { + fmt.Println("UNMARSHAL RESPONSE ERROR", err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Write(output) +} + +func tenancy(r *http.Request) *pbresource.Tenancy { + // TODO: Read querystring parameters. + return &pbresource.Tenancy{ + Partition: "default", + PeerName: "local", + Namespace: "default", + } +} + +func jsonMarshal(res *pbresource.Resource) ([]byte, error) { + output, err := protojson.Marshal(res) + if err != nil { + return nil, err + } + + var stuff map[string]any + if err := json.Unmarshal(output, &stuff); err != nil { + return nil, err + } + + delete(stuff["data"].(map[string]any), "@type") + return json.MarshalIndent(stuff, "", " ") +} diff --git a/internal/resource/http/http_test.go b/internal/resource/http/http_test.go new file mode 100644 index 00000000000..66d809e018b --- /dev/null +++ b/internal/resource/http/http_test.go @@ -0,0 +1,77 @@ +package http + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing" + "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/internal/resource/demo" + "github.com/hashicorp/consul/proto-public/pbresource" + pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2" + "github.com/hashicorp/consul/sdk/testutil" +) + +func TestHandler(t *testing.T) { + svc := svctest.RunResourceService(t, demo.RegisterTypes) + + r := resource.NewRegistry() + demo.RegisterTypes(r) + + h := NewHandler(svc, r) + + t.Run("Write", func(t *testing.T) { + rsp := httptest.NewRecorder() + req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban", strings.NewReader(` + { + "metadata": { + "foo": "bar" + }, + "data": { + "name": "Keith Urban", + "genre": "GENRE_COUNTRY" + } + } + `)) + + h.ServeHTTP(rsp, req) + + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + + var result map[string]any + require.NoError(t, json.NewDecoder(rsp.Body).Decode(&result)) + require.Equal(t, "Keith Urban", result["data"].(map[string]any)["name"]) + + readRsp, err := svc.Read(testutil.TestContext(t), &pbresource.ReadRequest{ + Id: &pbresource.ID{ + Type: demo.TypeV2Artist, + Tenancy: demo.TenancyDefault, + Name: "keith-urban", + }, + }) + require.NoError(t, err) + require.NotNil(t, readRsp.Resource) + + var artist pbdemov2.Artist + require.NoError(t, readRsp.Resource.Data.UnmarshalTo(&artist)) + require.Equal(t, "Keith Urban", artist.Name) + }) + + t.Run("Read", func(t *testing.T) { + rsp := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban", nil) + + h.ServeHTTP(rsp, req) + + require.Equal(t, http.StatusOK, rsp.Result().StatusCode) + + var result map[string]any + require.NoError(t, json.NewDecoder(rsp.Body).Decode(&result)) + require.Equal(t, "Keith Urban", result["data"].(map[string]any)["name"]) + }) +} diff --git a/internal/resource/registry.go b/internal/resource/registry.go index 0004acfff4c..4f06c6faa10 100644 --- a/internal/resource/registry.go +++ b/internal/resource/registry.go @@ -26,6 +26,8 @@ type Registry interface { // Resolve the given resource type and its hooks. Resolve(typ *pbresource.Type) (reg Registration, ok bool) + + Types() []Registration } type Registration struct { @@ -154,6 +156,17 @@ func (r *TypeRegistry) Resolve(typ *pbresource.Type) (reg Registration, ok bool) return Registration{}, false } +func (r *TypeRegistry) Types() []Registration { + r.lock.RLock() + defer r.lock.RUnlock() + + types := make([]Registration, 0, len(r.registrations)) + for _, v := range r.registrations { + types = append(types, v) + } + return types +} + func ToGVK(resourceType *pbresource.Type) string { return fmt.Sprintf("%s.%s.%s", resourceType.Group, resourceType.GroupVersion, resourceType.Kind) } From eaf297d887d649a4aede3b4992554c50899ba140 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Wed, 19 Jul 2023 17:37:55 -0700 Subject: [PATCH 2/6] pass x-consul-token to the grpc server --- agent/http.go | 1 + internal/resource/http/http.go | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/agent/http.go b/agent/http.go index 5c3429a8a73..66e346847b4 100644 --- a/agent/http.go +++ b/agent/http.go @@ -265,6 +265,7 @@ func (s *HTTPHandlers) handler() http.Handler { resourcehttp.NewHandler( s.agent.delegate.ResourceServiceClient(), s.agent.baseDeps.Registry, + s.parseToken, ), ), ) diff --git a/internal/resource/http/http.go b/internal/resource/http/http.go index f92b07a71cc..c0c4a927a20 100644 --- a/internal/resource/http/http.go +++ b/internal/resource/http/http.go @@ -1,10 +1,12 @@ package http import ( + "context" "encoding/json" "fmt" "net/http" + "google.golang.org/grpc/metadata" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/anypb" @@ -12,13 +14,13 @@ import ( "github.com/hashicorp/consul/proto-public/pbresource" ) -func NewHandler(client pbresource.ResourceServiceClient, registry resource.Registry) http.Handler { +func NewHandler(client pbresource.ResourceServiceClient, registry resource.Registry, parseToken func(req *http.Request, token *string)) http.Handler { mux := http.NewServeMux() for _, t := range registry.Types() { // Individual Resource Endpoints. prefix := fmt.Sprintf("/%s/%s/%s/", t.Type.Group, t.Type.GroupVersion, t.Type.Kind) fmt.Println("REGISTERED URLS: ", prefix) - mux.Handle(prefix, http.StripPrefix(prefix, &resourceHandler{t, client})) + mux.Handle(prefix, http.StripPrefix(prefix, &resourceHandler{t, client, parseToken})) } return mux @@ -32,24 +34,28 @@ type writeRequest struct { } type resourceHandler struct { - reg resource.Registration - client pbresource.ResourceServiceClient + reg resource.Registration + client pbresource.ResourceServiceClient + parseToken func(req *http.Request, token *string) } func (h *resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var token string + h.parseToken(r, &token) + ctx := metadata.AppendToOutgoingContext(r.Context(), "x-consul-token", token) switch r.Method { case http.MethodGet: - h.handleRead(w, r) + h.handleRead(w, r, ctx) case http.MethodPut: - h.handleWrite(w, r) + h.handleWrite(w, r, ctx) default: w.WriteHeader(http.StatusMethodNotAllowed) return } } -func (h *resourceHandler) handleRead(w http.ResponseWriter, r *http.Request) { - rsp, err := h.client.Read(r.Context(), &pbresource.ReadRequest{ +func (h *resourceHandler) handleRead(w http.ResponseWriter, r *http.Request, ctx context.Context) { + rsp, err := h.client.Read(ctx, &pbresource.ReadRequest{ Id: &pbresource.ID{ Type: h.reg.Type, Tenancy: tenancy(r), @@ -70,7 +76,7 @@ func (h *resourceHandler) handleRead(w http.ResponseWriter, r *http.Request) { w.Write(output) } -func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request) { +func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ctx context.Context) { // do we introduce logger in this server? //logger := hclog.New(&hclog.LoggerOptions{Name: "xinyi"}) //logger.Debug("DECODING ERROR", "error", err.Error()) @@ -93,7 +99,7 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request) { return } - rsp, err := h.client.Write(r.Context(), &pbresource.WriteRequest{ + rsp, err := h.client.Write(ctx, &pbresource.WriteRequest{ Resource: &pbresource.Resource{ Id: &pbresource.ID{ Type: h.reg.Type, From 97dacbc29304b0e56873e3f779f394794346a24b Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Thu, 20 Jul 2023 10:30:23 -0700 Subject: [PATCH 3/6] query params and add logger --- agent/http.go | 1 + internal/resource/http/http.go | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/agent/http.go b/agent/http.go index 66e346847b4..31aaf68c201 100644 --- a/agent/http.go +++ b/agent/http.go @@ -266,6 +266,7 @@ func (s *HTTPHandlers) handler() http.Handler { s.agent.delegate.ResourceServiceClient(), s.agent.baseDeps.Registry, s.parseToken, + s.agent.logger.Named(logging.HTTP), ), ), ) diff --git a/internal/resource/http/http.go b/internal/resource/http/http.go index c0c4a927a20..548745c8c58 100644 --- a/internal/resource/http/http.go +++ b/internal/resource/http/http.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" + "github.com/hashicorp/go-hclog" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/anypb" @@ -14,13 +15,17 @@ import ( "github.com/hashicorp/consul/proto-public/pbresource" ) -func NewHandler(client pbresource.ResourceServiceClient, registry resource.Registry, parseToken func(req *http.Request, token *string)) http.Handler { +func NewHandler( + client pbresource.ResourceServiceClient, + registry resource.Registry, + parseToken func(req *http.Request, token *string), + logger hclog.Logger) http.Handler { mux := http.NewServeMux() for _, t := range registry.Types() { // Individual Resource Endpoints. prefix := fmt.Sprintf("/%s/%s/%s/", t.Type.Group, t.Type.GroupVersion, t.Type.Kind) fmt.Println("REGISTERED URLS: ", prefix) - mux.Handle(prefix, http.StripPrefix(prefix, &resourceHandler{t, client, parseToken})) + mux.Handle(prefix, http.StripPrefix(prefix, &resourceHandler{t, client, parseToken, logger})) } return mux @@ -37,6 +42,7 @@ type resourceHandler struct { reg resource.Registration client pbresource.ResourceServiceClient parseToken func(req *http.Request, token *string) + logger hclog.Logger } func (h *resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -88,7 +94,6 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ct data := h.reg.Proto.ProtoReflect().New().Interface() if err := protojson.Unmarshal(req.Data, data); err != nil { - fmt.Println("UNMARSHAL REQUEST ERROR", err.Error()) w.WriteHeader(http.StatusBadRequest) return } @@ -127,11 +132,15 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ct } func tenancy(r *http.Request) *pbresource.Tenancy { - // TODO: Read querystring parameters. + // are partition peername and namespace required fields? + params := r.URL.Query() + partition := params.Get("partition") + peername := params.Get("peer_name") + namespace := params.Get("namespace") return &pbresource.Tenancy{ - Partition: "default", - PeerName: "local", - Namespace: "default", + Partition: partition, + PeerName: peername, + Namespace: namespace, } } From 70624293601869c8314bf210fa0ebc09e641a258 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Thu, 20 Jul 2023 11:21:44 -0700 Subject: [PATCH 4/6] log message --- internal/resource/http/http.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/resource/http/http.go b/internal/resource/http/http.go index 548745c8c58..9937af8f290 100644 --- a/internal/resource/http/http.go +++ b/internal/resource/http/http.go @@ -6,11 +6,12 @@ import ( "fmt" "net/http" - "github.com/hashicorp/go-hclog" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/anypb" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/proto-public/pbresource" ) @@ -24,7 +25,7 @@ func NewHandler( for _, t := range registry.Types() { // Individual Resource Endpoints. prefix := fmt.Sprintf("/%s/%s/%s/", t.Type.Group, t.Type.GroupVersion, t.Type.Kind) - fmt.Println("REGISTERED URLS: ", prefix) + logger.Info("Registered resource endpoint: ", prefix) mux.Handle(prefix, http.StripPrefix(prefix, &resourceHandler{t, client, parseToken, logger})) } @@ -83,24 +84,23 @@ func (h *resourceHandler) handleRead(w http.ResponseWriter, r *http.Request, ctx } func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ctx context.Context) { - // do we introduce logger in this server? - //logger := hclog.New(&hclog.LoggerOptions{Name: "xinyi"}) - //logger.Debug("DECODING ERROR", "error", err.Error()) var req writeRequest + // convert req data to struct if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) return } - + // struct to proto message data := h.reg.Proto.ProtoReflect().New().Interface() if err := protojson.Unmarshal(req.Data, data); err != nil { w.WriteHeader(http.StatusBadRequest) return } - - a, err := anypb.New(data) + // proto message to any + anyProtoMsg, err := anypb.New(data) if err != nil { w.WriteHeader(http.StatusInternalServerError) + h.logger.Error("Failed to convert proto message to any type: ", err) return } @@ -113,19 +113,19 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ct }, Version: req.Version, Metadata: req.Metadata, - Data: a, + Data: anyProtoMsg, }, }) if err != nil { - fmt.Println("WRITE ERROR", err.Error()) w.WriteHeader(http.StatusInternalServerError) + h.logger.Error("Failed to write to GRPC resource: ", err) return } output, err := jsonMarshal(rsp.Resource) if err != nil { - fmt.Println("UNMARSHAL RESPONSE ERROR", err.Error()) w.WriteHeader(http.StatusInternalServerError) + h.logger.Error("Failed to unmarshal GRPC resource response: ", err) return } w.Write(output) From e69aa2dd64bb1d716e82a792132b63bf7e6f17dc Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Thu, 20 Jul 2023 11:29:34 -0700 Subject: [PATCH 5/6] change log message --- internal/resource/http/http.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/resource/http/http.go b/internal/resource/http/http.go index 9937af8f290..d40b719d811 100644 --- a/internal/resource/http/http.go +++ b/internal/resource/http/http.go @@ -25,7 +25,7 @@ func NewHandler( for _, t := range registry.Types() { // Individual Resource Endpoints. prefix := fmt.Sprintf("/%s/%s/%s/", t.Type.Group, t.Type.GroupVersion, t.Type.Kind) - logger.Info("Registered resource endpoint: ", prefix) + logger.Info("Registered resource endpoint", "endpoint", prefix) mux.Handle(prefix, http.StripPrefix(prefix, &resourceHandler{t, client, parseToken, logger})) } @@ -100,7 +100,7 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ct anyProtoMsg, err := anypb.New(data) if err != nil { w.WriteHeader(http.StatusInternalServerError) - h.logger.Error("Failed to convert proto message to any type: ", err) + h.logger.Error("Failed to convert proto message to any type", "error", err) return } @@ -118,14 +118,14 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ct }) if err != nil { w.WriteHeader(http.StatusInternalServerError) - h.logger.Error("Failed to write to GRPC resource: ", err) + h.logger.Error("Failed to write to GRPC resource", "error", err) return } output, err := jsonMarshal(rsp.Resource) if err != nil { w.WriteHeader(http.StatusInternalServerError) - h.logger.Error("Failed to unmarshal GRPC resource response: ", err) + h.logger.Error("Failed to unmarshal GRPC resource response", "error", err) return } w.Write(output) From 4975291f45fbcb69277f1c58461c9d1d106b7568 Mon Sep 17 00:00:00 2001 From: Xinyi Wang Date: Thu, 20 Jul 2023 15:04:54 -0700 Subject: [PATCH 6/6] fix unit test --- internal/resource/http/http.go | 50 +++++++++++++++++++++-------- internal/resource/http/http_test.go | 37 +++++++++------------ 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/internal/resource/http/http.go b/internal/resource/http/http.go index d40b719d811..161a859f73b 100644 --- a/internal/resource/http/http.go +++ b/internal/resource/http/http.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "net/http" + "path" + "strings" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/encoding/protojson" @@ -24,7 +26,7 @@ func NewHandler( mux := http.NewServeMux() for _, t := range registry.Types() { // Individual Resource Endpoints. - prefix := fmt.Sprintf("/%s/%s/%s/", t.Type.Group, t.Type.GroupVersion, t.Type.Kind) + prefix := strings.ToLower(fmt.Sprintf("/%s/%s/%s/", t.Type.Group, t.Type.GroupVersion, t.Type.Kind)) logger.Info("Registered resource endpoint", "endpoint", prefix) mux.Handle(prefix, http.StripPrefix(prefix, &resourceHandler{t, client, parseToken, logger})) } @@ -62,10 +64,15 @@ func (h *resourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (h *resourceHandler) handleRead(w http.ResponseWriter, r *http.Request, ctx context.Context) { + tenancyInfo, _ := checkURL(r) + if tenancyInfo == nil { + w.WriteHeader(http.StatusBadRequest) + return + } rsp, err := h.client.Read(ctx, &pbresource.ReadRequest{ Id: &pbresource.ID{ Type: h.reg.Type, - Tenancy: tenancy(r), + Tenancy: tenancyInfo, Name: r.URL.Path, }, }) @@ -88,13 +95,13 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ct // convert req data to struct if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.WriteHeader(http.StatusBadRequest) - return + w.Write([]byte("Request body didn't follow schema.")) } // struct to proto message data := h.reg.Proto.ProtoReflect().New().Interface() if err := protojson.Unmarshal(req.Data, data); err != nil { - w.WriteHeader(http.StatusBadRequest) - return + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Request body didn't follow schema.")) } // proto message to any anyProtoMsg, err := anypb.New(data) @@ -104,12 +111,21 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ct return } + tenancyInfo, resourceName := checkURL(r) + if tenancyInfo == nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Missing partition, peer_name or namespace in the query params")) + } + if resourceName == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Missing resource name in the URL")) + } rsp, err := h.client.Write(ctx, &pbresource.WriteRequest{ Resource: &pbresource.Resource{ Id: &pbresource.ID{ Type: h.reg.Type, - Tenancy: tenancy(r), - Name: r.URL.Path, + Tenancy: tenancyInfo, + Name: resourceName, }, Version: req.Version, Metadata: req.Metadata, @@ -131,17 +147,23 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ct w.Write(output) } -func tenancy(r *http.Request) *pbresource.Tenancy { - // are partition peername and namespace required fields? +func checkURL(r *http.Request) (tenancy *pbresource.Tenancy, resourceName string) { params := r.URL.Query() partition := params.Get("partition") - peername := params.Get("peer_name") + peerName := params.Get("peer_name") namespace := params.Get("namespace") - return &pbresource.Tenancy{ - Partition: partition, - PeerName: peername, - Namespace: namespace, + if partition == "" || peerName == "" || namespace == "" { + tenancy = nil + } else { + tenancy = &pbresource.Tenancy{ + Partition: partition, + PeerName: peerName, + Namespace: namespace, + } } + resourceName = path.Base(r.URL.Path) + + return } func jsonMarshal(res *pbresource.Resource) ([]byte, error) { diff --git a/internal/resource/http/http_test.go b/internal/resource/http/http_test.go index 66d809e018b..6bc9be9a2cf 100644 --- a/internal/resource/http/http_test.go +++ b/internal/resource/http/http_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing" @@ -17,17 +18,22 @@ import ( "github.com/hashicorp/consul/sdk/testutil" ) -func TestHandler(t *testing.T) { - svc := svctest.RunResourceService(t, demo.RegisterTypes) +func TestResourceHandler(t *testing.T) { + client := svctest.RunResourceService(t, demo.RegisterTypes) - r := resource.NewRegistry() - demo.RegisterTypes(r) - - h := NewHandler(svc, r) + resourceHandler := resourceHandler{ + resource.Registration{ + Type: demo.TypeV2Artist, + Proto: &pbdemov2.Artist{}, + }, + client, + func(req *http.Request, token *string) { return }, + hclog.NewNullLogger(), + } t.Run("Write", func(t *testing.T) { rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban", strings.NewReader(` + req := httptest.NewRequest("PUT", "/api/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(` { "metadata": { "foo": "bar" @@ -39,7 +45,7 @@ func TestHandler(t *testing.T) { } `)) - h.ServeHTTP(rsp, req) + resourceHandler.ServeHTTP(rsp, req) require.Equal(t, http.StatusOK, rsp.Result().StatusCode) @@ -47,7 +53,7 @@ func TestHandler(t *testing.T) { require.NoError(t, json.NewDecoder(rsp.Body).Decode(&result)) require.Equal(t, "Keith Urban", result["data"].(map[string]any)["name"]) - readRsp, err := svc.Read(testutil.TestContext(t), &pbresource.ReadRequest{ + readRsp, err := client.Read(testutil.TestContext(t), &pbresource.ReadRequest{ Id: &pbresource.ID{ Type: demo.TypeV2Artist, Tenancy: demo.TenancyDefault, @@ -61,17 +67,4 @@ func TestHandler(t *testing.T) { require.NoError(t, readRsp.Resource.Data.UnmarshalTo(&artist)) require.Equal(t, "Keith Urban", artist.Name) }) - - t.Run("Read", func(t *testing.T) { - rsp := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/demo/v2/artist/keith-urban", nil) - - h.ServeHTTP(rsp, req) - - require.Equal(t, http.StatusOK, rsp.Result().StatusCode) - - var result map[string]any - require.NoError(t, json.NewDecoder(rsp.Body).Decode(&result)) - require.Equal(t, "Keith Urban", result["data"].(map[string]any)["name"]) - }) }