Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/v1alpha1/envoygateway_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ func (e *EnvoyGateway) GetEnvoyProxyDefaultSpec() *EnvoyProxySpec {
var defaultRuntimeFlags = map[RuntimeFlag]bool{
XDSNameSchemeV2: false,
EndpointSliceIndex: true,
EndpointFastPath: false,
EnvoyProxyPatch: true,
}

Expand Down
10 changes: 9 additions & 1 deletion api/v1alpha1/envoygateway_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ type GatewayAPISettings struct {
// RuntimeFlag defines a runtime flag used to guard breaking changes or risky experimental features in new Envoy Gateway releases.
// A runtime flag may be enabled or disabled by default and can be toggled through the EnvoyGateway resource.
// +enum
// +kubebuilder:validation:Enum=XDSNameSchemeV2;EndpointSliceIndex;PerResourceSystemCASecret;EnvoyProxyPatch;ApplyClientTrafficPolicyHTTP1SettingsToClientsOnly
// +kubebuilder:validation:Enum=XDSNameSchemeV2;EndpointSliceIndex;PerResourceSystemCASecret;EndpointFastPath;EnvoyProxyPatch;ApplyClientTrafficPolicyHTTP1SettingsToClientsOnly
type RuntimeFlag string

const (
Expand All @@ -192,6 +192,14 @@ const (
// it, which may cause a brief disruption to new connections on first enable.
PerResourceSystemCASecret RuntimeFlag = "PerResourceSystemCASecret" //nolint:gosec // not a credential

// EndpointFastPath enables propagating EndpointSlice updates to Envoy as EDS-only
// pushes without waiting for a full translation: endpoint changes are patched into
// the current xDS snapshot using cached per-cluster context from the last successful
// full translation. Endpoint changes that affect configuration (e.g. a backend's
// endpoint count crossing zero) still take the full path, and full translations
// remain authoritative. Disabled by default.
EndpointFastPath RuntimeFlag = "EndpointFastPath"

// EnvoyProxyPatch enables applying the Kubernetes resource `patch` fields configured on
// EnvoyProxy's Kubernetes provider settings. It is enabled by default to preserve
// pre-existing behavior. Because EnvoyProxy is commonly namespace-scoped and
Expand Down
26 changes: 17 additions & 9 deletions internal/gatewayapi/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,22 @@ func (c *ControllerResourcesContext) StoredAtTime() time.Time {
return c.StoredAt
}

// SortEndpointSlices sorts EndpointSlices by creation timestamp, then
// namespace/name. It is shared with the provider's endpoint fast path
// publisher, which must derive endpoints in the same order as a full
// translation.
func SortEndpointSlices(endpointSlices []*discoveryv1.EndpointSlice) {
sort.Slice(endpointSlices, func(i, j int) bool {
if endpointSlices[i].CreationTimestamp.Equal(&endpointSlices[j].CreationTimestamp) {
if endpointSlices[i].Namespace != endpointSlices[j].Namespace {
return endpointSlices[i].Namespace < endpointSlices[j].Namespace
}
return endpointSlices[i].Name < endpointSlices[j].Name
}
return endpointSlices[i].CreationTimestamp.Before(&endpointSlices[j].CreationTimestamp)
})
}

// DeepCopy creates a new ControllerResourcesContext.
// The Context field is preserved (not deep copied) since contexts are meant to be passed around.
func (c *ControllerResourcesContext) DeepCopy() *ControllerResourcesContext {
Expand Down Expand Up @@ -376,15 +392,7 @@ func (r *Resources) Sort() {
})

// Sort EndpointSlices by creation timestamp, then namespace/name
sort.Slice(r.EndpointSlices, func(i, j int) bool {
if r.EndpointSlices[i].CreationTimestamp.Equal(&r.EndpointSlices[j].CreationTimestamp) {
if r.EndpointSlices[i].Namespace != r.EndpointSlices[j].Namespace {
return r.EndpointSlices[i].Namespace < r.EndpointSlices[j].Namespace
}
return r.EndpointSlices[i].Name < r.EndpointSlices[j].Name
}
return r.EndpointSlices[i].CreationTimestamp.Before(&r.EndpointSlices[j].CreationTimestamp)
})
SortEndpointSlices(r.EndpointSlices)

// Sort Secrets by creation timestamp, then namespace/name
sort.Slice(r.Secrets, func(i, j int) bool {
Expand Down
94 changes: 29 additions & 65 deletions internal/gatewayapi/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
"github.com/envoyproxy/gateway/internal/gatewayapi/status"
"github.com/envoyproxy/gateway/internal/ir"
endpointsutil "github.com/envoyproxy/gateway/internal/utils/endpoints"
labelsutil "github.com/envoyproxy/gateway/internal/utils/labels"
"github.com/envoyproxy/gateway/internal/utils/regex"
)
Expand Down Expand Up @@ -3051,9 +3052,10 @@ func (t *Translator) processServiceImportDestinationSetting(
btpRoutingType *egv1a1.RoutingType,
) (*ir.DestinationSetting, status.Error) {
var (
endpoints []*ir.DestinationEndpoint
addrType *ir.DestinationAddressType
protocol = defaultProtocol
endpoints []*ir.DestinationEndpoint
addrType *ir.DestinationAddressType
endpointSource *ir.EndpointSource
protocol = defaultProtocol
)

serviceImport := t.GetServiceImport(backendNamespace, string(backendRef.Name))
Expand Down Expand Up @@ -3086,6 +3088,15 @@ func (t *Translator) processServiceImportDestinationSetting(
status.RouteReasonEndpointsNotFound,
)
}
if t.EndpointFastPathEnabled {
endpointSource = &ir.EndpointSource{
Kind: resource.KindServiceImport,
Namespace: backendNamespace,
Name: string(backendRef.Name),
PortName: servicePort.Name,
Protocol: getServicePortProtocol(servicePort.Protocol),
}
}
} else {
// Use ServiceImport IPs for routing
for _, ip := range backendIps {
Expand All @@ -3101,6 +3112,7 @@ func (t *Translator) processServiceImportDestinationSetting(
Endpoints: endpoints,
AddressType: addrType,
Metadata: buildResourceMetadata(serviceImport, new(gwapiv1.SectionName(strconv.Itoa(int(*backendRef.Port))))),
EndpointSource: endpointSource,
}, nil
}

Expand All @@ -3113,8 +3125,9 @@ func (t *Translator) processServiceDestinationSetting(
btpRoutingType *egv1a1.RoutingType,
) (*ir.DestinationSetting, status.Error) {
var (
endpoints []*ir.DestinationEndpoint
addrType *ir.DestinationAddressType
endpoints []*ir.DestinationEndpoint
addrType *ir.DestinationAddressType
endpointSource *ir.EndpointSource
)
protocol := defaultProtocol

Expand Down Expand Up @@ -3158,6 +3171,15 @@ func (t *Translator) processServiceDestinationSetting(
status.RouteReasonEndpointsNotFound,
)
}
if t.EndpointFastPathEnabled {
endpointSource = &ir.EndpointSource{
Kind: KindDerefOr(backendRef.Kind, resource.KindService),
Namespace: backendNamespace,
Name: string(backendRef.Name),
PortName: servicePort.Name,
Protocol: getServicePortProtocol(servicePort.Protocol),
}
}
} else {
// Use Service ClusterIP routing
ep := ir.NewDestEndpoint(nil, service.Spec.ClusterIP, uint32(*backendRef.Port), false, nil)
Expand All @@ -3172,6 +3194,7 @@ func (t *Translator) processServiceDestinationSetting(
AddressType: addrType,
PreferLocal: processPreferLocalZone(service),
Metadata: buildResourceMetadata(service, new(gwapiv1.SectionName(strconv.Itoa(int(*backendRef.Port))))),
EndpointSource: endpointSource,
}, nil
}

Expand Down Expand Up @@ -3365,66 +3388,7 @@ func (t *Translator) processAllowedListenersForParentRefs(
}

func getIREndpointsFromEndpointSlices(endpointSlices []*discoveryv1.EndpointSlice, portName string, portProtocol corev1.Protocol) ([]*ir.DestinationEndpoint, *ir.DestinationAddressType) {
var (
dstEndpoints []*ir.DestinationEndpoint
dstAddrType *ir.DestinationAddressType
)

addrTypeMap := make(map[ir.DestinationAddressType]int)
for _, endpointSlice := range endpointSlices {
if endpointSlice.AddressType == discoveryv1.AddressTypeFQDN {
addrTypeMap[ir.FQDN]++
} else {
addrTypeMap[ir.IP]++
}
endpoints := getIREndpointsFromEndpointSlice(endpointSlice, portName, portProtocol)
dstEndpoints = append(dstEndpoints, endpoints...)
}

for addrTypeState, addrTypeCounts := range addrTypeMap {
if addrTypeCounts == len(endpointSlices) {
dstAddrType = new(addrTypeState)
break
}
}

if len(addrTypeMap) > 0 && dstAddrType == nil {
dstAddrType = new(ir.MIXED)
}

return dstEndpoints, dstAddrType
}

func getIREndpointsFromEndpointSlice(endpointSlice *discoveryv1.EndpointSlice, portName string, portProtocol corev1.Protocol) []*ir.DestinationEndpoint {
var endpoints []*ir.DestinationEndpoint
for _, endpoint := range endpointSlice.Endpoints {
for _, endpointPort := range endpointSlice.Ports {
// Check if the endpoint port matches the service port
if *endpointPort.Name != portName || *endpointPort.Protocol != portProtocol {
continue
}
conditions := endpoint.Conditions

// Unknown Serving/Terminating (nil) should fall-back to Ready, see https://pkg.go.dev/k8s.io/api/discovery/v1#EndpointConditions
// So drain the endpoint if:
// 1. Both `Terminating` and `Serving` are != null, and either `Terminating=true` or `Serving=false`
// 2. Or `Ready=false`
var draining bool
if conditions.Serving != nil && conditions.Terminating != nil {
draining = *conditions.Terminating || !*conditions.Serving
} else {
draining = conditions.Ready != nil && !*conditions.Ready
}

for _, address := range endpoint.Addresses {
ep := ir.NewDestEndpoint(nil, address, uint32(*endpointPort.Port), draining, endpoint.Zone)
endpoints = append(endpoints, ep)
}

}
}

return endpoints
return endpointsutil.EndpointsFromSlices(endpointSlices, portName, portProtocol)
}

// isCustomBackendResource checks if the given group and kind match any of the configured custom backend resources
Expand Down
1 change: 1 addition & 0 deletions internal/gatewayapi/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ func (r *Runner) subscribeAndTranslate(sub <-chan watchable.Snapshot[string, *re
MergeGateways: gatewayapi.IsMergeGatewaysEnabled(resources),
MergeBackends: gatewayapi.ResolveMergeBackendsConfig(resources),
PerResourceSystemCASecret: r.EnvoyGateway.RuntimeFlags.IsEnabled(egv1a1.PerResourceSystemCASecret),
EndpointFastPathEnabled: r.EnvoyGateway.RuntimeFlags.IsEnabled(egv1a1.EndpointFastPath),
WasmCache: r.wasmCache,
RunningOnHost: r.EnvoyGateway.Provider != nil && r.EnvoyGateway.Provider.IsRunningOnHost(),
InfraRemotelyManaged: r.EnvoyGateway.Provider != nil && r.EnvoyGateway.Provider.IsInfraManagedRemotely(),
Expand Down
5 changes: 5 additions & 0 deletions internal/gatewayapi/translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ type Translator struct {
// sharing a single system_ca_certificates secret. Disabled by default (shared secret used).
PerResourceSystemCASecret bool

// EndpointFastPathEnabled records the endpoint source on EndpointSlice-backed
// destination settings so the xDS runner can re-resolve endpoint updates
// without a full translation.
EndpointFastPathEnabled bool

// GatewayNamespaceMode is true if controller uses gateway namespace mode for infra deployments.
GatewayNamespaceMode bool

Expand Down
22 changes: 22 additions & 0 deletions internal/ir/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -2502,6 +2502,28 @@ type DestinationSetting struct {
// * invalid 500
// * without endpoints 503
Invalid bool `json:"invalid,omitempty" yaml:"invalid,omitempty"`
// EndpointSource identifies the Kubernetes backend this setting's endpoints are
// derived from, so endpoint updates can be re-resolved without a full translation.
// Only set for EndpointSlice-backed (EDS) settings when the EndpointFastPath
// runtime flag is enabled.
EndpointSource *EndpointSource `json:"endpointSource,omitempty" yaml:"endpointSource,omitempty"`
}

// EndpointSource identifies the Kubernetes backend an EDS-managed DestinationSetting's
// endpoints are derived from, along with the parameters needed to filter that backend's
// EndpointSlices the same way the full translation does.
// +kubebuilder:object:generate=true
type EndpointSource struct {
// Kind is the backend kind: Service or ServiceImport.
Kind string `json:"kind" yaml:"kind"`
// Namespace of the backend.
Namespace string `json:"namespace" yaml:"namespace"`
// Name of the backend.
Name string `json:"name" yaml:"name"`
// PortName is the service port name used to filter EndpointSlice ports.
PortName string `json:"portName,omitempty" yaml:"portName,omitempty"`
// Protocol is the service port protocol used to filter EndpointSlice ports.
Protocol corev1.Protocol `json:"protocol,omitempty" yaml:"protocol,omitempty"`
}

// Validate the fields within the DestinationSetting structure
Expand Down
20 changes: 20 additions & 0 deletions internal/ir/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading