expose grpc as http endpoint (#18221)

expose resource grpc endpoints as http endpoints
This commit is contained in:
wangxinyi7 2023-08-04 11:27:48 -07:00 committed by GitHub
parent 0a48a24a2f
commit 1f28ac2664
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 563 additions and 5 deletions

View File

@ -1601,14 +1601,35 @@ func TestAgent_Metrics_ACLDeny(t *testing.T) {
}) })
} }
func newDefaultBaseDeps(t *testing.T) BaseDeps {
dataDir := testutil.TempDir(t, "acl-agent")
logBuffer := testutil.NewLogBuffer(t)
logger := hclog.NewInterceptLogger(nil)
loader := func(source config.Source) (config.LoadResult, error) {
dataDir := fmt.Sprintf(`data_dir = "%s"`, dataDir)
opts := config.LoadOpts{
HCL: []string{TestConfigHCL(NodeID()), "", dataDir},
DefaultConfig: source,
}
result, err := config.Load(opts)
if result.RuntimeConfig != nil {
result.RuntimeConfig.Telemetry.Disable = true
}
return result, err
}
bd, err := NewBaseDeps(loader, logBuffer, logger)
require.NoError(t, err)
return bd
}
func TestHTTPHandlers_AgentMetricsStream_ACLDeny(t *testing.T) { func TestHTTPHandlers_AgentMetricsStream_ACLDeny(t *testing.T) {
bd := BaseDeps{} bd := newDefaultBaseDeps(t)
bd.Tokens = new(tokenStore.Store) bd.Tokens = new(tokenStore.Store)
sink := metrics.NewInmemSink(30*time.Millisecond, time.Second) sink := metrics.NewInmemSink(30*time.Millisecond, time.Second)
bd.MetricsConfig = &lib.MetricsConfig{ bd.MetricsConfig = &lib.MetricsConfig{
Handler: sink, Handler: sink,
} }
d := fakeResolveTokenDelegate{authorizer: acl.DenyAll()} d := fakeResolveTokenDelegate{delegate: &delegateMock{}, authorizer: acl.DenyAll()}
agent := &Agent{ agent := &Agent{
baseDeps: bd, baseDeps: bd,
delegate: d, delegate: d,
@ -1631,13 +1652,13 @@ func TestHTTPHandlers_AgentMetricsStream_ACLDeny(t *testing.T) {
} }
func TestHTTPHandlers_AgentMetricsStream(t *testing.T) { func TestHTTPHandlers_AgentMetricsStream(t *testing.T) {
bd := BaseDeps{} bd := newDefaultBaseDeps(t)
bd.Tokens = new(tokenStore.Store) bd.Tokens = new(tokenStore.Store)
sink := metrics.NewInmemSink(20*time.Millisecond, time.Second) sink := metrics.NewInmemSink(20*time.Millisecond, time.Second)
bd.MetricsConfig = &lib.MetricsConfig{ bd.MetricsConfig = &lib.MetricsConfig{
Handler: sink, Handler: sink,
} }
d := fakeResolveTokenDelegate{authorizer: acl.ManageAll()} d := fakeResolveTokenDelegate{delegate: &delegateMock{}, authorizer: acl.ManageAll()}
agent := &Agent{ agent := &Agent{
baseDeps: bd, baseDeps: bd,
delegate: d, delegate: d,

View File

@ -57,6 +57,7 @@ import (
"github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest" "github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/ipaddr" "github.com/hashicorp/consul/ipaddr"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/proto/private/pbautoconf" "github.com/hashicorp/consul/proto/private/pbautoconf"
@ -322,6 +323,7 @@ func TestAgent_HTTPMaxHeaderBytes(t *testing.T) {
Tokens: new(token.Store), Tokens: new(token.Store),
TLSConfigurator: tlsConf, TLSConfigurator: tlsConf,
GRPCConnPool: &fakeGRPCConnPool{}, GRPCConnPool: &fakeGRPCConnPool{},
Registry: resource.NewRegistry(),
}, },
RuntimeConfig: &config.RuntimeConfig{ RuntimeConfig: &config.RuntimeConfig{
HTTPAddrs: []net.Addr{ HTTPAddrs: []net.Addr{
@ -344,6 +346,7 @@ func TestAgent_HTTPMaxHeaderBytes(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
a, err := New(bd) a, err := New(bd)
a.delegate = &delegateMock{}
require.NoError(t, err) require.NoError(t, err)
a.startLicenseManager(testutil.TestContext(t)) a.startLicenseManager(testutil.TestContext(t))
@ -5477,6 +5480,7 @@ func TestAgent_ListenHTTP_MultipleAddresses(t *testing.T) {
Tokens: new(token.Store), Tokens: new(token.Store),
TLSConfigurator: tlsConf, TLSConfigurator: tlsConf,
GRPCConnPool: &fakeGRPCConnPool{}, GRPCConnPool: &fakeGRPCConnPool{},
Registry: resource.NewRegistry(),
}, },
RuntimeConfig: &config.RuntimeConfig{ RuntimeConfig: &config.RuntimeConfig{
HTTPAddrs: []net.Addr{ HTTPAddrs: []net.Addr{
@ -5499,6 +5503,7 @@ func TestAgent_ListenHTTP_MultipleAddresses(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
agent, err := New(bd) agent, err := New(bd)
agent.delegate = &delegateMock{}
require.NoError(t, err) require.NoError(t, err)
agent.startLicenseManager(testutil.TestContext(t)) agent.startLicenseManager(testutil.TestContext(t))
@ -6073,6 +6078,7 @@ func TestAgent_startListeners(t *testing.T) {
Logger: hclog.NewInterceptLogger(nil), Logger: hclog.NewInterceptLogger(nil),
Tokens: new(token.Store), Tokens: new(token.Store),
GRPCConnPool: &fakeGRPCConnPool{}, GRPCConnPool: &fakeGRPCConnPool{},
Registry: resource.NewRegistry(),
}, },
RuntimeConfig: &config.RuntimeConfig{ RuntimeConfig: &config.RuntimeConfig{
HTTPAddrs: []net.Addr{}, HTTPAddrs: []net.Addr{},
@ -6091,6 +6097,7 @@ func TestAgent_startListeners(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
agent, err := New(bd) agent, err := New(bd)
agent.delegate = &delegateMock{}
require.NoError(t, err) require.NoError(t, err)
// use up an address // use up an address
@ -6213,6 +6220,7 @@ func TestAgent_startListeners_scada(t *testing.T) {
HCP: hcp.Deps{ HCP: hcp.Deps{
Provider: pvd, Provider: pvd,
}, },
Registry: resource.NewRegistry(),
}, },
RuntimeConfig: &config.RuntimeConfig{}, RuntimeConfig: &config.RuntimeConfig{},
Cache: cache.New(cache.Options{}), Cache: cache.New(cache.Options{}),
@ -6230,6 +6238,7 @@ func TestAgent_startListeners_scada(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
agent, err := New(bd) agent, err := New(bd)
agent.delegate = &delegateMock{}
require.NoError(t, err) require.NoError(t, err)
_, err = agent.startListeners([]net.Addr{c}) _, err = agent.startListeners([]net.Addr{c})
@ -6273,6 +6282,7 @@ func TestAgent_checkServerLastSeen(t *testing.T) {
Logger: hclog.NewInterceptLogger(nil), Logger: hclog.NewInterceptLogger(nil),
Tokens: new(token.Store), Tokens: new(token.Store),
GRPCConnPool: &fakeGRPCConnPool{}, GRPCConnPool: &fakeGRPCConnPool{},
Registry: resource.NewRegistry(),
}, },
RuntimeConfig: &config.RuntimeConfig{}, RuntimeConfig: &config.RuntimeConfig{},
Cache: cache.New(cache.Options{}), Cache: cache.New(cache.Options{}),
@ -6284,6 +6294,7 @@ func TestAgent_checkServerLastSeen(t *testing.T) {
Config: leafcert.Config{}, Config: leafcert.Config{},
}) })
agent, err := New(bd) agent, err := New(bd)
agent.delegate = &delegateMock{}
require.NoError(t, err) require.NoError(t, err)
// Test that an ErrNotExist OS error is treated as ok. // Test that an ErrNotExist OS error is treated as ok.

View File

@ -8,18 +8,50 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/acl/resolver" "github.com/hashicorp/consul/acl/resolver"
svc "github.com/hashicorp/consul/agent/grpc-external/services/resource" svc "github.com/hashicorp/consul/agent/grpc-external/services/resource"
internal "github.com/hashicorp/consul/agent/grpc-internal" internal "github.com/hashicorp/consul/agent/grpc-internal"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/storage/inmem" "github.com/hashicorp/consul/internal/storage/inmem"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil"
) )
func randomACLIdentity(t *testing.T) structs.ACLIdentity {
id, err := uuid.GenerateUUID()
require.NoError(t, err)
return &structs.ACLToken{AccessorID: id}
}
func AuthorizerFrom(t *testing.T, policyStrs ...string) resolver.Result {
policies := []*acl.Policy{}
for _, policyStr := range policyStrs {
policy, err := acl.NewPolicyFromSource(policyStr, nil, nil)
require.NoError(t, err)
policies = append(policies, policy)
}
authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), policies, nil)
require.NoError(t, err)
return resolver.Result{
Authorizer: authz,
ACLIdentity: randomACLIdentity(t),
}
}
// RunResourceService runs a Resource Service for the duration of the test and // RunResourceService runs a Resource Service for the duration of the test and
// returns a client to interact with it. ACLs will be disabled. // returns a client to interact with it. ACLs will be disabled.
func RunResourceService(t *testing.T, registerFns ...func(resource.Registry)) pbresource.ResourceServiceClient { func RunResourceService(t *testing.T, registerFns ...func(resource.Registry)) pbresource.ResourceServiceClient {
return RunResourceServiceWithACL(t, resolver.DANGER_NO_AUTH{}, registerFns...)
}
func RunResourceServiceWithACL(t *testing.T, aclResolver svc.ACLResolver, registerFns ...func(resource.Registry)) pbresource.ResourceServiceClient {
t.Helper() t.Helper()
backend, err := inmem.NewBackend() backend, err := inmem.NewBackend()
@ -40,7 +72,7 @@ func RunResourceService(t *testing.T, registerFns ...func(resource.Registry)) pb
Backend: backend, Backend: backend,
Registry: registry, Registry: registry,
Logger: testutil.Logger(t), Logger: testutil.Logger(t),
ACLResolver: resolver.DANGER_NO_AUTH{}, ACLResolver: aclResolver,
}).Register(server) }).Register(server)
pipe := internal.NewPipeListener() pipe := internal.NewPipeListener()

View File

@ -36,6 +36,7 @@ import (
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/uiserver" "github.com/hashicorp/consul/agent/uiserver"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
resourcehttp "github.com/hashicorp/consul/internal/resource/http"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/logging" "github.com/hashicorp/consul/logging"
"github.com/hashicorp/consul/proto/private/pbcommon" "github.com/hashicorp/consul/proto/private/pbcommon"
@ -259,6 +260,17 @@ func (s *HTTPHandlers) handler() http.Handler {
handlePProf("/debug/pprof/symbol", pprof.Symbol) handlePProf("/debug/pprof/symbol", pprof.Symbol)
handlePProf("/debug/pprof/trace", pprof.Trace) handlePProf("/debug/pprof/trace", pprof.Trace)
mux.Handle("/api/",
http.StripPrefix("/api",
resourcehttp.NewHandler(
s.agent.delegate.ResourceServiceClient(),
s.agent.baseDeps.Registry,
s.parseToken,
s.agent.logger.Named(logging.HTTP),
),
),
)
if s.IsUIEnabled() { if s.IsUIEnabled() {
// Note that we _don't_ support reloading ui_config.{enabled, content_dir, // Note that we _don't_ support reloading ui_config.{enabled, content_dir,
// content_path} since this only runs at initial startup. // content_path} since this only runs at initial startup.

View File

@ -0,0 +1,170 @@
package http
import (
"context"
"encoding/json"
"fmt"
"net/http"
"path"
"strings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"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"
)
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 := 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}))
}
return mux
}
type writeRequest struct {
Metadata map[string]string `json:"metadata"`
Data json.RawMessage `json:"data"`
Owner *pbresource.ID `json:"owner"`
}
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) {
var token string
h.parseToken(r, &token)
ctx := metadata.AppendToOutgoingContext(r.Context(), "x-consul-token", token)
switch r.Method {
case http.MethodPut:
h.handleWrite(w, r, ctx)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
}
func (h *resourceHandler) handleWrite(w http.ResponseWriter, r *http.Request, ctx context.Context) {
var req writeRequest
// convert req body to writeRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Request body didn't follow schema."))
}
// convert data struct to proto message
data := h.reg.Proto.ProtoReflect().New().Interface()
if err := protojson.Unmarshal(req.Data, data); err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Request body didn't follow schema."))
}
// 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", "error", err)
return
}
tenancyInfo, resourceName, version := checkURL(r)
rsp, err := h.client.Write(ctx, &pbresource.WriteRequest{
Resource: &pbresource.Resource{
Id: &pbresource.ID{
Type: h.reg.Type,
Tenancy: tenancyInfo,
Name: resourceName,
},
Owner: req.Owner,
Version: version,
Metadata: req.Metadata,
Data: anyProtoMsg,
},
})
if err != nil {
handleResponseError(err, w, h)
return
}
output, err := jsonMarshal(rsp.Resource)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
h.logger.Error("Failed to unmarshal GRPC resource response", "error", err)
return
}
w.Write(output)
}
func checkURL(r *http.Request) (tenancy *pbresource.Tenancy, resourceName string, version string) {
params := r.URL.Query()
tenancy = &pbresource.Tenancy{
Partition: params.Get("partition"),
PeerName: params.Get("peer_name"),
Namespace: params.Get("namespace"),
}
resourceName = path.Base(r.URL.Path)
if resourceName == "." || resourceName == "/" {
resourceName = ""
}
version = params.Get("version")
return
}
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, "", " ")
}
func handleResponseError(err error, w http.ResponseWriter, h *resourceHandler) {
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)
case codes.NotFound:
w.WriteHeader(http.StatusNotFound)
h.logger.Info("Failed to write to GRPC resource: Not found", "error", err)
case codes.PermissionDenied:
w.WriteHeader(http.StatusForbidden)
h.logger.Info("Failed to write to GRPC resource: User not authenticated", "error", err)
case codes.Aborted:
w.WriteHeader(http.StatusConflict)
h.logger.Info("Failed to write to GRPC resource: the request conflict with the current state of the target resource", "error", err)
default:
w.WriteHeader(http.StatusInternalServerError)
h.logger.Error("Failed to write to GRPC resource", "error", err)
}
} else {
w.WriteHeader(http.StatusInternalServerError)
h.logger.Error("Failed to write to GRPC resource: not able to parse error returned", "error", err)
}
w.Write([]byte(err.Error()))
}

View File

@ -0,0 +1,299 @@
package http
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
resourceSvc "github.com/hashicorp/consul/agent/grpc-external/services/resource"
svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing"
pbdemov1 "github.com/hashicorp/consul/proto/private/pbdemo/v1"
"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"
)
const testACLTokenArtistReadPolicy = "00000000-0000-0000-0000-000000000001"
const testACLTokenArtistWritePolicy = "00000000-0000-0000-0000-000000000002"
func parseToken(req *http.Request, token *string) {
*token = req.Header.Get("X-Consul-Token")
}
func TestResourceHandler_InputValidation(t *testing.T) {
type testCase struct {
description string
request *http.Request
response *httptest.ResponseRecorder
expectedResponseCode int
}
client := svctest.RunResourceService(t, demo.RegisterTypes)
resourceHandler := resourceHandler{
resource.Registration{
Type: demo.TypeV2Artist,
Proto: &pbdemov2.Artist{},
},
client,
func(req *http.Request, token *string) { return },
hclog.NewNullLogger(),
}
testCases := []testCase{
{
description: "missing resource name",
request: httptest.NewRequest("PUT", "/?partition=default&peer_name=local&namespace=default", strings.NewReader(`
{
"metadata": {
"foo": "bar"
},
"data": {
"name": "Keith Urban",
"genre": "GENRE_COUNTRY"
}
}
`)),
response: httptest.NewRecorder(),
expectedResponseCode: http.StatusBadRequest,
},
{
description: "wrong schema",
request: httptest.NewRequest("PUT", "/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(`
{
"metadata": {
"foo": "bar"
},
"dada": {
"name": "Keith Urban",
"genre": "GENRE_COUNTRY"
}
}
`)),
response: httptest.NewRecorder(),
expectedResponseCode: http.StatusBadRequest,
},
{
description: "missing tenancy info",
request: httptest.NewRequest("PUT", "/keith-urban?partition=default&peer_name=local", strings.NewReader(`
{
"metadata": {
"foo": "bar"
},
"data": {
"name": "Keith Urban",
"genre": "GENRE_COUNTRY"
}
}
`)),
response: httptest.NewRecorder(),
expectedResponseCode: http.StatusBadRequest,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
resourceHandler.ServeHTTP(tc.response, tc.request)
require.Equal(t, tc.expectedResponseCode, tc.response.Result().StatusCode)
})
}
}
func TestResourceWriteHandler(t *testing.T) {
aclResolver := &resourceSvc.MockACLResolver{}
aclResolver.On("ResolveTokenAndDefaultMeta", testACLTokenArtistReadPolicy, mock.Anything, mock.Anything).
Return(svctest.AuthorizerFrom(t, demo.ArtistV1ReadPolicy, demo.ArtistV2ReadPolicy), nil)
aclResolver.On("ResolveTokenAndDefaultMeta", testACLTokenArtistWritePolicy, mock.Anything, mock.Anything).
Return(svctest.AuthorizerFrom(t, demo.ArtistV1WritePolicy, demo.ArtistV2WritePolicy), nil)
client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes)
v1ArtistHandler := resourceHandler{
resource.Registration{
Type: demo.TypeV1Artist,
Proto: &pbdemov1.Artist{},
},
client,
parseToken,
hclog.NewNullLogger(),
}
v2ArtistHandler := resourceHandler{
resource.Registration{
Type: demo.TypeV2Artist,
Proto: &pbdemov2.Artist{},
},
client,
parseToken,
hclog.NewNullLogger(),
}
t.Run("should be blocked if the token is not authorized", func(t *testing.T) {
rsp := httptest.NewRecorder()
req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(`
{
"metadata": {
"foo": "bar"
},
"data": {
"name": "Keith Urban",
"genre": "GENRE_COUNTRY"
}
}
`))
req.Header.Add("x-consul-token", testACLTokenArtistReadPolicy)
v2ArtistHandler.ServeHTTP(rsp, req)
require.Equal(t, http.StatusForbidden, rsp.Result().StatusCode)
})
t.Run("should write to the resource backend", func(t *testing.T) {
rsp := httptest.NewRecorder()
req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default", strings.NewReader(`
{
"metadata": {
"foo": "bar"
},
"data": {
"name": "Keith Urban",
"genre": "GENRE_COUNTRY"
}
}
`))
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
v2ArtistHandler.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"])
require.Equal(t, "keith-urban", result["id"].(map[string]any)["name"])
readRsp, err := client.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("should update the record with version parameter", func(t *testing.T) {
rsp := httptest.NewRecorder()
req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&version=1", strings.NewReader(`
{
"metadata": {
"foo": "bar"
},
"data": {
"name": "Keith Urban Two",
"genre": "GENRE_COUNTRY"
}
}
`))
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
v2ArtistHandler.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 Two", result["data"].(map[string]any)["name"])
require.Equal(t, "keith-urban", result["id"].(map[string]any)["name"])
})
t.Run("should fail the update if the resource's version doesn't match the version of the existing resource", func(t *testing.T) {
rsp := httptest.NewRecorder()
req := httptest.NewRequest("PUT", "/demo/v2/artist/keith-urban?partition=default&peer_name=local&namespace=default&version=1", strings.NewReader(`
{
"metadata": {
"foo": "bar"
},
"data": {
"name": "Keith Urban",
"genre": "GENRE_COUNTRY"
}
}
`))
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
v2ArtistHandler.ServeHTTP(rsp, req)
require.Equal(t, http.StatusConflict, rsp.Result().StatusCode)
})
t.Run("should write to the resource backend with owner", func(t *testing.T) {
rsp := httptest.NewRecorder()
req := httptest.NewRequest("PUT", "/demo/v1/artist/keith-urban-v1?partition=default&peer_name=local&namespace=default", strings.NewReader(`
{
"metadata": {
"foo": "bar"
},
"data": {
"name": "Keith Urban V1",
"genre": "GENRE_COUNTRY"
},
"owner": {
"name": "keith-urban",
"type": {
"group": "demo",
"group_version": "v2",
"kind": "Artist"
},
"tenancy": {
"partition": "default",
"peer_name": "local",
"namespace": "default"
}
}
}
`))
req.Header.Add("x-consul-token", testACLTokenArtistWritePolicy)
v1ArtistHandler.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 V1", result["data"].(map[string]any)["name"])
require.Equal(t, "keith-urban-v1", result["id"].(map[string]any)["name"])
readRsp, err := client.Read(testutil.TestContext(t), &pbresource.ReadRequest{
Id: &pbresource.ID{
Type: demo.TypeV1Artist,
Tenancy: demo.TenancyDefault,
Name: "keith-urban-v1",
},
})
require.NoError(t, err)
require.NotNil(t, readRsp.Resource)
require.Equal(t, "keith-urban", readRsp.Resource.Owner.Name)
var artist pbdemov1.Artist
require.NoError(t, readRsp.Resource.Data.UnmarshalTo(&artist))
require.Equal(t, "Keith Urban V1", artist.Name)
})
}

View File

@ -54,6 +54,8 @@ type Registry interface {
// Resolve the given resource type and its hooks. // Resolve the given resource type and its hooks.
Resolve(typ *pbresource.Type) (reg Registration, ok bool) Resolve(typ *pbresource.Type) (reg Registration, ok bool)
Types() []Registration
} }
type Registration struct { type Registration struct {
@ -183,6 +185,17 @@ func (r *TypeRegistry) Resolve(typ *pbresource.Type) (reg Registration, ok bool)
return Registration{}, false 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 { func ToGVK(resourceType *pbresource.Type) string {
return fmt.Sprintf("%s.%s.%s", resourceType.Group, resourceType.GroupVersion, resourceType.Kind) return fmt.Sprintf("%s.%s.%s", resourceType.Group, resourceType.GroupVersion, resourceType.Kind)
} }