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]
