diff --git a/docs/antctl.md b/docs/antctl.md index 82f4722e33b..27f15f8b1c3 100644 --- a/docs/antctl.md +++ b/docs/antctl.md @@ -781,6 +781,34 @@ PEER ASN STATE 192.168.77.201:179 65002 Active ``` +`antctl` agent command `get bgproutes` prints the advertised BGP routes on the local Node. +For more information about route advertisement, please refer to [Advertisements](./bgp-policy.md#advertisements). + +```bash +# Get the list of all advertised bgp routes +$ antctl get bgproutes + +ROUTE +10.96.10.10/32 +192.168.77.100/32 +fec0::10:96:10:10/128 +fec0::192:168:77:100/128 + +# Get the list of advertised IPv4 bgp routes +$ antctl get bgproutes --ipv4-only + +ROUTE +10.96.10.10/32 +192.168.77.100/32 + +# Get the list of advertised IPv6 bgp routes +$ antctl get bgproutes --ipv6-only + +ROUTE +fec0::10:96:10:10/128 +fec0::192:168:77:100/128 +``` + ### Upgrade existing objects of CRDs antctl supports upgrading existing objects of Antrea CRDs to the storage version. diff --git a/pkg/agent/apis/types.go b/pkg/agent/apis/types.go index 3aebbe0f663..1b00c9d80b8 100644 --- a/pkg/agent/apis/types.go +++ b/pkg/agent/apis/types.go @@ -229,3 +229,20 @@ func (r BGPPeerResponse) GetTableRow(_ int) []string { func (r BGPPeerResponse) SortRows() bool { return true } + +// BGPRouteResponse describes the response struct of bgproutes command. +type BGPRouteResponse struct { + Route string `json:"route,omitempty"` +} + +func (r BGPRouteResponse) GetTableHeader() []string { + return []string{"ROUTE"} +} + +func (r BGPRouteResponse) GetTableRow(_ int) []string { + return []string{r.Route} +} + +func (r BGPRouteResponse) SortRows() bool { + return true +} diff --git a/pkg/agent/apiserver/apiserver.go b/pkg/agent/apiserver/apiserver.go index 71c78cf6ee1..aaf5e25e9d6 100644 --- a/pkg/agent/apiserver/apiserver.go +++ b/pkg/agent/apiserver/apiserver.go @@ -38,6 +38,7 @@ import ( "antrea.io/antrea/pkg/agent/apiserver/handlers/appliedtogroup" "antrea.io/antrea/pkg/agent/apiserver/handlers/bgppeer" "antrea.io/antrea/pkg/agent/apiserver/handlers/bgppolicy" + "antrea.io/antrea/pkg/agent/apiserver/handlers/bgproute" "antrea.io/antrea/pkg/agent/apiserver/handlers/featuregates" "antrea.io/antrea/pkg/agent/apiserver/handlers/memberlist" "antrea.io/antrea/pkg/agent/apiserver/handlers/multicast" @@ -102,6 +103,7 @@ func installHandlers(aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolic s.Handler.NonGoRestfulMux.HandleFunc("/memberlist", memberlist.HandleFunc(aq)) s.Handler.NonGoRestfulMux.HandleFunc("/bgppolicy", bgppolicy.HandleFunc(bgpq)) s.Handler.NonGoRestfulMux.HandleFunc("/bgppeers", bgppeer.HandleFunc(bgpq)) + s.Handler.NonGoRestfulMux.HandleFunc("/bgproutes", bgproute.HandleFunc(bgpq)) } func installAPIGroup(s *genericapiserver.GenericAPIServer, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, v4Enabled, v6Enabled bool) error { diff --git a/pkg/agent/apiserver/handlers/bgproute/handler.go b/pkg/agent/apiserver/handlers/bgproute/handler.go new file mode 100644 index 00000000000..fbdd8006bcb --- /dev/null +++ b/pkg/agent/apiserver/handlers/bgproute/handler.go @@ -0,0 +1,82 @@ +// Copyright 2024 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 bgproute + +import ( + "encoding/json" + "errors" + "net/http" + "reflect" + + "k8s.io/klog/v2" + + "antrea.io/antrea/pkg/agent/apis" + "antrea.io/antrea/pkg/agent/controller/bgp" + "antrea.io/antrea/pkg/querier" +) + +// HandleFunc returns the function which can handle queries issued by the bgproutes command. +func HandleFunc(bq querier.AgentBGPPolicyInfoQuerier) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if bq == nil || reflect.ValueOf(bq).IsNil() { + // The error message must match the "FOO is not enabled" pattern to pass antctl e2e tests. + http.Error(w, "bgp is not enabled", http.StatusServiceUnavailable) + return + } + + values := r.URL.Query() + var ipv4Only, ipv6Only bool + if values.Has("ipv4-only") { + if values.Get("ipv4-only") != "" { + http.Error(w, "invalid query", http.StatusBadRequest) + return + } + ipv4Only = true + } + if values.Has("ipv6-only") { + if values.Get("ipv6-only") != "" { + http.Error(w, "invalid query", http.StatusBadRequest) + return + } + ipv6Only = true + } + if ipv4Only && ipv6Only { + http.Error(w, "invalid query", http.StatusBadRequest) + return + } + + bgpRoutes, err := bq.GetBGPRoutes(r.Context(), !ipv6Only, !ipv4Only) + if err != nil { + if errors.Is(err, bgp.ErrBGPPolicyNotFound) { + http.Error(w, "there is no effective bgp policy applied to the Node", http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var bgpRoutesResp []apis.BGPRouteResponse + for _, bgpRoute := range bgpRoutes { + bgpRoutesResp = append(bgpRoutesResp, apis.BGPRouteResponse{ + Route: bgpRoute, + }) + } + + if err := json.NewEncoder(w).Encode(bgpRoutesResp); err != nil { + w.WriteHeader(http.StatusInternalServerError) + klog.ErrorS(err, "Error when encoding BGPRoutesResp to json") + } + } +} diff --git a/pkg/agent/apiserver/handlers/bgproute/handler_test.go b/pkg/agent/apiserver/handlers/bgproute/handler_test.go new file mode 100644 index 00000000000..69333f9e3df --- /dev/null +++ b/pkg/agent/apiserver/handlers/bgproute/handler_test.go @@ -0,0 +1,138 @@ +// Copyright 2024 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 bgproute + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "antrea.io/antrea/pkg/agent/apis" + "antrea.io/antrea/pkg/agent/controller/bgp" + queriertest "antrea.io/antrea/pkg/querier/testing" +) + +func TestBGPRouteQuery(t *testing.T) { + ctx := context.Background() + tests := []struct { + name string + url string + expectedCalls func(mockBGPServer *queriertest.MockAgentBGPPolicyInfoQuerier) + expectedStatus int + expectedResponse []apis.BGPRouteResponse + }{ + { + name: "bgpPolicyState does not exist", + expectedCalls: func(mockBGPServer *queriertest.MockAgentBGPPolicyInfoQuerier) { + mockBGPServer.EXPECT().GetBGPRoutes(context.Background(), true, true).Return(nil, bgp.ErrBGPPolicyNotFound) + }, + expectedStatus: http.StatusNotFound, + }, + { + name: "get all advertised routes", + expectedCalls: func(mockBGPServer *queriertest.MockAgentBGPPolicyInfoQuerier) { + mockBGPServer.EXPECT().GetBGPRoutes(ctx, true, true).Return( + []string{"192.168.1.0/24", "192.168.2.0/24", "fec0::10:96:10:10/128"}, nil) + }, + expectedStatus: http.StatusOK, + expectedResponse: []apis.BGPRouteResponse{ + { + Route: "192.168.1.0/24", + }, + { + Route: "192.168.2.0/24", + }, + { + Route: "fec0::10:96:10:10/128", + }, + }, + }, + { + name: "get advertised ipv4 routes only", + url: "?ipv4-only", + expectedCalls: func(mockBGPServer *queriertest.MockAgentBGPPolicyInfoQuerier) { + mockBGPServer.EXPECT().GetBGPRoutes(ctx, true, false).Return( + []string{"192.168.1.0/24", "192.168.2.0/24"}, nil) + }, + expectedStatus: http.StatusOK, + expectedResponse: []apis.BGPRouteResponse{ + { + Route: "192.168.1.0/24", + }, + { + Route: "192.168.2.0/24", + }, + }, + }, + { + name: "get advertised ipv6 routes only", + url: "?ipv6-only=", + expectedCalls: func(mockBGPServer *queriertest.MockAgentBGPPolicyInfoQuerier) { + mockBGPServer.EXPECT().GetBGPRoutes(ctx, false, true).Return( + []string{"fec0::192:168:77:150/128", "fec0::10:10:0:10/128"}, nil) + }, + expectedStatus: http.StatusOK, + expectedResponse: []apis.BGPRouteResponse{ + { + Route: "fec0::192:168:77:150/128", + }, + { + Route: "fec0::10:10:0:10/128", + }, + }, + }, + { + name: "flag with value", + url: "?ipv4-only=true", + expectedStatus: http.StatusBadRequest, + }, + { + name: "both flags are passed", + url: "?ipv4-only&ipv6-only", + expectedStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + q := queriertest.NewMockAgentBGPPolicyInfoQuerier(ctrl) + if tt.expectedCalls != nil { + tt.expectedCalls(q) + } + handler := HandleFunc(q) + + req, err := http.NewRequest(http.MethodGet, tt.url, nil) + require.NoError(t, err) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + assert.Equal(t, tt.expectedStatus, recorder.Code) + + if tt.expectedStatus == http.StatusOK { + var received []apis.BGPRouteResponse + err = json.Unmarshal(recorder.Body.Bytes(), &received) + require.NoError(t, err) + assert.Equal(t, tt.expectedResponse, received) + } + }) + } +} diff --git a/pkg/agent/controller/bgp/controller.go b/pkg/agent/controller/bgp/controller.go index 888cb4a77ba..4fb8ea2c3fe 100644 --- a/pkg/agent/controller/bgp/controller.go +++ b/pkg/agent/controller/bgp/controller.go @@ -991,3 +991,37 @@ func (c *Controller) GetBGPPeerStatus(ctx context.Context) ([]bgp.PeerStatus, er } return peers, nil } + +// GetBGPRoutes returns the advertised BGP routes. +func (c *Controller) GetBGPRoutes(ctx context.Context, ipv4Routes, ipv6Routes bool) ([]string, error) { + getBgpRoutesAdvertised := func() sets.Set[bgp.Route] { + c.bgpPolicyStateMutex.RLock() + defer c.bgpPolicyStateMutex.RUnlock() + if c.bgpPolicyState == nil { + return nil + } + return c.bgpPolicyState.routes + } + + bgpRoutesAdvertised := getBgpRoutesAdvertised() + if bgpRoutesAdvertised == nil { + return nil, ErrBGPPolicyNotFound + } + + bgpRoutes := make([]string, 0, bgpRoutesAdvertised.Len()) + if ipv4Routes { // insert IPv4 advertised routes + for route := range bgpRoutesAdvertised { + if utilnet.IsIPv4CIDRString(route.Prefix) { + bgpRoutes = append(bgpRoutes, route.Prefix) + } + } + } + if ipv6Routes { // insert IPv6 advertised routes + for route := range bgpRoutesAdvertised { + if utilnet.IsIPv6CIDRString(route.Prefix) { + bgpRoutes = append(bgpRoutes, route.Prefix) + } + } + } + return bgpRoutes, nil +} diff --git a/pkg/agent/controller/bgp/controller_test.go b/pkg/agent/controller/bgp/controller_test.go index 60a9eb91d37..4e555e74c22 100644 --- a/pkg/agent/controller/bgp/controller_test.go +++ b/pkg/agent/controller/bgp/controller_test.go @@ -2230,3 +2230,84 @@ func TestGetBGPPeerStatus(t *testing.T) { }) } } + +func TestGetBGPRoutes(t *testing.T) { + effectivePolicyState := generateBGPPolicyState(bgpPolicyName1, + 179, + 65000, + nodeAnnotations1[types.NodeBGPRouterIDAnnotationKey], + []string{ipStrToPrefix(clusterIPv4), + ipStrToPrefix(loadBalancerIPv4), + podIPv4CIDR.String(), + ipStrToPrefix(clusterIPv6), + ipStrToPrefix(loadBalancerIPv6), + podIPv6CIDR.String(), + }, + []bgp.PeerConfig{}, + ) + ctx := context.Background() + testCases := []struct { + name string + ipv4Routes bool + ipv6Routes bool + existingState *bgpPolicyState + expectedBgpRoutes []string + expectedErr string + }{ + { + name: "get all advertised routes", + ipv4Routes: true, + ipv6Routes: true, + existingState: effectivePolicyState, + expectedBgpRoutes: []string{ + ipStrToPrefix(clusterIPv4), + ipStrToPrefix(loadBalancerIPv4), + podIPv4CIDR.String(), + ipStrToPrefix(clusterIPv6), + ipStrToPrefix(loadBalancerIPv6), + podIPv6CIDR.String(), + }, + }, + { + name: "get advertised ipv4 routes only", + ipv4Routes: true, + existingState: effectivePolicyState, + expectedBgpRoutes: []string{ + ipStrToPrefix(clusterIPv4), + ipStrToPrefix(loadBalancerIPv4), + podIPv4CIDR.String(), + }, + }, + { + name: "get advertised ipv6 routes only", + ipv6Routes: true, + existingState: effectivePolicyState, + expectedBgpRoutes: []string{ + ipStrToPrefix(clusterIPv6), + ipStrToPrefix(loadBalancerIPv6), + podIPv6CIDR.String(), + }, + }, + { + name: "bgpPolicyState does not exist", + ipv4Routes: true, + expectedErr: ErrBGPPolicyNotFound.Error(), + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + c := newFakeController(t, nil, nil, tt.ipv4Routes, tt.ipv6Routes) + + // Fake the BGPPolicy state. + c.bgpPolicyState = tt.existingState + + actualBgpRoutes, err := c.GetBGPRoutes(ctx, tt.ipv4Routes, tt.ipv6Routes) + if tt.expectedErr != "" { + assert.EqualError(t, err, tt.expectedErr) + } else { + require.NoError(t, err) + assert.ElementsMatch(t, tt.expectedBgpRoutes, actualBgpRoutes) + } + }) + } +} diff --git a/pkg/antctl/antctl.go b/pkg/antctl/antctl.go index 33aee26b692..3a180cf1919 100644 --- a/pkg/antctl/antctl.go +++ b/pkg/antctl/antctl.go @@ -669,6 +669,39 @@ $ antctl get podmulticaststats pod -n namespace`, commandGroup: get, transformedResponse: reflect.TypeOf(agentapis.BGPPeerResponse{}), }, + { + use: "bgproutes", + aliases: []string{"bgproute"}, + short: "Print the advertised bgp routes.", + long: "Print the advertised bgp routes.", + example: ` Get the list of all advertised bgp routes + $ antctl get bgproutes + Get the list of advertised IPv4 bgp routes + $ antctl get bgproutes --ipv4-only + Get the list of advertised IPv6 bgp routes + $ antctl get bgproutes --ipv6-only +`, + agentEndpoint: &endpoint{ + nonResourceEndpoint: &nonResourceEndpoint{ + path: "/bgproutes", + params: []flagInfo{ + { + name: "ipv4-only", + usage: "Get advertised IPv4 bgp routes only", + isBool: true, + }, + { + name: "ipv6-only", + usage: "Get advertised IPv6 bgp routes only", + isBool: true, + }, + }, + outputType: multiple, + }, + }, + commandGroup: get, + transformedResponse: reflect.TypeOf(agentapis.BGPRouteResponse{}), + }, }, rawCommands: []rawCommand{ { diff --git a/pkg/antctl/command_list_test.go b/pkg/antctl/command_list_test.go index 4e875055ec2..509d9c8701d 100644 --- a/pkg/antctl/command_list_test.go +++ b/pkg/antctl/command_list_test.go @@ -70,7 +70,7 @@ func TestGetDebugCommands(t *testing.T) { { name: "Antctl running against agent mode", mode: "agent", - expected: [][]string{{"version"}, {"get", "podmulticaststats"}, {"log-level"}, {"get", "networkpolicy"}, {"get", "appliedtogroup"}, {"get", "addressgroup"}, {"get", "agentinfo"}, {"get", "podinterface"}, {"get", "ovsflows"}, {"trace-packet"}, {"get", "serviceexternalip"}, {"get", "memberlist"}, {"get", "bgppolicy"}, {"get", "bgppeers"}, {"supportbundle"}, {"traceflow"}, {"get", "featuregates"}}, + expected: [][]string{{"version"}, {"get", "podmulticaststats"}, {"log-level"}, {"get", "networkpolicy"}, {"get", "appliedtogroup"}, {"get", "addressgroup"}, {"get", "agentinfo"}, {"get", "podinterface"}, {"get", "ovsflows"}, {"trace-packet"}, {"get", "serviceexternalip"}, {"get", "memberlist"}, {"get", "bgppolicy"}, {"get", "bgppeers"}, {"get", "bgproutes"}, {"supportbundle"}, {"traceflow"}, {"get", "featuregates"}}, }, { name: "Antctl running against flow-aggregator mode", diff --git a/pkg/querier/querier.go b/pkg/querier/querier.go index f64fdc924fb..9db11e3bc68 100644 --- a/pkg/querier/querier.go +++ b/pkg/querier/querier.go @@ -147,4 +147,6 @@ type AgentBGPPolicyInfoQuerier interface { GetBGPPolicyInfo() (string, string, int32, int32) // GetBGPPeerStatus returns current status of all BGP Peers of effective BGP Policy applied on the Node. GetBGPPeerStatus(ctx context.Context) ([]bgp.PeerStatus, error) + // GetBGPRoutes returns the advertised BGP routes. + GetBGPRoutes(ctx context.Context, ipv4Routes, ipv6Routes bool) ([]string, error) } diff --git a/pkg/querier/testing/mock_querier.go b/pkg/querier/testing/mock_querier.go index d8ef0e8d5dc..e197814ce6a 100644 --- a/pkg/querier/testing/mock_querier.go +++ b/pkg/querier/testing/mock_querier.go @@ -394,3 +394,18 @@ func (mr *MockAgentBGPPolicyInfoQuerierMockRecorder) GetBGPPolicyInfo() *gomock. mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBGPPolicyInfo", reflect.TypeOf((*MockAgentBGPPolicyInfoQuerier)(nil).GetBGPPolicyInfo)) } + +// GetBGPRoutes mocks base method. +func (m *MockAgentBGPPolicyInfoQuerier) GetBGPRoutes(ctx context.Context, ipv4Routes, ipv6Routes bool) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBGPRoutes", ctx, ipv4Routes, ipv6Routes) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBGPRoutes indicates an expected call of GetBGPRoutes. +func (mr *MockAgentBGPPolicyInfoQuerierMockRecorder) GetBGPRoutes(ctx, ipv4Routes, ipv6Routes any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBGPRoutes", reflect.TypeOf((*MockAgentBGPPolicyInfoQuerier)(nil).GetBGPRoutes), ctx, ipv4Routes, ipv6Routes) +}