Skip to content

Commit

Permalink
feat: add support for gzip compression of request bodies
Browse files Browse the repository at this point in the history
Fixes arf/planning-sdk-squad#2185

This commit adds support for peforming gzip compression
of request bodies, and consists of the following:
1. Two new functions: NewGzipCompressionReader() and
NewGzipDecompressionReader().  Each function will wrap
an existing io.Reader to provide compression/decompression
using a filter pattern.

2. A new field "EnableGzipCompression" was added to the
RequestBuilder struct to indicate whether or not request
bodies should be gzip-compressed.

3. RequestBuilder.Build() was updated to honor the new flag
and (if enabled) wrap the Body field in a gzip compression filter
prior to constructing the http.Request instance.

4. A new field "EnableGzipCompression" was added to the
ServiceOptions struct, as well as new function to support this
field as an externally configuratable option (<svcname>_ENABLE_GZIP)
  • Loading branch information
padamstx committed Oct 5, 2020
1 parent f4657bb commit 397cbaa
Show file tree
Hide file tree
Showing 11 changed files with 482 additions and 1 deletion.
27 changes: 27 additions & 0 deletions v4/core/base_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ type ServiceOptions struct {
// service instance to authenticate outbound requests, typically by adding the
// HTTP "Authorization" header.
Authenticator Authenticator

// EnableGzipCompression indicates whether or not request bodies
// should be gzip-compressed.
// This field has no effect on response bodies.
// If enabled, the Body field will be gzip-compressed and
// the "Content-Encoding" header will be added to the request with the
// value "gzip".
EnableGzipCompression bool
}

// BaseService implements the common functionality shared by generated services
Expand Down Expand Up @@ -126,6 +134,15 @@ func (service *BaseService) ConfigureService(serviceName string) error {
service.DisableSSLVerification()
}
}

// ENABLE_GZIP
if enableGzip, ok := serviceProps[PROPNAME_SVC_ENABLE_GZIP]; ok && enableGzip != "" {
// Convert the config string to bool.
boolValue, err := strconv.ParseBool(enableGzip)
if err == nil {
service.SetEnableGzipCompression(boolValue)
}
}
}
return nil
}
Expand Down Expand Up @@ -170,6 +187,16 @@ func (service *BaseService) DisableSSLVerification() {
service.Client.Transport = tr
}

// SetEnableGzipCompression sets the service's EnableGzipCompression field
func (service *BaseService) SetEnableGzipCompression(enableGzip bool) {
service.Options.EnableGzipCompression = enableGzip
}

// GetEnableGzipCompression returns the service's EnableGzipCompression field
func (service *BaseService) GetEnableGzipCompression() bool {
return service.Options.EnableGzipCompression
}

// buildUserAgent builds the user agent string.
func (service *BaseService) buildUserAgent() string {
return fmt.Sprintf("%s-%s %s", sdk_name, __VERSION__, SystemInfo())
Expand Down
42 changes: 42 additions & 0 deletions v4/core/base_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,21 @@ func TestSetServiceURL(t *testing.T) {
assert.Equal(t, "https://myserver.com/api/baseurl", service.GetServiceURL())
}

func TestSetEnableGzipCompression(t *testing.T) {
service, err := NewBaseService(
&ServiceOptions{
Authenticator: &NoAuthAuthenticator{},
})
assert.Nil(t, err)
assert.NotNil(t, service)

assert.False(t, service.GetEnableGzipCompression())
assert.False(t, service.Options.EnableGzipCompression)

service.SetEnableGzipCompression(true)
assert.True(t, service.GetEnableGzipCompression())
}

func TestExtConfigFromCredentialFile(t *testing.T) {
pwd, _ := os.Getwd()
credentialFilePath := path.Join(pwd, "/../resources/my-credentials.env")
Expand All @@ -1198,6 +1213,31 @@ func TestExtConfigFromCredentialFile(t *testing.T) {
assert.NotNil(t, service)
assert.Equal(t, "https://service1/api", service.Options.URL)
assert.NotNil(t, service.Client.Transport)
assert.True(t, service.GetEnableGzipCompression())

service, _ = NewBaseService(
&ServiceOptions{
Authenticator: &NoAuthAuthenticator{},
URL: "bad url",
})
err = service.ConfigureService("service2")
assert.Nil(t, err)
assert.NotNil(t, service)
assert.Equal(t, "https://service2/api", service.Options.URL)
assert.Nil(t, service.Client.Transport)
assert.False(t, service.GetEnableGzipCompression())

service, _ = NewBaseService(
&ServiceOptions{
Authenticator: &NoAuthAuthenticator{},
URL: "bad url",
})
err = service.ConfigureService("service3")
assert.Nil(t, err)
assert.NotNil(t, service)
assert.Equal(t, "https://service3/api", service.Options.URL)
assert.Nil(t, service.Client.Transport)
assert.False(t, service.GetEnableGzipCompression())

os.Unsetenv("IBM_CREDENTIALS_FILE")
}
Expand Down Expand Up @@ -1231,6 +1271,7 @@ func TestExtConfigFromEnvironment(t *testing.T) {
assert.NotNil(t, service)
assert.Equal(t, "https://service3/api", service.Options.URL)
assert.Nil(t, service.Client.Transport)
assert.False(t, service.GetEnableGzipCompression())

clearTestEnvironment()
}
Expand Down Expand Up @@ -1272,6 +1313,7 @@ func TestConfigureServiceFromCredFile(t *testing.T) {
assert.NotNil(t, service)
assert.Equal(t, "https://service5/api", service.Options.URL)
assert.NotNil(t, service.Client.Transport)
assert.False(t, service.GetEnableGzipCompression())

os.Unsetenv("IBM_CREDENTIALS_FILE")
}
Expand Down
9 changes: 9 additions & 0 deletions v4/core/config_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
var testEnvironment = map[string]string{
"SERVICE_1_URL": "https://service1/api",
"SERVICE_1_DISABLE_SSL": "true",
"SERVICE_1_ENABLE_GZIP": "true",
"SERVICE_1_AUTH_TYPE": "IaM",
"SERVICE_1_APIKEY": "my-api-key",
"SERVICE_1_CLIENT_ID": "my-client-id",
Expand All @@ -36,11 +37,13 @@ var testEnvironment = map[string]string{
"SERVICE_1_AUTH_DISABLE_SSL": "true",
"SERVICE2_URL": "https://service2/api",
"SERVICE2_DISABLE_SSL": "false",
"SERVICE2_ENABLE_GZIP": "false",
"SERVICE2_AUTH_TYPE": "bAsIC",
"SERVICE2_USERNAME": "my-user",
"SERVICE2_PASSWORD": "my-password",
"SERVICE3_URL": "https://service3/api",
"SERVICE3_DISABLE_SSL": "false",
"SERVICE3_ENABLE_GZIP": "notabool",
"SERVICE3_AUTH_TYPE": "Cp4D",
"SERVICE3_AUTH_URL": "https://cp4dhost/cp4d/api",
"SERVICE3_USERNAME": "my-cp4d-user",
Expand Down Expand Up @@ -102,6 +105,7 @@ func TestGetServicePropertiesFromCredentialFile(t *testing.T) {
assert.NotNil(t, props)
assert.Equal(t, "https://service1/api", props[PROPNAME_SVC_URL])
assert.Equal(t, "true", props[PROPNAME_SVC_DISABLE_SSL])
assert.Equal(t, "true", props[PROPNAME_SVC_ENABLE_GZIP])
assert.Equal(t, strings.ToUpper(AUTHTYPE_IAM), strings.ToUpper(props[PROPNAME_AUTH_TYPE]))
assert.Equal(t, "my-api-key", props[PROPNAME_APIKEY])
assert.Equal(t, "my-client-id", props[PROPNAME_CLIENT_ID])
Expand All @@ -114,6 +118,7 @@ func TestGetServicePropertiesFromCredentialFile(t *testing.T) {
assert.NotNil(t, props)
assert.Equal(t, "https://service2/api", props[PROPNAME_SVC_URL])
assert.Equal(t, "false", props[PROPNAME_SVC_DISABLE_SSL])
assert.Equal(t, "false", props[PROPNAME_SVC_ENABLE_GZIP])
assert.Equal(t, strings.ToUpper(AUTHTYPE_BASIC), strings.ToUpper(props[PROPNAME_AUTH_TYPE]))
assert.Equal(t, "my-user", props[PROPNAME_USERNAME])
assert.Equal(t, "my-password", props[PROPNAME_PASSWORD])
Expand All @@ -123,6 +128,7 @@ func TestGetServicePropertiesFromCredentialFile(t *testing.T) {
assert.NotNil(t, props)
assert.Equal(t, "https://service3/api", props[PROPNAME_SVC_URL])
assert.Equal(t, "false", props[PROPNAME_SVC_DISABLE_SSL])
assert.Equal(t, "notabool", props[PROPNAME_SVC_ENABLE_GZIP])
assert.Equal(t, strings.ToUpper(AUTHTYPE_CP4D), strings.ToUpper(props[PROPNAME_AUTH_TYPE]))
assert.Equal(t, "my-cp4d-user", props[PROPNAME_USERNAME])
assert.Equal(t, "my-cp4d-password", props[PROPNAME_PASSWORD])
Expand Down Expand Up @@ -158,6 +164,7 @@ func TestGetServicePropertiesFromEnvironment(t *testing.T) {
assert.NotNil(t, props)
assert.Equal(t, "https://service1/api", props[PROPNAME_SVC_URL])
assert.Equal(t, "true", props[PROPNAME_SVC_DISABLE_SSL])
assert.Equal(t, "true", props[PROPNAME_SVC_ENABLE_GZIP])
assert.Equal(t, strings.ToUpper(AUTHTYPE_IAM), strings.ToUpper(props[PROPNAME_AUTH_TYPE]))
assert.Equal(t, "my-api-key", props[PROPNAME_APIKEY])
assert.Equal(t, "my-client-id", props[PROPNAME_CLIENT_ID])
Expand All @@ -170,6 +177,7 @@ func TestGetServicePropertiesFromEnvironment(t *testing.T) {
assert.NotNil(t, props)
assert.Equal(t, "https://service2/api", props[PROPNAME_SVC_URL])
assert.Equal(t, "false", props[PROPNAME_SVC_DISABLE_SSL])
assert.Equal(t, "false", props[PROPNAME_SVC_ENABLE_GZIP])
assert.Equal(t, strings.ToUpper(AUTHTYPE_BASIC), strings.ToUpper(props[PROPNAME_AUTH_TYPE]))
assert.Equal(t, "my-user", props[PROPNAME_USERNAME])
assert.Equal(t, "my-password", props[PROPNAME_PASSWORD])
Expand All @@ -179,6 +187,7 @@ func TestGetServicePropertiesFromEnvironment(t *testing.T) {
assert.NotNil(t, props)
assert.Equal(t, "https://service3/api", props[PROPNAME_SVC_URL])
assert.Equal(t, "false", props[PROPNAME_SVC_DISABLE_SSL])
assert.Equal(t, "notabool", props[PROPNAME_SVC_ENABLE_GZIP])
assert.Equal(t, strings.ToUpper(AUTHTYPE_CP4D), strings.ToUpper(props[PROPNAME_AUTH_TYPE]))
assert.Equal(t, "my-cp4d-user", props[PROPNAME_USERNAME])
assert.Equal(t, "my-cp4d-password", props[PROPNAME_PASSWORD])
Expand Down
1 change: 1 addition & 0 deletions v4/core/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
// Service client properties.
PROPNAME_SVC_URL = "URL"
PROPNAME_SVC_DISABLE_SSL = "DISABLE_SSL"
PROPNAME_SVC_ENABLE_GZIP = "ENABLE_GZIP"

// Authenticator properties.
PROPNAME_AUTH_TYPE = "AUTH_TYPE"
Expand Down
54 changes: 54 additions & 0 deletions v4/core/gzip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package core

// (C) Copyright IBM Corp. 2020.
//
// 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.

import (
"compress/gzip"
"io"
)

// NewGzipCompressionReader will return an io.Reader instance that will deliver
// the gzip-compressed version of the "uncompressedReader" argument.
// This function was inspired by this github gist:
// https://gist.github.com/tomcatzh/cf8040820962e0f8c04700eb3b2f26be
func NewGzipCompressionReader(uncompressedReader io.Reader) (io.Reader, error) {
// Create a pipe whose reader will effectively replace "uncompressedReader"
// to deliver the gzip-compressed byte stream.
pipeReader, pipeWriter := io.Pipe()
go func() {
defer pipeWriter.Close()

// Wrap the pipe's writer with a gzip writer that will
// write the gzip-compressed bytes to the Pipe.
compressedWriter := gzip.NewWriter(pipeWriter)
defer compressedWriter.Close()

// To trigger the operation of the pipe, we'll simply start
// to copy bytes from "uncompressedReader" to "compressedWriter".
// This copy operation will block as needed in order to write bytes
// to the pipe only when the pipe reader is called to retrieve more bytes.
_, err := io.Copy(compressedWriter, uncompressedReader)
if err != nil {
panic(err)
}
}()
return pipeReader, nil
}

// NewGzipDecompressionReader will return an io.Reader instance that will deliver
// the gzip-decompressed version of the "compressedReader" argument.
func NewGzipDecompressionReader(compressedReader io.Reader) (io.Reader, error) {
return gzip.NewReader(compressedReader)
}
143 changes: 143 additions & 0 deletions v4/core/gzip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package core

// (C) Copyright IBM Corp. 2020.
//
// 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.

import (
"bytes"
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
)

func toJSON(obj interface{}) string {
buf := new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(obj)
if err != nil {
panic(err)
}
return buf.String()
}

func testRoundTripBytes(t *testing.T, src []byte) {
// Compress the input string and store in a buffer.
srcReader := bytes.NewReader(src)
gzipCompressor, err := NewGzipCompressionReader(srcReader)
assert.Nil(t, err)
compressedBuf := new(bytes.Buffer)
_, err = compressedBuf.ReadFrom(gzipCompressor)
assert.Nil(t, err)
t.Log("Compressed length: ", compressedBuf.Len())

// Now uncompress the compressed bytes and store in another buffer.
bytesReader := bytes.NewReader(compressedBuf.Bytes())
gzipDecompressor, err := NewGzipDecompressionReader(bytesReader)
assert.Nil(t, err)
decompressedBuf := new(bytes.Buffer)
_, err = decompressedBuf.ReadFrom(gzipDecompressor)
assert.Nil(t, err)
t.Log("Uncompressed length: ", decompressedBuf.Len())

// Verify that the uncompressed bytes produce the original string.
assert.Equal(t, src, decompressedBuf.Bytes())
}
func TestGzipCompressionString1(t *testing.T) {
testRoundTripBytes(t, []byte("Hello world!"))
}

func TestGzipCompressionString2(t *testing.T) {
s := "This is a somewhat longer string, which we'll try to use in our compression/decompression testing. Hopefully this will workout ok, but who knows???"
testRoundTripBytes(t, []byte(s))
}

func TestGzipCompressionString3(t *testing.T) {
s := "This is a string that should be able to be compressed by a LOT......................................................................................................................................................................................................................................................................................................................................................."
testRoundTripBytes(t, []byte(s))
}

func TestGzipCompressionJSON1(t *testing.T) {
jsonString := `{
"rules": [
{
"request_id": "request-0",
"rule": {
"account_id": "44890a2fd24641a5a111738e358686cc",
"name": "Go Test Rule #1",
"description": "This is the description for Go Test Rule #1.",
"rule_type": "user_defined",
"target": {
"service_name": "config-gov-sdk-integration-test-service",
"resource_kind": "bucket",
"additional_target_attributes": [
{
"name": "resource_id",
"operator": "is_not_empty"
}
]
},
"required_config": {
"description": "allowed_gb\u003c=20 \u0026\u0026 location=='us-east'",
"and": [
{
"property": "allowed_gb",
"operator": "num_less_than_equals",
"value": "20"
},
{
"property": "location",
"operator": "string_equals",
"value": "us-east"
}
]
},
"enforcement_actions": [
{
"action": "disallow"
}
],
"labels": [
"GoSDKIntegrationTest"
]
}
}
],
"Transaction-Id": "bb5bac98-fa55-4125-97a8-578811c39c81",
"Headers": null
}`

testRoundTripBytes(t, []byte(jsonString))
}

func TestGzipCompressionJSON2(t *testing.T) {
s := make([]string, 0)

// Create a large string slice with repeated values, which will result in a small compressed string.
for i := 0; i < 100000; i++ {
s = append(s, "This")
s = append(s, "is")
s = append(s, "a")
s = append(s, "test")
s = append(s, "that ")
s = append(s, "should")
s = append(s, "demonstrate")
s = append(s, "lots")
s = append(s, "of")
s = append(s, "compression")
}

jsonString := toJSON(s)

testRoundTripBytes(t, []byte(jsonString))
}
Loading

0 comments on commit 397cbaa

Please sign in to comment.