mirror of
https://github.com/hanzoai/gateway.git
synced 2026-10-30 15:04:44 +00:00
Every import in this module now resolves to github.com/hanzoai/gateway/v2.
The 34 github.com/krakend/* + github.com/luraproject/lura modules that
supplied the engine and its components are vendored under internal/ and
their 91 packages rewritten onto our own path.
Why fold rather than mirror into 34 hanzoai/* repos:
- It removes the exposure outright. 34 rows leave 34 rows of go.sum to
reason about; zero modules leave nothing. `go list -m all` and go.sum
now contain no krakend/lura entry at all.
- Cross-references cannot go wrong. 28 of the 34 import lura (97 files
inside lura self-reference it). Mirroring means rewriting every one of
those to our lura, and a single miss silently links a SECOND copy of
lura into the binary. With one module that is structurally impossible.
- It creates no duplicates. These are lura-coupled gateway glue with no
consumer outside this binary; publishing 34 repos would be 34 new
things to maintain for zero second caller. internal/ makes that
guarantee mechanical — Go forbids importing it from outside this
module, so none of it can become a competing public package. Verified
no repo outside gateway imports any hanzoai/gateway/v2 subpackage.
Layout mirrors provenance so an upstream re-sync stays a diff:
luraproject/lura/v2 -> internal/lura (the engine, 20 pkgs)
krakend/krakend-<x> -> internal/plugin/<x> (28 components)
krakend/<x> -> internal/pkg/<x> (6 standalone libs)
This commit is PURELY MECHANICAL: 196 of the 197 vendored files are
byte-identical to their upstream original once the import rewrite is
applied. The single exception is bloomfilter's subpackage, which upstream
names `krakend` — the one package name that could not survive as-is; it
becomes internal/pkg/bloomfilter/register. Hand edits land separately in
the next commit so they are readable on their own.
Per `go mod vendor` semantics, dependencies' _test.go files are not
vendored. They test upstream code at a pinned version that we do not
modify, several cannot run at all from a module distribution (they need
TLS certs and compiled .so plugins upstream generates but never ships),
and one is a 100k-request stress test that exhausts the ephemeral port
range and fails every package running beside it. Fold integrity is proven
by the byte-diff above instead, which is the stronger check.
Preserved deliberately — each of these is behaviour, not branding:
- the `github.com/devopsfaith/krakend*` extra_config namespace strings
(35 of them). These are config-surface keys, not import paths;
renaming one silently stops that component finding its config. The
jose validator namespace backs `auth/validator` on 212 endpoints.
- core.KrakendVersion / KrakendUserAgent / the X-KRAKEND consts, whose
VALUES rebrand.go overwrites — that seam is unchanged.
- the KRAKEND_ koanf env prefix (a hardcoded upstream const).
- every vendored module's LICENSE (34 files, Apache-2.0/MIT).
The Makefile/CI -X paths move with the engine. An -X naming a symbol that
does not exist is silently ignored by the linker, so a stale path here
would have shipped an "undefined" version in the backend User-Agent and
X-Api-Version with a perfectly green build. Verified live:
`gateway version` reports 2.13.0, not undefined.
Verification (GOWORK=off CGO_ENABLED=0, both tag modes):
go build ./... OK go build -tags legacy ./... OK
go vet ./... clean go vet -tags legacy ./... clean
go test ./... 6/6 ok go test -tags legacy 5/5 ok
tests/ integration harness 47 fixtures pass
gateway check -c configs/{hanzo,lux}/gateway.json -> Syntax OK!
go list -m all | grep -iE 'krakend|lura' -> empty
go mod verify -> all modules verified
Co-authored-by: Hanzo Dev <[email protected]>
423 lines
15 KiB
Go
423 lines
15 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// Parser reads a configuration file, parses it and returns the content as an init ServiceConfig struct
|
|
type Parser interface {
|
|
Parse(configFile string) (ServiceConfig, error)
|
|
}
|
|
|
|
// ParserFunc type is an adapter to allow the use of ordinary functions as subscribers.
|
|
// If f is a function with the appropriate signature, ParserFunc(f) is a Parser that calls f.
|
|
type ParserFunc func(string) (ServiceConfig, error)
|
|
|
|
// Parse implements the Parser interface
|
|
func (f ParserFunc) Parse(configFile string) (ServiceConfig, error) { return f(configFile) }
|
|
|
|
// NewParser creates a new parser using the json library
|
|
func NewParser() Parser {
|
|
return NewParserWithFileReader(os.ReadFile)
|
|
}
|
|
|
|
// NewParserWithFileReader returns a Parser with the injected FileReaderFunc function
|
|
func NewParserWithFileReader(f FileReaderFunc) Parser {
|
|
return parser{fileReader: f}
|
|
}
|
|
|
|
type parser struct {
|
|
fileReader FileReaderFunc
|
|
}
|
|
|
|
// Parser implements the Parse interface
|
|
func (p parser) Parse(configFile string) (ServiceConfig, error) {
|
|
var result ServiceConfig
|
|
var cfg parseableServiceConfig
|
|
data, err := p.fileReader(configFile)
|
|
if err != nil {
|
|
return result, CheckErr(err, configFile)
|
|
}
|
|
if err = json.Unmarshal(data, &cfg); err != nil {
|
|
return result, CheckErr(err, configFile)
|
|
}
|
|
result = cfg.normalize()
|
|
|
|
if err = result.Init(); err != nil {
|
|
return result, CheckErr(err, configFile)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// CheckErr returns a proper documented error
|
|
func CheckErr(err error, configFile string) error {
|
|
switch e := err.(type) {
|
|
case *json.SyntaxError:
|
|
return NewParseError(err, configFile, int(e.Offset))
|
|
case *json.UnmarshalTypeError:
|
|
return NewParseError(err, configFile, int(e.Offset))
|
|
case *os.PathError:
|
|
return fmt.Errorf(
|
|
"'%s' (%s): %s",
|
|
configFile,
|
|
e.Op,
|
|
e.Err.Error(),
|
|
)
|
|
default:
|
|
return fmt.Errorf("'%s': %v", configFile, err)
|
|
}
|
|
}
|
|
|
|
// NewParseError returns a new ParseError
|
|
func NewParseError(err error, configFile string, offset int) *ParseError {
|
|
b, _ := os.ReadFile(configFile)
|
|
row, col := getErrorRowCol(b, offset)
|
|
return &ParseError{
|
|
ConfigFile: configFile,
|
|
Err: err,
|
|
Offset: offset,
|
|
Row: row,
|
|
Col: col,
|
|
}
|
|
}
|
|
|
|
func getErrorRowCol(source []byte, offset int) (row, col int) {
|
|
if len(source) < offset {
|
|
offset = len(source) - 1
|
|
}
|
|
for i := 0; i < offset; i++ {
|
|
v := source[i]
|
|
if v == '\r' {
|
|
continue
|
|
}
|
|
if v == '\n' {
|
|
col = 0
|
|
row++
|
|
continue
|
|
}
|
|
col++
|
|
}
|
|
return
|
|
}
|
|
|
|
// ParseError is an error containing details regarding the row and column where
|
|
// an parse error occurred
|
|
type ParseError struct {
|
|
ConfigFile string
|
|
Offset int
|
|
Row int
|
|
Col int
|
|
Err error
|
|
}
|
|
|
|
// Error returns the error message for the ParseError
|
|
func (p *ParseError) Error() string {
|
|
return fmt.Sprintf(
|
|
"'%s': %v, offset: %v, row: %v, col: %v",
|
|
p.ConfigFile,
|
|
p.Err.Error(),
|
|
p.Offset,
|
|
p.Row,
|
|
p.Col,
|
|
)
|
|
}
|
|
|
|
// FileReaderFunc is a function used to read the content of a config file
|
|
type FileReaderFunc func(string) ([]byte, error)
|
|
|
|
type parseableServiceConfig struct {
|
|
Name string `json:"name"`
|
|
Endpoints []*parseableEndpointConfig `json:"endpoints"`
|
|
AsyncAgents []*parseableAsyncAgent `json:"async_agent"`
|
|
Timeout string `json:"timeout"`
|
|
CacheTTL string `json:"cache_ttl"`
|
|
Host []string `json:"host"`
|
|
Port int `json:"port"`
|
|
Address string `json:"listen_ip"`
|
|
Version int `json:"version"`
|
|
ExtraConfig *ExtraConfig `json:"extra_config,omitempty"`
|
|
ReadTimeout string `json:"read_timeout"`
|
|
WriteTimeout string `json:"write_timeout"`
|
|
IdleTimeout string `json:"idle_timeout"`
|
|
ReadHeaderTimeout string `json:"read_header_timeout"`
|
|
MaxHeaderBytes int `json:"max_header_bytes"`
|
|
DisableKeepAlives bool `json:"disable_keep_alives"`
|
|
DisableCompression bool `json:"disable_compression"`
|
|
DisableStrictREST bool `json:"disable_rest"`
|
|
MaxIdleConns int `json:"max_idle_connections"`
|
|
MaxIdleConnsPerHost int `json:"max_idle_connections_per_host"`
|
|
IdleConnTimeout string `json:"idle_connection_timeout"`
|
|
ResponseHeaderTimeout string `json:"response_header_timeout"`
|
|
ExpectContinueTimeout string `json:"expect_continue_timeout"`
|
|
OutputEncoding string `json:"output_encoding"`
|
|
DialerTimeout string `json:"dialer_timeout"`
|
|
DialerFallbackDelay string `json:"dialer_fallback_delay"`
|
|
DialerKeepAlive string `json:"dialer_keep_alive"`
|
|
Debug bool `json:"debug_endpoint"`
|
|
Echo bool `json:"echo_endpoint"`
|
|
Plugin *Plugin `json:"plugin,omitempty"`
|
|
TLS *parseableTLS `json:"tls,omitempty"`
|
|
ClientTLS *parseableClientTLS `json:"client_tls,omitempty"`
|
|
UseH2C bool `json:"use_h2c,omitempty"`
|
|
DNSCacheTTL string `json:"dns_cache_ttl"`
|
|
MaxShutdownDuration string `json:"max_shutdown_wait_time"`
|
|
}
|
|
|
|
func (p *parseableServiceConfig) normalize() ServiceConfig {
|
|
cfg := ServiceConfig{
|
|
Name: p.Name,
|
|
Timeout: parseDuration(p.Timeout),
|
|
CacheTTL: parseDuration(p.CacheTTL),
|
|
Host: p.Host,
|
|
Port: p.Port,
|
|
Address: p.Address,
|
|
Version: p.Version,
|
|
Debug: p.Debug,
|
|
Echo: p.Echo,
|
|
ReadTimeout: parseDuration(p.ReadTimeout),
|
|
WriteTimeout: parseDuration(p.WriteTimeout),
|
|
IdleTimeout: parseDuration(p.IdleTimeout),
|
|
ReadHeaderTimeout: parseDuration(p.ReadHeaderTimeout),
|
|
MaxHeaderBytes: p.MaxHeaderBytes,
|
|
DisableKeepAlives: p.DisableKeepAlives,
|
|
DisableCompression: p.DisableCompression,
|
|
DisableStrictREST: p.DisableStrictREST,
|
|
MaxIdleConns: p.MaxIdleConns,
|
|
MaxIdleConnsPerHost: p.MaxIdleConnsPerHost,
|
|
IdleConnTimeout: parseDuration(p.IdleConnTimeout),
|
|
ResponseHeaderTimeout: parseDuration(p.ResponseHeaderTimeout),
|
|
ExpectContinueTimeout: parseDuration(p.ExpectContinueTimeout),
|
|
DialerTimeout: parseDuration(p.DialerTimeout),
|
|
DialerFallbackDelay: parseDuration(p.DialerFallbackDelay),
|
|
DialerKeepAlive: parseDuration(p.DialerKeepAlive),
|
|
OutputEncoding: p.OutputEncoding,
|
|
Plugin: p.Plugin,
|
|
UseH2C: p.UseH2C,
|
|
DNSCacheTTL: parseDuration(p.DNSCacheTTL),
|
|
MaxShutdownDuration: parseDuration(p.MaxShutdownDuration),
|
|
}
|
|
if p.TLS != nil {
|
|
cfg.TLS = &TLS{
|
|
IsDisabled: p.TLS.IsDisabled,
|
|
PublicKey: p.TLS.PublicKey,
|
|
PrivateKey: p.TLS.PrivateKey,
|
|
CaCerts: p.TLS.CaCerts,
|
|
MinVersion: p.TLS.MinVersion,
|
|
MaxVersion: p.TLS.MaxVersion,
|
|
CurvePreferences: p.TLS.CurvePreferences,
|
|
PreferServerCipherSuites: p.TLS.PreferServerCipherSuites,
|
|
CipherSuites: p.TLS.CipherSuites,
|
|
EnableMTLS: p.TLS.EnableMTLS,
|
|
DisableSystemCaPool: p.TLS.DisableSystemCaPool,
|
|
}
|
|
for _, k := range p.TLS.Keys {
|
|
cfg.TLS.Keys = append(cfg.TLS.Keys, TLSKeyPair(k))
|
|
}
|
|
}
|
|
if p.ClientTLS != nil {
|
|
cfg.ClientTLS = &ClientTLS{
|
|
AllowInsecureConnections: p.ClientTLS.AllowInsecureConnections,
|
|
CaCerts: p.ClientTLS.CaCerts,
|
|
DisableSystemCaPool: p.ClientTLS.DisableSystemCaPool,
|
|
MinVersion: p.ClientTLS.MinVersion,
|
|
MaxVersion: p.ClientTLS.MaxVersion,
|
|
CurvePreferences: p.ClientTLS.CurvePreferences,
|
|
CipherSuites: p.ClientTLS.CipherSuites,
|
|
ClientCerts: make([]ClientTLSCert, 0, len(p.ClientTLS.ClientCerts)),
|
|
}
|
|
for _, cc := range p.ClientTLS.ClientCerts {
|
|
cfg.ClientTLS.ClientCerts = append(cfg.ClientTLS.ClientCerts, ClientTLSCert(cc))
|
|
}
|
|
}
|
|
if p.ExtraConfig != nil {
|
|
cfg.ExtraConfig = *p.ExtraConfig
|
|
}
|
|
endpoints := make([]*EndpointConfig, 0, len(p.Endpoints))
|
|
for _, e := range p.Endpoints {
|
|
endpoints = append(endpoints, e.normalize())
|
|
}
|
|
cfg.Endpoints = endpoints
|
|
agents := make([]*AsyncAgent, 0, len(p.AsyncAgents))
|
|
for _, a := range p.AsyncAgents {
|
|
agents = append(agents, a.normalize())
|
|
}
|
|
cfg.AsyncAgents = agents
|
|
return cfg
|
|
}
|
|
|
|
type parseableTLSKeyPair struct {
|
|
PublicKey string `json:"public_key"`
|
|
PrivateKey string `json:"private_key"`
|
|
}
|
|
|
|
type parseableTLS struct {
|
|
IsDisabled bool `json:"disabled"`
|
|
PublicKey string `json:"public_key"`
|
|
PrivateKey string `json:"private_key"`
|
|
CaCerts []string `json:"ca_certs"`
|
|
MinVersion string `json:"min_version"`
|
|
MaxVersion string `json:"max_version"`
|
|
CurvePreferences []uint16 `json:"curve_preferences"`
|
|
PreferServerCipherSuites bool `json:"prefer_server_cipher_suites"`
|
|
CipherSuites []uint16 `json:"cipher_suites"`
|
|
EnableMTLS bool `json:"enable_mtls"`
|
|
DisableSystemCaPool bool `json:"disable_system_ca_pool"`
|
|
Keys []parseableTLSKeyPair `json:"keys"`
|
|
}
|
|
|
|
type parseableClientTLS struct {
|
|
AllowInsecureConnections bool `json:"allow_insecure_connections"`
|
|
CaCerts []string `json:"ca_certs"`
|
|
DisableSystemCaPool bool `json:"disable_system_ca_pool"`
|
|
MinVersion string `json:"min_version"`
|
|
MaxVersion string `json:"max_version"`
|
|
CurvePreferences []uint16 `json:"curve_preferences"`
|
|
CipherSuites []uint16 `json:"cipher_suites"`
|
|
ClientCerts []parseableClientTLSCert `json:"client_certs"`
|
|
}
|
|
|
|
type parseableClientTLSCert struct {
|
|
Certificate string `json:"certificate"`
|
|
PrivateKey string `json:"private_key"`
|
|
}
|
|
|
|
type parseableEndpointConfig struct {
|
|
Endpoint string `json:"endpoint"`
|
|
Method string `json:"method"`
|
|
Backend []*parseableBackend `json:"backend"`
|
|
ConcurrentCalls int `json:"concurrent_calls"`
|
|
Timeout string `json:"timeout"`
|
|
CacheTTL string `json:"cache_ttl"`
|
|
QueryString []string `json:"input_query_strings"`
|
|
ExtraConfig *ExtraConfig `json:"extra_config,omitempty"`
|
|
HeadersToPass []string `json:"input_headers"`
|
|
OutputEncoding string `json:"output_encoding"`
|
|
}
|
|
|
|
func (p *parseableEndpointConfig) normalize() *EndpointConfig {
|
|
e := EndpointConfig{
|
|
Endpoint: p.Endpoint,
|
|
Method: p.Method,
|
|
ConcurrentCalls: p.ConcurrentCalls,
|
|
Timeout: parseDuration(p.Timeout),
|
|
CacheTTL: parseDuration(p.CacheTTL),
|
|
QueryString: p.QueryString,
|
|
HeadersToPass: p.HeadersToPass,
|
|
OutputEncoding: p.OutputEncoding,
|
|
}
|
|
if p.ExtraConfig != nil {
|
|
e.ExtraConfig = *p.ExtraConfig
|
|
}
|
|
backends := make([]*Backend, 0, len(p.Backend))
|
|
for _, b := range p.Backend {
|
|
backends = append(backends, b.normalize())
|
|
}
|
|
e.Backend = backends
|
|
return &e
|
|
}
|
|
|
|
type parseableAsyncAgent struct {
|
|
Name string `json:"name"`
|
|
Connection struct {
|
|
MaxRetries int `json:"max_retries"`
|
|
BackoffStrategy string `json:"backoff_strategy"`
|
|
HealthInterval string `json:"health_interval"`
|
|
} `json:"connection"`
|
|
Consumer struct {
|
|
Timeout string `json:"timeout"`
|
|
Workers int `json:"workers"`
|
|
Topic string `json:"topic"`
|
|
MaxRate float64 `json:"max_rate"`
|
|
} `json:"consumer"`
|
|
Encoding string `json:"encoding"`
|
|
Backend []*parseableBackend `json:"backend"`
|
|
ExtraConfig ExtraConfig `json:"extra_config"`
|
|
}
|
|
|
|
func (p *parseableAsyncAgent) normalize() *AsyncAgent {
|
|
e := AsyncAgent{
|
|
Name: p.Name,
|
|
Encoding: p.Encoding,
|
|
Connection: Connection{
|
|
MaxRetries: p.Connection.MaxRetries,
|
|
BackoffStrategy: p.Connection.BackoffStrategy,
|
|
HealthInterval: parseDuration(p.Connection.HealthInterval),
|
|
},
|
|
Consumer: Consumer{
|
|
Timeout: parseDuration(p.Consumer.Timeout),
|
|
Workers: p.Consumer.Workers,
|
|
Topic: p.Consumer.Topic,
|
|
MaxRate: p.Consumer.MaxRate,
|
|
},
|
|
}
|
|
if p.ExtraConfig != nil {
|
|
e.ExtraConfig = p.ExtraConfig
|
|
}
|
|
backends := make([]*Backend, 0, len(p.Backend))
|
|
for _, b := range p.Backend {
|
|
backends = append(backends, b.normalize())
|
|
}
|
|
e.Backend = backends
|
|
return &e
|
|
}
|
|
|
|
type parseableBackend struct {
|
|
Group string `json:"group"`
|
|
Method string `json:"method"`
|
|
Host []string `json:"host"`
|
|
HostSanitizationDisabled bool `json:"disable_host_sanitize"`
|
|
URLPattern string `json:"url_pattern"`
|
|
AllowList []string `json:"allow"`
|
|
DenyList []string `json:"deny"`
|
|
Mapping map[string]string `json:"mapping"`
|
|
Encoding string `json:"encoding"`
|
|
IsCollection bool `json:"is_collection"`
|
|
Target string `json:"target"`
|
|
ExtraConfig *ExtraConfig `json:"extra_config,omitempty"`
|
|
SD string `json:"sd"`
|
|
HeadersToPass []string `json:"input_headers"`
|
|
SDScheme string `json:"sd_scheme"`
|
|
QueryStringsToPass []string `json:"input_query_strings"`
|
|
}
|
|
|
|
func (p *parseableBackend) normalize() *Backend {
|
|
b := Backend{
|
|
Group: p.Group,
|
|
Method: p.Method,
|
|
Host: p.Host,
|
|
HostSanitizationDisabled: p.HostSanitizationDisabled,
|
|
URLPattern: p.URLPattern,
|
|
Mapping: p.Mapping,
|
|
Encoding: p.Encoding,
|
|
IsCollection: p.IsCollection,
|
|
Target: p.Target,
|
|
SD: p.SD,
|
|
SDScheme: p.SDScheme,
|
|
AllowList: p.AllowList,
|
|
DenyList: p.DenyList,
|
|
HeadersToPass: p.HeadersToPass,
|
|
QueryStringsToPass: p.QueryStringsToPass,
|
|
}
|
|
if b.SDScheme == "" {
|
|
b.SDScheme = "http"
|
|
}
|
|
if p.ExtraConfig != nil {
|
|
b.ExtraConfig = *p.ExtraConfig
|
|
}
|
|
return &b
|
|
}
|
|
|
|
func parseDuration(v string) time.Duration {
|
|
d, err := time.ParseDuration(v)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return d
|
|
}
|