diff --git a/.changelog/8875.txt b/.changelog/8875.txt new file mode 100644 index 000000000..61b6697bc --- /dev/null +++ b/.changelog/8875.txt @@ -0,0 +1,3 @@ +```release-note:improvement +agent: allow the /v1/connect/intentions/match endpoint to use the agent cache +``` diff --git a/agent/intentions_endpoint.go b/agent/intentions_endpoint.go index 5916ebf24..ba57e4733 100644 --- a/agent/intentions_endpoint.go +++ b/agent/intentions_endpoint.go @@ -5,6 +5,7 @@ import ( "net/http" "strings" + cachetype "github.com/hashicorp/consul/agent/cache-types" "github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/agent/structs" ) @@ -135,19 +136,44 @@ func (s *HTTPHandlers) IntentionMatch(resp http.ResponseWriter, req *http.Reques } } - var reply structs.IndexedIntentionMatches - if err := s.agent.RPC("Intention.Match", args, &reply); err != nil { - return nil, err + // Make the RPC request + var out structs.IndexedIntentionMatches + defer setMeta(resp, &out.QueryMeta) + + if s.agent.config.HTTPUseCache && args.QueryOptions.UseCache { + raw, m, err := s.agent.cache.Get(req.Context(), cachetype.IntentionMatchName, args) + if err != nil { + return nil, err + } + defer setCacheMeta(resp, &m) + + reply, ok := raw.(*structs.IndexedIntentionMatches) + if !ok { + // This should never happen, but we want to protect against panics + return nil, fmt.Errorf("internal error: response type not correct") + } + out = *reply + } else { + RETRY_ONCE: + if err := s.agent.RPC("Intention.Match", args, &out); err != nil { + return nil, err + } + if args.QueryOptions.AllowStale && args.MaxStaleDuration > 0 && args.MaxStaleDuration < out.LastContact { + args.AllowStale = false + args.MaxStaleDuration = 0 + goto RETRY_ONCE + } } + out.ConsistencyLevel = args.QueryOptions.ConsistencyLevel() // We must have an identical count of matches - if len(reply.Matches) != len(names) { + if len(out.Matches) != len(names) { return nil, fmt.Errorf("internal error: match response count didn't match input count") } // Use empty list instead of nil. response := make(map[string]structs.Intentions) - for i, ixns := range reply.Matches { + for i, ixns := range out.Matches { response[names[i]] = ixns } diff --git a/agent/intentions_endpoint_test.go b/agent/intentions_endpoint_test.go index 600ec1930..b5741965d 100644 --- a/agent/intentions_endpoint_test.go +++ b/agent/intentions_endpoint_test.go @@ -178,6 +178,46 @@ func TestIntentionMatch(t *testing.T) { require.Equal(t, expected, actual) }) + + t.Run("success with cache", func(t *testing.T) { + // First request is a MISS, but it primes the cache for the second attempt + for i := 0; i < 2; i++ { + req, err := http.NewRequest("GET", "/v1/connect/intentions/match?by=destination&name=bar&cached", nil) + require.NoError(t, err) + + resp := httptest.NewRecorder() + obj, err := a.srv.IntentionMatch(resp, req) + require.NoError(t, err) + + // The GET request primes the cache so the POST is a hit. + if i == 0 { + // Should be a cache miss + require.Equal(t, "MISS", resp.Header().Get("X-Cache")) + } else { + // Should be a cache HIT now! + require.Equal(t, "HIT", resp.Header().Get("X-Cache")) + } + + value := obj.(map[string]structs.Intentions) + require.Len(t, value, 1) + + var actual [][]string + expected := [][]string{ + {"default", "*", "default", "bar"}, + {"default", "*", "default", "*"}, + } + for _, ixn := range value["bar"] { + actual = append(actual, []string{ + ixn.SourceNS, + ixn.SourceName, + ixn.DestinationNS, + ixn.DestinationName, + }) + } + + require.Equal(t, expected, actual) + } + }) } func TestIntentionCheck(t *testing.T) {