Skip to content

Commit

Permalink
feat(parser) add UDPRoute support
Browse files Browse the repository at this point in the history
  • Loading branch information
rainest committed Mar 24, 2022
1 parent b9a6fc1 commit be273eb
Show file tree
Hide file tree
Showing 5 changed files with 600 additions and 6 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@
- Admission webhook certificate files now track updates to the file, and will
update when the corresponding Secret has changed.
[#2258](https://github.com/Kong/kubernetes-ingress-controller/pull/2258)
- Added support for Gateway API [UDPRoute](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.UDPRoute)
resources.
[#2363](https://github.com/Kong/kubernetes-ingress-controller/pull/2363)

#### Fixed

Expand Down
1 change: 1 addition & 0 deletions internal/dataplane/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func (p *Parser) Build() (*kongstate.KongState, error) {
p.ingressRulesFromUDPIngressV1beta1(),
p.ingressRulesFromKnativeIngress(),
p.ingressRulesFromHTTPRoutes(),
p.ingressRulesFromUDPRoutes(),
)

// populate any Kubernetes Service objects relevant objects
Expand Down
217 changes: 217 additions & 0 deletions internal/dataplane/parser/translate_udproute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package parser

import (
"fmt"

"github.com/kong/go-kong/kong"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"

"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
"github.com/kong/kubernetes-ingress-controller/v2/internal/util"
)

// -----------------------------------------------------------------------------
// Translate UDPRoute - IngressRules Translation
// -----------------------------------------------------------------------------

// ingressRulesFromUDPRoutes processes a list of UDPRoute objects and translates
// then into Kong configuration objects.
func (p *Parser) ingressRulesFromUDPRoutes() ingressRules {
result := newIngressRules()

udpRouteList, err := p.storer.ListUDPRoutes()
if err != nil {
p.logger.Errorf("failed to list UDPRoutes: %w", err)
return result
}

var errs []error
for _, udproute := range udpRouteList {
if err := ingressRulesFromUDPRoute(&result, udproute); err != nil {
err = fmt.Errorf("UDPRoute %s/%s can't be routed: %w", udproute.Namespace, udproute.Name, err)
errs = append(errs, err)
} else {
// at this point the object has been configured and can be
// reported as successfully parsed.
p.ReportKubernetesObjectUpdate(udproute)
}
}

if len(errs) > 0 {
for _, err := range errs {
p.logger.Errorf(err.Error())
}
}

return result
}

func ingressRulesFromUDPRoute(result *ingressRules, udproute *gatewayv1alpha2.UDPRoute) error {
// first we grab the spec and gather some metdata about the object
spec := udproute.Spec

// validation for UDPRoutes will happen at a higher layer, but in spite of that we run
// validation at this level as well as a fallback so that if routes are posted which
// are invalid somehow make it past validation (e.g. the webhook is not enabled) we can
// at least try to provide a helpful message about the situation in the manager logs.
if len(spec.Rules) == 0 {
return fmt.Errorf("no rules provided")
}

// each rule may represent a different set of backend services that will be accepting
// traffic, so we make separate routes and Kong services for every present rule.
for ruleNumber, rule := range spec.Rules {
// TODO: add this to a generic UDPRoute validation, and then we should probably
// simply be calling validation on each udproute object at the begininning
// of the topmost list.
if len(rule.BackendRefs) == 0 {
return fmt.Errorf("missing backendRef in rule")
}

// TODO: support multiple backend refs
if len(rule.BackendRefs) > 1 {
return fmt.Errorf("multiple backendRefs are not yet supported")
}

// determine the routes needed to route traffic to services for this rule
routes, err := generateKongRoutesFromUDPRouteRule(udproute, ruleNumber, rule)
if err != nil {
return err
}

// create a service and attach the routes to it
service := generateKongServiceFromUDPRouteBackendRef(result, udproute, rule.BackendRefs[0])
service.Routes = append(service.Routes, routes...)

// cache the service to avoid duplicates in further loop iterations
result.ServiceNameToServices[*service.Service.Name] = service
}

return nil
}

// -----------------------------------------------------------------------------
// Translate UDPRoute - Utils
// -----------------------------------------------------------------------------

// generateKongRoutesFromUDPRouteRule converts an UDPRoute rule to one or more
// Kong Route objects to route traffic to services.
func generateKongRoutesFromUDPRouteRule(udproute *gatewayv1alpha2.UDPRoute, ruleNumber int, rule gatewayv1alpha2.UDPRouteRule) ([]kongstate.Route, error) {
// gather the k8s object information and hostnames from the udproute
objectInfo := util.FromK8sObject(udproute)

var routes []kongstate.Route
if len(rule.Matches) > 0 {
// As of 2022-03-04, matches are supported only in experimental CRDs. if you apply a UDPRoute with matches against
// the stable CRDs, the matches disappear into the ether (only if doing it via client-go, kubectl rejects them)

// the UDPRoute specification upstream doesn't clearly indicate whether matches are ANDed or ORed, so we OR for
// now, same as HTTPRoute. TODO clarify upstream
for matchNumber, _ := range rule.Matches {
routeName := kong.String(fmt.Sprintf(
"udproute.%s.%s.%d.%d",
udproute.Namespace,
udproute.Name,
ruleNumber,
matchNumber,
))

// build the route object
r := kongstate.Route{
Ingress: objectInfo,
Route: kong.Route{
Name: routeName,
Protocols: kong.StringSlice("udp"),
//Sources: TODO extract from match.SourceAddresses
//Destinations: TODO extract from match.DestinationAddresses and backendRef
// TODO upstream does not appear to have many defined means of applying PNAT: matches only support
// IPAddress and NamedAddress address matches. although NamedAddress is documented as
// "vendor-specific behavior, you're on your own", its naming doesn't suggest we should use it for
// ports, though we could. However, there doesn't appear to be anything else we'd need to use it
// for at present, given that SNI has dedicated functionality in TLSRoute. For now, we hard-code
// this to our test port, to defer this question while testing the rest of the feature.
Destinations: []*kong.CIDRPort{
{
Port: kong.Int(9999),
},
},
},
}

routes = append(routes, r)
}
} else {
routeName := kong.String(fmt.Sprintf(
"udproute.%s.%s.%d.%d",
udproute.Namespace,
udproute.Name,
ruleNumber,
0,
))

// build the route object
r := kongstate.Route{
Ingress: objectInfo,
Route: kong.Route{
Name: routeName,
Protocols: kong.StringSlice("udp"),
Destinations: []*kong.CIDRPort{
{
Port: kong.Int(9999),
},
},
},
}

routes = append(routes, r)
}

return routes, nil
}

// generateKongServiceFromUDPRouteBackendRef converts a provided backendRef for an UDPRoute
// into a kong.Service so that routes for that object can be attached to the Service.
// TODO add a generic backendRef handler for all GW routes. HTTPRoute needs a wrapper because it uses a special wrapped
// type with filters. Deferred til after https://github.com/Kong/kubernetes-ingress-controller/issues/2166 though
// we probably shouldn't see much change to the service (just the upstream it references in Host)
func generateKongServiceFromUDPRouteBackendRef(result *ingressRules, udproute *gatewayv1alpha2.UDPRoute, backendRef gatewayv1alpha2.BackendRef) kongstate.Service {
// determine the service namespace
// TODO: need to add validation to restrict namespaces in backendRefs
namespace := udproute.Namespace
if backendRef.Namespace != nil {
namespace = string(*backendRef.Namespace)
}

// determine the name of the Service
serviceName := fmt.Sprintf("%s.%s.%d", namespace, backendRef.Name, *backendRef.Port)

// determine the Service port
port := kongstate.PortDef{
Mode: kongstate.PortModeByNumber,
Number: int32(*backendRef.Port),
}

// check if the service is already known, and if not create it
service, ok := result.ServiceNameToServices[serviceName]
if !ok {
service = kongstate.Service{
Service: kong.Service{
Name: kong.String(serviceName),
Host: kong.String(fmt.Sprintf("%s.%s.%s.svc", backendRef.Name, namespace, port.CanonicalString())),
Port: kong.Int(int(*backendRef.Port)),
Protocol: kong.String("udp"),
ConnectTimeout: kong.Int(DefaultServiceTimeout),
ReadTimeout: kong.Int(DefaultServiceTimeout),
WriteTimeout: kong.Int(DefaultServiceTimeout),
Retries: kong.Int(DefaultRetries),
},
Namespace: udproute.Namespace,
Backend: kongstate.ServiceBackend{
Name: string(backendRef.Name),
Port: port,
},
}
}

return service
}
13 changes: 7 additions & 6 deletions test/integration/udpingress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"testing"
"time"

ktfkong "github.com/kong/kubernetes-testing-framework/pkg/clusters/addons/kong"
"github.com/kong/kubernetes-testing-framework/pkg/utils/kubernetes/generators"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -99,7 +100,7 @@ func TestUDPIngressEssentials(t *testing.T) {
},
Spec: kongv1beta1.UDPIngressSpec{Rules: []kongv1beta1.UDPIngressRule{
{
Port: 9999,
Port: ktfkong.DefaultUDPServicePort,
Backend: kongv1beta1.IngressBackend{
ServiceName: service.Name,
ServicePort: int(service.Spec.Ports[0].Port),
Expand Down Expand Up @@ -128,7 +129,7 @@ func TestUDPIngressEssentials(t *testing.T) {
d := net.Dialer{
Timeout: time.Second * 5,
}
return d.DialContext(ctx, network, fmt.Sprintf("%s:9999", proxyUDPURL.Hostname()))
return d.DialContext(ctx, network, fmt.Sprintf("%s:%d", proxyUDPURL.Hostname(), ktfkong.DefaultUDPServicePort))
},
}

Expand Down Expand Up @@ -237,7 +238,7 @@ func TestUDPIngressTCPIngressCollision(t *testing.T) {
},
Spec: kongv1beta1.UDPIngressSpec{Rules: []kongv1beta1.UDPIngressRule{
{
Port: 9999,
Port: ktfkong.DefaultUDPServicePort,
Backend: kongv1beta1.IngressBackend{
ServiceName: service.Name,
ServicePort: int(service.Spec.Ports[0].Port),
Expand Down Expand Up @@ -286,7 +287,7 @@ func TestUDPIngressTCPIngressCollision(t *testing.T) {

t.Logf("checking DNS to resolve via UDPIngress %s", udp.Name)
assert.Eventually(t, func() bool {
_, _, err := dnsUDPClient.Exchange(query, fmt.Sprintf("%s:9999", proxyUDPURL.Hostname()))
_, _, err := dnsUDPClient.Exchange(query, fmt.Sprintf("%s:%d", proxyUDPURL.Hostname(), ktfkong.DefaultUDPServicePort))
return err == nil
}, ingressWait, waitTick)

Expand Down Expand Up @@ -347,15 +348,15 @@ func TestUDPIngressTCPIngressCollision(t *testing.T) {

t.Logf("checking DNS to resolve via UDPIngress %s still works also", udp.Name)
assert.Eventually(t, func() bool {
_, _, err := dnsUDPClient.Exchange(query, fmt.Sprintf("%s:9999", proxyUDPURL.Hostname()))
_, _, err := dnsUDPClient.Exchange(query, fmt.Sprintf("%s:%d", proxyUDPURL.Hostname(), ktfkong.DefaultUDPServicePort))
return err == nil
}, ingressWait, waitTick)

// Cleanup
t.Logf("tearing down UDPIngress %s and ensuring backends are torn down", udp.Name)
assert.NoError(t, c.ConfigurationV1beta1().UDPIngresses(ns.Name).Delete(ctx, udp.Name, metav1.DeleteOptions{}))
assert.Eventually(t, func() bool {
_, _, err := dnsUDPClient.Exchange(query, fmt.Sprintf("%s:9999", proxyUDPURL.Hostname()))
_, _, err := dnsUDPClient.Exchange(query, fmt.Sprintf("%s:%d", proxyUDPURL.Hostname(), ktfkong.DefaultUDPServicePort))
if err != nil {
if strings.Contains(err.Error(), "i/o timeout") {
return true
Expand Down
Loading

0 comments on commit be273eb

Please sign in to comment.