Add ingress.Rule PathReplacement
This commit is contained in:
parent
870193c064
commit
4d7d56e8f9
|
@ -179,6 +179,7 @@ func ValidateUrl(c *cli.Context, allowURLFromArgs bool) (*url.URL, error) {
|
||||||
type UnvalidatedIngressRule struct {
|
type UnvalidatedIngressRule struct {
|
||||||
Hostname string `json:"hostname,omitempty"`
|
Hostname string `json:"hostname,omitempty"`
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
|
PathReplacement string `yaml:"pathReplacement" json:"pathReplacement,omitempty"`
|
||||||
Service string `json:"service,omitempty"`
|
Service string `json:"service,omitempty"`
|
||||||
OriginRequest OriginRequestConfig `yaml:"originRequest" json:"originRequest"`
|
OriginRequest OriginRequestConfig `yaml:"originRequest" json:"originRequest"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,6 +294,8 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
|
||||||
return Ingress{}, errors.Wrapf(err, "Rule #%d has an invalid regex", i+1)
|
return Ingress{}, errors.Wrapf(err, "Rule #%d has an invalid regex", i+1)
|
||||||
}
|
}
|
||||||
pathRegexp = &Regexp{Regexp: regex}
|
pathRegexp = &Regexp{Regexp: regex}
|
||||||
|
} else if r.PathReplacement != "" {
|
||||||
|
return Ingress{}, fmt.Errorf("rule #%d has a path replacement without a path specified", i+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
rules[i] = Rule{
|
rules[i] = Rule{
|
||||||
|
@ -301,6 +303,7 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
|
||||||
punycodeHostname: punycodeHostname,
|
punycodeHostname: punycodeHostname,
|
||||||
Service: service,
|
Service: service,
|
||||||
Path: pathRegexp,
|
Path: pathRegexp,
|
||||||
|
PathReplacement: r.PathReplacement,
|
||||||
Handlers: handlers,
|
Handlers: handlers,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
|
|
|
@ -442,6 +442,41 @@ ingress:
|
||||||
service: https://localhost:8000
|
service: https://localhost:8000
|
||||||
- hostname: "*"
|
- hostname: "*"
|
||||||
service: https://localhost:8001
|
service: https://localhost:8001
|
||||||
|
`},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Path replacement",
|
||||||
|
args: args{rawYAML: `
|
||||||
|
ingress:
|
||||||
|
- hostname: test.example.com
|
||||||
|
service: https://localhost:8000
|
||||||
|
path: ^/api/(.*)$
|
||||||
|
pathReplacement: /$1
|
||||||
|
- service: http_status:404
|
||||||
|
`},
|
||||||
|
want: []Rule{
|
||||||
|
{
|
||||||
|
Hostname: "test.example.com",
|
||||||
|
Service: &httpService{url: localhost8000},
|
||||||
|
Path: &Regexp{Regexp: regexp.MustCompile("^/api/(.*)$")},
|
||||||
|
PathReplacement: "/$1",
|
||||||
|
Config: defaultConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Service: &fourOhFour,
|
||||||
|
Config: defaultConfig,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Path replacement without a path specified",
|
||||||
|
args: args{rawYAML: `
|
||||||
|
ingress:
|
||||||
|
- hostname: test.example.com
|
||||||
|
service: https://localhost:8000
|
||||||
|
pathReplacement: /$1
|
||||||
|
- service: http_status:404
|
||||||
`},
|
`},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,6 +20,10 @@ type Rule struct {
|
||||||
// Path is an optional regex that can specify path-driven ingress rules.
|
// Path is an optional regex that can specify path-driven ingress rules.
|
||||||
Path *Regexp `json:"path"`
|
Path *Regexp `json:"path"`
|
||||||
|
|
||||||
|
// PathReplacement is an optional regexp replacement string for Path,
|
||||||
|
// that gets sent to the Service
|
||||||
|
PathReplacement string `json:"pathReplacement"`
|
||||||
|
|
||||||
// A (probably local) address. Requests for a hostname which matches this
|
// A (probably local) address. Requests for a hostname which matches this
|
||||||
// rule's hostname pattern will be proxied to the service running on this
|
// rule's hostname pattern will be proxied to the service running on this
|
||||||
// address.
|
// address.
|
||||||
|
|
|
@ -194,25 +194,25 @@ func TestMarshalJSON(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "Nil",
|
name: "Nil",
|
||||||
path: nil,
|
path: nil,
|
||||||
expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
|
expected: `{"hostname":"example.com","path":null,"pathReplacement":"","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nil regex",
|
name: "Nil regex",
|
||||||
path: &Regexp{Regexp: nil},
|
path: &Regexp{Regexp: nil},
|
||||||
expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
|
expected: `{"hostname":"example.com","path":null,"pathReplacement":"","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Empty",
|
name: "Empty",
|
||||||
path: &Regexp{Regexp: regexp.MustCompile("")},
|
path: &Regexp{Regexp: regexp.MustCompile("")},
|
||||||
expected: `{"hostname":"example.com","path":"","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
|
expected: `{"hostname":"example.com","path":"","pathReplacement":"","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Basic",
|
name: "Basic",
|
||||||
path: &Regexp{Regexp: regexp.MustCompile("/echo")},
|
path: &Regexp{Regexp: regexp.MustCompile("/echo")},
|
||||||
expected: `{"hostname":"example.com","path":"/echo","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
|
expected: `{"hostname":"example.com","path":"/echo","pathReplacement":"","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
|
||||||
want: true,
|
want: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,8 @@ func (p *Proxy) ProxyHTTP(
|
||||||
originProxy,
|
originProxy,
|
||||||
isWebsocket,
|
isWebsocket,
|
||||||
rule.Config.DisableChunkedEncoding,
|
rule.Config.DisableChunkedEncoding,
|
||||||
|
rule.Path,
|
||||||
|
rule.PathReplacement,
|
||||||
logFields,
|
logFields,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
rule, srv := ruleField(p.ingressRules, ruleNum)
|
rule, srv := ruleField(p.ingressRules, ruleNum)
|
||||||
|
@ -189,11 +191,18 @@ func (p *Proxy) proxyHTTPRequest(
|
||||||
httpService ingress.HTTPOriginProxy,
|
httpService ingress.HTTPOriginProxy,
|
||||||
isWebsocket bool,
|
isWebsocket bool,
|
||||||
disableChunkedEncoding bool,
|
disableChunkedEncoding bool,
|
||||||
|
pathRegexp *ingress.Regexp,
|
||||||
|
pathReplacement string,
|
||||||
fields logFields,
|
fields logFields,
|
||||||
) error {
|
) error {
|
||||||
roundTripReq := tr.Request
|
roundTripReq := tr.Request
|
||||||
if isWebsocket {
|
if isWebsocket || pathReplacement != "" {
|
||||||
roundTripReq = tr.Clone(tr.Request.Context())
|
roundTripReq = tr.Clone(tr.Request.Context())
|
||||||
|
}
|
||||||
|
if pathReplacement != "" {
|
||||||
|
roundTripReq.URL.Path = pathRegexp.ReplaceAllString(roundTripReq.URL.Path, pathReplacement)
|
||||||
|
}
|
||||||
|
if isWebsocket {
|
||||||
roundTripReq.Header.Set("Connection", "Upgrade")
|
roundTripReq.Header.Set("Connection", "Upgrade")
|
||||||
roundTripReq.Header.Set("Upgrade", "websocket")
|
roundTripReq.Header.Set("Upgrade", "websocket")
|
||||||
roundTripReq.Header.Set("Sec-Websocket-Version", "13")
|
roundTripReq.Header.Set("Sec-Websocket-Version", "13")
|
||||||
|
|
|
@ -46,9 +46,9 @@ func TestUnixSocketOrigin(t *testing.T) {
|
||||||
|
|
||||||
tests := []MultipleIngressTest{
|
tests := []MultipleIngressTest{
|
||||||
{
|
{
|
||||||
url: "http://unix.example.com",
|
url: "http://unix.example.com/created",
|
||||||
expectedStatus: http.StatusCreated,
|
expectedStatus: http.StatusCreated,
|
||||||
expectedBody: []byte("Created"),
|
expectedBody: []byte("/created"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -295,6 +295,18 @@ func TestProxyMultipleOrigins(t *testing.T) {
|
||||||
defer api.Close()
|
defer api.Close()
|
||||||
|
|
||||||
unvalidatedIngress := []config.UnvalidatedIngressRule{
|
unvalidatedIngress := []config.UnvalidatedIngressRule{
|
||||||
|
{
|
||||||
|
Hostname: "api.example.com",
|
||||||
|
Service: api.URL,
|
||||||
|
Path: "^/service1/(.*)$",
|
||||||
|
PathReplacement: "/$1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Hostname: "api.example.com",
|
||||||
|
Service: api.URL,
|
||||||
|
Path: "^/service2/(.*)$",
|
||||||
|
PathReplacement: "/route2/$1",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Hostname: "api.example.com",
|
Hostname: "api.example.com",
|
||||||
Service: api.URL,
|
Service: api.URL,
|
||||||
|
@ -305,7 +317,7 @@ func TestProxyMultipleOrigins(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Hostname: "health.example.com",
|
Hostname: "health.example.com",
|
||||||
Path: "/health",
|
Path: "^/health$",
|
||||||
Service: "http_status:200",
|
Service: "http_status:200",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -316,9 +328,19 @@ func TestProxyMultipleOrigins(t *testing.T) {
|
||||||
|
|
||||||
tests := []MultipleIngressTest{
|
tests := []MultipleIngressTest{
|
||||||
{
|
{
|
||||||
url: "http://api.example.com",
|
url: fmt.Sprintf("http://api.example.com/service1%s", hello.HealthRoute),
|
||||||
expectedStatus: http.StatusCreated,
|
expectedStatus: http.StatusCreated,
|
||||||
expectedBody: []byte("Created"),
|
expectedBody: []byte(hello.HealthRoute),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: fmt.Sprintf("http://api.example.com/service2%s", hello.HealthRoute),
|
||||||
|
expectedStatus: http.StatusCreated,
|
||||||
|
expectedBody: []byte(fmt.Sprintf("/route2%s", hello.HealthRoute)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "http://api.example.com/created",
|
||||||
|
expectedStatus: http.StatusCreated,
|
||||||
|
expectedBody: []byte("/created"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: fmt.Sprintf("http://hello.example.com%s", hello.HealthRoute),
|
url: fmt.Sprintf("http://hello.example.com%s", hello.HealthRoute),
|
||||||
|
@ -384,7 +406,7 @@ type mockAPI struct{}
|
||||||
|
|
||||||
func (ma mockAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (ma mockAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
_, _ = w.Write([]byte("Created"))
|
_, _ = w.Write([]byte(r.URL.Path))
|
||||||
}
|
}
|
||||||
|
|
||||||
type errorOriginTransport struct{}
|
type errorOriginTransport struct{}
|
||||||
|
|
Loading…
Reference in New Issue