Internal endpoint to query intentions associated with a gateway (#8400)
This commit is contained in:
parent
c2d02e8a06
commit
50fee12d62
|
@ -205,6 +205,90 @@ func (m *Internal) GatewayServiceDump(args *structs.ServiceSpecificRequest, repl
|
|||
return err
|
||||
}
|
||||
|
||||
// Match returns the set of intentions that match the given source/destination.
|
||||
func (m *Internal) GatewayIntentions(args *structs.IntentionQueryRequest, reply *structs.IndexedIntentions) error {
|
||||
// Forward if necessary
|
||||
if done, err := m.srv.ForwardRPC("Internal.GatewayIntentions", args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args.Match.Entries) > 1 {
|
||||
return fmt.Errorf("Expected 1 gateway name, got %d", len(args.Match.Entries))
|
||||
}
|
||||
|
||||
// Get the ACL token for the request for the checks below.
|
||||
var entMeta structs.EnterpriseMeta
|
||||
var authzContext acl.AuthorizerContext
|
||||
|
||||
authz, err := m.srv.ResolveTokenAndDefaultMeta(args.Token, &entMeta, &authzContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if args.Match.Entries[0].Namespace == "" {
|
||||
args.Match.Entries[0].Namespace = entMeta.NamespaceOrDefault()
|
||||
}
|
||||
if err := m.srv.validateEnterpriseIntentionNamespace(args.Match.Entries[0].Namespace, true); err != nil {
|
||||
return fmt.Errorf("Invalid match entry namespace %q: %v", args.Match.Entries[0].Namespace, err)
|
||||
}
|
||||
|
||||
// We need read access to the gateway we're trying to find intentions for, so check that first.
|
||||
if authz != nil && authz.ServiceRead(args.Match.Entries[0].Name, &authzContext) != acl.Allow {
|
||||
return acl.ErrPermissionDenied
|
||||
}
|
||||
|
||||
return m.srv.blockingQuery(
|
||||
&args.QueryOptions,
|
||||
&reply.QueryMeta,
|
||||
func(ws memdb.WatchSet, state *state.Store) error {
|
||||
var maxIdx uint64
|
||||
idx, gatewayServices, err := state.GatewayServices(ws, args.Match.Entries[0].Name, &entMeta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if idx > maxIdx {
|
||||
maxIdx = idx
|
||||
}
|
||||
|
||||
// Loop over the gateway <-> serviceName mappings and fetch all intentions for each
|
||||
seen := make(map[string]bool)
|
||||
result := make(structs.Intentions, 0)
|
||||
|
||||
for _, gs := range gatewayServices {
|
||||
entry := structs.IntentionMatchEntry{
|
||||
Namespace: gs.Service.NamespaceOrDefault(),
|
||||
Name: gs.Service.Name,
|
||||
}
|
||||
idx, intentions, err := state.IntentionMatchOne(ws, entry, structs.IntentionMatchDestination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if idx > maxIdx {
|
||||
maxIdx = idx
|
||||
}
|
||||
|
||||
// Deduplicate wildcard intentions
|
||||
for _, ixn := range intentions {
|
||||
if !seen[ixn.ID] {
|
||||
result = append(result, ixn)
|
||||
seen[ixn.ID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reply.Index, reply.Intentions = maxIdx, result
|
||||
if reply.Intentions == nil {
|
||||
reply.Intentions = make(structs.Intentions, 0)
|
||||
}
|
||||
|
||||
if err := m.srv.filterACL(args.Token, reply); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// EventFire is a bit of an odd endpoint, but it allows for a cross-DC RPC
|
||||
// call to fire an event. The primary use case is to enable user events being
|
||||
// triggered in a remote DC.
|
||||
|
|
|
@ -1329,3 +1329,231 @@ func TestInternal_GatewayServiceDump_Ingress_ACL(t *testing.T) {
|
|||
require.Equal(t, nodes[0].Service.Service, "db")
|
||||
require.Equal(t, nodes[0].Checks[0].Status, api.HealthWarning)
|
||||
}
|
||||
|
||||
func TestInternal_GatewayIntentions(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir1, s1 := testServer(t)
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForTestAgent(t, s1.RPC, "dc1")
|
||||
|
||||
// Register terminating gateway and config entry linking it to postgres + redis
|
||||
{
|
||||
arg := structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
ID: "terminating-gateway",
|
||||
Service: "terminating-gateway",
|
||||
Kind: structs.ServiceKindTerminatingGateway,
|
||||
Port: 443,
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "terminating connect",
|
||||
Status: api.HealthPassing,
|
||||
ServiceID: "terminating-gateway",
|
||||
},
|
||||
}
|
||||
var regOutput struct{}
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, ®Output))
|
||||
|
||||
args := &structs.TerminatingGatewayConfigEntry{
|
||||
Name: "terminating-gateway",
|
||||
Kind: structs.TerminatingGateway,
|
||||
Services: []structs.LinkedService{
|
||||
{
|
||||
Name: "postgres",
|
||||
},
|
||||
{
|
||||
Name: "redis",
|
||||
CAFile: "/etc/certs/ca.pem",
|
||||
CertFile: "/etc/certs/cert.pem",
|
||||
KeyFile: "/etc/certs/key.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req := structs.ConfigEntryRequest{
|
||||
Op: structs.ConfigEntryUpsert,
|
||||
Datacenter: "dc1",
|
||||
Entry: args,
|
||||
}
|
||||
var configOutput bool
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &req, &configOutput))
|
||||
require.True(t, configOutput)
|
||||
}
|
||||
|
||||
// create some symmetric intentions to ensure we are only matching on destination
|
||||
{
|
||||
for _, v := range []string{"*", "mysql", "redis", "postgres"} {
|
||||
req := structs.IntentionRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.IntentionOpCreate,
|
||||
Intention: structs.TestIntention(t),
|
||||
}
|
||||
req.Intention.SourceName = "api"
|
||||
req.Intention.DestinationName = v
|
||||
|
||||
var reply string
|
||||
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &req, &reply))
|
||||
|
||||
req = structs.IntentionRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.IntentionOpCreate,
|
||||
Intention: structs.TestIntention(t),
|
||||
}
|
||||
req.Intention.SourceName = v
|
||||
req.Intention.DestinationName = "api"
|
||||
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &req, &reply))
|
||||
}
|
||||
}
|
||||
|
||||
// Request intentions matching the gateway named "terminating-gateway"
|
||||
req := structs.IntentionQueryRequest{
|
||||
Datacenter: "dc1",
|
||||
Match: &structs.IntentionQueryMatch{
|
||||
Type: structs.IntentionMatchDestination,
|
||||
Entries: []structs.IntentionMatchEntry{
|
||||
{
|
||||
Namespace: structs.IntentionDefaultNamespace,
|
||||
Name: "terminating-gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
var reply structs.IndexedIntentions
|
||||
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.GatewayIntentions", &req, &reply))
|
||||
assert.Len(t, reply.Intentions, 3)
|
||||
|
||||
// Only intentions with linked services as a destination should be returned, and wildcard matches should be deduped
|
||||
expected := []string{"postgres", "*", "redis"}
|
||||
actual := []string{
|
||||
reply.Intentions[0].DestinationName,
|
||||
reply.Intentions[1].DestinationName,
|
||||
reply.Intentions[2].DestinationName,
|
||||
}
|
||||
assert.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestInternal_GatewayIntentions_aclDeny(t *testing.T) {
|
||||
dir1, s1 := testServerWithConfig(t, testServerACLConfig(nil))
|
||||
defer os.RemoveAll(dir1)
|
||||
defer s1.Shutdown()
|
||||
codec := rpcClient(t, s1)
|
||||
defer codec.Close()
|
||||
|
||||
testrpc.WaitForTestAgent(t, s1.RPC, "dc1", testrpc.WithToken(TestDefaultMasterToken))
|
||||
|
||||
// Register terminating gateway and config entry linking it to postgres + redis
|
||||
{
|
||||
arg := structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
ID: "terminating-gateway",
|
||||
Service: "terminating-gateway",
|
||||
Kind: structs.ServiceKindTerminatingGateway,
|
||||
Port: 443,
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "terminating connect",
|
||||
Status: api.HealthPassing,
|
||||
ServiceID: "terminating-gateway",
|
||||
},
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||
}
|
||||
var regOutput struct{}
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Catalog.Register", &arg, ®Output))
|
||||
|
||||
args := &structs.TerminatingGatewayConfigEntry{
|
||||
Name: "terminating-gateway",
|
||||
Kind: structs.TerminatingGateway,
|
||||
Services: []structs.LinkedService{
|
||||
{
|
||||
Name: "postgres",
|
||||
},
|
||||
{
|
||||
Name: "redis",
|
||||
CAFile: "/etc/certs/ca.pem",
|
||||
CertFile: "/etc/certs/cert.pem",
|
||||
KeyFile: "/etc/certs/key.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req := structs.ConfigEntryRequest{
|
||||
Op: structs.ConfigEntryUpsert,
|
||||
Datacenter: "dc1",
|
||||
Entry: args,
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||
}
|
||||
var configOutput bool
|
||||
require.NoError(t, msgpackrpc.CallWithCodec(codec, "ConfigEntry.Apply", &req, &configOutput))
|
||||
require.True(t, configOutput)
|
||||
}
|
||||
|
||||
// create some symmetric intentions to ensure we are only matching on destination
|
||||
{
|
||||
for _, v := range []string{"*", "mysql", "redis", "postgres"} {
|
||||
req := structs.IntentionRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.IntentionOpCreate,
|
||||
Intention: structs.TestIntention(t),
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||
}
|
||||
req.Intention.SourceName = "api"
|
||||
req.Intention.DestinationName = v
|
||||
|
||||
var reply string
|
||||
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &req, &reply))
|
||||
|
||||
req = structs.IntentionRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.IntentionOpCreate,
|
||||
Intention: structs.TestIntention(t),
|
||||
WriteRequest: structs.WriteRequest{Token: TestDefaultMasterToken},
|
||||
}
|
||||
req.Intention.SourceName = v
|
||||
req.Intention.DestinationName = "api"
|
||||
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &req, &reply))
|
||||
}
|
||||
}
|
||||
|
||||
userToken, err := upsertTestTokenWithPolicyRules(codec, TestDefaultMasterToken, "dc1", `
|
||||
service_prefix "redis" { policy = "read" }
|
||||
service_prefix "terminating-gateway" { policy = "read" }
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Request intentions matching the gateway named "terminating-gateway"
|
||||
req := structs.IntentionQueryRequest{
|
||||
Datacenter: "dc1",
|
||||
Match: &structs.IntentionQueryMatch{
|
||||
Type: structs.IntentionMatchDestination,
|
||||
Entries: []structs.IntentionMatchEntry{
|
||||
{
|
||||
Namespace: structs.IntentionDefaultNamespace,
|
||||
Name: "terminating-gateway",
|
||||
},
|
||||
},
|
||||
},
|
||||
QueryOptions: structs.QueryOptions{Token: userToken.SecretID},
|
||||
}
|
||||
var reply structs.IndexedIntentions
|
||||
assert.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.GatewayIntentions", &req, &reply))
|
||||
assert.Len(t, reply.Intentions, 2)
|
||||
|
||||
// Only intentions for redis should be returned, due to ACLs
|
||||
expected := []string{"*", "redis"}
|
||||
actual := []string{
|
||||
reply.Intentions[0].DestinationName,
|
||||
reply.Intentions[1].DestinationName,
|
||||
}
|
||||
assert.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
|
|
|
@ -338,31 +338,11 @@ func (s *Store) IntentionMatch(ws memdb.WatchSet, args *structs.IntentionQueryMa
|
|||
// Make all the calls and accumulate the results
|
||||
results := make([]structs.Intentions, len(args.Entries))
|
||||
for i, entry := range args.Entries {
|
||||
// Each search entry may require multiple queries to memdb, so this
|
||||
// returns the arguments for each necessary Get. Note on performance:
|
||||
// this is not the most optimal set of queries since we repeat some
|
||||
// many times (such as */*). We can work on improving that in the
|
||||
// future, the test cases shouldn't have to change for that.
|
||||
getParams, err := s.intentionMatchGetParams(entry)
|
||||
ixns, err := s.intentionMatchOneTxn(tx, ws, entry, args.Type)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
// Perform each call and accumulate the result.
|
||||
var ixns structs.Intentions
|
||||
for _, params := range getParams {
|
||||
iter, err := tx.Get(intentionsTableName, string(args.Type), params...)
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("failed intention lookup: %s", err)
|
||||
}
|
||||
|
||||
ws.Add(iter.WatchCh())
|
||||
|
||||
for ixn := iter.Next(); ixn != nil; ixn = iter.Next() {
|
||||
ixns = append(ixns, ixn.(*structs.Intention))
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the results by precedence
|
||||
sort.Sort(structs.IntentionPrecedenceSorter(ixns))
|
||||
|
||||
|
@ -373,9 +353,66 @@ func (s *Store) IntentionMatch(ws memdb.WatchSet, args *structs.IntentionQueryMa
|
|||
return idx, results, nil
|
||||
}
|
||||
|
||||
// IntentionMatchOne returns the list of intentions that match the namespace and
|
||||
// name for a single source or destination. This applies the resolution rules
|
||||
// so wildcards will match any value.
|
||||
//
|
||||
// The returned intentions are sorted based on the intention precedence rules.
|
||||
// i.e. result[0] is the highest precedent rule to match
|
||||
func (s *Store) IntentionMatchOne(ws memdb.WatchSet,
|
||||
entry structs.IntentionMatchEntry, matchType structs.IntentionMatchType) (uint64, structs.Intentions, error) {
|
||||
tx := s.db.Txn(false)
|
||||
defer tx.Abort()
|
||||
|
||||
// Get the table index.
|
||||
idx := maxIndexTxn(tx, intentionsTableName)
|
||||
if idx < 1 {
|
||||
idx = 1
|
||||
}
|
||||
|
||||
results, err := s.intentionMatchOneTxn(tx, ws, entry, matchType)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
sort.Sort(structs.IntentionPrecedenceSorter(results))
|
||||
|
||||
return idx, results, nil
|
||||
}
|
||||
|
||||
func (s *Store) intentionMatchOneTxn(tx ReadTxn, ws memdb.WatchSet,
|
||||
entry structs.IntentionMatchEntry, matchType structs.IntentionMatchType) (structs.Intentions, error) {
|
||||
|
||||
// Each search entry may require multiple queries to memdb, so this
|
||||
// returns the arguments for each necessary Get. Note on performance:
|
||||
// this is not the most optimal set of queries since we repeat some
|
||||
// many times (such as */*). We can work on improving that in the
|
||||
// future, the test cases shouldn't have to change for that.
|
||||
getParams, err := intentionMatchGetParams(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Perform each call and accumulate the result.
|
||||
var result structs.Intentions
|
||||
for _, params := range getParams {
|
||||
iter, err := tx.Get(intentionsTableName, string(matchType), params...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed intention lookup: %s", err)
|
||||
}
|
||||
|
||||
ws.Add(iter.WatchCh())
|
||||
|
||||
for ixn := iter.Next(); ixn != nil; ixn = iter.Next() {
|
||||
result = append(result, ixn.(*structs.Intention))
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// intentionMatchGetParams returns the tx.Get parameters to find all the
|
||||
// intentions for a certain entry.
|
||||
func (s *Store) intentionMatchGetParams(entry structs.IntentionMatchEntry) ([][]interface{}, error) {
|
||||
func intentionMatchGetParams(entry structs.IntentionMatchEntry) ([][]interface{}, error) {
|
||||
// We always query for "*/*" so include that. If the namespace is a
|
||||
// wildcard, then we're actually done.
|
||||
result := make([][]interface{}, 0, 3)
|
||||
|
|
|
@ -463,6 +463,141 @@ func TestStore_IntentionMatch_table(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Equivalent to TestStore_IntentionMatch_table but for IntentionMatchOne which matches a single service
|
||||
func TestStore_IntentionMatchOne_table(t *testing.T) {
|
||||
type testCase struct {
|
||||
Name string
|
||||
Insert [][]string // List of intentions to insert
|
||||
Query []string // List of intentions to match
|
||||
Expected [][]string // List of matches, where each match is a list of intentions
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
{
|
||||
"single exact namespace/name",
|
||||
[][]string{
|
||||
{"foo", "*"},
|
||||
{"foo", "bar"},
|
||||
{"foo", "baz"}, // shouldn't match
|
||||
{"bar", "bar"}, // shouldn't match
|
||||
{"bar", "*"}, // shouldn't match
|
||||
{"*", "*"},
|
||||
},
|
||||
[]string{
|
||||
"foo", "bar",
|
||||
},
|
||||
[][]string{
|
||||
{"foo", "bar"},
|
||||
{"foo", "*"},
|
||||
{"*", "*"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"single exact namespace/name with duplicate destinations",
|
||||
[][]string{
|
||||
// 4-tuple specifies src and destination to test duplicate destinations
|
||||
// with different sources. We flip them around to test in both
|
||||
// directions. The first pair are the ones searched on in both cases so
|
||||
// the duplicates need to be there.
|
||||
{"foo", "bar", "foo", "*"},
|
||||
{"foo", "bar", "bar", "*"},
|
||||
{"*", "*", "*", "*"},
|
||||
},
|
||||
[]string{
|
||||
"foo", "bar",
|
||||
},
|
||||
[][]string{
|
||||
// Note the first two have the same precedence so we rely on arbitrary
|
||||
// lexicographical tie-break behavior.
|
||||
{"foo", "bar", "bar", "*"},
|
||||
{"foo", "bar", "foo", "*"},
|
||||
{"*", "*", "*", "*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testRunner := func(t *testing.T, tc testCase, typ structs.IntentionMatchType) {
|
||||
// Insert the set
|
||||
assert := assert.New(t)
|
||||
s := testStateStore(t)
|
||||
var idx uint64 = 1
|
||||
for _, v := range tc.Insert {
|
||||
ixn := &structs.Intention{ID: testUUID()}
|
||||
switch typ {
|
||||
case structs.IntentionMatchDestination:
|
||||
ixn.DestinationNS = v[0]
|
||||
ixn.DestinationName = v[1]
|
||||
if len(v) == 4 {
|
||||
ixn.SourceNS = v[2]
|
||||
ixn.SourceName = v[3]
|
||||
}
|
||||
case structs.IntentionMatchSource:
|
||||
ixn.SourceNS = v[0]
|
||||
ixn.SourceName = v[1]
|
||||
if len(v) == 4 {
|
||||
ixn.DestinationNS = v[2]
|
||||
ixn.DestinationName = v[3]
|
||||
}
|
||||
}
|
||||
|
||||
assert.NoError(s.IntentionSet(idx, ixn))
|
||||
|
||||
idx++
|
||||
}
|
||||
|
||||
// Build the arguments and match
|
||||
entry := structs.IntentionMatchEntry{
|
||||
Namespace: tc.Query[0],
|
||||
Name: tc.Query[1],
|
||||
}
|
||||
_, matches, err := s.IntentionMatchOne(nil, entry, typ)
|
||||
assert.NoError(err)
|
||||
|
||||
// Should have equal lengths
|
||||
require.Len(t, matches, len(tc.Expected))
|
||||
|
||||
// Verify matches
|
||||
var actual [][]string
|
||||
for _, ixn := range matches {
|
||||
switch typ {
|
||||
case structs.IntentionMatchDestination:
|
||||
if len(tc.Expected) > 1 && len(tc.Expected[0]) == 4 {
|
||||
actual = append(actual, []string{
|
||||
ixn.DestinationNS,
|
||||
ixn.DestinationName,
|
||||
ixn.SourceNS,
|
||||
ixn.SourceName,
|
||||
})
|
||||
} else {
|
||||
actual = append(actual, []string{ixn.DestinationNS, ixn.DestinationName})
|
||||
}
|
||||
case structs.IntentionMatchSource:
|
||||
if len(tc.Expected) > 1 && len(tc.Expected[0]) == 4 {
|
||||
actual = append(actual, []string{
|
||||
ixn.SourceNS,
|
||||
ixn.SourceName,
|
||||
ixn.DestinationNS,
|
||||
ixn.DestinationName,
|
||||
})
|
||||
} else {
|
||||
actual = append(actual, []string{ixn.SourceNS, ixn.SourceName})
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.Equal(tc.Expected, actual)
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name+" (destination)", func(t *testing.T) {
|
||||
testRunner(t, tc, structs.IntentionMatchDestination)
|
||||
})
|
||||
|
||||
t.Run(tc.Name+" (source)", func(t *testing.T) {
|
||||
testRunner(t, tc, structs.IntentionMatchSource)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_Intention_Snapshot_Restore(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
s := testStateStore(t)
|
||||
|
|
|
@ -98,6 +98,7 @@ func init() {
|
|||
registerEndpoint("/v1/internal/ui/node/", []string{"GET"}, (*HTTPServer).UINodeInfo)
|
||||
registerEndpoint("/v1/internal/ui/services", []string{"GET"}, (*HTTPServer).UIServices)
|
||||
registerEndpoint("/v1/internal/ui/gateway-services-nodes/", []string{"GET"}, (*HTTPServer).UIGatewayServicesNodes)
|
||||
registerEndpoint("/v1/internal/ui/gateway-intentions/", []string{"GET"}, (*HTTPServer).UIGatewayIntentions)
|
||||
registerEndpoint("/v1/internal/acl/authorize", []string{"POST"}, (*HTTPServer).ACLAuthorize)
|
||||
registerEndpoint("/v1/kv/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).KVSEndpoint)
|
||||
registerEndpoint("/v1/operator/raft/configuration", []string{"GET"}, (*HTTPServer).OperatorRaftConfiguration)
|
||||
|
|
|
@ -339,3 +339,42 @@ func modifySummaryForGatewayService(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GET /v1/internal/ui/gateway-intentions/:gateway
|
||||
func (s *HTTPServer) UIGatewayIntentions(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
var args structs.IntentionQueryRequest
|
||||
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var entMeta structs.EnterpriseMeta
|
||||
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Pull out the service name
|
||||
name := strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/gateway-intentions/")
|
||||
if name == "" {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprint(resp, "Missing gateway name")
|
||||
return nil, nil
|
||||
}
|
||||
args.Match = &structs.IntentionQueryMatch{
|
||||
Type: structs.IntentionMatchDestination,
|
||||
Entries: []structs.IntentionMatchEntry{
|
||||
{
|
||||
Namespace: entMeta.NamespaceOrEmpty(),
|
||||
Name: name,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var reply structs.IndexedIntentions
|
||||
|
||||
defer setMeta(resp, &reply.QueryMeta)
|
||||
if err := s.agent.RPC("Internal.GatewayIntentions", args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reply.Intentions, nil
|
||||
}
|
||||
|
|
|
@ -700,3 +700,101 @@ func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
|||
}
|
||||
assert.ElementsMatch(t, expect, dump)
|
||||
}
|
||||
|
||||
func TestUIGatewayIntentions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a := NewTestAgent(t, "")
|
||||
defer a.Shutdown()
|
||||
|
||||
// Register terminating gateway and config entry linking it to postgres + redis
|
||||
{
|
||||
arg := structs.RegisterRequest{
|
||||
Datacenter: "dc1",
|
||||
Node: "foo",
|
||||
Address: "127.0.0.1",
|
||||
Service: &structs.NodeService{
|
||||
ID: "terminating-gateway",
|
||||
Service: "terminating-gateway",
|
||||
Kind: structs.ServiceKindTerminatingGateway,
|
||||
Port: 443,
|
||||
},
|
||||
Check: &structs.HealthCheck{
|
||||
Name: "terminating connect",
|
||||
Status: api.HealthPassing,
|
||||
ServiceID: "terminating-gateway",
|
||||
},
|
||||
}
|
||||
var regOutput struct{}
|
||||
require.NoError(t, a.RPC("Catalog.Register", &arg, ®Output))
|
||||
|
||||
args := &structs.TerminatingGatewayConfigEntry{
|
||||
Name: "terminating-gateway",
|
||||
Kind: structs.TerminatingGateway,
|
||||
Services: []structs.LinkedService{
|
||||
{
|
||||
Name: "postgres",
|
||||
},
|
||||
{
|
||||
Name: "redis",
|
||||
CAFile: "/etc/certs/ca.pem",
|
||||
CertFile: "/etc/certs/cert.pem",
|
||||
KeyFile: "/etc/certs/key.pem",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req := structs.ConfigEntryRequest{
|
||||
Op: structs.ConfigEntryUpsert,
|
||||
Datacenter: "dc1",
|
||||
Entry: args,
|
||||
}
|
||||
var configOutput bool
|
||||
require.NoError(t, a.RPC("ConfigEntry.Apply", &req, &configOutput))
|
||||
require.True(t, configOutput)
|
||||
}
|
||||
|
||||
// create some symmetric intentions to ensure we are only matching on destination
|
||||
{
|
||||
for _, v := range []string{"*", "mysql", "redis", "postgres"} {
|
||||
req := structs.IntentionRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.IntentionOpCreate,
|
||||
Intention: structs.TestIntention(t),
|
||||
}
|
||||
req.Intention.SourceName = "api"
|
||||
req.Intention.DestinationName = v
|
||||
|
||||
var reply string
|
||||
assert.NoError(t, a.RPC("Intention.Apply", &req, &reply))
|
||||
|
||||
req = structs.IntentionRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.IntentionOpCreate,
|
||||
Intention: structs.TestIntention(t),
|
||||
}
|
||||
req.Intention.SourceName = v
|
||||
req.Intention.DestinationName = "api"
|
||||
assert.NoError(t, a.RPC("Intention.Apply", &req, &reply))
|
||||
}
|
||||
}
|
||||
|
||||
// Request intentions matching the gateway named "terminating-gateway"
|
||||
req, _ := http.NewRequest("GET", "/v1/internal/ui/gateway-intentions/terminating-gateway", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
obj, err := a.srv.UIGatewayIntentions(resp, req)
|
||||
assert.Nil(t, err)
|
||||
assertIndex(t, resp)
|
||||
|
||||
intentions := obj.(structs.Intentions)
|
||||
assert.Len(t, intentions, 3)
|
||||
|
||||
// Only intentions with linked services as a destination should be returned, and wildcard matches should be deduped
|
||||
expected := []string{"postgres", "*", "redis"}
|
||||
actual := []string{
|
||||
intentions[0].DestinationName,
|
||||
intentions[1].DestinationName,
|
||||
intentions[2].DestinationName,
|
||||
}
|
||||
assert.ElementsMatch(t, expected, actual)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue