Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(expression router) Implement translator of ingresses #3935

Merged
merged 7 commits into from
May 4, 2023
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/_integration_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ jobs:
- name: postgres-traditional-compatible
test: postgres
router-flavor: 'traditional_compatible'
- name: dbless-expression-router
test: dbless
feature_gates: "ExpressionRoutes=true"
router-flavor: "expressions"

steps:
- uses: Kong/kong-license@master
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ Adding a new version? You'll need three changes:
IDs based on their unique properties (name, username, etc.) instead of random
UUIDs.
[#3933](https://github.com/Kong/kubernetes-ingress-controller/pull/3933)
- Added translator to translate ingresses under `networking.k8s.io/v1` to
expression based Kong routes. The translator is enabled when feature gate
`ExpressionRoutes` is turned on and the managed Kong gateway runs in router
flavor `expressions`.
Note: this feature is experimental, and not available to translate other
kubernetes objects that was originally supported.
[#3935](https://github.com/Kong/kubernetes-ingress-controller/pull/3935)

### Fixed

Expand Down
13 changes: 7 additions & 6 deletions FEATURE_GATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ Features that reach GA and over time become stable will be removed from this tab

### Feature gates for Alpha or Beta features

| Feature | Default | Stage | Since | Until |
|------------------------|---------|-------|-------|-------|
| Knative | `false` | Alpha | 0.8.0 | TBD |
| Gateway | `true` | Beta | 2.2.0 | TBD |
| CombinedRoutes | `true` | Beta | 2.8.0 | TBD |
| GatewayAlpha | `false` | Alpha | 2.6.0 | TBD |
| Feature | Default | Stage | Since | Until |
|------------------------|---------|-------|---------|-------|
| Knative | `false` | Alpha | 0.8.0 | TBD |
| Gateway | `true` | Beta | 2.2.0 | TBD |
| CombinedRoutes | `true` | Beta | 2.8.0 | TBD |
| GatewayAlpha | `false` | Alpha | 2.6.0 | TBD |
| ExpressionRoutes | `false` | Alpha | 2.10.0 | TBD |

**NOTE**: The `Gateway` feature gate refers to [Gateway
API](https://github.com/kubernetes-sigs/gateway-api) APIs which are in
Expand Down
7 changes: 7 additions & 0 deletions internal/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ func ExtractProtocolName(anns map[string]string) string {
// ExtractProtocolNames extracts the protocols supplied in the annotation.
func ExtractProtocolNames(anns map[string]string) []string {
val := anns[AnnotationPrefix+ProtocolsKey]
if len(val) == 0 {
return nil
}
return strings.Split(val, ",")
}

Expand Down Expand Up @@ -200,6 +203,10 @@ func ExtractPreserveHost(anns map[string]string) string {
return anns[AnnotationPrefix+PreserveHostKey]
}

func ExtractRegexPrefix(anns map[string]string) string {
return anns[AnnotationPrefix+RegexPrefixKey]
}

// HasServiceUpstreamAnnotation returns true if the annotation
// ingress.kubernetes.io/service-upstream is set to "true" in anns.
func HasServiceUpstreamAnnotation(anns map[string]string) bool {
Expand Down
30 changes: 18 additions & 12 deletions internal/dataplane/deckgen/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,23 @@ type PluginSchemaStore interface {
Schema(ctx context.Context, pluginName string) (map[string]interface{}, error)
}

// GenerateDeckContentParams is the parameters used to generate deck contents.
type GenerateDeckContentParams struct {
FormatVersion string
SelectorTags []string
ExpressionRoutes bool
PluginSchemas PluginSchemaStore
}

// ToDeckContent generates a decK configuration from `k8sState` and auxiliary parameters.
func ToDeckContent(
ctx context.Context,
log logrus.FieldLogger,
k8sState *kongstate.KongState,
schemas PluginSchemaStore,
selectorTags []string,
formatVersion string,
params GenerateDeckContentParams,
) *file.Content {
var content file.Content
content.FormatVersion = formatVersion
content.FormatVersion = params.FormatVersion
var err error

for _, s := range k8sState.Services {
Expand All @@ -36,7 +42,7 @@ func ToDeckContent(
plugin := file.FPlugin{
Plugin: *p.DeepCopy(),
}
err = fillPlugin(ctx, &plugin, schemas)
err = fillPlugin(ctx, &plugin, params.PluginSchemas)
if err != nil {
log.Errorf("failed to fill-in defaults for plugin: %s", *plugin.Name)
}
Expand All @@ -48,13 +54,13 @@ func ToDeckContent(

for _, r := range s.Routes {
route := file.FRoute{Route: r.Route}
fillRoute(&route.Route)
fillRoute(&route.Route, params.ExpressionRoutes)

for _, p := range r.Plugins {
plugin := file.FPlugin{
Plugin: *p.DeepCopy(),
}
err = fillPlugin(ctx, &plugin, schemas)
err = fillPlugin(ctx, &plugin, params.PluginSchemas)
if err != nil {
log.Errorf("failed to fill-in defaults for plugin: %s", *plugin.Name)
}
Expand All @@ -78,7 +84,7 @@ func ToDeckContent(
plugin := file.FPlugin{
Plugin: plugin.Plugin,
}
err = fillPlugin(ctx, &plugin, schemas)
err = fillPlugin(ctx, &plugin, params.PluginSchemas)
if err != nil {
log.Errorf("failed to fill-in defaults for plugin: %s", *plugin.Name)
}
Expand Down Expand Up @@ -171,20 +177,20 @@ func ToDeckContent(
sort.SliceStable(content.Consumers, func(i, j int) bool {
return strings.Compare(*content.Consumers[i].Username, *content.Consumers[j].Username) > 0
})
if len(selectorTags) > 0 {
if len(params.SelectorTags) > 0 {
content.Info = &file.Info{
SelectorTags: selectorTags,
SelectorTags: params.SelectorTags,
}
}

return &content
}

func fillRoute(route *kong.Route) {
func fillRoute(route *kong.Route, expressionRoutes bool) {
if route.HTTPSRedirectStatusCode == nil {
route.HTTPSRedirectStatusCode = kong.Int(426)
}
if route.PathHandling == nil {
if route.PathHandling == nil && !expressionRoutes {
route.PathHandling = kong.String("v0")
}
}
Expand Down
40 changes: 30 additions & 10 deletions internal/dataplane/kong_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ type KongClient struct {
// the newer logic which combines them.
enableCombinedServiceRoutes bool

// enableExpressionRoutes indicates whether the data-plane client will
// translate kubernetes object to expression based routes.
enableExpressionRoutes bool

// requestTimeout is the maximum amount of time that should be waited for
// requests to the data-plane to receive a response.
requestTimeout time.Duration
Expand Down Expand Up @@ -372,6 +376,18 @@ func (c *KongClient) AreCombinedServiceRoutesEnabled() bool {
return c.enableCombinedServiceRoutes
}

func (c *KongClient) EnableExpressionRoutes() {
c.additionalFeaturesLock.Lock()
defer c.additionalFeaturesLock.Unlock()
c.enableExpressionRoutes = true
}

func (c *KongClient) AreExpressionRoutesEnabled() bool {
c.additionalFeaturesLock.RLock()
defer c.additionalFeaturesLock.RUnlock()
return c.enableExpressionRoutes
}

func (c *KongClient) EnableLicenseAgent(agent *license.Agent) {
c.licenseAgent = agent
}
Expand Down Expand Up @@ -411,6 +427,10 @@ func (c *KongClient) Update(ctx context.Context) error {
if c.AreCombinedServiceRoutesEnabled() {
p.EnableCombinedServiceRoutes()
}
if c.AreExpressionRoutesEnabled() {
p.EnableExpressionRoutes()
}

if c.licenseAgent != nil {
c.logger.Debug("retrieving license from agent and adding it to config")
p.InjectLicense(c.licenseAgent.GetLicense())
Expand Down Expand Up @@ -495,16 +515,20 @@ func (c *KongClient) sendToClient(
logger := c.logger.WithField("url", client.AdminAPIClient().BaseRootURL())

// generate the deck configuration to be applied to the admin API
deckGenParams := deckgen.GenerateDeckContentParams{
FormatVersion: formatVersion,
SelectorTags: config.FilterTags,
ExpressionRoutes: c.AreExpressionRoutesEnabled(),
PluginSchemas: client.PluginSchemaStore(),
}
logger.Debug("converting configuration to deck config")
targetConfig := deckgen.ToDeckContent(ctx,
logger,
s,
client.PluginSchemaStore(),
config.FilterTags,
formatVersion,
deckGenParams,
)

sendDiagnostic := prepareSendDiagnosticFn(ctx, logger, c.diagnostic, s, targetConfig, client.PluginSchemaStore(), config.FilterTags, formatVersion)
sendDiagnostic := prepareSendDiagnosticFn(ctx, logger, c.diagnostic, s, targetConfig, deckGenParams)

// apply the configuration update in Kong
timedCtx, cancel := context.WithTimeout(ctx, c.requestTimeout)
Expand Down Expand Up @@ -573,9 +597,7 @@ func prepareSendDiagnosticFn(
diagnosticConfig util.ConfigDumpDiagnostic,
targetState *kongstate.KongState,
targetContent *file.Content,
pluginSchemaStore deckgen.PluginSchemaStore,
filterTags []string,
formatVersion string,
deckGenParams deckgen.GenerateDeckContentParams,
) sendDiagnosticFn {
if diagnosticConfig == (util.ConfigDumpDiagnostic{}) {
// noop, diagnostics won't be sent
Expand All @@ -587,9 +609,7 @@ func prepareSendDiagnosticFn(
redactedConfig := deckgen.ToDeckContent(ctx,
log,
targetState.SanitizedCopy(),
pluginSchemaStore,
filterTags,
formatVersion,
deckGenParams,
)
config = redactedConfig
} else {
Expand Down
47 changes: 31 additions & 16 deletions internal/dataplane/kongstate/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import (
type Route struct {
kong.Route

Ingress util.K8sObjectInfo
Plugins []kong.Plugin
Ingress util.K8sObjectInfo
Plugins []kong.Plugin
ExpressionRoutes bool
}

var (
Expand All @@ -35,6 +36,10 @@ var (

// normalizeProtocols prevents users from mismatching grpc/http.
func (r *Route) normalizeProtocols() {
// skip updating protocols if expression routes enabled.
if r.ExpressionRoutes {
return
}
protocols := r.Protocols
var http, grpc bool

Expand All @@ -53,6 +58,11 @@ func (r *Route) normalizeProtocols() {
if grpc && http {
r.Protocols = kong.StringSlice("http", "https")
}

if grpc {
// grpc(s) doesn't accept strip_path
r.StripPath = nil
}
}

// useSSLProtocol updates the protocol of the route to either https or grpcs, or https and grpcs.
Expand Down Expand Up @@ -106,6 +116,9 @@ func (r *Route) overrideStripPath(anns map[string]string) {

func (r *Route) overrideProtocols(anns map[string]string) {
protocols := annotations.ExtractProtocolNames(anns)
if len(protocols) == 0 {
return
}
var prots []*string
for _, prot := range protocols {
if !util.ValidateProtocol(prot) {
Expand Down Expand Up @@ -218,18 +231,22 @@ func (r *Route) overrideSNIs(log logrus.FieldLogger, anns map[string]string) {

// overrideByAnnotation sets Route protocols via annotation.
func (r *Route) overrideByAnnotation(log logrus.FieldLogger) {
r.overrideProtocols(r.Ingress.Annotations)
r.overrideStripPath(r.Ingress.Annotations)
r.overrideHTTPSRedirectCode(r.Ingress.Annotations)
r.overridePreserveHost(r.Ingress.Annotations)
r.overrideRegexPriority(r.Ingress.Annotations)
r.overrideMethods(log, r.Ingress.Annotations)
r.overrideSNIs(log, r.Ingress.Annotations)
r.overrideRequestBuffering(log, r.Ingress.Annotations)
r.overrideResponseBuffering(log, r.Ingress.Annotations)
r.overrideHosts(log, r.Ingress.Annotations)
r.overrideHeaders(r.Ingress.Annotations)
r.overridePathHandling(log, r.Ingress.Annotations)
// skip the fields that are not supported when kong is using expression router:
// `protocols`, `regexPriority`, `methods`, `snis`, `hosts`, `headers`, `pathHandling`,
if !r.ExpressionRoutes {
randmonkey marked this conversation as resolved.
Show resolved Hide resolved
r.overrideProtocols(r.Ingress.Annotations)
r.overrideRegexPriority(r.Ingress.Annotations)
r.overrideMethods(log, r.Ingress.Annotations)
r.overrideSNIs(log, r.Ingress.Annotations)
r.overrideHosts(log, r.Ingress.Annotations)
r.overrideHeaders(r.Ingress.Annotations)
r.overridePathHandling(log, r.Ingress.Annotations)
}
}

// override sets Route fields by KongIngress first, then by annotation.
Expand All @@ -255,17 +272,15 @@ func (r *Route) override(log logrus.FieldLogger, kongIngress *configurationv1.Ko
r.overrideByKongIngress(log, kongIngress)
r.overrideByAnnotation(log)
r.normalizeProtocols()
for _, val := range r.Protocols {
if *val == "grpc" || *val == "grpcs" {
// grpc(s) doesn't accept strip_path
r.StripPath = nil
break
}
}
}

// overrideByKongIngress sets Route fields by KongIngress.
func (r *Route) overrideByKongIngress(log logrus.FieldLogger, kongIngress *configurationv1.KongIngress) {
// disable overriding routes by KongIngress if expression routes is enabled.
if r.ExpressionRoutes {
return
}

if kongIngress == nil || kongIngress.Route == nil {
return
}
Expand Down
8 changes: 8 additions & 0 deletions internal/dataplane/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Parser struct {

featureEnabledReportConfiguredKubernetesObjects bool
featureEnabledCombinedServiceRoutes bool
featureEnabledExpressionRoutes bool

license *kong.License

Expand Down Expand Up @@ -192,6 +193,13 @@ func (p *Parser) EnableRegexPathPrefix() {
p.flagEnabledRegexPathPrefix = true
}

// EnableExpressionRoutes enables parser to translate kubernetes managed resources (like Ingresses, HTTPRoutes)
// into expression based routes in kong configurations. It should be turned on when kong is running with
// expressions router flavor and feature gate `ExpressionRoutes` is turned on.
func (p *Parser) EnableExpressionRoutes() {
p.featureEnabledExpressionRoutes = true
}

// InjectLicense sets a license to inject into configuration.
func (p *Parser) InjectLicense(license kong.License) {
p.license = &license
Expand Down
Loading