mirror of https://github.com/status-im/consul.git
parent
cda884ac81
commit
f88d4fe28f
|
@ -23,6 +23,11 @@ import (
|
|||
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderConsulToken = "x-consul-token"
|
||||
HeaderConsistencyMode = "x-consul-consistency-mode"
|
||||
)
|
||||
|
||||
func NewHandler(
|
||||
client pbresource.ResourceServiceClient,
|
||||
registry resource.Registry,
|
||||
|
@ -30,8 +35,12 @@ func NewHandler(
|
|||
logger hclog.Logger) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
for _, t := range registry.Types() {
|
||||
// Individual Resource Endpoints.
|
||||
prefix := strings.ToLower(fmt.Sprintf("/%s/%s/%s/", t.Type.Group, t.Type.GroupVersion, t.Type.Kind))
|
||||
// List Endpoint
|
||||
base := strings.ToLower(fmt.Sprintf("/%s/%s/%s", t.Type.Group, t.Type.GroupVersion, t.Type.Kind))
|
||||
mux.Handle(base, http.StripPrefix(base, &listHandler{t, client, parseToken, logger}))
|
||||
|
||||
// Individual Resource Endpoints
|
||||
prefix := strings.ToLower(fmt.Sprintf("%s/", base))
|
||||
logger.Info("Registered resource endpoint", "endpoint", prefix)
|
||||
mux.Handle(prefix, http.StripPrefix(prefix, &resourceHandler{t, client, parseToken, logger}))
|
||||
}
|
||||
|
@ -55,7 +64,7 @@ type resourceHandler struct {
|
|||
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)
|
||||
ctx := metadata.AppendToOutgoingContext(r.Context(), HeaderConsulToken, token)
|
||||
switch r.Method {
|
||||
case http.MethodPut:
|
||||
h.handleWrite(w, r, ctx)
|
||||
|
@ -106,7 +115,7 @@ func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ct
|
|||
},
|
||||
})
|
||||
if err != nil {
|
||||
handleResponseError(err, w, h)
|
||||
handleResponseError(err, w, h.logger)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -133,7 +142,7 @@ func (h *resourceHandler) handleRead(w http.ResponseWriter, r *http.Request, ctx
|
|||
},
|
||||
})
|
||||
if err != nil {
|
||||
handleResponseError(err, w, h)
|
||||
handleResponseError(err, w, h.logger)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -158,7 +167,7 @@ func (h *resourceHandler) handleDelete(w http.ResponseWriter, r *http.Request, c
|
|||
Version: params["version"],
|
||||
})
|
||||
if err != nil {
|
||||
handleResponseError(err, w, h)
|
||||
handleResponseError(err, w, h.logger)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
@ -181,11 +190,12 @@ func parseParams(r *http.Request) (tenancy *pbresource.Tenancy, params map[strin
|
|||
params = make(map[string]string)
|
||||
params["resourceName"] = resourceName
|
||||
params["version"] = query.Get("version")
|
||||
params["namePrefix"] = query.Get("name_prefix")
|
||||
if _, ok := query["consistent"]; ok {
|
||||
params["consistent"] = "true"
|
||||
}
|
||||
|
||||
return
|
||||
return tenancy, params
|
||||
}
|
||||
|
||||
func jsonMarshal(res *pbresource.Resource) ([]byte, error) {
|
||||
|
@ -203,28 +213,82 @@ func jsonMarshal(res *pbresource.Resource) ([]byte, error) {
|
|||
return json.MarshalIndent(stuff, "", " ")
|
||||
}
|
||||
|
||||
func handleResponseError(err error, w http.ResponseWriter, h *resourceHandler) {
|
||||
func handleResponseError(err error, w http.ResponseWriter, logger hclog.Logger) {
|
||||
if e, ok := status.FromError(err); ok {
|
||||
switch e.Code() {
|
||||
case codes.InvalidArgument:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
h.logger.Info("User has mal-formed request", "error", err)
|
||||
logger.Info("User has mal-formed request", "error", err)
|
||||
case codes.NotFound:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
h.logger.Info("Received error from resource service: Not found", "error", err)
|
||||
logger.Info("Received error from resource service: Not found", "error", err)
|
||||
case codes.PermissionDenied:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
h.logger.Info("Received error from resource service: User not authenticated", "error", err)
|
||||
logger.Info("Received error from resource service: User not authenticated", "error", err)
|
||||
case codes.Aborted:
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
h.logger.Info("Received error from resource service: the request conflict with the current state of the target resource", "error", err)
|
||||
logger.Info("Received error from resource service: the request conflict with the current state of the target resource", "error", err)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
h.logger.Error("Received error from resource service", "error", err)
|
||||
logger.Error("Received error from resource service", "error", err)
|
||||
}
|
||||
} else {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
h.logger.Error("Received error from resource service: not able to parse error returned", "error", err)
|
||||
logger.Error("Received error from resource service: not able to parse error returned", "error", err)
|
||||
}
|
||||
w.Write([]byte(err.Error()))
|
||||
}
|
||||
|
||||
type listHandler struct {
|
||||
reg resource.Registration
|
||||
client pbresource.ResourceServiceClient
|
||||
parseToken func(req *http.Request, token *string)
|
||||
logger hclog.Logger
|
||||
}
|
||||
|
||||
func (h *listHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var token string
|
||||
h.parseToken(r, &token)
|
||||
ctx := metadata.AppendToOutgoingContext(r.Context(), HeaderConsulToken, token)
|
||||
|
||||
tenancyInfo, params := parseParams(r)
|
||||
if params["consistent"] == "true" {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, HeaderConsistencyMode, "consistent")
|
||||
}
|
||||
|
||||
rsp, err := h.client.List(ctx, &pbresource.ListRequest{
|
||||
Type: h.reg.Type,
|
||||
Tenancy: tenancyInfo,
|
||||
NamePrefix: params["namePrefix"],
|
||||
})
|
||||
if err != nil {
|
||||
handleResponseError(err, w, h.logger)
|
||||
return
|
||||
}
|
||||
|
||||
output := make([]json.RawMessage, len(rsp.Resources))
|
||||
for idx, res := range rsp.Resources {
|
||||
b, err := jsonMarshal(res)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
h.logger.Error("Failed to unmarshal GRPC resource response", "error", err)
|
||||
return
|
||||
}
|
||||
output[idx] = b
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(struct {
|
||||
Resources []json.RawMessage `json:"resources"`
|
||||
}{output}, "", " ")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
h.logger.Error("Failed to correctly format the list response", "error", err)
|
||||
return
|
||||
}
|
||||
w.Write(b)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ package http
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
@ -27,6 +28,7 @@ import (
|
|||
|
||||
const testACLTokenArtistReadPolicy = "00000000-0000-0000-0000-000000000001"
|
||||
const testACLTokenArtistWritePolicy = "00000000-0000-0000-0000-000000000002"
|
||||
const testACLTokenArtistListPolicy = "00000000-0000-0000-0000-000000000003"
|
||||
const fakeToken = "fake-token"
|
||||
|
||||
func parseToken(req *http.Request, token *string) {
|
||||
|
@ -276,15 +278,27 @@ func TestResourceWriteHandler(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func createResource(t *testing.T, artistHandler http.Handler) map[string]any {
|
||||
type ResourceUri struct {
|
||||
group string
|
||||
version string
|
||||
kind string
|
||||
resourceName string
|
||||
}
|
||||
|
||||
func createResource(t *testing.T, artistHandler http.Handler, resourceUri *ResourceUri) map[string]any {
|
||||
rsp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(`
|
||||
|
||||
if resourceUri == nil {
|
||||
resourceUri = &ResourceUri{group: "demo", version: "v2", kind: "artist", resourceName: "keith-urban"}
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("PUT", fmt.Sprintf("/%s/%s/%s/%s?partition=default&peer_name=local&namespace=default", resourceUri.group, resourceUri.version, resourceUri.kind, resourceUri.resourceName), strings.NewReader(`
|
||||
{
|
||||
"metadata": {
|
||||
"foo": "bar"
|
||||
},
|
||||
"data": {
|
||||
"name": "Keith Urban",
|
||||
"name": "test",
|
||||
"genre": "GENRE_COUNTRY"
|
||||
}
|
||||
}
|
||||
|
@ -300,6 +314,21 @@ func createResource(t *testing.T, artistHandler http.Handler) map[string]any {
|
|||
return result
|
||||
}
|
||||
|
||||
func deleteResource(t *testing.T, artistHandler http.Handler, resourceUri *ResourceUri) {
|
||||
rsp := httptest.NewRecorder()
|
||||
|
||||
if resourceUri == nil {
|
||||
resourceUri = &ResourceUri{group: "demo", version: "v2", kind: "artist", resourceName: "keith-urban"}
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("DELETE", fmt.Sprintf("/%s/%s/%s/%s?partition=default&peer_name=local&namespace=default", resourceUri.group, resourceUri.version, resourceUri.kind, resourceUri.resourceName), strings.NewReader(""))
|
||||
|
||||
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
||||
|
||||
artistHandler.ServeHTTP(rsp, req)
|
||||
require.Equal(t, http.StatusNoContent, rsp.Result().StatusCode)
|
||||
}
|
||||
|
||||
func TestResourceReadHandler(t *testing.T) {
|
||||
aclResolver := &resourceSvc.MockACLResolver{}
|
||||
aclResolver.On("ResolveTokenAndDefaultMeta", testACLTokenArtistReadPolicy, mock.Anything, mock.Anything).
|
||||
|
@ -315,7 +344,7 @@ func TestResourceReadHandler(t *testing.T) {
|
|||
demo.RegisterTypes(r)
|
||||
handler := NewHandler(client, r, parseToken, hclog.NewNullLogger())
|
||||
|
||||
createdResource := createResource(t, handler)
|
||||
createdResource := createResource(t, handler, nil)
|
||||
|
||||
t.Run("Read resource", func(t *testing.T) {
|
||||
rsp := httptest.NewRecorder()
|
||||
|
@ -370,7 +399,7 @@ func TestResourceDeleteHandler(t *testing.T) {
|
|||
handler := NewHandler(client, r, parseToken, hclog.NewNullLogger())
|
||||
|
||||
t.Run("should surface PermissionDenied error from resource service", func(t *testing.T) {
|
||||
createResource(t, handler)
|
||||
createResource(t, handler, nil)
|
||||
|
||||
deleteRsp := httptest.NewRecorder()
|
||||
deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(""))
|
||||
|
@ -383,7 +412,7 @@ func TestResourceDeleteHandler(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("should delete a resource without version", func(t *testing.T) {
|
||||
createResource(t, handler)
|
||||
createResource(t, handler, nil)
|
||||
|
||||
deleteRsp := httptest.NewRecorder()
|
||||
deletReq := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(""))
|
||||
|
@ -409,12 +438,13 @@ func TestResourceDeleteHandler(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("should delete a resource with version", func(t *testing.T) {
|
||||
createResource(t, handler)
|
||||
createResource(t, handler, nil)
|
||||
|
||||
rsp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("DELETE", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&version=1", strings.NewReader(""))
|
||||
|
||||
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
||||
req.Header.Add("x-consul-token", testACLTokenArtistListPolicy)
|
||||
|
||||
handler.ServeHTTP(rsp, req)
|
||||
|
||||
|
@ -430,3 +460,137 @@ func TestResourceDeleteHandler(t *testing.T) {
|
|||
require.ErrorContains(t, err, "resource not found")
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourceListHandler(t *testing.T) {
|
||||
aclResolver := &resourceSvc.MockACLResolver{}
|
||||
aclResolver.On("ResolveTokenAndDefaultMeta", testACLTokenArtistListPolicy, mock.Anything, mock.Anything).
|
||||
Return(svctest.AuthorizerFrom(t, demo.ArtistV2ListPolicy), nil)
|
||||
aclResolver.On("ResolveTokenAndDefaultMeta", testACLTokenArtistWritePolicy, mock.Anything, mock.Anything).
|
||||
Return(svctest.AuthorizerFrom(t, demo.ArtistV2WritePolicy), nil)
|
||||
|
||||
client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes)
|
||||
|
||||
r := resource.NewRegistry()
|
||||
demo.RegisterTypes(r)
|
||||
|
||||
handler := NewHandler(client, r, parseToken, hclog.NewNullLogger())
|
||||
|
||||
t.Run("should return MethodNotAllowed", func(t *testing.T) {
|
||||
rsp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("PUT", "/demo/v2/artist?partition=default&peer_name=local&namespace=default", strings.NewReader(""))
|
||||
|
||||
req.Header.Add("x-consul-token", testACLTokenArtistListPolicy)
|
||||
|
||||
handler.ServeHTTP(rsp, req)
|
||||
|
||||
require.Equal(t, http.StatusMethodNotAllowed, rsp.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("should be blocked if the token is not authorized", func(t *testing.T) {
|
||||
rsp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/demo/v2/artist?partition=default&peer_name=local&namespace=default", strings.NewReader(""))
|
||||
|
||||
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
|
||||
|
||||
handler.ServeHTTP(rsp, req)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, rsp.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("should return list of resources", func(t *testing.T) {
|
||||
resourceUri1 := &ResourceUri{group: "demo", version: "v2", kind: "artist", resourceName: "steve"}
|
||||
resource1 := createResource(t, handler, resourceUri1)
|
||||
resourceUri2 := &ResourceUri{group: "demo", version: "v2", kind: "artist", resourceName: "elvis"}
|
||||
resource2 := createResource(t, handler, resourceUri2)
|
||||
|
||||
rsp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/demo/v2/artist?partition=default&peer_name=local&namespace=default", strings.NewReader(""))
|
||||
|
||||
req.Header.Add("x-consul-token", testACLTokenArtistListPolicy)
|
||||
|
||||
handler.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))
|
||||
|
||||
resources, _ := result["resources"].([]any)
|
||||
require.Len(t, resources, 2)
|
||||
|
||||
expected := []map[string]any{resource1, resource2}
|
||||
require.Contains(t, expected, resources[0])
|
||||
require.Contains(t, expected, resources[1])
|
||||
|
||||
// clean up
|
||||
deleteResource(t, handler, resourceUri1)
|
||||
deleteResource(t, handler, resourceUri2)
|
||||
})
|
||||
|
||||
t.Run("should return empty list when no resources are found", func(t *testing.T) {
|
||||
rsp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/demo/v2/artist?partition=default&peer_name=local&namespace=default", strings.NewReader(""))
|
||||
|
||||
req.Header.Add("x-consul-token", testACLTokenArtistListPolicy)
|
||||
|
||||
handler.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))
|
||||
|
||||
resources, _ := result["resources"].([]any)
|
||||
require.Len(t, resources, 0)
|
||||
})
|
||||
|
||||
t.Run("should return empty list when name prefix matches don't match", func(t *testing.T) {
|
||||
createResource(t, handler, nil)
|
||||
|
||||
rsp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/demo/v2/artist?partition=default&peer_name=local&namespace=default&name_prefix=noname", strings.NewReader(""))
|
||||
|
||||
req.Header.Add("x-consul-token", testACLTokenArtistListPolicy)
|
||||
|
||||
handler.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))
|
||||
|
||||
resources, _ := result["resources"].([]any)
|
||||
require.Len(t, resources, 0)
|
||||
|
||||
// clean up
|
||||
deleteResource(t, handler, nil)
|
||||
})
|
||||
|
||||
t.Run("should return list of resources matching name prefix", func(t *testing.T) {
|
||||
resourceUri1 := &ResourceUri{group: "demo", version: "v2", kind: "artist", resourceName: "steve"}
|
||||
resource1 := createResource(t, handler, resourceUri1)
|
||||
resourceUri2 := &ResourceUri{group: "demo", version: "v2", kind: "artist", resourceName: "elvis"}
|
||||
createResource(t, handler, resourceUri2)
|
||||
|
||||
rsp := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/demo/v2/artist?partition=default&peer_name=local&namespace=default&name_prefix=steve", strings.NewReader(""))
|
||||
|
||||
req.Header.Add("x-consul-token", testACLTokenArtistListPolicy)
|
||||
|
||||
handler.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))
|
||||
|
||||
resources, _ := result["resources"].([]any)
|
||||
require.Len(t, resources, 1)
|
||||
|
||||
require.Equal(t, resource1, resources[0])
|
||||
|
||||
// clean up
|
||||
deleteResource(t, handler, resourceUri1)
|
||||
deleteResource(t, handler, resourceUri2)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue