Skip to content

Commit 5af4e44

Browse files
authored
fix: preserve env var key casing and show the env vars on the UI (#65)
1 parent 854ece9 commit 5af4e44

4 files changed

Lines changed: 100 additions & 2 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ require (
1414
github.com/spf13/viper v1.19.0
1515
github.com/stretchr/testify v1.11.1
1616
golang.org/x/sync v0.19.0
17+
gopkg.in/yaml.v3 v3.0.1
1718
)
1819

1920
require (
@@ -66,6 +67,5 @@ require (
6667
golang.org/x/text v0.31.0 // indirect
6768
golang.org/x/time v0.14.0 // indirect
6869
gopkg.in/ini.v1 v1.67.0 // indirect
69-
gopkg.in/yaml.v3 v3.0.1 // indirect
7070
gotest.tools/v3 v3.5.2 // indirect
7171
)

pkg/config/config.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/mitchellh/mapstructure"
1616
"github.com/shirou/gopsutil/v4/cpu"
1717
"github.com/spf13/viper"
18+
"gopkg.in/yaml.v3"
1819
)
1920

2021
const (
@@ -439,14 +440,19 @@ func Load(paths ...string) (*Config, error) {
439440

440441
v.SetConfigType("yaml")
441442

442-
// Load and merge configs in order.
443+
// Load and merge configs in order, collecting expanded YAML for
444+
// post-processing (Viper lowercases map keys, so we re-parse to
445+
// restore original casing for environment variables).
446+
rawYAMLs := make([]string, 0, len(paths))
447+
443448
for i, path := range paths {
444449
content, err := os.ReadFile(path)
445450
if err != nil {
446451
return nil, fmt.Errorf("reading config file %q: %w", path, err)
447452
}
448453

449454
expanded := os.ExpandEnv(string(content))
455+
rawYAMLs = append(rawYAMLs, expanded)
450456

451457
if i == 0 {
452458
if err := v.ReadConfig(strings.NewReader(expanded)); err != nil {
@@ -474,6 +480,8 @@ func Load(paths ...string) (*Config, error) {
474480
return nil, fmt.Errorf("parsing config: %w", err)
475481
}
476482

483+
restoreEnvironmentKeyCasing(&cfg, rawYAMLs)
484+
477485
cfg.applyDefaults()
478486

479487
return &cfg, nil
@@ -1183,3 +1191,39 @@ func bootstrapFCUDecodeHook() mapstructure.DecodeHookFuncType {
11831191
return data, nil
11841192
}
11851193
}
1194+
1195+
// rawClientConfig is a minimal struct used to re-parse environment map keys
1196+
// with their original casing, since Viper lowercases all map keys internally.
1197+
type rawClientConfig struct {
1198+
Client struct {
1199+
Instances []struct {
1200+
ID string `yaml:"id"`
1201+
Environment map[string]string `yaml:"environment"`
1202+
} `yaml:"instances"`
1203+
} `yaml:"client"`
1204+
}
1205+
1206+
// restoreEnvironmentKeyCasing re-parses the raw YAML to recover the original
1207+
// casing of environment variable keys that Viper lowercased.
1208+
func restoreEnvironmentKeyCasing(cfg *Config, rawYAMLs []string) {
1209+
envByID := make(map[string]map[string]string, len(cfg.Client.Instances))
1210+
1211+
for _, raw := range rawYAMLs {
1212+
var parsed rawClientConfig
1213+
if err := yaml.Unmarshal([]byte(raw), &parsed); err != nil {
1214+
continue
1215+
}
1216+
1217+
for _, inst := range parsed.Client.Instances {
1218+
if inst.Environment != nil {
1219+
envByID[inst.ID] = inst.Environment
1220+
}
1221+
}
1222+
}
1223+
1224+
for i := range cfg.Client.Instances {
1225+
if orig, ok := envByID[cfg.Client.Instances[i].ID]; ok {
1226+
cfg.Client.Instances[i].Environment = orig
1227+
}
1228+
}
1229+
}

pkg/config/config_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,39 @@ func TestGetBootstrapFCU(t *testing.T) {
765765
}
766766
}
767767

768+
func TestLoad_PreservesEnvironmentKeyCasing(t *testing.T) {
769+
configContent := `
770+
global:
771+
docker_network: test-network
772+
client:
773+
config:
774+
jwt: test-jwt
775+
genesis:
776+
geth: http://example.com/genesis.json
777+
instances:
778+
- id: test-instance
779+
client: geth
780+
environment:
781+
MAX_REORG_DEPTH: "512"
782+
SOME_lower_Mixed: "value"
783+
`
784+
tmpDir := t.TempDir()
785+
configPath := filepath.Join(tmpDir, "config.yaml")
786+
require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o644))
787+
788+
cfg, err := Load(configPath)
789+
require.NoError(t, err)
790+
require.Len(t, cfg.Client.Instances, 1)
791+
792+
env := cfg.Client.Instances[0].Environment
793+
assert.Equal(t, "512", env["MAX_REORG_DEPTH"])
794+
assert.Equal(t, "value", env["SOME_lower_Mixed"])
795+
796+
// Verify lowercased keys are NOT present.
797+
_, hasLower := env["max_reorg_depth"]
798+
assert.False(t, hasLower)
799+
}
800+
768801
func TestLoad_BootstrapFCU(t *testing.T) {
769802
t.Run("shorthand bool true", func(t *testing.T) {
770803
configContent := `

ui/src/components/run-detail/RunConfiguration.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,27 @@ export function RunConfiguration({ instance, system, startBlock }: RunConfigurat
143143
</div>
144144
)}
145145

146+
{instance.environment && Object.keys(instance.environment).length > 0 && (
147+
<div>
148+
<dt className="text-xs/5 font-medium text-gray-500 dark:text-gray-400">
149+
Environment
150+
</dt>
151+
<dd className="mt-1 overflow-x-auto rounded-xs bg-gray-100 p-2 dark:bg-gray-900">
152+
<div className="flex flex-col gap-1 font-mono text-xs/5 text-gray-900 dark:text-gray-100">
153+
{Object.entries(instance.environment).map(([key, value]) => (
154+
<div key={key} className="flex items-start gap-2">
155+
<span className="break-all">
156+
<span className="text-gray-500 dark:text-gray-400">{key}=</span>
157+
{value}
158+
</span>
159+
<CopyButton text={`${key}=${value}`} />
160+
</div>
161+
))}
162+
</div>
163+
</dd>
164+
</div>
165+
)}
166+
146167
{instance.datadir && (
147168
<div>
148169
<dt className="flex items-center gap-2 text-xs/5 font-medium text-gray-500 dark:text-gray-400">

0 commit comments

Comments
 (0)