diff --git a/internal/gatewayapi/resource.go b/internal/gatewayapi/resource.go index 2251d346674..6dc630ddc72 100644 --- a/internal/gatewayapi/resource.go +++ b/internal/gatewayapi/resource.go @@ -108,7 +108,7 @@ func (r *Resources) GetSecret(namespace, name string) *v1.Secret { } func (r *Resources) GetEndpointSlicesForBackend(svcNamespace, svcName string, backendKind string) []*discoveryv1.EndpointSlice { - endpointSlices := []*discoveryv1.EndpointSlice{} + var endpointSlices []*discoveryv1.EndpointSlice for _, endpointSlice := range r.EndpointSlices { var backendSelectorLabel string switch backendKind { diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go index 4edc6cce86c..411139039a2 100644 --- a/internal/gatewayapi/route.go +++ b/internal/gatewayapi/route.go @@ -11,7 +11,7 @@ import ( "time" corev1 "k8s.io/api/core/v1" - v1 "k8s.io/api/discovery/v1" + discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a1 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -1042,7 +1042,7 @@ func (t *Translator) processDestination(backendRef gwapiv1.BackendRef, endpointSlices := resources.GetEndpointSlicesForBackend(backendNamespace, string(backendRef.Name), KindDerefOr(backendRef.Kind, KindService)) endpoints = getIREndpointsFromEndpointSlice(endpointSlices, servicePort.Name, servicePort.Protocol) } else { - // Fall back to Service CluserIP routing + // Fall back to Service ClusterIP routing ep := ir.NewDestEndpoint( service.Spec.ClusterIP, uint32(*backendRef.Port)) @@ -1127,8 +1127,8 @@ func (t *Translator) processAllowedListenersForParentRefs(routeContext RouteCont return relevantRoute } -func getIREndpointsFromEndpointSlice(endpointSlices []*v1.EndpointSlice, portName string, portProtocol corev1.Protocol) []*ir.DestinationEndpoint { - endpoints := []*ir.DestinationEndpoint{} +func getIREndpointsFromEndpointSlice(endpointSlices []*discoveryv1.EndpointSlice, portName string, portProtocol corev1.Protocol) []*ir.DestinationEndpoint { + var endpoints []*ir.DestinationEndpoint for _, endpointSlice := range endpointSlices { for _, endpoint := range endpointSlice.Endpoints { for _, endpointPort := range endpointSlice.Ports { @@ -1141,6 +1141,7 @@ func getIREndpointsFromEndpointSlice(endpointSlices []*v1.EndpointSlice, portNam ep := ir.NewDestEndpoint( address, uint32(*endpointPort.Port)) + ep.SetHostAddressType(endpointSlice.AddressType) endpoints = append(endpoints, ep) } } diff --git a/internal/ir/xds.go b/internal/ir/xds.go index c0aa13ffbf6..e6d66197138 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -12,13 +12,14 @@ import ( "github.com/tetratelabs/multierror" "golang.org/x/exp/slices" - + discoveryv1 "k8s.io/api/discovery/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/validation" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" - "github.com/envoyproxy/gateway/api/v1alpha1/validation" + egv1a1validation "github.com/envoyproxy/gateway/api/v1alpha1/validation" ) var ( @@ -32,7 +33,7 @@ var ( ErrHTTPRouteNameEmpty = errors.New("field Name must be specified") ErrHTTPRouteHostnameEmpty = errors.New("field Hostname must be specified") ErrDestinationNameEmpty = errors.New("field Name must be specified") - ErrDestEndpointHostInvalid = errors.New("field Address must be a valid IP address") + ErrDestEndpointHostInvalid = errors.New("field Address must be a valid IP or FQDN address") ErrDestEndpointPortInvalid = errors.New("field Port specified is invalid") ErrStringMatchConditionInvalid = errors.New("only one of the Exact, Prefix, SafeRegex or Distinct fields must be set") ErrStringMatchNameIsEmpty = errors.New("field Name must be specified") @@ -436,7 +437,7 @@ func (h HTTPRoute) Validate() error { func (j *JWT) validate() error { var errs error - if err := validation.ValidateJWTProvider(j.Providers); err != nil { + if err := egv1a1validation.ValidateJWTProvider(j.Providers); err != nil { errs = multierror.Append(errs, err) } @@ -466,7 +467,6 @@ func (r RouteDestination) Validate() error { } return errs - } // DestinationSetting holds the settings associated with the destination @@ -488,7 +488,6 @@ func (d DestinationSetting) Validate() error { } return errs - } // DestinationEndpoint holds the endpoint details associated with the destination @@ -498,15 +497,24 @@ type DestinationEndpoint struct { Host string `json:"host" yaml:"host"` // Port on the service to forward the request to. Port uint32 `json:"port" yaml:"port"` + // Type specifies the type of Host address. + Type discoveryv1.AddressType `json:"type" yaml:"type"` } // Validate the fields within the DestinationEndpoint structure func (d DestinationEndpoint) Validate() error { var errs error - // Only support IP hosts for now - if ip := net.ParseIP(d.Host); ip == nil { - errs = multierror.Append(errs, ErrDestEndpointHostInvalid) + switch d.Type { + case discoveryv1.AddressTypeFQDN: + if err := validation.IsDNS1123Subdomain(d.Host); err != nil { + errs = multierror.Append(errs, ErrDestEndpointHostInvalid) + } + default: + if ip := net.ParseIP(d.Host); ip == nil { + errs = multierror.Append(errs, ErrDestEndpointHostInvalid) + } } + if d.Port == 0 { errs = multierror.Append(errs, ErrDestEndpointPortInvalid) } @@ -514,6 +522,11 @@ func (d DestinationEndpoint) Validate() error { return errs } +// SetHostAddressType sets the host address type of DestinationEndpoint. +func (d *DestinationEndpoint) SetHostAddressType(hostType discoveryv1.AddressType) { + d.Type = hostType +} + // NewDestEndpoint creates a new DestinationEndpoint. func NewDestEndpoint(host string, port uint32) *DestinationEndpoint { return &DestinationEndpoint{ @@ -530,7 +543,7 @@ type AddHeader struct { Append bool `json:"append" yaml:"append"` } -// / Validate the fields within the AddHeader structure +// Validate the fields within the AddHeader structure func (h AddHeader) Validate() error { var errs error if h.Name == "" { diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index 82fd0937aef..48380bcd784 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -19,6 +19,7 @@ import ( matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "github.com/tetratelabs/multierror" + discoveryv1 "k8s.io/api/discovery/v1" extensionTypes "github.com/envoyproxy/gateway/internal/extension/types" "github.com/envoyproxy/gateway/internal/ir" @@ -211,15 +212,11 @@ func (t *Translator) processHTTPListenerXdsTranslation(tCtx *types.ResourceVersi vHost.Routes = append(vHost.Routes, xdsRoute) if httpRoute.Destination != nil { - if err := addXdsCluster(tCtx, &xdsClusterArgs{ - name: httpRoute.Destination.Name, - settings: httpRoute.Destination.Settings, - tSocket: nil, - protocol: protocol, - endpointType: Static, - loadBalancer: httpRoute.LoadBalancer, - }); err != nil && !errors.Is(err, ErrXdsClusterExists) { - return err + clusterArgs := splitEndpointsByHostAddressType(httpRoute.Destination, httpRoute.LoadBalancer, protocol) + for _, clusterArg := range clusterArgs { + if err := addXdsCluster(tCtx, clusterArg); err != nil && !errors.Is(err, ErrXdsClusterExists) { + return err + } } } @@ -368,7 +365,7 @@ func findXdsListener(tCtx *types.ResourceVersionTable, name string) *listenerv3. return nil } -// findXdsRouteConfig finds an xds route with the name and returns nil if there is no match. +// findXdsRouteConfig finds a xds route with the name and returns nil if there is no match. func findXdsRouteConfig(tCtx *types.ResourceVersionTable, name string) *routev3.RouteConfiguration { if tCtx == nil || tCtx.XdsResources == nil || tCtx.XdsResources[resourcev3.RouteType] == nil { return nil @@ -461,5 +458,53 @@ const ( const ( DefaultEndpointType EndpointType = iota Static + DNS EDS ) + +// splitEndpointsByHostAddressType splits FQDN host address type DestinationEndpoints from DestinationSettings. +func splitEndpointsByHostAddressType(rd *ir.RouteDestination, lb *ir.LoadBalancer, protocol ProtocolType) []*xdsClusterArgs { + // Group DestinationSettings by the type of endpoint. + groups := make(map[EndpointType][]*ir.DestinationSetting) + groupOrders := []EndpointType{Static, DNS} + + for _, ds := range rd.Settings { + var fqdn, ips []*ir.DestinationEndpoint + for _, ep := range ds.Endpoints { + if ep.Type == discoveryv1.AddressTypeFQDN { + fqdn = append(fqdn, ep) + } else { + ips = append(ips, ep) + } + } + if len(fqdn) > 0 { + groups[DNS] = append(groups[DNS], &ir.DestinationSetting{ + Weight: ds.Weight, + Endpoints: fqdn, + }) + } + if len(ips) > 0 { + groups[Static] = append(groups[Static], &ir.DestinationSetting{ + Weight: ds.Weight, + Endpoints: ips, + }) + } + } + + clusterArgs := make([]*xdsClusterArgs, 0, len(groups)) + for _, endpointType := range groupOrders { + if len(groups[endpointType]) == 0 { + continue + } + + clusterArgs = append(clusterArgs, &xdsClusterArgs{ + name: fmt.Sprintf("%s-%d", rd.Name, endpointType), + settings: groups[endpointType], + tSocket: nil, + protocol: protocol, + endpointType: endpointType, + loadBalancer: lb, + }) + } + return clusterArgs +} diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index 15fac37f495..fb371a7d1d5 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -18,6 +18,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" + discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" @@ -27,6 +28,7 @@ import ( "github.com/envoyproxy/gateway/internal/ir" "github.com/envoyproxy/gateway/internal/utils/field" "github.com/envoyproxy/gateway/internal/utils/file" + "github.com/envoyproxy/gateway/internal/utils/ptr" xtypes "github.com/envoyproxy/gateway/internal/xds/types" "github.com/envoyproxy/gateway/internal/xds/utils" ) @@ -449,6 +451,275 @@ func TestTranslateXdsWithExtension(t *testing.T) { } } +func TestSplitEndpointsByHostAddressType(t *testing.T) { + protocol := DefaultProtocol + + testCases := []struct { + name string + input *ir.RouteDestination + output []*xdsClusterArgs + }{ + { + name: "all ip host type addresses", + input: &ir.RouteDestination{ + Name: "test-cluster", + Settings: []*ir.DestinationSetting{ + { + Weight: ptr.To(uint32(1)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "1.2.3.4", + Port: 1001, + Type: discoveryv1.AddressTypeIPv4, + }, + { + Host: "5.6.7.8", + Port: 1002, + Type: discoveryv1.AddressTypeIPv4, + }, + }, + }, + { + Weight: ptr.To(uint32(10)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "9.8.7.6", + Port: 1003, + Type: discoveryv1.AddressTypeIPv4, + }, + { + Host: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + Port: 1004, + Type: discoveryv1.AddressTypeIPv6, + }, + }, + }, + }, + }, + output: []*xdsClusterArgs{ + { + name: "test-cluster-1", + protocol: protocol, + endpointType: Static, + settings: []*ir.DestinationSetting{ + { + Weight: ptr.To(uint32(1)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "1.2.3.4", + Port: 1001, + Type: discoveryv1.AddressTypeIPv4, + }, + { + Host: "5.6.7.8", + Port: 1002, + Type: discoveryv1.AddressTypeIPv4, + }, + }, + }, + { + Weight: ptr.To(uint32(10)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "9.8.7.6", + Port: 1003, + Type: discoveryv1.AddressTypeIPv4, + }, + { + Host: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + Port: 1004, + Type: discoveryv1.AddressTypeIPv6, + }, + }, + }, + }, + }, + }, + }, + { + name: "all fqdn host type addresses", + input: &ir.RouteDestination{ + Name: "test-cluster", + Settings: []*ir.DestinationSetting{ + { + Weight: ptr.To(uint32(1)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "foobar.com", + Port: 1001, + Type: discoveryv1.AddressTypeFQDN, + }, + { + Host: "foo.bar.com", + Port: 1002, + Type: discoveryv1.AddressTypeFQDN, + }, + }, + }, + { + Weight: ptr.To(uint32(10)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "foo.bar", + Port: 1003, + Type: discoveryv1.AddressTypeFQDN, + }, + }, + }, + }, + }, + output: []*xdsClusterArgs{ + { + name: "test-cluster-2", + protocol: protocol, + endpointType: DNS, + settings: []*ir.DestinationSetting{ + { + Weight: ptr.To(uint32(1)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "foobar.com", + Port: 1001, + Type: discoveryv1.AddressTypeFQDN, + }, + { + Host: "foo.bar.com", + Port: 1002, + Type: discoveryv1.AddressTypeFQDN, + }, + }, + }, + { + Weight: ptr.To(uint32(10)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "foo.bar", + Port: 1003, + Type: discoveryv1.AddressTypeFQDN, + }, + }, + }, + }, + }, + }, + }, + { + name: "mixed ip and fqdn host type addresses", + input: &ir.RouteDestination{ + Name: "test-cluster", + Settings: []*ir.DestinationSetting{ + { + Weight: ptr.To(uint32(1)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "1.2.3.4", + Port: 1001, + Type: discoveryv1.AddressTypeIPv4, + }, + { + Host: "foo.bar.com", + Port: 1002, + Type: discoveryv1.AddressTypeFQDN, + }, + }, + }, + { + Weight: ptr.To(uint32(10)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "foobar.com", + Port: 1003, + Type: discoveryv1.AddressTypeFQDN, + }, + { + Host: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + Port: 1004, + Type: discoveryv1.AddressTypeIPv6, + }, + }, + }, + }, + }, + output: []*xdsClusterArgs{ + { + name: "test-cluster-1", + protocol: protocol, + endpointType: Static, + settings: []*ir.DestinationSetting{ + { + Weight: ptr.To(uint32(1)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "1.2.3.4", + Port: 1001, + Type: discoveryv1.AddressTypeIPv4, + }, + }, + }, + { + Weight: ptr.To(uint32(10)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + Port: 1004, + Type: discoveryv1.AddressTypeIPv6, + }, + }, + }, + }, + }, + { + name: "test-cluster-2", + protocol: protocol, + endpointType: DNS, + settings: []*ir.DestinationSetting{ + { + Weight: ptr.To(uint32(1)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "foo.bar.com", + Port: 1002, + Type: discoveryv1.AddressTypeFQDN, + }, + }, + }, + { + Weight: ptr.To(uint32(10)), + Endpoints: []*ir.DestinationEndpoint{ + { + Host: "foobar.com", + Port: 1003, + Type: discoveryv1.AddressTypeFQDN, + }, + }, + }, + }, + }, + }, + }, + { + name: "empty addresses", + input: &ir.RouteDestination{ + Name: "test-cluster", + Settings: []*ir.DestinationSetting{ + { + Weight: ptr.To(uint32(100)), + Endpoints: []*ir.DestinationEndpoint{}, + }, + }, + }, + output: []*xdsClusterArgs{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := splitEndpointsByHostAddressType(tc.input, nil, protocol) + require.Equal(t, tc.output, actual) + }) + } +} + func requireXdsIRFromInputTestData(t *testing.T, name ...string) *ir.Xds { t.Helper() elems := append([]string{"testdata", "in"}, name...)