diff --git a/docs/antctl.md b/docs/antctl.md index 96976844d18..02d8b25b835 100644 --- a/docs/antctl.md +++ b/docs/antctl.md @@ -25,6 +25,7 @@ running in three different modes: - [controllerinfo and agentinfo commands](#controllerinfo-and-agentinfo-commands) - [NetworkPolicy commands](#networkpolicy-commands) - [Mapping endpoints to NetworkPolicies](#mapping-endpoints-to-networkpolicies) + - [Analyzing expected NetworkPolicies behavior](#analyzing-expected-networkpolicies-behavior) - [Dumping Pod network interface information](#dumping-pod-network-interface-information) - [Dumping OVS flows](#dumping-ovs-flows) - [OVS packet tracing](#ovs-packet-tracing) @@ -263,6 +264,21 @@ Namespace. This command only works in "controller mode" and **as of now it can only be run from inside the Antrea Controller Pod, and not from out-of-cluster**. +#### Analyzing expected NetworkPolicies behavior + +`antctl` supports analyzing all the existing Antrea Native NetworkPolicies and +Kubernetes NetworkPolicies to predict the effective policy rule, given two +specific Pods as source and destination. + +```bash +antctl query networkpolicyanalysis -S NAMESPACE/POD -D NAMESPACE/POD +``` + +If only Pod name is provided, the command will default to the "default" Namespace. + +This command only works in "controller mode" and **as of now it can only be run +from inside the Antrea Controller Pod, and not from out-of-cluster**. + ### Dumping Pod network interface information `antctl` agent command `get podinterface` (or `get pi`) can dump network diff --git a/pkg/antctl/antctl.go b/pkg/antctl/antctl.go index 2988c9b67f2..73f46156e5a 100644 --- a/pkg/antctl/antctl.go +++ b/pkg/antctl/antctl.go @@ -509,6 +509,34 @@ $ antctl get podmulticaststats pod -n namespace`, }, transformedResponse: reflect.TypeOf(controllernetworkpolicy.EndpointQueryResponse{}), }, + {use: "networkpolicyanalysis", + aliases: []string{"npanalysis"}, + short: "Filter network policy rules .", + long: "Filter network policies expected to be effective on the source and destination endpoints provided.", + example: ` Query network policies between two pods + $ antctl query networkpolicyanalysis -S ns1/pod1 -D ns2/pod2 +`, + commandGroup: query, + controllerEndpoint: &endpoint{ + nonResourceEndpoint: &nonResourceEndpoint{ + path: "/networkpolicyanalysis", + params: []flagInfo{ + { + name: "source", + usage: "Source endpoint of network policies. Can be a (local or remote) Pod (specified by /).", + shorthand: "S", + }, + { + name: "destination", + usage: "Source endpoint of network policies. Can be a (local or remote) Pod (specified by /).", + shorthand: "D", + }, + }, + outputType: single, + }, + }, + transformedResponse: reflect.TypeOf(controllernetworkpolicy.Rule{}), + }, { use: "flowrecords", short: "Print the matching flow records in the flow aggregator", diff --git a/pkg/antctl/command_definition.go b/pkg/antctl/command_definition.go index 1f6cc36665a..491dca22bba 100644 --- a/pkg/antctl/command_definition.go +++ b/pkg/antctl/command_definition.go @@ -497,6 +497,27 @@ func (cd *commandDefinition) tableOutputForQueryEndpoint(obj interface{}, writer return nil } +// tableOutputForQueryNetworkPolicyAnalysis implements printing rule as query result +func (cd *commandDefinition) tableOutputForQueryNetworkPolicyAnalysis(obj interface{}, writer io.Writer) error { + constructTable := func(header []string, body []string) error { + rows := [][]string{header, body} + numRows, numCol := len(rows), len(rows[0]) + widths := output.GetColumnWidths(numRows, numCol, rows) + if err := output.ConstructTable(numRows, numCol, widths, rows, writer); err != nil { + return err + } + return nil + } + queryResponse := obj.(*networkpolicy.Rule) + if queryResponse.Name != "" { + ruleStr := []string{queryResponse.Name, queryResponse.Namespace, strconv.Itoa(queryResponse.RuleIndex), string(queryResponse.UID), string(queryResponse.Direction)} + if err := constructTable([]string{"Name", "Namespace", "RuleIndex", "PolicyUID", "Direction"}, ruleStr); err != nil { + return err + } + } + return nil +} + // output reads bytes from the resp and outputs the data to the writer in desired // format. If the AddonTransform is set, it will use the function to transform // the data first. It will try to output the resp in the format ft specified after @@ -542,6 +563,9 @@ func (cd *commandDefinition) output(resp io.Reader, writer io.Writer, ft formatt if cd.controllerEndpoint.nonResourceEndpoint.path == "/endpoint" { return cd.tableOutputForQueryEndpoint(obj, writer) } + if cd.controllerEndpoint.nonResourceEndpoint.path == "/networkpolicyanalysis" { + return cd.tableOutputForQueryNetworkPolicyAnalysis(obj, writer) + } } else { return output.TableOutput(obj, writer) } diff --git a/pkg/antctl/command_definition_test.go b/pkg/antctl/command_definition_test.go index e4c74cdc13e..1f4acbce761 100644 --- a/pkg/antctl/command_definition_test.go +++ b/pkg/antctl/command_definition_test.go @@ -1008,6 +1008,41 @@ test-ingress-egress testNamespace 0 uid-1 } } +func TestTableOutputForQueryNetworkPolicyAnalysis(t *testing.T) { + policyRef0 := controllernetworkpolicy.PolicyRef{Namespace: "testNamespace", Name: "test-default-deny", UID: "uid-1"} + tc := []struct { + name string + rawResponseData interface{} + expected string + }{ + { + name: "No matching rule", + rawResponseData: &controllernetworkpolicy.Rule{}, + expected: ``, + }, + { + name: "Matched KNP default drop rule", + rawResponseData: &controllernetworkpolicy.Rule{ + PolicyRef: policyRef0, + Direction: cpv1beta.DirectionIn, + RuleIndex: -1, + }, + expected: `Name Namespace RuleIndex PolicyUID Direction +test-default-deny testNamespace -1 uid-1 In +`, + }, + } + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + cd := &commandDefinition{} + var outputBuf bytes.Buffer + err := cd.tableOutputForQueryNetworkPolicyAnalysis(tt.rawResponseData, &outputBuf) + assert.Nil(t, err) + assert.Equal(t, tt.expected, outputBuf.String()) + }) + } +} + func TestCollectFlags(t *testing.T) { tc := []struct { name string diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index f93a69aef96..3de181e9589 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -41,6 +41,7 @@ import ( "antrea.io/antrea/pkg/apiserver/handlers/endpoint" "antrea.io/antrea/pkg/apiserver/handlers/featuregates" "antrea.io/antrea/pkg/apiserver/handlers/loglevel" + "antrea.io/antrea/pkg/apiserver/handlers/networkpolicyanalysis" "antrea.io/antrea/pkg/apiserver/handlers/webhook" "antrea.io/antrea/pkg/apiserver/registry/controlplane/egressgroup" "antrea.io/antrea/pkg/apiserver/registry/controlplane/nodestatssummary" @@ -295,6 +296,7 @@ func installHandlers(c *ExtraConfig, s *genericapiserver.GenericAPIServer) { s.Handler.NonGoRestfulMux.HandleFunc("/loglevel", loglevel.HandleFunc()) s.Handler.NonGoRestfulMux.HandleFunc("/featuregates", featuregates.HandleFunc(c.k8sClient)) s.Handler.NonGoRestfulMux.HandleFunc("/endpoint", endpoint.HandleFunc(c.endpointQuerier)) + s.Handler.NonGoRestfulMux.HandleFunc("/networkpolicyanalysis", networkpolicyanalysis.HandleFunc(c.endpointQuerier)) // Webhook to mutate Namespace labels and add its metadata.name as a label s.Handler.NonGoRestfulMux.HandleFunc("/mutate/namespace", webhook.HandleMutationLabels()) if features.DefaultFeatureGate.Enabled(features.AntreaPolicy) { diff --git a/pkg/apiserver/handlers/networkpolicyanalysis/handler.go b/pkg/apiserver/handlers/networkpolicyanalysis/handler.go new file mode 100644 index 00000000000..ea62491bd2b --- /dev/null +++ b/pkg/apiserver/handlers/networkpolicyanalysis/handler.go @@ -0,0 +1,179 @@ +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package networkpolicyanalysis + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + + "antrea.io/antrea/pkg/apis/controlplane" + "antrea.io/antrea/pkg/apis/controlplane/v1beta2" + "antrea.io/antrea/pkg/controller/networkpolicy" +) + +// parsePeer parses Namespace/Pod name, empty string is returned if the argument is not of a +// valid Namespace/Pod reference (missing pod name or invalid format). Namespace will be set +// as default if missing, string without separator will be considered as pod name. +func parsePeer(str string) (string, string) { + parts := strings.Split(str, "/") + ns, pod := "", "" + if len(parts) == 1 { + ns, pod = "default", parts[0] + } else if len(parts) == 2 { + ns, pod = parts[0], parts[1] + } + return ns, pod +} + +// ruleKey returns policyUID+direction+ruleIndex as the key +func ruleKey(uid types.UID, direction v1beta2.Direction, idx int) string { + return fmt.Sprintf("%s/%s%d", uid, direction, idx) +} + +// higherPrecedence compares the two rules and returns if newRule has a higher precedence than oldRule. +func higherPrecedence(oldRule, newRule *networkpolicy.RuleInfo) bool { + if oldRule == nil { + return true + } + // compare tier priorities + effectiveTierPriorityK8sNP := (networkpolicy.DefaultTierPriority + networkpolicy.BaselineTierPriority) / 2 + oldTierPriority, newTierPriority := effectiveTierPriorityK8sNP, effectiveTierPriorityK8sNP + if oldRule.Policy.TierPriority != nil { + oldTierPriority = *oldRule.Policy.TierPriority + } + if newRule.Policy.TierPriority != nil { + newTierPriority = *newRule.Policy.TierPriority + } + if oldTierPriority != newTierPriority { + return oldTierPriority > newTierPriority + } + // compare policy priorities if tier priorities equal + if oldRule.Policy.Priority != nil && newRule.Policy.Priority != nil && *newRule.Policy.Priority != *oldRule.Policy.Priority { + return *oldRule.Policy.Priority > *newRule.Policy.Priority + } + // compare rule priorities further as previous steps have confirmed policy priorities are equal + // Kubernetes NetworkPolicies rules have the same default priorities, so rule index is hacked for comparison + // "-1" indicates default isolation, which has a lower precedence than KNP policy rules with ">=0" rule indexes + if oldRule.Index != newRule.Index { + if newRule.Policy.SourceRef.Type == controlplane.K8sNetworkPolicy && oldRule.Policy.SourceRef.Type == controlplane.K8sNetworkPolicy { + return oldRule.Index < newRule.Index + } else { + return oldRule.Index > newRule.Index + } + } + // use policy name as the tie-breaker if all priorities are equal + return oldRule.Policy.Name < newRule.Policy.Name +} + +// predictEndpointsRules returns the predicted rules effective from srcEndpoints to dstEndpoints. +// Rules returned satisfy a. in source applied policies and destination egress rules, +// or b. in source ingress rules and destination applied policies or c. applied to KNP default isolation. +func predictEndpointsRules(srcEndpoints, dstEndpoints *networkpolicy.EndpointRuleAnalysis) *networkpolicy.Rule { + var commonRule *networkpolicy.RuleInfo + dstEgressRules, srcIngressRules := sets.New[string](), sets.New[string]() + compareRules := func(commonRule, rule *networkpolicy.RuleInfo) *networkpolicy.RuleInfo { + if higherPrecedence(commonRule, rule) { + return rule + } + return commonRule + } + if srcEndpoints != nil && dstEndpoints != nil { + for _, rule := range dstEndpoints.EgressRules { + if srcEndpoints.PolicyUIDs.Has(rule.Policy.SourceRef.UID) { + dstEgressRules.Insert(ruleKey(rule.Policy.SourceRef.UID, rule.Direction, rule.Index)) + commonRule = compareRules(commonRule, rule) + } + } + for _, rule := range srcEndpoints.IngressRules { + if dstEndpoints.PolicyUIDs.Has(rule.Policy.SourceRef.UID) { + srcIngressRules.Insert(ruleKey(rule.Policy.SourceRef.UID, rule.Direction, rule.Index)) + commonRule = compareRules(commonRule, rule) + } + } + // Manually insert Kubernetes NetworkPolicy default isolation rules if exists. + // Default isolation rules has the index of -1 to indicate lower precedence. + for _, rule := range srcEndpoints.EgressIsolated { + if !dstEgressRules.Has(ruleKey(rule.Policy.SourceRef.UID, rule.Direction, rule.Index)) { + defaultDropRule := &networkpolicy.RuleInfo{ + Policy: rule.Policy, + Index: -1, + Direction: v1beta2.DirectionOut, + } + commonRule = compareRules(commonRule, defaultDropRule) + } + } + for _, rule := range dstEndpoints.IngressIsolated { + if !srcIngressRules.Has(ruleKey(rule.Policy.SourceRef.UID, rule.Direction, rule.Index)) { + defaultDropRule := &networkpolicy.RuleInfo{ + Policy: rule.Policy, + Index: -1, + Direction: v1beta2.DirectionIn, + } + commonRule = compareRules(commonRule, defaultDropRule) + } + } + } + // if no common rule or default isolation is found, return empty result + if commonRule == nil { + return &networkpolicy.Rule{} + } + return &networkpolicy.Rule{ + PolicyRef: networkpolicy.PolicyRef{ + Namespace: commonRule.Policy.SourceRef.Namespace, + Name: commonRule.Policy.SourceRef.Name, + UID: commonRule.Policy.SourceRef.UID, + }, + Direction: commonRule.Direction, + RuleIndex: commonRule.Index, + } +} + +// HandleFunc creates a http.HandlerFunc which uses an AgentNetworkPolicyInfoQuerier +// to query network policy rules in current agent. +func HandleFunc(eq networkpolicy.EndpointQuerier) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + src := r.URL.Query().Get("source") + dst := r.URL.Query().Get("destination") + + var srcNS, srcPod, dstNS, dstPod string + srcNS, srcPod = parsePeer(src) + dstNS, dstPod = parsePeer(dst) + if srcPod == "" || dstPod == "" { + http.Error(w, "invalid command argument format", http.StatusBadRequest) + return + } + + // query endpoints and handle response errors + endpointAnalysisSource, err := eq.QueryNetworkPolicyRules(srcNS, srcPod) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + endpointAnalysisDestination, err := eq.QueryNetworkPolicyRules(dstNS, dstPod) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + endpointAnalysisRule := predictEndpointsRules(endpointAnalysisSource, endpointAnalysisDestination) + if err := json.NewEncoder(w).Encode(endpointAnalysisRule); err != nil { + http.Error(w, "failed to encode response: "+err.Error(), http.StatusInternalServerError) + } + } +} diff --git a/pkg/apiserver/handlers/networkpolicyanalysis/handler_test.go b/pkg/apiserver/handlers/networkpolicyanalysis/handler_test.go new file mode 100644 index 00000000000..4b3919b64e2 --- /dev/null +++ b/pkg/apiserver/handlers/networkpolicyanalysis/handler_test.go @@ -0,0 +1,251 @@ +// Copyright 2023 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package networkpolicyanalysis + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + + "antrea.io/antrea/pkg/apis/controlplane" + "antrea.io/antrea/pkg/apis/controlplane/v1beta2" + "antrea.io/antrea/pkg/controller/networkpolicy" + queriermock "antrea.io/antrea/pkg/controller/networkpolicy/testing" + antreatypes "antrea.io/antrea/pkg/controller/types" +) + +type TestCase struct { + name string + handlerRequest string + argsMock []string + mockQueryResponse []mockResponse + + expectedStatus int + expectedResult *networkpolicy.Rule +} + +type mockResponse struct { + response *networkpolicy.EndpointRuleAnalysis + error error +} + +// TestIncompleteArguments tests how the handler function responds when the user +// passes in a query command with incomplete arguments (missing pod names or namespaces) +func TestIncompleteArguments(t *testing.T) { + mockCtrl := gomock.NewController(t) + testCases := []TestCase{ + { + name: "Invalid format", + handlerRequest: "?source=ns1/&destination=ns2/pod2/foo", + expectedStatus: http.StatusBadRequest, + }, + { + name: "Missing pod names", + handlerRequest: "?source=&destination=", + expectedStatus: http.StatusBadRequest, + }, + { + name: "Default namespaces", + handlerRequest: "?source=pod1&destination=/pod2", + argsMock: []string{"default", "pod1", "default", "pod2"}, + mockQueryResponse: []mockResponse{{response: nil, error: nil}, {response: nil, error: nil}}, + expectedStatus: http.StatusOK, + expectedResult: &networkpolicy.Rule{}, + }, + } + evaluateTestCases(testCases, mockCtrl, t) +} + +func TestNetworkPolicyAnalysis(t *testing.T) { + mockCtrl := gomock.NewController(t) + handlerRequest := "?source=ns/pod1&destination=ns/pod2" + var namespace = "ns" + argsMock := []string{namespace, "pod1", namespace, "pod2"} + uid1, uid2 := types.UID(fmt.Sprint(111)), types.UID(fmt.Sprint(222)) + priority1, priority2, defaultPriority, tierEmergency := float64(10), float64(15), float64(-1), int32(50) + + mockResponses := make([]networkpolicy.EndpointRuleAnalysis, 11) + generateRuleInfo := func(policyUID types.UID, policyType controlplane.NetworkPolicyType, direction controlplane.Direction, tierPriority *int32, policyPriority *float64, numMatches int) []*networkpolicy.RuleInfo { + rules := make([]controlplane.NetworkPolicyRule, numMatches) + for i := 0; i < numMatches; i++ { + rules[i] = controlplane.NetworkPolicyRule{ + Direction: direction, + Name: fmt.Sprintf("Policy%sRule%d", policyUID, i), + Priority: int32(i), + } + } + ruleInfoMatches := make([]*networkpolicy.RuleInfo, numMatches) + for i := 0; i < numMatches; i++ { + ruleInfoMatches[i] = &networkpolicy.RuleInfo{ + Policy: &antreatypes.NetworkPolicy{ + UID: policyUID, + Name: fmt.Sprintf("Policy%s", policyUID), + SourceRef: &controlplane.NetworkPolicyReference{Type: policyType, Namespace: namespace, Name: fmt.Sprintf("Policy%s", policyUID), UID: policyUID}, + Rules: rules, + TierPriority: tierPriority, + Priority: policyPriority, + }, + Index: i, + Direction: v1beta2.Direction(direction), + } + } + return ruleInfoMatches + } + generateResponse := func(podID int, policyUID types.UID, matchedRule, isolations []*networkpolicy.RuleInfo) networkpolicy.EndpointRuleAnalysis { + endpointRule := networkpolicy.EndpointRuleAnalysis{ + Namespace: namespace, + Name: fmt.Sprintf("pod%d", podID), + PolicyUIDs: sets.New[types.UID](policyUID), + } + if podID == 1 { + endpointRule.IngressRules = matchedRule + endpointRule.EgressIsolated = isolations + } else if podID == 2 { + endpointRule.EgressRules = matchedRule + endpointRule.IngressIsolated = isolations + } + return endpointRule + } + + // construct policy Tier comparison responses + mockResponses[0] = generateResponse(1, uid1, generateRuleInfo(uid2, controlplane.AntreaNetworkPolicy, controlplane.DirectionIn, &tierEmergency, nil, 1), nil) + mockResponses[1] = generateResponse(2, uid2, generateRuleInfo(uid1, controlplane.AntreaNetworkPolicy, controlplane.DirectionOut, &networkpolicy.DefaultTierPriority, nil, 1), nil) + // construct policy priority comparison responses + mockResponses[2] = generateResponse(1, uid1, generateRuleInfo(uid2, controlplane.AntreaNetworkPolicy, controlplane.DirectionIn, &networkpolicy.DefaultTierPriority, &priority2, 1), nil) + mockResponses[3] = generateResponse(2, uid2, generateRuleInfo(uid1, controlplane.AntreaNetworkPolicy, controlplane.DirectionOut, &networkpolicy.DefaultTierPriority, &priority1, 1), nil) + // construct policy rule priority comparison responses + mockResponses[4] = generateResponse(1, uid1, generateRuleInfo(uid2, controlplane.AntreaNetworkPolicy, controlplane.DirectionIn, &networkpolicy.DefaultTierPriority, &priority1, 2), nil) + mockResponses[5] = generateResponse(2, uid2, generateRuleInfo(uid1, controlplane.AntreaNetworkPolicy, controlplane.DirectionOut, &networkpolicy.DefaultTierPriority, &priority1, 0), nil) + // construct KNP and ANP comparison responses + mockResponses[6] = generateResponse(1, uid1, generateRuleInfo(uid2, controlplane.K8sNetworkPolicy, controlplane.DirectionIn, nil, &defaultPriority, 1), nil) + mockResponses[7] = generateResponse(2, uid2, generateRuleInfo(uid1, controlplane.AntreaNetworkPolicy, controlplane.DirectionOut, &networkpolicy.BaselineTierPriority, nil, 1), + mockResponses[6].IngressRules) + // construct KNP and KNP default isolation comparison responses + mockResponses[8] = generateResponse(1, uid1, nil, generateRuleInfo(uid1, controlplane.K8sNetworkPolicy, controlplane.DirectionOut, nil, &defaultPriority, 1)) + mockResponses[9] = generateResponse(2, uid2, mockResponses[8].EgressIsolated, nil) + // non policy effective response + mockResponses[10] = generateResponse(2, "", nil, nil) + + expectedRuleEgress := networkpolicy.Rule{ + PolicyRef: networkpolicy.PolicyRef{Namespace: namespace, Name: "Policy111", UID: uid1}, + Direction: v1beta2.DirectionOut, + RuleIndex: 0, + } + expectedRuleIngress := networkpolicy.Rule{ + PolicyRef: networkpolicy.PolicyRef{Namespace: namespace, Name: "Policy222", UID: uid2}, + Direction: v1beta2.DirectionIn, + RuleIndex: 0, + } + expectedRuleKNPIsolation := networkpolicy.Rule{ + PolicyRef: networkpolicy.PolicyRef{Namespace: namespace, Name: "Policy111", UID: uid1}, + Direction: v1beta2.DirectionOut, + RuleIndex: -1, + } + + testCases := []TestCase{ + { + name: "Different Tier priorities", + handlerRequest: handlerRequest, + argsMock: argsMock, + mockQueryResponse: []mockResponse{{response: &mockResponses[0]}, {response: &mockResponses[1]}}, + expectedStatus: http.StatusOK, + expectedResult: &expectedRuleIngress, + }, + { + name: "Different policy priorities", + handlerRequest: handlerRequest, + argsMock: argsMock, + mockQueryResponse: []mockResponse{{response: &mockResponses[2]}, {response: &mockResponses[3]}}, + expectedStatus: http.StatusOK, + expectedResult: &expectedRuleEgress, + }, + { + name: "Different rule priorities", + handlerRequest: handlerRequest, + argsMock: argsMock, + mockQueryResponse: []mockResponse{{response: &mockResponses[4]}, {response: &mockResponses[5]}}, + expectedStatus: http.StatusOK, + expectedResult: &expectedRuleIngress, + }, + { + name: "KNP and ANP", + handlerRequest: handlerRequest, + argsMock: argsMock, + mockQueryResponse: []mockResponse{{response: &mockResponses[6]}, {response: &mockResponses[7]}}, + expectedStatus: http.StatusOK, + expectedResult: &expectedRuleIngress, + }, + { + name: "KNP rule and default isolation", + handlerRequest: handlerRequest, + argsMock: argsMock, + mockQueryResponse: []mockResponse{{response: &mockResponses[8]}, {response: &mockResponses[9]}}, + expectedStatus: http.StatusOK, + expectedResult: &expectedRuleEgress, + }, + { + name: "KNP single default isolation", + handlerRequest: handlerRequest, + argsMock: argsMock, + mockQueryResponse: []mockResponse{{response: &mockResponses[8]}, {response: &mockResponses[10]}}, + expectedStatus: http.StatusOK, + expectedResult: &expectedRuleKNPIsolation, + }, + { + name: "Querier error", + handlerRequest: handlerRequest, + argsMock: argsMock, + mockQueryResponse: []mockResponse{{}, {error: errors.NewInternalError(fmt.Errorf("querier error"))}}, + expectedStatus: http.StatusInternalServerError, + }, + } + evaluateTestCases(testCases, mockCtrl, t) +} + +// evaluateTestCases executes the test cases by mocking QueryNetworkPolicyRules mock. It assumes that +// argsMock has at least 4 entries and mockQueryResponse has at least 2 entries if not expecting bad request. +func evaluateTestCases(testCases []TestCase, mockCtrl *gomock.Controller, t *testing.T) { + for _, tc := range testCases { + mockQuerier := queriermock.NewMockEndpointQuerier(mockCtrl) + if tc.expectedStatus != http.StatusBadRequest { + mockQuerier.EXPECT().QueryNetworkPolicyRules(tc.argsMock[0], tc.argsMock[1]).Return(tc.mockQueryResponse[0].response, tc.mockQueryResponse[0].error) + mockQuerier.EXPECT().QueryNetworkPolicyRules(tc.argsMock[2], tc.argsMock[3]).Return(tc.mockQueryResponse[1].response, tc.mockQueryResponse[1].error) + } + + handler := HandleFunc(mockQuerier) + req, err := http.NewRequest(http.MethodGet, tc.handlerRequest, nil) + assert.Nil(t, err) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + assert.Equal(t, tc.expectedStatus, recorder.Code) + if tc.expectedStatus != http.StatusOK { + return + } + + var received networkpolicy.Rule + err = json.Unmarshal(recorder.Body.Bytes(), &received) + assert.Nil(t, err) + assert.EqualValues(t, *tc.expectedResult, received) + } +} diff --git a/pkg/controller/networkpolicy/endpoint_querier.go b/pkg/controller/networkpolicy/endpoint_querier.go index 928e0ad5e93..437af434f29 100644 --- a/pkg/controller/networkpolicy/endpoint_querier.go +++ b/pkg/controller/networkpolicy/endpoint_querier.go @@ -19,9 +19,11 @@ package networkpolicy import ( "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" "antrea.io/antrea/pkg/apis/controlplane" cpv1beta "antrea.io/antrea/pkg/apis/controlplane/v1beta2" + "antrea.io/antrea/pkg/controller/grouping" "antrea.io/antrea/pkg/controller/networkpolicy/store" antreatypes "antrea.io/antrea/pkg/controller/types" ) @@ -32,6 +34,8 @@ type EndpointQuerier interface { // along with the list NetworkPolicies which select the provided Pod in one of their policy // rules (ingress or egress). QueryNetworkPolicies(namespace string, podName string) (*EndpointQueryResponse, error) + // QueryNetworkPolicyRules returns the detailed rules in addition to QueryNetworkPolicies. + QueryNetworkPolicyRules(namespace, podName string) (*EndpointRuleAnalysis, error) } // endpointQuerier implements the EndpointQuerier interface @@ -67,6 +71,22 @@ type Rule struct { RuleIndex int `json:"ruleindex,omitempty"` } +type RuleInfo struct { + Policy *antreatypes.NetworkPolicy + Index int + Direction cpv1beta.Direction +} + +type EndpointRuleAnalysis struct { + Namespace string + Name string + PolicyUIDs sets.Set[types.UID] + IngressIsolated []*RuleInfo + EgressIsolated []*RuleInfo + IngressRules []*RuleInfo + EgressRules []*RuleInfo +} + // NewEndpointQuerier returns a new *endpointQuerier. func NewEndpointQuerier(networkPolicyController *NetworkPolicyController) *endpointQuerier { n := &endpointQuerier{ @@ -75,24 +95,12 @@ func NewEndpointQuerier(networkPolicyController *NetworkPolicyController) *endpo return n } -// QueryNetworkPolicies returns kubernetes network policy references relevant to the selected -// network endpoint. Relevant policies fall into three categories: applied policies (Policies in -// Endpoint type) are policies which directly apply to an endpoint, egress and ingress rules (Rules -// in Endpoint type) are policies which reference the endpoint in an ingress/egress rule -// respectively. -func (eq *endpointQuerier) QueryNetworkPolicies(namespace string, podName string) (*EndpointQueryResponse, error) { - groups, exists := eq.networkPolicyController.groupingInterface.GetGroupsForPod(namespace, podName) - if !exists { - return nil, nil - } - type ruleTemp struct { - policy *antreatypes.NetworkPolicy - index int - } +// categorizeNetworkPolicies gets and categorizes related network policies based on groups. +func (eq *endpointQuerier) categorizeNetworkPolicies(groups map[grouping.GroupType][]string) ([]*antreatypes.NetworkPolicy, []*RuleInfo, []*RuleInfo, error) { // create network policies categories applied := make([]*antreatypes.NetworkPolicy, 0) - ingress := make([]*ruleTemp, 0) - egress := make([]*ruleTemp, 0) + ingress := make([]*RuleInfo, 0) + egress := make([]*RuleInfo, 0) // get all appliedToGroups using filter, then get applied policies using appliedToGroup appliedToGroupKeys := groups[appliedToGroupType] // We iterate over all AppliedToGroups (same for AddressGroups below). This is acceptable @@ -107,7 +115,7 @@ func (eq *endpointQuerier) QueryNetworkPolicies(namespace string, podName string appliedToGroupKey, ) if err != nil { - return nil, err + return nil, nil, nil, err } for _, policy := range policies { applied = append(applied, policy.(*antreatypes.NetworkPolicy)) @@ -125,21 +133,21 @@ func (eq *endpointQuerier) QueryNetworkPolicies(namespace string, podName string addressGroupKey, ) if err != nil { - return nil, err + return nil, nil, nil, err } for _, policy := range policies { egressIndex, ingressIndex := 0, 0 for _, rule := range policy.(*antreatypes.NetworkPolicy).Rules { for _, addressGroupTrial := range rule.To.AddressGroups { if addressGroupTrial == string(addressGroup.(*antreatypes.AddressGroup).UID) { - egress = append(egress, &ruleTemp{policy: policy.(*antreatypes.NetworkPolicy), index: egressIndex}) + egress = append(egress, &RuleInfo{Policy: policy.(*antreatypes.NetworkPolicy), Index: egressIndex, Direction: cpv1beta.DirectionOut}) // an AddressGroup can only be referenced in a rule once break } } for _, addressGroupTrial := range rule.From.AddressGroups { if addressGroupTrial == string(addressGroup.(*antreatypes.AddressGroup).UID) { - ingress = append(ingress, &ruleTemp{policy: policy.(*antreatypes.NetworkPolicy), index: ingressIndex}) + ingress = append(ingress, &RuleInfo{Policy: policy.(*antreatypes.NetworkPolicy), Index: ingressIndex, Direction: cpv1beta.DirectionIn}) // an AddressGroup can only be referenced in a rule once break } @@ -155,6 +163,24 @@ func (eq *endpointQuerier) QueryNetworkPolicies(namespace string, podName string } } } + return applied, ingress, egress, nil +} + +// QueryNetworkPolicies returns kubernetes network policy references relevant to the selected +// network endpoint. Relevant policies fall into three categories: applied policies (Policies in +// Endpoint type) are policies which directly apply to an endpoint, egress and ingress rules (Rules +// in Endpoint type) are policies which reference the endpoint in an ingress/egress rule +// respectively. +func (eq *endpointQuerier) QueryNetworkPolicies(namespace string, podName string) (*EndpointQueryResponse, error) { + groups, exists := eq.networkPolicyController.groupingInterface.GetGroupsForPod(namespace, podName) + if !exists { + return nil, nil + } + applied, ingress, egress, err := eq.categorizeNetworkPolicies(groups) + if err != nil { + return nil, err + } + // make response policies responsePolicies := make([]Policy, 0) for _, internalPolicy := range applied { @@ -169,27 +195,15 @@ func (eq *endpointQuerier) QueryNetworkPolicies(namespace string, podName string } responseRules := make([]Rule, 0) // create rules based on egress and ingress policies - for _, internalPolicy := range egress { + for _, internalPolicy := range append(egress, ingress...) { newRule := Rule{ PolicyRef: PolicyRef{ - Namespace: internalPolicy.policy.SourceRef.Namespace, - Name: internalPolicy.policy.SourceRef.Name, - UID: internalPolicy.policy.SourceRef.UID, + Namespace: internalPolicy.Policy.SourceRef.Namespace, + Name: internalPolicy.Policy.SourceRef.Name, + UID: internalPolicy.Policy.SourceRef.UID, }, - Direction: cpv1beta.DirectionOut, - RuleIndex: internalPolicy.index, - } - responseRules = append(responseRules, newRule) - } - for _, internalPolicy := range ingress { - newRule := Rule{ - PolicyRef: PolicyRef{ - Namespace: internalPolicy.policy.SourceRef.Namespace, - Name: internalPolicy.policy.SourceRef.Name, - UID: internalPolicy.policy.SourceRef.UID, - }, - Direction: cpv1beta.DirectionIn, - RuleIndex: internalPolicy.index, + Direction: internalPolicy.Direction, + RuleIndex: internalPolicy.Index, } responseRules = append(responseRules, newRule) } @@ -202,3 +216,38 @@ func (eq *endpointQuerier) QueryNetworkPolicies(namespace string, podName string } return &EndpointQueryResponse{[]Endpoint{endpoint}}, nil } + +// QueryNetworkPolicyRules returns network policies and rules relevant to the selected +// network endpoint. Policies which directly apply to an endpoint are returned with UIDs. +// Ingress and egress rules which reference the endpoint are returned respectively with +// policy UID. Kubernetes NetworkPolicies that create isolation are listed separately. +func (eq *endpointQuerier) QueryNetworkPolicyRules(namespace, podName string) (*EndpointRuleAnalysis, error) { + groups, exists := eq.networkPolicyController.groupingInterface.GetGroupsForPod(namespace, podName) + if !exists { + return nil, nil + } + applied, ingress, egress, err := eq.categorizeNetworkPolicies(groups) + if err != nil { + return nil, err + } + + policyUIDs := sets.New[types.UID]() + ingressIsolation, egressIsolation := make([]*RuleInfo, 0), make([]*RuleInfo, 0) + for _, internalPolicy := range applied { + policyUIDs.Insert(internalPolicy.SourceRef.UID) + if internalPolicy.SourceRef.Type == controlplane.K8sNetworkPolicy { + // check if the Kubernetes NetworkPolicy creates ingress or egress isolation + egressIndex, ingressIndex := 0, 0 + for _, rule := range internalPolicy.Rules { + if rule.Direction == controlplane.DirectionIn { + ingressIsolation = append(ingressIsolation, &RuleInfo{Policy: internalPolicy, Index: ingressIndex, Direction: cpv1beta.DirectionIn}) + ingressIndex++ + } else if rule.Direction == controlplane.DirectionOut { + egressIsolation = append(egressIsolation, &RuleInfo{Policy: internalPolicy, Index: egressIndex, Direction: cpv1beta.DirectionOut}) + egressIndex++ + } + } + } + } + return &EndpointRuleAnalysis{namespace, podName, policyUIDs, ingressIsolation, egressIsolation, ingress, egress}, nil +} diff --git a/pkg/controller/networkpolicy/endpoint_querier_test.go b/pkg/controller/networkpolicy/endpoint_querier_test.go index c9a0a0a80dc..d7aa7740c89 100644 --- a/pkg/controller/networkpolicy/endpoint_querier_test.go +++ b/pkg/controller/networkpolicy/endpoint_querier_test.go @@ -25,8 +25,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + "antrea.io/antrea/pkg/apis/controlplane" "antrea.io/antrea/pkg/apis/controlplane/v1beta2" + antreatypes "antrea.io/antrea/pkg/controller/types" ) // pods represent kubernetes pods for testing proper query results @@ -212,7 +215,7 @@ func makeControllerAndEndpointQuerier(objects ...runtime.Object) *endpointQuerie return querier } -func TestEndpointQuery(t *testing.T) { +func TestQueryNetworkPolicies(t *testing.T) { policyRef0 := PolicyRef{policies[0].Namespace, policies[0].Name, policies[0].UID} policyRef1 := PolicyRef{policies[1].Namespace, policies[1].Name, policies[1].UID} policyRef2 := PolicyRef{policies[2].Namespace, policies[2].Name, policies[2].UID} @@ -327,3 +330,84 @@ func TestEndpointQuery(t *testing.T) { }) } } + +func TestQueryNetworkPolicyRules(t *testing.T) { + policyRef := controlplane.NetworkPolicyReference{Type: controlplane.K8sNetworkPolicy, Namespace: policies[0].Namespace, Name: policies[0].Name, UID: policies[0].UID} + ns, podA := "testNamespace", "podA" + + testCases := []struct { + name string + objs []runtime.Object + podNamespace string + podName string + expectedResponse *EndpointRuleAnalysis + }{ + { + "No matching pod", + []runtime.Object{}, + "non-existing-namespace", + "non-existing-pod", + nil, + }, + { + "Empty response", + []runtime.Object{namespaces[0], pods[0]}, + ns, + podA, + &EndpointRuleAnalysis{Namespace: ns, Name: podA, PolicyUIDs: sets.New[types.UID]()}, + }, + { + name: "KNP ingress and egress policy applied", + objs: []runtime.Object{namespaces[0], pods[0], policies[0]}, + podNamespace: "testNamespace", + podName: podA, + expectedResponse: &EndpointRuleAnalysis{ + Namespace: ns, + Name: podA, + PolicyUIDs: sets.New[types.UID](policies[0].UID), + IngressRules: []*RuleInfo{ + {&antreatypes.NetworkPolicy{SourceRef: &policyRef}, 0, v1beta2.DirectionIn}, + }, + EgressRules: []*RuleInfo{ + {&antreatypes.NetworkPolicy{SourceRef: &policyRef}, 0, v1beta2.DirectionOut}, + }, + IngressIsolated: []*RuleInfo{ + {&antreatypes.NetworkPolicy{SourceRef: &policyRef}, 0, v1beta2.DirectionIn}, + }, + EgressIsolated: []*RuleInfo{ + {&antreatypes.NetworkPolicy{SourceRef: &policyRef}, 0, v1beta2.DirectionOut}, + }, + }, + }, + } + + evaluateResponse := func(expectedRules, responseRules []*RuleInfo) { + assert.Equal(t, len(expectedRules), len(responseRules)) + for idx := range expectedRules { + assert.Equal(t, expectedRules[idx].Index, responseRules[idx].Index) + assert.Equal(t, expectedRules[idx].Policy.SourceRef, responseRules[idx].Policy.SourceRef) + } + return + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + endpointQuerier := makeControllerAndEndpointQuerier(tc.objs...) + response, err := endpointQuerier.QueryNetworkPolicyRules(tc.podNamespace, tc.podName) + require.NoErrorf(t, err, "Expected QueryNetworkPolicies to succeed") + if tc.expectedResponse == nil { + assert.Nil(t, response, "Expected nil response from QueryNetworkPolicyRules") + } else { + assert.Equal(t, tc.expectedResponse.Namespace, response.Namespace) + assert.Equal(t, tc.expectedResponse.Name, response.Name) + assert.Equal(t, tc.expectedResponse.PolicyUIDs, response.PolicyUIDs) + evaluateResponse(tc.expectedResponse.IngressRules, response.IngressRules) + evaluateResponse(tc.expectedResponse.EgressRules, response.EgressRules) + evaluateResponse(tc.expectedResponse.IngressIsolated, response.IngressIsolated) + evaluateResponse(tc.expectedResponse.EgressIsolated, response.EgressIsolated) + } + }) + } +} diff --git a/pkg/controller/networkpolicy/testing/mock_networkpolicy.go b/pkg/controller/networkpolicy/testing/mock_networkpolicy.go index ffd00ee9273..26684c42404 100644 --- a/pkg/controller/networkpolicy/testing/mock_networkpolicy.go +++ b/pkg/controller/networkpolicy/testing/mock_networkpolicy.go @@ -67,3 +67,18 @@ func (mr *MockEndpointQuerierMockRecorder) QueryNetworkPolicies(arg0, arg1 any) mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryNetworkPolicies", reflect.TypeOf((*MockEndpointQuerier)(nil).QueryNetworkPolicies), arg0, arg1) } + +// QueryNetworkPolicyRules mocks base method. +func (m *MockEndpointQuerier) QueryNetworkPolicyRules(arg0, arg1 string) (*networkpolicy.EndpointRuleAnalysis, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "QueryNetworkPolicyRules", arg0, arg1) + ret0, _ := ret[0].(*networkpolicy.EndpointRuleAnalysis) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// QueryNetworkPolicyRules indicates an expected call of QueryNetworkPolicyRules. +func (mr *MockEndpointQuerierMockRecorder) QueryNetworkPolicyRules(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryNetworkPolicyRules", reflect.TypeOf((*MockEndpointQuerier)(nil).QueryNetworkPolicyRules), arg0, arg1) +}