Repository: emissary-ingress/emissary

Issue:
Users want support for regex path (prefix) matching 
Envoy supports this; It could be nice that Ambassador expose it.

Working thougth is a `regex_prefix` element in a Mapping, parallel to `prefix`.


Discussion:

Resolving patch:
diff --git a/ambassador/ambassador/mapping.py b/ambassador/ambassador/mapping.py
--- a/ambassador/ambassador/mapping.py
+++ b/ambassador/ambassador/mapping.py
@@ -49,6 +49,7 @@ def route_weight(klass, route):
         "priority": True,
         "timeout_ms": True,
         "use_websocket": True,
+        "prefix_regex": True
     }
 
     def __init__(self, _source="--internal--", _from=None, **kwargs):
diff --git a/end-to-end/stats-test-service/stats-test.py b/end-to-end/stats-test-service/stats-test.py
new file mode 100644
--- /dev/null
+++ b/end-to-end/stats-test-service/stats-test.py
@@ -0,0 +1,59 @@
+import sys
+
+import json
+import socket
+
+UDP_IP = "0.0.0.0"
+UDP_PORT = 8125
+
+sock = socket.socket(socket.AF_INET, # Internet
+                     socket.SOCK_DGRAM) # UDP
+sock.bind((UDP_IP, UDP_PORT))
+
+sys.stdout.write("Listening on %d\n" % UDP_PORT)
+sys.stdout.flush()
+
+interesting = {
+    'envoy.cluster.cluster_qotm.upstream_rq_time': 0,
+    'envoy.cluster.cluster_qotm.upstream_rq_total': 0,
+    'envoy.cluster.cluster_qotm.upstream_rq_2xx': 0
+}
+
+recvd = 0
+
+while True:
+    data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes
+
+    data = data.decode('utf-8').strip()
+    peer_ip, peer_port = addr
+
+    if data == 'DUMP':
+        sock.sendto(json.dumps(interesting).encode("utf-8"), addr)
+    else:
+        recvd += 1
+
+        try:
+            (key, rest) = data.split(':')
+            (value, kind) = rest.split('|')
+
+            if key not in interesting:
+                continue
+            
+            interesting[key] += int(value)
+        except Exception:
+            continue
+    
+        if (recvd % 60) == 0:
+            trq = interesting['envoy.cluster.cluster_qotm.upstream_rq_total']
+            grq = interesting['envoy.cluster.cluster_qotm.upstream_rq_2xx']
+            ttm = interesting['envoy.cluster.cluster_qotm.upstream_rq_time']
+
+            if trq > 0:
+                ttm = ", %.1f ms avg" % (ttm / trq)
+            else:
+                ttm = ""
+
+            sys.stdout.write("Requests: %d (%d good)%s\n" % (trq, grq, ttm))
+            sys.stdout.flush()
+
+


Test patch:
diff --git a/ambassador/tests/008-regex-prefix/config/mapping-regex.yaml b/ambassador/tests/008-regex-prefix/config/mapping-regex.yaml
new file mode 100644
--- /dev/null
+++ b/ambassador/tests/008-regex-prefix/config/mapping-regex.yaml
@@ -0,0 +1,9 @@
+---
+apiVersion: ambassador/v0
+kind:  Mapping
+name:  regex_mapping
+prefix: ^\/http(bin)\/$
+prefix_regex: true
+service: http://httpbin.org
+host_rewrite: httpbin.org
+host: test.datawire.io
\ No newline at end of file
diff --git a/ambassador/tests/008-regex-prefix/gold.intermediate.json b/ambassador/tests/008-regex-prefix/gold.intermediate.json
new file mode 100644
--- /dev/null
+++ b/ambassador/tests/008-regex-prefix/gold.intermediate.json
@@ -0,0 +1,204 @@
+{
+    "envoy_config": {
+        "admin": [
+            {
+                "_source": "--internal--",
+                "admin_port": 8001
+            }
+        ],
+        "clusters": [
+            {
+                "_referenced_by": [
+                    "--internal--"
+                ],
+                "_service": "127.0.0.1:8877",
+                "_source": "--internal--",
+                "lb_type": "round_robin",
+                "name": "cluster_127_0_0_1_8877",
+                "type": "strict_dns",
+                "urls": [
+                    "tcp://127.0.0.1:8877"
+                ]
+            },
+            {
+                "_referenced_by": [
+                    "mapping-regex.yaml.1"
+                ],
+                "_service": "httpbin.org",
+                "_source": "mapping-regex.yaml.1",
+                "lb_type": "round_robin",
+                "name": "cluster_http___httpbin_org",
+                "type": "strict_dns",
+                "urls": [
+                    "tcp://httpbin.org:80"
+                ]
+            }
+        ],
+        "filters": [
+            {
+                "_source": "--internal--",
+                "config": {},
+                "name": "cors"
+            },
+            {
+                "_source": "--internal--",
+                "config": {},
+                "name": "router",
+                "type": "decoder"
+            }
+        ],
+        "listeners": [
+            {
+                "_source": "--internal--",
+                "require_tls": false,
+                "service_port": 80,
+                "use_proxy_proto": false
+            }
+        ],
+        "routes": [
+            {
+                "__saved": [
+                    0,
+                    26,
+                    0,
+                    "/ambassador/v0/check_ready",
+                    "GET"
+                ],
+                "_group_id": "0a42187b7b3d283e0079ddab01825e1c71fad9f6",
+                "_method": "GET",
+                "_precedence": 0,
+                "_referenced_by": [
+                    "--internal--"
+                ],
+                "_source": "--internal--",
+                "clusters": [
+                    {
+                        "name": "cluster_127_0_0_1_8877",
+                        "weight": 100.0
+                    }
+                ],
+                "prefix": "/ambassador/v0/check_ready",
+                "prefix_rewrite": "/ambassador/v0/check_ready"
+            },
+            {
+                "__saved": [
+                    0,
+                    15,
+                    26,
+                    "^\\/http(bin)\\/$",
+                    "GET",
+                    ":authority-test.datawire.io"
+                ],
+                "_group_id": "0e7ced56a4604bdde2e44a278fceca27be993d03",
+                "_method": "GET",
+                "_precedence": 0,
+                "_referenced_by": [
+                    "mapping-regex.yaml.1"
+                ],
+                "_source": "mapping-regex.yaml.1",
+                "clusters": [
+                    {
+                        "name": "cluster_http___httpbin_org",
+                        "weight": 100.0
+                    }
+                ],
+                "headers": [
+                    {
+                        "name": ":authority",
+                        "regex": false,
+                        "value": "test.datawire.io"
+                    }
+                ],
+                "host_rewrite": "httpbin.org",
+                "prefix": "^\\/http(bin)\\/$",
+                "prefix_regex": true,
+                "prefix_rewrite": "/"
+            },
+            {
+                "__saved": [
+                    0,
+                    26,
+                    0,
+                    "/ambassador/v0/check_alive",
+                    "GET"
+                ],
+                "_group_id": "6151893fbc2f1f730a1bb946b2eefac391f0d2ec",
+                "_method": "GET",
+                "_precedence": 0,
+                "_referenced_by": [
+                    "--internal--"
+                ],
+                "_source": "--internal--",
+                "clusters": [
+                    {
+                        "name": "cluster_127_0_0_1_8877",
+                        "weight": 100.0
+                    }
+                ],
+                "prefix": "/ambassador/v0/check_alive",
+                "prefix_rewrite": "/ambassador/v0/check_alive"
+            },
+            {
+                "__saved": [
+                    0,
+                    15,
+                    0,
+                    "/ambassador/v0/",
+                    "GET"
+                ],
+                "_group_id": "b69eeb747b38f5e5fd3955d044ea7797d67d024f",
+                "_method": "GET",
+                "_precedence": 0,
+                "_referenced_by": [
+                    "--internal--"
+                ],
+                "_source": "--internal--",
+                "clusters": [
+                    {
+                        "name": "cluster_127_0_0_1_8877",
+                        "weight": 100.0
+                    }
+                ],
+                "prefix": "/ambassador/v0/",
+                "prefix_rewrite": "/ambassador/v0/"
+            }
+        ]
+    },
+    "source_map": {
+        "--internal--": {
+            "--internal--": true
+        },
+        "mapping-regex.yaml": {
+            "mapping-regex.yaml.1": true
+        }
+    },
+    "sources": {
+        "--diagnostics--": {
+            "_source": "--diagnostics--",
+            "description": "The '--diagnostics--' source marks objects created by Ambassador to assist with diagnostic output.",
+            "filename": "--diagnostics--",
+            "index": 0,
+            "kind": "diagnostics",
+            "name": "Ambassador Diagnostics",
+            "version": "v0"
+        },
+        "--internal--": {
+            "_source": "--internal--",
+            "description": "The '--internal--' source marks objects created by Ambassador's internal logic.",
+            "filename": "--internal--",
+            "index": 0,
+            "kind": "Internal",
+            "name": "Ambassador Internals",
+            "version": "v0"
+        },
+        "mapping-regex.yaml.1": {
+            "_source": "mapping-regex.yaml",
+            "filename": "mapping-regex.yaml",
+            "index": 1,
+            "kind": "Mapping",
+            "name": "regex_mapping",
+            "version": "ambassador/v0",
+            "yaml": "apiVersion: ambassador/v0\nhost: test.datawire.io\nhost_rewrite: httpbin.org\nkind: Mapping\nname: regex_mapping\nprefix: ^\\/http(bin)\\/$\nprefix_regex: true\nservice: http://httpbin.org\n"
+        }
+    }
+}
\ No newline at end of file
diff --git a/ambassador/tests/008-regex-prefix/gold.json b/ambassador/tests/008-regex-prefix/gold.json
new file mode 100644
--- /dev/null
+++ b/ambassador/tests/008-regex-prefix/gold.json
@@ -0,0 +1,128 @@
+{
+  "listeners": [
+    
+    {
+      "address": "tcp://0.0.0.0:80",
+      
+      "filters": [
+        {
+          "type": "read",
+          "name": "http_connection_manager",
+          "config": {"codec_type": "auto",
+            "stat_prefix": "ingress_http",
+            "access_log": [
+              {
+                "format": "ACCESS [%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\"\n",
+                "path": "/dev/fd/1"
+              }
+            ],
+            "route_config": {
+              "virtual_hosts": [
+                {
+                  "name": "backend",
+                  "domains": ["*"],"routes": [
+                    
+                    {
+                      "timeout_ms": 3000,"prefix": "/ambassador/v0/check_ready","prefix_rewrite": "/ambassador/v0/check_ready",
+                      "weighted_clusters": {
+                          "clusters": [
+                              
+                                 { "name": "cluster_127_0_0_1_8877", "weight": 100.0 }
+                              
+                          ]
+                      }
+                      
+                    }
+                    ,
+                    
+                    {
+                      "timeout_ms": 3000,"prefix": "/ambassador/v0/check_alive","prefix_rewrite": "/ambassador/v0/check_alive",
+                      "weighted_clusters": {
+                          "clusters": [
+                              
+                                 { "name": "cluster_127_0_0_1_8877", "weight": 100.0 }
+                              
+                          ]
+                      }
+                      
+                    }
+                    ,
+                    
+                    {
+                      "timeout_ms": 3000,"regex": "^\/http(bin)\/$","prefix_rewrite": "/","host_rewrite": "httpbin.org","headers": [{"name": ":authority", "regex": false, "value": "test.datawire.io"}],
+                      "weighted_clusters": {
+                          "clusters": [
+                              
+                                 { "name": "cluster_http___httpbin_org", "weight": 100.0 }
+                              
+                          ]
+                      }
+                      
+                    }
+                    ,
+                    
+                    {
+                      "timeout_ms": 3000,"prefix": "/ambassador/v0/","prefix_rewrite": "/ambassador/v0/",
+                      "weighted_clusters": {
+                          "clusters": [
+                              
+                                 { "name": "cluster_127_0_0_1_8877", "weight": 100.0 }
+                              
+                          ]
+                      }
+                      
+                    }
+                    
+                    
+                  ]
+                }
+              ]
+            },
+            "filters": [
+              {
+                "name": "cors",
+                "config": {}
+              },{"type": "decoder",
+                "name": "router",
+                "config": {}
+              }
+            ]
+          }
+        }
+      ]
+    }
+  ],
+  "admin": {
+    "address": "tcp://127.0.0.1:8001",
+    "access_log_path": "/tmp/admin_access_log"
+  },
+  "cluster_manager": {
+    "clusters": [
+      {
+        "name": "cluster_127_0_0_1_8877",
+        "connect_timeout_ms": 3000,
+        "type": "strict_dns",
+        "lb_type": "round_robin",        
+        "hosts": [
+          {
+            "url": "tcp://127.0.0.1:8877"
+          }
+          
+        ]},
+      {
+        "name": "cluster_http___httpbin_org",
+        "connect_timeout_ms": 3000,
+        "type": "strict_dns",
+        "lb_type": "round_robin",        
+        "hosts": [
+          {
+            "url": "tcp://httpbin.org:80"
+          }
+          
+        ]}
+      
+    ]
+  },
+  "statsd_udp_ip_address": "127.0.0.1:8125",
+  "stats_flush_interval_ms": 1000
+}
\ No newline at end of file
diff --git a/end-to-end/001-simple/test.sh b/end-to-end/001-simple/test.sh
--- a/end-to-end/001-simple/test.sh
+++ b/end-to-end/001-simple/test.sh
@@ -18,6 +18,7 @@ kubectl cluster-info
 kubectl create cm ambassador-config --from-file k8s/base-config.yaml
 kubectl apply -f k8s/ambassador.yaml
 kubectl apply -f ${ROOT}/ambassador-deployment.yaml
+kubectl apply -f ${ROOT}/stats-test.yaml
 
 set +e +o pipefail
 
@@ -71,4 +72,30 @@ if ! qtest.py $CLUSTER:$APORT test-2.yaml; then
     exit 1
 fi
 
+APOD=$(ambassador_pod)
+kubectl exec "$APOD" -c ambassador -- sh -c 'echo "DUMP" | nc -u -w 1 statsd-sink 8125' > stats.json
+
+rqt=$(jget.py envoy.cluster.cluster_qotm.upstream_rq_total < stats.json)
+
+rc=0
+
+if [ $rqt != 18 ]; then
+    echo "Upstream RQ total mismatch: wanted 18, got $rqt"
+    rc=1
+else
+    echo "Upstream RQ total stat good"
+fi
+
+rq200=$(jget.py envoy.cluster.cluster_qotm.upstream_rq_2xx < stats.json)
+
+if [ $rq200 != 12 ]; then
+    echo "Upstream RQ 200 mismatch: wanted 12, got $rq200"
+    rc=1
+else
+    echo "Upstream RQ 200 stat good"
+fi
+
+exit $rc
+
+
 # kubernaut discard