This is an automated email from the ASF dual-hosted git repository. tokers pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push: new a8352fa feat: implement API to get apisix instances status (#958) a8352fa is described below commit a8352fafcce5f4abb158fcd14ed8658c51901643 Author: Peter Zhu <starsz...@gmail.com> AuthorDate: Fri Dec 18 20:00:03 2020 +0800 feat: implement API to get apisix instances status (#958) related #849 . --- api/internal/core/entity/entity.go | 10 + api/internal/core/store/storehub.go | 25 ++- api/internal/handler/server_info/server_info.go | 89 +++++++++ .../handler/server_info/server_info_test.go | 222 +++++++++++++++++++++ api/internal/route.go | 2 + api/test/docker/apisix_config.yaml | 6 + .../{apisix_config.yaml => apisix_config2.yaml} | 6 + api/test/docker/docker-compose.yaml | 8 +- api/test/e2e/server_info_test.go | 90 +++++++++ 9 files changed, 449 insertions(+), 9 deletions(-) diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go index e1270c8..9f02c35 100644 --- a/api/internal/core/entity/entity.go +++ b/api/internal/core/entity/entity.go @@ -225,3 +225,13 @@ type Script struct { ID string `json:"id"` Script interface{} `json:"script,omitempty"` } + +type ServerInfo struct { + BaseInfo + LastReportTime int64 `json:"last_report_time,omitempty"` + UpTime int64 `json:"up_time,omitempty"` + BootTime int64 `json:"boot_time,omitempty"` + EtcdVersion string `json:"etcd_version,omitempty"` + Hostname string `json:"hostname,omitempty"` + Version string `json:"version,omitempty"` +} diff --git a/api/internal/core/store/storehub.go b/api/internal/core/store/storehub.go index 25a5f56..e9c54ce 100644 --- a/api/internal/core/store/storehub.go +++ b/api/internal/core/store/storehub.go @@ -28,12 +28,13 @@ import ( type HubKey string const ( - HubKeyConsumer HubKey = "consumer" - HubKeyRoute HubKey = "route" - HubKeyService HubKey = "service" - HubKeySsl HubKey = "ssl" - HubKeyUpstream HubKey = "upstream" - HubKeyScript HubKey = "script" + HubKeyConsumer HubKey = "consumer" + HubKeyRoute HubKey = "route" + HubKeyService HubKey = "service" + HubKeySsl HubKey = "ssl" + HubKeyUpstream HubKey = "upstream" + HubKeyScript HubKey = "script" + HubKeyServerInfo HubKey = `server_info` ) var ( @@ -144,5 +145,17 @@ func InitStores() error { return err } + err = InitStore(HubKeyServerInfo, GenericStoreOption{ + BasePath: "/apisix/data_plane/server_info", + ObjType: reflect.TypeOf(entity.ServerInfo{}), + KeyFunc: func(obj interface{}) string { + r := obj.(*entity.ServerInfo) + return utils.InterfaceToString(r.ID) + }, + }) + if err != nil { + return err + } + return nil } diff --git a/api/internal/handler/server_info/server_info.go b/api/internal/handler/server_info/server_info.go new file mode 100644 index 0000000..23ead6d --- /dev/null +++ b/api/internal/handler/server_info/server_info.go @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 server_info + +import ( + "reflect" + "strings" + + "github.com/gin-gonic/gin" + "github.com/shiningrush/droplet" + "github.com/shiningrush/droplet/wrapper" + wgin "github.com/shiningrush/droplet/wrapper/gin" + + "github.com/apisix/manager-api/internal/core/entity" + "github.com/apisix/manager-api/internal/core/store" + "github.com/apisix/manager-api/internal/handler" +) + +type Handler struct { + serverInfoStore store.Interface +} + +func NewHandler() (handler.RouteRegister, error) { + return &Handler{ + serverInfoStore: store.GetStore(store.HubKeyServerInfo), + }, nil +} + +func (h *Handler) ApplyRoute(r *gin.Engine) { + r.GET("/apisix/server_info/:id", wgin.Wraps(h.Get, + wrapper.InputType(reflect.TypeOf(GetInput{})))) + r.GET("/apisix/server_info", wgin.Wraps(h.List, + wrapper.InputType(reflect.TypeOf(ListInput{})))) +} + +type GetInput struct { + ID string `auto_read:"id,path" validate:"required"` +} + +func (h *Handler) Get(c droplet.Context) (interface{}, error) { + input := c.Input().(*GetInput) + + r, err := h.serverInfoStore.Get(input.ID) + if err != nil { + return handler.SpecCodeResponse(err), err + } + + return r, nil +} + +type ListInput struct { + store.Pagination + Hostname string `auto_read:"hostname,query"` +} + +func (h *Handler) List(c droplet.Context) (interface{}, error) { + input := c.Input().(*ListInput) + + ret, err := h.serverInfoStore.List(store.ListInput{ + Predicate: func(obj interface{}) bool { + if input.Hostname != "" { + return strings.Contains(obj.(*entity.ServerInfo).Hostname, input.Hostname) + } + return true + }, + PageSize: input.PageSize, + PageNumber: input.PageNumber, + }) + + if err != nil { + return nil, err + } + + return ret, nil +} diff --git a/api/internal/handler/server_info/server_info_test.go b/api/internal/handler/server_info/server_info_test.go new file mode 100644 index 0000000..5160d9b --- /dev/null +++ b/api/internal/handler/server_info/server_info_test.go @@ -0,0 +1,222 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 server_info + +import ( + "errors" + "testing" + + "github.com/shiningrush/droplet" + "github.com/shiningrush/droplet/data" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/apisix/manager-api/internal/core/entity" + "github.com/apisix/manager-api/internal/core/store" +) + +func TestHandler_Get(t *testing.T) { + var ( + tests = []struct { + caseDesc string + giveInput *GetInput + giveErr error + giveRet interface{} + wantErr error + wantGetKey string + wantRet interface{} + }{ + { + caseDesc: "get server_info", + giveInput: &GetInput{ID: "server_1"}, + giveRet: &entity.ServerInfo{ + BaseInfo: entity.BaseInfo{ID: "server_1"}, + UpTime: 10, + LastReportTime: 1608195454, + BootTime: 1608195454, + Hostname: "gentoo", + Version: "v3", + }, + wantGetKey: "server_1", + wantRet: &entity.ServerInfo{ + BaseInfo: entity.BaseInfo{ID: "server_1"}, + UpTime: 10, + LastReportTime: 1608195454, + BootTime: 1608195454, + Hostname: "gentoo", + Version: "v3", + }, + }, + { + caseDesc: "get server_info not exist", + giveInput: &GetInput{ID: "server_3"}, + giveRet: &data.SpecCodeResponse{Response: data.Response{Code: 0}, StatusCode: 404}, + giveErr: errors.New("not found"), + wantGetKey: "server_3", + wantRet: &data.SpecCodeResponse{Response: data.Response{Code: 0}, StatusCode: 404}, + wantErr: errors.New("not found"), + }, + } + ) + + for _, tc := range tests { + t.Run(tc.caseDesc, func(t *testing.T) { + getCalled := true + mStore := &store.MockInterface{} + mStore.On("Get", mock.Anything).Run(func(args mock.Arguments) { + getCalled = true + assert.Equal(t, tc.wantGetKey, args.Get(0)) + }).Return(tc.giveRet, tc.giveErr) + + h := Handler{serverInfoStore: mStore} + ctx := droplet.NewContext() + ctx.SetInput(tc.giveInput) + ret, err := h.Get(ctx) + assert.True(t, getCalled) + assert.Equal(t, tc.wantErr, err) + assert.Equal(t, tc.wantRet, ret) + }) + } +} + +func TestHandler_List(t *testing.T) { + var ( + tests = []struct { + caseDesc string + giveInput *ListInput + giveData []interface{} + giveErr error + wantErr error + wantGetKey *ListInput + wantRet interface{} + }{ + { + caseDesc: "list server_info", + giveInput: &ListInput{Hostname: ""}, + giveData: []interface{}{ + &entity.ServerInfo{ + BaseInfo: entity.BaseInfo{ID: "server_1"}, + UpTime: 10, + LastReportTime: 1608195454, + BootTime: 1608195454, + Hostname: "gentoo", + Version: "v3", + }, + &entity.ServerInfo{ + BaseInfo: entity.BaseInfo{ID: "server_2"}, + UpTime: 10, + LastReportTime: 1608195454, + BootTime: 1608195454, + Hostname: "ubuntu", + Version: "v2", + }, + }, + wantRet: &store.ListOutput{ + Rows: []interface{}{ + &entity.ServerInfo{ + BaseInfo: entity.BaseInfo{ID: "server_1"}, + UpTime: 10, + LastReportTime: 1608195454, + BootTime: 1608195454, + Hostname: "gentoo", + Version: "v3", + }, + &entity.ServerInfo{ + BaseInfo: entity.BaseInfo{ID: "server_2"}, + UpTime: 10, + LastReportTime: 1608195454, + BootTime: 1608195454, + Hostname: "ubuntu", + Version: "v2", + }, + }, + TotalSize: 2, + }, + }, + { + caseDesc: "list server_info with hostname", + giveInput: &ListInput{Hostname: "ubuntu"}, + giveData: []interface{}{ + &entity.ServerInfo{ + BaseInfo: entity.BaseInfo{ID: "server_1"}, + UpTime: 10, + LastReportTime: 1608195454, + BootTime: 1608195454, + Hostname: "gentoo", + Version: "v3", + }, + &entity.ServerInfo{ + BaseInfo: entity.BaseInfo{ID: "server_2"}, + UpTime: 10, + LastReportTime: 1608195454, + BootTime: 1608195454, + Hostname: "ubuntu", + Version: "v2", + }, + }, + wantRet: &store.ListOutput{ + Rows: []interface{}{ + &entity.ServerInfo{ + BaseInfo: entity.BaseInfo{ID: "server_2"}, + UpTime: 10, + LastReportTime: 1608195454, + BootTime: 1608195454, + Hostname: "ubuntu", + Version: "v2", + }, + }, + TotalSize: 1, + }, + }, + } + ) + + for _, tc := range tests { + t.Run(tc.caseDesc, func(t *testing.T) { + getCalled := true + mStore := &store.MockInterface{} + mStore.On("List", mock.Anything).Run(func(args mock.Arguments) { + getCalled = true + }).Return(func(input store.ListInput) *store.ListOutput { + var res []interface{} + for _, c := range tc.giveData { + if input.Predicate(c) { + if input.Format != nil { + res = append(res, input.Format(c)) + } else { + res = append(res, c) + } + } + } + + return &store.ListOutput{ + Rows: res, + TotalSize: len(res), + } + }, tc.giveErr) + + h := Handler{serverInfoStore: mStore} + ctx := droplet.NewContext() + ctx.SetInput(tc.giveInput) + ret, err := h.List(ctx) + assert.True(t, getCalled) + assert.Equal(t, tc.wantErr, err) + assert.Equal(t, tc.wantRet, ret) + }) + } +} diff --git a/api/internal/route.go b/api/internal/route.go index 4b3d576..06c4271 100644 --- a/api/internal/route.go +++ b/api/internal/route.go @@ -34,6 +34,7 @@ import ( "github.com/apisix/manager-api/internal/handler/healthz" "github.com/apisix/manager-api/internal/handler/plugin" "github.com/apisix/manager-api/internal/handler/route" + "github.com/apisix/manager-api/internal/handler/server_info" "github.com/apisix/manager-api/internal/handler/service" "github.com/apisix/manager-api/internal/handler/ssl" "github.com/apisix/manager-api/internal/handler/upstream" @@ -65,6 +66,7 @@ func SetUpRouter() *gin.Engine { plugin.NewHandler, healthz.NewHandler, authentication.NewHandler, + server_info.NewHandler, } for i := range factories { diff --git a/api/test/docker/apisix_config.yaml b/api/test/docker/apisix_config.yaml index 4758397..9e48c29 100644 --- a/api/test/docker/apisix_config.yaml +++ b/api/test/docker/apisix_config.yaml @@ -24,6 +24,7 @@ etcd: - "http://172.16.238.12:2379" apisix: + id: "apisix-server1" admin_key: - name: "admin" # yamllint disable rule:comments-indentation @@ -82,9 +83,14 @@ plugins: # plugin list (sorted in alphabetical order) - uri-blocker - wolf-rbac - zipkin + - server-info plugin_attr: skywalking: service_name: APISIX service_instance_name: "APISIX Instance Name" endpoint_addr: http://172.16.238.50:12800 + + server-info: + report_interval: 60 + report_ttl: 3600 diff --git a/api/test/docker/apisix_config.yaml b/api/test/docker/apisix_config2.yaml similarity index 96% copy from api/test/docker/apisix_config.yaml copy to api/test/docker/apisix_config2.yaml index 4758397..84e593e 100644 --- a/api/test/docker/apisix_config.yaml +++ b/api/test/docker/apisix_config2.yaml @@ -24,6 +24,7 @@ etcd: - "http://172.16.238.12:2379" apisix: + id: "apisix-server2" admin_key: - name: "admin" # yamllint disable rule:comments-indentation @@ -82,9 +83,14 @@ plugins: # plugin list (sorted in alphabetical order) - uri-blocker - wolf-rbac - zipkin + - server-info plugin_attr: skywalking: service_name: APISIX service_instance_name: "APISIX Instance Name" endpoint_addr: http://172.16.238.50:12800 + + server-info: + report_interval: 60 + report_ttl: 3600 diff --git a/api/test/docker/docker-compose.yaml b/api/test/docker/docker-compose.yaml index 410a078..cacca65 100644 --- a/api/test/docker/docker-compose.yaml +++ b/api/test/docker/docker-compose.yaml @@ -126,11 +126,12 @@ services: ipv4_address: 172.16.238.20 apisix: + hostname: apisix_server1 build: context: ../../ dockerfile: test/docker/Dockerfile-apisix args: - - APISIX_VERSION=2.1 + - APISIX_VERSION=master restart: always volumes: - ./apisix_config.yaml:/usr/local/apisix/conf/config.yaml:ro @@ -149,14 +150,15 @@ services: ipv4_address: 172.16.238.30 apisix2: + hostname: apisix_server2 build: context: ../../ dockerfile: test/docker/Dockerfile-apisix args: - - APISIX_VERSION=2.1 + - APISIX_VERSION=master restart: always volumes: - - ./apisix_config.yaml:/usr/local/apisix/conf/config.yaml:ro + - ./apisix_config2.yaml:/usr/local/apisix/conf/config.yaml:ro - ../certs/apisix.crt:/usr/local/apisix/certs/apisix.crt:ro - ../certs/apisix.key:/usr/local/apisix/certs/apisix.key:ro depends_on: diff --git a/api/test/e2e/server_info_test.go b/api/test/e2e/server_info_test.go new file mode 100644 index 0000000..8e2f069 --- /dev/null +++ b/api/test/e2e/server_info_test.go @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 e2e + +import ( + "net/http" + "testing" + "time" +) + +func TestServerInfo_Get(t *testing.T) { + // wait for apisix report + time.Sleep(2 * time.Second) + testCases := []HttpTestCase{ + { + caseDesc: "get server info", + Object: ManagerApiExpect(t), + Path: "/apisix/server_info/apisix-server1", + Method: http.MethodGet, + Headers: map[string]string{"Authorization": token}, + ExpectStatus: http.StatusOK, + ExpectBody: "\"hostname\":\"apisix_server1\"", + }, + { + caseDesc: "get server info", + Object: ManagerApiExpect(t), + Path: "/apisix/server_info/apisix-server2", + Method: http.MethodGet, + Headers: map[string]string{"Authorization": token}, + ExpectStatus: http.StatusOK, + ExpectBody: "\"hostname\":\"apisix_server2\"", + }, + } + + for _, tc := range testCases { + testCaseCheck(tc) + } +} + +func TestServerInfo_List(t *testing.T) { + testCases := []HttpTestCase{ + { + caseDesc: "list all server info", + Object: ManagerApiExpect(t), + Path: "/apisix/server_info", + Method: http.MethodGet, + Headers: map[string]string{"Authorization": token}, + ExpectStatus: http.StatusOK, + ExpectBody: "\"total_size\":2", + }, + { + caseDesc: "list server info with hostname", + Object: ManagerApiExpect(t), + Path: "/apisix/server_info", + Query: "hostname=apisix_", + Method: http.MethodGet, + Headers: map[string]string{"Authorization": token}, + ExpectStatus: http.StatusOK, + ExpectBody: "\"total_size\":2", + }, + { + caseDesc: "list server info with hostname", + Object: ManagerApiExpect(t), + Path: "/apisix/server_info", + Query: "hostname=apisix_server2", + Method: http.MethodGet, + Headers: map[string]string{"Authorization": token}, + ExpectStatus: http.StatusOK, + ExpectBody: "\"total_size\":1", + }, + } + + for _, tc := range testCases { + testCaseCheck(tc) + } +}