You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by to...@apache.org on 2020/12/18 12:00:14 UTC

[apisix-dashboard] branch master updated: feat: implement API to get apisix instances status (#958)

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 <st...@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)
+	}
+}