Repository: heptio/contour (go)

Issue:
TLS Passthrough & HTTP Redirect
I have a pod that accepts only HTTPS traffic on port 443. To route the traffic, I used an IngressRoute with the new TLS Passthrough option:

```yml
apiVersion: contour.heptio.com/v1beta1
kind: IngressRoute
metadata:
  name: test
spec:
  virtualhost:
    fqdn: test.example.com
    tls:
      passthrough: true
  tcpproxy:
    services:
      - name: test
        port: 443
  routes:
    - match: /
      services:
        - name: test
          port: 443
```

The TLS forwarding works great. However, contour is not accepting traffic on port 80 and therefore does not auto-redirect the request to the HTTPS port. Am I missing some configuration?

**Environment:**

- Contour version: 0.9.0
- Kubernetes version: 1.11 (kubectl: 1.13)
- Cloud provider or hardware configuration: GKE


Pull request description:
internal/dag: permit combining non tls routing with tcp proxy
Fixes #910

The use case is for a service offering tls passthrough on port 443, the
same service may wish to offer a non tls port which informs users they
need to connect over port 443. Presumably port 443 is not HTTPS but some
other protocol, the port 80 fallback is a convenience for people typing
the address into their browsers.

Signed-off-by: Dave Cheney <dave@cheney.net>

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 78faf102..beaba5d8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -128,7 +128,7 @@ warbed.
 Before a change is submitted it should pass all the pre commit CI jobs.
 If there are unrelated test failures the change can be merged so long as a reference to an issue that tracks the test failures is provided.
 
-Once a change lands in master it will be built and available at this tag, `gcr.io/heptio-images/contour:master`.
+Once a change lands in master it will be built and available at this tag, `projectcontour/contour:master`.
 You can read more about the available contour images in the [tagging][7] document.
 
 ### Build an image
diff --git a/apis/projectcontour/v1alpha1/httploadbalancer.go b/apis/projectcontour/v1alpha1/httploadbalancer.go
index a0cd2544..933650a8 100644
--- a/apis/projectcontour/v1alpha1/httploadbalancer.go
+++ b/apis/projectcontour/v1alpha1/httploadbalancer.go
@@ -79,7 +79,7 @@ type TLS struct {
 // Route contains the set of routes for a virtual host
 type Route struct {
 	// Condition defines additional routing parameters on the route
-	Condition `json:"condition,omitempty"`
+	Condition *Condition `json:"condition,omitempty"`
 	// Services are the services to proxy traffic
 	Services []Service `json:"services,omitempty"`
 	// Enables websocket support for the route
diff --git a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go
index 72eec2b2..a2827f96 100644
--- a/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/projectcontour/v1alpha1/zz_generated.deepcopy.go
@@ -244,7 +244,11 @@ func (in *RetryPolicy) DeepCopy() *RetryPolicy {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Route) DeepCopyInto(out *Route) {
 	*out = *in
-	in.Condition.DeepCopyInto(&out.Condition)
+	if in.Condition != nil {
+		in, out := &in.Condition, &out.Condition
+		*out = new(Condition)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Services != nil {
 		in, out := &in.Services, &out.Services
 		*out = make([]Service, len(*in))
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 00000000..a5bc8950
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,33 @@
+# Configuration File
+
+A configuration file can be passed to the `contour serve` command which specified additional properties that Contour should use when starting up.
+This file is passed to Contour via a ConfigMap which is mounted as a volume to the Contour pod.
+
+Following is an example ConfigMap with configuration file included: 
+
+```
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: contour
+  namespace: heptio-contour
+data:
+  contour.yaml: |
+    # should contour expect to be running inside a k8s cluster
+    # incluster: true
+    #
+    # path to kubeconfig (if not running inside a k8s cluster)
+    # kubeconfig: /path/to/.kube/config
+    #
+    # disable ingressroute permitInsecure field
+    # disablePermitInsecure: false
+    tls:
+      # minimum TLS version that Contour will negotiate
+      # minimumProtocolVersion: "1.1"
+    # The following config shows the defaults for the leader election.
+    # leaderelection:
+      # configmap-name: contour
+      # configmap-namespace: leader-elect
+```
+
+_Note:_ The default example `contour` includes this [file](`../examples/contour/01-contour-config.yaml`) for easy deployment of Contour.
\ No newline at end of file
diff --git a/docs/proxy-proto.md b/docs/proxy-proto.md
index 557b7458..81c33f59 100644
--- a/docs/proxy-proto.md
+++ b/docs/proxy-proto.md
@@ -33,7 +33,7 @@ spec:
 ...
 spec:
   containers:
-  - image: gcr.io/heptio-images/contour:master
+  - image: projectcontour/contour:master
     imagePullPolicy: Always
     name: contour
     command: ["contour"]
diff --git a/docs/tagging.md b/docs/tagging.md
index 4ca11623..72d86ff3 100644
--- a/docs/tagging.md
+++ b/docs/tagging.md
@@ -16,6 +16,6 @@ The `latest` tag follows the most recently released version of Contour.
 
 ## Development
 
-`gcr.io/heptio-images/contour:master`
+`projectcontour/contour:master`
 
 The `master` tag follows the latest commit to land on the `master` branch.
diff --git a/examples/common/job-certgen.yaml b/examples/common/job-certgen.yaml
index 6cbd5ffc..07aebb2b 100644
--- a/examples/common/job-certgen.yaml
+++ b/examples/common/job-certgen.yaml
@@ -50,7 +50,7 @@ spec:
     spec:
       containers:
       - name: contour
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         command:
         - contour
         - certgen
diff --git a/examples/contour/03-contour.yaml b/examples/contour/03-contour.yaml
index 2507b40b..38a5cb84 100644
--- a/examples/contour/03-contour.yaml
+++ b/examples/contour/03-contour.yaml
@@ -42,7 +42,7 @@ spec:
         - --contour-key-file=/certs/tls.key
         - --config-path=/config/contour.yaml
         command: ["contour"]
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         imagePullPolicy: Always
         name: contour
         ports:
diff --git a/examples/contour/03-envoy.yaml b/examples/contour/03-envoy.yaml
index e5d376f1..36920696 100644
--- a/examples/contour/03-envoy.yaml
+++ b/examples/contour/03-envoy.yaml
@@ -89,7 +89,7 @@ spec:
         - --envoy-key-file=/certs/tls.key
         command:
         - contour
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         imagePullPolicy: Always
         name: envoy-initconfig
         volumeMounts:
diff --git a/examples/deployment-grpc-v2/02-contour.yaml b/examples/deployment-grpc-v2/02-contour.yaml
index 140e0049..b4d870a3 100644
--- a/examples/deployment-grpc-v2/02-contour.yaml
+++ b/examples/deployment-grpc-v2/02-contour.yaml
@@ -20,7 +20,7 @@ spec:
         prometheus.io/path: "/stats/prometheus"
     spec:
       containers:
-      - image: gcr.io/heptio-images/contour:master
+      - image: projectcontour/contour:master
         imagePullPolicy: Always
         name: contour
         command: ["contour"]
@@ -88,7 +88,7 @@ spec:
               - "POST /healthcheck/fail HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
               - '>/dev/tcp/localhost/9001'
       initContainers:
-      - image: gcr.io/heptio-images/contour:master
+      - image: projectcontour/contour:master
         imagePullPolicy: Always
         name: envoy-initconfig
         command: ["contour"]
diff --git a/examples/ds-grpc-v2/02-contour.yaml b/examples/ds-grpc-v2/02-contour.yaml
index 4e298c5f..f210ebb8 100644
--- a/examples/ds-grpc-v2/02-contour.yaml
+++ b/examples/ds-grpc-v2/02-contour.yaml
@@ -21,7 +21,7 @@ spec:
         prometheus.io/path: "/stats/prometheus"
     spec:
       containers:
-      - image: gcr.io/heptio-images/contour:master
+      - image: projectcontour/contour:master
         imagePullPolicy: Always
         name: contour
         command: ["contour"]
@@ -89,7 +89,7 @@ spec:
               - "POST /healthcheck/fail HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
               - '>/dev/tcp/localhost/9001'
       initContainers:
-      - image: gcr.io/heptio-images/contour:master
+      - image: projectcontour/contour:master
         imagePullPolicy: Always
         name: envoy-initconfig
         command: ["contour"]
diff --git a/examples/ds-hostnet-split-httploadbalancer/02-job-certgen.yaml b/examples/ds-hostnet-split-httploadbalancer/02-job-certgen.yaml
index 8aefa0f1..b4ff295e 100644
--- a/examples/ds-hostnet-split-httploadbalancer/02-job-certgen.yaml
+++ b/examples/ds-hostnet-split-httploadbalancer/02-job-certgen.yaml
@@ -51,7 +51,7 @@ spec:
     spec:
       containers:
       - name: contour
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         command:
         - contour
         - certgen
diff --git a/examples/ds-hostnet-split-httploadbalancer/03-contour.yaml b/examples/ds-hostnet-split-httploadbalancer/03-contour.yaml
index 0e11e003..ce55c8ed 100644
--- a/examples/ds-hostnet-split-httploadbalancer/03-contour.yaml
+++ b/examples/ds-hostnet-split-httploadbalancer/03-contour.yaml
@@ -43,7 +43,7 @@ spec:
         - --contour-key-file=/certs/tls.key
         - --config-path=/config/contour.yaml
         command: ["contour"]
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         imagePullPolicy: Always
         name: contour
         ports:
diff --git a/examples/ds-hostnet-split-httploadbalancer/03-envoy.yaml b/examples/ds-hostnet-split-httploadbalancer/03-envoy.yaml
index 23958ed2..a1fed256 100644
--- a/examples/ds-hostnet-split-httploadbalancer/03-envoy.yaml
+++ b/examples/ds-hostnet-split-httploadbalancer/03-envoy.yaml
@@ -91,7 +91,7 @@ spec:
         - --envoy-key-file=/certs/tls.key
         command:
         - contour
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         imagePullPolicy: Always
         name: envoy-initconfig
         volumeMounts:
diff --git a/examples/ds-hostnet-split/02-job-certgen.yaml b/examples/ds-hostnet-split/02-job-certgen.yaml
index 8aefa0f1..b4ff295e 100644
--- a/examples/ds-hostnet-split/02-job-certgen.yaml
+++ b/examples/ds-hostnet-split/02-job-certgen.yaml
@@ -51,7 +51,7 @@ spec:
     spec:
       containers:
       - name: contour
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         command:
         - contour
         - certgen
diff --git a/examples/ds-hostnet-split/03-contour.yaml b/examples/ds-hostnet-split/03-contour.yaml
index 0e11e003..ce55c8ed 100644
--- a/examples/ds-hostnet-split/03-contour.yaml
+++ b/examples/ds-hostnet-split/03-contour.yaml
@@ -43,7 +43,7 @@ spec:
         - --contour-key-file=/certs/tls.key
         - --config-path=/config/contour.yaml
         command: ["contour"]
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         imagePullPolicy: Always
         name: contour
         ports:
diff --git a/examples/ds-hostnet-split/03-envoy.yaml b/examples/ds-hostnet-split/03-envoy.yaml
index 23958ed2..a1fed256 100644
--- a/examples/ds-hostnet-split/03-envoy.yaml
+++ b/examples/ds-hostnet-split/03-envoy.yaml
@@ -91,7 +91,7 @@ spec:
         - --envoy-key-file=/certs/tls.key
         command:
         - contour
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         imagePullPolicy: Always
         name: envoy-initconfig
         volumeMounts:
diff --git a/examples/ds-hostnet/02-contour.yaml b/examples/ds-hostnet/02-contour.yaml
index 18ad3550..b67c1ac2 100644
--- a/examples/ds-hostnet/02-contour.yaml
+++ b/examples/ds-hostnet/02-contour.yaml
@@ -22,7 +22,7 @@ spec:
     spec:
       hostNetwork: true
       containers:
-      - image: gcr.io/heptio-images/contour:master
+      - image: projectcontour/contour:master
         imagePullPolicy: Always
         ports:
         - containerPort: 8000
@@ -93,7 +93,7 @@ spec:
               - "POST /healthcheck/fail HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
               - '>/dev/tcp/localhost/9001'
       initContainers:
-      - image: gcr.io/heptio-images/contour:master
+      - image: projectcontour/contour:master
         imagePullPolicy: Always
         name: envoy-initconfig
         command: ["contour"]
diff --git a/examples/render/contour.yaml b/examples/render/contour.yaml
index 265e787c..f083464e 100644
--- a/examples/render/contour.yaml
+++ b/examples/render/contour.yaml
@@ -399,7 +399,7 @@ spec:
     spec:
       containers:
       - name: contour
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         command:
         - contour
         - certgen
@@ -605,7 +605,7 @@ spec:
         - --contour-key-file=/certs/tls.key
         - --config-path=/config/contour.yaml
         command: ["contour"]
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         imagePullPolicy: Always
         name: contour
         ports:
@@ -748,7 +748,7 @@ spec:
         - --envoy-key-file=/certs/tls.key
         command:
         - contour
-        image: gcr.io/heptio-images/contour:master
+        image: projectcontour/contour:master
         imagePullPolicy: Always
         name: envoy-initconfig
         volumeMounts:
diff --git a/examples/render/daemonset-rbac.yaml b/examples/render/daemonset-rbac.yaml
index ef43cfb1..0fa2b2f8 100644
--- a/examples/render/daemonset-rbac.yaml
+++ b/examples/render/daemonset-rbac.yaml
@@ -370,7 +370,7 @@ spec:
         prometheus.io/path: "/stats/prometheus"
     spec:
       containers:
-      - image: gcr.io/heptio-images/contour:master
+      - image: projectcontour/contour:master
         imagePullPolicy: Always
         name: contour
         command: ["contour"]
@@ -438,7 +438,7 @@ spec:
               - "POST /healthcheck/fail HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
               - '>/dev/tcp/localhost/9001'
       initContainers:
-      - image: gcr.io/heptio-images/contour:master
+      - image: projectcontour/contour:master
         imagePullPolicy: Always
         name: envoy-initconfig
         command: ["contour"]
diff --git a/examples/render/deployment-rbac.yaml b/examples/render/deployment-rbac.yaml
index 9a069dda..7ac093e6 100644
--- a/examples/render/deployment-rbac.yaml
+++ b/examples/render/deployment-rbac.yaml
@@ -369,7 +369,7 @@ spec:
         prometheus.io/path: "/stats/prometheus"
     spec:
       containers:
-      - image: gcr.io/heptio-images/contour:master
+      - image: projectcontour/contour:master
         imagePullPolicy: Always
         name: contour
         command: ["contour"]
@@ -437,7 +437,7 @@ spec:
               - "POST /healthcheck/fail HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
               - '>/dev/tcp/localhost/9001'
       initContainers:
-      - image: gcr.io/heptio-images/contour:master
+      - image: projectcontour/contour:master
         imagePullPolicy: Always
         name: envoy-initconfig
         command: ["contour"]
diff --git a/internal/dag/builder.go b/internal/dag/builder.go
index d7ba0c7a..6481e81f 100644
--- a/internal/dag/builder.go
+++ b/internal/dag/builder.go
@@ -390,13 +390,10 @@ func (b *Builder) computeIngressRoutes() {
 		}
 
 		enforceTLS, passthrough := b.configureSecureVirtualHost(ir)
-
-		switch {
-		case ir.Spec.TCPProxy != nil && (passthrough || enforceTLS):
+		if ir.Spec.TCPProxy != nil && (passthrough || enforceTLS) {
 			b.processTCPProxy(ir, nil, host)
-		case ir.Spec.Routes != nil:
-			b.processIngressRoutes(ir, "", nil, host, enforceTLS)
 		}
+		b.processIngressRoutes(ir, "", nil, host, ir.Spec.TCPProxy == nil && enforceTLS)
 	}
 }
 
@@ -475,11 +472,24 @@ func (b *Builder) computeHTTPLoadBalancers() {
 			}
 		}
 
+		// Loop over and process all includes
+		for _, include := range httplb.Spec.Includes {
+			if dest, ok := b.Source.httploadbalancers[Meta{name: include.Name, namespace: include.Namespace}]; ok {
+				switch {
+				//	case ir.Spec.TCPProxy != nil && (passthrough || enforceTLS):
+				//		b.processTCPProxy(ir, nil, host)
+				case httplb.Spec.Routes != nil:
+					b.processRoutes(dest, host, &include.Condition, enforceTLS)
+				}
+			}
+		}
+
+		// Process any routes
 		switch {
 		//	case ir.Spec.TCPProxy != nil && (passthrough || enforceTLS):
 		//		b.processTCPProxy(ir, nil, host)
 		case httplb.Spec.Routes != nil:
-			b.processRoutes(httplb, host, enforceTLS) // TODO (sas) Add back `condition` & `visited []*projcontour.HTTPLoadBalancer` as arg to processRoutes
+			b.processRoutes(httplb, host, nil, enforceTLS)
 		}
 	}
 }
@@ -604,11 +614,12 @@ func (b *Builder) processIngressRoutes(ir *ingressroutev1.IngressRoute, prefixMa
 				return
 			}
 
+			permitInsecure := route.PermitInsecure && !b.DisablePermitInsecure
 			r := &PrefixRoute{
 				Prefix: route.Match,
 				Route: Route{
 					Websocket:     route.EnableWebsockets,
-					HTTPSUpgrade:  routeEnforceTLS(enforceTLS, route.PermitInsecure && !b.DisablePermitInsecure),
+					HTTPSUpgrade:  routeEnforceTLS(enforceTLS, permitInsecure),
 					PrefixRewrite: route.PrefixRewrite,
 					TimeoutPolicy: timeoutPolicy(route.TimeoutPolicy),
 					RetryPolicy:   retryPolicy(route.RetryPolicy),
@@ -650,7 +661,9 @@ func (b *Builder) processIngressRoutes(ir *ingressroutev1.IngressRoute, prefixMa
 			}
 
 			b.lookupVirtualHost(host).addRoute(r)
-			b.lookupSecureVirtualHost(host).addRoute(r)
+			if enforceTLS {
+				b.lookupSecureVirtualHost(host).addRoute(r)
+			}
 			continue
 		}
 
@@ -697,19 +710,17 @@ func (b *Builder) processIngressRoutes(ir *ingressroutev1.IngressRoute, prefixMa
 	b.setStatus(Status{Object: ir, Status: StatusValid, Description: "valid IngressRoute", Vhost: host})
 }
 
-func (b *Builder) processRoutes(httplb *projcontour.HTTPLoadBalancer, host string, enforceTLS bool) {
-	// visited = append(visited, httplb) //TODO (sas) Implement delegation
-
+func (b *Builder) processRoutes(httplb *projcontour.HTTPLoadBalancer, host string, condition *projcontour.Condition, enforceTLS bool) {
 	for _, route := range httplb.Spec.Routes {
 		// Cannot support multiple services with websockets (See: https://github.com/heptio/contour/issues/732)
 		if len(route.Services) > 1 && route.EnableWebsockets {
-			b.setStatus(Status{Object: httplb, Status: StatusInvalid, Description: fmt.Sprintf("route %q: cannot specify multiple services and enable websockets", conditionPath(route.Condition)), Vhost: host})
+			b.setStatus(Status{Object: httplb, Status: StatusInvalid, Description: fmt.Sprintf("route %q: cannot specify multiple services and enable websockets", conditionPath(route.Condition, condition)), Vhost: host})
 			return
 		}
 
 		// base case: The route points to services, so we add them to the vhost
 		if len(route.Services) > 0 {
-			routePath := conditionPath(route.Condition)
+			routePath := conditionPath(route.Condition, condition)
 
 			r := &PrefixRoute{
 				Prefix: routePath,
@@ -743,7 +754,7 @@ func (b *Builder) processRoutes(httplb *projcontour.HTTPLoadBalancer, host strin
 				var err error
 				if s.Protocol == "tls" {
 					// we can only validate TLS connections to services that talk TLS
-					uv, err = b.lookupUpstreamValidation(route.Condition.Prefix, service.Name, service.UpstreamValidation, httplb.Namespace)
+					uv, err = b.lookupUpstreamValidation(routePath, service.Name, service.UpstreamValidation, httplb.Namespace)
 					if err != nil {
 						b.setStatus(Status{Object: httplb, Status: StatusInvalid, Description: err.Error(), Vhost: host})
 					}
@@ -761,41 +772,6 @@ func (b *Builder) processRoutes(httplb *projcontour.HTTPLoadBalancer, host strin
 			b.lookupSecureVirtualHost(host).addRoute(r)
 			continue
 		}
-
-		//if len(route.Includes) == 0 {
-		//	// not a delegate route
-		//	continue
-		//}
-
-		//for _, inc := range route.Includes {
-		//	namespace := inc.Namespace
-		//	if namespace == "" {
-		//		// we are delegating to another IngressRoute in the same namespace
-		//		namespace = httplb.Namespace
-		//	}
-		//
-		//	if dest, ok := b.Source.httploadbalancers[Meta{name: inc.Name, namespace: namespace}]; ok {
-		//		// dest is not an orphaned ingress route, as there is an HTTPLoadBalancer that points to it
-		//		delete(b.orphaned, Meta{name: dest.Name, namespace: dest.Namespace})
-		//
-		//		// ensure we are not following an edge that produces a cycle
-		//		var path []string
-		//		for _, vir := range visited {
-		//			path = append(path, fmt.Sprintf("%s/%s", vir.Namespace, vir.Name))
-		//		}
-		//		for _, vir := range visited {
-		//			if dest.Name == vir.Name && dest.Namespace == vir.Namespace {
-		//				path = append(path, fmt.Sprintf("%s/%s", dest.Namespace, dest.Name))
-		//				description := fmt.Sprintf("route creates a delegation cycle: %s", strings.Join(path, " -> "))
-		//				b.setStatus(Status{Object: httplb, Status: StatusInvalid, Description: description, Vhost: host})
-		//				return
-		//			}
-		//		}
-		//
-		//		// follow the link and process the target ingress route
-		//		b.processRoutes(dest, &inc.Condition, visited, host, enforceTLS)
-		//	}
-		//}
 	}
 
 	b.setStatus(Status{Object: httplb, Status: StatusValid, Description: "valid HTTPLoadBalancer", Vhost: host})
@@ -889,11 +865,21 @@ func (b *Builder) processTCPProxy(ir *ingressroutev1.IngressRoute, visited []*in
 	b.setStatus(Status{Object: ir, Status: StatusValid, Description: "valid IngressRoute", Vhost: host})
 }
 
-func conditionPath(c projcontour.Condition) string {
-	if c.Prefix == "" {
+func conditionPath(routeCondition, includeCondition *projcontour.Condition) string {
+	pathPrefix := ""
+
+	if includeCondition != nil {
+		pathPrefix = includeCondition.Prefix
+	}
+	if routeCondition != nil {
+		pathPrefix += routeCondition.Prefix
+	}
+
+	if pathPrefix == "" {
 		return "/"
 	}
-	return c.Prefix
+
+	return pathPrefix
 }
 
 func externalName(svc *v1.Service) string {
diff --git a/site/_posts/2019-09-5-contour-v015.md b/site/_posts/2019-09-5-contour-v015.md
new file mode 100644
index 00000000..ca3e5b14
--- /dev/null
+++ b/site/_posts/2019-09-5-contour-v015.md
@@ -0,0 +1,96 @@
+---
+title: Leader Election and Configuration Files with Contour v0.15
+image: /img/posts/leader-election.png
+excerpt: This blog post covers key features of the Contour v0.15 release including leader election and the Contour configuration file.
+author_name: Steve Sloka
+author_avatar: /img/contributors/steve-sloka.png
+categories: [kubernetes]
+# Tag should match author to drive author pages
+tags: ['Contour Team', 'Steve Sloka', 'release']
+---
+
+In the previous release of Contour, a split deployment model was improved to secure communication between Envoy and Contour. Now, with our latest release, Contour v0.15, leader election is available to ensure that all instances of Envoy take their configuration from a single Contour instance.
+
+![img](/img/posts/leader-election.png)
+*Overview of leader election.*
+
+## Leader Election
+
+Each instance of Contour configures a connection to the Kubernetes API server in order to watch for changes to objects in the cluster. Contour is concerned with Services, Endpoints, Secrets, Ingress, and IngressRoute objects. Having multiple readers to the Kubernetes API is fine (and is implemented in many different components); however, since Contour updates the status of an IngressRoute object, multiple writers (that is, multiple instances of Contour) can cause issues when each one of them attempts to update the status. Additionally, it’s possible that each instance of Contour processes events from Kubernetes at a different time, causing different configurations to be passed to Envoy.
+
+In leader election mode, only one Contour pod in a deployment, the leader, will open its gRPC endpoint to serve requests from Envoy. All other Contour instances will continue to watch the API server but will not serve gRPC requests. Leader election can be used to ensure that all instances of Envoy take their configuration from a single Contour instance.
+
+Leader election is currently opt in. In future versions of Contour, we plan to make leader election mode the default.
+
+For more information, please consult the [documentation on upgrading](https://github.com/heptio/contour/blob/v0.15.0/docs/upgrading.md#enabling-leader-election).
+
+## Contour Configuration File
+
+Contour has previously supported configuration options to be passed via command-line arguments to the Contour process. Changes to these parameters meant updating the deployment spec. 
+
+Here’s an example spec from a Kubernetes Deployment manifest:
+
+```yaml
+containers:
+ - args:
+   - serve
+   - --incluster
+   - --enable-leader-election
+   - --xds-address=0.0.0.0
+   - --xds-port=8001
+```
+
+Now with v0.15, a configuration file can specify configurations that apply to each Contour installation. However, per-Ingress or per-Route configuration continues to be drawn from the objects and CRDs in the Kubernetes API server.
+
+Sample configuration file:
+
+```
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: contour
+  namespace: heptio-contour
+data:
+  contour.yaml: |
+    # should contour expect to be running inside a k8s cluster
+    # incluster: true
+    #
+    # path to kubeconfig (if not running inside a k8s cluster)
+    # kubeconfig: /path/to/.kube/config
+    #
+    # disable ingressroute permitInsecure field
+    # disablePermitInsecure: false
+    tls:
+      # minimum TLS version that Contour will negotiate
+      # minimumProtocolVersion: "1.1"
+    # The following config shows the defaults for the leader election.
+    # leaderelection:
+      # configmap-name: contour
+      # configmap-namespace: leader-elect
+```
+
+## More new features in Contour v0.15
+
+Version 0.15 includes several fixes. It patches several CVEs related to HTTP/2 by upgrading Envoy to v1.11.1. To help with the number and frequency of configuration updates sent to Envoy, Contour now ignores unrelated Secrets and Services that are not referenced by an active Ingress or IngressRoute object.
+
+We recommend reading the full release notes for [Contour v0.15](https://github.com/heptio/contour/releases/tag/v0.15.0) as well as digging into the [upgrade guide](https://github.com/heptio/contour/blob/v0.15.0/docs/upgrading.md), which outlines some key changes to be aware of when moving from v0.14 to v0.15.
+
+## Future Plans
+
+The Contour project is very community driven and the team would love to hear your feedback! Many features (including IngressRoute) were driven by users who needed a better way to solve their problems. We’re working hard to add features to Contour, especially in expanding how we approach routing. 
+
+If you are interested in contributing, a great place to start is to comment on one of the issues labeled with [Help Wanted](https://github.com/heptio/contour/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) and work with the team on how to resolve them. 
+
+## Join the Contour Community!
+
+Please reach out in one of the following ways and let us know how you are using Contour, if you run into a problem, or want to do more:
+
+- Get updates on Twitter [@projectcontour](https://twitter.com/projectcontour)
+- Chat with us in [#contour on the Kubernetes Slack](https://kubernetes.slack.com/messages/contour)
+- Collaborate with us on [GitHub](https://github.com/heptio/contour)
+
+We’re immensely grateful for all the community contributions that help make Contour even better! For version v0.15, special thanks go out to:
+
+- [@DylanGraham](https://github.com/DylanGraham)
+- [@so0k](https://github.com/so0k)
+- [@mattalberts](https://github.com/mattalberts)


Test patch:
diff --git a/internal/dag/builder_test.go b/internal/dag/builder_test.go
index 99fd9305..5b9fd105 100644
--- a/internal/dag/builder_test.go
+++ b/internal/dag/builder_test.go
@@ -719,13 +719,6 @@ func TestDAGInsert(t *testing.T) {
 					SecretName: sec1.Name,
 				},
 			},
-			Routes: []ingressroutev1.Route{{
-				Match: "/",
-				Services: []ingressroutev1.Service{{
-					Name: "kuard",
-					Port: 8080,
-				}},
-			}},
 			TCPProxy: &ingressroutev1.TCPProxy{
 				Services: []ingressroutev1.Service{{
 					Name: "kuard",
@@ -1315,7 +1308,6 @@ func TestDAGInsert(t *testing.T) {
 			}},
 		},
 	}
-
 	s5 := &v1.Service{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "blog-admin",
@@ -1489,6 +1481,57 @@ func TestDAGInsert(t *testing.T) {
 		},
 	}
 
+	s10 := &v1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "tls-passthrough",
+			Namespace: "default",
+		},
+		Spec: v1.ServiceSpec{
+			Ports: []v1.ServicePort{{
+				Name:       "https",
+				Protocol:   "TCP",
+				Port:       443,
+				TargetPort: intstr.FromInt(443),
+			}, {
+				Name:       "http",
+				Protocol:   "TCP",
+				Port:       80,
+				TargetPort: intstr.FromInt(80),
+			}},
+		},
+	}
+
+	// ir18 tcp forwards traffic to by TLS pass-throughing
+	// it. It also exposes non HTTP traffic to the the non secure port of the
+	// application so it can give an informational message
+	ir18 := &ingressroutev1.IngressRoute{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "kuard-tcp",
+			Namespace: s10.Namespace,
+		},
+		Spec: ingressroutev1.IngressRouteSpec{
+			VirtualHost: &projcontour.VirtualHost{
+				Fqdn: "kuard.example.com",
+				TLS: &projcontour.TLS{
+					Passthrough: true,
+				},
+			},
+			Routes: []ingressroutev1.Route{{
+				Match: "/",
+				Services: []ingressroutev1.Service{{
+					Name: s10.Name,
+					Port: 80, // proxy non secure traffic to port 80
+				}},
+			}},
+			TCPProxy: &ingressroutev1.TCPProxy{
+				Services: []ingressroutev1.Service{{
+					Name: s10.Name,
+					Port: 443, // ssl passthrough to secure port
+				}},
+			},
+		},
+	}
+
 	httlb1 := &projcontour.HTTPLoadBalancer{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "example-com",
@@ -1517,7 +1560,7 @@ func TestDAGInsert(t *testing.T) {
 				Fqdn: "example.com",
 			},
 			Routes: []projcontour.Route{{
-				Condition: projcontour.Condition{
+				Condition: &projcontour.Condition{
 					Prefix: "/",
 				},
 				Services: []projcontour.Service{{
@@ -1553,6 +1596,28 @@ func TestDAGInsert(t *testing.T) {
 		},
 	}
 
+	httplb17 := &projcontour.HTTPLoadBalancer{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "example-com",
+			Namespace: "default",
+		},
+		Spec: projcontour.HTTPLoadBalancerSpec{
+			VirtualHost: &projcontour.VirtualHost{
+				Fqdn: "example.com",
+			},
+			Routes: []projcontour.Route{{
+				Services: []projcontour.Service{{
+					Name: "kuard",
+					Port: 8080,
+					UpstreamValidation: &projcontour.UpstreamValidation{
+						CACertificate: cert1.Name,
+						SubjectName:   "example.com",
+					},
+				}},
+			}},
+		},
+	}
+
 	// httplb10 has a websocket route
 	httplb10 := &projcontour.HTTPLoadBalancer{
 		ObjectMeta: metav1.ObjectMeta{
@@ -1569,7 +1634,7 @@ func TestDAGInsert(t *testing.T) {
 					Port: 8080,
 				}},
 			}, {
-				Condition: projcontour.Condition{
+				Condition: &projcontour.Condition{
 					Prefix: "/websocket",
 				},
 				EnableWebsockets: true,
@@ -1597,7 +1662,7 @@ func TestDAGInsert(t *testing.T) {
 					Port: 8080,
 				}},
 			}, {
-				Condition: projcontour.Condition{
+				Condition: &projcontour.Condition{
 					Prefix: "/websocket",
 				},
 				EnableWebsockets: true,
@@ -1628,7 +1693,7 @@ func TestDAGInsert(t *testing.T) {
 					Port: 8080,
 				}},
 			}, {
-				Condition: projcontour.Condition{
+				Condition: &projcontour.Condition{
 					Prefix: "/websocket",
 				},
 				PrefixRewrite: "/",
@@ -1640,7 +1705,7 @@ func TestDAGInsert(t *testing.T) {
 			}},
 	}
 
-	httplb17 := &projcontour.HTTPLoadBalancer{
+	httplb100 := &projcontour.HTTPLoadBalancer{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "example-com",
 			Namespace: "default",
@@ -1649,14 +1714,50 @@ func TestDAGInsert(t *testing.T) {
 			VirtualHost: &projcontour.VirtualHost{
 				Fqdn: "example.com",
 			},
+			Includes: []projcontour.Include{{
+				Name:      "marketingwww",
+				Namespace: "marketing",
+				Condition: projcontour.Condition{
+					Prefix: "/blog",
+				},
+			}},
 			Routes: []projcontour.Route{{
 				Services: []projcontour.Service{{
 					Name: "kuard",
 					Port: 8080,
-					UpstreamValidation: &projcontour.UpstreamValidation{
-						CACertificate: cert1.Name,
-						SubjectName:   "example.com",
-					},
+				}},
+			}},
+		},
+	}
+
+	httplb100a := &projcontour.HTTPLoadBalancer{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "marketingwww",
+			Namespace: "marketing",
+		},
+		Spec: projcontour.HTTPLoadBalancerSpec{
+			Routes: []projcontour.Route{{
+				Services: []projcontour.Service{{
+					Name: "blog",
+					Port: 8080,
+				}},
+			}},
+		},
+	}
+
+	httplb100b := &projcontour.HTTPLoadBalancer{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "marketingwww",
+			Namespace: "marketing",
+		},
+		Spec: projcontour.HTTPLoadBalancerSpec{
+			Routes: []projcontour.Route{{
+				Condition: &projcontour.Condition{
+					Prefix: "/infotech",
+				},
+				Services: []projcontour.Service{{
+					Name: "blog",
+					Port: 8080,
 				}},
 			}},
 		},
@@ -2683,7 +2784,43 @@ func TestDAGInsert(t *testing.T) {
 				},
 			),
 		},
-
+		"insert ingressroute routing and tcpproxying": {
+			objs: []interface{}{
+				s10, ir18,
+			},
+			want: listeners(
+				&Listener{
+					Port: 80,
+					VirtualHosts: virtualhosts(
+						virtualhost(ir18.Spec.VirtualHost.Fqdn,
+							routeCluster("/",
+								&Cluster{
+									Upstream: &Service{
+										Name:        s10.Name,
+										Namespace:   s10.Namespace,
+										ServicePort: &s10.Spec.Ports[1],
+									},
+								},
+							),
+						),
+					),
+				},
+				&Listener{
+					Port: 443,
+					VirtualHosts: virtualhosts(
+						&SecureVirtualHost{
+							VirtualHost: VirtualHost{
+								Name: ir18.Spec.VirtualHost.Fqdn,
+							},
+							TCPProxy: &TCPProxy{
+								Clusters: clusters(service(s10)),
+							},
+							MinProtoVersion: auth.TlsParameters_TLS_AUTO, // tls passthrough does not specify a TLS version; that's the domain of the backend
+						},
+					),
+				},
+			),
+		},
 		"insert root ingress route and delegate ingress route": {
 			objs: []interface{}{
 				ir5, s4, ir4, s5, ir3,
@@ -3416,6 +3553,70 @@ func TestDAGInsert(t *testing.T) {
 				},
 			),
 		},
+		"insert httplb with pathPrefix include": {
+			objs: []interface{}{
+				httplb100, httplb100a, s1, s4,
+			},
+			want: listeners(
+				&Listener{
+					Port: 80,
+					VirtualHosts: virtualhosts(
+						virtualhost("example.com",
+							routeCluster("/",
+								&Cluster{
+									Upstream: &Service{
+										Name:        s1.Name,
+										Namespace:   s1.Namespace,
+										ServicePort: &s1.Spec.Ports[0],
+									},
+								},
+							),
+							routeCluster("/blog",
+								&Cluster{
+									Upstream: &Service{
+										Name:        s4.Name,
+										Namespace:   s4.Namespace,
+										ServicePort: &s4.Spec.Ports[0],
+									},
+								},
+							),
+						),
+					),
+				},
+			),
+		},
+		"insert httplb with pathPrefix include, child adds to pathPrefix": {
+			objs: []interface{}{
+				httplb100, httplb100b, s1, s4,
+			},
+			want: listeners(
+				&Listener{
+					Port: 80,
+					VirtualHosts: virtualhosts(
+						virtualhost("example.com",
+							routeCluster("/",
+								&Cluster{
+									Upstream: &Service{
+										Name:        s1.Name,
+										Namespace:   s1.Namespace,
+										ServicePort: &s1.Spec.Ports[0],
+									},
+								},
+							),
+							routeCluster("/blog/infotech",
+								&Cluster{
+									Upstream: &Service{
+										Name:        s4.Name,
+										Namespace:   s4.Namespace,
+										ServicePort: &s4.Spec.Ports[0],
+									},
+								},
+							),
+						),
+					),
+				},
+			),
+		},
 	}
 
 	for name, tc := range tests {
diff --git a/internal/dag/status_test.go b/internal/dag/status_test.go
index 9d43a60b..29efba81 100644
--- a/internal/dag/status_test.go
+++ b/internal/dag/status_test.go
@@ -654,6 +654,54 @@ func TestDAGIngressRouteStatus(t *testing.T) {
 		},
 	}
 
+	s10 := &v1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "tls-passthrough",
+			Namespace: "roots",
+		},
+		Spec: v1.ServiceSpec{
+			Ports: []v1.ServicePort{{
+				Name:       "https",
+				Protocol:   "TCP",
+				Port:       443,
+				TargetPort: intstr.FromInt(443),
+			}, {
+				Name:       "http",
+				Protocol:   "TCP",
+				Port:       80,
+				TargetPort: intstr.FromInt(80),
+			}},
+		},
+	}
+
+	ir27 := &ingressroutev1.IngressRoute{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "kuard-tcp",
+			Namespace: s10.Namespace,
+		},
+		Spec: ingressroutev1.IngressRouteSpec{
+			VirtualHost: &projcontour.VirtualHost{
+				Fqdn: "kuard.example.com",
+				TLS: &projcontour.TLS{
+					Passthrough: true,
+				},
+			},
+			Routes: []ingressroutev1.Route{{
+				Match: "/",
+				Services: []ingressroutev1.Service{{
+					Name: s10.Name,
+					Port: 80, // proxy non secure traffic to port 80
+				}},
+			}},
+			TCPProxy: &ingressroutev1.TCPProxy{
+				Services: []ingressroutev1.Service{{
+					Name: s10.Name,
+					Port: 443, // ssl passthrough to secure port
+				}},
+			},
+		},
+	}
+
 	tests := map[string]struct {
 		objs []interface{}
 		want map[Meta]Status
@@ -874,6 +922,21 @@ func TestDAGIngressRouteStatus(t *testing.T) {
 				},
 			},
 		},
+		// issue 910
+		"non tls routes can be combined with tcp proxy": {
+			objs: []interface{}{
+				s10,
+				ir27,
+			},
+			want: map[Meta]Status{
+				{name: ir27.Name, namespace: ir27.Namespace}: {
+					Object:      ir27,
+					Status:      StatusValid,
+					Description: `valid IngressRoute`,
+					Vhost:       ir27.Spec.VirtualHost.Fqdn,
+				},
+			},
+		},
 	}
 
 	for name, tc := range tests {