diff --git a/api/v1alpha1/backend_types.go b/api/v1alpha1/backend_types.go index ceec9083492..4cc32696246 100644 --- a/api/v1alpha1/backend_types.go +++ b/api/v1alpha1/backend_types.go @@ -134,12 +134,31 @@ type BackendSpec struct { // Endpoints defines the endpoints to be used when connecting to the backend. // // +kubebuilder:validation:MinItems=1 - // +kubebuilder:validation:MaxItems=64 - // +kubebuilder:validation:XValidation:rule="self.all(f, has(f.fqdn)) || !self.exists(f, has(f.fqdn))",message="fqdn addresses cannot be mixed with other address types" - Endpoints []BackendEndpoint `json:"endpoints,omitempty"` + // +kubebuilder:validation:MaxItems=4 + // +kubebuilder:validation:XValidation:rule="self.all(f, has(f.fqdn)) || !self.exists(f, has(f.fqdn))",message="fqdn addresses cannot be mixed with other address types" + // +optional Endpoints []BackendEndpoint `json:"endpoints,omitempty"` // AppProtocols defines the application protocols to be supported when connecting to the backend. - // + // + // +optional + AppProtocols []AppProtocolType `json:"appProtocols,omitempty"` + + // FQDN defines the FQDN used to contact the backend. + // + // +kubebuilder:validation:MaxLength=253 + // +optional + FQDN *string `json:"fqdn,omitempty"` + + // Fallback indicates whether the backend is designated as a fallback. + // It is highly recommended to configure active or passive health checks to ensure that failover can be detected + // when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again. + // The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when + // the health of the active backends falls below 72%. + // + // +optional + Fallback *bool `json:"fallback,omitempty"` + + // TLS defines the TLS configuration for the backend. // // +optional AppProtocols []AppProtocolType `json:"appProtocols,omitempty"` @@ -152,10 +171,26 @@ type BackendSpec struct { // +optional Fallback *bool `json:"fallback,omitempty"` - // TLS defines the TLS settings for the backend. - // TLS.CACertificateRefs and TLS.WellKnownCACertificates can only be specified for DynamicResolver backends. - // TLS.InsecureSkipVerify can be specified for any Backends - // + // + // +optional + AppProtocols []AppProtocolType `json:"appProtocols,omitempty"` + + // FQDN defines the FQDN used to contact the backend. + // + // +kubebuilder:validation:MaxLength=253 + // +optional + FQDN *string `json:"fqdn,omitempty"` + + // Fallback indicates whether the backend is designated as a fallback. + // It is highly recommended to configure active or passive health checks to ensure that failover can be detected + // when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again. + // The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when + // the health of the active backends falls below 72%. + // + // +optional + Fallback *bool `json:"fallback,omitempty"` + + // TLS defines the TLS configuration for the backend. // // +optional TLS *BackendTLSSettings `json:"tls,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0ae6350b2b4..459a21f9d6b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -464,18 +464,16 @@ func (in *BackendSpec) DeepCopyInto(out *BackendSpec) { *out = new(BackendType) **out = **in } - if in.Endpoints != nil { - in, out := &in.Endpoints, &out.Endpoints - *out = make([]BackendEndpoint, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } if in.AppProtocols != nil { in, out := &in.AppProtocols, &out.AppProtocols *out = make([]AppProtocolType, len(*in)) copy(*out, *in) } + if in.FQDN != nil { + in, out := &in.FQDN, &out.FQDN + *out = new(string) + **out = **in + } if in.Fallback != nil { in, out := &in.Fallback, &out.Fallback *out = new(bool) diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backends.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backends.yaml index 3688b15c9cc..26d1d882d08 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backends.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_backends.yaml @@ -140,7 +140,7 @@ spec: rule: ((has(self.fqdn) && !(has(self.ip) || has(self.unix))) || (has(self.ip) && !(has(self.fqdn) || has(self.unix))) || (has(self.unix) && !(has(self.ip) || has(self.fqdn)))) - maxItems: 64 + maxItems: 4 minItems: 1 type: array x-kubernetes-validations: @@ -154,11 +154,41 @@ spec: The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when the health of the active backends falls below 72%. type: boolean - tls: + fqdn: + description: FQDN defines the FQDN used to contact the backend. + maxLength: 253 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + originalDestinationSettings: description: |- - TLS defines the TLS settings for the backend. - TLS.CACertificateRefs and TLS.WellKnownCACertificates can only be specified for DynamicResolver backends. - TLS.InsecureSkipVerify can be specified for any Backends + OriginalDestinationSettings defines settings for Original Destination backend type. + This field is only valid when Type is "OriginalDestination". + properties: + allowedDestinations: + description: |- + AllowedDestinations specifies CIDR blocks or hostnames that are permitted + as routing destinations. If empty, all destinations are allowed. + Use this for security to prevent routing to unintended endpoints. + + Examples: + - "10.0.0.0/8" (private networks) + - "backend.example.com" (specific hostname) + - "*.example.com" (wildcard hostname) + items: + type: string + maxItems: 20 + type: array + header: + default: x-envoy-original-dst-host + description: |- + Header specifies the header name containing the destination address. + The header value must be in "host:port" format (e.g., "backend.example.com:8080"). + If not specified, defaults to "x-envoy-original-dst-host". + pattern: ^[a-zA-Z0-9]([a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])?$ + type: string + type: object + tls: + description: TLS defines the TLS configuration for the backend. properties: caCertificateRefs: description: |- @@ -248,11 +278,16 @@ spec: enum: - Endpoints - DynamicResolver + - OriginalDestination type: string type: object x-kubernetes-validations: - message: DynamicResolver type cannot have endpoints specified rule: self.type != 'DynamicResolver' || !has(self.endpoints) + - message: OriginalDestination type cannot have endpoints specified + rule: self.type != 'OriginalDestination' || !has(self.endpoints) + - message: OriginalDestination type must specify originalDestinationSettings + rule: self.type != 'OriginalDestination' || has(self.originalDestinationSettings) status: description: Status defines the current status of Backend. properties: diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backends.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backends.yaml index a821ded07ee..f8deced3552 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backends.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backends.yaml @@ -139,7 +139,7 @@ spec: rule: ((has(self.fqdn) && !(has(self.ip) || has(self.unix))) || (has(self.ip) && !(has(self.fqdn) || has(self.unix))) || (has(self.unix) && !(has(self.ip) || has(self.fqdn)))) - maxItems: 64 + maxItems: 4 minItems: 1 type: array x-kubernetes-validations: @@ -153,11 +153,41 @@ spec: The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when the health of the active backends falls below 72%. type: boolean - tls: + fqdn: + description: FQDN defines the FQDN used to contact the backend. + maxLength: 253 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + originalDestinationSettings: description: |- - TLS defines the TLS settings for the backend. - TLS.CACertificateRefs and TLS.WellKnownCACertificates can only be specified for DynamicResolver backends. - TLS.InsecureSkipVerify can be specified for any Backends + OriginalDestinationSettings defines settings for Original Destination backend type. + This field is only valid when Type is "OriginalDestination". + properties: + allowedDestinations: + description: |- + AllowedDestinations specifies CIDR blocks or hostnames that are permitted + as routing destinations. If empty, all destinations are allowed. + Use this for security to prevent routing to unintended endpoints. + + Examples: + - "10.0.0.0/8" (private networks) + - "backend.example.com" (specific hostname) + - "*.example.com" (wildcard hostname) + items: + type: string + maxItems: 20 + type: array + header: + default: x-envoy-original-dst-host + description: |- + Header specifies the header name containing the destination address. + The header value must be in "host:port" format (e.g., "backend.example.com:8080"). + If not specified, defaults to "x-envoy-original-dst-host". + pattern: ^[a-zA-Z0-9]([a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])?$ + type: string + type: object + tls: + description: TLS defines the TLS configuration for the backend. properties: caCertificateRefs: description: |- @@ -247,11 +277,16 @@ spec: enum: - Endpoints - DynamicResolver + - OriginalDestination type: string type: object x-kubernetes-validations: - message: DynamicResolver type cannot have endpoints specified rule: self.type != 'DynamicResolver' || !has(self.endpoints) + - message: OriginalDestination type cannot have endpoints specified + rule: self.type != 'OriginalDestination' || !has(self.endpoints) + - message: OriginalDestination type must specify originalDestinationSettings + rule: self.type != 'OriginalDestination' || has(self.originalDestinationSettings) status: description: Status defines the current status of Backend. properties: diff --git a/internal/gatewayapi/backend.go b/internal/gatewayapi/backend.go index 59a3cf5d0a8..09ca1622dea 100644 --- a/internal/gatewayapi/backend.go +++ b/internal/gatewayapi/backend.go @@ -41,25 +41,57 @@ func (t *Translator) ProcessBackends(backends []*egv1a1.Backend) []*egv1a1.Backe } func validateBackend(backend *egv1a1.Backend) status.Error { - if backend.Spec.Type != nil && - *backend.Spec.Type == egv1a1.BackendTypeDynamicResolver { - if len(backend.Spec.Endpoints) > 0 { - return status.NewRouteStatusError( - fmt.Errorf("DynamicResolver type cannot have endpoints specified"), - status.RouteReasonInvalidBackendRef, - ) - } + if backend.Spec.Type != nil { + switch *backend.Spec.Type { + case egv1a1.BackendTypeDynamicResolver: + if len(backend.Spec.Endpoints) > 0 { + return status.NewRouteStatusError( + fmt.Errorf("DynamicResolver type cannot have endpoints specified"), + status.RouteReasonInvalidBackendRef, + ) + } - if backend.Spec.TLS != nil && - !ptr.Deref(backend.Spec.TLS.InsecureSkipVerify, false) && - backend.Spec.TLS.WellKnownCACertificates == nil && - len(backend.Spec.TLS.CACertificateRefs) == 0 { - return status.NewRouteStatusError( - fmt.Errorf("must specify either CACertificateRefs or WellKnownCACertificates for DynamicResolver type when InsecureSkipVerify is unset or false"), - status.RouteReasonInvalidBackendRef, - ) - } + if backend.Spec.TLS != nil && + !ptr.Deref(backend.Spec.TLS.InsecureSkipVerify, false) && + backend.Spec.TLS.WellKnownCACertificates == nil && + len(backend.Spec.TLS.CACertificateRefs) == 0 { + return status.NewRouteStatusError( + fmt.Errorf("must specify either CACertificateRefs or WellKnownCACertificates for DynamicResolver type when InsecureSkipVerify is unset or false"), + status.RouteReasonInvalidBackendRef, + ) + } + + case egv1a1.BackendTypeOriginalDestination: + if len(backend.Spec.Endpoints) > 0 { + return status.NewRouteStatusError( + fmt.Errorf("OriginalDestination type cannot have endpoints specified"), + status.RouteReasonInvalidBackendRef, + ) + } + if backend.Spec.OriginalDestinationSettings == nil { + return status.NewRouteStatusError( + fmt.Errorf("OriginalDestination type must have OriginalDestinationSettings specified"), + status.RouteReasonInvalidBackendRef, + ) + } + + default: + if backend.Spec.TLS != nil { + if backend.Spec.TLS.WellKnownCACertificates != nil { + return status.NewRouteStatusError( + fmt.Errorf("TLS.WellKnownCACertificates settings can only be specified for DynamicResolver backends"), + status.RouteReasonInvalidBackendRef, + ) + } + if len(backend.Spec.TLS.CACertificateRefs) > 0 { + return status.NewRouteStatusError( + fmt.Errorf("TLS.CACertificateRefs settings can only be specified for DynamicResolver backends"), + status.RouteReasonInvalidBackendRef, + ) + } + } + } } else if backend.Spec.TLS != nil { if backend.Spec.TLS.WellKnownCACertificates != nil { return status.NewRouteStatusError( diff --git a/internal/gatewayapi/testdata/accesslog-als-backend.out.yaml b/internal/gatewayapi/testdata/accesslog-als-backend.out.yaml index 1d655683d02..112f1287502 100644 --- a/internal/gatewayapi/testdata/accesslog-als-backend.out.yaml +++ b/internal/gatewayapi/testdata/accesslog-als-backend.out.yaml @@ -183,7 +183,7 @@ xdsIR: name: backend-fqdn namespace: envoy-gateway name: accesslog_otel_0_0/backend/-1 - protocol: HTTP2 + protocol: TCP - addressType: IP endpoints: - host: 1.1.1.1 diff --git a/internal/gatewayapi/testdata/backend-with-fallback.out.yaml b/internal/gatewayapi/testdata/backend-with-fallback.out.yaml index cf6beeb9adc..682e1805386 100644 --- a/internal/gatewayapi/testdata/backend-with-fallback.out.yaml +++ b/internal/gatewayapi/testdata/backend-with-fallback.out.yaml @@ -184,7 +184,6 @@ xdsIR: name: backend-2 namespace: default name: httproute/default/httproute-1/rule/0/backend/1 - priority: 1 protocol: HTTP weight: 1 hostname: '*' diff --git a/internal/gatewayapi/testdata/envoyproxy-priority-backend.out.yaml b/internal/gatewayapi/testdata/envoyproxy-priority-backend.out.yaml index a56756b74a0..900d030fa9b 100644 --- a/internal/gatewayapi/testdata/envoyproxy-priority-backend.out.yaml +++ b/internal/gatewayapi/testdata/envoyproxy-priority-backend.out.yaml @@ -49,19 +49,7 @@ backendTLSPolicies: name: ca-cmap hostname: ip-backend status: - ancestors: - - ancestorRef: - group: gateway.envoyproxy.io - kind: EnvoyExtensionPolicy - name: policy-for-http-route - namespace: default - conditions: - - lastTransitionTime: null - message: Policy has been accepted. - reason: Accepted - status: "True" - type: Accepted - controllerName: gateway.envoyproxy.io/gatewayclass-controller + ancestors: null backends: - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: Backend @@ -123,7 +111,11 @@ envoyExtensionPolicies: group: gateway.envoyproxy.io kind: Backend name: backend-ip-tls + - group: gateway.envoyproxy.io + kind: Backend + name: grpc-backend-2 namespace: envoy-gateway + port: 9000 targetRef: group: gateway.networking.k8s.io kind: HTTPRoute @@ -138,9 +130,9 @@ envoyExtensionPolicies: sectionName: http conditions: - lastTransitionTime: null - message: Policy has been accepted. - reason: Accepted - status: "True" + message: 'ExtProc: backend envoy-gateway/grpc-backend-2 not found.' + reason: Invalid + status: "False" type: Accepted controllerName: gateway.envoyproxy.io/gatewayclass-controller gateways: @@ -320,75 +312,8 @@ xdsIR: name: httproute/default/httproute-1/rule/0/backend/0 protocol: HTTP weight: 1 - envoyExtensions: - extProcs: - - authority: grpc-backend.envoy-gateway:8000 - destination: - metadata: - kind: EnvoyExtensionPolicy - name: policy-for-http-route - namespace: default - name: envoyextensionpolicy/default/policy-for-http-route/extproc/0 - settings: - - addressType: IP - metadata: - kind: Service - name: grpc-backend - namespace: envoy-gateway - sectionName: "8000" - name: envoyextensionpolicy/default/policy-for-http-route/extproc/0/backend/0 - protocol: GRPC - tls: - alpnProtocols: null - caCertificate: - certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - name: policy-btls-grpc/envoy-gateway-ca - sni: grpc-backend - weight: 1 - - addressType: IP - endpoints: - - host: 8.8.8.8 - port: 9000 - metadata: - kind: Service - name: grpc-backend-2 - namespace: default - sectionName: "9000" - name: envoyextensionpolicy/default/policy-for-http-route/extproc/0/backend/1 - priority: 1 - protocol: GRPC - weight: 1 - - addressType: IP - endpoints: - - host: 1.1.1.1 - port: 3001 - metadata: - kind: Backend - name: backend-ip - namespace: default - name: envoyextensionpolicy/default/policy-for-http-route/extproc/0/backend/2 - priority: 1 - protocol: GRPC - weight: 1 - - addressType: IP - endpoints: - - host: 2.2.2.2 - port: 3443 - metadata: - kind: Backend - name: backend-ip-tls - namespace: envoy-gateway - name: envoyextensionpolicy/default/policy-for-http-route/extproc/0/backend/3 - priority: 1 - protocol: GRPC - tls: - alpnProtocols: null - caCertificate: - certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - name: policy-btls-backend-ip/envoy-gateway-ca - sni: ip-backend - weight: 1 - name: envoyextensionpolicy/default/policy-for-http-route/extproc/0 + directResponse: + statusCode: 500 hostname: www.foo.com isHTTP2: false metadata: diff --git a/internal/gatewayapi/testdata/httproute-dynamic-resolver.out.yaml b/internal/gatewayapi/testdata/httproute-dynamic-resolver.out.yaml index e2da694bf10..35d06523a38 100644 --- a/internal/gatewayapi/testdata/httproute-dynamic-resolver.out.yaml +++ b/internal/gatewayapi/testdata/httproute-dynamic-resolver.out.yaml @@ -202,7 +202,7 @@ xdsIR: settings: - isDynamicResolver: true name: httproute/default/httproute-1/rule/0/backend/0 - protocol: HTTP2 + protocol: HTTP tls: alpnProtocols: null caCertificate: diff --git a/internal/gatewayapi/testdata/httproute-rule-with-non-service-backends-and-app-protocols.out.yaml b/internal/gatewayapi/testdata/httproute-rule-with-non-service-backends-and-app-protocols.out.yaml index 22d9b51958b..c24e74a906f 100644 --- a/internal/gatewayapi/testdata/httproute-rule-with-non-service-backends-and-app-protocols.out.yaml +++ b/internal/gatewayapi/testdata/httproute-rule-with-non-service-backends-and-app-protocols.out.yaml @@ -221,7 +221,7 @@ xdsIR: name: backend-mixed-ip-uds namespace: default name: httproute/default/httproute-1/rule/0/backend/1 - protocol: HTTP2 + protocol: HTTP weight: 1 hostname: '*' isHTTP2: false @@ -250,7 +250,7 @@ xdsIR: name: backend-fqdn namespace: default name: httproute/default/httproute-2/rule/0/backend/0 - protocol: HTTP2 + protocol: HTTP weight: 1 hostname: '*' isHTTP2: false diff --git a/internal/xds/translator/cluster_test.go b/internal/xds/translator/cluster_test.go index a05363a7a22..9f49177e537 100644 --- a/internal/xds/translator/cluster_test.go +++ b/internal/xds/translator/cluster_test.go @@ -47,6 +47,291 @@ func TestBuildXdsCluster(t *testing.T) { assert.True(t, proto.Equal(bootstrapXdsCluster.ConnectTimeout, dynamicXdsCluster.ConnectTimeout)) } +func TestCheckZoneAwareRouting(t *testing.T) { + tests := []struct { + name string + zoneRouting *ir.ZoneAwareRouting + loadBalancerCfg *ir.LoadBalancer + }{ + { + name: "zone-routing with default lb", + zoneRouting: &ir.ZoneAwareRouting{MinSize: 1}, + loadBalancerCfg: &ir.LoadBalancer{ + LeastRequest: &ir.LeastRequest{}, + }, + }, + { + name: "zone-routing with default lb and topology aware routing", + zoneRouting: &ir.ZoneAwareRouting{MinSize: 3}, + loadBalancerCfg: &ir.LoadBalancer{ + LeastRequest: &ir.LeastRequest{}, + }, + }, + { + name: "zone-routing with nil lb", + zoneRouting: &ir.ZoneAwareRouting{MinSize: 1}, + loadBalancerCfg: nil, + }, + { + name: "zone-routing with least request", + zoneRouting: &ir.ZoneAwareRouting{MinSize: 1}, + loadBalancerCfg: &ir.LoadBalancer{ + LeastRequest: &ir.LeastRequest{ + SlowStart: &ir.SlowStart{Window: &metav1.Duration{Duration: 1 * time.Second}}, + }, + }, + }, + { + name: "zone-routing with round robin", + zoneRouting: &ir.ZoneAwareRouting{MinSize: 1}, + loadBalancerCfg: &ir.LoadBalancer{ + RoundRobin: &ir.RoundRobin{ + SlowStart: &ir.SlowStart{Window: &metav1.Duration{Duration: 1 * time.Second}}, + }, + }, + }, + { + name: "zone-routing with random", + zoneRouting: &ir.ZoneAwareRouting{MinSize: 1}, + loadBalancerCfg: &ir.LoadBalancer{Random: &ir.Random{}}, + }, + { + name: "zone-routing with maglev", + zoneRouting: &ir.ZoneAwareRouting{MinSize: 1}, + loadBalancerCfg: &ir.LoadBalancer{ + ConsistentHash: &ir.ConsistentHash{ + TableSize: proto.Uint64(65537), + }, + }, + }, + { + name: "zone-routing with round robin", + zoneRouting: &ir.ZoneAwareRouting{MinSize: 1}, + loadBalancerCfg: &ir.LoadBalancer{ + RoundRobin: &ir.RoundRobin{ + SlowStart: &ir.SlowStart{Window: &metav1.Duration{Duration: 1 * time.Second}}, + }, + }, + }, + { + name: "zone-routing disabled", + zoneRouting: nil, + loadBalancerCfg: &ir.LoadBalancer{ + LeastRequest: &ir.LeastRequest{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bootstrapXdsCluster := getXdsClusterObjFromBootstrap(t) + ds := &ir.DestinationSetting{ + Endpoints: []*ir.DestinationEndpoint{{Host: envoyGatewayXdsServerHost, Port: bootstrap.DefaultXdsServerPort}}, + ZoneAwareRouting: tt.zoneRouting, + } + args := &xdsClusterArgs{ + name: bootstrapXdsCluster.Name, + tSocket: bootstrapXdsCluster.TransportSocket, + endpointType: EndpointTypeDNS, + healthCheck: &ir.HealthCheck{ + PanicThreshold: ptr.To[uint32](66), + }, + loadBalancer: tt.loadBalancerCfg, + settings: []*ir.DestinationSetting{ds}, + } + clusterResult, err := buildXdsCluster(args) + dynamicXdsCluster := clusterResult.cluster + require.NoError(t, err) + + if tt.zoneRouting == nil { + require.Nil(t, dynamicXdsCluster.LoadBalancingPolicy) + require.Equal(t, &clusterv3.Cluster_CommonLbConfig_LocalityWeightedLbConfig_{LocalityWeightedLbConfig: &clusterv3.Cluster_CommonLbConfig_LocalityWeightedLbConfig{}}, dynamicXdsCluster.CommonLbConfig.LocalityConfigSpecifier) + } else { + require.Nil(t, dynamicXdsCluster.CommonLbConfig.LocalityConfigSpecifier) + expectedLoadBalancingPolicy := getExpectedClusterLbPolicies(tt.zoneRouting, dynamicXdsCluster.LbPolicy, args.loadBalancer) + require.Equal(t, expectedLoadBalancingPolicy.Policies[0].TypedExtensionConfig.Name, dynamicXdsCluster.LoadBalancingPolicy.Policies[0].TypedExtensionConfig.Name) + require.Equal(t, expectedLoadBalancingPolicy.Policies[0].GetTypedExtensionConfig().GetTypedConfig().String(), dynamicXdsCluster.LoadBalancingPolicy.Policies[0].GetTypedExtensionConfig().GetTypedConfig().String()) + } + }) + } +} + +func getExpectedClusterLbPolicies(zoneRouting *ir.ZoneAwareRouting, policy clusterv3.Cluster_LbPolicy, lb *ir.LoadBalancer) *clusterv3.LoadBalancingPolicy { + localityLbConfig := &commonv3.LocalityLbConfig{ + LocalityConfigSpecifier: &commonv3.LocalityLbConfig_ZoneAwareLbConfig_{ + ZoneAwareLbConfig: &commonv3.LocalityLbConfig_ZoneAwareLbConfig{ + MinClusterSize: wrapperspb.UInt64(1), + ForceLocalZone: &commonv3.LocalityLbConfig_ZoneAwareLbConfig_ForceLocalZone{ + MinSize: wrapperspb.UInt32(uint32(zoneRouting.MinSize)), + }, + }, + }, + } + leastRequest := &least_requestv3.LeastRequest{ + LocalityLbConfig: localityLbConfig, + } + typedLeastRequest, _ := anypb.New(leastRequest) + loadBalancingPolicy := &clusterv3.LoadBalancingPolicy{ + Policies: []*clusterv3.LoadBalancingPolicy_Policy{{ + TypedExtensionConfig: &corev3.TypedExtensionConfig{ + Name: "envoy.load_balancing_policies.least_request", + TypedConfig: typedLeastRequest, + }, + }}, + } + + if lb == nil { + return loadBalancingPolicy + } + switch policy { + case clusterv3.Cluster_LEAST_REQUEST: + if lb.LeastRequest != nil && lb.LeastRequest.SlowStart != nil && lb.LeastRequest.SlowStart.Window != nil { + leastRequest.SlowStartConfig = &commonv3.SlowStartConfig{ + SlowStartWindow: durationpb.New(lb.LeastRequest.SlowStart.Window.Duration), + } + } + loadBalancingPolicy.Policies[0].TypedExtensionConfig.TypedConfig, _ = anypb.New(leastRequest) + return loadBalancingPolicy + case clusterv3.Cluster_ROUND_ROBIN: + roundRobin := &round_robinv3.RoundRobin{ + LocalityLbConfig: localityLbConfig, + } + if lb.RoundRobin.SlowStart != nil && lb.RoundRobin.SlowStart.Window != nil { + roundRobin.SlowStartConfig = &commonv3.SlowStartConfig{ + SlowStartWindow: durationpb.New(lb.RoundRobin.SlowStart.Window.Duration), + } + } + typedRoundRobin, _ := anypb.New(roundRobin) + return &clusterv3.LoadBalancingPolicy{ + Policies: []*clusterv3.LoadBalancingPolicy_Policy{{ + TypedExtensionConfig: &corev3.TypedExtensionConfig{ + Name: "envoy.load_balancing_policies.round_robin", + TypedConfig: typedRoundRobin, + }, + }}, + } + case clusterv3.Cluster_RANDOM: + random := &randomv3.Random{ + LocalityLbConfig: localityLbConfig, + } + typeRandom, _ := anypb.New(random) + return &clusterv3.LoadBalancingPolicy{ + Policies: []*clusterv3.LoadBalancingPolicy_Policy{{ + TypedExtensionConfig: &corev3.TypedExtensionConfig{ + Name: "envoy.load_balancing_policies.random", + TypedConfig: typeRandom, + }, + }}, + } + case clusterv3.Cluster_MAGLEV: + consistentHash := &maglevv3.Maglev{} + if lb.ConsistentHash.TableSize != nil { + consistentHash.TableSize = wrapperspb.UInt64(*lb.ConsistentHash.TableSize) + } + typedConsistentHash, _ := anypb.New(consistentHash) + + return &clusterv3.LoadBalancingPolicy{ + Policies: []*clusterv3.LoadBalancingPolicy_Policy{{ + TypedExtensionConfig: &corev3.TypedExtensionConfig{ + Name: "envoy.load_balancing_policies.maglev", + TypedConfig: typedConsistentHash, + }, + }}, + } + + } + return nil +} + +func TestBuildXdsClusterOriginalDestination(t *testing.T) { + tests := []struct { + name string + settings []*ir.DestinationSetting + expected *clusterv3.Cluster_OriginalDstLbConfig_ + }{ + { + name: "original destination with header", + settings: []*ir.DestinationSetting{ + { + IsOriginalDestination: true, + OriginalDestinationSettings: &ir.OriginalDestinationSettings{ + Header: ptr.To("target-host"), + }, + }, + }, + expected: &clusterv3.Cluster_OriginalDstLbConfig_{ + OriginalDstLbConfig: &clusterv3.Cluster_OriginalDstLbConfig{ + UseHttpHeader: true, + HttpHeaderName: "target-host", + }, + }, + }, + { + name: "original destination with custom header", + settings: []*ir.DestinationSetting{ + { + IsOriginalDestination: true, + OriginalDestinationSettings: &ir.OriginalDestinationSettings{ + Header: ptr.To("custom-target-header"), + AllowedDestinations: []string{"10.0.0.0/8"}, + }, + }, + }, + expected: &clusterv3.Cluster_OriginalDstLbConfig_{ + OriginalDstLbConfig: &clusterv3.Cluster_OriginalDstLbConfig{ + UseHttpHeader: true, + HttpHeaderName: "custom-target-header", + }, + }, + }, + { + name: "normal destination", + settings: []*ir.DestinationSetting{ + { + IsOriginalDestination: false, + Endpoints: []*ir.DestinationEndpoint{ + {Host: "example.com", Port: 80}, + }, + }, + }, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := &xdsClusterArgs{ + name: "test-cluster", + settings: tt.settings, + endpointType: EndpointTypeOriginalDestination, + } + + if !tt.settings[0].IsOriginalDestination { + args.endpointType = EndpointTypeStatic + } + + result, err := buildXdsCluster(args) + require.NoError(t, err) + cluster := result.cluster + + switch { + case tt.expected != nil: + // Check cluster type and load balancing policy + assert.Equal(t, clusterv3.Cluster_ORIGINAL_DST, cluster.ClusterDiscoveryType.(*clusterv3.Cluster_Type).Type) + assert.Equal(t, clusterv3.Cluster_CLUSTER_PROVIDED, cluster.LbPolicy) + assert.Equal(t, tt.expected, cluster.LbConfig) + case tt.settings[0].IsOriginalDestination: + // Original destination without header should still be ORIGINAL_DST type + assert.Equal(t, clusterv3.Cluster_ORIGINAL_DST, cluster.ClusterDiscoveryType.(*clusterv3.Cluster_Type).Type) + assert.Equal(t, clusterv3.Cluster_CLUSTER_PROVIDED, cluster.LbPolicy) + assert.Nil(t, cluster.LbConfig) + default: + // Normal destination should not be ORIGINAL_DST + assert.NotEqual(t, clusterv3.Cluster_ORIGINAL_DST, cluster.ClusterDiscoveryType.(*clusterv3.Cluster_Type).Type) + } + }) + } +} + func TestBuildXdsClusterLoadAssignment(t *testing.T) { bootstrapXdsCluster := getXdsClusterObjFromBootstrap(t) ds := &ir.DestinationSetting{ diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 95ed18572f3..9214ddb6eee 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -352,7 +352,6 @@ BackendEndpoint describes a backend endpoint, which can be either a fully-qualif corresponding to Envoy's Address: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-address _Appears in:_ -- [BackendSpec](#backendspec) - [ExtensionService](#extensionservice) | Field | Type | Required | Default | Description | @@ -403,10 +402,15 @@ _Appears in:_ | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `type` | _[BackendType](#backendtype)_ | false | Endpoints | Type defines the type of the backend. Defaults to "Endpoints" | -| `endpoints` | _[BackendEndpoint](#backendendpoint) array_ | true | | Endpoints defines the endpoints to be used when connecting to the backend. | | `appProtocols` | _[AppProtocolType](#appprotocoltype) array_ | false | | AppProtocols defines the application protocols to be supported when connecting to the backend. | +| `fqdn` | _string_ | false | | FQDN defines the FQDN used to contact the backend. | +| `fallback` | _boolean_ | false | | Fallback indicates whether the backend is designated as a fallback.
It is highly recommended to configure active or passive health checks to ensure that failover can be detected
when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again.
The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when
the health of the active backends falls below 72%. | +| `appProtocols` | _[AppProtocolType](#appprotocoltype) array_ | false | | TLS defines the TLS configuration for the backend. // | +| `fallback` | _boolean_ | false | | Fallback indicates whether the backend is designated as a fallback.
It is highly recommended to configure active or passive health checks to ensure that failover can be detected
when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again.
The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when
the health of the active backends falls below 72%. | +| `appProtocols` | _[AppProtocolType](#appprotocoltype) array_ | false | | | +| `fqdn` | _string_ | false | | FQDN defines the FQDN used to contact the backend. | | `fallback` | _boolean_ | false | | Fallback indicates whether the backend is designated as a fallback.
It is highly recommended to configure active or passive health checks to ensure that failover can be detected
when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again.
The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when
the health of the active backends falls below 72%. | -| `tls` | _[BackendTLSSettings](#backendtlssettings)_ | false | | TLS defines the TLS settings for the backend.
TLS.CACertificateRefs and TLS.WellKnownCACertificates can only be specified for DynamicResolver backends.
TLS.InsecureSkipVerify can be specified for any Backends | +| `tls` | _[BackendTLSSettings](#backendtlssettings)_ | false | | TLS defines the TLS configuration for the backend. // | #### BackendStatus diff --git a/test/cel-validation/backend_test.go b/test/cel-validation/backend_test.go index 3ff5dca52e5..8ce737bd801 100644 --- a/test/cel-validation/backend_test.go +++ b/test/cel-validation/backend_test.go @@ -89,12 +89,6 @@ func TestBackend(t *testing.T) { Port: 443, }, }, - { - FQDN: &egv1a1.FQDNEndpoint{ - Hostname: "sub.s.example.com", - Port: 443, - }, - }, }, } }, @@ -255,7 +249,7 @@ func TestBackend(t *testing.T) { mutate: func(backend *egv1a1.Backend) { backend.Spec = egv1a1.BackendSpec{Type: ptr.To[egv1a1.BackendType]("FOO")} }, - wantErrors: []string{`spec.type: Unsupported value: "FOO": supported values: "Endpoints", "DynamicResolver"`}, + wantErrors: []string{`spec.type: Unsupported value: "FOO": supported values: "Endpoints", "DynamicResolver", "OriginalDestination"`}, }, { desc: "dynamic resolver ok", diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 14f90f39a40..71eb84d5730 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -17452,7 +17452,7 @@ spec: rule: ((has(self.fqdn) && !(has(self.ip) || has(self.unix))) || (has(self.ip) && !(has(self.fqdn) || has(self.unix))) || (has(self.unix) && !(has(self.ip) || has(self.fqdn)))) - maxItems: 64 + maxItems: 4 minItems: 1 type: array x-kubernetes-validations: @@ -17466,11 +17466,41 @@ spec: The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when the health of the active backends falls below 72%. type: boolean - tls: + fqdn: + description: FQDN defines the FQDN used to contact the backend. + maxLength: 253 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + originalDestinationSettings: description: |- - TLS defines the TLS settings for the backend. - TLS.CACertificateRefs and TLS.WellKnownCACertificates can only be specified for DynamicResolver backends. - TLS.InsecureSkipVerify can be specified for any Backends + OriginalDestinationSettings defines settings for Original Destination backend type. + This field is only valid when Type is "OriginalDestination". + properties: + allowedDestinations: + description: |- + AllowedDestinations specifies CIDR blocks or hostnames that are permitted + as routing destinations. If empty, all destinations are allowed. + Use this for security to prevent routing to unintended endpoints. + + Examples: + - "10.0.0.0/8" (private networks) + - "backend.example.com" (specific hostname) + - "*.example.com" (wildcard hostname) + items: + type: string + maxItems: 20 + type: array + header: + default: x-envoy-original-dst-host + description: |- + Header specifies the header name containing the destination address. + The header value must be in "host:port" format (e.g., "backend.example.com:8080"). + If not specified, defaults to "x-envoy-original-dst-host". + pattern: ^[a-zA-Z0-9]([a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])?$ + type: string + type: object + tls: + description: TLS defines the TLS configuration for the backend. properties: caCertificateRefs: description: |- @@ -17560,11 +17590,16 @@ spec: enum: - Endpoints - DynamicResolver + - OriginalDestination type: string type: object x-kubernetes-validations: - message: DynamicResolver type cannot have endpoints specified rule: self.type != 'DynamicResolver' || !has(self.endpoints) + - message: OriginalDestination type cannot have endpoints specified + rule: self.type != 'OriginalDestination' || !has(self.endpoints) + - message: OriginalDestination type must specify originalDestinationSettings + rule: self.type != 'OriginalDestination' || has(self.originalDestinationSettings) status: description: Status defines the current status of Backend. properties: diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index 089a2574d62..fa4615901bd 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -140,7 +140,7 @@ spec: rule: ((has(self.fqdn) && !(has(self.ip) || has(self.unix))) || (has(self.ip) && !(has(self.fqdn) || has(self.unix))) || (has(self.unix) && !(has(self.ip) || has(self.fqdn)))) - maxItems: 64 + maxItems: 4 minItems: 1 type: array x-kubernetes-validations: @@ -154,11 +154,41 @@ spec: The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when the health of the active backends falls below 72%. type: boolean - tls: + fqdn: + description: FQDN defines the FQDN used to contact the backend. + maxLength: 253 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + originalDestinationSettings: description: |- - TLS defines the TLS settings for the backend. - TLS.CACertificateRefs and TLS.WellKnownCACertificates can only be specified for DynamicResolver backends. - TLS.InsecureSkipVerify can be specified for any Backends + OriginalDestinationSettings defines settings for Original Destination backend type. + This field is only valid when Type is "OriginalDestination". + properties: + allowedDestinations: + description: |- + AllowedDestinations specifies CIDR blocks or hostnames that are permitted + as routing destinations. If empty, all destinations are allowed. + Use this for security to prevent routing to unintended endpoints. + + Examples: + - "10.0.0.0/8" (private networks) + - "backend.example.com" (specific hostname) + - "*.example.com" (wildcard hostname) + items: + type: string + maxItems: 20 + type: array + header: + default: x-envoy-original-dst-host + description: |- + Header specifies the header name containing the destination address. + The header value must be in "host:port" format (e.g., "backend.example.com:8080"). + If not specified, defaults to "x-envoy-original-dst-host". + pattern: ^[a-zA-Z0-9]([a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])?$ + type: string + type: object + tls: + description: TLS defines the TLS configuration for the backend. properties: caCertificateRefs: description: |- @@ -248,11 +278,16 @@ spec: enum: - Endpoints - DynamicResolver + - OriginalDestination type: string type: object x-kubernetes-validations: - message: DynamicResolver type cannot have endpoints specified rule: self.type != 'DynamicResolver' || !has(self.endpoints) + - message: OriginalDestination type cannot have endpoints specified + rule: self.type != 'OriginalDestination' || !has(self.endpoints) + - message: OriginalDestination type must specify originalDestinationSettings + rule: self.type != 'OriginalDestination' || has(self.originalDestinationSettings) status: description: Status defines the current status of Backend. properties: