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

APIGW4CONK8S: HTTP Route/TCPRoute/Secrets Translation #2088

Merged
merged 11 commits into from
May 8, 2023
212 changes: 209 additions & 3 deletions control-plane/api-gateway/consul/config_entry_translation.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

// Package consul handles interaction with consul servers from the controller
package consul

import (
Expand All @@ -8,7 +12,6 @@ import (
gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

"github.com/hashicorp/consul-k8s/control-plane/namespaces"
"github.com/hashicorp/consul/api"
capi "github.com/hashicorp/consul/api"
)

Expand All @@ -18,7 +21,10 @@ const (
metaKeyKubeNS = "k8s-namespace"
metaKeyKubeServiceName = "k8s-service-name"

// AnnotationGateway is the annotation used to override the gateway name.
AnnotationGateway = "consul.hashicorp.com/gateway"
// AnnotationHTTPRoute is the annotation used to override the http route name.
AnnotationHTTPRoute = "consul.hashicorp.com/http-route"
)

type consulIdentifier struct {
Expand All @@ -27,13 +33,15 @@ type consulIdentifier struct {
partition string
}

// Translator handles translating K8s resources into Consul config entries.
type Translator struct {
EnableConsulNamespaces bool
ConsulDestNamespace string
EnableK8sMirroring bool
MirroringPrefix string
}

// GatewayToAPIGateway translates a k8s API gateway into a Consul API gateway.
func (t Translator) GatewayToAPIGateway(k8sGW gwv1beta1.Gateway, certs map[types.NamespacedName]consulIdentifier) capi.APIGatewayConfigEntry {
listeners := make([]capi.APIGatewayListener, 0, len(k8sGW.Spec.Listeners))
consulPartition := os.Getenv("CONSUL_PARTITION")
Expand Down Expand Up @@ -73,7 +81,7 @@ func (t Translator) GatewayToAPIGateway(k8sGW gwv1beta1.Gateway, certs map[types
}

return capi.APIGatewayConfigEntry{
Kind: api.APIGateway,
Kind: capi.APIGateway,
Name: gwName,
Meta: map[string]string{
metaKeyManagedBy: metaValueManagedBy,
Expand All @@ -82,8 +90,206 @@ func (t Translator) GatewayToAPIGateway(k8sGW gwv1beta1.Gateway, certs map[types
},
Listeners: listeners,
Partition: consulPartition,
Namespace: namespaces.ConsulNamespace(k8sGW.GetObjectMeta().GetNamespace(), t.EnableK8sMirroring, t.ConsulDestNamespace, t.EnableK8sMirroring, t.MirroringPrefix),
Namespace: t.getConsulNamespace(k8sGW.GetObjectMeta().GetNamespace()),
}
}

// HTTPRouteToHTTPRoute translates a k8s http route into a consul config http route.
func (t Translator) HTTPRouteToHTTPRoute(k8sHTTPRoute gwv1beta1.HTTPRoute, parentRefs map[types.NamespacedName]consulIdentifier) capi.HTTPRouteConfigEntry {
consulPartition := os.Getenv("CONSUL_PARTITION")
jm96441n marked this conversation as resolved.
Show resolved Hide resolved

routeName := k8sHTTPRoute.Name
if routeNameFromAnnotation, ok := k8sHTTPRoute.Annotations[AnnotationHTTPRoute]; ok && routeNameFromAnnotation != "" && !strings.Contains(routeNameFromAnnotation, ",") {
routeName = routeNameFromAnnotation
}

consulHTTPRoute := capi.HTTPRouteConfigEntry{
Kind: capi.HTTPRoute,
Name: routeName,
Meta: map[string]string{
metaKeyManagedBy: metaValueManagedBy,
metaKeyKubeNS: k8sHTTPRoute.GetObjectMeta().GetNamespace(),
metaKeyKubeServiceName: k8sHTTPRoute.GetObjectMeta().GetName(),
},
Partition: consulPartition,

Namespace: t.getConsulNamespace(k8sHTTPRoute.GetObjectMeta().GetNamespace()),
}

// translate hostnames
hostnames := make([]string, 0, len(k8sHTTPRoute.Spec.Hostnames))
for _, k8Hostname := range k8sHTTPRoute.Spec.Hostnames {
hostnames = append(hostnames, string(k8Hostname))
}
consulHTTPRoute.Hostnames = hostnames

// translate parent refs
consulHTTPRoute.Parents = translateHTTPRouteParentRefs(k8sHTTPRoute.Spec.CommonRouteSpec.ParentRefs, parentRefs)

// translate rules
consulHTTPRoute.Rules = t.translateHTTPRouteRules(k8sHTTPRoute.Spec.Rules)

return consulHTTPRoute
}

func translateHTTPRouteParentRefs(k8sParentRefs []gwv1beta1.ParentReference, parentRefs map[types.NamespacedName]consulIdentifier) []capi.ResourceReference {
parents := make([]capi.ResourceReference, 0, len(k8sParentRefs))
for _, k8sParentRef := range k8sParentRefs {
parentRef, ok := parentRefs[types.NamespacedName{Name: string(k8sParentRef.Name), Namespace: string(*k8sParentRef.Namespace)}]
if !(ok && isRefAPIGateway(k8sParentRef)) {
// we drop any parent refs that consul does not know about
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we actually might not need to do this: we should be able to use the namespace resolution logic that we use elsewhere and not care about the partition since consul will put everything into the same partition right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also should we be logging when we drop a ref here and in the certs earlier in this file? that might be something we want to signal to the user

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not certain, @andrewstucki?

continue
}
sectionName := ""
if k8sParentRef.SectionName != nil {
sectionName = string(*k8sParentRef.SectionName)
}
ref := capi.ResourceReference{
Kind: capi.APIGateway, // Will this ever not be a gateway? is that something we need to handle?
jm96441n marked this conversation as resolved.
Show resolved Hide resolved
Name: parentRef.name,
SectionName: sectionName,
Partition: parentRef.partition,
Namespace: parentRef.namespace,
}
parents = append(parents, ref)
}
return parents
}

func isRefAPIGateway(ref gwv1beta1.ParentReference) bool {
return ref.Kind != nil && *ref.Kind == gwv1beta1.Kind("Gateway") || ref.Group != nil && string(*ref.Group) == gwv1beta1.GroupName
}

func (t Translator) translateHTTPRouteRules(k8sRules []gwv1beta1.HTTPRouteRule) []capi.HTTPRouteRule {
rules := make([]capi.HTTPRouteRule, 0, len(k8sRules))
for _, k8sRule := range k8sRules {
rule := capi.HTTPRouteRule{}
// translate matches
rule.Matches = translateHTTPMatches(k8sRule.Matches)

// translate filters
rule.Filters = translateHTTPFilters(k8sRule.Filters)

// translate services
rule.Services = t.translateHTTPServices(k8sRule.BackendRefs)

rules = append(rules, rule)
}
return rules
}

var headerMatchTypeTranslation = map[gwv1beta1.HeaderMatchType]capi.HTTPHeaderMatchType{
gwv1beta1.HeaderMatchExact: capi.HTTPHeaderMatchExact,
gwv1beta1.HeaderMatchRegularExpression: capi.HTTPHeaderMatchRegularExpression,
}

var headerPathMatchTypeTranslation = map[gwv1beta1.PathMatchType]capi.HTTPPathMatchType{
gwv1beta1.PathMatchExact: capi.HTTPPathMatchExact,
gwv1beta1.PathMatchPathPrefix: capi.HTTPPathMatchPrefix,
gwv1beta1.PathMatchRegularExpression: capi.HTTPPathMatchRegularExpression,
}

var queryMatchTypeTranslation = map[gwv1beta1.QueryParamMatchType]capi.HTTPQueryMatchType{
gwv1beta1.QueryParamMatchExact: capi.HTTPQueryMatchExact,
gwv1beta1.QueryParamMatchRegularExpression: capi.HTTPQueryMatchRegularExpression,
}

// translate the http matches section.
func translateHTTPMatches(k8sMatches []gwv1beta1.HTTPRouteMatch) []capi.HTTPMatch {
matches := make([]capi.HTTPMatch, 0, len(k8sMatches))
for _, k8sMatch := range k8sMatches {
// translate header matches
headers := make([]capi.HTTPHeaderMatch, 0, len(k8sMatch.Headers))
for _, k8sHeader := range k8sMatch.Headers {
header := capi.HTTPHeaderMatch{
Match: headerMatchTypeTranslation[*k8sHeader.Type],
Name: string(k8sHeader.Name),
Value: k8sHeader.Value,
}
headers = append(headers, header)
}

// translate query matches
queries := make([]capi.HTTPQueryMatch, 0, len(k8sMatch.QueryParams))
for _, k8sQuery := range k8sMatch.QueryParams {
query := capi.HTTPQueryMatch{
Match: queryMatchTypeTranslation[*k8sQuery.Type],
Name: k8sQuery.Name,
Value: k8sQuery.Value,
}
queries = append(queries, query)
}

match := capi.HTTPMatch{
Headers: headers,
Method: capi.HTTPMatchMethod(*k8sMatch.Method),
Path: capi.HTTPPathMatch{
Match: headerPathMatchTypeTranslation[*k8sMatch.Path.Type],
Value: string(*k8sMatch.Path.Value),
},
Query: queries,
}
matches = append(matches, match)
}
return matches
}

// translate the http filters section.
func translateHTTPFilters(k8sFilters []gwv1beta1.HTTPRouteFilter) capi.HTTPFilters {
add := make(map[string]string)
set := make(map[string]string)
remove := make([]string, 0)
var urlRewrite *capi.URLRewrite
for _, k8sFilter := range k8sFilters {
for _, adder := range k8sFilter.RequestHeaderModifier.Add {
add[string(adder.Name)] = adder.Value
}

for _, setter := range k8sFilter.RequestHeaderModifier.Set {
set[string(setter.Name)] = setter.Value
}

remove = append(remove, k8sFilter.RequestHeaderModifier.Remove...)

// we drop any path rewrites that are not prefix matches as we don't support those
if k8sFilter.URLRewrite != nil && k8sFilter.URLRewrite.Path.Type == gwv1beta1.PrefixMatchHTTPPathModifier {
urlRewrite = &capi.URLRewrite{Path: *k8sFilter.URLRewrite.Path.ReplacePrefixMatch}
}

}
filter := capi.HTTPFilters{
Headers: []capi.HTTPHeaderFilter{
{
Add: add,
Remove: remove,
Set: set,
},
},
URLRewrite: urlRewrite,
}

return filter
}

// translate the backendrefs into services.
func (t Translator) translateHTTPServices(k8sBackendRefs []gwv1beta1.HTTPBackendRef) []capi.HTTPService {
services := make([]capi.HTTPService, 0, len(k8sBackendRefs))

for _, k8sRef := range k8sBackendRefs {
service := capi.HTTPService{
Name: string(k8sRef.Name),
Weight: int(*k8sRef.Weight),
Filters: translateHTTPFilters(k8sRef.Filters),
Namespace: t.getConsulNamespace(string(*k8sRef.Namespace)),
}
services = append(services, service)
}

return services
}

func (t Translator) getConsulNamespace(k8sNS string) string {
return namespaces.ConsulNamespace(k8sNS, t.EnableK8sMirroring, t.ConsulDestNamespace, t.EnableK8sMirroring, t.MirroringPrefix)
}

func ptrTo[T any](v T) *T {
Expand Down
Loading