feat: load service adapters from ConfigMap, remove k8s API dependency
CI / Vet, test, build (push) Canceled after 2m10s
CI / Build and push image (push) Canceled after 0s

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:
Admin Bot
2026-08-26 16:39:30 -07:00
parent 0cdfae2a93
commit 9c5fb0ce84
13 changed files with 276 additions and 423 deletions
+9 -2
View File
@@ -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
View File
@@ -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
}
+7 -7
View File
@@ -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)
}
+17 -17
View File
@@ -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.