mirror of https://github.com/status-im/consul.git
Enable filtering language support for the v1/connect/intentions… (#7593)
* Enable filtering language support for the v1/connect/intentions listing API * Update website for filtering of Intentions * Update website/source/api/connect/intentions.html.md
This commit is contained in:
parent
8549cc2d99
commit
0e7d3d93b3
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/hashicorp/consul/agent/consul/state"
|
"github.com/hashicorp/consul/agent/consul/state"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/lib"
|
"github.com/hashicorp/consul/lib"
|
||||||
|
"github.com/hashicorp/go-bexpr"
|
||||||
"github.com/hashicorp/go-hclog"
|
"github.com/hashicorp/go-hclog"
|
||||||
"github.com/hashicorp/go-memdb"
|
"github.com/hashicorp/go-memdb"
|
||||||
)
|
)
|
||||||
|
@ -290,6 +291,11 @@ func (s *Intention) List(
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filter, err := bexpr.CreateFilter(args.Filter, nil, reply.Intentions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return s.srv.blockingQuery(
|
return s.srv.blockingQuery(
|
||||||
&args.QueryOptions, &reply.QueryMeta,
|
&args.QueryOptions, &reply.QueryMeta,
|
||||||
func(ws memdb.WatchSet, state *state.Store) error {
|
func(ws memdb.WatchSet, state *state.Store) error {
|
||||||
|
@ -303,7 +309,17 @@ func (s *Intention) List(
|
||||||
reply.Intentions = make(structs.Intentions, 0)
|
reply.Intentions = make(structs.Intentions, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.srv.filterACL(args.Token, reply)
|
if err := s.srv.filterACL(args.Token, reply); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := filter.Execute(reply.Intentions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reply.Intentions = raw.(structs.Intentions)
|
||||||
|
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1062,41 +1062,17 @@ func TestIntentionList(t *testing.T) {
|
||||||
func TestIntentionList_acl(t *testing.T) {
|
func TestIntentionList_acl(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
assert := assert.New(t)
|
dir1, s1 := testServerWithConfig(t, testServerACLConfig(nil))
|
||||||
dir1, s1 := testServerWithConfig(t, func(c *Config) {
|
|
||||||
c.ACLDatacenter = "dc1"
|
|
||||||
c.ACLsEnabled = true
|
|
||||||
c.ACLMasterToken = "root"
|
|
||||||
c.ACLDefaultPolicy = "deny"
|
|
||||||
})
|
|
||||||
defer os.RemoveAll(dir1)
|
defer os.RemoveAll(dir1)
|
||||||
defer s1.Shutdown()
|
defer s1.Shutdown()
|
||||||
codec := rpcClient(t, s1)
|
codec := rpcClient(t, s1)
|
||||||
defer codec.Close()
|
defer codec.Close()
|
||||||
|
|
||||||
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
testrpc.WaitForLeader(t, s1.RPC, "dc1")
|
||||||
|
waitForNewACLs(t, s1)
|
||||||
|
|
||||||
// Create an ACL with service write permissions. This will grant
|
token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultMasterToken, "dc1", `service_prefix "foo" { policy = "write" }`)
|
||||||
// intentions read.
|
require.NoError(t, err)
|
||||||
var token string
|
|
||||||
{
|
|
||||||
var rules = `
|
|
||||||
service "foo" {
|
|
||||||
policy = "write"
|
|
||||||
}`
|
|
||||||
|
|
||||||
req := structs.ACLRequest{
|
|
||||||
Datacenter: "dc1",
|
|
||||||
Op: structs.ACLSet,
|
|
||||||
ACL: structs.ACL{
|
|
||||||
Name: "User token",
|
|
||||||
Type: structs.ACLTokenTypeClient,
|
|
||||||
Rules: rules,
|
|
||||||
},
|
|
||||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
|
||||||
}
|
|
||||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a few records
|
// Create a few records
|
||||||
for _, name := range []string{"foobar", "bar", "baz"} {
|
for _, name := range []string{"foobar", "bar", "baz"} {
|
||||||
|
@ -1108,44 +1084,58 @@ service "foo" {
|
||||||
ixn.Intention.SourceNS = "default"
|
ixn.Intention.SourceNS = "default"
|
||||||
ixn.Intention.DestinationNS = "default"
|
ixn.Intention.DestinationNS = "default"
|
||||||
ixn.Intention.DestinationName = name
|
ixn.Intention.DestinationName = name
|
||||||
ixn.WriteRequest.Token = "root"
|
ixn.WriteRequest.Token = TestDefaultMasterToken
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
var reply string
|
var reply string
|
||||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with no token
|
// Test with no token
|
||||||
{
|
t.Run("no-token", func(t *testing.T) {
|
||||||
req := &structs.DCSpecificRequest{
|
req := &structs.DCSpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
}
|
}
|
||||||
var resp structs.IndexedIntentions
|
var resp structs.IndexedIntentions
|
||||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
||||||
assert.Len(resp.Intentions, 0)
|
require.Len(t, resp.Intentions, 0)
|
||||||
}
|
})
|
||||||
|
|
||||||
// Test with management token
|
// Test with management token
|
||||||
{
|
t.Run("master-token", func(t *testing.T) {
|
||||||
req := &structs.DCSpecificRequest{
|
req := &structs.DCSpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
QueryOptions: structs.QueryOptions{Token: "root"},
|
QueryOptions: structs.QueryOptions{Token: TestDefaultMasterToken},
|
||||||
}
|
}
|
||||||
var resp structs.IndexedIntentions
|
var resp structs.IndexedIntentions
|
||||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
||||||
assert.Len(resp.Intentions, 3)
|
require.Len(t, resp.Intentions, 3)
|
||||||
}
|
})
|
||||||
|
|
||||||
// Test with user token
|
// Test with user token
|
||||||
{
|
t.Run("user-token", func(t *testing.T) {
|
||||||
req := &structs.DCSpecificRequest{
|
req := &structs.DCSpecificRequest{
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
QueryOptions: structs.QueryOptions{Token: token},
|
QueryOptions: structs.QueryOptions{Token: token.SecretID},
|
||||||
}
|
}
|
||||||
var resp structs.IndexedIntentions
|
var resp structs.IndexedIntentions
|
||||||
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
||||||
assert.Len(resp.Intentions, 1)
|
require.Len(t, resp.Intentions, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("filtered", func(t *testing.T) {
|
||||||
|
req := &structs.DCSpecificRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
QueryOptions: structs.QueryOptions{
|
||||||
|
Token: TestDefaultMasterToken,
|
||||||
|
Filter: "DestinationName == foobar",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resp structs.IndexedIntentions
|
||||||
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
|
||||||
|
require.Len(t, resp.Intentions, 1)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test basic matching. We don't need to exhaustively test inputs since this
|
// Test basic matching. We don't need to exhaustively test inputs since this
|
||||||
|
|
|
@ -71,16 +71,16 @@ type Intention struct {
|
||||||
|
|
||||||
// CreatedAt and UpdatedAt keep track of when this record was created
|
// CreatedAt and UpdatedAt keep track of when this record was created
|
||||||
// or modified.
|
// or modified.
|
||||||
CreatedAt, UpdatedAt time.Time `mapstructure:"-"`
|
CreatedAt, UpdatedAt time.Time `mapstructure:"-" bexpr:"-"`
|
||||||
|
|
||||||
// Hash of the contents of the intention
|
// Hash of the contents of the intention
|
||||||
//
|
//
|
||||||
// This is needed mainly for replication purposes. When replicating from
|
// This is needed mainly for replication purposes. When replicating from
|
||||||
// one DC to another keeping the content Hash will allow us to detect
|
// one DC to another keeping the content Hash will allow us to detect
|
||||||
// content changes more efficiently than checking every single field
|
// content changes more efficiently than checking every single field
|
||||||
Hash []byte
|
Hash []byte `bexpr:"-"`
|
||||||
|
|
||||||
RaftIndex
|
RaftIndex `bexpr:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Intention) UnmarshalJSON(data []byte) (err error) {
|
func (t *Intention) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
|
|
@ -525,6 +525,70 @@ var expectedFieldConfigNodeInfo bexpr.FieldConfigurations = bexpr.FieldConfigura
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var expectedFieldConfigIntention bexpr.FieldConfigurations = bexpr.FieldConfigurations{
|
||||||
|
"ID": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "ID",
|
||||||
|
CoerceFn: bexpr.CoerceString,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches},
|
||||||
|
},
|
||||||
|
"Description": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "Description",
|
||||||
|
CoerceFn: bexpr.CoerceString,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches},
|
||||||
|
},
|
||||||
|
"SourceNS": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "SourceNS",
|
||||||
|
CoerceFn: bexpr.CoerceString,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches},
|
||||||
|
},
|
||||||
|
"SourceName": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "SourceName",
|
||||||
|
CoerceFn: bexpr.CoerceString,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches},
|
||||||
|
},
|
||||||
|
"DestinationNS": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "DestinationNS",
|
||||||
|
CoerceFn: bexpr.CoerceString,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches},
|
||||||
|
},
|
||||||
|
"DestinationName": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "DestinationName",
|
||||||
|
CoerceFn: bexpr.CoerceString,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches},
|
||||||
|
},
|
||||||
|
"SourceType": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "SourceType",
|
||||||
|
CoerceFn: bexpr.CoerceString,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches},
|
||||||
|
},
|
||||||
|
"Action": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "Action",
|
||||||
|
CoerceFn: bexpr.CoerceString,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches},
|
||||||
|
},
|
||||||
|
"DefaultAddr": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "DefaultAddr",
|
||||||
|
CoerceFn: bexpr.CoerceString,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches},
|
||||||
|
},
|
||||||
|
"DefaultPort": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "DefaultPort",
|
||||||
|
CoerceFn: bexpr.CoerceInt,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual},
|
||||||
|
},
|
||||||
|
"Precedence": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "Precedence",
|
||||||
|
CoerceFn: bexpr.CoerceInt,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual},
|
||||||
|
},
|
||||||
|
"Meta": &bexpr.FieldConfiguration{
|
||||||
|
StructFieldName: "Meta",
|
||||||
|
CoerceFn: bexpr.CoerceString,
|
||||||
|
SupportedOperations: []bexpr.MatchOperator{bexpr.MatchIsEmpty, bexpr.MatchIsNotEmpty, bexpr.MatchIn, bexpr.MatchNotIn},
|
||||||
|
SubFields: expectedFieldConfigMapStringValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Only need to generate the field configurations for the top level filtered types
|
// Only need to generate the field configurations for the top level filtered types
|
||||||
// The internal types will be checked within these.
|
// The internal types will be checked within these.
|
||||||
var fieldConfigTests map[string]fieldConfigTest = map[string]fieldConfigTest{
|
var fieldConfigTests map[string]fieldConfigTest = map[string]fieldConfigTest{
|
||||||
|
@ -558,6 +622,10 @@ var fieldConfigTests map[string]fieldConfigTest = map[string]fieldConfigTest{
|
||||||
// registered with an agent stays in sync with our internal NodeService structure
|
// registered with an agent stays in sync with our internal NodeService structure
|
||||||
expected: expectedFieldConfigNodeService,
|
expected: expectedFieldConfigNodeService,
|
||||||
},
|
},
|
||||||
|
"Intention": fieldConfigTest{
|
||||||
|
dataType: (*Intention)(nil),
|
||||||
|
expected: expectedFieldConfigIntention,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateFieldConfigurationsRecurse(t *testing.T, expected, actual bexpr.FieldConfigurations, path string) bool {
|
func validateFieldConfigurationsRecurse(t *testing.T, expected, actual bexpr.FieldConfigurations, path string) bool {
|
||||||
|
|
|
@ -165,11 +165,16 @@ The table below shows this endpoint's support for
|
||||||
<sup>1</sup> Intention ACL rules are specified as part of a `service` rule.
|
<sup>1</sup> Intention ACL rules are specified as part of a `service` rule.
|
||||||
See [Intention Management Permissions](/docs/connect/intentions.html#intention-management-permissions) for more details.
|
See [Intention Management Permissions](/docs/connect/intentions.html#intention-management-permissions) for more details.
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
- `filter` `(string: "")` - Specifies the expression used to filter the
|
||||||
|
queries results prior to returning the data.
|
||||||
|
|
||||||
### Sample Request
|
### Sample Request
|
||||||
|
|
||||||
```text
|
```text
|
||||||
$ curl \
|
$ curl \
|
||||||
http://127.0.0.1:8500/v1/connect/intentions
|
'http://127.0.0.1:8500/v1/connect/intentions?filter=SourceName==web'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Sample Response
|
### Sample Response
|
||||||
|
@ -197,6 +202,27 @@ $ curl \
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Filtering
|
||||||
|
|
||||||
|
The filter will be executed against each Intention in the result list with
|
||||||
|
the following selectors and filter operations being supported:
|
||||||
|
|
||||||
|
| Selector | Supported Operations |
|
||||||
|
| --------------- | -------------------------------------------------- |
|
||||||
|
| Action | Equal, Not Equal, In, Not In, Matches, Not Matches |
|
||||||
|
| DefaultAddr | Equal, Not Equal, In, Not In, Matches, Not Matches |
|
||||||
|
| DefaultPort | Equal, Not Equal |
|
||||||
|
| Description | Equal, Not Equal, In, Not In, Matches, Not Matches |
|
||||||
|
| DestinationNS | Equal, Not Equal, In, Not In, Matches, Not Matches |
|
||||||
|
| DestinationName | Equal, Not Equal, In, Not In, Matches, Not Matches |
|
||||||
|
| ID | Equal, Not Equal, In, Not In, Matches, Not Matches |
|
||||||
|
| Meta | Is Empty, Is Not Empty, In, Not In |
|
||||||
|
| Meta.<any> | Equal, Not Equal, In, Not In, Matches, Not Matches |
|
||||||
|
| Precedence | Equal, Not Equal |
|
||||||
|
| SourceNS | Equal, Not Equal, In, Not In, Matches, Not Matches |
|
||||||
|
| SourceName | Equal, Not Equal, In, Not In, Matches, Not Matches |
|
||||||
|
| SourceType | Equal, Not Equal, In, Not In, Matches, Not Matches |
|
||||||
|
|
||||||
## Update Intention
|
## Update Intention
|
||||||
|
|
||||||
This endpoint updates an intention with the given values.
|
This endpoint updates an intention with the given values.
|
||||||
|
|
Loading…
Reference in New Issue