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)
+       }
+}

Reply via email to