diff --git a/api/dashboard/v1/plugin_types.go b/api/dashboard/v1/plugin_types.go index 5a4e753ed..c9364fb77 100644 --- a/api/dashboard/v1/plugin_types.go +++ b/api/dashboard/v1/plugin_types.go @@ -143,7 +143,8 @@ type WolfRBACConsumerConfig struct { type RewriteConfig struct { RewriteTarget string `json:"uri,omitempty"` RewriteTargetRegex []string `json:"regex_uri,omitempty"` - Headers Headers `json:"headers,omitempty"` + Headers *Headers `json:"headers,omitempty"` + Host string `json:"host,omitempty"` } // ResponseRewriteConfig is the rule config for response-rewrite plugin. @@ -152,7 +153,7 @@ type ResponseRewriteConfig struct { StatusCode int `json:"status_code,omitempty"` Body string `json:"body,omitempty"` BodyBase64 bool `json:"body_base64,omitempty"` - Headers ResponseHeaders `json:"headers,omitempty"` + Headers *ResponseHeaders `json:"headers,omitempty"` LuaRestyExpr []expr.Expr `json:"vars,omitempty"` Filters []map[string]string `json:"filters,omitempty"` } diff --git a/api/dashboard/v1/zz_generated.deepcopy.go b/api/dashboard/v1/zz_generated.deepcopy.go index 895773185..241088840 100644 --- a/api/dashboard/v1/zz_generated.deepcopy.go +++ b/api/dashboard/v1/zz_generated.deepcopy.go @@ -431,7 +431,11 @@ func (in *ResponseHeaders) DeepCopy() *ResponseHeaders { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResponseRewriteConfig) DeepCopyInto(out *ResponseRewriteConfig) { *out = *in - in.Headers.DeepCopyInto(&out.Headers) + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = new(ResponseHeaders) + (*in).DeepCopyInto(*out) + } if in.LuaRestyExpr != nil { in, out := &in.LuaRestyExpr, &out.LuaRestyExpr *out = make([]expr.Expr, len(*in)) @@ -472,7 +476,11 @@ func (in *RewriteConfig) DeepCopyInto(out *RewriteConfig) { *out = make([]string, len(*in)) copy(*out, *in) } - in.Headers.DeepCopyInto(&out.Headers) + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = new(Headers) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RewriteConfig. diff --git a/internal/controlplane/translator/httproute.go b/internal/controlplane/translator/httproute.go index 2cd086f70..9cd51f39b 100644 --- a/internal/controlplane/translator/httproute.go +++ b/internal/controlplane/translator/httproute.go @@ -14,11 +14,7 @@ import ( "github.com/api7/api7-ingress-controller/internal/id" ) -const ( - RequestMirror = "proxy-mirror" -) - -func (t *Translator) fillPluginsFromHTTPRouteFilters(plugins v1.Plugins, namespace string, filters []gatewayv1.HTTPRouteFilter) { +func (t *Translator) fillPluginsFromHTTPRouteFilters(plugins v1.Plugins, namespace string, filters []gatewayv1.HTTPRouteFilter, matches []gatewayv1.HTTPRouteMatch) { for _, filter := range filters { switch filter.Type { case gatewayv1.HTTPRouteFilterRequestHeaderModifier: @@ -28,20 +24,58 @@ func (t *Translator) fillPluginsFromHTTPRouteFilters(plugins v1.Plugins, namespa case gatewayv1.HTTPRouteFilterRequestMirror: t.fillPluginFromHTTPRequestMirrorFilter(namespace, plugins, filter.RequestMirror) case gatewayv1.HTTPRouteFilterURLRewrite: - // TODO: Supported, to be implemented + t.fillPluginFromURLRewriteFilter(plugins, filter.URLRewrite, matches) case gatewayv1.HTTPRouteFilterResponseHeaderModifier: t.fillPluginFromHTTPResponseHeaderFilter(plugins, filter.ResponseHeaderModifier) } } } +func (t *Translator) fillPluginFromURLRewriteFilter(plugins v1.Plugins, urlRewrite *gatewayv1.HTTPURLRewriteFilter, matches []gatewayv1.HTTPRouteMatch) { + pluginName := v1.PluginProxyRewrite + obj := plugins[pluginName] + var plugin *v1.RewriteConfig + if obj == nil { + plugin = &v1.RewriteConfig{} + plugins[pluginName] = plugin + } else { + plugin = obj.(*v1.RewriteConfig) + } + if urlRewrite.Hostname != nil { + plugin.Host = string(*urlRewrite.Hostname) + } + + if urlRewrite.Path != nil { + switch urlRewrite.Path.Type { + case gatewayv1.FullPathHTTPPathModifier: + plugin.RewriteTarget = *urlRewrite.Path.ReplaceFullPath + case gatewayv1.PrefixMatchHTTPPathModifier: + prefixPaths := make([]string, 0, len(matches)) + for _, match := range matches { + if match.Path == nil || match.Path.Type == nil || *match.Path.Type != gatewayv1.PathMatchPathPrefix { + continue + } + prefixPaths = append(prefixPaths, *match.Path.Value) + } + regexPattern := "^(" + strings.Join(prefixPaths, "|") + ")" + "/(.*)" + replaceTarget := *urlRewrite.Path.ReplacePrefixMatch + regexTarget := replaceTarget + "/$2" + + plugin.RewriteTargetRegex = []string{ + regexPattern, + regexTarget, + } + } + } +} + func (t *Translator) fillPluginFromHTTPRequestHeaderFilter(plugins v1.Plugins, reqHeaderModifier *gatewayv1.HTTPHeaderFilter) { pluginName := v1.PluginProxyRewrite obj := plugins[pluginName] var plugin *v1.RewriteConfig if obj == nil { plugin = &v1.RewriteConfig{ - Headers: v1.Headers{ + Headers: &v1.Headers{ Add: make(map[string]string, len(reqHeaderModifier.Add)), Set: make(map[string]string, len(reqHeaderModifier.Set)), Remove: make([]string, 0, len(reqHeaderModifier.Remove)), @@ -72,7 +106,7 @@ func (t *Translator) fillPluginFromHTTPResponseHeaderFilter(plugins v1.Plugins, var plugin *v1.ResponseRewriteConfig if obj == nil { plugin = &v1.ResponseRewriteConfig{ - Headers: v1.ResponseHeaders{ + Headers: &v1.ResponseHeaders{ Add: make([]string, 0, len(respHeaderModifier.Add)), Set: make(map[string]string, len(respHeaderModifier.Set)), Remove: make([]string, 0, len(respHeaderModifier.Remove)), @@ -262,7 +296,7 @@ func (t *Translator) TranslateGatewayHTTPRoute(tctx *TranslateContext, httpRoute service.ID = id.GenID(service.Name) service.Labels = label.GenLabel(httpRoute) service.Hosts = hosts - t.fillPluginsFromHTTPRouteFilters(service.Plugins, httpRoute.GetNamespace(), rule.Filters) + t.fillPluginsFromHTTPRouteFilters(service.Plugins, httpRoute.GetNamespace(), rule.Filters, rule.Matches) result.Services = append(result.Services, service) diff --git a/test/e2e/gatewayapi/httproute.go b/test/e2e/gatewayapi/httproute.go index 01281f01a..7e9653a7a 100644 --- a/test/e2e/gatewayapi/httproute.go +++ b/test/e2e/gatewayapi/httproute.go @@ -458,6 +458,59 @@ spec: statusCode: 301 ` + var replacePrefixMatch = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httpbin +spec: + parentRefs: + - name: api7ee + hostnames: + - httpbin.example + rules: + - matches: + - path: + type: PathPrefix + value: /replace + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /status + backendRefs: + - name: httpbin-service-e2e-test + port: 80 +` + + var replaceFullPathAndHost = ` +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: httpbin +spec: + parentRefs: + - name: api7ee + hostnames: + - httpbin.example + rules: + - matches: + - path: + type: PathPrefix + value: /replace + filters: + - type: URLRewrite + urlRewrite: + hostname: replace.example.org + path: + type: ReplaceFullPath + replaceFullPath: /headers + backendRefs: + - name: httpbin-service-e2e-test + port: 80 +` + BeforeEach(beforeEachHTTP) It("HTTPRoute RequestHeaderModifier", func() { @@ -592,6 +645,44 @@ spec: echoLogs := s.GetDeploymentLogs("echo") Expect(echoLogs).To(ContainSubstring("GET /headers")) }) + + It("HTTPRoute URLRewrite with ReplaceFullPath And Hostname", func() { + By("create HTTPRoute") + ResourceApplied("HTTPRoute", "httpbin", replaceFullPathAndHost, 1) + + By("/replace/201 should be rewritten to /headers") + s.NewAPISIXClient().GET("/replace/201"). + WithHeader("Host", "httpbin.example"). + Expect(). + Status(http.StatusOK). + Body(). + Contains("replace.example.org") + + By("/replace/500 should be rewritten to /headers") + s.NewAPISIXClient().GET("/replace/500"). + WithHeader("Host", "httpbin.example"). + Expect(). + Status(http.StatusOK). + Body(). + Contains("replace.example.org") + }) + + It("HTTPRoute URLRewrite with ReplacePrefixMatch", func() { + By("create HTTPRoute") + ResourceApplied("HTTPRoute", "httpbin", replacePrefixMatch, 1) + + By("/replace/201 should be rewritten to /status/201") + s.NewAPISIXClient().GET("/replace/201"). + WithHeader("Host", "httpbin.example"). + Expect(). + Status(http.StatusCreated) + + By("/replace/500 should be rewritten to /status/500") + s.NewAPISIXClient().GET("/replace/500"). + WithHeader("Host", "httpbin.example"). + Expect(). + Status(http.StatusInternalServerError) + }) }) Context("HTTPRoute Multiple Backend", func() {