pkg/providers/gateway/translation/gateway_httproute.go (288 lines of code) (raw):

// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package translation import ( "fmt" "strings" "github.com/pkg/errors" "go.uber.org/zap" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/apache/apisix-ingress-controller/pkg/id" "github.com/apache/apisix-ingress-controller/pkg/log" "github.com/apache/apisix-ingress-controller/pkg/providers/translation" "github.com/apache/apisix-ingress-controller/pkg/providers/utils" "github.com/apache/apisix-ingress-controller/pkg/types" apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1" ) func (t *translator) generatePluginsFromHTTPRouteFilter(namespace string, filters []gatewayv1beta1.HTTPRouteFilter) apisixv1.Plugins { plugins := apisixv1.Plugins{} for _, filter := range filters { switch filter.Type { case gatewayv1beta1.HTTPRouteFilterRequestHeaderModifier: t.generatePluginFromHTTPRequestHeaderFilter(plugins, filter.RequestHeaderModifier) case gatewayv1beta1.HTTPRouteFilterRequestRedirect: t.generatePluginFromHTTPRequestRedirectFilter(plugins, filter.RequestRedirect) case gatewayv1beta1.HTTPRouteFilterRequestMirror: t.generatePluginFromHTTPRequestMirrorFilter(namespace, plugins, filter.RequestMirror) case gatewayv1beta1.HTTPRouteFilterURLRewrite: // TODO: Supported, to be implemented case gatewayv1beta1.HTTPRouteFilterResponseHeaderModifier: // TODO: Supported, to be implemented } } return plugins } func (t *translator) generatePluginFromHTTPRequestHeaderFilter(plugins apisixv1.Plugins, reqHeaderModifier *gatewayv1beta1.HTTPHeaderFilter) { if reqHeaderModifier == nil { return } headers := map[string]any{} // TODO: The current apisix plugin does not conform to the specification. for _, header := range reqHeaderModifier.Add { headers[string(header.Name)] = header.Value } for _, header := range reqHeaderModifier.Set { headers[string(header.Name)] = header.Value } for _, header := range reqHeaderModifier.Remove { headers[header] = "" } plugins["proxy-rewrite"] = apisixv1.RewriteConfig{ Headers: headers, } } func (t *translator) generatePluginFromHTTPRequestMirrorFilter(namespace string, plugins apisixv1.Plugins, reqMirror *gatewayv1beta1.HTTPRequestMirrorFilter) { if reqMirror == nil { return } var ( port int = 80 ns string = namespace ) if reqMirror.BackendRef.Port != nil { port = int(*reqMirror.BackendRef.Port) } if reqMirror.BackendRef.Namespace != nil { ns = string(*reqMirror.BackendRef.Namespace) } // TODO 1: Need to support https. // TODO 2: https://github.com/apache/apisix/issues/8351 APISIX 3.0 support {service.namespace} and {service.namespace.svc}, but APISIX <= 2.15 version is not supported. host := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", reqMirror.BackendRef.Name, ns, port) plugins["proxy-mirror"] = apisixv1.RequestMirror{ Host: host, } } func (t *translator) generatePluginFromHTTPRequestRedirectFilter(plugins apisixv1.Plugins, reqRedirect *gatewayv1beta1.HTTPRequestRedirectFilter) { if reqRedirect == nil { return } var uri string code := 302 if reqRedirect.StatusCode != nil { code = *reqRedirect.StatusCode } hostname := "$host" if reqRedirect.Hostname != nil { hostname = string(*reqRedirect.Hostname) } scheme := "$scheme" if reqRedirect.Scheme != nil { scheme = *reqRedirect.Scheme } if reqRedirect.Port != nil { uri = fmt.Sprintf("%s://%s:%d$request_uri", scheme, hostname, int(*reqRedirect.Port)) } else { uri = fmt.Sprintf("%s://%s$request_uri", scheme, hostname) } plugins["redirect"] = apisixv1.RedirectConfig{ RetCode: code, URI: uri, } } func (t *translator) TranslateGatewayHTTPRouteV1beta1(httpRoute *gatewayv1beta1.HTTPRoute) (*translation.TranslateContext, error) { ctx := translation.DefaultEmptyTranslateContext() var hosts []string for _, hostname := range httpRoute.Spec.Hostnames { hosts = append(hosts, string(hostname)) // TODO: See the document of gatewayv1beta1.Listener.Hostname _ = gatewayv1beta1.Listener{}.Hostname // For HTTPRoute and TLSRoute resources, there is an interaction with the // `spec.hostnames` array. When both listener and route specify hostnames, // there MUST be an intersection between the values for a Route to be // accepted. For more information, refer to the Route specific Hostnames // documentation. } rules := httpRoute.Spec.Rules for i, rule := range rules { backends := rule.BackendRefs if len(backends) == 0 { continue } var ruleUpstreams []*apisixv1.Upstream var weightedUpstreams []apisixv1.TrafficSplitConfigRuleWeightedUpstream for j, backend := range backends { // TODO: Support filters // filters := backend.Filters var kind string if backend.Kind == nil { kind = "service" } else { kind = strings.ToLower(string(*backend.Kind)) } if kind != "service" { log.Warnw(fmt.Sprintf("ignore non-service kind at Rules[%v].BackendRefs[%v]", i, j), zap.String("kind", kind), ) continue } var ns string if backend.Namespace == nil { ns = httpRoute.Namespace } else { ns = string(*backend.Namespace) } //if ns != httpRoute.Namespace { // TODO: check gatewayv1beta1.ReferencePolicy //} if backend.Port == nil { log.Warnw(fmt.Sprintf("ignore nil port at Rules[%v].BackendRefs[%v]", i, j), zap.String("kind", kind), ) continue } ups, err := t.KubeTranslator.TranslateService(ns, string(backend.Name), "", int32(*backend.Port)) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("failed to translate Rules[%v].BackendRefs[%v]", i, j)) } ups.Name = apisixv1.ComposeUpstreamName(ns, string(backend.Name), "", int32(*backend.Port), types.ResolveGranularity.Endpoint) // APISIX limits max length of label value // https://github.com/apache/apisix/blob/5b95b85faea3094d5e466ee2d39a52f1f805abbb/apisix/schema_def.lua#L85 ups.Labels["meta_namespace"] = utils.TruncateString(ns, 64) ups.Labels["meta_backend"] = utils.TruncateString(string(backend.Name), 64) ups.Labels["meta_port"] = fmt.Sprintf("%v", int32(*backend.Port)) ups.ID = id.GenID(ups.Name) log.Debugw("translated HTTPRoute upstream", zap.Int("backendRefs_index", j), zap.String("backendRefs_name", string(backend.Name)), zap.String("name", ups.Name), ) ctx.AddUpstream(ups) ruleUpstreams = append(ruleUpstreams, ups) if backend.Weight == nil { weightedUpstreams = append(weightedUpstreams, apisixv1.TrafficSplitConfigRuleWeightedUpstream{ UpstreamID: ups.ID, Weight: 1, // 1 is default value of BackendRef }) } else { weightedUpstreams = append(weightedUpstreams, apisixv1.TrafficSplitConfigRuleWeightedUpstream{ UpstreamID: ups.ID, Weight: int(*backend.Weight), }) } } if len(ruleUpstreams) == 0 { log.Warnw(fmt.Sprintf("ignore all-failed backend refs at Rules[%v]", i), zap.Any("BackendRefs", rule.BackendRefs), ) continue } matches := rule.Matches if len(matches) == 0 { defaultType := gatewayv1beta1.PathMatchPathPrefix defaultValue := "/" matches = []gatewayv1beta1.HTTPRouteMatch{ { Path: &gatewayv1beta1.HTTPPathMatch{ Type: &defaultType, Value: &defaultValue, }, }, } } plugins := t.generatePluginsFromHTTPRouteFilter(httpRoute.Namespace, rule.Filters) for j, match := range matches { route, err := t.translateGatewayHTTPRouteMatch(&match) if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("failed to translate Rules[%v].Matches[%v]", i, j)) } name := apisixv1.ComposeRouteName(httpRoute.Namespace, httpRoute.Name, fmt.Sprintf("%d-%d", i, j)) route.ID = id.GenID(name) route.Hosts = hosts route.Plugins = plugins // Bind Upstream if len(ruleUpstreams) == 1 { route.UpstreamId = ruleUpstreams[0].ID } else if len(ruleUpstreams) > 0 { route.Plugins["traffic-split"] = &apisixv1.TrafficSplitConfig{ Rules: []apisixv1.TrafficSplitConfigRule{ { WeightedUpstreams: weightedUpstreams, }, }, } } ctx.AddRoute(route) } // TODO: Support filters // filters := rule.Filters } return ctx, nil } func (t *translator) translateGatewayHTTPRouteMatch(match *gatewayv1beta1.HTTPRouteMatch) (*apisixv1.Route, error) { route := apisixv1.NewDefaultRoute() if match.Path != nil { switch *match.Path.Type { case gatewayv1beta1.PathMatchExact: route.Uri = *match.Path.Value case gatewayv1beta1.PathMatchPathPrefix: route.Uri = *match.Path.Value + "*" case gatewayv1beta1.PathMatchRegularExpression: var this []apisixv1.StringOrSlice this = append(this, apisixv1.StringOrSlice{ StrVal: "uri", }) this = append(this, apisixv1.StringOrSlice{ StrVal: "~~", }) this = append(this, apisixv1.StringOrSlice{ StrVal: *match.Path.Value, }) route.Vars = append(route.Vars, this) default: return nil, errors.New("unknown path match type " + string(*match.Path.Type)) } } if match.Headers != nil && len(match.Headers) > 0 { for _, header := range match.Headers { name := strings.ToLower(string(header.Name)) name = strings.ReplaceAll(name, "-", "_") var this []apisixv1.StringOrSlice this = append(this, apisixv1.StringOrSlice{ StrVal: "http_" + name, }) switch *header.Type { case gatewayv1beta1.HeaderMatchExact: this = append(this, apisixv1.StringOrSlice{ StrVal: "==", }) case gatewayv1beta1.HeaderMatchRegularExpression: this = append(this, apisixv1.StringOrSlice{ StrVal: "~~", }) default: return nil, errors.New("unknown header match type " + string(*header.Type)) } this = append(this, apisixv1.StringOrSlice{ StrVal: header.Value, }) route.Vars = append(route.Vars, this) } } if match.QueryParams != nil && len(match.QueryParams) > 0 { for _, query := range match.QueryParams { var this []apisixv1.StringOrSlice this = append(this, apisixv1.StringOrSlice{ StrVal: "arg_" + strings.ToLower(fmt.Sprintf("%v", query.Name)), }) switch *query.Type { case gatewayv1beta1.QueryParamMatchExact: this = append(this, apisixv1.StringOrSlice{ StrVal: "==", }) case gatewayv1beta1.QueryParamMatchRegularExpression: this = append(this, apisixv1.StringOrSlice{ StrVal: "~~", }) default: return nil, errors.New("unknown query match type " + string(*query.Type)) } this = append(this, apisixv1.StringOrSlice{ StrVal: query.Value, }) route.Vars = append(route.Vars, this) } } if match.Method != nil { route.Methods = []string{ string(*match.Method), } } return route, nil }