Skip to content

Commit

Permalink
Merge pull request #154 from launchdarkly/eb/ch83954/enterprise-1
Browse files Browse the repository at this point in the history
(RPE - #1) add Relay Proxy Enterprise basic infrastructure and config
  • Loading branch information
eli-darkly authored Aug 6, 2020
2 parents 58ff55f + 5d762b5 commit cf2b224
Show file tree
Hide file tree
Showing 20 changed files with 662 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ issues:
- goconst
- gochecknoglobals
- golint
- path: enterprise/ld-relay-enterprise.go # temporary exclusion because linter is confused by two command-line entry points
linters:
- deadcode
- golint
- unused
exclude-use-default: false
max-same-issues: 1000
max-per-linter: 1000
8 changes: 4 additions & 4 deletions core/application/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import (
"fmt"
"net/http"

"github.com/launchdarkly/ld-relay/v6/core/config"
"gopkg.in/launchdarkly/go-sdk-common.v2/ldlog"
)

// StartHTTPServer starts the server, with or without TLS. It returns immediately, starting the server
// on a separate goroutine; if the server fails to start up, it sends an error to the error channel.
func StartHTTPServer(
mainConfig config.MainConfig,
port int,
handler http.Handler,
tls bool,
tlsCertFile, tlsKeyFile string,
loggers ldlog.Loggers,
) <-chan error {
srv := &http.Server{
Expand All @@ -26,9 +26,9 @@ func StartHTTPServer(
go func() {
var err error
loggers.Infof("Starting server listening on port %d\n", port)
if mainConfig.TLSEnabled {
if tls {
loggers.Infof("TLS Enabled for server")
err = srv.ListenAndServeTLS(mainConfig.TLSCert, mainConfig.TLSKey)
err = srv.ListenAndServeTLS(tlsCertFile, tlsKeyFile)
} else {
err = srv.ListenAndServe()
}
Expand Down
18 changes: 13 additions & 5 deletions core/config/config_from_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ func errObsoleteVariableWithReplacement(preferredName string) error {
//
// The Config parameter should be initialized with default values first.
func LoadConfigFromEnvironment(c *Config, loggers ldlog.Loggers) error {
result := LoadConfigFromEnvironmentBase(c, loggers)

if !result.OK() {
return result.GetError()
}

return ValidateConfig(c, loggers)
}

// LoadConfigFromEnvironmentBase performs the initial steps of reading Config fields from
// environment variables, but returns the intermediate result before fully validating it.
func LoadConfigFromEnvironmentBase(c *Config, loggers ldlog.Loggers) ct.ValidationResult {
reader := ct.NewVarReaderFromEnvironment()

reader.ReadStruct(&c.Main, false)
Expand Down Expand Up @@ -106,11 +118,7 @@ func LoadConfigFromEnvironment(c *Config, loggers ldlog.Loggers) error {

reader.ReadStruct(&c.Proxy, false)

if !reader.Result().OK() {
return reader.Result().GetError()
}

return ValidateConfig(c, loggers)
return reader.Result()
}

func rejectObsoleteVariableName(oldName, preferredName string, reader *ct.VarReader) {
Expand Down
35 changes: 35 additions & 0 deletions enterprise/entconfig/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package entconfig

import (
ct "github.com/launchdarkly/go-configtypes"
"github.com/launchdarkly/ld-relay/v6/core/config"
)

// This file contains extended configuration for Relay Proxy Enterprise. It will be moved to a
// separate project later in development.

const (
// AutoConfigEnvironmentIDPlaceholder is a string that can appear within
// AutoConfigConfig.EnvDataStorePrefix or AutoConfigConfig.EnvDataStoreTableName to indicate that
// the environment ID should be substituted at that point.
//
// For instance, if EnvDataStorePrefix is "LD-$CID", the value of that setting for an environment
// whose ID is "12345" would be "LD-12345".
AutoConfigEnvironmentIDPlaceholder = "$CID"
)

// EnterpriseConfig describes the configuration for a RelayEnterprise instance.
//
// This is mostly the same as regular Relay, with some added options.
type EnterpriseConfig struct {
config.Config
AutoConfig AutoConfigConfig
}

// AutoConfigConfig contains configuration parameters for the auto-configuration feature.
type AutoConfigConfig struct {
Key AutoConfigKey `conf:"AUTO_CONFIG_KEY"`
EnvDatastorePrefix string `conf:"ENV_DATASTORE_PREFIX"`
EnvDatastoreTableName string `conf:"ENV_DATASTORE_TABLE_NAME"`
EnvAllowedOrigin ct.OptStringList `conf:"ENV_ALLOWED_ORIGIN"`
}
17 changes: 17 additions & 0 deletions enterprise/entconfig/config_field_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package entconfig

// AutoConfigKey is a type tag to indicate when a string is used as an auto-config key.
type AutoConfigKey string

// GetAuthorizationHeaderValue for AutoConfigKey returns the same string, since these keys are passed in
// the Authorization header. Note that unlike the other kinds of authorization keys, this one is never
// present in an incoming request; it is only used in requests from Relay to LaunchDarkly.
func (k AutoConfigKey) GetAuthorizationHeaderValue() string {
return string(k)
}

// UnmarshalText allows the AutoConfigKey type to be set from environment variables.
func (k *AutoConfigKey) UnmarshalText(data []byte) error {
*k = AutoConfigKey(string(data))
return nil
}
11 changes: 11 additions & 0 deletions enterprise/entconfig/config_field_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package entconfig

import (
"testing"

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

func TestAutoConfigKey(t *testing.T) {
assert.Equal(t, "123", AutoConfigKey("123").GetAuthorizationHeaderValue())
}
27 changes: 27 additions & 0 deletions enterprise/entconfig/config_from_env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package entconfig

import (
ct "github.com/launchdarkly/go-configtypes"
"github.com/launchdarkly/ld-relay/v6/core/config"
"gopkg.in/launchdarkly/go-sdk-common.v2/ldlog"
)

// LoadConfigFromEnvironment sets parameters in an EnterpriseConfig struct from environment variables.
//
// The Config parameter should be initialized with default values first.
func LoadConfigFromEnvironment(c *EnterpriseConfig, loggers ldlog.Loggers) error {
reader := ct.NewVarReaderFromEnvironment()

baseResult := config.LoadConfigFromEnvironmentBase(&c.Config, loggers)
for _, e := range baseResult.Errors() {
reader.AddError(e.Path, e.Err)
}

reader.ReadStruct(&c.AutoConfig, false)

if !reader.Result().OK() {
return reader.Result().GetError()
}

return ValidateConfig(c, loggers)
}
71 changes: 71 additions & 0 deletions enterprise/entconfig/config_from_env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package entconfig

import (
"os"
"strings"
"testing"

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

"gopkg.in/launchdarkly/go-sdk-common.v2/ldlog"
)

func TestConfigFromEnvironmentWithValidProperties(t *testing.T) {
for _, tdc := range makeValidConfigs() {
t.Run(tdc.name, func(t *testing.T) {
testValidConfigVars(t, tdc.makeConfig, tdc.envVars)
})
}
}

func TestConfigFromEnvironmentWithInvalidProperties(t *testing.T) {
for _, tdc := range makeInvalidConfigs() {
if len(tdc.envVars) != 0 {
t.Run(tdc.name, func(t *testing.T) {
testInvalidConfigVars(t, tdc.envVars, tdc.envVarsError)
})
}
}
}

func testValidConfigVars(t *testing.T, buildConfig func(c *EnterpriseConfig), vars map[string]string) {
withEnvironment(vars, func() {
var expectedConfig EnterpriseConfig
buildConfig(&expectedConfig)

var c EnterpriseConfig
err := LoadConfigFromEnvironment(&c, ldlog.NewDisabledLoggers())
require.NoError(t, err)

assert.Equal(t, expectedConfig, c)
})
}

func testInvalidConfigVars(t *testing.T, vars map[string]string, errMessage string) {
withEnvironment(vars, func() {
var c EnterpriseConfig
err := LoadConfigFromEnvironment(&c, ldlog.NewDisabledLoggers())
require.Error(t, err)
assert.Contains(t, err.Error(), errMessage)
})
}

func withEnvironment(vars map[string]string, action func()) {
saved := make(map[string]string)
for _, kv := range os.Environ() {
p := strings.Index(kv, "=")
saved[kv[:p]] = kv[p+1:]
}
defer func() {
os.Clearenv()
for k, v := range saved {
os.Setenv(k, v)
}
}()
os.Clearenv()
for k, v := range vars {
os.Setenv(k, v)
}
action()
}
19 changes: 19 additions & 0 deletions enterprise/entconfig/config_from_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package entconfig

import (
"fmt"

"github.com/launchdarkly/gcfg"
"gopkg.in/launchdarkly/go-sdk-common.v2/ldlog"
)

// LoadConfigFile reads a configuration file into an EnterpriseConfig struct and performs basic validation.
//
// The Config parameter could be initialized with default values first, but does not need to be.
func LoadConfigFile(c *EnterpriseConfig, path string, loggers ldlog.Loggers) error {
if err := gcfg.ReadFileInto(c, path); err != nil {
return fmt.Errorf(`failed to read configuration file "%s": %w`, path, err)
}

return ValidateConfig(c, loggers)
}
66 changes: 66 additions & 0 deletions enterprise/entconfig/config_from_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package entconfig

import (
"io/ioutil"
"testing"

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

"gopkg.in/launchdarkly/go-sdk-common.v2/ldlog"

helpers "github.com/launchdarkly/go-test-helpers/v2"
)

func TestConfigFromFileWithValidProperties(t *testing.T) {
for _, tdc := range makeValidConfigs() {
if tdc.fileContent == "" {
// some tests only apply to environment variables, not files
continue
}
t.Run(tdc.name, func(t *testing.T) {
testFileWithValidConfig(t, tdc.makeConfig, tdc.fileContent)
})
}
}

func TestConfigFromFileWithInvalidProperties(t *testing.T) {
for _, tdc := range makeInvalidConfigs() {
if tdc.fileContent == "" {
// some tests only apply to environment variables, not files
continue
}
t.Run(tdc.name, func(t *testing.T) {
e := tdc.fileError
if e == "" {
e = tdc.envVarsError
}
testFileWithInvalidConfig(t, tdc.fileContent, e)
})
}
}

func testFileWithValidConfig(t *testing.T, buildConfig func(c *EnterpriseConfig), fileContent string) {
var expectedConfig EnterpriseConfig
buildConfig(&expectedConfig)

helpers.WithTempFile(func(filename string) {
require.NoError(t, ioutil.WriteFile(filename, []byte(fileContent), 0))

var c EnterpriseConfig
err := LoadConfigFile(&c, filename, ldlog.NewDisabledLoggers())
require.NoError(t, err)
assert.Equal(t, expectedConfig, c)
})
}

func testFileWithInvalidConfig(t *testing.T, fileContent string, errMessage string) {
helpers.WithTempFile(func(filename string) {
require.NoError(t, ioutil.WriteFile(filename, []byte(fileContent), 0))

var c EnterpriseConfig
err := LoadConfigFile(&c, filename, ldlog.NewDisabledLoggers())
require.Error(t, err)
assert.Contains(t, err.Error(), errMessage)
})
}
1 change: 1 addition & 0 deletions enterprise/entconfig/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package entconfig
51 changes: 51 additions & 0 deletions enterprise/entconfig/config_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package entconfig

import (
"errors"

ct "github.com/launchdarkly/go-configtypes"
"github.com/launchdarkly/ld-relay/v6/core/config"
"gopkg.in/launchdarkly/go-sdk-common.v2/ldlog"
)

var (
errAutoConfPropertiesWithNoKey = errors.New("must specify auto-configuration key if other auto-configuration properties are set")
errAutoConfWithEnvironments = errors.New("cannot configure specific environments if auto-configuration is enabled")
)

// ValidateConfig ensures that the configuration does not contain contradictory properties.
//
// This method covers validation rules that can't be enforced on a per-field basis (for instance, if
// either field A or field B can be specified but it's invalid to specify both). It is allowed to modify
// the Config struct in order to canonicalize settings in a way that simplifies things for the Relay code
// (for instance, converting Redis host/port settings into a Redis URL, or converting deprecated fields to
// non-deprecated ones).
//
// LoadConfigFromEnvironment and LoadConfigFromFile both call this method as a last step.
func ValidateConfig(c *EnterpriseConfig, loggers ldlog.Loggers) error {
var result ct.ValidationResult

baseError := config.ValidateConfig(&c.Config, loggers)
if baseError != nil {
if ae, ok := baseError.(ct.ValidationAggregateError); ok {
for _, e := range ae {
result.AddError(e.Path, e.Err)
}
} else if e, ok := baseError.(ct.ValidationError); ok {
result.AddError(e.Path, e.Err)
} else {
result.AddError(nil, baseError)
}
}

if c.AutoConfig.Key == "" {
if c.AutoConfig.EnvDatastorePrefix != "" || c.AutoConfig.EnvDatastoreTableName != "" ||
len(c.AutoConfig.EnvAllowedOrigin.Values()) != 0 {
result.AddError(nil, errAutoConfPropertiesWithNoKey)
}
} else if len(c.Environment) != 0 {
result.AddError(nil, errAutoConfWithEnvironments)
}

return result.GetError()
}
2 changes: 2 additions & 0 deletions enterprise/entconfig/package_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package entconfig contains the additional configuration schema for Relay Proxy Enterprise.
package entconfig
Loading

0 comments on commit cf2b224

Please sign in to comment.