Merge pull request 'Bump mcdsl for $PORT env var support' (#1) from feature/port-env-adoption into master
This commit was merged in pull request #1.
This commit is contained in:
2
go.mod
2
go.mod
@@ -3,7 +3,7 @@ module git.wntrmute.dev/kyle/mcr
|
|||||||
go 1.25.7
|
go 1.25.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.wntrmute.dev/kyle/mcdsl v1.0.0
|
git.wntrmute.dev/kyle/mcdsl v1.1.0
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,5 +1,5 @@
|
|||||||
git.wntrmute.dev/kyle/mcdsl v1.0.0 h1:YB7dx4gdNYKKcVySpL6UkwHqdCJ9Nl1yS0+eHk0hNtk=
|
git.wntrmute.dev/kyle/mcdsl v1.1.0 h1:NXfEXRtaCRPNjCbqqgU7L2SgDAZkQn9kd40xJDgxnns=
|
||||||
git.wntrmute.dev/kyle/mcdsl v1.0.0/go.mod h1:wo0tGfUAxci3XnOe4/rFmR0RjUElKdYUazc+Np986sg=
|
git.wntrmute.dev/kyle/mcdsl v1.1.0/go.mod h1:wo0tGfUAxci3XnOe4/rFmR0RjUElKdYUazc+Np986sg=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ type Server struct {
|
|||||||
//
|
//
|
||||||
// If certFile or keyFile is empty, TLS is skipped (for testing only).
|
// If certFile or keyFile is empty, TLS is skipped (for testing only).
|
||||||
func New(certFile, keyFile string, deps Deps, logger *slog.Logger) (*Server, error) {
|
func New(certFile, keyFile string, deps Deps, logger *slog.Logger) (*Server, error) {
|
||||||
srv, err := mcdslgrpc.New(certFile, keyFile, deps.Authenticator, methodMap(), logger)
|
srv, err := mcdslgrpc.New(certFile, keyFile, deps.Authenticator, methodMap(), logger, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -48,21 +48,45 @@ type Server struct {
|
|||||||
listener net.Listener
|
listener net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Options configures optional behavior for the gRPC server.
|
||||||
|
type Options struct {
|
||||||
|
// PreInterceptors run before the logging and auth interceptors.
|
||||||
|
// Use for lifecycle gates like seal checks that should reject
|
||||||
|
// requests before any auth validation occurs.
|
||||||
|
PreInterceptors []grpc.UnaryServerInterceptor
|
||||||
|
|
||||||
|
// PostInterceptors run after auth but before the handler.
|
||||||
|
// Use for audit logging, rate limiting, or other cross-cutting
|
||||||
|
// concerns that need access to the authenticated identity.
|
||||||
|
PostInterceptors []grpc.UnaryServerInterceptor
|
||||||
|
}
|
||||||
|
|
||||||
// New creates a gRPC server with TLS (if certFile and keyFile are
|
// New creates a gRPC server with TLS (if certFile and keyFile are
|
||||||
// non-empty) and an interceptor chain: logging → auth → handler.
|
// non-empty) and an interceptor chain:
|
||||||
|
//
|
||||||
|
// [pre-interceptors] → logging → auth → [post-interceptors] → handler
|
||||||
//
|
//
|
||||||
// The auth interceptor uses methods to determine the access level for
|
// The auth interceptor uses methods to determine the access level for
|
||||||
// each RPC. Methods not in any map are denied by default.
|
// each RPC. Methods not in any map are denied by default.
|
||||||
//
|
//
|
||||||
// If certFile and keyFile are empty, TLS is skipped (for testing).
|
// If certFile and keyFile are empty, TLS is skipped (for testing).
|
||||||
func New(certFile, keyFile string, authenticator *auth.Authenticator, methods MethodMap, logger *slog.Logger) (*Server, error) {
|
// opts is optional; pass nil for the default chain (logging + auth only).
|
||||||
chain := grpc.ChainUnaryInterceptor(
|
func New(certFile, keyFile string, authenticator *auth.Authenticator, methods MethodMap, logger *slog.Logger, opts *Options) (*Server, error) {
|
||||||
|
var interceptors []grpc.UnaryServerInterceptor
|
||||||
|
if opts != nil {
|
||||||
|
interceptors = append(interceptors, opts.PreInterceptors...)
|
||||||
|
}
|
||||||
|
interceptors = append(interceptors,
|
||||||
loggingInterceptor(logger),
|
loggingInterceptor(logger),
|
||||||
authInterceptor(authenticator, methods),
|
authInterceptor(authenticator, methods),
|
||||||
)
|
)
|
||||||
|
if opts != nil {
|
||||||
|
interceptors = append(interceptors, opts.PostInterceptors...)
|
||||||
|
}
|
||||||
|
chain := grpc.ChainUnaryInterceptor(interceptors...)
|
||||||
|
|
||||||
var opts []grpc.ServerOption
|
var serverOpts []grpc.ServerOption
|
||||||
opts = append(opts, chain)
|
serverOpts = append(serverOpts, chain)
|
||||||
|
|
||||||
if certFile != "" && keyFile != "" {
|
if certFile != "" && keyFile != "" {
|
||||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
@@ -73,11 +97,11 @@ func New(certFile, keyFile string, authenticator *auth.Authenticator, methods Me
|
|||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
MinVersion: tls.VersionTLS13,
|
MinVersion: tls.VersionTLS13,
|
||||||
}
|
}
|
||||||
opts = append(opts, grpc.Creds(credentials.NewTLS(tlsCfg)))
|
serverOpts = append(serverOpts, grpc.Creds(credentials.NewTLS(tlsCfg)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
GRPCServer: grpc.NewServer(opts...),
|
GRPCServer: grpc.NewServer(serverOpts...),
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@@ -1,4 +1,4 @@
|
|||||||
# git.wntrmute.dev/kyle/mcdsl v1.0.0
|
# git.wntrmute.dev/kyle/mcdsl v1.1.0
|
||||||
## explicit; go 1.25.7
|
## explicit; go 1.25.7
|
||||||
git.wntrmute.dev/kyle/mcdsl/auth
|
git.wntrmute.dev/kyle/mcdsl/auth
|
||||||
git.wntrmute.dev/kyle/mcdsl/config
|
git.wntrmute.dev/kyle/mcdsl/config
|
||||||
|
|||||||
Reference in New Issue
Block a user