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