chore: initial commit of Go API gateway
Baseline for the Kong replacement on api.riotpiao.com. Brings the working tree under version control for the first time: gateway source, the task board that drives the agent runs, test fixtures, and K8s manifests. Anchor the gateway ignore rule to the repo root. Unanchored, "gateway" also matched the cmd/gateway/ source directory, so the program entrypoint was excluded from every commit. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// rawConfig represents the structure of the YAML configuration file.
|
||||
type rawConfig struct {
|
||||
Routes []rawRoute `yaml:"routes"`
|
||||
Models []rawModel `yaml:"models"`
|
||||
}
|
||||
|
||||
// rawRoute represents a single route in the YAML configuration.
|
||||
type rawRoute struct {
|
||||
Name string `yaml:"name"`
|
||||
Upstream rawUpstream `yaml:"upstream"`
|
||||
}
|
||||
|
||||
// rawModel represents a single model entry in the YAML configuration.
|
||||
type rawModel struct {
|
||||
Name string `yaml:"name"`
|
||||
Address string `yaml:"address"`
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
|
||||
// rawUpstream represents upstream configuration in YAML.
|
||||
type rawUpstream struct {
|
||||
Address string `yaml:"address"`
|
||||
PathRewrite string `yaml:"pathRewrite"`
|
||||
ConnectTimeout string `yaml:"connectTimeout"`
|
||||
ReadTimeout string `yaml:"readTimeout"`
|
||||
WriteTimeout string `yaml:"writeTimeout"`
|
||||
MaxBodySize int64 `yaml:"maxBodySize"`
|
||||
AuthRequired *bool `yaml:"authRequired"`
|
||||
}
|
||||
|
||||
// LoadRoutesAndModelsFromFile loads both route and model configuration from a YAML file.
|
||||
func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*ModelUpstream, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read config file %q: %w", path, err)
|
||||
}
|
||||
|
||||
var raw rawConfig
|
||||
if err := yaml.Unmarshal(data, &raw); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse config file %q: %w", path, err)
|
||||
}
|
||||
|
||||
// Load routes
|
||||
routes := make(map[string]*Route)
|
||||
for _, rawRoute := range raw.Routes {
|
||||
if rawRoute.Name == "" {
|
||||
return nil, nil, fmt.Errorf("route has empty name")
|
||||
}
|
||||
|
||||
if _, exists := routes[rawRoute.Name]; exists {
|
||||
return nil, nil, fmt.Errorf("duplicate route: \"%s\"", rawRoute.Name)
|
||||
}
|
||||
|
||||
upstream, err := parseUpstream(rawRoute.Name, rawRoute.Upstream)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
routes[rawRoute.Name] = &Route{
|
||||
Name: rawRoute.Name,
|
||||
Upstream: upstream,
|
||||
}
|
||||
}
|
||||
|
||||
// Load models
|
||||
models := make(map[string]*ModelUpstream)
|
||||
for _, rawModel := range raw.Models {
|
||||
// Validate model name is not empty
|
||||
if rawModel.Name == "" {
|
||||
return nil, nil, fmt.Errorf("model has empty name")
|
||||
}
|
||||
|
||||
// Check for duplicate model names
|
||||
if _, exists := models[rawModel.Name]; exists {
|
||||
return nil, nil, fmt.Errorf("duplicate model: \"%s\"", rawModel.Name)
|
||||
}
|
||||
|
||||
// Validate address is not empty
|
||||
if rawModel.Address == "" {
|
||||
return nil, nil, fmt.Errorf("model \"%s\": field 'address' is required", rawModel.Name)
|
||||
}
|
||||
|
||||
// Validate address format (host:port)
|
||||
if _, _, err := net.SplitHostPort(rawModel.Address); err != nil {
|
||||
return nil, nil, fmt.Errorf("model \"%s\": invalid address \"%s\": %w", rawModel.Name, rawModel.Address, err)
|
||||
}
|
||||
|
||||
models[rawModel.Name] = &ModelUpstream{
|
||||
Name: rawModel.Name,
|
||||
Address: rawModel.Address,
|
||||
Path: rawModel.Path,
|
||||
}
|
||||
}
|
||||
|
||||
return routes, models, nil
|
||||
}
|
||||
|
||||
// LoadRoutesFromFile loads route configuration from a YAML file.
|
||||
// It validates that all required fields are present and have valid values.
|
||||
// Returns an error if the configuration is invalid.
|
||||
// Deprecated: Use LoadRoutesAndModelsFromFile instead.
|
||||
func LoadRoutesFromFile(path string) (map[string]*Route, error) {
|
||||
routes, _, err := LoadRoutesAndModelsFromFile(path)
|
||||
return routes, err
|
||||
}
|
||||
|
||||
// parseUpstream validates and parses upstream configuration from raw YAML.
|
||||
func parseUpstream(routeName string, raw rawUpstream) (Upstream, error) {
|
||||
// Validate address is not empty
|
||||
if raw.Address == "" {
|
||||
return Upstream{}, fmt.Errorf("route \"%s\": field 'address' is required", routeName)
|
||||
}
|
||||
|
||||
// Validate address format (host:port)
|
||||
if _, _, err := net.SplitHostPort(raw.Address); err != nil {
|
||||
return Upstream{}, fmt.Errorf("route \"%s\": invalid address \"%s\": %w", routeName, raw.Address, err)
|
||||
}
|
||||
|
||||
// Validate connectTimeout
|
||||
if raw.ConnectTimeout == "" {
|
||||
return Upstream{}, fmt.Errorf("route \"%s\": field 'connectTimeout' is required", routeName)
|
||||
}
|
||||
connectTimeout, err := time.ParseDuration(raw.ConnectTimeout)
|
||||
if err != nil {
|
||||
return Upstream{}, fmt.Errorf("route \"%s\": invalid connectTimeout \"%s\": %w", routeName, raw.ConnectTimeout, err)
|
||||
}
|
||||
|
||||
// Validate readTimeout
|
||||
if raw.ReadTimeout == "" {
|
||||
return Upstream{}, fmt.Errorf("route \"%s\": field 'readTimeout' is required", routeName)
|
||||
}
|
||||
readTimeout, err := time.ParseDuration(raw.ReadTimeout)
|
||||
if err != nil {
|
||||
return Upstream{}, fmt.Errorf("route \"%s\": invalid readTimeout \"%s\": %w", routeName, raw.ReadTimeout, err)
|
||||
}
|
||||
|
||||
// Validate writeTimeout
|
||||
if raw.WriteTimeout == "" {
|
||||
return Upstream{}, fmt.Errorf("route \"%s\": field 'writeTimeout' is required", routeName)
|
||||
}
|
||||
writeTimeout, err := time.ParseDuration(raw.WriteTimeout)
|
||||
if err != nil {
|
||||
return Upstream{}, fmt.Errorf("route \"%s\": invalid writeTimeout \"%s\": %w", routeName, raw.WriteTimeout, err)
|
||||
}
|
||||
|
||||
// Validate maxBodySize is not zero (it must be explicitly set)
|
||||
if raw.MaxBodySize == 0 {
|
||||
return Upstream{}, fmt.Errorf("route \"%s\": field 'maxBodySize' is required and must be > 0", routeName)
|
||||
}
|
||||
|
||||
// Validate authRequired is not missing
|
||||
if raw.AuthRequired == nil {
|
||||
return Upstream{}, fmt.Errorf("route \"%s\": field 'authRequired' is required", routeName)
|
||||
}
|
||||
|
||||
return Upstream{
|
||||
Address: raw.Address,
|
||||
PathRewrite: raw.PathRewrite,
|
||||
ConnectTimeout: connectTimeout,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
MaxBodySize: raw.MaxBodySize,
|
||||
AuthRequired: *raw.AuthRequired,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user