feat: load service adapters from ConfigMap, remove k8s API dependency
Adapters defined in config.yaml alongside routes and models. Parsed by existing config loader, populated into registry at startup. Removed: client-go deps, REST loader, informer, nginx proxy, CiliumNetworkPolicy, apis/gateway/v1/ (duplicate types). Kept: merged CI pipeline, imagePullPolicy Always, CA certs in Dockerfile.
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/serviceadapter"
|
||||
)
|
||||
|
||||
// Config holds the gateway configuration.
|
||||
@@ -18,6 +20,8 @@ type Config struct {
|
||||
// Models maps model names to their upstream configuration.
|
||||
// Multiple models can point to the same upstream address.
|
||||
Models map[string]*ModelUpstream
|
||||
// Adapters holds service adapter definitions for X-Service routing.
|
||||
Adapters []*serviceadapter.ServiceAdapter
|
||||
}
|
||||
|
||||
// ModelUpstream holds upstream configuration for a specific model.
|
||||
@@ -94,16 +98,18 @@ func Load() (*Config, error) {
|
||||
shutdownTimeout = d
|
||||
}
|
||||
|
||||
// Load routes and models from config file
|
||||
// Load routes, models, and adapters from config file
|
||||
routes := make(map[string]*Route)
|
||||
models := make(map[string]*ModelUpstream)
|
||||
var adapters []*serviceadapter.ServiceAdapter
|
||||
if configPath, ok := os.LookupEnv("CONFIG_PATH"); ok {
|
||||
loadedRoutes, loadedModels, err := LoadRoutesAndModelsFromFile(configPath)
|
||||
loadedRoutes, loadedModels, loadedAdapters, err := LoadRoutesAndModelsFromFile(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routes = loadedRoutes
|
||||
models = loadedModels
|
||||
adapters = loadedAdapters
|
||||
}
|
||||
|
||||
return &Config{
|
||||
@@ -111,5 +117,6 @@ func Load() (*Config, error) {
|
||||
ShutdownTimeout: shutdownTimeout,
|
||||
Routes: routes,
|
||||
Models: models,
|
||||
Adapters: adapters,
|
||||
}, nil
|
||||
}
|
||||
|
||||
+72
-25
@@ -6,13 +6,15 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/serviceadapter"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// rawConfig represents the structure of the YAML configuration file.
|
||||
type rawConfig struct {
|
||||
Routes []rawRoute `yaml:"routes"`
|
||||
Models []rawModel `yaml:"models"`
|
||||
Routes []rawRoute `yaml:"routes"`
|
||||
Models []rawModel `yaml:"models"`
|
||||
Adapters []rawAdapter `yaml:"adapters"`
|
||||
}
|
||||
|
||||
// rawRoute represents a single route in the YAML configuration.
|
||||
@@ -28,6 +30,29 @@ type rawModel struct {
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
|
||||
// rawAdapter represents a service adapter in the YAML configuration.
|
||||
type rawAdapter struct {
|
||||
ServiceName string `yaml:"serviceName"`
|
||||
Upstream struct {
|
||||
URL string `yaml:"url"`
|
||||
TimeoutSeconds int32 `yaml:"timeoutSeconds"`
|
||||
} `yaml:"upstream"`
|
||||
Auth struct {
|
||||
Required bool `yaml:"required"`
|
||||
Capability string `yaml:"capability"`
|
||||
} `yaml:"auth"`
|
||||
Retryable bool `yaml:"retryable"`
|
||||
Resources []struct {
|
||||
Name string `yaml:"name"`
|
||||
Methods []struct {
|
||||
Verb string `yaml:"verb"`
|
||||
UpstreamPath string `yaml:"upstreamPath"`
|
||||
RequestSchema string `yaml:"requestSchema"`
|
||||
ResponseSchema string `yaml:"responseSchema"`
|
||||
} `yaml:"methods"`
|
||||
} `yaml:"resources"`
|
||||
}
|
||||
|
||||
// rawUpstream represents upstream configuration in YAML.
|
||||
type rawUpstream struct {
|
||||
Address string `yaml:"address"`
|
||||
@@ -39,32 +64,32 @@ type rawUpstream struct {
|
||||
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) {
|
||||
// LoadRoutesAndModelsFromFile loads route, model, and adapter configuration from a YAML file.
|
||||
func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*ModelUpstream, []*serviceadapter.ServiceAdapter, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read config file %q: %w", path, err)
|
||||
return nil, 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)
|
||||
return nil, 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")
|
||||
return nil, nil, nil, fmt.Errorf("route has empty name")
|
||||
}
|
||||
|
||||
if _, exists := routes[rawRoute.Name]; exists {
|
||||
return nil, nil, fmt.Errorf("duplicate route: \"%s\"", rawRoute.Name)
|
||||
return nil, nil, nil, fmt.Errorf("duplicate route: \"%s\"", rawRoute.Name)
|
||||
}
|
||||
|
||||
upstream, err := parseUpstream(rawRoute.Name, rawRoute.Upstream)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
routes[rawRoute.Name] = &Route{
|
||||
@@ -76,26 +101,18 @@ func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*Mo
|
||||
// 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")
|
||||
return nil, 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)
|
||||
return nil, 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)
|
||||
return nil, 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)
|
||||
return nil, 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,
|
||||
@@ -103,15 +120,45 @@ func LoadRoutesAndModelsFromFile(path string) (map[string]*Route, map[string]*Mo
|
||||
}
|
||||
}
|
||||
|
||||
return routes, models, nil
|
||||
// Load adapters
|
||||
adapters := make([]*serviceadapter.ServiceAdapter, 0, len(raw.Adapters))
|
||||
for _, ra := range raw.Adapters {
|
||||
if ra.ServiceName == "" {
|
||||
return nil, nil, nil, fmt.Errorf("adapter has empty serviceName")
|
||||
}
|
||||
a := &serviceadapter.ServiceAdapter{
|
||||
Name: ra.ServiceName,
|
||||
ServiceName: ra.ServiceName,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
a.Spec.ServiceName = ra.ServiceName
|
||||
a.Spec.Upstream.URL = ra.Upstream.URL
|
||||
a.Spec.Upstream.TimeoutSeconds = ra.Upstream.TimeoutSeconds
|
||||
a.Spec.Auth.Required = ra.Auth.Required
|
||||
a.Spec.Auth.Capability = ra.Auth.Capability
|
||||
a.Spec.Retryable = ra.Retryable
|
||||
for _, rr := range ra.Resources {
|
||||
res := serviceadapter.Resource{Name: rr.Name}
|
||||
for _, rm := range rr.Methods {
|
||||
res.Methods = append(res.Methods, serviceadapter.Method{
|
||||
Verb: rm.Verb,
|
||||
UpstreamPath: rm.UpstreamPath,
|
||||
RequestSchema: rm.RequestSchema,
|
||||
ResponseSchema: rm.ResponseSchema,
|
||||
})
|
||||
}
|
||||
a.Spec.Resources = append(a.Spec.Resources, res)
|
||||
}
|
||||
adapters = append(adapters, a)
|
||||
}
|
||||
|
||||
return routes, models, adapters, 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)
|
||||
routes, _, _, err := LoadRoutesAndModelsFromFile(path)
|
||||
return routes, err
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ models:
|
||||
tmpFile.WriteString(data)
|
||||
tmpFile.Close()
|
||||
|
||||
_, models, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
_, models, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
@@ -94,7 +94,7 @@ models:
|
||||
tmpFile.WriteString(data)
|
||||
tmpFile.Close()
|
||||
|
||||
_, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
_, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
if err == nil {
|
||||
t.Errorf("expected error for duplicate model name, got nil")
|
||||
}
|
||||
@@ -127,7 +127,7 @@ models:
|
||||
tmpFile.WriteString(data)
|
||||
tmpFile.Close()
|
||||
|
||||
_, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
_, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
if err == nil {
|
||||
t.Errorf("expected error for empty model name, got nil")
|
||||
}
|
||||
@@ -157,7 +157,7 @@ models:
|
||||
tmpFile.WriteString(data)
|
||||
tmpFile.Close()
|
||||
|
||||
_, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
_, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
if err == nil {
|
||||
t.Errorf("expected error for missing address, got nil")
|
||||
}
|
||||
@@ -190,7 +190,7 @@ models:
|
||||
tmpFile.WriteString(data)
|
||||
tmpFile.Close()
|
||||
|
||||
_, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
_, _, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
if err == nil {
|
||||
t.Errorf("expected error for invalid address, got nil")
|
||||
}
|
||||
@@ -219,7 +219,7 @@ models:
|
||||
tmpFile.WriteString(data)
|
||||
tmpFile.Close()
|
||||
|
||||
_, models, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
_, models, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
@@ -261,7 +261,7 @@ models:
|
||||
tmpFile.WriteString(data)
|
||||
tmpFile.Close()
|
||||
|
||||
routes, models, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
routes, models, _, err := LoadRoutesAndModelsFromFile(tmpFile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
@@ -6,39 +6,39 @@ import (
|
||||
|
||||
// Upstream defines an upstream target.
|
||||
type Upstream struct {
|
||||
URL string `json:"url"`
|
||||
TimeoutSeconds int32 `json:"timeoutSeconds"`
|
||||
URL string `json:"url" yaml:"url"`
|
||||
TimeoutSeconds int32 `json:"timeoutSeconds" yaml:"timeoutSeconds"`
|
||||
}
|
||||
|
||||
// Auth defines authentication requirements.
|
||||
type Auth struct {
|
||||
Required bool `json:"required"`
|
||||
Capability string `json:"capability,omitempty"`
|
||||
Required bool `json:"required" yaml:"required"`
|
||||
Capability string `json:"capability,omitempty" yaml:"capability,omitempty"`
|
||||
}
|
||||
|
||||
// Method defines an HTTP method endpoint.
|
||||
type Method struct {
|
||||
Verb string `json:"verb"`
|
||||
UpstreamPath string `json:"upstreamPath"`
|
||||
RequestSchema string `json:"requestSchema,omitempty"`
|
||||
ResponseSchema string `json:"responseSchema,omitempty"`
|
||||
Auth *Auth `json:"auth,omitempty"`
|
||||
Verb string `json:"verb" yaml:"verb"`
|
||||
UpstreamPath string `json:"upstreamPath" yaml:"upstreamPath"`
|
||||
RequestSchema string `json:"requestSchema,omitempty" yaml:"requestSchema,omitempty"`
|
||||
ResponseSchema string `json:"responseSchema,omitempty" yaml:"responseSchema,omitempty"`
|
||||
Auth *Auth `json:"auth,omitempty" yaml:"auth,omitempty"`
|
||||
}
|
||||
|
||||
// Resource defines a resource with multiple methods.
|
||||
type Resource struct {
|
||||
Name string `json:"name"`
|
||||
Methods []Method `json:"methods"`
|
||||
Auth *Auth `json:"auth,omitempty"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Methods []Method `json:"methods" yaml:"methods"`
|
||||
Auth *Auth `json:"auth,omitempty" yaml:"auth,omitempty"`
|
||||
}
|
||||
|
||||
// Spec is the ServiceAdapter spec.
|
||||
type Spec struct {
|
||||
ServiceName string `json:"serviceName"`
|
||||
Upstream Upstream `json:"upstream"`
|
||||
Auth Auth `json:"auth"`
|
||||
Retryable bool `json:"retryable,omitempty"`
|
||||
Resources []Resource `json:"resources"`
|
||||
ServiceName string `json:"serviceName" yaml:"serviceName"`
|
||||
Upstream Upstream `json:"upstream" yaml:"upstream"`
|
||||
Auth Auth `json:"auth" yaml:"auth"`
|
||||
Retryable bool `json:"retryable,omitempty" yaml:"retryable,omitempty"`
|
||||
Resources []Resource `json:"resources" yaml:"resources"`
|
||||
}
|
||||
|
||||
// Status is the ServiceAdapter status.
|
||||
|
||||
Reference in New Issue
Block a user