Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,40 @@ func (c *Config) LoadPluginEnvVars(ctx context.Context) *gerr.GatewayDError {
}

func loadEnvVars() *env.Env {
return env.Provider(EnvPrefix, ".", func(env string) string {
return strings.ReplaceAll(strings.ToLower(strings.TrimPrefix(env, EnvPrefix)), "_", ".")
})
return env.Provider(EnvPrefix, ".", transformEnvVariable)
}

// transformEnvVariable transforms the environment variable name to a format based on JSON tags.
func transformEnvVariable(envVar string) string {
structs := []interface{}{
&API{},
&Logger{},
&Pool{},
&Proxy{},
&Server{},
&Metrics{},
&PluginConfig{},
}
tagMapping := make(map[string]string)
generateTagMapping(structs, tagMapping)

lowerEnvVar := strings.ToLower(strings.TrimPrefix(envVar, EnvPrefix))
parts := strings.Split(lowerEnvVar, "_")

var transformedParts strings.Builder

for i, part := range parts {
if i > 0 {
transformedParts.WriteString(".")
}
if mappedValue, exists := tagMapping[part]; exists {
transformedParts.WriteString(mappedValue)
} else {
transformedParts.WriteString(part)
}
}

return transformedParts.String()
}

// LoadGlobalConfigFile loads the plugin configuration file.
Expand Down Expand Up @@ -594,3 +625,28 @@ func (c *Config) ValidateGlobalConfig(ctx context.Context) *gerr.GatewayDError {

return nil
}

// generateTagMapping generates a map of JSON tags to lower case json tags.
func generateTagMapping(structs []interface{}, tagMapping map[string]string) {
for _, s := range structs {
structValue := reflect.ValueOf(s).Elem()
structType := structValue.Type()

for i := range structValue.NumField() {
field := structType.Field(i)
fieldValue := structValue.Field(i)

// Handle nested structs
if field.Type.Kind() == reflect.Struct {
generateTagMapping([]interface{}{fieldValue.Addr().Interface()}, tagMapping)
}

jsonTag := field.Tag.Get("json")
if jsonTag != "" {
tagMapping[strings.ToLower(jsonTag)] = jsonTag
} else {
tagMapping[strings.ToLower(field.Name)] = strings.ToLower(field.Name)
}
}
}
}
61 changes: 61 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package config

import (
"context"
"fmt"
"strings"
"testing"

"github.com/knadh/koanf"
Expand Down Expand Up @@ -142,3 +144,62 @@ func TestMergeGlobalConfig(t *testing.T) {
// The log level should now be debug.
assert.Equal(t, "debug", config.Global.Loggers[Default].Level)
}

// initializeConfig initializes the configuration with the given context.
// It returns a pointer to the Config struct. If configuration initialization fails,
// the test will fail with an error message.
func initializeConfig(ctx context.Context, t *testing.T) *Config {
t.Helper()
config := NewConfig(ctx, Config{
GlobalConfigFile: parentDir + GlobalConfigFilename,
PluginConfigFile: parentDir + PluginsConfigFilename,
})
err := config.InitConfig(ctx)
require.Nil(t, err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
require.Nil(t, err)
require.NoError(t, err)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config.InitConfig returns a errors.GatewayDError, not a standard error. so, it’s an object, and we can assert it using require.NoError.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So strange, I would have added the Error() string interface to this struct to solve this.

If you don't/cannot want to do this, you can simply rename the variable err, err is type connoted.

Useres or whatever

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GatewayDError is actually an implementation of an error, and we have Error() string. The issue is that our functions return *gerr.GatewayDError instead of the error type. When we return nil as *gerr.GatewayDError, it’s basically like returning (gerr.GatewayDError, nil), and require.NoError can’t handle that. This link explains it better: Go FAQ - nil error.

The solution is to change all return types to the error type. I assume this is outside the scope of this PR.

Renaming it to something else isn’t a great idea because it’s really just an error. Later on, if we switch to using error, it’ll be easier to handle.

return config
}

// serverLoadBalancerStrategyOverwrite sets the environment variable for server nested configuration
// and verifies that the configuration is correctly loaded with the expected value.
func ServerLoadBalancerStrategyOverwrite(t *testing.T) {
t.Helper()
ctx := context.Background()
// Convert to uppercase
upperDefaultGroup := strings.ToUpper(Default)

// Format environment variable name
envVarName := fmt.Sprintf("GATEWAYD_SERVERS_%s_LOADBALANCER_STRATEGY", upperDefaultGroup)

// Set the environment variable
t.Setenv(envVarName, "test")
config := initializeConfig(ctx, t)
assert.Equal(t, "test", config.Global.Servers[Default].LoadBalancer.Strategy)
}

// pluginDefaultPolicyOverwrite sets the environment variable for plugin configuration
// and verifies that the configuration is correctly loaded with the expected value.
func pluginDefaultPolicyOverwrite(t *testing.T) {
t.Helper()
ctx := context.Background()

// Set the environment variable
t.Setenv("GATEWAYD_DEFAULTPOLICY", "test")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected

Suggested change
t.Setenv("GATEWAYD_DEFAULTPOLICY", "test")
t.Setenv("GATEWAYD_DEFAULT_POLICY", "test")

Is there a problem or something to code differently?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a test case for the plugin. In the plugin configuration, there isn't a configuration group, so DefaultPolicy is a parameter directly within gatewayd_plugins.yaml.

CompatibilityPolicy: "strict"
DefaultPolicy: "passthrough"

config := initializeConfig(ctx, t)
assert.Equal(t, "test", config.Plugin.DefaultPolicy)
}

// TestLoadEnvVariables runs a suite of tests to verify that environment variables are correctly
// loaded into the configuration. Each test scenario sets a specific environment variable and
// checks if the configuration reflects the expected value.
func TestLoadEnvVariables(t *testing.T) {
scenarios := map[string]func(t *testing.T){
"serverLoadBalancerStrategyOverwrite": ServerLoadBalancerStrategyOverwrite,
"pluginLocalPathOverwrite": pluginDefaultPolicyOverwrite,
}

for scenario, fn := range scenarios {
t.Run(scenario, func(t *testing.T) {
fn(t)
})
}
}