From 3cc1c7877fbcaa34516f7e160fe0aabf6ef2e1bd Mon Sep 17 00:00:00 2001 From: Jun <108045855+2456868764@users.noreply.github.com> Date: Mon, 18 Dec 2023 19:05:29 +0800 Subject: [PATCH] feat: add gzip global setting in configmap (#660) --- pkg/ingress/kube/configmap/config.go | 2 + pkg/ingress/kube/configmap/controller.go | 2 + pkg/ingress/kube/configmap/gzip.go | 336 ++++++++++++ pkg/ingress/kube/configmap/gzip_test.go | 495 ++++++++++++++++++ pkg/ingress/kube/configmap/tracing.go | 2 +- test/e2e/conformance/tests/configmap-gzip.go | 208 ++++++++ .../e2e/conformance/tests/configmap-gzip.yaml | 32 ++ .../conformance/utils/kubernetes/helpers.go | 26 + 8 files changed, 1102 insertions(+), 1 deletion(-) create mode 100644 pkg/ingress/kube/configmap/gzip.go create mode 100644 pkg/ingress/kube/configmap/gzip_test.go create mode 100644 test/e2e/conformance/tests/configmap-gzip.go create mode 100644 test/e2e/conformance/tests/configmap-gzip.yaml diff --git a/pkg/ingress/kube/configmap/config.go b/pkg/ingress/kube/configmap/config.go index ceca05d31..cba582d59 100644 --- a/pkg/ingress/kube/configmap/config.go +++ b/pkg/ingress/kube/configmap/config.go @@ -35,11 +35,13 @@ type ItemEventHandler = func(name string) type HigressConfig struct { Tracing *Tracing `json:"tracing,omitempty"` + Gzip *Gzip `json:"gzip,omitempty"` } func NewDefaultHigressConfig() *HigressConfig { higressConfig := &HigressConfig{ Tracing: NewDefaultTracing(), + Gzip: NewDefaultGzip(), } return higressConfig } diff --git a/pkg/ingress/kube/configmap/controller.go b/pkg/ingress/kube/configmap/controller.go index bf2dbef3f..594a61a5c 100644 --- a/pkg/ingress/kube/configmap/controller.go +++ b/pkg/ingress/kube/configmap/controller.go @@ -73,7 +73,9 @@ func NewConfigmapMgr(XDSUpdater model.XDSUpdater, namespace string, higressConfi configmapMgr.SetHigressConfig(NewDefaultHigressConfig()) tracingController := NewTracingController(namespace) + gzipController := NewGzipController(namespace) configmapMgr.AddItemControllers(tracingController) + configmapMgr.AddItemControllers(gzipController) configmapMgr.initEventHandlers() return configmapMgr diff --git a/pkg/ingress/kube/configmap/gzip.go b/pkg/ingress/kube/configmap/gzip.go new file mode 100644 index 000000000..48106c27f --- /dev/null +++ b/pkg/ingress/kube/configmap/gzip.go @@ -0,0 +1,336 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// 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 configmap + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + "sync/atomic" + + "github.com/alibaba/higress/pkg/ingress/kube/util" + . "github.com/alibaba/higress/pkg/ingress/log" + networking "istio.io/api/networking/v1alpha3" + "istio.io/istio/pkg/config" + "istio.io/istio/pkg/config/schema/gvk" +) + +const ( + higressGzipEnvoyFilterName = "higress-config-gzip" + compressionStrategyValues = "DEFAULT_STRATEGY,FILTERED,HUFFMAN_ONLY,RLE,FIXED" + compressionLevelValues = "BEST_COMPRESSION,BEST_SPEED,COMPRESSION_LEVEL_1,COMPRESSION_LEVEL_2,COMPRESSION_LEVEL_3,COMPRESSION_LEVEL_4,COMPRESSION_LEVEL_5,COMPRESSION_LEVEL_6,COMPRESSION_LEVEL_7,COMPRESSION_LEVEL_8,COMPRESSION_LEVEL_9" +) + +type Gzip struct { + // Flag to control gzip + Enable bool `json:"enable,omitempty"` + MinContentLength int32 `json:"minContentLength,omitempty"` + ContentType []string `json:"contentType,omitempty"` + DisableOnEtagHeader bool `json:"disableOnEtagHeader,omitempty"` + // Value from 1 to 9 that controls the amount of internal memory used by zlib. + // Higher values use more memory, but are faster and produce better compression results. The default value is 5. + MemoryLevel int32 `json:"memoryLevel,omitempty"` + // Value from 9 to 15 that represents the base two logarithmic of the compressor’s window size. + // Larger window results in better compression at the expense of memory usage. + // The default is 12 which will produce a 4096 bytes window + WindowBits int32 `json:"windowBits,omitempty"` + // Value for Zlib’s next output buffer. If not set, defaults to 4096. + ChunkSize int32 `json:"chunkSize,omitempty"` + // A value used for selecting the zlib compression level. + // From COMPRESSION_LEVEL_1 to COMPRESSION_LEVEL_9 + // BEST_COMPRESSION == COMPRESSION_LEVEL_9 , BEST_SPEED == COMPRESSION_LEVEL_1 + CompressionLevel string `json:"compressionLevel,omitempty"` + // A value used for selecting the zlib compression strategy which is directly related to the characteristics of the content. + // Most of the time “DEFAULT_STRATEGY” + // Value is one of DEFAULT_STRATEGY, FILTERED, HUFFMAN_ONLY, RLE, FIXED + CompressionStrategy string `json:"compressionStrategy,omitempty"` +} + +func validGzip(g *Gzip) error { + if g == nil { + return nil + } + + if g.MinContentLength <= 0 { + return errors.New("minContentLength can not be less than zero") + } + + if len(g.ContentType) == 0 { + return errors.New("content type can not be empty") + } + + if !(g.MemoryLevel >= 1 && g.MemoryLevel <= 9) { + return errors.New("memory level need be between 1 and 9") + } + + if !(g.WindowBits >= 9 && g.WindowBits <= 15) { + return errors.New("window bits need be between 9 and 15") + } + + if g.ChunkSize <= 0 { + return errors.New("chunk size need be large than zero") + } + + compressionLevels := strings.Split(compressionLevelValues, ",") + isFound := false + for _, v := range compressionLevels { + if g.CompressionLevel == v { + isFound = true + break + } + } + if !isFound { + return fmt.Errorf("compressionLevel need be one of %s", compressionLevelValues) + } + + isFound = false + compressionStrategies := strings.Split(compressionStrategyValues, ",") + for _, v := range compressionStrategies { + if g.CompressionStrategy == v { + isFound = true + break + } + } + if !isFound { + return fmt.Errorf("compressionStrategy need be one of %s", compressionStrategyValues) + } + + return nil +} + +func compareGzip(old *Gzip, new *Gzip) (Result, error) { + if old == nil && new == nil { + return ResultNothing, nil + } + + if new == nil { + return ResultDelete, nil + } + + if !reflect.DeepEqual(old, new) { + return ResultReplace, nil + } + + return ResultNothing, nil +} + +func deepCopyGzip(gzip *Gzip) (*Gzip, error) { + newGzip := NewDefaultGzip() + bytes, err := json.Marshal(gzip) + if err != nil { + return nil, err + } + err = json.Unmarshal(bytes, newGzip) + return newGzip, err +} + +func NewDefaultGzip() *Gzip { + gzip := &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", + } + return gzip +} + +type GzipController struct { + Namespace string + gzip atomic.Value + Name string + eventHandler ItemEventHandler +} + +func NewGzipController(namespace string) *GzipController { + gzipController := &GzipController{ + Namespace: namespace, + gzip: atomic.Value{}, + Name: "gzip", + } + gzipController.SetGzip(NewDefaultGzip()) + return gzipController +} + +func (g *GzipController) GetName() string { + return g.Name +} + +func (t *GzipController) SetGzip(gzip *Gzip) { + t.gzip.Store(gzip) +} + +func (g *GzipController) GetGzip() *Gzip { + value := g.gzip.Load() + if value != nil { + if gzip, ok := value.(*Gzip); ok { + return gzip + } + } + return nil +} + +func (g *GzipController) AddOrUpdateHigressConfig(name util.ClusterNamespacedName, old *HigressConfig, new *HigressConfig) error { + if err := validGzip(new.Gzip); err != nil { + IngressLog.Errorf("data:%+v convert to gzip , error: %+v", new.Gzip, err) + return nil + } + + result, _ := compareGzip(old.Gzip, new.Gzip) + + switch result { + case ResultReplace: + if newGzip, err := deepCopyGzip(new.Gzip); err != nil { + IngressLog.Infof("gzip deepcopy error:%v", err) + } else { + g.SetGzip(newGzip) + IngressLog.Infof("AddOrUpdate Higress config gzip") + g.eventHandler(higressGzipEnvoyFilterName) + IngressLog.Infof("send event with filter name:%s", higressGzipEnvoyFilterName) + } + case ResultDelete: + g.SetGzip(NewDefaultGzip()) + IngressLog.Infof("Delete Higress config gzip") + g.eventHandler(higressGzipEnvoyFilterName) + IngressLog.Infof("send event with filter name:%s", higressGzipEnvoyFilterName) + } + + return nil +} + +func (g *GzipController) ValidHigressConfig(higressConfig *HigressConfig) error { + if higressConfig == nil { + return nil + } + if higressConfig.Gzip == nil { + return nil + } + + return validGzip(higressConfig.Gzip) +} + +func (g *GzipController) ConstructEnvoyFilters() ([]*config.Config, error) { + configs := make([]*config.Config, 0) + gzip := g.GetGzip() + namespace := g.Namespace + + if gzip == nil { + return configs, nil + } + + if gzip.Enable == false { + return configs, nil + } + + gzipStruct := g.constructGzipStruct(gzip, namespace) + if len(gzipStruct) == 0 { + return configs, nil + } + + config := &config.Config{ + Meta: config.Meta{ + GroupVersionKind: gvk.EnvoyFilter, + Name: higressGzipEnvoyFilterName, + Namespace: namespace, + }, + Spec: &networking.EnvoyFilter{ + ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{ + { + ApplyTo: networking.EnvoyFilter_HTTP_FILTER, + Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{ + Context: networking.EnvoyFilter_GATEWAY, + ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{ + Listener: &networking.EnvoyFilter_ListenerMatch{ + FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{ + Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{ + Name: "envoy.filters.network.http_connection_manager", + SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{ + Name: "envoy.filters.http.router", + }, + }, + }, + }, + }, + }, + Patch: &networking.EnvoyFilter_Patch{ + Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE, + Value: util.BuildPatchStruct(gzipStruct), + }, + }, + }, + }, + } + + configs = append(configs, config) + return configs, nil +} + +func (g *GzipController) RegisterItemEventHandler(eventHandler ItemEventHandler) { + g.eventHandler = eventHandler +} + +func (g *GzipController) constructGzipStruct(gzip *Gzip, namespace string) string { + gzipConfig := "" + contentType := "" + index := 0 + for _, v := range gzip.ContentType { + contentType = contentType + fmt.Sprintf("\"%s\"", v) + if index < len(gzip.ContentType)-1 { + contentType = contentType + "," + } + index++ + } + structFmt := `{ + "name": "envoy.filters.http.compressor", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor", + "response_direction_config": { + "common_config": { + "min_content_length": %d, + "content_type": [%s], + "disable_on_etag_header": %t + } + }, + "request_direction_config": { + "common_config": { + "enabled": { + "default_value": false, + "runtime_key": "request_compressor_enabled" + } + } + }, + "compressor_library": { + "name": "text_optimized", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip", + "memory_level": %d, + "window_bits": %d, + "check_size": %d, + "compression_level": "%s", + "compression_strategy": "%s" + } + } + } +}` + gzipConfig = fmt.Sprintf(structFmt, gzip.MinContentLength, contentType, gzip.DisableOnEtagHeader, + gzip.MemoryLevel, gzip.WindowBits, gzip.ChunkSize, gzip.CompressionLevel, gzip.CompressionStrategy) + return gzipConfig +} diff --git a/pkg/ingress/kube/configmap/gzip_test.go b/pkg/ingress/kube/configmap/gzip_test.go new file mode 100644 index 000000000..3400ea126 --- /dev/null +++ b/pkg/ingress/kube/configmap/gzip_test.go @@ -0,0 +1,495 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// 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 configmap + +import ( + "errors" + "fmt" + "github.com/alibaba/higress/pkg/ingress/kube/util" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_validGzip(t *testing.T) { + + tests := []struct { + name string + gzip *Gzip + wantErr error + }{ + { + name: "default", + gzip: &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", + }, + wantErr: nil, + }, + { + name: "nil", + gzip: nil, + wantErr: nil, + }, + + { + name: "no content type", + gzip: &Gzip{ + Enable: false, + MinContentLength: 1024, + ContentType: []string{}, + DisableOnEtagHeader: true, + MemoryLevel: 5, + WindowBits: 12, + ChunkSize: 4096, + CompressionLevel: "BEST_COMPRESSION", + CompressionStrategy: "DEFAULT_STRATEGY", + }, + wantErr: errors.New("content type can not be empty"), + }, + + { + name: "MinContentLength less than zero", + gzip: &Gzip{ + Enable: false, + MinContentLength: 0, + 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", + }, + wantErr: errors.New("minContentLength can not be less than zero"), + }, + + { + name: "MemoryLevel less than 1", + gzip: &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", + }, + wantErr: errors.New("memory level need be between 1 and 9"), + }, + + { + name: "WindowBits less than 9", + gzip: &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: 8, + ChunkSize: 4096, + CompressionLevel: "BEST_COMPRESSION", + CompressionStrategy: "DEFAULT_STRATEGY", + }, + wantErr: errors.New("window bits need be between 9 and 15"), + }, + + { + name: "ChunkSize less than zero", + gzip: &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", + }, + wantErr: errors.New("chunk size need be large than zero"), + }, + + { + name: "CompressionLevel is not right", + gzip: &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_COMPRESSIONA", + CompressionStrategy: "DEFAULT_STRATEGY", + }, + wantErr: fmt.Errorf("compressionLevel need be one of %s", compressionLevelValues), + }, + + { + name: "CompressionStrategy is not right", + gzip: &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_STRATEGYA", + }, + wantErr: fmt.Errorf("compressionStrategy need be one of %s", compressionStrategyValues), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validGzip(tt.gzip); err != nil { + assert.Equal(t, tt.wantErr, err) + } + }) + } +} + +func Test_compareGzip(t *testing.T) { + tests := []struct { + name string + old *Gzip + new *Gzip + wantResult Result + wantErr error + }{ + { + name: "compare both nil", + old: nil, + new: nil, + wantResult: ResultNothing, + wantErr: nil, + }, + { + name: "compare result delete", + old: &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", + }, + new: nil, + wantResult: ResultDelete, + wantErr: nil, + }, + { + name: "compare result equal", + old: &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", + }, + new: &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", + }, + wantResult: ResultNothing, + wantErr: nil, + }, + { + name: "compare result replace", + old: &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", + }, + new: &Gzip{ + Enable: true, + 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", + }, + wantResult: ResultReplace, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := compareGzip(tt.old, tt.new) + assert.Equal(t, tt.wantResult, result) + assert.Equal(t, tt.wantErr, err) + }) + } +} + +func Test_deepCopyGzip(t *testing.T) { + + tests := []struct { + name string + gzip *Gzip + wantGzip *Gzip + wantErr error + }{ + { + name: "deep copy", + gzip: &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", + }, + wantGzip: &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", + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gzip, err := deepCopyGzip(tt.gzip) + assert.Equal(t, tt.wantGzip, gzip) + assert.Equal(t, tt.wantErr, err) + }) + } +} + +func TestGzipController_AddOrUpdateHigressConfig(t *testing.T) { + eventPush := "default" + defaultHandler := func(name string) { + eventPush = "push" + } + + defaultName := util.ClusterNamespacedName{} + + tests := []struct { + name string + old *HigressConfig + new *HigressConfig + wantErr error + wantEventPush string + wantGzip *Gzip + }{ + { + name: "default", + old: &HigressConfig{ + Gzip: NewDefaultGzip(), + }, + new: &HigressConfig{ + Gzip: NewDefaultGzip(), + }, + wantErr: nil, + wantEventPush: "default", + wantGzip: NewDefaultGzip(), + }, + { + name: "replace and push 1", + old: &HigressConfig{ + Gzip: NewDefaultGzip(), + }, + new: &HigressConfig{ + Gzip: &Gzip{ + Enable: true, + 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", + }, + }, + wantErr: nil, + wantEventPush: "push", + wantGzip: &Gzip{ + Enable: true, + 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", + }, + }, + + { + name: "replace and push 2", + old: &HigressConfig{ + Gzip: &Gzip{ + Enable: true, + 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", + }, + }, + new: &HigressConfig{ + Gzip: &Gzip{ + Enable: true, + MinContentLength: 2048, + 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", + }, + }, + wantErr: nil, + wantEventPush: "push", + wantGzip: &Gzip{ + Enable: true, + MinContentLength: 2048, + 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", + }, + }, + + { + name: "replace and push 3", + old: &HigressConfig{ + Gzip: &Gzip{ + Enable: true, + 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", + }, + }, + new: &HigressConfig{ + Gzip: &Gzip{ + Enable: false, + MinContentLength: 2048, + 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", + }, + }, + wantErr: nil, + wantEventPush: "push", + wantGzip: &Gzip{ + Enable: false, + MinContentLength: 2048, + 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", + }, + }, + { + name: "delete and push", + old: &HigressConfig{ + Gzip: &Gzip{ + Enable: true, + 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", + }, + }, + new: &HigressConfig{ + Gzip: nil, + }, + wantErr: nil, + wantEventPush: "push", + wantGzip: NewDefaultGzip(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewGzipController("higress-system") + g.eventHandler = defaultHandler + eventPush = "default" + err := g.AddOrUpdateHigressConfig(defaultName, tt.old, tt.new) + assert.Equal(t, tt.wantEventPush, eventPush) + assert.Equal(t, tt.wantErr, err) + assert.Equal(t, tt.wantGzip, g.GetGzip()) + }) + } +} diff --git a/pkg/ingress/kube/configmap/tracing.go b/pkg/ingress/kube/configmap/tracing.go index ffcb3c05d..82cb58556 100644 --- a/pkg/ingress/kube/configmap/tracing.go +++ b/pkg/ingress/kube/configmap/tracing.go @@ -117,7 +117,7 @@ func validTracing(t *Tracing) error { } } - if tracerNum != 1 { + if tracerNum != 1 && t.Enable == true { return errors.New("only one of skywalking,zipkin and opentelemetry configuration can be set") } return nil diff --git a/test/e2e/conformance/tests/configmap-gzip.go b/test/e2e/conformance/tests/configmap-gzip.go new file mode 100644 index 000000000..3ebeaf116 --- /dev/null +++ b/test/e2e/conformance/tests/configmap-gzip.go @@ -0,0 +1,208 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// 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 tests + +import ( + "testing" + + "github.com/alibaba/higress/pkg/ingress/kube/configmap" + "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" +) + +func init() { + Register(ConfigmapGzip) +} + +var ConfigmapGzip = suite.ConformanceTest{ + ShortName: "ConfigmapGzip", + Description: "The Ingress in the higress-conformance-infra namespace uses the configmap gzip.", + 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) + if err != nil { + t.Fatalf("can't apply conifgmap %s in namespace %s for data key %s", "higress-config", "higress-system", "higress") + } + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase.httpAssert) + } + }) + }, +} diff --git a/test/e2e/conformance/tests/configmap-gzip.yaml b/test/e2e/conformance/tests/configmap-gzip.yaml new file mode 100644 index 000000000..12b1ceb34 --- /dev/null +++ b/test/e2e/conformance/tests/configmap-gzip.yaml @@ -0,0 +1,32 @@ +# Copyright (c) 2022 Alibaba Group Holding Ltd. +# +# 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. + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: higress-conformance-infra-configmap-gzip-test + namespace: higress-conformance-infra +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/foo" + backend: + service: + name: infra-backend-v3 + port: + number: 8080 \ No newline at end of file diff --git a/test/e2e/conformance/utils/kubernetes/helpers.go b/test/e2e/conformance/utils/kubernetes/helpers.go index fd91c9a61..de69a1012 100644 --- a/test/e2e/conformance/utils/kubernetes/helpers.go +++ b/test/e2e/conformance/utils/kubernetes/helpers.go @@ -15,6 +15,7 @@ package kubernetes import ( "context" + "sigs.k8s.io/yaml" "strings" "testing" "time" @@ -119,3 +120,28 @@ func FindPodConditionInList(t *testing.T, conditions []v1.PodCondition, condName t.Logf("⌛️ %s was not in conditions list", condName) return false } + +func ApplyConfigmapDataWithYaml(c client.Client, namespace string, name string, key string, val any) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + cm := &v1.ConfigMap{} + if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, cm); err != nil { + return err + } + y, err := yaml.Marshal(val) + if err != nil { + return err + } + data := string(y) + + if cm.Data == nil { + cm.Data = make(map[string]string, 0) + } + cm.Data[key] = data + + if err := c.Update(ctx, cm); err != nil { + return err + } + return nil +}