nic-chen commented on a change in pull request #2065:
URL: https://github.com/apache/apisix-dashboard/pull/2065#discussion_r696207991



##########
File path: api/cmd/cache_verify.go
##########
@@ -0,0 +1,149 @@
+/*
+ * 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 cmd
+
+import (
+       "bytes"
+       "encoding/json"
+       "fmt"
+       "github.com/apisix/manager-api/internal/conf"
+       "github.com/apisix/manager-api/internal/handler/cache_verify"
+       "github.com/spf13/cobra"
+       "github.com/tidwall/gjson"
+       "io/ioutil"
+       "log"
+       "net/http"

Review comment:
       ```suggestion
        "bytes"
        "encoding/json"
        "fmt"
        "io/ioutil"
        "log"
        "net/http"
        
        "github.com/spf13/cobra"
        "github.com/tidwall/gjson"
   
        "github.com/apisix/manager-api/internal/conf"
        "github.com/apisix/manager-api/internal/handler/cache_verify"
   ```

##########
File path: api/internal/handler/cache_verify/cache_verify.go
##########
@@ -0,0 +1,268 @@
+/*
+ * 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 cache_verify
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "github.com/apisix/manager-api/internal/core/storage"
+       "github.com/apisix/manager-api/internal/core/store"
+       "github.com/apisix/manager-api/internal/handler"
+       "github.com/gin-gonic/gin"
+       "github.com/shiningrush/droplet"
+       wgin "github.com/shiningrush/droplet/wrapper/gin"
+       "path"
+       "reflect"

Review comment:
       code style

##########
File path: api/cmd/cache_verify.go
##########
@@ -0,0 +1,149 @@
+/*
+ * 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 cmd
+
+import (
+       "bytes"
+       "encoding/json"
+       "fmt"
+       "github.com/apisix/manager-api/internal/conf"
+       "github.com/apisix/manager-api/internal/handler/cache_verify"
+       "github.com/spf13/cobra"
+       "github.com/tidwall/gjson"
+       "io/ioutil"
+       "log"
+       "net/http"
+)
+
+var port int
+
+type response struct {
+       Data cache_verify.OutputResult `json:"data"`
+}
+
+func newCacheVerifyCommand() *cobra.Command {
+       return &cobra.Command{
+               Use:   "cache-verify",
+               Short: "verify that data in cache are consistent with that in 
ETCD",
+               Run: func(cmd *cobra.Command, args []string) {
+                       conf.InitConf()
+                       port = conf.ServerPort
+                       token := getToken()
+
+                       url := 
fmt.Sprintf("http://localhost:%d/apisix/admin/cache_verify";, port)
+                       client := &http.Client{}
+
+                       get, err := http.NewRequest("GET", url, nil)
+                       if err != nil {
+                               log.Fatal("new http request failed")

Review comment:
       need to log the error

##########
File path: api/cmd/cache_verify.go
##########
@@ -0,0 +1,149 @@
+/*
+ * 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 cmd
+
+import (
+       "bytes"
+       "encoding/json"
+       "fmt"
+       "github.com/apisix/manager-api/internal/conf"
+       "github.com/apisix/manager-api/internal/handler/cache_verify"
+       "github.com/spf13/cobra"
+       "github.com/tidwall/gjson"
+       "io/ioutil"
+       "log"
+       "net/http"
+)
+
+var port int
+
+type response struct {
+       Data cache_verify.OutputResult `json:"data"`
+}
+
+func newCacheVerifyCommand() *cobra.Command {
+       return &cobra.Command{
+               Use:   "cache-verify",
+               Short: "verify that data in cache are consistent with that in 
ETCD",
+               Run: func(cmd *cobra.Command, args []string) {
+                       conf.InitConf()
+                       port = conf.ServerPort
+                       token := getToken()
+
+                       url := 
fmt.Sprintf("http://localhost:%d/apisix/admin/cache_verify";, port)
+                       client := &http.Client{}
+
+                       get, err := http.NewRequest("GET", url, nil)
+                       if err != nil {
+                               log.Fatal("new http request failed")
+                       }
+
+                       get.Header.Set("Authorization", token)
+
+                       rsp, err := client.Do(get)
+                       if err != nil {
+                               fmt.Println("get result from migrate/export 
failed")

Review comment:
       ditto

##########
File path: api/cmd/cache_verify.go
##########
@@ -0,0 +1,149 @@
+/*
+ * 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 cmd
+
+import (
+       "bytes"
+       "encoding/json"
+       "fmt"
+       "github.com/apisix/manager-api/internal/conf"
+       "github.com/apisix/manager-api/internal/handler/cache_verify"
+       "github.com/spf13/cobra"
+       "github.com/tidwall/gjson"
+       "io/ioutil"
+       "log"
+       "net/http"
+)
+
+var port int
+
+type response struct {
+       Data cache_verify.OutputResult `json:"data"`
+}
+
+func newCacheVerifyCommand() *cobra.Command {
+       return &cobra.Command{
+               Use:   "cache-verify",
+               Short: "verify that data in cache are consistent with that in 
ETCD",
+               Run: func(cmd *cobra.Command, args []string) {
+                       conf.InitConf()
+                       port = conf.ServerPort
+                       token := getToken()
+
+                       url := 
fmt.Sprintf("http://localhost:%d/apisix/admin/cache_verify";, port)
+                       client := &http.Client{}
+
+                       get, err := http.NewRequest("GET", url, nil)
+                       if err != nil {
+                               log.Fatal("new http request failed")
+                       }
+
+                       get.Header.Set("Authorization", token)
+
+                       rsp, err := client.Do(get)
+                       if err != nil {
+                               fmt.Println("get result from migrate/export 
failed")
+                               return
+                       }
+                       defer func() {
+                               err := rsp.Body.Close()
+                               if err != nil {
+                                       fmt.Println("close on response body 
failed")
+                               }
+                       }()
+
+                       data, err := ioutil.ReadAll(rsp.Body)
+                       if err != nil {
+                               fmt.Println(err)
+                       }
+
+                       var rs response
+                       err = json.Unmarshal(data, &rs)
+                       if err != nil {
+                               log.Fatal("bad Data format,json unmarshal 
failed")
+                       }
+
+                       fmt.Printf("cache verification result as follows:\n\n")
+                       fmt.Printf("There are %d items in total,%d of them are 
consistent,%d of them are inconsistent\n",
+                               rs.Data.Total, rs.Data.ConsistentCount, 
rs.Data.InconsistentCount)
+
+                       printResult("ssls", rs.Data.Items.SSLs)
+
+                       printResult("routes", rs.Data.Items.Routes)
+
+                       printResult("scripts", rs.Data.Items.Scripts)
+
+                       printResult("services", rs.Data.Items.Services)
+
+                       printResult("upstreams", rs.Data.Items.Upstreams)
+
+                       printResult("consumers", rs.Data.Items.Consumers)
+
+                       printResult("server infos", rs.Data.Items.ServerInfos)
+
+                       printResult("global plugins", 
rs.Data.Items.GlobalPlugins)
+
+                       printResult("plugin configs", 
rs.Data.Items.PluginConfigs)

Review comment:
       why not loop them in `printResult`?

##########
File path: api/internal/handler/cache_verify/cache_verify_test.go
##########
@@ -0,0 +1,135 @@
+/*
+ * 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 cache_verify
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "github.com/apisix/manager-api/internal/core/storage"
+       "github.com/apisix/manager-api/internal/core/store"
+       "github.com/shiningrush/droplet"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+       "testing"
+)
+
+func TestHandler_CacheVerify(t *testing.T) {
+       andyStr := 
`{"username":"andy","plugins":{"key-auth":{"key":"key-of-john"}},"create_time":1627739045,"update_time":1627744978}`
+       var andyObj interface{}
+       err := json.Unmarshal([]byte(andyStr), &andyObj)
+       if err != nil {
+               fmt.Printf("unmarshal error :: %s", err.Error())
+       }
+       brokenAndyStr := 
`{"username":"andy","plugins":{"key-auth":{"key":"key-of-john"}},"create_time":1627739046,"update_time":1627744978}`
+       var brokenAndyObj interface{}
+       err = json.Unmarshal([]byte(brokenAndyStr), &brokenAndyObj)
+       if err != nil {
+               fmt.Printf("unmarshal error :: %s", err.Error())
+       }
+       consumerPrefix := "/apisix/consumers/"
+
+       tests := []struct {
+               caseDesc                 string
+               listInput                string
+               listRet                  []storage.Keypair
+               getInput                 string
+               getRet                   interface{}
+               wantInconsistentConsumer int
+       }{
+               {
+                       caseDesc:  "consistent",
+                       listInput: consumerPrefix,
+                       listRet: []storage.Keypair{
+                               {
+                                       Key:   consumerPrefix + "andy",
+                                       Value: andyStr,
+                               },
+                       },
+                       getInput:                 "andy",
+                       getRet:                   andyObj,
+                       wantInconsistentConsumer: 0,
+               },
+               {
+                       caseDesc:  "inconsistent",
+                       listInput: consumerPrefix,
+                       listRet: []storage.Keypair{
+                               {
+                                       Key:   consumerPrefix + "andy",
+                                       Value: andyStr,
+                               },
+                       },
+                       getInput:                 "andy",
+                       getRet:                   brokenAndyObj,
+                       wantInconsistentConsumer: 1,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.caseDesc, func(t *testing.T) {
+                       mockConsumerCache := store.MockInterface{}
+                       mockEtcdStorage := storage.MockInterface{}
+                       mockEtcdStorage.On("List", context.TODO(), 
consumerPrefix).Return(tc.listRet, nil)
+                       // for any other type of configs,etcd.List just return 
empty slice,so it will not check further
+                       mockEtcdStorage.On("List", context.TODO(), 
mock.Anything).Return([]storage.Keypair{}, nil)
+
+                       mockConsumerCache.On("Get", 
tc.getInput).Return(tc.getRet, nil)
+                       handler := Handler{consumerStore: &mockConsumerCache, 
etcdStorage: &mockEtcdStorage}
+                       rs, err := handler.CacheVerify(droplet.NewContext())
+                       //fmt.Println((string)(rs))
+                       assert.Nil(t, err, nil)
+                       // todo 因为现在输出了很多统计信息,那么测试的时候,就要相应的assert这些
+                       v, ok := rs.(OutputResult)
+                       assert.True(t, ok, true)
+                       assert.Equal(t, v.Items.Consumers.InconsistentCount, 
tc.wantInconsistentConsumer)
+
+                       // test output of command line,when there are 
inconsistent items
+                       //fmt.Printf("cache verification result as 
follows:\n\n")
+                       //fmt.Printf("There are %d items in total,%d of them 
are consistent,%d of them are inconsistent\n",
+                       //      v.Total, v.ConsistentCount, v.InconsistentCount)
+                       //
+                       //printResult("ssls", v.Items.SSLs)
+                       //
+                       //printResult("routes", v.Items.Routes)
+                       //
+                       //printResult("scripts", v.Items.Scripts)
+                       //
+                       //printResult("services", v.Items.Services)
+                       //
+                       //printResult("upstreams", v.Items.Upstreams)
+                       //
+                       //printResult("consumers", v.Items.Consumers)
+                       //
+                       //printResult("server infos", v.Items.ServerInfos)
+                       //
+                       //printResult("global plugins", v.Items.GlobalPlugins)
+                       //
+                       //printResult("plugin configs", v.Items.PluginConfigs)
+               })
+       }
+
+}
+
+//func printResult(name string, data StatisticalData) {
+//     fmt.Printf("%-15s: %d in total,%d consistent,%d inconsistent\n", name, 
data.Total, data.ConsistentCount, data.InconsistentCount)
+//     if data.InconsistentCount > 0 {
+//             fmt.Printf("inconsistent %s:\n", name)
+//             for _, pair := range data.InconsistentPairs {
+//                     fmt.Printf("[key](%s)\n[etcd](%s)\n[cache](%s)\n", 
pair.Key, pair.EtcdValue, pair.CacheValue)
+//             }
+//     }
+//}

Review comment:
       Please recheck and improve this file
   

##########
File path: api/test/e2enew/cache_verify/cache_verify_test.go
##########
@@ -0,0 +1,148 @@
+/*
+ * 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 cache_verify
+
+import (
+       "github.com/apisix/manager-api/test/e2enew/base"
+       "github.com/onsi/ginkgo"
+       "github.com/onsi/gomega"
+       "github.com/tidwall/gjson"
+       "net/http"
+)
+
+var _ = ginkgo.Describe("Cache verify", func() {
+
+       //ginkgo.It("prepare config data", prepareConfigData)
+
+       ginkgo.It("cache verify ", func() {
+
+               // we access this API twice,assert the diff
+               headers := map[string]string{
+                       "Authorization": base.GetToken(),
+               }
+
+               oldData, status, err := 
base.HttpGet(base.ManagerAPIHost+"/apisix/admin/cache_verify", headers)
+               gomega.Expect(status).Should(gomega.Equal(http.StatusOK))
+               gomega.Expect(err).Should(gomega.BeNil())
+
+               oldTotal := gjson.Get((string)(oldData), "data.total")
+               gomega.Expect(oldTotal.Exists()).Should(gomega.Equal(true))
+               oldTotalInt := oldTotal.Int()
+
+               prepareConfigData()
+
+               newData, status, err := 
base.HttpGet(base.ManagerAPIHost+"/apisix/admin/cache_verify", headers)
+               gomega.Expect(status).Should(gomega.Equal(http.StatusOK))
+               gomega.Expect(err).Should(gomega.BeNil())
+
+               newTotal := gjson.Get((string)(newData), "data.total")
+               gomega.Expect(newTotal.Exists()).Should(gomega.Equal(true))
+               newTotalInt := newTotal.Int()
+
+               gomega.Expect(newTotalInt).Should(gomega.Equal(oldTotalInt + 3))
+
+       })
+
+       ginkgo.It("request hit route r1", func() {
+               base.RunTestCase(base.HttpTestCase{
+                       Object:       base.APISIXExpect(),
+                       Method:       http.MethodGet,
+                       Path:         "/hello_",
+                       ExpectStatus: http.StatusOK,
+                       ExpectBody:   "hello world",
+                       Sleep:        base.SleepTime,
+               })
+       })
+
+       ginkgo.It("delete all config", deleteConfigData)
+
+})
+
+func prepareConfigData() {
+       headers := map[string]string{
+               "Content-Type":  "application/json",
+               "Authorization": base.GetToken(),
+       }
+       _, statusCode, err := 
base.HttpPut(base.ManagerAPIHost+"/apisix/admin/routes/r1", headers, `{
+               "name": "route1",
+               "uri": "/hello_",
+               "upstream": {
+                       "nodes": {
+                               "`+base.UpstreamIp+`:1980": 1
+                       },
+                       "type": "roundrobin"
+               }
+       }`)
+       gomega.Expect(statusCode).Should(gomega.Equal(http.StatusOK))
+       gomega.Expect(err).Should(gomega.BeNil())
+
+       _, statusCode, err = 
base.HttpPut(base.ManagerAPIHost+"/apisix/admin/upstreams/1", headers, `{
+               "name": "upstream1",
+               "nodes": [
+                       {
+                               "host": "`+base.UpstreamIp+`",
+                               "port": 1980,
+                               "weight": 1
+                       }
+               ],
+               "type": "roundrobin"
+       }`)
+       gomega.Expect(statusCode).Should(gomega.Equal(http.StatusOK))
+       gomega.Expect(err).Should(gomega.BeNil())
+
+       _, statusCode, err = 
base.HttpPut(base.ManagerAPIHost+"/apisix/admin/services/s1", headers, `{
+ "name": "testservice",
+ "upstream": {
+   "nodes": [
+     {
+       "host": "`+base.UpstreamIp+`",
+       "port": 1980,
+       "weight": 1
+     },
+     {
+       "host": "`+base.UpstreamIp+`",
+       "port": 1981,
+       "weight": 2
+     },
+     {
+       "host": "`+base.UpstreamIp+`",
+       "port": 1982,
+       "weight": 3
+     }
+   ],
+   "type": "roundrobin"
+ }
+}`)
+       gomega.Expect(statusCode).Should(gomega.Equal(http.StatusOK))
+       gomega.Expect(err).Should(gomega.BeNil())
+}

Review comment:
       need more test cases.

##########
File path: api/internal/handler/cache_verify/cache_verify_test.go
##########
@@ -0,0 +1,135 @@
+/*
+ * 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 cache_verify
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "github.com/apisix/manager-api/internal/core/storage"
+       "github.com/apisix/manager-api/internal/core/store"
+       "github.com/shiningrush/droplet"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+       "testing"

Review comment:
       style

##########
File path: api/internal/handler/cache_verify/cache_verify.go
##########
@@ -0,0 +1,268 @@
+/*
+ * 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 cache_verify
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "github.com/apisix/manager-api/internal/core/storage"
+       "github.com/apisix/manager-api/internal/core/store"
+       "github.com/apisix/manager-api/internal/handler"
+       "github.com/gin-gonic/gin"
+       "github.com/shiningrush/droplet"
+       wgin "github.com/shiningrush/droplet/wrapper/gin"
+       "path"
+       "reflect"
+)
+
+type Handler struct {
+       consumerStore     store.Interface
+       routeStore        store.Interface
+       serviceStore      store.Interface
+       sslStore          store.Interface
+       upstreamStore     store.Interface
+       scriptStore       store.Interface
+       globalPluginStore store.Interface
+       pluginConfigStore store.Interface
+       serverInfoStore   store.Interface
+       etcdStorage       storage.Interface
+}
+
+type inconsistentPair struct {
+       Key        string `json:"key"`
+       CacheValue string `json:"cache_value"`
+       EtcdValue  string `json:"etcd_value"`
+}
+
+type StatisticalData struct {
+       Total             int                `json:"total"`
+       ConsistentCount   int                `json:"consistent_count"`
+       InconsistentCount int                `json:"inconsistent_count"`
+       InconsistentPairs []inconsistentPair `json:"inconsistent_pairs"`
+}
+
+type items struct {
+       Consumers     StatisticalData `json:"consumers"`
+       Routes        StatisticalData `json:"routes"`
+       Services      StatisticalData `json:"services"`
+       SSLs          StatisticalData `json:"ssls"`
+       Upstreams     StatisticalData `json:"upstreams"`
+       Scripts       StatisticalData `json:"scripts"`
+       GlobalPlugins StatisticalData `json:"global_plugins"`
+       PluginConfigs StatisticalData `json:"plugin_configs"`
+       ServerInfos   StatisticalData `json:"server_infos"`
+}
+
+type OutputResult struct {
+       Total             int   `json:"total"`
+       ConsistentCount   int   `json:"consistent_count"`
+       InconsistentCount int   `json:"inconsistent_count"`
+       Items             items `json:"items"`
+}
+
+var infixMap = map[store.HubKey]string{
+       store.HubKeyConsumer:     "consumers",
+       store.HubKeyRoute:        "routes",
+       store.HubKeyService:      "services",
+       store.HubKeySsl:          "ssl",
+       store.HubKeyUpstream:     "upstreams",
+       store.HubKeyScript:       "scripts",
+       store.HubKeyGlobalRule:   "global_rules",
+       store.HubKeyServerInfo:   "data_plane/server_info",
+       store.HubKeyPluginConfig: "plugin_configs",
+}
+
+func NewHandler() (handler.RouteRegister, error) {
+       return &Handler{
+               consumerStore:     store.GetStore(store.HubKeyConsumer),
+               routeStore:        store.GetStore(store.HubKeyRoute),
+               serviceStore:      store.GetStore(store.HubKeyService),
+               sslStore:          store.GetStore(store.HubKeySsl),
+               upstreamStore:     store.GetStore(store.HubKeyUpstream),
+               scriptStore:       store.GetStore(store.HubKeyScript),
+               globalPluginStore: store.GetStore(store.HubKeyGlobalRule),
+               pluginConfigStore: store.GetStore(store.HubKeyPluginConfig),
+               serverInfoStore:   store.GetStore(store.HubKeyServerInfo),
+               etcdStorage:       storage.GenEtcdStorage(),
+       }, nil
+}
+
+func (h *Handler) ApplyRoute(r *gin.Engine) {
+       r.GET("/apisix/admin/cache_verify", wgin.Wraps(h.CacheVerify))
+}
+
+func (h *Handler) CacheVerify(_ droplet.Context) (interface{}, error) {
+       checkConsistent := func(hubKey store.HubKey, s *store.Interface, rs 
*OutputResult, etcd *storage.Interface) {
+
+               keyPairs, err := (*etcd).List(context.TODO(), 
fmt.Sprintf("/apisix/%s/", infixMap[hubKey]))
+               if err != nil {
+                       fmt.Println(err)
+               }
+
+               rs.Total += len(keyPairs)
+
+               for i := range keyPairs {
+                       key := path.Base(keyPairs[i].Key)
+
+                       cacheObj, err := (*s).Get(context.TODO(), key)
+                       if err != nil {
+                               fmt.Println(err)
+                       }
+
+                       etcdValue := keyPairs[i].Value
+                       cmp, cacheValue := compare(keyPairs[i].Value, cacheObj)
+
+                       if !cmp {
+                               rs.InconsistentCount++
+                               cmpResult := inconsistentPair{EtcdValue: 
etcdValue, CacheValue: cacheValue, Key: key}
+                               if hubKey == store.HubKeyConsumer {

Review comment:
       why not use switch ?

##########
File path: api/internal/handler/cache_verify/cache_verify.go
##########
@@ -0,0 +1,268 @@
+/*
+ * 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 cache_verify
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "github.com/apisix/manager-api/internal/core/storage"
+       "github.com/apisix/manager-api/internal/core/store"
+       "github.com/apisix/manager-api/internal/handler"
+       "github.com/gin-gonic/gin"
+       "github.com/shiningrush/droplet"
+       wgin "github.com/shiningrush/droplet/wrapper/gin"
+       "path"
+       "reflect"
+)
+
+type Handler struct {
+       consumerStore     store.Interface
+       routeStore        store.Interface
+       serviceStore      store.Interface
+       sslStore          store.Interface
+       upstreamStore     store.Interface
+       scriptStore       store.Interface
+       globalPluginStore store.Interface
+       pluginConfigStore store.Interface
+       serverInfoStore   store.Interface
+       etcdStorage       storage.Interface
+}
+
+type inconsistentPair struct {
+       Key        string `json:"key"`
+       CacheValue string `json:"cache_value"`
+       EtcdValue  string `json:"etcd_value"`
+}
+
+type StatisticalData struct {
+       Total             int                `json:"total"`
+       ConsistentCount   int                `json:"consistent_count"`
+       InconsistentCount int                `json:"inconsistent_count"`
+       InconsistentPairs []inconsistentPair `json:"inconsistent_pairs"`
+}
+
+type items struct {
+       Consumers     StatisticalData `json:"consumers"`
+       Routes        StatisticalData `json:"routes"`
+       Services      StatisticalData `json:"services"`
+       SSLs          StatisticalData `json:"ssls"`
+       Upstreams     StatisticalData `json:"upstreams"`
+       Scripts       StatisticalData `json:"scripts"`
+       GlobalPlugins StatisticalData `json:"global_plugins"`
+       PluginConfigs StatisticalData `json:"plugin_configs"`
+       ServerInfos   StatisticalData `json:"server_infos"`
+}
+
+type OutputResult struct {
+       Total             int   `json:"total"`
+       ConsistentCount   int   `json:"consistent_count"`
+       InconsistentCount int   `json:"inconsistent_count"`
+       Items             items `json:"items"`
+}
+
+var infixMap = map[store.HubKey]string{
+       store.HubKeyConsumer:     "consumers",
+       store.HubKeyRoute:        "routes",
+       store.HubKeyService:      "services",
+       store.HubKeySsl:          "ssl",
+       store.HubKeyUpstream:     "upstreams",
+       store.HubKeyScript:       "scripts",
+       store.HubKeyGlobalRule:   "global_rules",
+       store.HubKeyServerInfo:   "data_plane/server_info",
+       store.HubKeyPluginConfig: "plugin_configs",
+}
+
+func NewHandler() (handler.RouteRegister, error) {
+       return &Handler{
+               consumerStore:     store.GetStore(store.HubKeyConsumer),
+               routeStore:        store.GetStore(store.HubKeyRoute),
+               serviceStore:      store.GetStore(store.HubKeyService),
+               sslStore:          store.GetStore(store.HubKeySsl),
+               upstreamStore:     store.GetStore(store.HubKeyUpstream),
+               scriptStore:       store.GetStore(store.HubKeyScript),
+               globalPluginStore: store.GetStore(store.HubKeyGlobalRule),
+               pluginConfigStore: store.GetStore(store.HubKeyPluginConfig),
+               serverInfoStore:   store.GetStore(store.HubKeyServerInfo),
+               etcdStorage:       storage.GenEtcdStorage(),
+       }, nil
+}
+
+func (h *Handler) ApplyRoute(r *gin.Engine) {
+       r.GET("/apisix/admin/cache_verify", wgin.Wraps(h.CacheVerify))
+}
+
+func (h *Handler) CacheVerify(_ droplet.Context) (interface{}, error) {
+       checkConsistent := func(hubKey store.HubKey, s *store.Interface, rs 
*OutputResult, etcd *storage.Interface) {
+
+               keyPairs, err := (*etcd).List(context.TODO(), 
fmt.Sprintf("/apisix/%s/", infixMap[hubKey]))
+               if err != nil {
+                       fmt.Println(err)
+               }
+
+               rs.Total += len(keyPairs)
+
+               for i := range keyPairs {
+                       key := path.Base(keyPairs[i].Key)
+
+                       cacheObj, err := (*s).Get(context.TODO(), key)
+                       if err != nil {
+                               fmt.Println(err)
+                       }
+
+                       etcdValue := keyPairs[i].Value
+                       cmp, cacheValue := compare(keyPairs[i].Value, cacheObj)
+
+                       if !cmp {
+                               rs.InconsistentCount++
+                               cmpResult := inconsistentPair{EtcdValue: 
etcdValue, CacheValue: cacheValue, Key: key}
+                               if hubKey == store.HubKeyConsumer {
+                                       // is there a way that I can avoid this 
if else ?
+                                       // 
可以尝试用map实现?比如:items作为一个hubKey为key,statisticalData为value的map
+                                       rs.Items.Consumers.InconsistentCount++
+                                       rs.Items.Consumers.Total++
+                                       rs.Items.Consumers.InconsistentPairs = 
append(rs.Items.Consumers.InconsistentPairs, cmpResult)
+                               }
+                               if hubKey == store.HubKeyRoute {
+                                       rs.Items.Routes.InconsistentCount++
+                                       rs.Items.Routes.Total++
+                                       rs.Items.Routes.InconsistentPairs = 
append(rs.Items.Routes.InconsistentPairs, cmpResult)
+                               }
+                               if hubKey == store.HubKeyScript {
+                                       rs.Items.Scripts.InconsistentCount++
+                                       rs.Items.Scripts.Total++
+                                       rs.Items.Scripts.InconsistentPairs = 
append(rs.Items.Scripts.InconsistentPairs, cmpResult)
+                               }
+                               if hubKey == store.HubKeyService {
+                                       rs.Items.Services.InconsistentCount++
+                                       rs.Items.Services.Total++
+                                       rs.Items.Services.InconsistentPairs = 
append(rs.Items.Services.InconsistentPairs, cmpResult)
+                               }
+                               if hubKey == store.HubKeyGlobalRule {
+                                       
rs.Items.GlobalPlugins.InconsistentCount++
+                                       rs.Items.GlobalPlugins.Total++
+                                       
rs.Items.GlobalPlugins.InconsistentPairs = 
append(rs.Items.GlobalPlugins.InconsistentPairs, cmpResult)
+                               }
+                               if hubKey == store.HubKeyPluginConfig {
+                                       
rs.Items.PluginConfigs.InconsistentCount++
+                                       rs.Items.PluginConfigs.Total++
+                                       
rs.Items.PluginConfigs.InconsistentPairs = 
append(rs.Items.PluginConfigs.InconsistentPairs, cmpResult)
+                               }
+                               if hubKey == store.HubKeyUpstream {
+                                       rs.Items.Upstreams.InconsistentCount++
+                                       rs.Items.Upstreams.Total++
+                                       rs.Items.Upstreams.InconsistentPairs = 
append(rs.Items.Upstreams.InconsistentPairs, cmpResult)
+                               }
+                               if hubKey == store.HubKeySsl {
+                                       rs.Items.SSLs.InconsistentCount++
+                                       rs.Items.SSLs.Total++
+                                       rs.Items.SSLs.InconsistentPairs = 
append(rs.Items.SSLs.InconsistentPairs, cmpResult)
+                               }
+                               if hubKey == store.HubKeyServerInfo {
+                                       rs.Items.ServerInfos.Total++
+                                       rs.Items.ServerInfos.InconsistentCount++
+                                       rs.Items.ServerInfos.InconsistentPairs 
= append(rs.Items.ServerInfos.InconsistentPairs, cmpResult)
+                               }
+                       } else {
+                               rs.ConsistentCount++
+
+                               if hubKey == store.HubKeyConsumer {
+                                       // is there a way that I can avoid this 
if else ?
+                                       // 
可以尝试用map实现?比如:items作为一个hubKey为key,statisticalData为value的map
+                                       rs.Items.Consumers.ConsistentCount++
+                                       rs.Items.Consumers.Total++
+                               }
+                               if hubKey == store.HubKeyRoute {
+                                       rs.Items.Routes.ConsistentCount++
+                                       rs.Items.Routes.Total++
+                               }
+                               if hubKey == store.HubKeyScript {
+                                       rs.Items.Scripts.ConsistentCount++
+                                       rs.Items.Scripts.Total++
+                               }
+                               if hubKey == store.HubKeyService {
+                                       rs.Items.Services.ConsistentCount++
+                                       rs.Items.Services.Total++
+                               }
+                               if hubKey == store.HubKeyGlobalRule {
+                                       rs.Items.GlobalPlugins.ConsistentCount++
+                                       rs.Items.GlobalPlugins.Total++
+                               }
+                               if hubKey == store.HubKeyPluginConfig {
+                                       rs.Items.PluginConfigs.ConsistentCount++
+                                       rs.Items.PluginConfigs.Total++
+                               }
+                               if hubKey == store.HubKeyUpstream {
+                                       rs.Items.Upstreams.ConsistentCount++
+                                       rs.Items.Upstreams.Total++
+                               }
+                               if hubKey == store.HubKeySsl {
+                                       rs.Items.SSLs.ConsistentCount++
+                                       rs.Items.SSLs.Total++
+                               }
+                               if hubKey == store.HubKeyServerInfo {
+                                       rs.Items.ServerInfos.Total++
+                                       rs.Items.ServerInfos.ConsistentCount++
+                               }
+                       }
+               }
+       }
+
+       var rs OutputResult
+       // todo this will panic when consumerStore is nil?
+       checkConsistent(store.HubKeyConsumer, &h.consumerStore, &rs, 
&h.etcdStorage)
+       checkConsistent(store.HubKeyRoute, &h.routeStore, &rs, &h.etcdStorage)
+       checkConsistent(store.HubKeyService, &h.serviceStore, &rs, 
&h.etcdStorage)
+       checkConsistent(store.HubKeySsl, &h.sslStore, &rs, &h.etcdStorage)
+       checkConsistent(store.HubKeyUpstream, &h.upstreamStore, &rs, 
&h.etcdStorage)
+       checkConsistent(store.HubKeyScript, &h.scriptStore, &rs, &h.etcdStorage)
+       checkConsistent(store.HubKeyGlobalRule, &h.globalPluginStore, &rs, 
&h.etcdStorage)
+       checkConsistent(store.HubKeyPluginConfig, &h.pluginConfigStore, &rs, 
&h.etcdStorage)
+       checkConsistent(store.HubKeyServerInfo, &h.serverInfoStore, &rs, 
&h.etcdStorage)
+       return rs, nil
+}
+
+func compare(etcdValue string, cacheObj interface{}) (bool, string) {
+       s, err := json.Marshal(cacheObj)
+       if err != nil {
+               fmt.Printf("json marsharl failed : %cacheObj\n", err)
+               return false, ""
+       }
+       cacheValue := string(s)
+       cmp, err := areEqualJSON(cacheValue, etcdValue)
+       if err != nil {
+               fmt.Printf("compare json failed %cacheObj\n", err)
+       }
+       return cmp, cacheValue
+}
+
+func areEqualJSON(s1, s2 string) (bool, error) {
+       var o1 interface{}
+       var o2 interface{}
+
+       var err error
+       err = json.Unmarshal([]byte(s1), &o1)

Review comment:
       Why Marshal first and then Unmarshal them?

##########
File path: api/internal/handler/cache_verify/cache_verify.go
##########
@@ -0,0 +1,268 @@
+/*
+ * 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 cache_verify
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "github.com/apisix/manager-api/internal/core/storage"
+       "github.com/apisix/manager-api/internal/core/store"
+       "github.com/apisix/manager-api/internal/handler"
+       "github.com/gin-gonic/gin"
+       "github.com/shiningrush/droplet"
+       wgin "github.com/shiningrush/droplet/wrapper/gin"
+       "path"
+       "reflect"
+)
+
+type Handler struct {
+       consumerStore     store.Interface
+       routeStore        store.Interface
+       serviceStore      store.Interface
+       sslStore          store.Interface
+       upstreamStore     store.Interface
+       scriptStore       store.Interface
+       globalPluginStore store.Interface
+       pluginConfigStore store.Interface
+       serverInfoStore   store.Interface
+       etcdStorage       storage.Interface
+}
+
+type inconsistentPair struct {
+       Key        string `json:"key"`
+       CacheValue string `json:"cache_value"`
+       EtcdValue  string `json:"etcd_value"`
+}
+
+type StatisticalData struct {
+       Total             int                `json:"total"`
+       ConsistentCount   int                `json:"consistent_count"`
+       InconsistentCount int                `json:"inconsistent_count"`
+       InconsistentPairs []inconsistentPair `json:"inconsistent_pairs"`
+}
+
+type items struct {
+       Consumers     StatisticalData `json:"consumers"`
+       Routes        StatisticalData `json:"routes"`
+       Services      StatisticalData `json:"services"`
+       SSLs          StatisticalData `json:"ssls"`
+       Upstreams     StatisticalData `json:"upstreams"`
+       Scripts       StatisticalData `json:"scripts"`
+       GlobalPlugins StatisticalData `json:"global_plugins"`
+       PluginConfigs StatisticalData `json:"plugin_configs"`
+       ServerInfos   StatisticalData `json:"server_infos"`
+}
+
+type OutputResult struct {
+       Total             int   `json:"total"`
+       ConsistentCount   int   `json:"consistent_count"`
+       InconsistentCount int   `json:"inconsistent_count"`
+       Items             items `json:"items"`
+}
+
+var infixMap = map[store.HubKey]string{
+       store.HubKeyConsumer:     "consumers",
+       store.HubKeyRoute:        "routes",
+       store.HubKeyService:      "services",
+       store.HubKeySsl:          "ssl",
+       store.HubKeyUpstream:     "upstreams",
+       store.HubKeyScript:       "scripts",
+       store.HubKeyGlobalRule:   "global_rules",
+       store.HubKeyServerInfo:   "data_plane/server_info",
+       store.HubKeyPluginConfig: "plugin_configs",
+}
+
+func NewHandler() (handler.RouteRegister, error) {
+       return &Handler{
+               consumerStore:     store.GetStore(store.HubKeyConsumer),
+               routeStore:        store.GetStore(store.HubKeyRoute),
+               serviceStore:      store.GetStore(store.HubKeyService),
+               sslStore:          store.GetStore(store.HubKeySsl),
+               upstreamStore:     store.GetStore(store.HubKeyUpstream),
+               scriptStore:       store.GetStore(store.HubKeyScript),
+               globalPluginStore: store.GetStore(store.HubKeyGlobalRule),
+               pluginConfigStore: store.GetStore(store.HubKeyPluginConfig),
+               serverInfoStore:   store.GetStore(store.HubKeyServerInfo),
+               etcdStorage:       storage.GenEtcdStorage(),
+       }, nil
+}
+
+func (h *Handler) ApplyRoute(r *gin.Engine) {
+       r.GET("/apisix/admin/cache_verify", wgin.Wraps(h.CacheVerify))
+}
+
+func (h *Handler) CacheVerify(_ droplet.Context) (interface{}, error) {
+       checkConsistent := func(hubKey store.HubKey, s *store.Interface, rs 
*OutputResult, etcd *storage.Interface) {
+
+               keyPairs, err := (*etcd).List(context.TODO(), 
fmt.Sprintf("/apisix/%s/", infixMap[hubKey]))

Review comment:
       ```suggestion
        checkConsistent := func(hubKey store.HubKey, s *store.Interface, rs 
*OutputResult, etcdStorage *storage.Interface) {
                keyPairs, err := (*etcdStorage).List(context.TODO(), 
fmt.Sprintf("/apisix/%s/", infixMap[hubKey]))
   ```

##########
File path: api/cmd/cache_verify.go
##########
@@ -0,0 +1,149 @@
+/*
+ * 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 cmd
+
+import (
+       "bytes"
+       "encoding/json"
+       "fmt"
+       "github.com/apisix/manager-api/internal/conf"
+       "github.com/apisix/manager-api/internal/handler/cache_verify"
+       "github.com/spf13/cobra"
+       "github.com/tidwall/gjson"
+       "io/ioutil"
+       "log"
+       "net/http"
+)
+
+var port int
+
+type response struct {
+       Data cache_verify.OutputResult `json:"data"`
+}
+
+func newCacheVerifyCommand() *cobra.Command {
+       return &cobra.Command{
+               Use:   "cache-verify",
+               Short: "verify that data in cache are consistent with that in 
ETCD",
+               Run: func(cmd *cobra.Command, args []string) {
+                       conf.InitConf()
+                       port = conf.ServerPort
+                       token := getToken()
+
+                       url := 
fmt.Sprintf("http://localhost:%d/apisix/admin/cache_verify";, port)
+                       client := &http.Client{}
+
+                       get, err := http.NewRequest("GET", url, nil)
+                       if err != nil {
+                               log.Fatal("new http request failed")
+                       }
+
+                       get.Header.Set("Authorization", token)
+
+                       rsp, err := client.Do(get)
+                       if err != nil {
+                               fmt.Println("get result from migrate/export 
failed")
+                               return
+                       }
+                       defer func() {
+                               err := rsp.Body.Close()
+                               if err != nil {
+                                       fmt.Println("close on response body 
failed")
+                               }
+                       }()
+
+                       data, err := ioutil.ReadAll(rsp.Body)
+                       if err != nil {
+                               fmt.Println(err)
+                       }
+
+                       var rs response
+                       err = json.Unmarshal(data, &rs)
+                       if err != nil {
+                               log.Fatal("bad Data format,json unmarshal 
failed")
+                       }
+
+                       fmt.Printf("cache verification result as follows:\n\n")
+                       fmt.Printf("There are %d items in total,%d of them are 
consistent,%d of them are inconsistent\n",
+                               rs.Data.Total, rs.Data.ConsistentCount, 
rs.Data.InconsistentCount)
+
+                       printResult("ssls", rs.Data.Items.SSLs)
+
+                       printResult("routes", rs.Data.Items.Routes)
+
+                       printResult("scripts", rs.Data.Items.Scripts)
+
+                       printResult("services", rs.Data.Items.Services)
+
+                       printResult("upstreams", rs.Data.Items.Upstreams)
+
+                       printResult("consumers", rs.Data.Items.Consumers)
+
+                       printResult("server infos", rs.Data.Items.ServerInfos)
+
+                       printResult("global plugins", 
rs.Data.Items.GlobalPlugins)
+
+                       printResult("plugin configs", 
rs.Data.Items.PluginConfigs)
+               },
+       }
+}
+
+func getToken() string {
+       account := map[string]string{
+               "username": "admin",
+               "password": "admin",

Review comment:
       Do not write a fixed string here, read the configuration or let the user 
enter it by himself
   




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to