diff --git a/.licenserc.yaml b/.licenserc.yaml index 23ccfb517..4d34399c8 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -26,7 +26,7 @@ header: - 'VERSION' - 'tools/' - 'test/README.md' - - 'pkg/cmd/hgctl/testdata/config' + - 'cmd/hgctl/config/testdata/config' - 'pkg/cmd/hgctl/manifests' comment: on-failure diff --git a/pkg/cmd/hgctl/config_retriever.go b/cmd/hgctl/config/gateway_config.go similarity index 50% rename from pkg/cmd/hgctl/config_retriever.go rename to cmd/hgctl/config/gateway_config.go index d0e8760b0..6a4fd4be9 100644 --- a/pkg/cmd/hgctl/config_retriever.go +++ b/cmd/hgctl/config/gateway_config.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package hgctl +package config import ( "encoding/json" @@ -27,26 +27,61 @@ import ( ) var ( - output string - podName string - podNamespace string + BootstrapEnvoyConfigType EnvoyConfigType = "bootstrap" + ClusterEnvoyConfigType EnvoyConfigType = "cluster" + EndpointEnvoyConfigType EnvoyConfigType = "endpoint" + ListenerEnvoyConfigType EnvoyConfigType = "listener" + RouteEnvoyConfigType EnvoyConfigType = "route" + AllEnvoyConfigType EnvoyConfigType = "all" ) const ( defaultProxyAdminPort = 15000 - containerName = "envoy" ) -func retrieveConfigDump(args []string, includeEds bool) ([]byte, error) { - if len(args) != 0 { - podName = args[0] - } +type EnvoyConfigType string +type GetEnvoyConfigOptions struct { + IncludeEds bool + PodName string + PodNamespace string + BindAddress string + Output string + EnvoyConfigType EnvoyConfigType +} + +func NewDefaultGetEnvoyConfigOptions() *GetEnvoyConfigOptions { + return &GetEnvoyConfigOptions{ + IncludeEds: true, + PodName: "", + PodNamespace: "higress-system", + BindAddress: "localhost", + Output: "json", + EnvoyConfigType: AllEnvoyConfigType, + } +} + +func GetEnvoyConfig(config *GetEnvoyConfigOptions) ([]byte, error) { + configDump, err := retrieveConfigDump(config.PodName, config.PodNamespace, config.BindAddress, config.IncludeEds) + if err != nil { + return nil, err + } + if config.EnvoyConfigType == AllEnvoyConfigType { + return configDump, nil + } + resource, err := getXDSResource(config.EnvoyConfigType, configDump) + if err != nil { + return nil, err + } + return formatGatewayConfig(resource, config.Output) +} + +func retrieveConfigDump(podName, podNamespace, bindAddress string, includeEds bool) ([]byte, error) { if podNamespace == "" { return nil, fmt.Errorf("pod namespace is required") } - if podName == "" || len(args) == 0 { + if podName == "" { c, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader()) if err != nil { return nil, fmt.Errorf("failed to build kubernetes client: %w", err) @@ -65,7 +100,7 @@ func retrieveConfigDump(args []string, includeEds bool) ([]byte, error) { fw, err := portForwarder(types.NamespacedName{ Namespace: podNamespace, Name: podName, - }) + }, bindAddress) if err != nil { return nil, err } @@ -82,7 +117,7 @@ func retrieveConfigDump(args []string, includeEds bool) ([]byte, error) { return configDump, nil } -func portForwarder(nn types.NamespacedName) (kubernetes.PortForwarder, error) { +func portForwarder(nn types.NamespacedName, bindAddress string) (kubernetes.PortForwarder, error) { c, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader()) if err != nil { return nil, fmt.Errorf("build CLI client fail: %w", err) @@ -149,3 +184,53 @@ func configDumpRequest(address string, includeEds bool) ([]byte, error) { return io.ReadAll(resp.Body) } + +func getXDSResource(resourceType EnvoyConfigType, configDump []byte) (any, error) { + cd := map[string]any{} + if err := json.Unmarshal(configDump, &cd); err != nil { + return nil, err + } + if resourceType == AllEnvoyConfigType { + return cd, nil + } + configs := cd["configs"] + globalConfigs := configs.([]any) + + switch resourceType { + case BootstrapEnvoyConfigType: + for _, config := range globalConfigs { + if config.(map[string]interface{})["@type"] == "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump" { + return config, nil + } + } + case EndpointEnvoyConfigType: + for _, config := range globalConfigs { + if config.(map[string]interface{})["@type"] == "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump" { + return config, nil + } + } + + case ClusterEnvoyConfigType: + for _, config := range globalConfigs { + if config.(map[string]interface{})["@type"] == "type.googleapis.com/envoy.admin.v3.ClustersConfigDump" { + return config, nil + } + } + case ListenerEnvoyConfigType: + for _, config := range globalConfigs { + if config.(map[string]interface{})["@type"] == "type.googleapis.com/envoy.admin.v3.ListenersConfigDump" { + return config, nil + } + } + case RouteEnvoyConfigType: + for _, config := range globalConfigs { + if config.(map[string]interface{})["@type"] == "type.googleapis.com/envoy.admin.v3.RoutesConfigDump" { + return config, nil + } + } + default: + return nil, fmt.Errorf("unknown resourceType %s", resourceType) + } + + return nil, fmt.Errorf("unknown resourceType %s", resourceType) +} diff --git a/pkg/cmd/hgctl/config_test.go b/cmd/hgctl/config/gateway_config_test.go similarity index 96% rename from pkg/cmd/hgctl/config_test.go rename to cmd/hgctl/config/gateway_config_test.go index 9a515af7e..104b22d40 100644 --- a/pkg/cmd/hgctl/config_test.go +++ b/cmd/hgctl/config/gateway_config_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package hgctl +package config import ( "fmt" @@ -109,7 +109,7 @@ func TestExtractAllConfigDump(t *testing.T) { t.Run(tc.output, func(t *testing.T) { configDump, err := fetchGatewayConfig(fw, true) assert.NoError(t, err) - data, err := GetXDSResource(AllEnvoyConfigType, configDump) + data, err := getXDSResource(AllEnvoyConfigType, configDump) assert.NoError(t, err) got, err := formatGatewayConfig(data, tc.output) assert.NoError(t, err) @@ -137,7 +137,7 @@ func TestExtractSubResourcesConfigDump(t *testing.T) { cases := []struct { output string expected string - resourceType envoyConfigType + resourceType EnvoyConfigType }{ { output: "json", @@ -192,7 +192,7 @@ func TestExtractSubResourcesConfigDump(t *testing.T) { t.Run(tc.output, func(t *testing.T) { configDump, err := fetchGatewayConfig(fw, false) assert.NoError(t, err) - resource, err := GetXDSResource(tc.resourceType, configDump) + resource, err := getXDSResource(tc.resourceType, configDump) assert.NoError(t, err) got, err := formatGatewayConfig(resource, tc.output) assert.NoError(t, err) diff --git a/pkg/cmd/hgctl/testdata/config/input/in.all.json b/cmd/hgctl/config/testdata/config/input/in.all.json similarity index 100% rename from pkg/cmd/hgctl/testdata/config/input/in.all.json rename to cmd/hgctl/config/testdata/config/input/in.all.json diff --git a/pkg/cmd/hgctl/testdata/config/output/out.all.json b/cmd/hgctl/config/testdata/config/output/out.all.json similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.all.json rename to cmd/hgctl/config/testdata/config/output/out.all.json diff --git a/pkg/cmd/hgctl/testdata/config/output/out.all.yaml b/cmd/hgctl/config/testdata/config/output/out.all.yaml similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.all.yaml rename to cmd/hgctl/config/testdata/config/output/out.all.yaml diff --git a/pkg/cmd/hgctl/testdata/config/output/out.bootstrap.json b/cmd/hgctl/config/testdata/config/output/out.bootstrap.json similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.bootstrap.json rename to cmd/hgctl/config/testdata/config/output/out.bootstrap.json diff --git a/pkg/cmd/hgctl/testdata/config/output/out.bootstrap.yaml b/cmd/hgctl/config/testdata/config/output/out.bootstrap.yaml similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.bootstrap.yaml rename to cmd/hgctl/config/testdata/config/output/out.bootstrap.yaml diff --git a/pkg/cmd/hgctl/testdata/config/output/out.cluster.json b/cmd/hgctl/config/testdata/config/output/out.cluster.json similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.cluster.json rename to cmd/hgctl/config/testdata/config/output/out.cluster.json diff --git a/pkg/cmd/hgctl/testdata/config/output/out.cluster.yaml b/cmd/hgctl/config/testdata/config/output/out.cluster.yaml similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.cluster.yaml rename to cmd/hgctl/config/testdata/config/output/out.cluster.yaml diff --git a/pkg/cmd/hgctl/testdata/config/output/out.endpoints.json b/cmd/hgctl/config/testdata/config/output/out.endpoints.json similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.endpoints.json rename to cmd/hgctl/config/testdata/config/output/out.endpoints.json diff --git a/pkg/cmd/hgctl/testdata/config/output/out.endpoints.yaml b/cmd/hgctl/config/testdata/config/output/out.endpoints.yaml similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.endpoints.yaml rename to cmd/hgctl/config/testdata/config/output/out.endpoints.yaml diff --git a/pkg/cmd/hgctl/testdata/config/output/out.listener.json b/cmd/hgctl/config/testdata/config/output/out.listener.json similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.listener.json rename to cmd/hgctl/config/testdata/config/output/out.listener.json diff --git a/pkg/cmd/hgctl/testdata/config/output/out.listener.yaml b/cmd/hgctl/config/testdata/config/output/out.listener.yaml similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.listener.yaml rename to cmd/hgctl/config/testdata/config/output/out.listener.yaml diff --git a/pkg/cmd/hgctl/testdata/config/output/out.route.json b/cmd/hgctl/config/testdata/config/output/out.route.json similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.route.json rename to cmd/hgctl/config/testdata/config/output/out.route.json diff --git a/pkg/cmd/hgctl/testdata/config/output/out.route.yaml b/cmd/hgctl/config/testdata/config/output/out.route.yaml similarity index 100% rename from pkg/cmd/hgctl/testdata/config/output/out.route.yaml rename to cmd/hgctl/config/testdata/config/output/out.route.yaml diff --git a/go.mod b/go.mod index 016b4160d..4ad93de7f 100644 --- a/go.mod +++ b/go.mod @@ -238,6 +238,8 @@ require ( github.com/spf13/cast v1.3.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/tonistiigi/fsutil v0.0.0-20220930225714-4638ad635be5 // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect @@ -302,6 +304,7 @@ require ( github.com/evanphx/json-patch/v5 v5.6.0 github.com/google/yamlfmt v0.10.0 github.com/kylelemons/godebug v1.1.0 + github.com/tidwall/gjson v1.17.0 helm.sh/helm/v3 v3.7.1 k8s.io/apiextensions-apiserver v0.25.4 knative.dev/networking v0.0.0-20220302134042-e8b2eb995165 diff --git a/go.sum b/go.sum index 3b844b0e7..c60871db1 100644 --- a/go.sum +++ b/go.sum @@ -1582,6 +1582,12 @@ github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIU github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= diff --git a/pkg/cmd/hgctl/config_bootstrap.go b/pkg/cmd/hgctl/config_bootstrap.go index 23111fe64..b5835b37b 100644 --- a/pkg/cmd/hgctl/config_bootstrap.go +++ b/pkg/cmd/hgctl/config_bootstrap.go @@ -17,6 +17,7 @@ package hgctl import ( "fmt" + "github.com/alibaba/higress/cmd/hgctl/config" "github.com/spf13/cobra" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) @@ -45,21 +46,20 @@ func bootstrapConfigCmd() *cobra.Command { } func runBootstrapConfig(c *cobra.Command, args []string) error { - configDump, err := retrieveConfigDump(args, false) + if len(args) != 0 { + podName = args[0] + } + envoyConfig, err := config.GetEnvoyConfig(&config.GetEnvoyConfigOptions{ + PodName: podName, + PodNamespace: podNamespace, + BindAddress: bindAddress, + Output: output, + EnvoyConfigType: config.BootstrapEnvoyConfigType, + IncludeEds: true, + }) if err != nil { return err } - - bootstrap, err := GetXDSResource(BootstrapEnvoyConfigType, configDump) - if err != nil { - return err - } - - out, err := formatGatewayConfig(bootstrap, output) - if err != nil { - return err - } - - _, err = fmt.Fprintln(c.OutOrStdout(), string(out)) + _, err = fmt.Fprintln(c.OutOrStdout(), string(envoyConfig)) return err } diff --git a/pkg/cmd/hgctl/config_cluster.go b/pkg/cmd/hgctl/config_cluster.go index 3df8fb048..2a8eab256 100644 --- a/pkg/cmd/hgctl/config_cluster.go +++ b/pkg/cmd/hgctl/config_cluster.go @@ -11,11 +11,13 @@ // 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 hgctl import ( "fmt" + "github.com/alibaba/higress/cmd/hgctl/config" "github.com/spf13/cobra" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) @@ -44,21 +46,20 @@ func clusterConfigCmd() *cobra.Command { } func runClusterConfig(c *cobra.Command, args []string) error { - configDump, err := retrieveConfigDump(args, false) + if len(args) != 0 { + podName = args[0] + } + envoyConfig, err := config.GetEnvoyConfig(&config.GetEnvoyConfigOptions{ + PodName: podName, + PodNamespace: podNamespace, + BindAddress: bindAddress, + Output: output, + EnvoyConfigType: config.ClusterEnvoyConfigType, + IncludeEds: true, + }) if err != nil { return err } - - cluster, err := GetXDSResource(ClusterEnvoyConfigType, configDump) - if err != nil { - return err - } - - out, err := formatGatewayConfig(cluster, output) - if err != nil { - return err - } - - _, err = fmt.Fprintln(c.OutOrStdout(), string(out)) + _, err = fmt.Fprintln(c.OutOrStdout(), string(envoyConfig)) return err } diff --git a/pkg/cmd/hgctl/config_cmd.go b/pkg/cmd/hgctl/config_cmd.go index 007554644..19f71078f 100644 --- a/pkg/cmd/hgctl/config_cmd.go +++ b/pkg/cmd/hgctl/config_cmd.go @@ -17,11 +17,23 @@ package hgctl import ( "fmt" + "github.com/alibaba/higress/cmd/hgctl/config" "github.com/alibaba/higress/pkg/cmd/options" "github.com/spf13/cobra" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) +var ( + output string + podName string + podNamespace string +) + +const ( + defaultProxyAdminPort = 15000 + containerName = "envoy" +) + func newConfigCommand() *cobra.Command { cfgCommand := &cobra.Command{ Use: "gateway-config", @@ -69,11 +81,20 @@ func allConfigCmd() *cobra.Command { } func runAllConfig(c *cobra.Command, args []string) error { - configDump, err := retrieveConfigDump(args, true) + if len(args) != 0 { + podName = args[0] + } + envoyConfig, err := config.GetEnvoyConfig(&config.GetEnvoyConfigOptions{ + PodName: podName, + PodNamespace: podNamespace, + BindAddress: bindAddress, + Output: output, + EnvoyConfigType: config.AllEnvoyConfigType, + IncludeEds: true, + }) if err != nil { return err } - - _, err = fmt.Fprintln(c.OutOrStdout(), string(configDump)) + _, err = fmt.Fprintln(c.OutOrStdout(), string(envoyConfig)) return err } diff --git a/pkg/cmd/hgctl/config_endpoint.go b/pkg/cmd/hgctl/config_endpoint.go index 80666e878..010d16192 100644 --- a/pkg/cmd/hgctl/config_endpoint.go +++ b/pkg/cmd/hgctl/config_endpoint.go @@ -17,6 +17,7 @@ package hgctl import ( "fmt" + "github.com/alibaba/higress/cmd/hgctl/config" "github.com/spf13/cobra" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) @@ -45,21 +46,20 @@ func endpointConfigCmd() *cobra.Command { } func runEndpointConfig(c *cobra.Command, args []string) error { - configDump, err := retrieveConfigDump(args, true) + if len(args) != 0 { + podName = args[0] + } + envoyConfig, err := config.GetEnvoyConfig(&config.GetEnvoyConfigOptions{ + PodName: podName, + PodNamespace: podNamespace, + BindAddress: bindAddress, + Output: output, + EnvoyConfigType: config.EndpointEnvoyConfigType, + IncludeEds: true, + }) if err != nil { return err } - - endpoint, err := GetXDSResource(EndpointEnvoyConfigType, configDump) - if err != nil { - return err - } - - out, err := formatGatewayConfig(endpoint, output) - if err != nil { - return err - } - - _, err = fmt.Fprintln(c.OutOrStdout(), string(out)) + _, err = fmt.Fprintln(c.OutOrStdout(), string(envoyConfig)) return err } diff --git a/pkg/cmd/hgctl/config_listener.go b/pkg/cmd/hgctl/config_listener.go index 22978fdec..143875fd2 100644 --- a/pkg/cmd/hgctl/config_listener.go +++ b/pkg/cmd/hgctl/config_listener.go @@ -17,6 +17,7 @@ package hgctl import ( "fmt" + "github.com/alibaba/higress/cmd/hgctl/config" "github.com/spf13/cobra" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) @@ -45,21 +46,20 @@ func listenerConfigCmd() *cobra.Command { } func runListenerConfig(c *cobra.Command, args []string) error { - configDump, err := retrieveConfigDump(args, false) + if len(args) != 0 { + podName = args[0] + } + envoyConfig, err := config.GetEnvoyConfig(&config.GetEnvoyConfigOptions{ + PodName: podName, + PodNamespace: podNamespace, + BindAddress: bindAddress, + Output: output, + EnvoyConfigType: config.ListenerEnvoyConfigType, + IncludeEds: true, + }) if err != nil { return err } - - listener, err := GetXDSResource(ListenerEnvoyConfigType, configDump) - if err != nil { - return err - } - - out, err := formatGatewayConfig(listener, output) - if err != nil { - return err - } - - _, err = fmt.Fprintln(c.OutOrStdout(), string(out)) + _, err = fmt.Fprintln(c.OutOrStdout(), string(envoyConfig)) return err } diff --git a/pkg/cmd/hgctl/config_route.go b/pkg/cmd/hgctl/config_route.go index 15f8bfc5d..66be5a11e 100644 --- a/pkg/cmd/hgctl/config_route.go +++ b/pkg/cmd/hgctl/config_route.go @@ -17,6 +17,7 @@ package hgctl import ( "fmt" + "github.com/alibaba/higress/cmd/hgctl/config" "github.com/spf13/cobra" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) @@ -45,21 +46,20 @@ func routeConfigCmd() *cobra.Command { } func runRouteConfig(c *cobra.Command, args []string) error { - configDump, err := retrieveConfigDump(args, false) + if len(args) != 0 { + podName = args[0] + } + envoyConfig, err := config.GetEnvoyConfig(&config.GetEnvoyConfigOptions{ + PodName: podName, + PodNamespace: podNamespace, + BindAddress: bindAddress, + Output: output, + EnvoyConfigType: config.RouteEnvoyConfigType, + IncludeEds: true, + }) if err != nil { return err } - - route, err := GetXDSResource(RouteEnvoyConfigType, configDump) - if err != nil { - return err - } - - out, err := formatGatewayConfig(route, output) - if err != nil { - return err - } - - _, err = fmt.Fprintln(c.OutOrStdout(), string(out)) + _, err = fmt.Fprintln(c.OutOrStdout(), string(envoyConfig)) return err } diff --git a/test/e2e/conformance/tests/configmap-gzip.go b/test/e2e/conformance/tests/configmap-gzip.go index 3ebeaf116..36caea2a6 100644 --- a/test/e2e/conformance/tests/configmap-gzip.go +++ b/test/e2e/conformance/tests/configmap-gzip.go @@ -18,6 +18,7 @@ import ( "testing" "github.com/alibaba/higress/pkg/ingress/kube/configmap" + "github.com/alibaba/higress/test/e2e/conformance/utils/envoy" "github.com/alibaba/higress/test/e2e/conformance/utils/http" "github.com/alibaba/higress/test/e2e/conformance/utils/kubernetes" "github.com/alibaba/higress/test/e2e/conformance/utils/suite" @@ -25,6 +26,266 @@ import ( func init() { Register(ConfigmapGzip) + Register(ConfigMapGzipEnvoy) +} + +var testCases = []struct { + higressConfig *configmap.HigressConfig + envoyAssertion envoy.Assertion + httpAssert http.Assertion +}{ + { + higressConfig: &configmap.HigressConfig{ + Gzip: &configmap.Gzip{ + Enable: false, + MinContentLength: 1024, + ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"}, + DisableOnEtagHeader: true, + MemoryLevel: 5, + WindowBits: 12, + ChunkSize: 4096, + CompressionLevel: "BEST_COMPRESSION", + CompressionStrategy: "DEFAULT_STRATEGY", + }, + }, + httpAssert: http.Assertion{ + Meta: http.AssertionMeta{ + TestCaseName: "case1: disable gzip output", + TargetBackend: "web-backend", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Method: "GET", + Headers: map[string]string{ + "Accept-Encoding": "*", + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponseNoRequest: true, + ExpectedResponse: http.Response{ + StatusCode: 200, + AbsentHeaders: []string{"content-encoding"}, + }, + }, + }, + envoyAssertion: envoy.Assertion{ + Path: "configs.#.dynamic_listeners.#.active_state.listener.filter_chains", + TargetNamespace: "higress-system", + CheckType: envoy.CheckTypeNotExist, + ExpectEnvoyConfig: map[string]interface{}{ + "memory_level": 5, + "compression_level": "COMPRESSION_LEVEL_9", + "window_bits": 12, + "min_content_length": 1024, + "disable_on_etag_header": true, + "content_type": []interface{}{ + "text/html", + "text/css", + "text/plain", + "text/xml", + "application/json", + "application/javascript", + "application/xhtml+xml", + "image/svg+xml", + }, + }, + }, + }, + { + higressConfig: &configmap.HigressConfig{ + Gzip: &configmap.Gzip{ + Enable: true, + MinContentLength: 100, + ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"}, + DisableOnEtagHeader: true, + MemoryLevel: 5, + WindowBits: 12, + ChunkSize: 4096, + CompressionLevel: "BEST_COMPRESSION", + CompressionStrategy: "DEFAULT_STRATEGY", + }, + }, + httpAssert: http.Assertion{ + Meta: http.AssertionMeta{ + TestCaseName: "case2: enable gzip output", + TargetBackend: "web-backend", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Method: "GET", + Headers: map[string]string{ + "Accept-Encoding": "*", + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponseNoRequest: true, + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + AdditionalResponseHeaders: map[string]string{"content-encoding": "gzip"}, + }, + }, + envoyAssertion: envoy.Assertion{ + Path: "configs.#.dynamic_listeners.#.active_state.listener.filter_chains", + TargetNamespace: "higress-system", + CheckType: envoy.CheckTypeExist, + ExpectEnvoyConfig: map[string]interface{}{ + "name": "envoy.filters.network.http_connection_manager", + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "stat_prefix": "outbound_0.0.0.0_80", + "memory_level": 5, + "compression_level": "COMPRESSION_LEVEL_9", + "window_bits": 12, + "min_content_length": 100, + "disable_on_etag_header": true, + "content_type": []interface{}{ + "text/html", + "text/css", + "text/plain", + "text/xml", + "application/json", + "application/javascript", + "application/xhtml+xml", + "image/svg+xml", + }, + }, + }, + }, + { + higressConfig: &configmap.HigressConfig{ + Gzip: &configmap.Gzip{ + Enable: true, + MinContentLength: 4096, + ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"}, + DisableOnEtagHeader: true, + MemoryLevel: 5, + WindowBits: 12, + ChunkSize: 4096, + CompressionLevel: "BEST_COMPRESSION", + CompressionStrategy: "DEFAULT_STRATEGY", + }, + }, + httpAssert: http.Assertion{ + Meta: http.AssertionMeta{ + TestCaseName: "case3: disable gzip output because content length less hhan 4096 ", + TargetBackend: "web-backend", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Method: "GET", + Headers: map[string]string{ + "Accept-Encoding": "*", + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponseNoRequest: true, + ExpectedResponse: http.Response{ + StatusCode: 200, + AbsentHeaders: []string{"content-encoding"}, + }, + }, + }, + envoyAssertion: envoy.Assertion{ + Path: "configs.#.dynamic_listeners.#.active_state.listener.filter_chains", + TargetNamespace: "higress-system", + CheckType: envoy.CheckTypeExist, + ExpectEnvoyConfig: map[string]interface{}{ + "name": "envoy.filters.network.http_connection_manager", + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "stat_prefix": "outbound_0.0.0.0_80", + "memory_level": 5, + "compression_level": "COMPRESSION_LEVEL_9", + "window_bits": 12, + "min_content_length": 4096, + "disable_on_etag_header": true, + "content_type": []interface{}{ + "text/html", + "text/css", + "text/plain", + "text/xml", + "application/json", + "application/javascript", + "application/xhtml+xml", + "image/svg+xml", + }, + }, + }, + }, + { + higressConfig: &configmap.HigressConfig{ + Gzip: &configmap.Gzip{ + Enable: true, + MinContentLength: 100, + ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/javascript", "application/xhtml+xml", "image/svg+xml"}, + DisableOnEtagHeader: true, + MemoryLevel: 5, + WindowBits: 12, + ChunkSize: 4096, + CompressionLevel: "BEST_COMPRESSION", + CompressionStrategy: "DEFAULT_STRATEGY", + }, + }, + httpAssert: http.Assertion{ + Meta: http.AssertionMeta{ + TestCaseName: "case4: disable gzip output because application/json missed in content types ", + TargetBackend: "web-backend", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/foo", + Method: "GET", + Headers: map[string]string{ + "Accept-Encoding": "*", + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponseNoRequest: true, + ExpectedResponse: http.Response{ + StatusCode: 200, + AbsentHeaders: []string{"content-encoding"}, + }, + }, + }, + envoyAssertion: envoy.Assertion{ + Path: "configs.#.dynamic_listeners.#.active_state.listener.filter_chains", + TargetNamespace: "higress-system", + CheckType: envoy.CheckTypeExist, + ExpectEnvoyConfig: map[string]interface{}{ + "name": "envoy.filters.network.http_connection_manager", + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "stat_prefix": "outbound_0.0.0.0_80", + "memory_level": 5, + "compression_level": "COMPRESSION_LEVEL_9", + "window_bits": 12, + "min_content_length": 100, + "disable_on_etag_header": true, + "content_type": []interface{}{ + "text/html", + "text/css", + "text/plain", + "text/xml", + "application/javascript", + "application/xhtml+xml", + "image/svg+xml", + }, + }, + }, + }, } var ConfigmapGzip = suite.ConformanceTest{ @@ -33,171 +294,9 @@ var ConfigmapGzip = suite.ConformanceTest{ Manifests: []string{"tests/configmap-gzip.yaml"}, Features: []suite.SupportedFeature{suite.HTTPConformanceFeature}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { - testcases := []struct { - higressConfig *configmap.HigressConfig - httpAssert http.Assertion - }{ - { - higressConfig: &configmap.HigressConfig{ - Gzip: &configmap.Gzip{ - Enable: false, - MinContentLength: 1024, - ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"}, - DisableOnEtagHeader: true, - MemoryLevel: 5, - WindowBits: 12, - ChunkSize: 4096, - CompressionLevel: "BEST_COMPRESSION", - CompressionStrategy: "DEFAULT_STRATEGY", - }, - }, - httpAssert: http.Assertion{ - Meta: http.AssertionMeta{ - TestCaseName: "case1: disable gzip output", - TargetBackend: "web-backend", - TargetNamespace: "higress-conformance-infra", - }, - Request: http.AssertionRequest{ - ActualRequest: http.Request{ - Host: "foo.com", - Path: "/foo", - Method: "GET", - Headers: map[string]string{ - "Accept-Encoding": "*", - }, - }, - }, - Response: http.AssertionResponse{ - ExpectedResponseNoRequest: true, - ExpectedResponse: http.Response{ - StatusCode: 200, - AbsentHeaders: []string{"content-encoding"}, - }, - }, - }, - }, - { - higressConfig: &configmap.HigressConfig{ - Gzip: &configmap.Gzip{ - Enable: true, - MinContentLength: 100, - ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"}, - DisableOnEtagHeader: true, - MemoryLevel: 5, - WindowBits: 12, - ChunkSize: 4096, - CompressionLevel: "BEST_COMPRESSION", - CompressionStrategy: "DEFAULT_STRATEGY", - }, - }, - httpAssert: http.Assertion{ - Meta: http.AssertionMeta{ - TestCaseName: "case2: enable gzip output", - TargetBackend: "web-backend", - TargetNamespace: "higress-conformance-infra", - }, - Request: http.AssertionRequest{ - ActualRequest: http.Request{ - Host: "foo.com", - Path: "/foo", - Method: "GET", - Headers: map[string]string{ - "Accept-Encoding": "*", - }, - }, - }, - Response: http.AssertionResponse{ - ExpectedResponseNoRequest: true, - ExpectedResponse: http.Response{ - StatusCode: 200, - }, - AdditionalResponseHeaders: map[string]string{"content-encoding": "gzip"}, - }, - }, - }, - { - higressConfig: &configmap.HigressConfig{ - Gzip: &configmap.Gzip{ - Enable: true, - MinContentLength: 4096, - ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"}, - DisableOnEtagHeader: true, - MemoryLevel: 5, - WindowBits: 12, - ChunkSize: 4096, - CompressionLevel: "BEST_COMPRESSION", - CompressionStrategy: "DEFAULT_STRATEGY", - }, - }, - httpAssert: http.Assertion{ - Meta: http.AssertionMeta{ - TestCaseName: "case3: disable gzip output because content length less hhan 4096 ", - TargetBackend: "web-backend", - TargetNamespace: "higress-conformance-infra", - }, - Request: http.AssertionRequest{ - ActualRequest: http.Request{ - Host: "foo.com", - Path: "/foo", - Method: "GET", - Headers: map[string]string{ - "Accept-Encoding": "*", - }, - }, - }, - Response: http.AssertionResponse{ - ExpectedResponseNoRequest: true, - ExpectedResponse: http.Response{ - StatusCode: 200, - AbsentHeaders: []string{"content-encoding"}, - }, - }, - }, - }, - { - higressConfig: &configmap.HigressConfig{ - Gzip: &configmap.Gzip{ - Enable: true, - MinContentLength: 100, - ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/javascript", "application/xhtml+xml", "image/svg+xml"}, - DisableOnEtagHeader: true, - MemoryLevel: 5, - WindowBits: 12, - ChunkSize: 4096, - CompressionLevel: "BEST_COMPRESSION", - CompressionStrategy: "DEFAULT_STRATEGY", - }, - }, - httpAssert: http.Assertion{ - Meta: http.AssertionMeta{ - TestCaseName: "case4: disable gzip output because application/json missed in content types ", - TargetBackend: "web-backend", - TargetNamespace: "higress-conformance-infra", - }, - Request: http.AssertionRequest{ - ActualRequest: http.Request{ - Host: "foo.com", - Path: "/foo", - Method: "GET", - Headers: map[string]string{ - "Accept-Encoding": "*", - }, - }, - }, - Response: http.AssertionResponse{ - ExpectedResponseNoRequest: true, - ExpectedResponse: http.Response{ - StatusCode: 200, - AbsentHeaders: []string{"content-encoding"}, - }, - }, - }, - }, - } - t.Run("Configmap Gzip", func(t *testing.T) { - for _, testcase := range testcases { - err := kubernetes.ApplyConfigmapDataWithYaml(suite.Client, "higress-system", "higress-config", "higress", testcase.higressConfig) + for _, testcase := range testCases { + err := kubernetes.ApplyConfigmapDataWithYaml(t, suite.Client, "higress-system", "higress-config", "higress", testcase.higressConfig) if err != nil { t.Fatalf("can't apply conifgmap %s in namespace %s for data key %s", "higress-config", "higress-system", "higress") } @@ -206,3 +305,22 @@ var ConfigmapGzip = suite.ConformanceTest{ }) }, } + +var ConfigMapGzipEnvoy = suite.ConformanceTest{ + ShortName: "ConfigMapGzipEnvoy", + Description: "The Envoy config should contain gzip config", + Manifests: []string{"tests/configmap-gzip.yaml"}, + Features: []suite.SupportedFeature{suite.EnvoyConfigConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("ConfigMap Gzip Envoy", func(t *testing.T) { + for _, testcase := range testCases { + // apply config + err := kubernetes.ApplyConfigmapDataWithYaml(t, suite.Client, "higress-system", "higress-config", "higress", testcase.higressConfig) + if err != nil { + t.Fatalf("can't apply conifgmap %s in namespace %s for data key %s", "higress-config", "higress-system", "higress") + } + envoy.AssertEnvoyConfig(t, suite.TimeoutConfig, testcase.envoyAssertion) + } + }) + }, +} diff --git a/test/e2e/conformance/tests/configmap-gzip.yaml b/test/e2e/conformance/tests/configmap-gzip.yaml index 12b1ceb34..db9036ef9 100644 --- a/test/e2e/conformance/tests/configmap-gzip.yaml +++ b/test/e2e/conformance/tests/configmap-gzip.yaml @@ -29,4 +29,5 @@ spec: service: name: infra-backend-v3 port: - number: 8080 \ No newline at end of file + number: 8080 + diff --git a/test/e2e/conformance/utils/envoy/envoy.go b/test/e2e/conformance/utils/envoy/envoy.go new file mode 100644 index 000000000..3c219f33b --- /dev/null +++ b/test/e2e/conformance/utils/envoy/envoy.go @@ -0,0 +1,287 @@ +/* +Copyright 2022 The Kubernetes Authors. +Licensed 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 envoy + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/alibaba/higress/cmd/hgctl/config" + cfg "github.com/alibaba/higress/test/e2e/conformance/utils/config" + "github.com/tidwall/gjson" + "k8s.io/apimachinery/pkg/util/wait" +) + +type CheckType string + +const ( + // CheckTypeMatch checks if the actual value matches the expected value. + CheckTypeMatch CheckType = "match" + // CheckTypeExist checks if the actual value exists. + CheckTypeExist CheckType = "exist" + // CheckTypeNotExist checks if the actual value does not exist. + CheckTypeNotExist CheckType = "notexist" + + // defaultSuccessThreshold is the default number of times the assertion must succeed in a row. + defaultSuccessThreshold = 3 +) + +// Assertion defines the assertion to be made on the Envoy config. +// TODO: It can support localization judgment so that this configuration check function will be more universal. +// TODO: Can be used for general e2e tests, rather than just envoy filter scenarios. +type Assertion struct { + // Path is the path of gjson to the value to be asserted. + Path string + // CheckType is the type of assertion to be made. + CheckType CheckType + // ExpectEnvoyConfig is the expected value of the Envoy config. + ExpectEnvoyConfig map[string]interface{} + // TargetNamespace is the namespace of the Envoy pod. + TargetNamespace string +} + +// AssertEnvoyConfig asserts the Envoy config. +func AssertEnvoyConfig(t *testing.T, timeoutConfig cfg.TimeoutConfig, expected Assertion) { + options := config.NewDefaultGetEnvoyConfigOptions() + options.PodNamespace = expected.TargetNamespace + waitForEnvoyConfig(t, timeoutConfig, options, expected) +} + +// waitForEnvoyConfig waits for the Envoy config to be ready and asserts it. +func waitForEnvoyConfig(t *testing.T, timeoutConfig cfg.TimeoutConfig, options *config.GetEnvoyConfigOptions, expected Assertion) { + awaitConvergence(t, defaultSuccessThreshold, timeoutConfig.MaxTimeToConsistency, func(elapsed time.Duration) bool { + allEnvoyConfig := "" + err := wait.Poll(1*time.Second, 10*time.Second, func() (bool, error) { + out, err := config.GetEnvoyConfig(options) + if err != nil { + return false, err + } + allEnvoyConfig = string(out) + return true, nil + }) + if err != nil { + return false + } + switch expected.CheckType { + case CheckTypeMatch: + err = assertEnvoyConfigMatch(t, allEnvoyConfig, expected) + case CheckTypeExist: + err = assertEnvoyConfigExist(t, allEnvoyConfig, expected) + case CheckTypeNotExist: + err = assertEnvoyConfigNotExist(t, allEnvoyConfig, expected) + default: + err = fmt.Errorf("unsupported check type %s", expected.CheckType) + } + if err != nil { + return false + } + return true + }) + t.Logf("✅ Envoy config checked") +} + +// assertEnvoyConfigNotExist asserts the Envoy config does not exist. +func assertEnvoyConfigNotExist(t *testing.T, envoyConfig string, expected Assertion) error { + result := gjson.Get(envoyConfig, expected.Path).Value() + if result == nil { + return nil + } + if !findMustNotExist(t, result, expected.ExpectEnvoyConfig) { + return fmt.Errorf("the expected value %s exists in path '%s'", expected.ExpectEnvoyConfig, expected.Path) + } + return nil +} + +// assertEnvoyConfigExist asserts the Envoy config exists. +func assertEnvoyConfigExist(t *testing.T, envoyConfig string, expected Assertion) error { + result := gjson.Get(envoyConfig, expected.Path).Value() + if result == nil { + return fmt.Errorf("failed to get value from path '%s'", expected.Path) + } + if !findMustExist(t, result, expected.ExpectEnvoyConfig) { + return fmt.Errorf("the expected value %s does not exist in path '%s'", expected.ExpectEnvoyConfig, expected.Path) + } + return nil +} + +// assertEnvoyConfigMatch asserts the Envoy config matches the expected value. +func assertEnvoyConfigMatch(t *testing.T, envoyConfig string, expected Assertion) error { + result := gjson.Get(envoyConfig, expected.Path).Value() + if result == nil { + return fmt.Errorf("failed to get value from path '%s'", expected.Path) + } + if !match(t, result, expected.ExpectEnvoyConfig) { + return fmt.Errorf("failed to match value from path '%s'", expected.Path) + } + t.Logf("✅ Matched value %s in path '%s'", expected.ExpectEnvoyConfig, expected.Path) + return nil +} + +// awaitConvergence runs the given function until it returns 'true' `threshold` times in a row. +// Each failed attempt has a 1s delay; successful attempts have no delay. +func awaitConvergence(t *testing.T, threshold int, maxTimeToConsistency time.Duration, fn func(elapsed time.Duration) bool) { + successes := 0 + attempts := 0 + start := time.Now() + to := time.After(maxTimeToConsistency) + delay := time.Second + for { + select { + case <-to: + t.Fatalf("timeout while waiting after %d attempts", attempts) + default: + } + + completed := fn(time.Now().Sub(start)) + attempts++ + if completed { + successes++ + if successes >= threshold { + return + } + // Skip delay if we have a success + continue + } + + successes = 0 + select { + // Capture the overall timeout + case <-to: + t.Fatalf("timeout while waiting after %d attempts, %d/%d sucessess", attempts, successes, threshold) + // And the per-try delay + case <-time.After(delay): + } + } +} + +// match +// 1. interface{} is a slice: if one of the slice elements matches, the assertion passes +// Notice: can recursively find slices +// 2. interface{} is a map: if all the map elements match, the assertion passes +// 3. interface{} is a field: if the field matches, the assertion passes +func match(t *testing.T, actual interface{}, expected map[string]interface{}) bool { + reflectValue := reflect.ValueOf(actual) + kind := reflectValue.Kind() + switch kind { + case reflect.Slice: + actualValueSlice := actual.([]interface{}) + for _, v := range actualValueSlice { + if match(t, v, expected) { + return true + } + } + return false + case reflect.Map: + actualValueMap := actual.(map[string]interface{}) + for key, expectValue := range expected { + actualValue, ok := actualValueMap[key] + if !ok { + return false + } + if !reflect.DeepEqual(actualValue, expectValue) { + return false + } + } + return true + default: + return reflect.DeepEqual(actual, expected) + } +} + +// findMustExist finds the value of the given path in the given Envoy config. +func findMustExist(t *testing.T, actual interface{}, expected map[string]interface{}) bool { + for key, expectValue := range expected { + // If the key does not exist, the assertion fails. + t.Logf("🔍 Finding key %s", key) + if !findKey(actual, key, expectValue) { + t.Logf("❌ Not found key %s", key) + return false + } + t.Logf("✅ Found key %s", key) + } + return true +} + +// findMustNotExist finds the value of the given path in the given Envoy config. +func findMustNotExist(t *testing.T, actual interface{}, expected map[string]interface{}) bool { + for key, expectValue := range expected { + // If the key exists, the assertion fails. + t.Logf("🔍 Finding key %s", key) + if findKey(actual, key, expectValue) { + t.Logf("❌ Found key %s", key) + return false + } + t.Logf("✅ Not found key %s", key) + } + return true +} + +// findKey finds the value of the given key in the given Envoy config. +func findKey(actual interface{}, key string, expectValue interface{}) bool { + reflectValue := reflect.ValueOf(actual) + kind := reflectValue.Kind() + switch kind { + case reflect.Slice: + actualValueSlice := actual.([]interface{}) + for _, v := range actualValueSlice { + if findKey(v, key, expectValue) { + return true + } + } + return false + case reflect.Map: + actualValueMap := actual.(map[string]interface{}) + for actualKey, actualValue := range actualValueMap { + if actualKey == key && reflect.DeepEqual(convertType(actualValue, expectValue), expectValue) { + return true + } + if findKey(actualValue, key, expectValue) { + return true + } + } + return false + default: + if reflectValue.String() == key && reflect.DeepEqual(convertType(actual, expectValue), expectValue) { + return true + } + return false + } +} + +// convertType converts the type of the given value to the type of the given target value. +func convertType(value interface{}, targetType interface{}) interface{} { + targetTypeValue := reflect.ValueOf(targetType) + targetTypeKind := targetTypeValue.Kind() + + switch targetTypeKind { + case reflect.Int: + switch value.(type) { + case int: + return value + case float64: + return int(value.(float64)) + } + case reflect.Float64: + switch value.(type) { + case int: + return float64(value.(int)) + case float64: + return value + } + } + + return value +} diff --git a/test/e2e/conformance/utils/envoy/envoy_test.go b/test/e2e/conformance/utils/envoy/envoy_test.go new file mode 100644 index 000000000..6e43bbc4c --- /dev/null +++ b/test/e2e/conformance/utils/envoy/envoy_test.go @@ -0,0 +1,462 @@ +/* +Copyright 2022 The Kubernetes Authors. +Licensed 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 envoy + +import ( + "testing" +) + +func Test_match(t *testing.T) { + testCases := []struct { + name string + actual interface{} + expected map[string]interface{} + expectResult bool + }{ + { + name: "case 1", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + map[string]interface{}{ + "foo": "baz", + }, + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + expectResult: true, + }, + { + name: "case 2", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + map[string]interface{}{ + "foo": "baz", + }, + }, + expected: map[string]interface{}{ + "foo": "bay", + }, + expectResult: false, + }, + { + name: "case 3", + actual: map[string]interface{}{ + "foo": "bar", + "bar": "baz", + "baz": "bay", + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + expectResult: true, + }, + { + name: "case 4", + actual: map[string]interface{}{ + "foo": "bar", + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + expectResult: true, + }, + { + name: "case 5", + actual: map[string]interface{}{ + "foo": "bar", + }, + expected: map[string]interface{}{ + "foo": "baz", + }, + expectResult: false, + }, + { + name: "case 6", + actual: []interface{}{ + []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + }, + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + expectResult: true, + }, + { + name: "case 7", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + []interface{}{ + map[string]interface{}{ + "foo": "baz", + }, + }, + }, + expected: map[string]interface{}{ + "foo": "baz", + }, + expectResult: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result := match(t, testCase.actual, testCase.expected) + if result != testCase.expectResult { + t.Errorf("expected %v, got %v", testCase.expectResult, result) + } + }) + } +} + +func Test_findMustExist(t *testing.T) { + testCases := []struct { + name string + actual interface{} + expected map[string]interface{} + expectResult bool + }{ + { + name: "case 1", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + expectResult: true, + }, + { + name: "case 2", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + }, + expected: map[string]interface{}{ + "foo": "baz", + }, + expectResult: false, + }, + { + name: "case 3", + actual: map[string]interface{}{ + "foo": "bar", + "bar": "baz", + "baz": "bay", + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + expectResult: true, + }, + { + name: "case 4", + actual: map[string]interface{}{ + "foo": "bar", + }, + expected: map[string]interface{}{ + "foo": "baz", + }, + expectResult: false, + }, + { + name: "case 5", + actual: []interface{}{ + []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + }, + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + expectResult: true, + }, + { + name: "case 6", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + []interface{}{ + map[string]interface{}{ + "foo": "baz", + }, + }, + }, + expected: map[string]interface{}{ + "foo": "baz", + }, + expectResult: true, + }, + { + name: "case 7", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + []interface{}{ + map[string]interface{}{ + "test": "baz", + }, + }, + }, + expected: map[string]interface{}{ + "foo": "bar", + "test": "baz", + }, + expectResult: true, + }, + { + name: "case 8", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + "test": "baz", + }, + []interface{}{ + map[string]interface{}{ + "foo": "baz", + }, + }, + }, + expected: map[string]interface{}{ + "foo": "baz", + "test": "baz", + }, + expectResult: true, + }, + { + name: "case 9", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + []interface{}{ + []interface{}{ + map[string]interface{}{ + "foo": "baz", + }, + }, + }, + }, + expected: map[string]interface{}{ + "foo": "baz", + }, + expectResult: true, + }, + { + name: "case 9", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + []interface{}{ + []interface{}{ + map[string]interface{}{ + "foo": "baz", + }, + }, + }, + map[string]interface{}{ + "content": []interface{}{ + "one", + "two", + }, + }, + }, + expected: map[string]interface{}{ + "foo": "baz", + "content": []interface{}{ + "one", + "two", + }, + }, + expectResult: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result := findMustExist(t, testCase.actual, testCase.expected) + if result != testCase.expectResult { + t.Errorf("expected %v, got %v", testCase.expectResult, result) + } + }) + } +} + +func Test_findMustNotExist(t *testing.T) { + testCases := []struct { + name string + actual interface{} + expected map[string]interface{} + expectResult bool + }{ + { + name: "case 1", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + expectResult: false, + }, + { + name: "case 2", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + }, + expected: map[string]interface{}{ + "foo": "baz", + }, + expectResult: true, + }, + { + name: "case 3", + actual: map[string]interface{}{ + "foo": "bar", + "bar": "baz", + "baz": "bay", + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + expectResult: false, + }, + { + name: "case 4", + actual: map[string]interface{}{ + "foo": "bar", + }, + expected: map[string]interface{}{ + "foo": "baz", + }, + expectResult: true, + }, + { + name: "case 5", + actual: []interface{}{ + []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + }, + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + expectResult: false, + }, + { + name: "case 6", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + []interface{}{ + map[string]interface{}{ + "foo": "baz", + }, + }, + }, + expected: map[string]interface{}{ + "foo": "baz", + }, + expectResult: false, + }, + { + name: "case 7", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + []interface{}{ + map[string]interface{}{ + "test": "baz", + }, + }, + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + expectResult: false, + }, + { + name: "case 8", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + "test": "baz", + }, + []interface{}{ + map[string]interface{}{ + "foo": "baz", + }, + }, + }, + expected: map[string]interface{}{ + "foo": "baz", + "test": "baz", + }, + expectResult: false, + }, + { + name: "case 9", + actual: []interface{}{ + map[string]interface{}{ + "foo": "bar", + }, + []interface{}{ + []interface{}{ + map[string]interface{}{ + "foo": "baz", + }, + }, + }, + }, + expected: map[string]interface{}{ + "foo": "baz", + }, + expectResult: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result := findMustNotExist(t, testCase.actual, testCase.expected) + if result != testCase.expectResult { + t.Errorf("expected %v, got %v", testCase.expectResult, result) + } + }) + } +} diff --git a/test/e2e/conformance/utils/flags/flags.go b/test/e2e/conformance/utils/flags/flags.go index 9c2f42567..29b3c342c 100644 --- a/test/e2e/conformance/utils/flags/flags.go +++ b/test/e2e/conformance/utils/flags/flags.go @@ -26,4 +26,5 @@ var ( IsWasmPluginTest = flag.Bool("isWasmPluginTest", false, "Determine if run wasm plugin conformance test") WasmPluginType = flag.String("wasmPluginType", "GO", "Define wasm plugin type, currently supports GO, CPP") WasmPluginName = flag.String("wasmPluginName", "", "Define wasm plugin name") + IsEnvoyConfigTest = flag.Bool("isEnvoyConfigTest", false, "Determine if run envoy config conformance test") ) diff --git a/test/e2e/conformance/utils/kubernetes/helpers.go b/test/e2e/conformance/utils/kubernetes/helpers.go index de69a1012..ce002be24 100644 --- a/test/e2e/conformance/utils/kubernetes/helpers.go +++ b/test/e2e/conformance/utils/kubernetes/helpers.go @@ -121,7 +121,7 @@ func FindPodConditionInList(t *testing.T, conditions []v1.PodCondition, condName return false } -func ApplyConfigmapDataWithYaml(c client.Client, namespace string, name string, key string, val any) error { +func ApplyConfigmapDataWithYaml(t *testing.T, c client.Client, namespace string, name string, key string, val any) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -140,8 +140,11 @@ func ApplyConfigmapDataWithYaml(c client.Client, namespace string, name string, } cm.Data[key] = data + t.Logf("🏗 Updating %s %s", name, namespace) + if err := c.Update(ctx, cm); err != nil { return err } + return nil } diff --git a/test/e2e/conformance/utils/suite/features.go b/test/e2e/conformance/utils/suite/features.go index a969054f3..0c558a1b6 100644 --- a/test/e2e/conformance/utils/suite/features.go +++ b/test/e2e/conformance/utils/suite/features.go @@ -31,6 +31,9 @@ const ( EurekaConformanceFeature SupportedFeature = "eureka" ConsulConformanceFeature SupportedFeature = "consul" NacosConformanceFeature SupportedFeature = "nacos" + + // extended: envoy config + EnvoyConfigConformanceFeature SupportedFeature = "envoy-config" ) var AllFeatures = sets.Set{}. @@ -38,7 +41,8 @@ var AllFeatures = sets.Set{}. Insert(string(DubboConformanceFeature)). Insert(string(EurekaConformanceFeature)). Insert(string(ConsulConformanceFeature)). - Insert(string(NacosConformanceFeature)) + Insert(string(NacosConformanceFeature)). + Insert(string(EnvoyConfigConformanceFeature)) var ExperimentFeatures = sets.Set{}. Insert(string(WASMGoConformanceFeature)). diff --git a/test/e2e/conformance/utils/suite/suite.go b/test/e2e/conformance/utils/suite/suite.go index f15a55fc5..2f6969ba8 100644 --- a/test/e2e/conformance/utils/suite/suite.go +++ b/test/e2e/conformance/utils/suite/suite.go @@ -60,6 +60,9 @@ type Options struct { // resources such as Gateways should be cleaned up after the run. CleanupBaseResources bool TimeoutConfig config.TimeoutConfig + + // IsEnvoyConfigTest indicates whether or not the test is for envoy config + IsEnvoyConfigTest bool } type WASMOptions struct { @@ -87,6 +90,8 @@ func New(s Options) *ConformanceTestSuite { } else { s.SupportedFeatures.Insert(string(WASMGoConformanceFeature)) } + } else if s.IsEnvoyConfigTest { + s.SupportedFeatures.Insert(string(EnvoyConfigConformanceFeature)) } else if s.EnableAllSupportedFeatures { s.SupportedFeatures = AllFeatures } diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 5c1875434..e32a6ec0c 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -51,6 +51,7 @@ func TestHigressConformanceTests(t *testing.T) { }, GatewayAddress: "localhost", EnableAllSupportedFeatures: true, + IsEnvoyConfigTest: *flags.IsEnvoyConfigTest, }) cSuite.Setup(t)