|
| 1 | +package controllerconfig |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "time" |
| 6 | + |
| 7 | + v1 "k8s.io/api/core/v1" |
| 8 | + "k8s.io/client-go/kubernetes" |
| 9 | +) |
| 10 | + |
| 11 | +// MultiClusterConfigName is the inferenceservice-config ConfigMap key holding |
| 12 | +// the multi-cluster tuning block. |
| 13 | +const MultiClusterConfigName = "multicluster" |
| 14 | + |
| 15 | +// +kubebuilder:object:generate=false |
| 16 | +// MultiClusterConfig holds operator-level tuning for the multi-cluster fan-out |
| 17 | +// layer (the WorkloadCluster connection/transport, the placement controller, |
| 18 | +// and the global endpoint publisher). It is loaded once at manager startup, from |
| 19 | +// the inferenceservice-config ConfigMap, only when multi-cluster is enabled. |
| 20 | +// |
| 21 | +// Topology, role, identity, and security stay manager flags, not config: they |
| 22 | +// decide which controllers run and the manager's identity, so they are |
| 23 | +// deploy-time decisions (a restart), not hot-tunable config. |
| 24 | +// |
| 25 | +// Every field degrades gracefully when omitted. Durations are stored as strings |
| 26 | +// and parsed by the *Duration() accessors, which yield 0 on an empty or |
| 27 | +// unparsable value; a zero handed to the workloadcluster/placement options |
| 28 | +// makes those packages apply their OWN in-package default. So the default for |
| 29 | +// each knob stays single-sourced in the package that owns it — never duplicated |
| 30 | +// as a literal here — and an absent "multicluster" block reproduces the |
| 31 | +// built-in behavior exactly. |
| 32 | +type MultiClusterConfig struct { |
| 33 | + WorkloadCluster WorkloadClusterConfig `json:"workloadCluster,omitempty"` |
| 34 | + Placement PlacementConfig `json:"placement,omitempty"` |
| 35 | + Endpoint EndpointConfig `json:"endpoint,omitempty"` |
| 36 | +} |
| 37 | + |
| 38 | +// +kubebuilder:object:generate=false |
| 39 | +// WorkloadClusterConfig tunes the remote-cluster connection and transport layer |
| 40 | +// and the cross-cluster status watch funnel. |
| 41 | +type WorkloadClusterConfig struct { |
| 42 | + // ClientQPS / ClientBurst are the steady-state request rate and burst to each |
| 43 | + // remote workload-cluster apiserver. Zero leaves the client-go default. |
| 44 | + ClientQPS float64 `json:"clientQPS,omitempty"` |
| 45 | + ClientBurst int `json:"clientBurst,omitempty"` |
| 46 | + // PerCallTimeout bounds one remote request. Empty leaves no client-level timeout. |
| 47 | + PerCallTimeout string `json:"perCallTimeout,omitempty"` |
| 48 | + // CacheEnabled serves cross-cluster derived-InferenceService reads from a |
| 49 | + // per-cluster informer cache instead of live apiserver reads, and is the |
| 50 | + // prerequisite event source for the status watch funnel. False (the default) |
| 51 | + // keeps reads live and the funnel off. |
| 52 | + CacheEnabled bool `json:"cacheEnabled,omitempty"` |
| 53 | + // HealthInterval is the re-probe cadence for an otherwise-idle WorkloadCluster. |
| 54 | + HealthInterval string `json:"healthInterval,omitempty"` |
| 55 | + // ConnectionGrace is how long a previously reachable cluster tolerates transient |
| 56 | + // probe failures before it is flipped to Ready=False and disconnected. |
| 57 | + ConnectionGrace string `json:"connectionGrace,omitempty"` |
| 58 | + // EventsBatchPeriod debounces a rotated-kubeconfig Secret's burst of key updates |
| 59 | + // into one reconcile. |
| 60 | + EventsBatchPeriod string `json:"eventsBatchPeriod,omitempty"` |
| 61 | + // EstablishInitial / EstablishMax bound a single remote watch-establish attempt |
| 62 | + // (the timeout grows from initial to max). ReconnectRetryMax caps the |
| 63 | + // inter-attempt backoff for an unreachable cluster. |
| 64 | + EstablishInitial string `json:"establishInitial,omitempty"` |
| 65 | + EstablishMax string `json:"establishMax,omitempty"` |
| 66 | + ReconnectRetryMax string `json:"reconnectRetryMax,omitempty"` |
| 67 | + // FunnelResyncInterval is how often the status funnel reconciles its per-cluster |
| 68 | + // watch set against the connected clusters (how fast a newly-connected cluster |
| 69 | + // is noticed, not the status latency). FunnelBufferSize is the depth of its |
| 70 | + // buffered event channel; a full channel drops events (the safety requeue |
| 71 | + // recovers). |
| 72 | + FunnelResyncInterval string `json:"funnelResyncInterval,omitempty"` |
| 73 | + FunnelBufferSize int `json:"funnelBufferSize,omitempty"` |
| 74 | +} |
| 75 | + |
| 76 | +// +kubebuilder:object:generate=false |
| 77 | +// PlacementConfig tunes the fan-out placement controller, its status |
| 78 | +// convergence, and its orphan GC. |
| 79 | +type PlacementConfig struct { |
| 80 | + // RequeueInterval is the status-refresh poll cadence. With the cache/funnel off |
| 81 | + // it also paces the cross-cluster status re-read. |
| 82 | + RequeueInterval string `json:"requeueInterval,omitempty"` |
| 83 | + // GCInterval is the orphan-sweep cadence for the placement GC runnable. |
| 84 | + GCInterval string `json:"gcInterval,omitempty"` |
| 85 | + // MaxConcurrentReconciles caps placement reconciles in parallel (distinct |
| 86 | + // ISVCs). Zero falls back to controller-runtime's single worker. |
| 87 | + MaxConcurrentReconciles int `json:"maxConcurrentReconciles,omitempty"` |
| 88 | + // FanoutTimeout is the per-cluster deadline bounding a single fan-out apply, so |
| 89 | + // one slow remote cannot block placement to healthy peers. |
| 90 | + FanoutTimeout string `json:"fanoutTimeout,omitempty"` |
| 91 | + // WinnerLostGrace is the grace window held before re-placing when the sticky |
| 92 | + // winner's derived is absent on a still-connected winner. Empty re-places |
| 93 | + // immediately. |
| 94 | + WinnerLostGrace string `json:"winnerLostGrace,omitempty"` |
| 95 | + // StatusBatchPeriod debounces a burst of cross-cluster derived-status events for |
| 96 | + // one ISVC into a single placement reconcile. |
| 97 | + StatusBatchPeriod string `json:"statusBatchPeriod,omitempty"` |
| 98 | + // StatusSafetyRequeue is the steady-state re-read backstop when the funnel is on |
| 99 | + // (events drive freshness; this only recovers a missed event). |
| 100 | + StatusSafetyRequeue string `json:"statusSafetyRequeue,omitempty"` |
| 101 | + // DispatcherMode is the fan-out breadth policy: "AllAtOnce" clones onto every |
| 102 | + // matched candidate at once; "Incremental" probes candidates in batches. Empty |
| 103 | + // or unrecognized means AllAtOnce. |
| 104 | + DispatcherMode string `json:"dispatcherMode,omitempty"` |
| 105 | + // DispatcherStepSize is the candidates the Incremental dispatcher adds per round. |
| 106 | + // Non-positive advances by one. |
| 107 | + DispatcherStepSize int `json:"dispatcherStepSize,omitempty"` |
| 108 | + // DispatcherRoundTimeout is how long an Incremental round waits for a nominated |
| 109 | + // cluster to win before adding the next batch. Non-positive enforces no dwell. |
| 110 | + DispatcherRoundTimeout string `json:"dispatcherRoundTimeout,omitempty"` |
| 111 | +} |
| 112 | + |
| 113 | +// +kubebuilder:object:generate=false |
| 114 | +// EndpointConfig tunes the global endpoint publisher (a Gateway API HTTPRoute to |
| 115 | +// the placement winner). The host template, gateway, and namespace are |
| 116 | +// deployment-identity values with no in-code default: empty disables publishing |
| 117 | +// (a no-op), never a baked-in gateway or host. |
| 118 | +type EndpointConfig struct { |
| 119 | + // GlobalHostTemplate is the text/template for the global host an ISVC publishes |
| 120 | + // to the winner. Empty means only ISVCs carrying the global-host annotation |
| 121 | + // publish. |
| 122 | + GlobalHostTemplate string `json:"globalHostTemplate,omitempty"` |
| 123 | + // GlobalGateway is the "namespace/name" of the global-traffic Gateway the |
| 124 | + // published HTTPRoute attaches to. Empty makes the publisher a no-op. |
| 125 | + GlobalGateway string `json:"globalGateway,omitempty"` |
| 126 | + // RouteNamespace is the namespace for the published HTTPRoute and backing |
| 127 | + // Service. Empty uses the ISVC's own namespace. |
| 128 | + RouteNamespace string `json:"routeNamespace,omitempty"` |
| 129 | + // BackendPort is the port on the winner cluster's ingress the global host |
| 130 | + // forwards to. |
| 131 | + BackendPort int `json:"backendPort,omitempty"` |
| 132 | +} |
| 133 | + |
| 134 | +// NewMultiClusterConfig loads the "multicluster" block from the |
| 135 | +// inferenceservice-config ConfigMap. It is read once at manager startup (the |
| 136 | +// multi-cluster wiring is built before any reconcile), so there is no |
| 137 | +// ConfigCache-backed variant. An absent block yields a zero-valued config and |
| 138 | +// every consumer applies its own in-package default. |
| 139 | +func NewMultiClusterConfig(clientset kubernetes.Interface) (*MultiClusterConfig, error) { |
| 140 | + configMap, err := getInferenceServiceConfigMap(clientset) |
| 141 | + if err != nil { |
| 142 | + return nil, err |
| 143 | + } |
| 144 | + return parseMultiClusterConfig(configMap) |
| 145 | +} |
| 146 | + |
| 147 | +func parseMultiClusterConfig(configMap *v1.ConfigMap) (*MultiClusterConfig, error) { |
| 148 | + cfg := &MultiClusterConfig{} |
| 149 | + if err := getComponentConfig(MultiClusterConfigName, configMap, cfg); err != nil { |
| 150 | + return nil, fmt.Errorf("unable to parse multicluster config json: %w", err) |
| 151 | + } |
| 152 | + return cfg, nil |
| 153 | +} |
| 154 | + |
| 155 | +// PerCallTimeoutDuration returns the parsed PerCallTimeout (0 if absent/unparsable). |
| 156 | +func (c WorkloadClusterConfig) PerCallTimeoutDuration() time.Duration { |
| 157 | + return parseDurationOrZero(c.PerCallTimeout) |
| 158 | +} |
| 159 | + |
| 160 | +// HealthIntervalDuration returns the parsed HealthInterval (0 if absent/unparsable). |
| 161 | +func (c WorkloadClusterConfig) HealthIntervalDuration() time.Duration { |
| 162 | + return parseDurationOrZero(c.HealthInterval) |
| 163 | +} |
| 164 | + |
| 165 | +// ConnectionGraceDuration returns the parsed ConnectionGrace (0 if absent/unparsable). |
| 166 | +func (c WorkloadClusterConfig) ConnectionGraceDuration() time.Duration { |
| 167 | + return parseDurationOrZero(c.ConnectionGrace) |
| 168 | +} |
| 169 | + |
| 170 | +// EventsBatchPeriodDuration returns the parsed EventsBatchPeriod (0 if absent/unparsable). |
| 171 | +func (c WorkloadClusterConfig) EventsBatchPeriodDuration() time.Duration { |
| 172 | + return parseDurationOrZero(c.EventsBatchPeriod) |
| 173 | +} |
| 174 | + |
| 175 | +// EstablishInitialDuration returns the parsed EstablishInitial (0 if absent/unparsable). |
| 176 | +func (c WorkloadClusterConfig) EstablishInitialDuration() time.Duration { |
| 177 | + return parseDurationOrZero(c.EstablishInitial) |
| 178 | +} |
| 179 | + |
| 180 | +// EstablishMaxDuration returns the parsed EstablishMax (0 if absent/unparsable). |
| 181 | +func (c WorkloadClusterConfig) EstablishMaxDuration() time.Duration { |
| 182 | + return parseDurationOrZero(c.EstablishMax) |
| 183 | +} |
| 184 | + |
| 185 | +// ReconnectRetryMaxDuration returns the parsed ReconnectRetryMax (0 if absent/unparsable). |
| 186 | +func (c WorkloadClusterConfig) ReconnectRetryMaxDuration() time.Duration { |
| 187 | + return parseDurationOrZero(c.ReconnectRetryMax) |
| 188 | +} |
| 189 | + |
| 190 | +// FunnelResyncIntervalDuration returns the parsed FunnelResyncInterval (0 if absent/unparsable). |
| 191 | +func (c WorkloadClusterConfig) FunnelResyncIntervalDuration() time.Duration { |
| 192 | + return parseDurationOrZero(c.FunnelResyncInterval) |
| 193 | +} |
| 194 | + |
| 195 | +// RequeueIntervalDuration returns the parsed RequeueInterval (0 if absent/unparsable). |
| 196 | +func (c PlacementConfig) RequeueIntervalDuration() time.Duration { |
| 197 | + return parseDurationOrZero(c.RequeueInterval) |
| 198 | +} |
| 199 | + |
| 200 | +// GCIntervalDuration returns the parsed GCInterval (0 if absent/unparsable). |
| 201 | +func (c PlacementConfig) GCIntervalDuration() time.Duration { |
| 202 | + return parseDurationOrZero(c.GCInterval) |
| 203 | +} |
| 204 | + |
| 205 | +// FanoutTimeoutDuration returns the parsed FanoutTimeout (0 if absent/unparsable). |
| 206 | +func (c PlacementConfig) FanoutTimeoutDuration() time.Duration { |
| 207 | + return parseDurationOrZero(c.FanoutTimeout) |
| 208 | +} |
| 209 | + |
| 210 | +// WinnerLostGraceDuration returns the parsed WinnerLostGrace (0 if absent/unparsable). |
| 211 | +func (c PlacementConfig) WinnerLostGraceDuration() time.Duration { |
| 212 | + return parseDurationOrZero(c.WinnerLostGrace) |
| 213 | +} |
| 214 | + |
| 215 | +// StatusBatchPeriodDuration returns the parsed StatusBatchPeriod (0 if absent/unparsable). |
| 216 | +func (c PlacementConfig) StatusBatchPeriodDuration() time.Duration { |
| 217 | + return parseDurationOrZero(c.StatusBatchPeriod) |
| 218 | +} |
| 219 | + |
| 220 | +// StatusSafetyRequeueDuration returns the parsed StatusSafetyRequeue (0 if absent/unparsable). |
| 221 | +func (c PlacementConfig) StatusSafetyRequeueDuration() time.Duration { |
| 222 | + return parseDurationOrZero(c.StatusSafetyRequeue) |
| 223 | +} |
| 224 | + |
| 225 | +// DispatcherRoundTimeoutDuration returns the parsed DispatcherRoundTimeout (0 if absent/unparsable). |
| 226 | +func (c PlacementConfig) DispatcherRoundTimeoutDuration() time.Duration { |
| 227 | + return parseDurationOrZero(c.DispatcherRoundTimeout) |
| 228 | +} |
| 229 | + |
| 230 | +// parseDurationOrZero parses s, returning 0 when it is empty, malformed, or |
| 231 | +// non-positive. Callers hand the zero to a workloadcluster/placement option, |
| 232 | +// which then applies its own in-package default — so the fallback stays |
| 233 | +// single-sourced in the consuming package, not duplicated here. |
| 234 | +func parseDurationOrZero(s string) time.Duration { |
| 235 | + if d, err := time.ParseDuration(s); err == nil && d > 0 { |
| 236 | + return d |
| 237 | + } |
| 238 | + return 0 |
| 239 | +} |
0 commit comments