282 lines
9.5 KiB
Go
282 lines
9.5 KiB
Go
package worker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
|
|
controlplanev1 "proxy-pool/gen/controlplane/v1"
|
|
"proxy-pool/internal/config"
|
|
"proxy-pool/internal/domain/adminstate"
|
|
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
)
|
|
|
|
var ErrRoutingUnavailable = errors.New("worker snapshot routing is unavailable")
|
|
|
|
// RoutingSource reads the complete, ordered Gateway routing payload for a
|
|
// WorkerSnapshot. It never performs data-plane reads.
|
|
type RoutingSource interface {
|
|
Read(context.Context) ([]*controlplanev1.RoutingRule, error)
|
|
}
|
|
|
|
type RoutingConfigurationReader interface {
|
|
Snapshot() (*config.Config, uint64)
|
|
}
|
|
|
|
type RoutingStateReader interface {
|
|
Snapshot(context.Context) (adminstate.Snapshot, error)
|
|
}
|
|
|
|
// GatewayRoutingSource combines immutable configuration semantics with the
|
|
// current Admin routing and Upstream state. Admin state is optional only for
|
|
// deployments that do not enable the Admin control surface.
|
|
type GatewayRoutingSource struct {
|
|
configuration RoutingConfigurationReader
|
|
state RoutingStateReader
|
|
}
|
|
|
|
func NewGatewayRoutingSource(configuration RoutingConfigurationReader, states ...RoutingStateReader) (*GatewayRoutingSource, error) {
|
|
if configuration == nil || len(states) > 1 || len(states) == 1 && states[0] == nil {
|
|
return nil, ErrRoutingUnavailable
|
|
}
|
|
source := &GatewayRoutingSource{configuration: configuration}
|
|
if len(states) == 1 {
|
|
source.state = states[0]
|
|
}
|
|
return source, nil
|
|
}
|
|
|
|
func (source *GatewayRoutingSource) Read(ctx context.Context) ([]*controlplanev1.RoutingRule, error) {
|
|
if source == nil || source.configuration == nil || ctx == nil {
|
|
return nil, ErrRoutingUnavailable
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
configuration, revision := source.configuration.Snapshot()
|
|
if configuration == nil {
|
|
return nil, ErrRoutingUnavailable
|
|
}
|
|
var state *adminstate.Snapshot
|
|
if source.state != nil {
|
|
current, err := source.state.Snapshot(ctx)
|
|
if err != nil {
|
|
return nil, errors.Join(ErrRoutingUnavailable, err)
|
|
}
|
|
if current.Config == nil || current.Config.Revision != revision {
|
|
return nil, ErrRoutingUnavailable
|
|
}
|
|
state = ¤t
|
|
}
|
|
rules, err := buildGatewayRouting(configuration, state)
|
|
if err != nil {
|
|
return nil, errors.Join(ErrRoutingUnavailable, err)
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
func buildGatewayRouting(configuration *config.Config, state *adminstate.Snapshot) ([]*controlplanev1.RoutingRule, error) {
|
|
if configuration == nil {
|
|
return nil, errors.New("configuration is nil")
|
|
}
|
|
upstreamEnabled, err := enabledGatewayUpstreams(configuration, state)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
routingStates, err := gatewayRoutingStates(configuration, state)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rules := make([]*controlplanev1.RoutingRule, 0, len(configuration.Routing))
|
|
for _, configured := range configuration.Routing {
|
|
if configured.Purpose != "gateway" {
|
|
continue
|
|
}
|
|
state, hasState := routingStates[configured.Name]
|
|
if !hasState && routingStates != nil {
|
|
return nil, fmt.Errorf("routing %q is missing from admin state", configured.Name)
|
|
}
|
|
routeAction, err := wireRoutingAction(configured.Action)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("routing %q action: %w", configured.Name, err)
|
|
}
|
|
enabled := configured.Enabled && (routingStates == nil || state.Enabled)
|
|
if routeAction != controlplanev1.RoutingAction_ROUTING_ACTION_PROXY {
|
|
rules = append(rules, &controlplanev1.RoutingRule{
|
|
Name: configured.Name, Enabled: enabled, HostRegex: configured.Match.HostRegex,
|
|
Methods: append([]string(nil), configured.Match.Methods...), PathRegex: configured.Match.PathRegex,
|
|
Headers: cloneRoutingHeaders(configured.Match.Headers), Action: routeAction,
|
|
})
|
|
continue
|
|
}
|
|
upstreams := enabledRoutingUpstreams(configured.Upstreams, upstreamEnabled)
|
|
current := configuredCurrentUpstream(configured, state, hasState)
|
|
strategy, err := wireRoutingStrategy(configured.Strategy, current, upstreams)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("routing %q strategy: %w", configured.Name, err)
|
|
}
|
|
enabled = enabled && len(upstreams) > 0
|
|
if strategy.GetType() == controlplanev1.StrategyType_STRATEGY_TYPE_SEQUENTIAL && !containsUpstream(upstreams, current) {
|
|
enabled = false
|
|
}
|
|
unavailableAction, err := wireUnavailableAction(configured.OnUnavailable.Action)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("routing %q unavailable action: %w", configured.Name, err)
|
|
}
|
|
var waitTimeout *durationpb.Duration
|
|
if unavailableAction == controlplanev1.UnavailableAction_UNAVAILABLE_ACTION_WAIT {
|
|
waitTimeout = durationpb.New(configured.OnUnavailable.WaitTimeout.Value())
|
|
}
|
|
rules = append(rules, &controlplanev1.RoutingRule{
|
|
Name: configured.Name, Enabled: enabled, HostRegex: configured.Match.HostRegex,
|
|
Methods: append([]string(nil), configured.Match.Methods...), PathRegex: configured.Match.PathRegex,
|
|
Headers: cloneRoutingHeaders(configured.Match.Headers), Upstreams: upstreams, Strategy: strategy,
|
|
OnUnavailable: unavailableAction, WaitTimeout: waitTimeout, Action: controlplanev1.RoutingAction_ROUTING_ACTION_PROXY,
|
|
})
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
func enabledGatewayUpstreams(configuration *config.Config, state *adminstate.Snapshot) (map[string]bool, error) {
|
|
adminStates := make(map[string]adminstate.UpstreamState)
|
|
if state != nil {
|
|
for _, upstream := range state.Upstreams {
|
|
if _, duplicate := adminStates[upstream.Name]; duplicate {
|
|
return nil, fmt.Errorf("duplicate upstream state %q", upstream.Name)
|
|
}
|
|
adminStates[upstream.Name] = upstream
|
|
}
|
|
}
|
|
result := make(map[string]bool, len(configuration.Upstreams))
|
|
for name, upstream := range configuration.Upstreams {
|
|
result[name] = upstream.Enabled
|
|
if state == nil {
|
|
continue
|
|
}
|
|
admin, exists := adminStates[name]
|
|
if !exists {
|
|
return nil, fmt.Errorf("upstream %q is missing from admin state", name)
|
|
}
|
|
result[name] = result[name] && admin.Enabled
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func gatewayRoutingStates(configuration *config.Config, state *adminstate.Snapshot) (map[string]adminstate.RoutingState, error) {
|
|
if state == nil {
|
|
return nil, nil
|
|
}
|
|
result := make(map[string]adminstate.RoutingState, len(state.Routings))
|
|
for _, routing := range state.Routings {
|
|
if _, duplicate := result[routing.Name]; duplicate {
|
|
return nil, fmt.Errorf("duplicate routing state %q", routing.Name)
|
|
}
|
|
result[routing.Name] = routing
|
|
}
|
|
for _, routing := range configuration.Routing {
|
|
if routing.Purpose == "gateway" {
|
|
if _, exists := result[routing.Name]; !exists {
|
|
return nil, fmt.Errorf("routing %q is missing from admin state", routing.Name)
|
|
}
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func enabledRoutingUpstreams(source []string, enabled map[string]bool) []string {
|
|
result := make([]string, 0, len(source))
|
|
for _, upstream := range source {
|
|
if enabled[upstream] {
|
|
result = append(result, upstream)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func configuredCurrentUpstream(routing config.Routing, state adminstate.RoutingState, hasState bool) string {
|
|
if hasState {
|
|
return state.CurrentUpstream
|
|
}
|
|
if len(routing.Upstreams) == 0 {
|
|
return ""
|
|
}
|
|
return routing.Upstreams[0]
|
|
}
|
|
|
|
func wireRoutingStrategy(configured config.Strategy, current string, upstreams []string) (*controlplanev1.RoutingStrategy, error) {
|
|
strategy := &controlplanev1.RoutingStrategy{CurrentUpstream: current}
|
|
switch configured.Type {
|
|
case "sequential":
|
|
strategy.Type = controlplanev1.StrategyType_STRATEGY_TYPE_SEQUENTIAL
|
|
case "random":
|
|
strategy.Type = controlplanev1.StrategyType_STRATEGY_TYPE_RANDOM
|
|
case "roundRobin":
|
|
strategy.Type = controlplanev1.StrategyType_STRATEGY_TYPE_ROUND_ROBIN
|
|
case "weighted":
|
|
strategy.Type = controlplanev1.StrategyType_STRATEGY_TYPE_WEIGHTED
|
|
strategy.Weights = make(map[string]uint32, len(upstreams))
|
|
for _, upstream := range upstreams {
|
|
weight, exists := configured.Weights[upstream]
|
|
if !exists || weight <= 0 || weight > math.MaxUint32 {
|
|
return nil, fmt.Errorf("invalid weight for upstream %q", upstream)
|
|
}
|
|
strategy.Weights[upstream] = uint32(weight)
|
|
}
|
|
case "leastConnections":
|
|
strategy.Type = controlplanev1.StrategyType_STRATEGY_TYPE_LEAST_CONNECTIONS
|
|
default:
|
|
return nil, fmt.Errorf("unsupported strategy type %q", configured.Type)
|
|
}
|
|
return strategy, nil
|
|
}
|
|
|
|
func wireUnavailableAction(action string) (controlplanev1.UnavailableAction, error) {
|
|
switch action {
|
|
case "reject":
|
|
return controlplanev1.UnavailableAction_UNAVAILABLE_ACTION_REJECT, nil
|
|
case "wait":
|
|
return controlplanev1.UnavailableAction_UNAVAILABLE_ACTION_WAIT, nil
|
|
case "direct":
|
|
return controlplanev1.UnavailableAction_UNAVAILABLE_ACTION_DIRECT, nil
|
|
default:
|
|
return controlplanev1.UnavailableAction_UNAVAILABLE_ACTION_UNSPECIFIED, fmt.Errorf("unsupported action %q", action)
|
|
}
|
|
}
|
|
|
|
func wireRoutingAction(action string) (controlplanev1.RoutingAction, error) {
|
|
switch action {
|
|
case "", "proxy":
|
|
return controlplanev1.RoutingAction_ROUTING_ACTION_PROXY, nil
|
|
case "direct":
|
|
return controlplanev1.RoutingAction_ROUTING_ACTION_DIRECT, nil
|
|
case "reject":
|
|
return controlplanev1.RoutingAction_ROUTING_ACTION_REJECT, nil
|
|
default:
|
|
return controlplanev1.RoutingAction_ROUTING_ACTION_UNSPECIFIED, fmt.Errorf("unsupported action %q", action)
|
|
}
|
|
}
|
|
|
|
func cloneRoutingHeaders(source map[string]string) map[string]string {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
result := make(map[string]string, len(source))
|
|
for name, value := range source {
|
|
result[name] = value
|
|
}
|
|
return result
|
|
}
|
|
|
|
func containsUpstream(upstreams []string, target string) bool {
|
|
for _, upstream := range upstreams {
|
|
if upstream == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var _ RoutingSource = (*GatewayRoutingSource)(nil)
|