TUN-5801: Add custom wrapper for OriginConfig for JSON serde

This commit is contained in:
João Oliveirinha 2022-03-08 16:10:24 +00:00
parent 9552bb7bc7
commit 5352b3cf04
5 changed files with 111 additions and 42 deletions

View File

@ -1,12 +1,14 @@
package config package config
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"time" "time"
homedir "github.com/mitchellh/go-homedir" homedir "github.com/mitchellh/go-homedir"
@ -190,17 +192,17 @@ type UnvalidatedIngressRule struct {
// - To specify a time.Duration in json, use int64 of the nanoseconds // - To specify a time.Duration in json, use int64 of the nanoseconds
type OriginRequestConfig struct { type OriginRequestConfig struct {
// HTTP proxy timeout for establishing a new connection // HTTP proxy timeout for establishing a new connection
ConnectTimeout *time.Duration `yaml:"connectTimeout" json:"connectTimeout"` ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout"`
// HTTP proxy timeout for completing a TLS handshake // HTTP proxy timeout for completing a TLS handshake
TLSTimeout *time.Duration `yaml:"tlsTimeout" json:"tlsTimeout"` TLSTimeout *CustomDuration `yaml:"tlsTimeout" json:"tlsTimeout"`
// HTTP proxy TCP keepalive duration // HTTP proxy TCP keepalive duration
TCPKeepAlive *time.Duration `yaml:"tcpKeepAlive" json:"tcpKeepAlive"` TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive"`
// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback // HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
NoHappyEyeballs *bool `yaml:"noHappyEyeballs" json:"noHappyEyeballs"` NoHappyEyeballs *bool `yaml:"noHappyEyeballs" json:"noHappyEyeballs"`
// HTTP proxy maximum keepalive connection pool size // HTTP proxy maximum keepalive connection pool size
KeepAliveConnections *int `yaml:"keepAliveConnections" json:"keepAliveConnections"` KeepAliveConnections *int `yaml:"keepAliveConnections" json:"keepAliveConnections"`
// HTTP proxy timeout for closing an idle connection // HTTP proxy timeout for closing an idle connection
KeepAliveTimeout *time.Duration `yaml:"keepAliveTimeout" json:"keepAliveTimeout"` KeepAliveTimeout *CustomDuration `yaml:"keepAliveTimeout" json:"keepAliveTimeout"`
// Sets the HTTP Host header for the local webserver. // Sets the HTTP Host header for the local webserver.
HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader"` HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader"`
// Hostname on the origin server certificate. // Hostname on the origin server certificate.
@ -399,3 +401,34 @@ func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (settings *configFileSe
return &configuration, warnings, nil return &configuration, warnings, nil
} }
// A CustomDuration is a Duration that has custom serialization for JSON.
// JSON in Javascript assumes that int fields are 32 bits and Duration fields are deserialized assuming that numbers
// are in nanoseconds, which in 32bit integers limits to just 2 seconds.
// This type assumes that when serializing/deserializing from JSON, that the number is in seconds, while it maintains
// the YAML serde assumptions.
type CustomDuration struct {
time.Duration
}
func (s *CustomDuration) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Duration.Seconds())
}
func (s *CustomDuration) UnmarshalJSON(data []byte) error {
seconds, err := strconv.ParseInt(string(data), 10, 64)
if err != nil {
return err
}
s.Duration = time.Duration(seconds * int64(time.Second))
return nil
}
func (s *CustomDuration) MarshalYAML() (interface{}, error) {
return s.Duration.String(), nil
}
func (s *CustomDuration) UnmarshalYAML(unmarshal func(interface{}) error) error {
return unmarshal(&s.Duration)
}

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
@ -110,14 +111,13 @@ counters:
} }
func TestUnmarshalOriginRequestConfig(t *testing.T) { var rawConfig = []byte(`
raw := []byte(`
{ {
"connectTimeout": 10000000000, "connectTimeout": 10,
"tlsTimeout": 30000000000, "tlsTimeout": 30,
"tcpKeepAlive": 30000000000, "tcpKeepAlive": 30,
"noHappyEyeballs": true, "noHappyEyeballs": true,
"keepAliveTimeout": 60000000000, "keepAliveTimeout": 60,
"keepAliveConnections": 10, "keepAliveConnections": 10,
"httpHostHeader": "app.tunnel.com", "httpHostHeader": "app.tunnel.com",
"originServerName": "app.tunnel.com", "originServerName": "app.tunnel.com",
@ -142,13 +142,41 @@ func TestUnmarshalOriginRequestConfig(t *testing.T) {
] ]
} }
`) `)
func TestMarshalUnmarshalOriginRequest(t *testing.T) {
testCases := []struct {
name string
marshalFunc func(in interface{}) (out []byte, err error)
unMarshalFunc func(in []byte, out interface{}) (err error)
baseUnit time.Duration
}{
{"json", json.Marshal, json.Unmarshal, time.Second},
{"yaml", yaml.Marshal, yaml.Unmarshal, time.Nanosecond},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assertConfig(t, tc.marshalFunc, tc.unMarshalFunc, tc.baseUnit)
})
}
}
func assertConfig(
t *testing.T,
marshalFunc func(in interface{}) (out []byte, err error),
unMarshalFunc func(in []byte, out interface{}) (err error),
baseUnit time.Duration,
) {
var config OriginRequestConfig var config OriginRequestConfig
assert.NoError(t, json.Unmarshal(raw, &config)) var config2 OriginRequestConfig
assert.Equal(t, time.Second*10, *config.ConnectTimeout)
assert.Equal(t, time.Second*30, *config.TLSTimeout) assert.NoError(t, unMarshalFunc(rawConfig, &config))
assert.Equal(t, time.Second*30, *config.TCPKeepAlive)
assert.Equal(t, baseUnit*10, config.ConnectTimeout.Duration)
assert.Equal(t, baseUnit*30, config.TLSTimeout.Duration)
assert.Equal(t, baseUnit*30, config.TCPKeepAlive.Duration)
assert.Equal(t, true, *config.NoHappyEyeballs) assert.Equal(t, true, *config.NoHappyEyeballs)
assert.Equal(t, time.Second*60, *config.KeepAliveTimeout) assert.Equal(t, baseUnit*60, config.KeepAliveTimeout.Duration)
assert.Equal(t, 10, *config.KeepAliveConnections) assert.Equal(t, 10, *config.KeepAliveConnections)
assert.Equal(t, "app.tunnel.com", *config.HTTPHostHeader) assert.Equal(t, "app.tunnel.com", *config.HTTPHostHeader)
assert.Equal(t, "app.tunnel.com", *config.OriginServerName) assert.Equal(t, "app.tunnel.com", *config.OriginServerName)
@ -176,4 +204,12 @@ func TestUnmarshalOriginRequestConfig(t *testing.T) {
}, },
} }
assert.Equal(t, ipRules, config.IPRules) assert.Equal(t, ipRules, config.IPRules)
// validate that serializing and deserializing again matches the deserialization from raw string
result, err := marshalFunc(config)
require.NoError(t, err)
err = unMarshalFunc(result, &config2)
require.NoError(t, err)
require.Equal(t, config2, config)
} }

View File

@ -158,13 +158,13 @@ func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig {
ProxyAddress: defaultProxyAddress, ProxyAddress: defaultProxyAddress,
} }
if c.ConnectTimeout != nil { if c.ConnectTimeout != nil {
out.ConnectTimeout = *c.ConnectTimeout out.ConnectTimeout = c.ConnectTimeout.Duration
} }
if c.TLSTimeout != nil { if c.TLSTimeout != nil {
out.TLSTimeout = *c.TLSTimeout out.TLSTimeout = c.TLSTimeout.Duration
} }
if c.TCPKeepAlive != nil { if c.TCPKeepAlive != nil {
out.TCPKeepAlive = *c.TCPKeepAlive out.TCPKeepAlive = c.TCPKeepAlive.Duration
} }
if c.NoHappyEyeballs != nil { if c.NoHappyEyeballs != nil {
out.NoHappyEyeballs = *c.NoHappyEyeballs out.NoHappyEyeballs = *c.NoHappyEyeballs
@ -173,7 +173,7 @@ func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig {
out.KeepAliveConnections = *c.KeepAliveConnections out.KeepAliveConnections = *c.KeepAliveConnections
} }
if c.KeepAliveTimeout != nil { if c.KeepAliveTimeout != nil {
out.KeepAliveTimeout = *c.KeepAliveTimeout out.KeepAliveTimeout = c.KeepAliveTimeout.Duration
} }
if c.HTTPHostHeader != nil { if c.HTTPHostHeader != nil {
out.HTTPHostHeader = *c.HTTPHostHeader out.HTTPHostHeader = *c.HTTPHostHeader
@ -257,13 +257,13 @@ type OriginRequestConfig struct {
func (defaults *OriginRequestConfig) setConnectTimeout(overrides config.OriginRequestConfig) { func (defaults *OriginRequestConfig) setConnectTimeout(overrides config.OriginRequestConfig) {
if val := overrides.ConnectTimeout; val != nil { if val := overrides.ConnectTimeout; val != nil {
defaults.ConnectTimeout = *val defaults.ConnectTimeout = val.Duration
} }
} }
func (defaults *OriginRequestConfig) setTLSTimeout(overrides config.OriginRequestConfig) { func (defaults *OriginRequestConfig) setTLSTimeout(overrides config.OriginRequestConfig) {
if val := overrides.TLSTimeout; val != nil { if val := overrides.TLSTimeout; val != nil {
defaults.TLSTimeout = *val defaults.TLSTimeout = val.Duration
} }
} }
@ -281,13 +281,13 @@ func (defaults *OriginRequestConfig) setKeepAliveConnections(overrides config.Or
func (defaults *OriginRequestConfig) setKeepAliveTimeout(overrides config.OriginRequestConfig) { func (defaults *OriginRequestConfig) setKeepAliveTimeout(overrides config.OriginRequestConfig) {
if val := overrides.KeepAliveTimeout; val != nil { if val := overrides.KeepAliveTimeout; val != nil {
defaults.KeepAliveTimeout = *val defaults.KeepAliveTimeout = val.Duration
} }
} }
func (defaults *OriginRequestConfig) setTCPKeepAlive(overrides config.OriginRequestConfig) { func (defaults *OriginRequestConfig) setTCPKeepAlive(overrides config.OriginRequestConfig) {
if val := overrides.TCPKeepAlive; val != nil { if val := overrides.TCPKeepAlive; val != nil {
defaults.TCPKeepAlive = *val defaults.TCPKeepAlive = val.Duration
} }
} }

View File

@ -191,12 +191,12 @@ ingress:
rawConfig := []byte(` rawConfig := []byte(`
{ {
"originRequest": { "originRequest": {
"connectTimeout": 60000000000, "connectTimeout": 60,
"tlsTimeout": 1000000000, "tlsTimeout": 1,
"noHappyEyeballs": true, "noHappyEyeballs": true,
"tcpKeepAlive": 1000000000, "tcpKeepAlive": 1,
"keepAliveConnections": 1, "keepAliveConnections": 1,
"keepAliveTimeout": 1000000000, "keepAliveTimeout": 1,
"httpHostHeader": "abc", "httpHostHeader": "abc",
"originServerName": "a1", "originServerName": "a1",
"caPool": "/tmp/path0", "caPool": "/tmp/path0",
@ -228,12 +228,12 @@ ingress:
"hostname": "*", "hostname": "*",
"service": "https://localhost:8001", "service": "https://localhost:8001",
"originRequest": { "originRequest": {
"connectTimeout": 120000000000, "connectTimeout": 120,
"tlsTimeout": 2000000000, "tlsTimeout": 2,
"noHappyEyeballs": false, "noHappyEyeballs": false,
"tcpKeepAlive": 2000000000, "tcpKeepAlive": 2,
"keepAliveConnections": 2, "keepAliveConnections": 2,
"keepAliveTimeout": 2000000000, "keepAliveTimeout": 2,
"httpHostHeader": "def", "httpHostHeader": "def",
"originServerName": "b2", "originServerName": "b2",
"caPool": "/tmp/path1", "caPool": "/tmp/path1",
@ -360,12 +360,12 @@ ingress:
"hostname": "*", "hostname": "*",
"service": "https://localhost:8001", "service": "https://localhost:8001",
"originRequest": { "originRequest": {
"connectTimeout": 120000000000, "connectTimeout": 120,
"tlsTimeout": 2000000000, "tlsTimeout": 2,
"noHappyEyeballs": false, "noHappyEyeballs": false,
"tcpKeepAlive": 2000000000, "tcpKeepAlive": 2,
"keepAliveConnections": 2, "keepAliveConnections": 2,
"keepAliveTimeout": 2000000000, "keepAliveTimeout": 2,
"httpHostHeader": "def", "httpHostHeader": "def",
"originServerName": "b2", "originServerName": "b2",
"caPool": "/tmp/path1", "caPool": "/tmp/path1",

View File

@ -58,7 +58,7 @@ func TestUpdateConfiguration(t *testing.T) {
{ {
"unknown_field": "not_deserialized", "unknown_field": "not_deserialized",
"originRequest": { "originRequest": {
"connectTimeout": 90000000000, "connectTimeout": 90,
"noHappyEyeballs": true "noHappyEyeballs": true
}, },
"ingress": [ "ingress": [
@ -68,7 +68,7 @@ func TestUpdateConfiguration(t *testing.T) {
"service": "http://192.16.19.1:443", "service": "http://192.16.19.1:443",
"originRequest": { "originRequest": {
"noTLSVerify": true, "noTLSVerify": true,
"connectTimeout": 10000000000 "connectTimeout": 10
} }
}, },
{ {
@ -76,7 +76,7 @@ func TestUpdateConfiguration(t *testing.T) {
"service": "http://172.32.20.6:80", "service": "http://172.32.20.6:80",
"originRequest": { "originRequest": {
"noTLSVerify": true, "noTLSVerify": true,
"connectTimeout": 30000000000 "connectTimeout": 30
} }
}, },
{ {
@ -192,7 +192,7 @@ func TestConcurrentUpdateAndRead(t *testing.T) {
configJSONV1 = []byte(fmt.Sprintf(` configJSONV1 = []byte(fmt.Sprintf(`
{ {
"originRequest": { "originRequest": {
"connectTimeout": 90000000000, "connectTimeout": 90,
"noHappyEyeballs": true "noHappyEyeballs": true
}, },
"ingress": [ "ingress": [
@ -201,7 +201,7 @@ func TestConcurrentUpdateAndRead(t *testing.T) {
"service": "%s", "service": "%s",
"originRequest": { "originRequest": {
"httpHostHeader": "%s", "httpHostHeader": "%s",
"connectTimeout": 10000000000 "connectTimeout": 10
} }
}, },
{ {