1529 lines
52 KiB
Go
1529 lines
52 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package nomad
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/go-memdb"
|
|
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
|
"github.com/hashicorp/nomad/acl"
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/nomad/mock"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/hashicorp/nomad/testutil"
|
|
"github.com/shoenig/test/must"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestServiceRegistration_Upsert(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testCases := []struct {
|
|
serverFn func(t *testing.T) (*Server, *structs.ACLToken, func())
|
|
testFn func(t *testing.T, s *Server, token *structs.ACLToken)
|
|
name string
|
|
}{
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
server, cleanup := TestServer(t, nil)
|
|
return server, nil, cleanup
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations and ensure
|
|
// they are in the same namespace.
|
|
services := mock.ServiceRegistrations()
|
|
services[1].Namespace = services[0].Namespace
|
|
|
|
// Attempt to upsert without a token.
|
|
serviceRegReq := &structs.ServiceRegistrationUpsertRequest{
|
|
Services: services,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
Namespace: services[0].Namespace,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationUpsertResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationUpsertRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "node lookup by SecretID failed")
|
|
|
|
// Generate a node and retry the upsert.
|
|
node := mock.Node()
|
|
require.NoError(t, s.State().UpsertNode(structs.MsgTypeTestSetup, 10, node))
|
|
|
|
ws := memdb.NewWatchSet()
|
|
node, err = s.State().NodeByID(ws, node.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, node)
|
|
|
|
serviceRegReq.WriteRequest.AuthToken = node.SecretID
|
|
err = msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationUpsertRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.Greater(t, serviceRegResp.Index, uint64(1))
|
|
},
|
|
name: "ACLs disabled without node secret",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
server, cleanup := TestServer(t, nil)
|
|
return server, nil, cleanup
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations and ensure
|
|
// they are in the same namespace.
|
|
services := mock.ServiceRegistrations()
|
|
services[1].Namespace = services[0].Namespace
|
|
|
|
// Generate a node.
|
|
node := mock.Node()
|
|
require.NoError(t, s.State().UpsertNode(structs.MsgTypeTestSetup, 10, node))
|
|
|
|
ws := memdb.NewWatchSet()
|
|
node, err := s.State().NodeByID(ws, node.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, node)
|
|
|
|
serviceRegReq := &structs.ServiceRegistrationUpsertRequest{
|
|
Services: services,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
Namespace: services[0].Namespace,
|
|
AuthToken: node.SecretID,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationUpsertRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.Greater(t, serviceRegResp.Index, uint64(1))
|
|
},
|
|
name: "ACLs disabled with node secret",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations and ensure
|
|
// they are in the same namespace.
|
|
services := mock.ServiceRegistrations()
|
|
services[1].Namespace = services[0].Namespace
|
|
|
|
// Attempt to upsert without a token.
|
|
serviceRegReq := &structs.ServiceRegistrationUpsertRequest{
|
|
Services: services,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
Namespace: services[0].Namespace,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationUpsertResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationUpsertRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "node lookup by SecretID failed")
|
|
|
|
// Generate a node and retry the upsert.
|
|
node := mock.Node()
|
|
require.NoError(t, s.State().UpsertNode(structs.MsgTypeTestSetup, 10, node))
|
|
|
|
ws := memdb.NewWatchSet()
|
|
node, err = s.State().NodeByID(ws, node.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, node)
|
|
|
|
serviceRegReq.WriteRequest.AuthToken = node.SecretID
|
|
err = msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationUpsertRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.Greater(t, serviceRegResp.Index, uint64(1))
|
|
},
|
|
name: "ACLs enabled without node secret",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations and ensure
|
|
// they are in the same namespace.
|
|
services := mock.ServiceRegistrations()
|
|
services[1].Namespace = services[0].Namespace
|
|
|
|
// Generate a node.
|
|
node := mock.Node()
|
|
require.NoError(t, s.State().UpsertNode(structs.MsgTypeTestSetup, 10, node))
|
|
|
|
ws := memdb.NewWatchSet()
|
|
node, err := s.State().NodeByID(ws, node.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, node)
|
|
|
|
serviceRegReq := &structs.ServiceRegistrationUpsertRequest{
|
|
Services: services,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
Namespace: services[0].Namespace,
|
|
AuthToken: node.SecretID,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationUpsertRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.Greater(t, serviceRegResp.Index, uint64(1))
|
|
},
|
|
name: "ACLs enabled with node secret",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
server, aclToken, cleanup := tc.serverFn(t)
|
|
defer cleanup()
|
|
tc.testFn(t, server, aclToken)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServiceRegistration_DeleteByID(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testCases := []struct {
|
|
serverFn func(t *testing.T) (*Server, *structs.ACLToken, func())
|
|
testFn func(t *testing.T, s *Server, token *structs.ACLToken)
|
|
name string
|
|
}{
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
server, cleanup := TestServer(t, nil)
|
|
return server, nil, cleanup
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Attempt to delete a service registration that does not
|
|
// exist.
|
|
serviceRegReq := &structs.ServiceRegistrationDeleteByIDRequest{
|
|
ID: "this-is-not-the-service-you're-looking-for",
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
Namespace: "default",
|
|
},
|
|
}
|
|
|
|
var serviceRegResp structs.ServiceRegistrationDeleteByIDResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationDeleteByIDRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "service registration not found")
|
|
},
|
|
name: "ACLs disabled unknown service",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
server, cleanup := TestServer(t, nil)
|
|
return server, nil, cleanup
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Try and delete one of the services that exist.
|
|
serviceRegReq := &structs.ServiceRegistrationDeleteByIDRequest{
|
|
ID: services[0].ID,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
Namespace: services[0].Namespace,
|
|
},
|
|
}
|
|
|
|
var serviceRegResp structs.ServiceRegistrationDeleteByIDResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationDeleteByIDRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
},
|
|
name: "ACLs disabled known service",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Try and delete one of the services that exist.
|
|
serviceRegReq := &structs.ServiceRegistrationDeleteByIDRequest{
|
|
ID: services[0].ID,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
Namespace: services[0].Namespace,
|
|
AuthToken: token.SecretID,
|
|
},
|
|
}
|
|
|
|
var serviceRegResp structs.ServiceRegistrationDeleteByIDResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationDeleteByIDRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
},
|
|
name: "ACLs enabled known service with management token",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Try and delete one of the services that exist but don't set
|
|
// an auth token.
|
|
serviceRegReq := &structs.ServiceRegistrationDeleteByIDRequest{
|
|
ID: services[0].ID,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
Namespace: services[0].Namespace,
|
|
},
|
|
}
|
|
|
|
var serviceRegResp structs.ServiceRegistrationDeleteByIDResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationDeleteByIDRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Permission denied")
|
|
},
|
|
name: "ACLs enabled known service without token",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Create a token using submit-job capability.
|
|
authToken := mock.CreatePolicyAndToken(t, s.State(), 30, "test-service-reg-delete",
|
|
mock.NamespacePolicy(services[0].Namespace, "", []string{acl.NamespaceCapabilitySubmitJob})).SecretID
|
|
|
|
// Try and delete one of the services that exist but don't set
|
|
// an auth token.
|
|
serviceRegReq := &structs.ServiceRegistrationDeleteByIDRequest{
|
|
ID: services[0].ID,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
Namespace: services[0].Namespace,
|
|
AuthToken: authToken,
|
|
},
|
|
}
|
|
|
|
var serviceRegResp structs.ServiceRegistrationDeleteByIDResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationDeleteByIDRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
},
|
|
name: "ACLs enabled known service with submit-job namespace token",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Create a token using submit-job capability.
|
|
authToken := mock.CreatePolicyAndToken(t, s.State(), 30, "test-service-reg-delete",
|
|
mock.NamespacePolicy(services[0].Namespace, "", []string{acl.NamespaceCapabilityReadJob})).SecretID
|
|
|
|
// Try and delete one of the services that exist but don't set
|
|
// an auth token.
|
|
serviceRegReq := &structs.ServiceRegistrationDeleteByIDRequest{
|
|
ID: services[0].ID,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
Namespace: services[0].Namespace,
|
|
AuthToken: authToken,
|
|
},
|
|
}
|
|
|
|
var serviceRegResp structs.ServiceRegistrationDeleteByIDResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationDeleteByIDRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Permission denied")
|
|
},
|
|
name: "ACLs enabled known service with read-job namespace token",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
server, aclToken, cleanup := tc.serverFn(t)
|
|
defer cleanup()
|
|
tc.testFn(t, server, aclToken)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServiceRegistration_List(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testCases := []struct {
|
|
serverFn func(t *testing.T) (*Server, *structs.ACLToken, func())
|
|
testFn func(t *testing.T, s *Server, token *structs.ACLToken)
|
|
name string
|
|
}{
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
server, cleanup := TestServer(t, nil)
|
|
return server, nil, cleanup
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Test a request without setting an ACL token.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: structs.AllNamespacesSentinel,
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
|
|
{
|
|
Namespace: "default",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "example-cache",
|
|
Tags: []string{"foo"},
|
|
},
|
|
}},
|
|
{
|
|
Namespace: "platform",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "countdash-api",
|
|
Tags: []string{"bar"},
|
|
},
|
|
}},
|
|
}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs disabled wildcard ns",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
server, cleanup := TestServer(t, nil)
|
|
return server, nil, cleanup
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Test a request without setting an ACL token.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: "platform",
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
|
|
{
|
|
Namespace: "platform",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "countdash-api",
|
|
Tags: []string{"bar"},
|
|
},
|
|
},
|
|
},
|
|
}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs disabled platform ns",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
server, cleanup := TestServer(t, nil)
|
|
return server, nil, cleanup
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Test a request without setting an ACL token.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: "platform",
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs disabled no services",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Test a request without setting an ACL token.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: structs.AllNamespacesSentinel,
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs enabled wildcard ns without token",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Test a request without setting an ACL token.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: "default",
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Permission denied")
|
|
},
|
|
name: "ACLs enabled default ns without token",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Test a request without setting an ACL token.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: structs.AllNamespacesSentinel,
|
|
Region: DefaultRegion,
|
|
AuthToken: token.SecretID,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
|
|
{
|
|
Namespace: "default",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "example-cache",
|
|
Tags: []string{"foo"},
|
|
},
|
|
}},
|
|
{
|
|
Namespace: "platform",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "countdash-api",
|
|
Tags: []string{"bar"},
|
|
},
|
|
}},
|
|
}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs enabled wildcard with management token",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Test a request without setting an ACL token.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: "default",
|
|
Region: DefaultRegion,
|
|
AuthToken: token.SecretID,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
|
|
{
|
|
Namespace: "default",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "example-cache",
|
|
Tags: []string{"foo"},
|
|
},
|
|
}},
|
|
}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs enabled default ns with management token",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Create a policy and grab the token which has the read-job
|
|
// capability on the platform namespace.
|
|
customToken := mock.CreatePolicyAndToken(t, s.State(), 5, "test-valid-autoscaler",
|
|
mock.NamespacePolicy("platform", "", []string{acl.NamespaceCapabilityReadJob})).SecretID
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Test a request without setting an ACL token.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: "platform",
|
|
Region: DefaultRegion,
|
|
AuthToken: customToken,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
|
|
{
|
|
Namespace: "platform",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "countdash-api",
|
|
Tags: []string{"bar"},
|
|
},
|
|
}},
|
|
}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs enabled with read-job policy token",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Create a namespace as this is needed when using an ACL like
|
|
// we do in this test.
|
|
ns := &structs.Namespace{
|
|
Name: "platform",
|
|
Description: "test namespace",
|
|
CreateIndex: 5,
|
|
ModifyIndex: 5,
|
|
}
|
|
ns.SetHash()
|
|
require.NoError(t, s.State().UpsertNamespaces(5, []*structs.Namespace{ns}))
|
|
|
|
// Create a policy and grab the token which has the read-job
|
|
// capability on the platform namespace.
|
|
customToken := mock.CreatePolicyAndToken(t, s.State(), 10, "test-valid-autoscaler",
|
|
mock.NamespacePolicy("platform", "", []string{acl.NamespaceCapabilityReadJob})).SecretID
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 20, services))
|
|
|
|
// Test a request without setting an ACL token.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: structs.AllNamespacesSentinel,
|
|
Region: DefaultRegion,
|
|
AuthToken: customToken,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
|
|
{
|
|
Namespace: "platform",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "countdash-api",
|
|
Tags: []string{"bar"},
|
|
},
|
|
}},
|
|
}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs enabled wildcard ns with restricted token",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Create a namespace as this is needed when using an ACL like
|
|
// we do in this test.
|
|
ns := &structs.Namespace{
|
|
Name: "platform",
|
|
Description: "test namespace",
|
|
CreateIndex: 5,
|
|
ModifyIndex: 5,
|
|
}
|
|
ns.SetHash()
|
|
require.NoError(t, s.State().UpsertNamespaces(5, []*structs.Namespace{ns}))
|
|
|
|
// Create a policy and grab the token which has the read policy
|
|
// on the platform namespace.
|
|
customToken := mock.CreatePolicyAndToken(t, s.State(), 10, "test-valid-autoscaler",
|
|
mock.NamespacePolicy("platform", "read", nil)).SecretID
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 20, services))
|
|
|
|
// Test a request without setting an ACL token.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: structs.AllNamespacesSentinel,
|
|
Region: DefaultRegion,
|
|
AuthToken: customToken,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
|
|
{
|
|
Namespace: "platform",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "countdash-api",
|
|
Tags: []string{"bar"},
|
|
},
|
|
}},
|
|
}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs enabled with read namespace policy token",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Create a namespace as this is needed when using an ACL like
|
|
// we do in this test.
|
|
ns := &structs.Namespace{
|
|
Name: "platform",
|
|
Description: "test namespace",
|
|
CreateIndex: 5,
|
|
ModifyIndex: 5,
|
|
}
|
|
ns.SetHash()
|
|
require.NoError(t, s.State().UpsertNamespaces(5, []*structs.Namespace{ns}))
|
|
|
|
// Generate a node.
|
|
node := mock.Node()
|
|
require.NoError(t, s.State().UpsertNode(structs.MsgTypeTestSetup, 10, node))
|
|
|
|
ws := memdb.NewWatchSet()
|
|
node, err := s.State().NodeByID(ws, node.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, node)
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 20, services))
|
|
|
|
// Test a request while setting the auth token to the node
|
|
// secret ID.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: "platform",
|
|
Region: DefaultRegion,
|
|
AuthToken: node.SecretID,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err = msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
|
|
{
|
|
Namespace: "platform",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "countdash-api",
|
|
Tags: []string{"bar"},
|
|
},
|
|
}},
|
|
}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs enabled with node secret token",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Create a namespace as this is needed when using an ACL like
|
|
// we do in this test.
|
|
ns := &structs.Namespace{
|
|
Name: "platform",
|
|
Description: "test namespace",
|
|
CreateIndex: 5,
|
|
ModifyIndex: 5,
|
|
}
|
|
ns.SetHash()
|
|
require.NoError(t, s.State().UpsertNamespaces(5, []*structs.Namespace{ns}))
|
|
|
|
// Generate an allocation with a signed identity
|
|
allocs := []*structs.Allocation{mock.Alloc()}
|
|
job := allocs[0].Job
|
|
job.Namespace = "platform"
|
|
allocs[0].Namespace = "platform"
|
|
require.NoError(t, s.State().UpsertJob(structs.MsgTypeTestSetup, 10, nil, job))
|
|
signAllocIdentities(s.encrypter, job, allocs)
|
|
require.NoError(t, s.State().UpsertAllocs(structs.MsgTypeTestSetup, 15, allocs))
|
|
|
|
signedToken := allocs[0].SignedIdentities["web"]
|
|
|
|
// Associate an unrelated policy with the identity's job to
|
|
// ensure it doesn't conflict.
|
|
policy := &structs.ACLPolicy{
|
|
Name: "policy-for-identity",
|
|
Rules: mock.NodePolicy("read"),
|
|
JobACL: &structs.JobACL{
|
|
Namespace: "platform",
|
|
JobID: job.ID,
|
|
},
|
|
}
|
|
policy.SetHash()
|
|
must.NoError(t, s.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 16,
|
|
[]*structs.ACLPolicy{policy}))
|
|
|
|
// Generate and upsert some service registrations.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.State().UpsertServiceRegistrations(
|
|
structs.MsgTypeTestSetup, 20, services))
|
|
|
|
// Test a request while setting the auth token to the signed token
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: "platform",
|
|
Region: DefaultRegion,
|
|
AuthToken: signedToken,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod,
|
|
serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
|
|
{
|
|
Namespace: "platform",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "countdash-api",
|
|
Tags: []string{"bar"},
|
|
},
|
|
}},
|
|
}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs enabled with valid signed identity",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
server, aclToken, cleanup := tc.serverFn(t)
|
|
defer cleanup()
|
|
tc.testFn(t, server, aclToken)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServiceRegistration_GetService(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testCases := []struct {
|
|
serverFn func(t *testing.T) (*Server, *structs.ACLToken, func())
|
|
testFn func(t *testing.T, s *Server, token *structs.ACLToken)
|
|
name string
|
|
}{
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
server, cleanup := TestServer(t, nil)
|
|
return server, nil, cleanup
|
|
},
|
|
testFn: func(t *testing.T, s *Server, _ *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate mock services then upsert them individually using different indexes.
|
|
services := mock.ServiceRegistrations()
|
|
|
|
require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ServiceRegistration{services[0]}))
|
|
|
|
require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
|
|
structs.MsgTypeTestSetup, 20, []*structs.ServiceRegistration{services[1]}))
|
|
|
|
// Lookup the first registration.
|
|
serviceRegReq := &structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: services[0].ServiceName,
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: services[0].Namespace,
|
|
Region: s.Region(),
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationByNameResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ServiceRegistrationGetServiceRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint64(10), serviceRegResp.Services[0].CreateIndex)
|
|
require.Equal(t, uint64(20), serviceRegResp.Index)
|
|
require.Len(t, serviceRegResp.Services, 1)
|
|
|
|
// Lookup the second registration.
|
|
serviceRegReq2 := &structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: services[1].ServiceName,
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: services[1].Namespace,
|
|
Region: s.Region(),
|
|
},
|
|
}
|
|
var serviceRegResp2 structs.ServiceRegistrationByNameResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ServiceRegistrationGetServiceRPCMethod, serviceRegReq2, &serviceRegResp2)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint64(20), serviceRegResp2.Services[0].CreateIndex)
|
|
require.Equal(t, uint64(20), serviceRegResp.Index)
|
|
require.Len(t, serviceRegResp2.Services, 1)
|
|
|
|
// Perform a lookup with namespace and service name that shouldn't produce
|
|
// results.
|
|
serviceRegReq3 := &structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: services[0].ServiceName,
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: services[1].Namespace,
|
|
Region: s.Region(),
|
|
},
|
|
}
|
|
var serviceRegResp3 structs.ServiceRegistrationByNameResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ServiceRegistrationGetServiceRPCMethod, serviceRegReq3, &serviceRegResp3)
|
|
require.NoError(t, err)
|
|
require.Len(t, serviceRegResp3.Services, 0)
|
|
},
|
|
name: "ACLs disabled",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate mock services then upsert them individually using different indexes.
|
|
services := mock.ServiceRegistrations()
|
|
|
|
require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ServiceRegistration{services[0]}))
|
|
|
|
require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
|
|
structs.MsgTypeTestSetup, 20, []*structs.ServiceRegistration{services[1]}))
|
|
|
|
// Lookup the first registration without using an ACL token
|
|
// which should fail.
|
|
serviceRegReq := &structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: services[0].ServiceName,
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: services[0].Namespace,
|
|
Region: s.Region(),
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationByNameResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ServiceRegistrationGetServiceRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Permission denied")
|
|
|
|
// Lookup the first registration using the management token.
|
|
serviceRegReq2 := &structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: services[0].ServiceName,
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: services[0].Namespace,
|
|
Region: s.Region(),
|
|
AuthToken: token.SecretID,
|
|
},
|
|
}
|
|
var serviceRegResp2 structs.ServiceRegistrationByNameResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ServiceRegistrationGetServiceRPCMethod, serviceRegReq2, &serviceRegResp2)
|
|
require.Nil(t, err)
|
|
require.Len(t, serviceRegResp2.Services, 1)
|
|
require.EqualValues(t, 20, serviceRegResp2.Index)
|
|
|
|
// Create a read policy for the default namespace and test this
|
|
// can correctly read the first service.
|
|
authToken1 := mock.CreatePolicyAndToken(t, s.State(), 30, "test-service-reg-get",
|
|
mock.NamespacePolicy(structs.DefaultNamespace, "read", nil)).SecretID
|
|
serviceRegReq3 := &structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: services[0].ServiceName,
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: services[0].Namespace,
|
|
Region: s.Region(),
|
|
AuthToken: authToken1,
|
|
},
|
|
}
|
|
var serviceRegResp3 structs.ServiceRegistrationByNameResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ServiceRegistrationGetServiceRPCMethod, serviceRegReq3, &serviceRegResp3)
|
|
require.Nil(t, err)
|
|
require.Len(t, serviceRegResp3.Services, 1)
|
|
require.EqualValues(t, 20, serviceRegResp2.Index)
|
|
|
|
// Attempting to lookup services in a different namespace should fail.
|
|
serviceRegReq4 := &structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: services[1].ServiceName,
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: services[1].Namespace,
|
|
Region: s.Region(),
|
|
AuthToken: authToken1,
|
|
},
|
|
}
|
|
var serviceRegResp4 structs.ServiceRegistrationByNameResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ServiceRegistrationGetServiceRPCMethod, serviceRegReq4, &serviceRegResp4)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "Permission denied")
|
|
},
|
|
name: "ACLs enabled",
|
|
},
|
|
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate mock services then upsert them individually using different indexes.
|
|
services := mock.ServiceRegistrations()
|
|
|
|
require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ServiceRegistration{services[0]}))
|
|
|
|
require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
|
|
structs.MsgTypeTestSetup, 20, []*structs.ServiceRegistration{services[1]}))
|
|
|
|
// Generate a node.
|
|
node := mock.Node()
|
|
require.NoError(t, s.State().UpsertNode(structs.MsgTypeTestSetup, 30, node))
|
|
|
|
ws := memdb.NewWatchSet()
|
|
node, err := s.State().NodeByID(ws, node.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, node)
|
|
|
|
// Test a request while setting the auth token to the node
|
|
// secret ID.
|
|
serviceRegReq := &structs.ServiceRegistrationListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: "platform",
|
|
Region: DefaultRegion,
|
|
AuthToken: node.SecretID,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationListResponse
|
|
err = msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
|
|
{
|
|
Namespace: "platform",
|
|
Services: []*structs.ServiceRegistrationStub{
|
|
{
|
|
ServiceName: "countdash-api",
|
|
Tags: []string{"bar"},
|
|
},
|
|
}},
|
|
}, serviceRegResp.Services)
|
|
},
|
|
name: "ACLs enabled using node secret",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
return TestACLServer(t, nil)
|
|
},
|
|
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate mock services then upsert them individually using different indexes.
|
|
services := mock.ServiceRegistrations()
|
|
|
|
require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ServiceRegistration{services[0]}))
|
|
|
|
require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
|
|
structs.MsgTypeTestSetup, 20, []*structs.ServiceRegistration{services[1]}))
|
|
|
|
// Generate an allocation with a signed identity
|
|
allocs := []*structs.Allocation{mock.Alloc()}
|
|
job := allocs[0].Job
|
|
require.NoError(t, s.State().UpsertJob(structs.MsgTypeTestSetup, 10, nil, job))
|
|
signAllocIdentities(s.encrypter, job, allocs)
|
|
require.NoError(t, s.State().UpsertAllocs(structs.MsgTypeTestSetup, 15, allocs))
|
|
|
|
signedToken := allocs[0].SignedIdentities["web"]
|
|
|
|
// Lookup the first registration.
|
|
serviceRegReq := &structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: services[0].ServiceName,
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: services[0].Namespace,
|
|
Region: s.Region(),
|
|
AuthToken: signedToken,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationByNameResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ServiceRegistrationGetServiceRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint64(10), serviceRegResp.Services[0].CreateIndex)
|
|
require.Equal(t, uint64(20), serviceRegResp.Index)
|
|
require.Len(t, serviceRegResp.Services, 1)
|
|
},
|
|
name: "ACLs enabled using valid signed identity",
|
|
},
|
|
{
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
server, cleanup := TestServer(t, nil)
|
|
return server, nil, cleanup
|
|
},
|
|
testFn: func(t *testing.T, s *Server, _ *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// Generate mock services then upsert them individually using different indexes.
|
|
services := mock.ServiceRegistrations()
|
|
require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
|
|
structs.MsgTypeTestSetup, 10, services))
|
|
|
|
// Generate a second set of mocks. Set the datacenter to the
|
|
// opposite or the mock, (dc1,dc2) which will be used to test
|
|
// filtering and alter the ID.
|
|
nextServices := mock.ServiceRegistrations()
|
|
nextServices[0].ID += "_next"
|
|
nextServices[0].Datacenter = "dc2"
|
|
nextServices[1].ID += "_next"
|
|
nextServices[1].Datacenter = "dc1"
|
|
require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
|
|
structs.MsgTypeTestSetup, 20, nextServices))
|
|
|
|
// Create and test a request where we filter for service
|
|
// registrations in the default namespace, running within
|
|
// datacenter "dc2" only.
|
|
serviceRegReq := &structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: services[0].ServiceName,
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: structs.DefaultNamespace,
|
|
Region: DefaultRegion,
|
|
Filter: `Datacenter == "dc2"`,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationByNameResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationGetServiceRPCMethod, serviceRegReq, &serviceRegResp)
|
|
require.NoError(t, err)
|
|
require.ElementsMatch(t, []*structs.ServiceRegistration{nextServices[0]}, serviceRegResp.Services)
|
|
|
|
// Create a test function which can be used for each namespace
|
|
// to ensure cross-namespace functionality of pagination.
|
|
namespaceTestFn := func(
|
|
req *structs.ServiceRegistrationByNameRequest,
|
|
resp *structs.ServiceRegistrationByNameResponse) {
|
|
|
|
// We have two service registrations, therefore loop twice in
|
|
// order to check the return array and pagination details.
|
|
for i := 0; i < 2; i++ {
|
|
|
|
// The message makes debugging test failures easier as we
|
|
// are inside a loop.
|
|
msg := fmt.Sprintf("iteration %v of 2", i)
|
|
|
|
err2 := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationGetServiceRPCMethod, req, resp)
|
|
require.NoError(t, err2, msg)
|
|
require.Len(t, resp.Services, 1, msg)
|
|
|
|
// Anything but the first iteration should result in an
|
|
// empty token as we only have two entries.
|
|
switch i {
|
|
case 1:
|
|
require.Empty(t, resp.NextToken)
|
|
default:
|
|
require.NotEmpty(t, resp.NextToken)
|
|
req.NextToken = resp.NextToken
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test the default namespace pagnination.
|
|
serviceRegReq2 := structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: services[0].ServiceName,
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: structs.DefaultNamespace,
|
|
Region: DefaultRegion,
|
|
PerPage: 1,
|
|
},
|
|
}
|
|
var serviceRegResp2 structs.ServiceRegistrationByNameResponse
|
|
namespaceTestFn(&serviceRegReq2, &serviceRegResp2)
|
|
|
|
// Test the platform namespace pagnination.
|
|
serviceRegReq3 := structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: services[1].ServiceName,
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: services[1].Namespace,
|
|
Region: DefaultRegion,
|
|
PerPage: 1,
|
|
},
|
|
}
|
|
var serviceRegResp3 structs.ServiceRegistrationByNameResponse
|
|
namespaceTestFn(&serviceRegReq3, &serviceRegResp3)
|
|
|
|
},
|
|
name: "filtering and pagination",
|
|
},
|
|
{
|
|
name: "choose 2 of 3",
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
server, cleanup := TestServer(t, nil)
|
|
return server, nil, cleanup
|
|
},
|
|
testFn: func(t *testing.T, s *Server, _ *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// insert 3 instances of service s1
|
|
nodeID, jobID, allocID := "node_id", "job_id", "alloc_id"
|
|
services := []*structs.ServiceRegistration{
|
|
{
|
|
ID: "id_1",
|
|
Namespace: "default",
|
|
ServiceName: "s1",
|
|
NodeID: nodeID,
|
|
Datacenter: "dc1",
|
|
JobID: jobID,
|
|
AllocID: allocID,
|
|
Tags: []string{"tag1"},
|
|
Address: "10.0.0.1",
|
|
Port: 9001,
|
|
CreateIndex: 101,
|
|
ModifyIndex: 201,
|
|
},
|
|
{
|
|
ID: "id_2",
|
|
Namespace: "default",
|
|
ServiceName: "s1",
|
|
NodeID: nodeID,
|
|
Datacenter: "dc1",
|
|
JobID: jobID,
|
|
AllocID: allocID,
|
|
Tags: []string{"tag2"},
|
|
Address: "10.0.0.2",
|
|
Port: 9002,
|
|
CreateIndex: 102,
|
|
ModifyIndex: 202,
|
|
},
|
|
{
|
|
ID: "id_3",
|
|
Namespace: "default",
|
|
ServiceName: "s1",
|
|
NodeID: nodeID,
|
|
Datacenter: "dc1",
|
|
JobID: jobID,
|
|
AllocID: allocID,
|
|
Tags: []string{"tag3"},
|
|
Address: "10.0.0.3",
|
|
Port: 9003,
|
|
CreateIndex: 103,
|
|
ModifyIndex: 103,
|
|
},
|
|
}
|
|
must.NoError(t, s.fsm.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
serviceRegReq := &structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: "s1",
|
|
Choose: "2|abc123", // select 2 in consistent order
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: structs.DefaultNamespace,
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationByNameResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationGetServiceRPCMethod, serviceRegReq, &serviceRegResp)
|
|
must.NoError(t, err)
|
|
|
|
result := serviceRegResp.Services
|
|
|
|
must.Len(t, 2, result)
|
|
must.Eq(t, "10.0.0.3", result[0].Address)
|
|
must.Eq(t, "10.0.0.2", result[1].Address)
|
|
},
|
|
},
|
|
{
|
|
name: "choose 3 of 2", // gracefully handle requesting too many
|
|
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
|
|
server, cleanup := TestServer(t, nil)
|
|
return server, nil, cleanup
|
|
},
|
|
testFn: func(t *testing.T, s *Server, _ *structs.ACLToken) {
|
|
codec := rpcClient(t, s)
|
|
testutil.WaitForLeader(t, s.RPC)
|
|
|
|
// insert 2 instances of service s1
|
|
nodeID, jobID, allocID := "node_id", "job_id", "alloc_id"
|
|
services := []*structs.ServiceRegistration{
|
|
{
|
|
ID: "id_1",
|
|
Namespace: "default",
|
|
ServiceName: "s1",
|
|
NodeID: nodeID,
|
|
Datacenter: "dc1",
|
|
JobID: jobID,
|
|
AllocID: allocID,
|
|
Tags: []string{"tag1"},
|
|
Address: "10.0.0.1",
|
|
Port: 9001,
|
|
CreateIndex: 101,
|
|
ModifyIndex: 201,
|
|
},
|
|
{
|
|
ID: "id_2",
|
|
Namespace: "default",
|
|
ServiceName: "s1",
|
|
NodeID: nodeID,
|
|
Datacenter: "dc1",
|
|
JobID: jobID,
|
|
AllocID: allocID,
|
|
Tags: []string{"tag2"},
|
|
Address: "10.0.0.2",
|
|
Port: 9002,
|
|
CreateIndex: 102,
|
|
ModifyIndex: 202,
|
|
},
|
|
}
|
|
must.NoError(t, s.fsm.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 10, services))
|
|
|
|
serviceRegReq := &structs.ServiceRegistrationByNameRequest{
|
|
ServiceName: "s1",
|
|
Choose: "3|abc123", // select 3 in consistent order (though there are only 2 total)
|
|
QueryOptions: structs.QueryOptions{
|
|
Namespace: structs.DefaultNamespace,
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var serviceRegResp structs.ServiceRegistrationByNameResponse
|
|
err := msgpackrpc.CallWithCodec(
|
|
codec, structs.ServiceRegistrationGetServiceRPCMethod, serviceRegReq, &serviceRegResp)
|
|
must.NoError(t, err)
|
|
|
|
result := serviceRegResp.Services
|
|
|
|
must.Len(t, 2, result)
|
|
must.Eq(t, "10.0.0.2", result[0].Address)
|
|
must.Eq(t, "10.0.0.1", result[1].Address)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
server, aclToken, cleanup := tc.serverFn(t)
|
|
defer cleanup()
|
|
tc.testFn(t, server, aclToken)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServiceRegistration_chooseErr(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
sr := (*ServiceRegistration)(nil)
|
|
try := func(input []*structs.ServiceRegistration, parameter string) {
|
|
result, err := sr.choose(input, parameter)
|
|
must.SliceEmpty(t, result)
|
|
must.ErrorIs(t, err, structs.ErrMalformedChooseParameter)
|
|
}
|
|
|
|
regs := []*structs.ServiceRegistration{
|
|
{ID: "abc001", ServiceName: "s1"},
|
|
{ID: "abc002", ServiceName: "s2"},
|
|
{ID: "abc003", ServiceName: "s3"},
|
|
}
|
|
|
|
try(regs, "")
|
|
try(regs, "1|")
|
|
try(regs, "|abc")
|
|
try(regs, "a|abc")
|
|
}
|
|
|
|
func TestServiceRegistration_choose(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
sr := (*ServiceRegistration)(nil)
|
|
try := func(input, exp []*structs.ServiceRegistration, parameter string) {
|
|
result, err := sr.choose(input, parameter)
|
|
must.NoError(t, err)
|
|
must.Eq(t, exp, result)
|
|
}
|
|
|
|
// zero services
|
|
try(nil, []*structs.ServiceRegistration{}, "1|aaa")
|
|
try(nil, []*structs.ServiceRegistration{}, "2|aaa")
|
|
|
|
// some unique services
|
|
regs := []*structs.ServiceRegistration{
|
|
{ID: "abc001", ServiceName: "s1"},
|
|
{ID: "abc002", ServiceName: "s1"},
|
|
{ID: "abc003", ServiceName: "s1"},
|
|
}
|
|
|
|
// same key, increasing n -> maintains order (n=1)
|
|
try(regs, []*structs.ServiceRegistration{
|
|
{ID: "abc002", ServiceName: "s1"},
|
|
}, "1|aaa")
|
|
|
|
// same key, increasing n -> maintains order (n=2)
|
|
try(regs, []*structs.ServiceRegistration{
|
|
{ID: "abc002", ServiceName: "s1"},
|
|
{ID: "abc003", ServiceName: "s1"},
|
|
}, "2|aaa")
|
|
|
|
// same key, increasing n -> maintains order (n=3)
|
|
try(regs, []*structs.ServiceRegistration{
|
|
{ID: "abc002", ServiceName: "s1"},
|
|
{ID: "abc003", ServiceName: "s1"},
|
|
{ID: "abc001", ServiceName: "s1"},
|
|
}, "3|aaa")
|
|
|
|
// unique key -> different orders
|
|
try(regs, []*structs.ServiceRegistration{
|
|
{ID: "abc001", ServiceName: "s1"},
|
|
{ID: "abc002", ServiceName: "s1"},
|
|
{ID: "abc003", ServiceName: "s1"},
|
|
}, "3|bbb")
|
|
|
|
// another key -> another order
|
|
try(regs, []*structs.ServiceRegistration{
|
|
{ID: "abc002", ServiceName: "s1"},
|
|
{ID: "abc003", ServiceName: "s1"},
|
|
{ID: "abc001", ServiceName: "s1"},
|
|
}, "3|ccc")
|
|
}
|