From 187eff5601c2fddaf723116a6a46d327e5277950 Mon Sep 17 00:00:00 2001 From: Kumar Atish Date: Tue, 7 Feb 2023 15:26:05 +0530 Subject: [PATCH] Add CLI command to get memberlist state Add antrea agent command `antctl get memberlist` to get state of memberlist cluster of antrea agent. Fixes #4601 Signed-off-by: Kumar Atish --- cmd/antrea-agent/agent.go | 2 + docs/antctl.md | 14 ++ docs/support-bundle-guide.md | 3 +- hack/update-codegen-dockerized.sh | 2 +- pkg/agent/apiserver/apiserver.go | 2 + .../apiserver/handlers/memberlist/handler.go | 76 +++++++ .../handlers/memberlist/handler_test.go | 107 ++++++++++ pkg/agent/memberlist/cluster_test.go | 7 +- pkg/agent/memberlist/mock_memberlist.go | 197 ++++++++++++++++++ .../memberlist/testing/mock_memberlist.go | 107 ---------- pkg/agent/querier/querier.go | 20 ++ pkg/agent/querier/testing/mock_querier.go | 32 ++- .../support_bundle_controller_test.go | 5 + pkg/antctl/antctl.go | 15 ++ pkg/antctl/command_definition_test.go | 20 ++ pkg/antctl/command_list_test.go | 2 +- .../registry/system/supportbundle/rest.go | 1 + .../system/supportbundle/rest_test.go | 4 + pkg/monitor/agent_test.go | 2 +- pkg/support/dump.go | 7 + 20 files changed, 509 insertions(+), 116 deletions(-) create mode 100644 pkg/agent/apiserver/handlers/memberlist/handler.go create mode 100644 pkg/agent/apiserver/handlers/memberlist/handler_test.go create mode 100644 pkg/agent/memberlist/mock_memberlist.go delete mode 100644 pkg/agent/memberlist/testing/mock_memberlist.go diff --git a/cmd/antrea-agent/agent.go b/cmd/antrea-agent/agent.go index 3cd581fc83f..13460461d6e 100644 --- a/cmd/antrea-agent/agent.go +++ b/cmd/antrea-agent/agent.go @@ -778,6 +778,8 @@ func run(o *Options) error { networkPolicyController, o.config.APIPort, o.config.NodePortLocal.PortRange, + memberlistCluster, + nodeInformer, ) agentMonitor := monitor.NewAgentMonitor(crdClient, agentQuerier) diff --git a/docs/antctl.md b/docs/antctl.md index 1637a29c625..d4b1f077c61 100644 --- a/docs/antctl.md +++ b/docs/antctl.md @@ -35,6 +35,7 @@ running in three different modes: - [Record metrics](#record-metrics) - [Multi-cluster commands](#multi-cluster-commands) - [Multicast commands](#multicast-commands) + - [Showing memberlist state](#showing-memberlist-state) ## Installation @@ -652,3 +653,16 @@ NAMESPACE NAME INBOUND OUTBOUND testmulticast-vw7gx5b9 test3-receiver-2 30 0 testmulticast-vw7gx5b9 test3-sender-1 0 10 ``` + +### Showing memberlist state + +`antctl` agent command `get memberlist` (or `get ml`) print the state of memberlist cluster of Antrea Agent. + +```bash +$ antctl get memberlist + +NODE IP STATUS +worker1 172.18.0.4 Alive +worker2 172.18.0.3 Alive +worker3 172.18.0.2 Dead +``` diff --git a/docs/support-bundle-guide.md b/docs/support-bundle-guide.md index 8ff499f59c8..8257bf82dfc 100644 --- a/docs/support-bundle-guide.md +++ b/docs/support-bundle-guide.md @@ -248,7 +248,8 @@ CR for external Nodes". | IP Address Info | `agent`, `outside`, `Node`, `ExternalNode` | Output of `ip address` command on Linux or `ipconfig /all` command on Windows | | IP Route Info | `agent`, `outside`, `Node`, `ExternalNode` | Output of `ip route` on Linux or `route print` on Windows | | IP Link Info | `agent`, `outside`, `Node`, `ExternalNode` | Output of `ip link` on Linux or `Get-NetAdapter` on Windows | -| Cluster Information | `outside` | Dump of resources in the cluster, including: 1. all Pods, Deployments, Replicasets and Daemonsets in all Namespaces with any resourceVersion. 2. all Nodes with any resourceVersion. 3. all ConfigMaps in all Namespaces with any resourceVersion and label `app=antrea`. | +| Cluster Information | `outside` | Dump of resources in the cluster, including: 1. all Pods, Deployments, Replicasets and Daemonsets in all Namespaces with any resourceVersion. 2. all Nodes with any resourceVersion. 3. all ConfigMaps in all Namespaces with any resourceVersion and label `app=antrea`. | +| Memberlist State | `agent`, `outside` | YAML output of `antctl get memberlist` | ## Limitations diff --git a/hack/update-codegen-dockerized.sh b/hack/update-codegen-dockerized.sh index d4b0b72f40c..1915f402a2f 100755 --- a/hack/update-codegen-dockerized.sh +++ b/hack/update-codegen-dockerized.sh @@ -41,7 +41,7 @@ MOCKGEN_TARGETS=( "pkg/agent/cniserver/ipam IPAMDriver testing" "pkg/agent/flowexporter/connections ConnTrackDumper,NetFilterConnTrack testing" "pkg/agent/interfacestore InterfaceStore testing" - "pkg/agent/memberlist Memberlist testing" + "pkg/agent/memberlist Interface,Memberlist . mock_memberlist.go" "pkg/agent/multicast RouteInterface testing" "pkg/agent/types McastNetworkPolicyController testing" "pkg/agent/nodeportlocal/portcache LocalPortOpener testing" diff --git a/pkg/agent/apiserver/apiserver.go b/pkg/agent/apiserver/apiserver.go index 636c4dde3f3..0a3c4d36e56 100644 --- a/pkg/agent/apiserver/apiserver.go +++ b/pkg/agent/apiserver/apiserver.go @@ -35,6 +35,7 @@ import ( "antrea.io/antrea/pkg/agent/apiserver/handlers/agentinfo" "antrea.io/antrea/pkg/agent/apiserver/handlers/appliedtogroup" "antrea.io/antrea/pkg/agent/apiserver/handlers/featuregates" + "antrea.io/antrea/pkg/agent/apiserver/handlers/memberlist" "antrea.io/antrea/pkg/agent/apiserver/handlers/multicast" "antrea.io/antrea/pkg/agent/apiserver/handlers/networkpolicy" "antrea.io/antrea/pkg/agent/apiserver/handlers/ovsflows" @@ -85,6 +86,7 @@ func installHandlers(aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolic s.Handler.NonGoRestfulMux.HandleFunc("/ovsflows", ovsflows.HandleFunc(aq)) s.Handler.NonGoRestfulMux.HandleFunc("/ovstracing", ovstracing.HandleFunc(aq)) s.Handler.NonGoRestfulMux.HandleFunc("/serviceexternalip", serviceexternalip.HandleFunc(seipq)) + s.Handler.NonGoRestfulMux.HandleFunc("/memberlist", memberlist.HandleFunc(aq)) } func installAPIGroup(s *genericapiserver.GenericAPIServer, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, v4Enabled, v6Enabled bool) error { diff --git a/pkg/agent/apiserver/handlers/memberlist/handler.go b/pkg/agent/apiserver/handlers/memberlist/handler.go new file mode 100644 index 00000000000..dcf75915945 --- /dev/null +++ b/pkg/agent/apiserver/handlers/memberlist/handler.go @@ -0,0 +1,76 @@ +// 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 memberlist + +import ( + "encoding/json" + "net/http" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/klog/v2" + + "antrea.io/antrea/pkg/agent/querier" +) + +// Response describes the response struct of memberlist command. +type Response struct { + NodeName string `json:"nodeName,omitempty"` + IP string `json:"ip,omitempty"` + Status string `json:"status,omitempty"` +} + +func generateResponse(node *v1.Node, aliveNodes sets.String) Response { + status := "Dead" + if aliveNodes.Has(node.Name) { + status = "Alive" + } + return Response{ + NodeName: node.Name, + Status: status, + IP: node.Status.Addresses[0].Address, + } +} + +// HandleFunc returns the function which can handle queries issued by the memberlist command. +func HandleFunc(aq querier.AgentQuerier) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var memberlist []Response + allNodes, _ := aq.GetNodeInformer().Lister().List(labels.Everything()) + aliveNodes := aq.GetMemberlistCluster().AliveNodes() + for _, node := range allNodes { + memberlist = append(memberlist, generateResponse(node, aliveNodes)) + } + + err := json.NewEncoder(w).Encode(memberlist) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + klog.Errorf("Error when encoding Memberlist to json: %v", err) + } + } +} + +func (r Response) GetTableHeader() []string { + return []string{"NODE", "IP", "STATUS"} +} + +func (r Response) GetTableRow(_ int) []string { + return []string{r.NodeName, r.IP, r.Status} +} + +func (r Response) SortRows() bool { + return true +} diff --git a/pkg/agent/apiserver/handlers/memberlist/handler_test.go b/pkg/agent/apiserver/handlers/memberlist/handler_test.go new file mode 100644 index 00000000000..8f63675be7b --- /dev/null +++ b/pkg/agent/apiserver/handlers/memberlist/handler_test.go @@ -0,0 +1,107 @@ +// 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 memberlist + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "sort" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes/fake" + + "antrea.io/antrea/pkg/agent/memberlist" + queriertest "antrea.io/antrea/pkg/agent/querier/testing" +) + +var ( + node1 = v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "node1"}, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Address: "172.16.0.11", + }, + }, + }, + } + node2 = v1.Node{ + ObjectMeta: metav1.ObjectMeta{Name: "node2"}, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Address: "172.16.0.12", + }, + }, + }, + } +) + +func TestMemberlistQuery(t *testing.T) { + clientset := fake.NewSimpleClientset() + informerFactory := informers.NewSharedInformerFactory(clientset, 0) + nodeInformer := informerFactory.Core().V1().Nodes() + + stopCh := make(chan struct{}) + defer close(stopCh) + + informerFactory.Start(stopCh) + informerFactory.WaitForCacheSync(stopCh) + informerFactory.Core().V1().Nodes().Informer().GetIndexer().Add(&node1) + informerFactory.Core().V1().Nodes().Informer().GetIndexer().Add(&node2) + + ctrl := gomock.NewController(t) + q := queriertest.NewMockAgentQuerier(ctrl) + memberlistInterface := memberlist.NewMockInterface(ctrl) + q.EXPECT().GetNodeInformer().Return(nodeInformer) + q.EXPECT().GetMemberlistCluster().Return(memberlistInterface) + memberlistInterface.EXPECT().AliveNodes().Return(sets.NewString("node1")) + handler := HandleFunc(q) + + req, err := http.NewRequest(http.MethodGet, "", nil) + require.NoError(t, err) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + assert.Equal(t, http.StatusOK, recorder.Code) + + expectedResponse := []Response{ + { + NodeName: "node1", + IP: "172.16.0.11", + Status: "Alive", + }, + { + NodeName: "node2", + IP: "172.16.0.12", + Status: "Dead", + }, + } + var received []Response + err = json.Unmarshal(recorder.Body.Bytes(), &received) + require.NoError(t, err) + sort.Slice(received, func(i, j int) bool { + return received[i].NodeName < received[j].NodeName + }) + assert.Equal(t, expectedResponse, received) +} diff --git a/pkg/agent/memberlist/cluster_test.go b/pkg/agent/memberlist/cluster_test.go index d8cef6728c1..c381a83391b 100644 --- a/pkg/agent/memberlist/cluster_test.go +++ b/pkg/agent/memberlist/cluster_test.go @@ -34,7 +34,6 @@ import ( "antrea.io/antrea/pkg/agent/config" "antrea.io/antrea/pkg/agent/consistenthash" - memberlisttest "antrea.io/antrea/pkg/agent/memberlist/testing" "antrea.io/antrea/pkg/apis" crdv1a2 "antrea.io/antrea/pkg/apis/crd/v1alpha2" fakeversioned "antrea.io/antrea/pkg/client/clientset/versioned/fake" @@ -148,7 +147,7 @@ func TestCluster_Run(t *testing.T) { Name: localNodeName, NodeIPv4Addr: &net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.IPv4Mask(255, 255, 255, 255)}, } - mockMemberlist := memberlisttest.NewMockMemberlist(controller) + mockMemberlist := NewMockMemberlist(controller) fakeCluster, err := newFakeCluster(nodeConfig, stopCh, mockMemberlist) if err != nil { t.Fatalf("New fake memberlist server error: %v", err) @@ -204,7 +203,7 @@ func TestCluster_RunClusterEvents(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "fakeEgress1", UID: "fakeUID1"}, Spec: crdv1a2.EgressSpec{ExternalIPPool: fakeEIP1.Name, EgressIP: "1.1.1.2"}, } - mockMemberlist := memberlisttest.NewMockMemberlist(controller) + mockMemberlist := NewMockMemberlist(controller) fakeCluster, err := newFakeCluster(nodeConfig, stopCh, mockMemberlist) if err != nil { t.Fatalf("New fake memberlist server error: %v", err) @@ -662,7 +661,7 @@ func TestCluster_RejoinNodes(t *testing.T) { defer close(stopCh) controller := gomock.NewController(t) defer controller.Finish() - mockMemberlist := memberlisttest.NewMockMemberlist(controller) + mockMemberlist := NewMockMemberlist(controller) mockMemberlist.EXPECT().Join([]string{"10.0.0.2"}) mockMemberlist.EXPECT().Join([]string{"10.0.0.3"}) fakeCluster, _ := newFakeCluster(localNodeConfig, stopCh, mockMemberlist, node1, node2, node3) diff --git a/pkg/agent/memberlist/mock_memberlist.go b/pkg/agent/memberlist/mock_memberlist.go new file mode 100644 index 00000000000..5278171bcc9 --- /dev/null +++ b/pkg/agent/memberlist/mock_memberlist.go @@ -0,0 +1,197 @@ +// 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. +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: antrea.io/antrea/pkg/agent/memberlist (interfaces: Interface,Memberlist) + +// Package memberlist is a generated GoMock package. +package memberlist + +import ( + gomock "github.com/golang/mock/gomock" + memberlist0 "github.com/hashicorp/memberlist" + sets "k8s.io/apimachinery/pkg/util/sets" + reflect "reflect" + time "time" +) + +// MockInterface is a mock of Interface interface +type MockInterface struct { + ctrl *gomock.Controller + recorder *MockInterfaceMockRecorder +} + +// MockInterfaceMockRecorder is the mock recorder for MockInterface +type MockInterfaceMockRecorder struct { + mock *MockInterface +} + +// NewMockInterface creates a new mock instance +func NewMockInterface(ctrl *gomock.Controller) *MockInterface { + mock := &MockInterface{ctrl: ctrl} + mock.recorder = &MockInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder { + return m.recorder +} + +// AddClusterEventHandler mocks base method +func (m *MockInterface) AddClusterEventHandler(arg0 ClusterNodeEventHandler) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddClusterEventHandler", arg0) +} + +// AddClusterEventHandler indicates an expected call of AddClusterEventHandler +func (mr *MockInterfaceMockRecorder) AddClusterEventHandler(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddClusterEventHandler", reflect.TypeOf((*MockInterface)(nil).AddClusterEventHandler), arg0) +} + +// AliveNodes mocks base method +func (m *MockInterface) AliveNodes() sets.String { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AliveNodes") + ret0, _ := ret[0].(sets.String) + return ret0 +} + +// AliveNodes indicates an expected call of AliveNodes +func (mr *MockInterfaceMockRecorder) AliveNodes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AliveNodes", reflect.TypeOf((*MockInterface)(nil).AliveNodes)) +} + +// SelectNodeForIP mocks base method +func (m *MockInterface) SelectNodeForIP(arg0, arg1 string, arg2 ...func(string) bool) (string, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "SelectNodeForIP", varargs...) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SelectNodeForIP indicates an expected call of SelectNodeForIP +func (mr *MockInterfaceMockRecorder) SelectNodeForIP(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectNodeForIP", reflect.TypeOf((*MockInterface)(nil).SelectNodeForIP), varargs...) +} + +// ShouldSelectIP mocks base method +func (m *MockInterface) ShouldSelectIP(arg0, arg1 string, arg2 ...func(string) bool) (bool, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ShouldSelectIP", varargs...) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ShouldSelectIP indicates an expected call of ShouldSelectIP +func (mr *MockInterfaceMockRecorder) ShouldSelectIP(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShouldSelectIP", reflect.TypeOf((*MockInterface)(nil).ShouldSelectIP), varargs...) +} + +// MockMemberlist is a mock of Memberlist interface +type MockMemberlist struct { + ctrl *gomock.Controller + recorder *MockMemberlistMockRecorder +} + +// MockMemberlistMockRecorder is the mock recorder for MockMemberlist +type MockMemberlistMockRecorder struct { + mock *MockMemberlist +} + +// NewMockMemberlist creates a new mock instance +func NewMockMemberlist(ctrl *gomock.Controller) *MockMemberlist { + mock := &MockMemberlist{ctrl: ctrl} + mock.recorder = &MockMemberlistMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockMemberlist) EXPECT() *MockMemberlistMockRecorder { + return m.recorder +} + +// Join mocks base method +func (m *MockMemberlist) Join(arg0 []string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Join", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Join indicates an expected call of Join +func (mr *MockMemberlistMockRecorder) Join(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Join", reflect.TypeOf((*MockMemberlist)(nil).Join), arg0) +} + +// Leave mocks base method +func (m *MockMemberlist) Leave(arg0 time.Duration) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Leave", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Leave indicates an expected call of Leave +func (mr *MockMemberlistMockRecorder) Leave(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Leave", reflect.TypeOf((*MockMemberlist)(nil).Leave), arg0) +} + +// Members mocks base method +func (m *MockMemberlist) Members() []*memberlist0.Node { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Members") + ret0, _ := ret[0].([]*memberlist0.Node) + return ret0 +} + +// Members indicates an expected call of Members +func (mr *MockMemberlistMockRecorder) Members() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Members", reflect.TypeOf((*MockMemberlist)(nil).Members)) +} + +// Shutdown mocks base method +func (m *MockMemberlist) Shutdown() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Shutdown") + ret0, _ := ret[0].(error) + return ret0 +} + +// Shutdown indicates an expected call of Shutdown +func (mr *MockMemberlistMockRecorder) Shutdown() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockMemberlist)(nil).Shutdown)) +} diff --git a/pkg/agent/memberlist/testing/mock_memberlist.go b/pkg/agent/memberlist/testing/mock_memberlist.go deleted file mode 100644 index b6a7597071b..00000000000 --- a/pkg/agent/memberlist/testing/mock_memberlist.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2022 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. -// - -// Code generated by MockGen. DO NOT EDIT. -// Source: antrea.io/antrea/pkg/agent/memberlist (interfaces: Memberlist) - -// Package testing is a generated GoMock package. -package testing - -import ( - gomock "github.com/golang/mock/gomock" - memberlist "github.com/hashicorp/memberlist" - reflect "reflect" - time "time" -) - -// MockMemberlist is a mock of Memberlist interface -type MockMemberlist struct { - ctrl *gomock.Controller - recorder *MockMemberlistMockRecorder -} - -// MockMemberlistMockRecorder is the mock recorder for MockMemberlist -type MockMemberlistMockRecorder struct { - mock *MockMemberlist -} - -// NewMockMemberlist creates a new mock instance -func NewMockMemberlist(ctrl *gomock.Controller) *MockMemberlist { - mock := &MockMemberlist{ctrl: ctrl} - mock.recorder = &MockMemberlistMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockMemberlist) EXPECT() *MockMemberlistMockRecorder { - return m.recorder -} - -// Join mocks base method -func (m *MockMemberlist) Join(arg0 []string) (int, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Join", arg0) - ret0, _ := ret[0].(int) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Join indicates an expected call of Join -func (mr *MockMemberlistMockRecorder) Join(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Join", reflect.TypeOf((*MockMemberlist)(nil).Join), arg0) -} - -// Leave mocks base method -func (m *MockMemberlist) Leave(arg0 time.Duration) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Leave", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Leave indicates an expected call of Leave -func (mr *MockMemberlistMockRecorder) Leave(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Leave", reflect.TypeOf((*MockMemberlist)(nil).Leave), arg0) -} - -// Members mocks base method -func (m *MockMemberlist) Members() []*memberlist.Node { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Members") - ret0, _ := ret[0].([]*memberlist.Node) - return ret0 -} - -// Members indicates an expected call of Members -func (mr *MockMemberlistMockRecorder) Members() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Members", reflect.TypeOf((*MockMemberlist)(nil).Members)) -} - -// Shutdown mocks base method -func (m *MockMemberlist) Shutdown() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Shutdown") - ret0, _ := ret[0].(error) - return ret0 -} - -// Shutdown indicates an expected call of Shutdown -func (mr *MockMemberlistMockRecorder) Shutdown() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockMemberlist)(nil).Shutdown)) -} diff --git a/pkg/agent/querier/querier.go b/pkg/agent/querier/querier.go index 219438d2e54..c5f1137bd6a 100644 --- a/pkg/agent/querier/querier.go +++ b/pkg/agent/querier/querier.go @@ -17,11 +17,13 @@ package querier import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + coreinformers "k8s.io/client-go/informers/core/v1" clientset "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" "antrea.io/antrea/pkg/agent/config" "antrea.io/antrea/pkg/agent/interfacestore" + "antrea.io/antrea/pkg/agent/memberlist" "antrea.io/antrea/pkg/agent/openflow" "antrea.io/antrea/pkg/agent/proxy" "antrea.io/antrea/pkg/apis/crd/v1beta1" @@ -42,6 +44,8 @@ type AgentQuerier interface { GetOVSCtlClient() ovsctl.OVSCtlClient GetProxier() proxy.Proxier GetNetworkPolicyInfoQuerier() querier.AgentNetworkPolicyInfoQuerier + GetMemberlistCluster() memberlist.Interface + GetNodeInformer() coreinformers.NodeInformer } type agentQuerier struct { @@ -55,6 +59,8 @@ type agentQuerier struct { networkPolicyInfoQuerier querier.AgentNetworkPolicyInfoQuerier apiPort int nplRange string + memberlistCluster memberlist.Interface + nodeInformer coreinformers.NodeInformer } func NewAgentQuerier( @@ -68,6 +74,8 @@ func NewAgentQuerier( networkPolicyInfoQuerier querier.AgentNetworkPolicyInfoQuerier, apiPort int, nplRange string, + memberlistCluster memberlist.Interface, + nodeInformer coreinformers.NodeInformer, ) *agentQuerier { return &agentQuerier{ nodeConfig: nodeConfig, @@ -80,9 +88,21 @@ func NewAgentQuerier( networkPolicyInfoQuerier: networkPolicyInfoQuerier, apiPort: apiPort, nplRange: nplRange, + memberlistCluster: memberlistCluster, + nodeInformer: nodeInformer, } } +// GetNodeInformer returns NodeInformer. +func (aq agentQuerier) GetNodeInformer() coreinformers.NodeInformer { + return aq.nodeInformer +} + +// GetMemberlistCluster returns MemberlistCluster Interface. +func (aq agentQuerier) GetMemberlistCluster() memberlist.Interface { + return aq.memberlistCluster +} + // GetNodeConfig returns NodeConfig. func (aq agentQuerier) GetNodeConfig() *config.NodeConfig { return aq.nodeConfig diff --git a/pkg/agent/querier/testing/mock_querier.go b/pkg/agent/querier/testing/mock_querier.go index 0a61d8192d7..5bca764e3bf 100644 --- a/pkg/agent/querier/testing/mock_querier.go +++ b/pkg/agent/querier/testing/mock_querier.go @@ -1,4 +1,4 @@ -// Copyright 2021 Antrea Authors +// 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. @@ -22,12 +22,14 @@ package testing import ( config "antrea.io/antrea/pkg/agent/config" interfacestore "antrea.io/antrea/pkg/agent/interfacestore" + memberlist "antrea.io/antrea/pkg/agent/memberlist" openflow "antrea.io/antrea/pkg/agent/openflow" proxy "antrea.io/antrea/pkg/agent/proxy" v1beta1 "antrea.io/antrea/pkg/apis/crd/v1beta1" ovsctl "antrea.io/antrea/pkg/ovs/ovsctl" querier "antrea.io/antrea/pkg/querier" gomock "github.com/golang/mock/gomock" + v1 "k8s.io/client-go/informers/core/v1" kubernetes "k8s.io/client-go/kubernetes" reflect "reflect" ) @@ -95,6 +97,20 @@ func (mr *MockAgentQuerierMockRecorder) GetK8sClient() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetK8sClient", reflect.TypeOf((*MockAgentQuerier)(nil).GetK8sClient)) } +// GetMemberlistCluster mocks base method +func (m *MockAgentQuerier) GetMemberlistCluster() memberlist.Interface { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMemberlistCluster") + ret0, _ := ret[0].(memberlist.Interface) + return ret0 +} + +// GetMemberlistCluster indicates an expected call of GetMemberlistCluster +func (mr *MockAgentQuerierMockRecorder) GetMemberlistCluster() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMemberlistCluster", reflect.TypeOf((*MockAgentQuerier)(nil).GetMemberlistCluster)) +} + // GetNetworkConfig mocks base method func (m *MockAgentQuerier) GetNetworkConfig() *config.NetworkConfig { m.ctrl.T.Helper() @@ -137,6 +153,20 @@ func (mr *MockAgentQuerierMockRecorder) GetNodeConfig() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeConfig", reflect.TypeOf((*MockAgentQuerier)(nil).GetNodeConfig)) } +// GetNodeInformer mocks base method +func (m *MockAgentQuerier) GetNodeInformer() v1.NodeInformer { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNodeInformer") + ret0, _ := ret[0].(v1.NodeInformer) + return ret0 +} + +// GetNodeInformer indicates an expected call of GetNodeInformer +func (mr *MockAgentQuerierMockRecorder) GetNodeInformer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeInformer", reflect.TypeOf((*MockAgentQuerier)(nil).GetNodeInformer)) +} + // GetOVSCtlClient mocks base method func (m *MockAgentQuerier) GetOVSCtlClient() ovsctl.OVSCtlClient { m.ctrl.T.Helper() diff --git a/pkg/agent/supportbundlecollection/support_bundle_controller_test.go b/pkg/agent/supportbundlecollection/support_bundle_controller_test.go index 1f03d8c1a2e..4767b203650 100644 --- a/pkg/agent/supportbundlecollection/support_bundle_controller_test.go +++ b/pkg/agent/supportbundlecollection/support_bundle_controller_test.go @@ -223,6 +223,7 @@ type mockAgentDumper struct { dumpNetworkPolicyResourcesErr error dumpHeapPprofErr error dumpOVSPortsErr error + dumpMemberlistErr error } func (d *mockAgentDumper) DumpLog(basedir string) error { @@ -252,3 +253,7 @@ func (d *mockAgentDumper) DumpHeapPprof(basedir string) error { func (d *mockAgentDumper) DumpOVSPorts(basedir string) error { return d.dumpOVSPortsErr } + +func (d *mockAgentDumper) DumpMemberlist(basedir string) error { + return d.dumpMemberlistErr +} diff --git a/pkg/antctl/antctl.go b/pkg/antctl/antctl.go index 089f60d5198..af87fca8255 100644 --- a/pkg/antctl/antctl.go +++ b/pkg/antctl/antctl.go @@ -19,6 +19,7 @@ import ( "reflect" "antrea.io/antrea/pkg/agent/apiserver/handlers/agentinfo" + "antrea.io/antrea/pkg/agent/apiserver/handlers/memberlist" "antrea.io/antrea/pkg/agent/apiserver/handlers/multicast" "antrea.io/antrea/pkg/agent/apiserver/handlers/ovsflows" "antrea.io/antrea/pkg/agent/apiserver/handlers/podinterface" @@ -568,6 +569,20 @@ $ antctl get podmulticaststats pod -n namespace`, }, transformedResponse: reflect.TypeOf(serviceexternalip.Response{}), }, + { + use: "memberlist", + aliases: []string{"ml"}, + short: "Print state of memberlist cluster", + long: "Print state of memberlist cluster of Antrea agent", + commandGroup: get, + agentEndpoint: &endpoint{ + nonResourceEndpoint: &nonResourceEndpoint{ + path: "/memberlist", + outputType: multiple, + }, + }, + transformedResponse: reflect.TypeOf(memberlist.Response{}), + }, }, rawCommands: []rawCommand{ { diff --git a/pkg/antctl/command_definition_test.go b/pkg/antctl/command_definition_test.go index 11c95cc3947..e4c74cdc13e 100644 --- a/pkg/antctl/command_definition_test.go +++ b/pkg/antctl/command_definition_test.go @@ -30,6 +30,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "antrea.io/antrea/pkg/agent/apiserver/handlers/agentinfo" + "antrea.io/antrea/pkg/agent/apiserver/handlers/memberlist" "antrea.io/antrea/pkg/agent/apiserver/handlers/podinterface" "antrea.io/antrea/pkg/antctl/output" "antrea.io/antrea/pkg/antctl/runtime" @@ -315,6 +316,25 @@ GroupName expected: `NAMESPACE NAME INTERFACE-NAME IP MAC PORT-UUID OF-PORT CONTAINER-ID default nginx-32b489d4b7-vgv7v Interface2 127.0.0.2 07-16-76-00-02-87 portuuid1 35572 uci2ucsd6dx default nginx-6db489d4b7-vgv7v Interface 127.0.0.1 07-16-76-00-02-86 portuuid0 80 dve7a2d6c22 +`, + }, + { + name: "StructuredData-Memberlist-State", + rawResponseData: []memberlist.Response{ + { + NodeName: "node1", + IP: "192.168.1.2", + Status: "Alive", + }, + { + NodeName: "node2", + IP: "192.168.1.3", + Status: "Dead", + }, + }, + expected: `NODE IP STATUS +node1 192.168.1.2 Alive +node2 192.168.1.3 Dead `, }, } { diff --git a/pkg/antctl/command_list_test.go b/pkg/antctl/command_list_test.go index 972a0532186..8a87fc666a3 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"}, {"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"}, {"supportbundle"}, {"traceflow"}, {"get", "featuregates"}}, }, { name: "Antctl running against flow-aggregator mode", diff --git a/pkg/apiserver/registry/system/supportbundle/rest.go b/pkg/apiserver/registry/system/supportbundle/rest.go index 470badf614b..6ff96225075 100644 --- a/pkg/apiserver/registry/system/supportbundle/rest.go +++ b/pkg/apiserver/registry/system/supportbundle/rest.go @@ -263,6 +263,7 @@ func (r *supportBundleREST) collectAgent(ctx context.Context, since string) (*sy dumper.DumpAgentInfo, dumper.DumpHeapPprof, dumper.DumpOVSPorts, + dumper.DumpMemberlist, ) } diff --git a/pkg/apiserver/registry/system/supportbundle/rest_test.go b/pkg/apiserver/registry/system/supportbundle/rest_test.go index 704993dfa06..bc5538e1e32 100644 --- a/pkg/apiserver/registry/system/supportbundle/rest_test.go +++ b/pkg/apiserver/registry/system/supportbundle/rest_test.go @@ -221,6 +221,10 @@ func (f *fakeAgentDumper) DumpOVSPorts(basedir string) error { return f.returnErr } +func (f *fakeAgentDumper) DumpMemberlist(basedir string) error { + return f.returnErr +} + func TestAgentStorage(t *testing.T) { defaultFS = afero.NewMemMapFs() defaultExecutor = new(testExec) diff --git a/pkg/monitor/agent_test.go b/pkg/monitor/agent_test.go index 2fcad18691b..40bab782a1a 100644 --- a/pkg/monitor/agent_test.go +++ b/pkg/monitor/agent_test.go @@ -147,7 +147,7 @@ func newAgentMonitor(crdClient *fakeclientset.Clientset, t *testing.T) *agentMon networkPolicyInfoQuerier.EXPECT().GetAddressGroupNum().Return(30).AnyTimes() networkPolicyInfoQuerier.EXPECT().GetControllerConnectionStatus().Return(true).AnyTimes() - querier := querier.NewAgentQuerier(nodeConfig, nil, interfaceStore, client, ofClient, ovsBridgeClient, nil, networkPolicyInfoQuerier, 10349, "") + querier := querier.NewAgentQuerier(nodeConfig, nil, interfaceStore, client, ofClient, ovsBridgeClient, nil, networkPolicyInfoQuerier, 10349, "", nil, nil) return NewAgentMonitor(crdClient, querier) } diff --git a/pkg/support/dump.go b/pkg/support/dump.go index 209d3fce2a2..598c3707a84 100644 --- a/pkg/support/dump.go +++ b/pkg/support/dump.go @@ -61,6 +61,9 @@ type AgentDumper interface { // DumpOVSPorts should create file that contains OF port descriptions under the basedir. DumpOVSPorts(basedir string) error + // DumpMemberlist should create a file that contains state of Memberlist + // cluster of the agent Pod under the basedir. + DumpMemberlist(basedir string) error } // ControllerDumper is the interface for dumping runtime information of the @@ -289,6 +292,10 @@ func (d *agentDumper) DumpAgentInfo(basedir string) error { return writeYAMLFile(d.fs, filepath.Join(basedir, "agentinfo"), "agentinfo", ai) } +func (d *agentDumper) DumpMemberlist(basedir string) error { + return dumpAntctlGet(d.fs, d.executor, "memberlist", basedir) +} + func (d *agentDumper) DumpNetworkPolicyResources(basedir string) error { dump := func(o interface{}, name string) error { return writeYAMLFile(d.fs, filepath.Join(basedir, name), name, o)