consul/agent/grpc-external/services/dataplane/get_envoy_bootstrap_params_...

382 lines
13 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package dataplane
import (
"context"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/acl/resolver"
external "github.com/hashicorp/consul/agent/grpc-external"
"github.com/hashicorp/consul/agent/grpc-external/testutils"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto-public/pbdataplane"
)
const (
testIdentity = "test-identity"
testToken = "acl-token-get-envoy-bootstrap-params"
testServiceName = "web"
proxyServiceID = "web-proxy"
nodeName = "foo"
nodeID = "2980b72b-bd9d-9d7b-d4f9-951bf7508d95"
proxyConfigKey = "envoy_dogstatsd_url"
proxyConfigValue = "udp://127.0.0.1:8125"
serverDC = "dc1"
protocolKey = "protocol"
connectTimeoutKey = "local_connect_timeout_ms"
requestTimeoutKey = "local_request_timeout_ms"
proxyDefaultsProtocol = "http"
proxyDefaultsRequestTimeout = 1111
serviceDefaultsProtocol = "tcp"
serviceDefaultsConnectTimeout = 4444
testAccessLogs = "{\"name\":\"Consul Listener Filter Log\",\"typedConfig\":{\"@type\":\"type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog\",\"logFormat\":{\"jsonFormat\":{\"custom_field\":\"%START_TIME%\"}}}}"
)
func testRegisterRequestProxy(t *testing.T) *structs.RegisterRequest {
return &structs.RegisterRequest{
Datacenter: serverDC,
Node: nodeName,
ID: nodeID,
Address: "127.0.0.1",
Service: &structs.NodeService{
Kind: structs.ServiceKindConnectProxy,
Service: proxyServiceID,
ID: proxyServiceID,
Address: "127.0.0.2",
Port: 2222,
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: testServiceName,
Config: map[string]interface{}{
proxyConfigKey: proxyConfigValue,
},
},
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
},
}
}
func testRegisterIngressGateway(t *testing.T) *structs.RegisterRequest {
registerReq := structs.TestRegisterIngressGateway(t)
registerReq.ID = "2980b72b-bd9d-9d7b-d4f9-951bf7508d95"
registerReq.Service.ID = registerReq.Service.Service
registerReq.Service.Proxy.Config = map[string]interface{}{
proxyConfigKey: proxyConfigValue,
}
return registerReq
}
func testProxyDefaults(t *testing.T, accesslogs bool) structs.ConfigEntry {
pd := &structs.ProxyConfigEntry{
Kind: structs.ProxyDefaults,
Name: structs.ProxyConfigGlobal,
Config: map[string]interface{}{
protocolKey: proxyDefaultsProtocol,
requestTimeoutKey: proxyDefaultsRequestTimeout,
},
}
if accesslogs {
pd.AccessLogs.Enabled = true
pd.AccessLogs.JSONFormat = "{ \"custom_field\": \"%START_TIME%\" }"
}
return pd
}
func testServiceDefaults(t *testing.T) structs.ConfigEntry {
return &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults,
Name: testServiceName,
Protocol: serviceDefaultsProtocol,
LocalConnectTimeoutMs: serviceDefaultsConnectTimeout,
}
}
func requireConfigField(t *testing.T, resp *pbdataplane.GetEnvoyBootstrapParamsResponse, key string, value interface{}) {
require.Contains(t, resp.Config.Fields, key)
require.Equal(t, value, resp.Config.Fields[key])
}
func TestGetEnvoyBootstrapParams_Success(t *testing.T) {
type testCase struct {
name string
registerReq *structs.RegisterRequest
nodeID bool
proxyDefaults structs.ConfigEntry
serviceDefaults structs.ConfigEntry
}
run := func(t *testing.T, tc testCase) {
store := testutils.TestStateStore(t, nil)
idx := uint64(1)
err := store.EnsureRegistration(idx, tc.registerReq)
require.NoError(t, err)
if tc.proxyDefaults != nil {
idx++
err := store.EnsureConfigEntry(idx, tc.proxyDefaults)
require.NoError(t, err)
}
if tc.serviceDefaults != nil {
idx++
err := store.EnsureConfigEntry(idx, tc.serviceDefaults)
require.NoError(t, err)
}
aclResolver := &MockACLResolver{}
aclResolver.On("ResolveTokenAndDefaultMeta", testToken, mock.Anything, mock.Anything).
Return(testutils.ACLServiceRead(t, tc.registerReq.Service.ID), nil)
options := structs.QueryOptions{Token: testToken}
ctx, err := external.ContextWithQueryOptions(context.Background(), options)
require.NoError(t, err)
server := NewServer(Config{
GetStore: func() StateStore { return store },
Logger: hclog.NewNullLogger(),
ACLResolver: aclResolver,
Datacenter: serverDC,
})
client := testClient(t, server)
req := &pbdataplane.GetEnvoyBootstrapParamsRequest{
ServiceId: tc.registerReq.Service.ID,
NodeSpec: &pbdataplane.GetEnvoyBootstrapParamsRequest_NodeName{NodeName: tc.registerReq.Node}}
if tc.nodeID {
req.NodeSpec = &pbdataplane.GetEnvoyBootstrapParamsRequest_NodeId{NodeId: string(tc.registerReq.ID)}
}
resp, err := client.GetEnvoyBootstrapParams(ctx, req)
require.NoError(t, err)
if tc.registerReq.Service.IsGateway() {
require.Equal(t, tc.registerReq.Service.Service, resp.Service)
} else {
require.Equal(t, tc.registerReq.Service.Proxy.DestinationServiceName, resp.Service)
}
require.Equal(t, serverDC, resp.Datacenter)
require.Equal(t, tc.registerReq.EnterpriseMeta.PartitionOrDefault(), resp.Partition)
require.Equal(t, tc.registerReq.EnterpriseMeta.NamespaceOrDefault(), resp.Namespace)
requireConfigField(t, resp, proxyConfigKey, structpb.NewStringValue(proxyConfigValue))
require.Equal(t, tc.registerReq.Node, resp.NodeName)
if tc.serviceDefaults != nil && tc.proxyDefaults != nil {
// service-defaults take precedence over proxy-defaults
requireConfigField(t, resp, protocolKey, structpb.NewStringValue(serviceDefaultsProtocol))
requireConfigField(t, resp, connectTimeoutKey, structpb.NewNumberValue(serviceDefaultsConnectTimeout))
requireConfigField(t, resp, requestTimeoutKey, structpb.NewNumberValue(proxyDefaultsRequestTimeout))
} else if tc.serviceDefaults != nil {
requireConfigField(t, resp, protocolKey, structpb.NewStringValue(serviceDefaultsProtocol))
requireConfigField(t, resp, connectTimeoutKey, structpb.NewNumberValue(serviceDefaultsConnectTimeout))
} else if tc.proxyDefaults != nil {
requireConfigField(t, resp, protocolKey, structpb.NewStringValue(proxyDefaultsProtocol))
requireConfigField(t, resp, requestTimeoutKey, structpb.NewNumberValue(proxyDefaultsRequestTimeout))
}
if tc.proxyDefaults != nil {
pd, ok := tc.proxyDefaults.(*structs.ProxyConfigEntry)
require.True(t, ok, "Invalid Proxy Defaults")
if pd.AccessLogs.Enabled {
require.JSONEq(t, "{\"name\":\"Consul Listener Filter Log\",\"typedConfig\":{\"@type\":\"type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog\",\"logFormat\":{\"jsonFormat\":{\"custom_field\":\"%START_TIME%\"}}}}", resp.AccessLogs[0])
}
}
}
testCases := []testCase{
{
name: "lookup service sidecar proxy by node name",
registerReq: testRegisterRequestProxy(t),
},
{
name: "lookup service sidecar proxy by node ID",
registerReq: testRegisterRequestProxy(t),
nodeID: true,
},
{
name: "lookup ingress gw service by node name",
registerReq: testRegisterIngressGateway(t),
},
{
name: "lookup ingress gw service by node ID",
registerReq: testRegisterIngressGateway(t),
nodeID: true,
},
{
name: "merge proxy defaults for sidecar proxy",
registerReq: testRegisterRequestProxy(t),
proxyDefaults: testProxyDefaults(t, false),
},
{
name: "proxy defaults access logs",
registerReq: testRegisterRequestProxy(t),
proxyDefaults: testProxyDefaults(t, true),
},
{
name: "merge service defaults for sidecar proxy",
registerReq: testRegisterRequestProxy(t),
serviceDefaults: testServiceDefaults(t),
},
{
name: "merge proxy defaults and service defaults for sidecar proxy",
registerReq: testRegisterRequestProxy(t),
serviceDefaults: testServiceDefaults(t),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
run(t, tc)
})
}
}
func TestGetEnvoyBootstrapParams_Error(t *testing.T) {
type testCase struct {
name string
req *pbdataplane.GetEnvoyBootstrapParamsRequest
expectedErrCode codes.Code
expecteErrMsg string
}
run := func(t *testing.T, tc testCase) {
aclResolver := &MockACLResolver{}
aclResolver.On("ResolveTokenAndDefaultMeta", testToken, mock.Anything, mock.Anything).
Return(testutils.ACLServiceRead(t, proxyServiceID), nil)
options := structs.QueryOptions{Token: testToken}
ctx, err := external.ContextWithQueryOptions(context.Background(), options)
require.NoError(t, err)
store := testutils.TestStateStore(t, nil)
registerReq := testRegisterRequestProxy(t)
err = store.EnsureRegistration(1, registerReq)
require.NoError(t, err)
server := NewServer(Config{
GetStore: func() StateStore { return store },
Logger: hclog.NewNullLogger(),
ACLResolver: aclResolver,
})
client := testClient(t, server)
resp, err := client.GetEnvoyBootstrapParams(ctx, tc.req)
require.Nil(t, resp)
require.Error(t, err)
errStatus, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, tc.expectedErrCode.String(), errStatus.Code().String())
require.Equal(t, tc.expecteErrMsg, errStatus.Message())
}
testCases := []testCase{
{
name: "lookup-service-by-unregistered-node-name",
req: &pbdataplane.GetEnvoyBootstrapParamsRequest{
ServiceId: proxyServiceID,
NodeSpec: &pbdataplane.GetEnvoyBootstrapParamsRequest_NodeName{NodeName: "blah"}},
expectedErrCode: codes.NotFound,
expecteErrMsg: "node not found",
},
{
name: "lookup-service-by-unregistered-node-id",
req: &pbdataplane.GetEnvoyBootstrapParamsRequest{
ServiceId: proxyServiceID,
NodeSpec: &pbdataplane.GetEnvoyBootstrapParamsRequest_NodeId{NodeId: "5980b72b-bd9d-9d7b-d4f9-951bf7508d98"}},
expectedErrCode: codes.NotFound,
expecteErrMsg: "node not found",
},
{
name: "lookup-service-by-unregistered-service",
req: &pbdataplane.GetEnvoyBootstrapParamsRequest{
ServiceId: "blah-service",
NodeSpec: &pbdataplane.GetEnvoyBootstrapParamsRequest_NodeName{NodeName: nodeName}},
expectedErrCode: codes.NotFound,
expecteErrMsg: "Service not found",
},
{
name: "lookup-service-without-node-details",
req: &pbdataplane.GetEnvoyBootstrapParamsRequest{
ServiceId: proxyServiceID},
expectedErrCode: codes.InvalidArgument,
expecteErrMsg: "Node ID or name required to lookup the service",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
run(t, tc)
})
}
}
func TestGetEnvoyBootstrapParams_Unauthenticated(t *testing.T) {
// Mock the ACL resolver to return ErrNotFound.
aclResolver := &MockACLResolver{}
aclResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything).
Return(resolver.Result{}, acl.ErrNotFound)
options := structs.QueryOptions{Token: testToken}
ctx, err := external.ContextWithQueryOptions(context.Background(), options)
require.NoError(t, err)
store := testutils.TestStateStore(t, nil)
server := NewServer(Config{
GetStore: func() StateStore { return store },
Logger: hclog.NewNullLogger(),
ACLResolver: aclResolver,
})
client := testClient(t, server)
resp, err := client.GetEnvoyBootstrapParams(ctx, &pbdataplane.GetEnvoyBootstrapParamsRequest{})
require.Error(t, err)
require.Equal(t, codes.Unauthenticated.String(), status.Code(err).String())
require.Nil(t, resp)
}
func TestGetEnvoyBootstrapParams_PermissionDenied(t *testing.T) {
// Mock the ACL resolver to return a deny all authorizer
aclResolver := &MockACLResolver{}
aclResolver.On("ResolveTokenAndDefaultMeta", testToken, mock.Anything, mock.Anything).
Return(testutils.ACLNoPermissions(t), nil)
options := structs.QueryOptions{Token: testToken}
ctx, err := external.ContextWithQueryOptions(context.Background(), options)
require.NoError(t, err)
store := testutils.TestStateStore(t, nil)
registerReq := structs.TestRegisterRequestProxy(t)
proxyServiceID := "web-sidecar-proxy"
registerReq.Service.ID = proxyServiceID
err = store.EnsureRegistration(1, registerReq)
require.NoError(t, err)
server := NewServer(Config{
GetStore: func() StateStore { return store },
Logger: hclog.NewNullLogger(),
ACLResolver: aclResolver,
})
client := testClient(t, server)
req := &pbdataplane.GetEnvoyBootstrapParamsRequest{
ServiceId: proxyServiceID,
NodeSpec: &pbdataplane.GetEnvoyBootstrapParamsRequest_NodeName{NodeName: registerReq.Node}}
resp, err := client.GetEnvoyBootstrapParams(ctx, req)
require.Error(t, err)
require.Equal(t, codes.PermissionDenied.String(), status.Code(err).String())
require.Nil(t, resp)
}