Merge pull request 'Add $PORT env var overrides for MCP agent port assignment' (#1) from port-env-support into master
This commit was merged in pull request #1.
This commit is contained in:
@@ -144,6 +144,8 @@ func Load[T any](path string, envPrefix string) (*T, error) {
|
|||||||
applyEnvToStruct(reflect.ValueOf(&cfg).Elem(), envPrefix)
|
applyEnvToStruct(reflect.ValueOf(&cfg).Elem(), envPrefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyPortEnv(&cfg)
|
||||||
|
|
||||||
applyBaseDefaults(&cfg)
|
applyBaseDefaults(&cfg)
|
||||||
|
|
||||||
if err := validateBase(&cfg); err != nil {
|
if err := validateBase(&cfg); err != nil {
|
||||||
@@ -239,6 +241,70 @@ func findBase(cfg any) *Base {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyPortEnv overrides ServerConfig.ListenAddr and ServerConfig.GRPCAddr
|
||||||
|
// from $PORT and $PORT_GRPC respectively. These environment variables are
|
||||||
|
// set by the MCP agent to assign authoritative port bindings, so they take
|
||||||
|
// precedence over both TOML values and generic env overrides.
|
||||||
|
func applyPortEnv(cfg any) {
|
||||||
|
sc := findServerConfig(cfg)
|
||||||
|
if sc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if port, ok := os.LookupEnv("PORT"); ok {
|
||||||
|
sc.ListenAddr = ":" + port
|
||||||
|
}
|
||||||
|
if port, ok := os.LookupEnv("PORT_GRPC"); ok {
|
||||||
|
sc.GRPCAddr = ":" + port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// findServerConfig returns a pointer to the ServerConfig in the config
|
||||||
|
// struct. It first checks for an embedded Base (which contains Server),
|
||||||
|
// then walks the struct tree via reflection to find any ServerConfig field
|
||||||
|
// directly (e.g., the Metacrypt pattern where ServerConfig is embedded
|
||||||
|
// without Base).
|
||||||
|
func findServerConfig(cfg any) *ServerConfig {
|
||||||
|
if base := findBase(cfg); base != nil {
|
||||||
|
return &base.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
return findServerConfigReflect(reflect.ValueOf(cfg))
|
||||||
|
}
|
||||||
|
|
||||||
|
// findServerConfigReflect walks the struct tree to find a ServerConfig field.
|
||||||
|
func findServerConfigReflect(v reflect.Value) *ServerConfig {
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scType := reflect.TypeOf(ServerConfig{})
|
||||||
|
t := v.Type()
|
||||||
|
for i := range t.NumField() {
|
||||||
|
field := t.Field(i)
|
||||||
|
fv := v.Field(i)
|
||||||
|
|
||||||
|
if field.Type == scType {
|
||||||
|
sc, ok := fv.Addr().Interface().(*ServerConfig)
|
||||||
|
if ok {
|
||||||
|
return sc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse into embedded or nested structs.
|
||||||
|
if fv.Kind() == reflect.Struct && field.Type != scType {
|
||||||
|
if sc := findServerConfigReflect(fv); sc != nil {
|
||||||
|
return sc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// applyEnvToStruct recursively walks a struct and overrides field values
|
// applyEnvToStruct recursively walks a struct and overrides field values
|
||||||
// from environment variables. The env variable name is built from the
|
// from environment variables. The env variable name is built from the
|
||||||
// prefix and the toml tag: PREFIX_SECTION_FIELD (uppercased).
|
// prefix and the toml tag: PREFIX_SECTION_FIELD (uppercased).
|
||||||
|
|||||||
@@ -401,3 +401,109 @@ func TestEmptyEnvPrefix(t *testing.T) {
|
|||||||
t.Fatalf("ListenAddr = %q, want %q", cfg.Server.ListenAddr, ":8443")
|
t.Fatalf("ListenAddr = %q, want %q", cfg.Server.ListenAddr, ":8443")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// directServerConfig embeds ServerConfig without Base (Metacrypt pattern).
|
||||||
|
type directServerConfig struct {
|
||||||
|
Server ServerConfig `toml:"server"`
|
||||||
|
Extra string `toml:"extra"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPortEnvOverridesListenAddr(t *testing.T) {
|
||||||
|
path := writeTOML(t, minimalTOML)
|
||||||
|
t.Setenv("PORT", "9999")
|
||||||
|
|
||||||
|
cfg, err := Load[testConfig](path, "TEST")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Server.ListenAddr != ":9999" {
|
||||||
|
t.Fatalf("ListenAddr = %q, want %q", cfg.Server.ListenAddr, ":9999")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPortGRPCEnvOverridesGRPCAddr(t *testing.T) {
|
||||||
|
path := writeTOML(t, minimalTOML)
|
||||||
|
t.Setenv("PORT_GRPC", "9998")
|
||||||
|
|
||||||
|
cfg, err := Load[testConfig](path, "TEST")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Server.GRPCAddr != ":9998" {
|
||||||
|
t.Fatalf("GRPCAddr = %q, want %q", cfg.Server.GRPCAddr, ":9998")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPortEnvOverridesTOMLValue(t *testing.T) {
|
||||||
|
// fullTOML sets listen_addr = ":8443" and grpc_addr = ":9443".
|
||||||
|
path := writeTOML(t, fullTOML)
|
||||||
|
t.Setenv("PORT", "9999")
|
||||||
|
|
||||||
|
cfg, err := Load[testConfig](path, "TEST")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Server.ListenAddr != ":9999" {
|
||||||
|
t.Fatalf("ListenAddr = %q, want %q ($PORT should override TOML)", cfg.Server.ListenAddr, ":9999")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPortEnvOverridesGenericEnv(t *testing.T) {
|
||||||
|
path := writeTOML(t, minimalTOML)
|
||||||
|
t.Setenv("TEST_SERVER_LISTEN_ADDR", ":7777")
|
||||||
|
t.Setenv("PORT", "9999")
|
||||||
|
|
||||||
|
cfg, err := Load[testConfig](path, "TEST")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Server.ListenAddr != ":9999" {
|
||||||
|
t.Fatalf("ListenAddr = %q, want %q ($PORT should override generic env)", cfg.Server.ListenAddr, ":9999")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoPortEnvNoChange(t *testing.T) {
|
||||||
|
path := writeTOML(t, minimalTOML)
|
||||||
|
|
||||||
|
cfg, err := Load[testConfig](path, "TEST")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// minimalTOML sets listen_addr = ":8443", no $PORT set.
|
||||||
|
if cfg.Server.ListenAddr != ":8443" {
|
||||||
|
t.Fatalf("ListenAddr = %q, want %q (TOML value preserved without $PORT)", cfg.Server.ListenAddr, ":8443")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPortEnvDirectServerConfig(t *testing.T) {
|
||||||
|
// Test the Metacrypt pattern: ServerConfig embedded without Base.
|
||||||
|
toml := `
|
||||||
|
[server]
|
||||||
|
listen_addr = ":8443"
|
||||||
|
tls_cert = "/tmp/cert.pem"
|
||||||
|
tls_key = "/tmp/key.pem"
|
||||||
|
grpc_addr = ":9443"
|
||||||
|
|
||||||
|
extra = "value"
|
||||||
|
`
|
||||||
|
path := writeTOML(t, toml)
|
||||||
|
t.Setenv("PORT", "5555")
|
||||||
|
t.Setenv("PORT_GRPC", "5556")
|
||||||
|
|
||||||
|
cfg, err := Load[directServerConfig](path, "TEST")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Server.ListenAddr != ":5555" {
|
||||||
|
t.Fatalf("ListenAddr = %q, want %q ($PORT on direct ServerConfig)", cfg.Server.ListenAddr, ":5555")
|
||||||
|
}
|
||||||
|
if cfg.Server.GRPCAddr != ":5556" {
|
||||||
|
t.Fatalf("GRPCAddr = %q, want %q ($PORT_GRPC on direct ServerConfig)", cfg.Server.GRPCAddr, ":5556")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user