Files
zeekayandHanzo Dev 53350699b6 deps(vendor): fold the krakend/lura module set into internal/
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]>
2026-07-25 14:57:19 -07:00

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
}