Repository: knative/serving
Pull request #8141 (2020-06-01 08:59:35)

Remove noop lines from tests
- Rename test with duplicated name
- TimeoutSeconds is already defaulted to &45 in revision() helper (via [`defaultRevision`](https://github.com/knative/serving/blob/master/pkg/reconciler/revision/resources/deploy_test.go#L226)) - no need to repetitively specify it again in every test
- Add explicit test to table to cover changed TimeoutSeconds case
- CONTAINER_CONCURRENCY is already [defaulted to 1](https://github.com/knative/serving/blob/master/pkg/reconciler/revision/resources/queue_test.go#L760) in env() helper so prefer setContainerConcurrency(1) rather than 0 in a few places to take advantage of default where that makes actual focus of test clearer
- Labels are already empty by default, no need to explicitly re-empty them

<!--
Request Prow to automatically lint any go code in this PR:

/lint
-->


knative-prow-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative/serving/pull/8141#" title="Author self-approved">julz</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **vagababov**
You can assign the PR to them by writing `/assign @vagababov` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative%2Fserving).

<details open>
Needs approval from an approver in each of these files:

- **[pkg/reconciler/OWNERS](https://github.com/knative/serving/blob/master/pkg/reconciler/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["vagababov"]} -->

Patch:
diff --git a/cmd/queue/main.go b/cmd/queue/main.go
index 50cd04083..00a491c7b 100644
--- a/cmd/queue/main.go
+++ b/cmd/queue/main.go
@@ -62,7 +62,7 @@ const (
 	badProbeTemplate   = "unexpected probe header value: %s"
 	failingHealthcheck = "failing healthcheck"
 
-	healthURLTemplate = "http://127.0.0.1:%d"
+	healthURLPrefix = "http://127.0.0.1:"
 	// The 25 millisecond retry interval is an unscientific compromise between wanting to get
 	// started as early as possible while still wanting to give the container some breathing
 	// room to get up and running.
@@ -77,6 +77,13 @@ var (
 	readinessProbeTimeout = flag.Int("probe-period", -1, "run readiness probe with given timeout")
 )
 
+// Scaled down config to use during exec probing.
+type probeConfig struct {
+	QueueServingPort int `split_words:"true" required:"true"`
+	// DownwardAPI configuration for pod labels
+	DownwardAPILabelsPath string `split_words:"true"`
+}
+
 type config struct {
 	ContainerConcurrency   int    `split_words:"true" required:"true"`
 	QueueServingPort       int    `split_words:"true" required:"true"`
@@ -236,40 +243,35 @@ func knativeProbeHandler(healthState *health.State, prober func() bool, isAggres
 	}
 }
 
-func probeQueueHealthPath(port, timeoutSeconds int, env config) error {
-	if port <= 0 {
-		return fmt.Errorf("port must be a positive value, got %d", port)
+func probeQueueHealthPath(timeoutSeconds int, env probeConfig) error {
+	if env.QueueServingPort <= 0 {
+		return fmt.Errorf("port must be a positive value, got %d", env.QueueServingPort)
 	}
 
-	url := fmt.Sprintf(healthURLTemplate, port)
+	url := healthURLPrefix + strconv.Itoa(env.QueueServingPort)
 	timeoutDuration := readiness.PollTimeout
 	if timeoutSeconds != 0 {
 		timeoutDuration = time.Duration(timeoutSeconds) * time.Second
 	}
+
+	req, err := http.NewRequest(http.MethodGet, url, nil)
+	if err != nil {
+		return fmt.Errorf("probe failed: error creating request: %w", err)
+	}
+	// Add the header to indicate this is a probe request.
+	req.Header.Add(network.ProbeHeaderName, queue.Name)
+	req.Header.Add(network.UserAgentKey, network.QueueProxyUserAgent)
+
 	httpClient := &http.Client{
-		Transport: &http.Transport{
-			// Do not use the cached connection
-			DisableKeepAlives: true,
-		},
 		Timeout: timeoutDuration,
 	}
 	ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
 	defer cancel()
-	stopCh := ctx.Done()
 
 	var lastErr error
 	// Using PollImmediateUntil instead of PollImmediate because if timeout is reached while waiting for first
 	// invocation of conditionFunc, it exits immediately without trying for a second time.
 	timeoutErr := wait.PollImmediateUntil(aggressivePollInterval, func() (bool, error) {
-		var req *http.Request
-		req, lastErr = http.NewRequest(http.MethodGet, url, nil)
-		if lastErr != nil {
-			// Return nil error for retrying
-			return false, nil
-		}
-		// Add the header to indicate this is a probe request.
-		req.Header.Add(network.ProbeHeaderName, queue.Name)
-		req.Header.Add(network.UserAgentKey, network.QueueProxyUserAgent)
 		res, lastErr := httpClient.Do(req)
 		if lastErr != nil {
 			// Return nil error for retrying
@@ -277,6 +279,7 @@ func probeQueueHealthPath(port, timeoutSeconds int, env config) error {
 		}
 		defer res.Body.Close()
 		success := health.IsHTTPProbeReady(res)
+
 		// The check for preferForScaledown() fails readiness faster
 		// in the presence of the label
 		if preferScaleDown, err := preferPodForScaledown(env.DownwardAPILabelsPath); err != nil {
@@ -285,7 +288,7 @@ func probeQueueHealthPath(port, timeoutSeconds int, env config) error {
 			return false, errors.New("failing probe deliberately for pod scaledown")
 		}
 		return success, nil
-	}, stopCh)
+	}, ctx.Done())
 
 	if lastErr != nil {
 		return fmt.Errorf("failed to probe: %w", lastErr)
@@ -302,16 +305,15 @@ func probeQueueHealthPath(port, timeoutSeconds int, env config) error {
 func main() {
 	flag.Parse()
 
-	// Parse the environment.
-	var env config
-	if err := envconfig.Process("", &env); err != nil {
-		fmt.Fprintln(os.Stderr, err)
-		os.Exit(1)
-	}
-
 	// If this is set, we run as a standalone binary to probe the queue-proxy.
 	if *readinessProbeTimeout >= 0 {
-		if err := probeQueueHealthPath(env.QueueServingPort, *readinessProbeTimeout, env); err != nil {
+		// Parse the environment.
+		var env probeConfig
+		if err := envconfig.Process("", &env); err != nil {
+			fmt.Fprintln(os.Stderr, err)
+			os.Exit(1)
+		}
+		if err := probeQueueHealthPath(*readinessProbeTimeout, env); err != nil {
 			// used instead of the logger to produce a concise event message
 			fmt.Fprintln(os.Stderr, err)
 			os.Exit(1)
@@ -319,6 +321,13 @@ func main() {
 		os.Exit(0)
 	}
 
+	// Parse the environment.
+	var env config
+	if err := envconfig.Process("", &env); err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+
 	// Setup the logger.
 	logger, _ = pkglogging.NewLogger(env.ServingLoggingConfig, env.ServingLoggingLevel)
 	logger = logger.Named("queueproxy")
diff --git a/pkg/apis/networking/v1alpha1/ingress_types.go b/pkg/apis/networking/v1alpha1/ingress_types.go
index 607249fd4..c004cb095 100644
--- a/pkg/apis/networking/v1alpha1/ingress_types.go
+++ b/pkg/apis/networking/v1alpha1/ingress_types.go
@@ -221,6 +221,14 @@ type HTTPIngressPath struct {
 	// If RewriteHost is specified, Splits must not be.
 	RewriteHost string `json:"rewriteHost,omitempty"`
 
+	// Headers defines header matching rules which is a map from a header name
+	// to HeaderMatch which specify a matching condition.
+	// When a request matched with all the header matching rules,
+	// the request is routed by the corresponding ingress rule.
+	// If it is empty, the headers are not used for matching
+	// +optional
+	Headers map[string]HeaderMatch `json:"headers,omitempty"`
+
 	// Splits defines the referenced service endpoints to which the traffic
 	// will be forwarded to.
 	//
@@ -361,3 +369,9 @@ const (
 func (t *Ingress) GetStatus() *duckv1.Status {
 	return &t.Status.Status
 }
+
+// HeaderMatch represents a matching value of Headers in HTTPIngressPath.
+// Currently, only the exact matching is supported.
+type HeaderMatch struct {
+	Exact string `json:"exact"`
+}
diff --git a/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go
index 0f4641d8b..bd179bb46 100644
--- a/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go
@@ -161,6 +161,13 @@ func (in *HTTP01Challenge) DeepCopy() *HTTP01Challenge {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *HTTPIngressPath) DeepCopyInto(out *HTTPIngressPath) {
 	*out = *in
+	if in.Headers != nil {
+		in, out := &in.Headers, &out.Headers
+		*out = make(map[string]HeaderMatch, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
 	if in.Splits != nil {
 		in, out := &in.Splits, &out.Splits
 		*out = make([]IngressBackendSplit, len(*in))
@@ -242,6 +249,22 @@ func (in *HTTPRetry) DeepCopy() *HTTPRetry {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HeaderMatch) DeepCopyInto(out *HeaderMatch) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeaderMatch.
+func (in *HeaderMatch) DeepCopy() *HeaderMatch {
+	if in == nil {
+		return nil
+	}
+	out := new(HeaderMatch)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Ingress) DeepCopyInto(out *Ingress) {
 	*out = *in
diff --git a/pkg/autoscaler/fake/fake_metric_client.go b/pkg/autoscaler/fake/fake_metric_client.go
index a2ca92e5e..9454fa2af 100644
--- a/pkg/autoscaler/fake/fake_metric_client.go
+++ b/pkg/autoscaler/fake/fake_metric_client.go
@@ -92,14 +92,6 @@ func (mc *MetricClient) StableAndPanicRPS(key types.NamespacedName, now time.Tim
 	return mc.StableRPS, mc.PanicRPS, err
 }
 
-// StaticMetricClient returns stable/panic concurrency and RPS with static value, i.e. 10.
-var StaticMetricClient = MetricClient{
-	StableConcurrency: 10.0,
-	PanicConcurrency:  10.0,
-	StableRPS:         10.0,
-	PanicRPS:          10.0,
-}
-
 // Endpoints is used to create endpoints.
 func Endpoints(count int, svc string) {
 	epAddresses := make([]corev1.EndpointAddress, count)
diff --git a/pkg/network/network.go b/pkg/network/network.go
index 7eef7a361..89400e881 100644
--- a/pkg/network/network.go
+++ b/pkg/network/network.go
@@ -137,6 +137,19 @@ const (
 	// AutoscalingUserAgent is the user-agent header value set in probe
 	// requests sent by autoscaling implementations.
 	AutoscalingUserAgent = "Knative-Autoscaling-Probe"
+
+	// TagHeaderName is the name of the header entry which has a tag name as value.
+	// The tag name specifies which route was expected to be chosen by Ingress.
+	TagHeaderName = "Knative-Serving-Tag"
+
+	// DefaultRouteHeaderName is the name of the header entry
+	// identifying whether a request is routed via the default route or not.
+	// It has one of the string value "true" or "false".
+	DefaultRouteHeaderName = "Knative-Serving-Default-Route"
+
+	// TagHeaderBasedRoutingKey is the name of the configuration entry
+	// that specifies enabling tag header based routing or not.
+	TagHeaderBasedRoutingKey = "tagHeaderBasedRouting"
 )
 
 // DomainTemplateValues are the available properties people can choose from
@@ -195,6 +208,9 @@ type Config struct {
 
 	// DefaultCertificateClass specifies the default Certificate class.
 	DefaultCertificateClass string
+
+	// TagHeaderBasedRouting specifies if TagHeaderBasedRouting is enabled or not.
+	TagHeaderBasedRouting bool
 }
 
 // HTTPProtocol indicates a type of HTTP endpoint behavior
@@ -270,6 +286,7 @@ func NewConfigFromMap(data map[string]string) (*Config, error) {
 	templateCache.Add(nc.TagTemplate, t)
 
 	nc.AutoTLS = strings.EqualFold(data[AutoTLSKey], "enabled")
+	nc.TagHeaderBasedRouting = strings.EqualFold(data[TagHeaderBasedRoutingKey], "enabled")
 
 	switch strings.ToLower(data[HTTPProtocolKey]) {
 	case "", string(HTTPEnabled):
diff --git a/pkg/reconciler/revision/resources/queue.go b/pkg/reconciler/revision/resources/queue.go
index 40ed65db9..18094b400 100644
--- a/pkg/reconciler/revision/resources/queue.go
+++ b/pkg/reconciler/revision/resources/queue.go
@@ -148,11 +148,19 @@ func makeQueueProbe(in *corev1.Probe) *corev1.Probe {
 					Command: []string{"/ko-app/queue", "-probe-period", "0"},
 				},
 			},
-			// We want to mark the service as not ready as soon as the
-			// PreStop handler is called, so we need to check a little
-			// bit more often than the default.  It is a small
-			// sacrifice for a low rate of 503s.
-			PeriodSeconds: 1,
+			// The exec probe enables us to retry failed probes quickly to get sub-second
+			// resolution and achieve faster cold-starts.  However, for draining pods as
+			// part of the K8s lifecycle this period will bound the tail of how quickly
+			// we can remove a Pod's endpoint from the K8s service.
+			//
+			// The trade-off here is that exec probes cost CPU to run, and for idle pods
+			// (e.g. due to minScale) we see ~50m/{period} of idle CPU usage in the
+			// queue-proxy.  So while setting this to 1s results in slightly faster drains
+			// it also means that in the steady state the queue-proxy is consuming 10x
+			// more CPU due to probes than with a period of 10s.
+			//
+			// See also: https://github.com/knative/serving/issues/8147
+			PeriodSeconds: 10,
 			// We keep the connection open for a while because we're
 			// actively probing the user-container on that endpoint and
 			// thus don't want to be limited by K8s granularity here.
diff --git a/pkg/reconciler/route/resources/ingress.go b/pkg/reconciler/route/resources/ingress.go
index fa4e7956b..9f143e846 100644
--- a/pkg/reconciler/route/resources/ingress.go
+++ b/pkg/reconciler/route/resources/ingress.go
@@ -30,6 +30,8 @@ import (
 	netv1alpha1 "knative.dev/serving/pkg/apis/networking/v1alpha1"
 	"knative.dev/serving/pkg/apis/serving"
 	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+	"knative.dev/serving/pkg/network"
+	"knative.dev/serving/pkg/reconciler/route/config"
 	"knative.dev/serving/pkg/reconciler/route/domains"
 	"knative.dev/serving/pkg/reconciler/route/resources/labels"
 	"knative.dev/serving/pkg/reconciler/route/resources/names"
@@ -99,6 +101,8 @@ func MakeIngressSpec(
 	rules := make([]netv1alpha1.IngressRule, 0, len(names))
 	challengeHosts := getChallengeHosts(acmeChallenges)
 
+	networkConfig := config.FromContext(ctx).Network
+
 	for _, name := range names {
 		visibilities := []netv1alpha1.IngressVisibility{netv1alpha1.IngressVisibilityClusterLocal}
 		// If this is a public target (or not being marked as cluster-local), we also make public rule.
@@ -111,6 +115,33 @@ func MakeIngressSpec(
 				return netv1alpha1.IngressSpec{}, err
 			}
 			rule := *makeIngressRule([]string{domain}, r.Namespace, visibility, targets[name])
+			if networkConfig.TagHeaderBasedRouting {
+				if rule.HTTP.Paths[0].AppendHeaders == nil {
+					rule.HTTP.Paths[0].AppendHeaders = make(map[string]string)
+				}
+
+				if name == traffic.DefaultTarget {
+					// To provide a information if a request is routed via the "default route" or not,
+					// the header "Knative-Serving-Default-Route: true" is appended here.
+					// If the header has "true" and there is a "Knative-Serving-Tag" header,
+					// then the request is having the undefined tag header, which will be observed in queue-proxy.
+					rule.HTTP.Paths[0].AppendHeaders[network.DefaultRouteHeaderName] = "true"
+					// Add ingress paths for a request with the tag header.
+					// If a request has one of the `names`(tag name) except the default path,
+					// the request will be routed via one of the ingress paths, corresponding to the tag name.
+					rule.HTTP.Paths = append(
+						makeTagBasedRoutingIngressPaths(r.Namespace, targets, names), rule.HTTP.Paths...)
+				} else {
+					// If a request is routed by a tag-attached hostname instead of the tag header,
+					// the request may not have the tag header "Knative-Serving-Tag",
+					// even though the ingress path used in the case is also originated
+					// from the same Knative route with the ingress path for the tag based routing.
+					//
+					// To prevent such inconsistency,
+					// the tag header is appended with the tag corresponding to the tag-attached hostname
+					rule.HTTP.Paths[0].AppendHeaders[network.TagHeaderName] = name
+				}
+			}
 			// If this is a public rule, we need to configure ACME challenge paths.
 			if visibility == netv1alpha1.IngressVisibilityExternalIP {
 				rule.HTTP.Paths = append(
@@ -173,6 +204,32 @@ func makeACMEIngressPaths(challenges map[string]netv1alpha1.HTTP01Challenge, dom
 }
 
 func makeIngressRule(domains []string, ns string, visibility netv1alpha1.IngressVisibility, targets traffic.RevisionTargets) *netv1alpha1.IngressRule {
+	return &netv1alpha1.IngressRule{
+		Hosts:      domains,
+		Visibility: visibility,
+		HTTP: &netv1alpha1.HTTPIngressRuleValue{
+			Paths: []netv1alpha1.HTTPIngressPath{
+				*makeBaseIngressPath(ns, targets),
+			},
+		},
+	}
+}
+
+func makeTagBasedRoutingIngressPaths(ns string, targets map[string]traffic.RevisionTargets, names []string) []netv1alpha1.HTTPIngressPath {
+	paths := make([]netv1alpha1.HTTPIngressPath, 0, len(names))
+
+	for _, name := range names {
+		if name != traffic.DefaultTarget {
+			path := makeBaseIngressPath(ns, targets[name])
+			path.Headers = map[string]netv1alpha1.HeaderMatch{network.TagHeaderName: {Exact: name}}
+			paths = append(paths, *path)
+		}
+	}
+
+	return paths
+}
+
+func makeBaseIngressPath(ns string, targets traffic.RevisionTargets) *netv1alpha1.HTTPIngressPath {
 	// Optimistically allocate |targets| elements.
 	splits := make([]netv1alpha1.IngressBackendSplit, 0, len(targets))
 	for _, t := range targets {
@@ -196,14 +253,8 @@ func makeIngressRule(domains []string, ns string, visibility netv1alpha1.Ingress
 		})
 	}
 
-	return &netv1alpha1.IngressRule{
-		Hosts:      domains,
-		Visibility: visibility,
-		HTTP: &netv1alpha1.HTTPIngressRuleValue{
-			Paths: []netv1alpha1.HTTPIngressPath{{
-				Splits: splits,
-				// TODO(lichuqiang): #2201, plumbing to config timeout and retries.
-			}},
-		},
+	return &netv1alpha1.HTTPIngressPath{
+		Splits: splits,
+		// TODO(lichuqiang): #2201, plumbing to config timeout and retries.
 	}
 }
diff --git a/third_party/net-istio.yaml b/third_party/net-istio.yaml
index cd460df7c..736377594 100644
--- a/third_party/net-istio.yaml
+++ b/third_party/net-istio.yaml
@@ -18,7 +18,7 @@ metadata:
   # These are the permissions needed by the Istio Ingress implementation.
   name: knative-serving-istio
   labels:
-    serving.knative.dev/release: "v20200529-2623cee"
+    serving.knative.dev/release: "v20200531-cb895b4"
     serving.knative.dev/controller: "true"
     networking.knative.dev/ingress-provider: istio
 rules:
@@ -48,7 +48,7 @@ metadata:
   name: knative-ingress-gateway
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20200529-2623cee"
+    serving.knative.dev/release: "v20200531-cb895b4"
     networking.knative.dev/ingress-provider: istio
 spec:
   selector:
@@ -85,7 +85,7 @@ metadata:
   name: cluster-local-gateway
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20200529-2623cee"
+    serving.knative.dev/release: "v20200531-cb895b4"
     networking.knative.dev/ingress-provider: istio
 spec:
   selector:
@@ -118,7 +118,7 @@ kind: MutatingWebhookConfiguration
 metadata:
   name: webhook.istio.networking.internal.knative.dev
   labels:
-    serving.knative.dev/release: "v20200529-2623cee"
+    serving.knative.dev/release: "v20200531-cb895b4"
     networking.knative.dev/ingress-provider: istio
 webhooks:
 - admissionReviewVersions:
@@ -154,7 +154,7 @@ kind: ValidatingWebhookConfiguration
 metadata:
   name: config.webhook.istio.networking.internal.knative.dev
   labels:
-    serving.knative.dev/release: "v20200529-2623cee"
+    serving.knative.dev/release: "v20200531-cb895b4"
     networking.knative.dev/ingress-provider: istio
 webhooks:
 - admissionReviewVersions:
@@ -192,7 +192,7 @@ metadata:
   name: istio-webhook-certs
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20200529-2623cee"
+    serving.knative.dev/release: "v20200531-cb895b4"
     networking.knative.dev/ingress-provider: istio
 
 ---
@@ -216,7 +216,7 @@ metadata:
   name: config-istio
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20200529-2623cee"
+    serving.knative.dev/release: "v20200531-cb895b4"
     networking.knative.dev/ingress-provider: istio
 data:
   _example: |
@@ -286,7 +286,7 @@ metadata:
   name: networking-istio
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20200529-2623cee"
+    serving.knative.dev/release: "v20200531-cb895b4"
     networking.knative.dev/ingress-provider: istio
 spec:
   selector:
@@ -302,14 +302,14 @@ spec:
         sidecar.istio.io/inject: "false"
       labels:
         app: networking-istio
-        serving.knative.dev/release: "v20200529-2623cee"
+        serving.knative.dev/release: "v20200531-cb895b4"
     spec:
       serviceAccountName: controller
       containers:
       - name: networking-istio
         # This is the Go import path for the binary that is containerized
         # and substituted here.
-        image: gcr.io/knative-nightly/knative.dev/net-istio/cmd/controller@sha256:cc5067849ec2430c8569ffd61cdc8ac1bf3de3ecd0da50f76620cef950452692
+        image: gcr.io/knative-nightly/knative.dev/net-istio/cmd/controller@sha256:7b787833cbd5706ce9fd58f802b6b622608e9bbf8c2e6593579ae4081b6c32d3
         resources:
           requests:
             cpu: 30m
@@ -361,7 +361,7 @@ metadata:
   name: istio-webhook
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20200529-2623cee"
+    serving.knative.dev/release: "v20200531-cb895b4"
     networking.knative.dev/ingress-provider: istio
 spec:
   selector:
@@ -375,14 +375,14 @@ spec:
       labels:
         app: istio-webhook
         role: istio-webhook
-        serving.knative.dev/release: "v20200529-2623cee"
+        serving.knative.dev/release: "v20200531-cb895b4"
     spec:
       serviceAccountName: controller
       containers:
       - name: webhook
         # This is the Go import path for the binary that is containerized
         # and substituted here.
-        image: gcr.io/knative-nightly/knative.dev/net-istio/cmd/webhook@sha256:860d78ac85abb26eceb04efcfa221ea7a0fa1f21e6f0164d07fa11c6b6a5f160
+        image: gcr.io/knative-nightly/knative.dev/net-istio/cmd/webhook@sha256:8511f6b143a2b804c9623e6f07ba2b468e778aba9c74a55782fd32de373703a2
         resources:
           requests:
             cpu: 20m
@@ -436,7 +436,7 @@ metadata:
   namespace: knative-serving
   labels:
     role: istio-webhook
-    serving.knative.dev/release: "v20200529-2623cee"
+    serving.knative.dev/release: "v20200531-cb895b4"
     networking.knative.dev/ingress-provider: istio
 spec:
   ports:


Test patch:
diff --git a/cmd/autoscaler/main_test.go b/cmd/autoscaler/main_test.go
index fbbf16393..cfcbb28c0 100644
--- a/cmd/autoscaler/main_test.go
+++ b/cmd/autoscaler/main_test.go
@@ -21,7 +21,6 @@ import (
 	"strings"
 	"testing"
 
-	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	kubeinformers "k8s.io/client-go/informers"
 	fakek8s "k8s.io/client-go/kubernetes/fake"
@@ -30,11 +29,6 @@ import (
 	"knative.dev/serving/pkg/autoscaler/scaling"
 )
 
-const (
-	testNamespace = "test-namespace"
-	testRevision  = "test-Revision"
-)
-
 var (
 	kubeClient   = fakek8s.NewSimpleClientset()
 	kubeInformer = kubeinformers.NewSharedInformerFactory(kubeClient, 0)
@@ -66,11 +60,11 @@ func TestUniscalerFactoryFailures(t *testing.T) {
 		}, "max length must be 255 characters",
 	}}
 
-	uniScalerFactory := getTestUniScalerFactory()
+	uniScalerFactory := testUniScalerFactory()
 	decider := &scaling.Decider{
 		ObjectMeta: metav1.ObjectMeta{
-			Namespace: testNamespace,
-			Name:      testRevision,
+			Namespace: autoscalerfake.TestNamespace,
+			Name:      autoscalerfake.TestRevision,
 		},
 		Spec: scaling.DeciderSpec{
 			ServiceName: "wholesome-service",
@@ -94,7 +88,7 @@ func TestUniscalerFactoryFailures(t *testing.T) {
 	// Now blank out service name and give correct labels.
 	decider.Spec.ServiceName = ""
 	decider.Labels = map[string]string{
-		serving.RevisionLabelKey:      testRevision,
+		serving.RevisionLabelKey:      autoscalerfake.TestRevision,
 		serving.ServiceLabelKey:       "some-nice-service",
 		serving.ConfigurationLabelKey: "test-config",
 	}
@@ -108,28 +102,16 @@ func TestUniscalerFactoryFailures(t *testing.T) {
 	}
 }
 
-func endpoints(ns, n string) {
-	ep := &corev1.Endpoints{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: ns,
-			Name:      n,
-		},
-		Subsets: []corev1.EndpointSubset{{}},
-	}
-	kubeClient.CoreV1().Endpoints(ns).Create(ep)
-	kubeInformer.Core().V1().Endpoints().Informer().GetIndexer().Add(ep)
-}
-
 func TestUniScalerFactoryFunc(t *testing.T) {
-	endpoints(testNamespace, "magic-services-offered")
-	uniScalerFactory := getTestUniScalerFactory()
+	autoscalerfake.Endpoints(1, "magic-services-offered")
+	uniScalerFactory := testUniScalerFactory()
 	for _, srv := range []string{"some", ""} {
 		decider := &scaling.Decider{
 			ObjectMeta: metav1.ObjectMeta{
-				Namespace: testNamespace,
-				Name:      testRevision,
+				Namespace: autoscalerfake.TestNamespace,
+				Name:      autoscalerfake.TestRevision,
 				Labels: map[string]string{
-					serving.RevisionLabelKey:      testRevision,
+					serving.RevisionLabelKey:      autoscalerfake.TestRevision,
 					serving.ServiceLabelKey:       srv,
 					serving.ConfigurationLabelKey: "test-config",
 				},
@@ -145,6 +127,6 @@ func TestUniScalerFactoryFunc(t *testing.T) {
 	}
 }
 
-func getTestUniScalerFactory() func(decider *scaling.Decider) (scaling.UniScaler, error) {
-	return uniScalerFactoryFunc(kubeInformer.Core().V1().Endpoints(), &autoscalerfake.StaticMetricClient)
+func testUniScalerFactory() func(decider *scaling.Decider) (scaling.UniScaler, error) {
+	return uniScalerFactoryFunc(kubeInformer.Core().V1().Endpoints(), &autoscalerfake.MetricClient{})
 }
diff --git a/cmd/queue/main_test.go b/cmd/queue/main_test.go
index 932409e1e..25664ccbb 100644
--- a/cmd/queue/main_test.go
+++ b/cmd/queue/main_test.go
@@ -169,9 +169,7 @@ func TestProbeHandler(t *testing.T) {
 }
 
 func TestProbeQueueInvalidPort(t *testing.T) {
-	const port = 0 // invalid port
-
-	if err := probeQueueHealthPath(port, 1, config{}); err == nil {
+	if err := probeQueueHealthPath(1, probeConfig{QueueServingPort: 0}); err == nil {
 		t.Error("Expected error, got nil")
 	} else if diff := cmp.Diff(err.Error(), "port must be a positive value, got 0"); diff != "" {
 		t.Errorf("Unexpected not ready message: %s", diff)
@@ -179,9 +177,7 @@ func TestProbeQueueInvalidPort(t *testing.T) {
 }
 
 func TestProbeQueueConnectionFailure(t *testing.T) {
-	port := 12345 // some random port (that's not listening)
-
-	if err := probeQueueHealthPath(port, 1, config{}); err == nil {
+	if err := probeQueueHealthPath(1, probeConfig{QueueServingPort: 12345}); err == nil {
 		t.Error("Expected error, got nil")
 	}
 }
@@ -205,7 +201,7 @@ func TestProbeQueueNotReady(t *testing.T) {
 		t.Fatalf("Failed to convert port(%s) to int: %v", u.Port(), err)
 	}
 
-	err = probeQueueHealthPath(port, 1, config{})
+	err = probeQueueHealthPath(1, probeConfig{QueueServingPort: port})
 
 	if diff := cmp.Diff(err.Error(), "probe returned not ready"); diff != "" {
 		t.Errorf("Unexpected not ready message: %s", diff)
@@ -235,7 +231,7 @@ func TestProbeQueueReady(t *testing.T) {
 		t.Fatalf("Failed to convert port(%s) to int: %v", u.Port(), err)
 	}
 
-	if err = probeQueueHealthPath(port, 1, config{}); err != nil {
+	if err = probeQueueHealthPath(1, probeConfig{QueueServingPort: port}); err != nil {
 		t.Errorf("probeQueueHealthPath(%d, 1s) = %s", port, err)
 	}
 
@@ -276,7 +272,10 @@ func TestProbeFailFast(t *testing.T) {
 	f.Close()
 
 	start := time.Now()
-	if err = probeQueueHealthPath(port, 1 /*seconds*/, config{DownwardAPILabelsPath: f.Name()}); err == nil {
+	if err = probeQueueHealthPath(1 /*seconds*/, probeConfig{
+		QueueServingPort:      port,
+		DownwardAPILabelsPath: f.Name(),
+	}); err == nil {
 		t.Error("probeQueueHealthPath did not fail")
 	}
 
@@ -307,7 +306,7 @@ func TestProbeQueueTimeout(t *testing.T) {
 	}
 
 	timeout := 1
-	if err = probeQueueHealthPath(port, timeout, config{}); err == nil {
+	if err = probeQueueHealthPath(timeout, probeConfig{QueueServingPort: port}); err == nil {
 		t.Errorf("Expected probeQueueHealthPath(%d, %v) to return timeout error", port, timeout)
 	}
 
@@ -341,7 +340,7 @@ func TestProbeQueueDelayedReady(t *testing.T) {
 	}
 
 	timeout := 0
-	if err := probeQueueHealthPath(port, timeout, config{}); err != nil {
+	if err := probeQueueHealthPath(timeout, probeConfig{QueueServingPort: port}); err != nil {
 		t.Errorf("probeQueueHealthPath(%d) = %s", port, err)
 	}
 }
diff --git a/pkg/autoscaler/scaling/autoscaler_test.go b/pkg/autoscaler/scaling/autoscaler_test.go
index f887e5e1f..1fd8452fc 100644
--- a/pkg/autoscaler/scaling/autoscaler_test.go
+++ b/pkg/autoscaler/scaling/autoscaler_test.go
@@ -556,7 +556,7 @@ func expectScale(t *testing.T, a UniScaler, now time.Time, want ScaleResult) {
 }
 
 func TestStartInPanicMode(t *testing.T) {
-	metrics := &fake.StaticMetricClient
+	metrics := &staticMetricClient
 	deciderSpec := &DeciderSpec{
 		TargetValue:         100,
 		TotalValue:          120,
@@ -599,7 +599,7 @@ func TestStartInPanicMode(t *testing.T) {
 
 func TestNewFail(t *testing.T) {
 	eraseEndpoints()
-	metrics := &fake.StaticMetricClient
+	metrics := &staticMetricClient
 	deciderSpec := &DeciderSpec{
 		TargetValue:         100,
 		TotalValue:          120,
@@ -630,3 +630,11 @@ func reset() {
 		targetRPSM.Name(), panicM.Name())
 	register()
 }
+
+// staticMetricClient returns stable/panic concurrency and RPS with static value, i.e. 10.
+var staticMetricClient = fake.MetricClient{
+	StableConcurrency: 10.0,
+	PanicConcurrency:  10.0,
+	StableRPS:         10.0,
+	PanicRPS:          10.0,
+}
diff --git a/pkg/reconciler/revision/resources/deploy_test.go b/pkg/reconciler/revision/resources/deploy_test.go
index 933ed2cab..401442bfc 100644
--- a/pkg/reconciler/revision/resources/deploy_test.go
+++ b/pkg/reconciler/revision/resources/deploy_test.go
@@ -87,7 +87,7 @@ var (
 					Command: []string{"/ko-app/queue", "-probe-period", "0"},
 				},
 			},
-			PeriodSeconds:  1,
+			PeriodSeconds:  10,
 			TimeoutSeconds: 10,
 		},
 		SecurityContext: queueSecurityContext,
diff --git a/pkg/reconciler/revision/resources/queue_test.go b/pkg/reconciler/revision/resources/queue_test.go
index dfa51a277..da69132d5 100644
--- a/pkg/reconciler/revision/resources/queue_test.go
+++ b/pkg/reconciler/revision/resources/queue_test.go
@@ -81,7 +81,6 @@ func TestMakeQueueContainer(t *testing.T) {
 		rev: revision("bar", "foo",
 			withContainerConcurrency(1),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
 				container(revision.Spec.GetContainer(), withTCPReadinessProbe())
 			}),
 		want: queueContainer(func(c *corev1.Container) {
@@ -90,11 +89,10 @@ func TestMakeQueueContainer(t *testing.T) {
 			})
 		}),
 	}, {
-		name: "no owner no autoscaler single",
+		name: "custom sidecar image, container port, protocol",
 		rev: revision("bar", "foo",
 			withContainerConcurrency(1),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
 				revision.Spec.PodSpec = corev1.PodSpec{
 					Containers: []corev1.Container{{
 						Name:           containerName,
@@ -117,14 +115,12 @@ func TestMakeQueueContainer(t *testing.T) {
 				"QUEUE_SERVING_PORT":    "8013",
 				"CONTAINER_CONCURRENCY": "1",
 			})
-		},
-		),
+		}),
 	}, {
 		name: "service name in labels",
 		rev: revision("bar", "foo",
 			withContainerConcurrency(1),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
 				revision.ObjectMeta.Labels = map[string]string{
 					serving.ServiceLabelKey: "svc",
 				}
@@ -145,7 +141,6 @@ func TestMakeQueueContainer(t *testing.T) {
 		rev: revision("blah", "baz",
 			withContainerConcurrency(0),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
 				revision.ObjectMeta.OwnerReferences = []metav1.OwnerReference{{
 					APIVersion:         v1.SchemeGroupVersion.String(),
 					Kind:               "Configuration",
@@ -167,9 +162,8 @@ func TestMakeQueueContainer(t *testing.T) {
 	}, {
 		name: "logging configuration as env var",
 		rev: revision("this", "log",
-			withContainerConcurrency(0),
+			withContainerConcurrency(1),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
 				container(revision.Spec.GetContainer(), withTCPReadinessProbe())
 			}),
 		lc: logging.Config{
@@ -180,7 +174,6 @@ func TestMakeQueueContainer(t *testing.T) {
 		},
 		want: queueContainer(func(c *corev1.Container) {
 			c.Env = env(map[string]string{
-				"CONTAINER_CONCURRENCY":  "0",
 				"SERVING_LOGGING_CONFIG": "The logging configuration goes here",
 				"SERVING_LOGGING_LEVEL":  "error",
 				"SERVING_NAMESPACE":      "log",
@@ -193,7 +186,6 @@ func TestMakeQueueContainer(t *testing.T) {
 		rev: revision("bar", "foo",
 			withContainerConcurrency(10),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
 				container(revision.Spec.GetContainer(), withTCPReadinessProbe())
 			}),
 		want: queueContainer(func(c *corev1.Container) {
@@ -205,9 +197,8 @@ func TestMakeQueueContainer(t *testing.T) {
 	}, {
 		name: "request log configuration as env var",
 		rev: revision("bar", "foo",
-			withContainerConcurrency(0),
+			withContainerConcurrency(1),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
 				container(revision.Spec.GetContainer(), withTCPReadinessProbe())
 			}),
 		oc: metrics.ObservabilityConfig{
@@ -216,7 +207,6 @@ func TestMakeQueueContainer(t *testing.T) {
 		},
 		want: queueContainer(func(c *corev1.Container) {
 			c.Env = env(map[string]string{
-				"CONTAINER_CONCURRENCY":            "0",
 				"SERVING_REQUEST_LOG_TEMPLATE":     "test template",
 				"SERVING_ENABLE_PROBE_REQUEST_LOG": "true",
 			})
@@ -225,9 +215,8 @@ func TestMakeQueueContainer(t *testing.T) {
 	}, {
 		name: "request metrics backend as env var",
 		rev: revision("bar", "foo",
-			withContainerConcurrency(0),
+			withContainerConcurrency(1),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
 				container(revision.Spec.GetContainer(), withTCPReadinessProbe())
 			}),
 		oc: metrics.ObservabilityConfig{
@@ -235,7 +224,6 @@ func TestMakeQueueContainer(t *testing.T) {
 		},
 		want: queueContainer(func(c *corev1.Container) {
 			c.Env = env(map[string]string{
-				"CONTAINER_CONCURRENCY":           "0",
 				"SERVING_REQUEST_METRICS_BACKEND": "prometheus",
 			})
 			c.Ports = append(queueNonServingPorts, queueHTTPPort)
@@ -243,19 +231,30 @@ func TestMakeQueueContainer(t *testing.T) {
 	}, {
 		name: "enable profiling",
 		rev: revision("bar", "foo",
-			withContainerConcurrency(0),
+			withContainerConcurrency(1),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
 				container(revision.Spec.GetContainer(), withTCPReadinessProbe())
 			}),
 		oc: metrics.ObservabilityConfig{EnableProfiling: true},
 		want: queueContainer(func(c *corev1.Container) {
 			c.Env = env(map[string]string{
-				"CONTAINER_CONCURRENCY": "0",
-				"ENABLE_PROFILING":      "true",
+				"ENABLE_PROFILING": "true",
 			})
 			c.Ports = append(queueNonServingPorts, profilingPort, queueHTTPPort)
 		}),
+	}, {
+		name: "custom TimeoutSeconds",
+		rev: revision("bar", "foo",
+			withContainerConcurrency(1),
+			func(revision *v1.Revision) {
+				container(revision.Spec.GetContainer(), withTCPReadinessProbe())
+				revision.Spec.TimeoutSeconds = ptr.Int64(99)
+			}),
+		want: queueContainer(func(c *corev1.Container) {
+			c.Env = env(map[string]string{
+				"REVISION_TIMEOUT_SECONDS": "99",
+			})
+		}),
 	}}
 
 	for _, test := range tests {
@@ -296,8 +295,6 @@ func TestMakeQueueContainerWithPercentageAnnotation(t *testing.T) {
 		rev: revision("bar", "foo",
 			withContainerConcurrency(1),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
-				revision.ObjectMeta.Labels = map[string]string{}
 				revision.ObjectMeta.Annotations = map[string]string{
 					serving.QueueSideCarResourcePercentageAnnotation: "20",
 				}
@@ -325,8 +322,6 @@ func TestMakeQueueContainerWithPercentageAnnotation(t *testing.T) {
 		rev: revision("bar", "foo",
 			withContainerConcurrency(1),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
-				revision.ObjectMeta.Labels = map[string]string{}
 				revision.ObjectMeta.Annotations = map[string]string{
 					serving.QueueSideCarResourcePercentageAnnotation: "0.2",
 				}
@@ -354,8 +349,6 @@ func TestMakeQueueContainerWithPercentageAnnotation(t *testing.T) {
 		rev: revision("bar", "foo",
 			withContainerConcurrency(1),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
-				revision.ObjectMeta.Labels = map[string]string{}
 				revision.ObjectMeta.Annotations = map[string]string{
 					serving.QueueSideCarResourcePercentageAnnotation: "foo",
 				}
@@ -382,8 +375,6 @@ func TestMakeQueueContainerWithPercentageAnnotation(t *testing.T) {
 		rev: revision("bar", "foo",
 			withContainerConcurrency(1),
 			func(revision *v1.Revision) {
-				revision.Spec.TimeoutSeconds = ptr.Int64(45)
-				revision.ObjectMeta.Labels = map[string]string{}
 				revision.ObjectMeta.Annotations = map[string]string{
 					serving.QueueSideCarResourcePercentageAnnotation: "100",
 				}
@@ -431,8 +422,6 @@ func TestProbeGenerationHTTPDefaults(t *testing.T) {
 	rev := revision("bar", "foo",
 		withContainerConcurrency(1),
 		func(revision *v1.Revision) {
-			revision.Spec.TimeoutSeconds = ptr.Int64(45)
-			revision.ObjectMeta.Labels = map[string]string{}
 			revision.Spec.PodSpec.Containers = []corev1.Container{{
 				Name: containerName,
 				ReadinessProbe: &corev1.Probe{
@@ -505,8 +494,6 @@ func TestProbeGenerationHTTP(t *testing.T) {
 	rev := revision("bar", "foo",
 		withContainerConcurrency(1),
 		func(revision *v1.Revision) {
-			revision.Spec.TimeoutSeconds = ptr.Int64(45)
-			revision.ObjectMeta.Labels = map[string]string{}
 			revision.Spec.PodSpec.Containers = []corev1.Container{{
 				Name: containerName,
 				Ports: []corev1.ContainerPort{{
@@ -621,7 +608,7 @@ func TestTCPProbeGeneration(t *testing.T) {
 						Command: []string{"/ko-app/queue", "-probe-period", "0"},
 					},
 				},
-				PeriodSeconds:  1,
+				PeriodSeconds:  10,
 				TimeoutSeconds: 10,
 			}
 			c.Env = env(map[string]string{"USER_PORT": strconv.Itoa(userPort)})
diff --git a/pkg/reconciler/route/resources/ingress_test.go b/pkg/reconciler/route/resources/ingress_test.go
index 88fe7003e..d94dc057c 100644
--- a/pkg/reconciler/route/resources/ingress_test.go
+++ b/pkg/reconciler/route/resources/ingress_test.go
@@ -306,6 +306,178 @@ func TestMakeIngressSpec_CorrectRuleVisibility(t *testing.T) {
 	}
 }
 
+func TestMakeIngressSpec_CorrectRulesWithTagBasedRouting(t *testing.T) {
+	targets := map[string]traffic.RevisionTargets{
+		traffic.DefaultTarget: {{
+			TrafficTarget: v1.TrafficTarget{
+				ConfigurationName: "config",
+				RevisionName:      "v2",
+				Percent:           ptr.Int64(100),
+			},
+			ServiceName: "gilberto",
+			Active:      true,
+		}},
+		"v1": {{
+			TrafficTarget: v1.TrafficTarget{
+				ConfigurationName: "config",
+				RevisionName:      "v1",
+				Percent:           ptr.Int64(100),
+			},
+			ServiceName: "jobim",
+			Active:      true,
+		}},
+	}
+
+	r := Route(ns, "test-route", WithURL)
+
+	expected := []netv1alpha1.IngressRule{{
+		Hosts: []string{
+			"test-route." + ns + ".svc.cluster.local",
+		},
+		HTTP: &netv1alpha1.HTTPIngressRuleValue{
+			Paths: []netv1alpha1.HTTPIngressPath{{
+				Headers: map[string]netv1alpha1.HeaderMatch{
+					network.TagHeaderName: {
+						Exact: "v1",
+					},
+				},
+				Splits: []netv1alpha1.IngressBackendSplit{{
+					IngressBackend: netv1alpha1.IngressBackend{
+						ServiceNamespace: ns,
+						ServiceName:      "jobim",
+						ServicePort:      intstr.FromInt(80),
+					},
+					Percent: 100,
+					AppendHeaders: map[string]string{
+						"Knative-Serving-Revision":  "v1",
+						"Knative-Serving-Namespace": ns,
+					},
+				}},
+			}, {
+				AppendHeaders: map[string]string{
+					network.DefaultRouteHeaderName: "true",
+				},
+				Splits: []netv1alpha1.IngressBackendSplit{{
+					IngressBackend: netv1alpha1.IngressBackend{
+						ServiceNamespace: ns,
+						ServiceName:      "gilberto",
+						ServicePort:      intstr.FromInt(80),
+					},
+					Percent: 100,
+					AppendHeaders: map[string]string{
+						"Knative-Serving-Revision":  "v2",
+						"Knative-Serving-Namespace": ns,
+					},
+				}},
+			}},
+		},
+		Visibility: netv1alpha1.IngressVisibilityClusterLocal,
+	}, {
+		Hosts: []string{
+			"test-route." + ns + ".example.com",
+		},
+		HTTP: &netv1alpha1.HTTPIngressRuleValue{
+			Paths: []netv1alpha1.HTTPIngressPath{{
+				Headers: map[string]netv1alpha1.HeaderMatch{
+					network.TagHeaderName: {
+						Exact: "v1",
+					},
+				},
+				Splits: []netv1alpha1.IngressBackendSplit{{
+					IngressBackend: netv1alpha1.IngressBackend{
+						ServiceNamespace: ns,
+						ServiceName:      "jobim",
+						ServicePort:      intstr.FromInt(80),
+					},
+					Percent: 100,
+					AppendHeaders: map[string]string{
+						"Knative-Serving-Revision":  "v1",
+						"Knative-Serving-Namespace": ns,
+					},
+				}},
+			}, {
+				AppendHeaders: map[string]string{
+					network.DefaultRouteHeaderName: "true",
+				},
+				Splits: []netv1alpha1.IngressBackendSplit{{
+					IngressBackend: netv1alpha1.IngressBackend{
+						ServiceNamespace: ns,
+						ServiceName:      "gilberto",
+						ServicePort:      intstr.FromInt(80),
+					},
+					Percent: 100,
+					AppendHeaders: map[string]string{
+						"Knative-Serving-Revision":  "v2",
+						"Knative-Serving-Namespace": ns,
+					},
+				}},
+			}},
+		},
+		Visibility: netv1alpha1.IngressVisibilityExternalIP,
+	}, {
+		Hosts: []string{
+			"v1-test-route." + ns + ".svc.cluster.local",
+		},
+		HTTP: &netv1alpha1.HTTPIngressRuleValue{
+			Paths: []netv1alpha1.HTTPIngressPath{{
+				AppendHeaders: map[string]string{
+					network.TagHeaderName: "v1",
+				},
+				Splits: []netv1alpha1.IngressBackendSplit{{
+					IngressBackend: netv1alpha1.IngressBackend{
+						ServiceNamespace: ns,
+						ServiceName:      "jobim",
+						ServicePort:      intstr.FromInt(80),
+					},
+					Percent: 100,
+					AppendHeaders: map[string]string{
+						"Knative-Serving-Revision":  "v1",
+						"Knative-Serving-Namespace": ns,
+					},
+				}},
+			}},
+		},
+		Visibility: netv1alpha1.IngressVisibilityClusterLocal,
+	}, {
+		Hosts: []string{
+			"v1-test-route." + ns + ".example.com",
+		},
+		HTTP: &netv1alpha1.HTTPIngressRuleValue{
+			Paths: []netv1alpha1.HTTPIngressPath{{
+				AppendHeaders: map[string]string{
+					network.TagHeaderName: "v1",
+				},
+				Splits: []netv1alpha1.IngressBackendSplit{{
+					IngressBackend: netv1alpha1.IngressBackend{
+						ServiceNamespace: ns,
+						ServiceName:      "jobim",
+						ServicePort:      intstr.FromInt(80),
+					},
+					Percent: 100,
+					AppendHeaders: map[string]string{
+						"Knative-Serving-Revision":  "v1",
+						"Knative-Serving-Namespace": ns,
+					},
+				}},
+			}},
+		},
+		Visibility: netv1alpha1.IngressVisibilityExternalIP,
+	}}
+
+	cfg := testConfig()
+	cfg.Network.TagHeaderBasedRouting = true
+	ctx := config.ToContext(context.Background(), cfg)
+
+	ci, err := MakeIngressSpec(ctx, r, nil, targets, nil /* visibility */)
+	if err != nil {
+		t.Errorf("Unexpected error %v", err)
+	}
+
+	if !cmp.Equal(expected, ci.Rules) {
+		t.Errorf("Unexpected rules (-want, +got): %s", cmp.Diff(expected, ci.Rules))
+	}
+}
+
 // One active target.
 func TestMakeIngressRule_Vanilla(t *testing.T) {
 	targets := []traffic.RevisionTarget{{
diff --git a/pkg/reconciler/route/route_test.go b/pkg/reconciler/route/route_test.go
index 585da1d18..3e19d4e5a 100644
--- a/pkg/reconciler/route/route_test.go
+++ b/pkg/reconciler/route/route_test.go
@@ -51,6 +51,7 @@ import (
 	"knative.dev/pkg/controller"
 	"knative.dev/pkg/ptr"
 	"knative.dev/pkg/system"
+	"knative.dev/serving/pkg/apis/networking/v1alpha1"
 	netv1alpha1 "knative.dev/serving/pkg/apis/networking/v1alpha1"
 	"knative.dev/serving/pkg/apis/serving"
 	v1 "knative.dev/serving/pkg/apis/serving/v1"
@@ -989,6 +990,314 @@ func TestCreateRouteWithNamedTargets(t *testing.T) {
 	}
 }
 
+func TestCreateRouteWithNamedTargetsAndTagBasedRouting(t *testing.T) {
+	ctx, _, ctl, watcher, cf := newTestSetup(t)
+	defer cf()
+
+	watcher.OnChange(&corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      network.ConfigName,
+			Namespace: system.Namespace(),
+		},
+		Data: map[string]string{
+			network.TagHeaderBasedRoutingKey: "enabled",
+		},
+	})
+	// A standalone revision
+	rev := getTestRevision("test-rev")
+	fakeservingclient.Get(ctx).ServingV1().Revisions(testNamespace).Create(rev)
+	fakerevisioninformer.Get(ctx).Informer().GetIndexer().Add(rev)
+
+	// A configuration and associated revision. Normally the revision would be
+	// created by the configuration reconciler.
+	config := getTestConfiguration()
+	cfgrev := getTestRevisionForConfig(config)
+	config.Status.SetLatestCreatedRevisionName(cfgrev.Name)
+	config.Status.SetLatestReadyRevisionName(cfgrev.Name)
+	fakeservingclient.Get(ctx).ServingV1().Configurations(testNamespace).Create(config)
+	// Since Reconcile looks in the lister, we need to add it to the informer
+	fakecfginformer.Get(ctx).Informer().GetIndexer().Add(config)
+	fakeservingclient.Get(ctx).ServingV1().Revisions(testNamespace).Create(cfgrev)
+	fakerevisioninformer.Get(ctx).Informer().GetIndexer().Add(cfgrev)
+
+	// A route targeting both the config and standalone revision with named
+	// targets
+	route := getTestRouteWithTrafficTargets(WithSpecTraffic(
+		v1.TrafficTarget{
+			Tag:          "foo",
+			RevisionName: "test-rev",
+			Percent:      ptr.Int64(50),
+		}, v1.TrafficTarget{
+			Tag:               "bar",
+			ConfigurationName: "test-config",
+			Percent:           ptr.Int64(50),
+		}))
+
+	fakeservingclient.Get(ctx).ServingV1().Routes(testNamespace).Create(route)
+	// Since Reconcile looks in the lister, we need to add it to the informer
+	fakerouteinformer.Get(ctx).Informer().GetIndexer().Add(route)
+
+	ctl.Reconciler.Reconcile(context.Background(), KeyOrDie(route))
+
+	ci := getRouteIngressFromClient(ctx, t, route)
+	domain := strings.Join([]string{route.Name, route.Namespace, defaultDomainSuffix}, ".")
+	expectedSpec := netv1alpha1.IngressSpec{
+		TLS: []netv1alpha1.IngressTLS{},
+		Rules: []netv1alpha1.IngressRule{{
+			Hosts: []string{
+				"test-route.test.svc.cluster.local",
+			},
+			HTTP: &netv1alpha1.HTTPIngressRuleValue{
+				Paths: []netv1alpha1.HTTPIngressPath{{
+					Headers: map[string]netv1alpha1.HeaderMatch{
+						network.TagHeaderName: {
+							Exact: "bar",
+						},
+					},
+					Splits: []v1alpha1.IngressBackendSplit{
+						{
+							IngressBackend: v1alpha1.IngressBackend{
+								ServiceNamespace: "test",
+								ServiceName:      "p-deadbeef",
+								ServicePort:      intstr.IntOrString{IntVal: 80},
+							},
+							Percent: 100,
+							AppendHeaders: map[string]string{
+								"Knative-Serving-Namespace": "test",
+								"Knative-Serving-Revision":  "p-deadbeef",
+							},
+						},
+					},
+				}, {
+					Headers: map[string]netv1alpha1.HeaderMatch{
+						network.TagHeaderName: {
+							Exact: "foo",
+						},
+					},
+					Splits: []v1alpha1.IngressBackendSplit{
+						{
+							IngressBackend: v1alpha1.IngressBackend{
+								ServiceNamespace: "test",
+								ServiceName:      "test-rev",
+								ServicePort:      intstr.IntOrString{IntVal: 80},
+							},
+							Percent: 100,
+							AppendHeaders: map[string]string{
+								"Knative-Serving-Namespace": "test",
+								"Knative-Serving-Revision":  "test-rev",
+							},
+						},
+					},
+				}, {
+					AppendHeaders: map[string]string{
+						network.DefaultRouteHeaderName: "true",
+					},
+					Splits: []netv1alpha1.IngressBackendSplit{{
+						IngressBackend: netv1alpha1.IngressBackend{
+							ServiceNamespace: testNamespace,
+							ServiceName:      rev.Name,
+							ServicePort:      intstr.FromInt(80),
+						},
+						Percent: 50,
+						AppendHeaders: map[string]string{
+							"Knative-Serving-Revision":  rev.Name,
+							"Knative-Serving-Namespace": testNamespace,
+						},
+					}, {
+						IngressBackend: netv1alpha1.IngressBackend{
+							ServiceNamespace: testNamespace,
+							ServiceName:      cfgrev.Name,
+							ServicePort:      intstr.FromInt(80),
+						},
+						Percent: 50,
+						AppendHeaders: map[string]string{
+							"Knative-Serving-Revision":  cfgrev.Name,
+							"Knative-Serving-Namespace": testNamespace,
+						},
+					}},
+				}},
+			},
+			Visibility: netv1alpha1.IngressVisibilityClusterLocal,
+		}, {
+			Hosts: []string{
+				domain,
+			},
+			HTTP: &netv1alpha1.HTTPIngressRuleValue{
+				Paths: []netv1alpha1.HTTPIngressPath{{
+					Headers: map[string]netv1alpha1.HeaderMatch{
+						network.TagHeaderName: {
+							Exact: "bar",
+						},
+					},
+					Splits: []v1alpha1.IngressBackendSplit{
+						{
+							IngressBackend: v1alpha1.IngressBackend{
+								ServiceNamespace: "test",
+								ServiceName:      "p-deadbeef",
+								ServicePort:      intstr.IntOrString{IntVal: 80},
+							},
+							Percent: 100,
+							AppendHeaders: map[string]string{
+								"Knative-Serving-Namespace": "test",
+								"Knative-Serving-Revision":  "p-deadbeef",
+							},
+						},
+					},
+				}, {
+					Headers: map[string]netv1alpha1.HeaderMatch{
+						network.TagHeaderName: {
+							Exact: "foo",
+						},
+					},
+					Splits: []v1alpha1.IngressBackendSplit{
+						{
+							IngressBackend: v1alpha1.IngressBackend{
+								ServiceNamespace: "test",
+								ServiceName:      "test-rev",
+								ServicePort:      intstr.IntOrString{IntVal: 80},
+							},
+							Percent: 100,
+							AppendHeaders: map[string]string{
+								"Knative-Serving-Namespace": "test",
+								"Knative-Serving-Revision":  "test-rev",
+							},
+						},
+					},
+				}, {
+					AppendHeaders: map[string]string{
+						network.DefaultRouteHeaderName: "true",
+					},
+					Splits: []netv1alpha1.IngressBackendSplit{{
+						IngressBackend: netv1alpha1.IngressBackend{
+							ServiceNamespace: testNamespace,
+							ServiceName:      rev.Name,
+							ServicePort:      intstr.FromInt(80),
+						},
+						Percent: 50,
+						AppendHeaders: map[string]string{
+							"Knative-Serving-Revision":  rev.Name,
+							"Knative-Serving-Namespace": testNamespace,
+						},
+					}, {
+						IngressBackend: netv1alpha1.IngressBackend{
+							ServiceNamespace: testNamespace,
+							ServiceName:      cfgrev.Name,
+							ServicePort:      intstr.FromInt(80),
+						},
+						Percent: 50,
+						AppendHeaders: map[string]string{
+							"Knative-Serving-Revision":  cfgrev.Name,
+							"Knative-Serving-Namespace": testNamespace,
+						},
+					}},
+				}},
+			},
+			Visibility: netv1alpha1.IngressVisibilityExternalIP,
+		}, {
+			Hosts: []string{
+				"bar-test-route.test.svc.cluster.local",
+			},
+			HTTP: &netv1alpha1.HTTPIngressRuleValue{
+				Paths: []netv1alpha1.HTTPIngressPath{{
+					Splits: []netv1alpha1.IngressBackendSplit{{
+						IngressBackend: netv1alpha1.IngressBackend{
+							ServiceNamespace: testNamespace,
+							ServiceName:      cfgrev.Name,
+							ServicePort:      intstr.FromInt(80),
+						},
+						Percent: 100,
+						AppendHeaders: map[string]string{
+							"Knative-Serving-Revision":  cfgrev.Name,
+							"Knative-Serving-Namespace": testNamespace,
+						},
+					}},
+					AppendHeaders: map[string]string{
+						network.TagHeaderName: "bar",
+					},
+				}},
+			},
+			Visibility: netv1alpha1.IngressVisibilityClusterLocal,
+		}, {
+			Hosts: []string{
+				"bar-test-route.test.test-domain.dev",
+			},
+			HTTP: &netv1alpha1.HTTPIngressRuleValue{
+				Paths: []netv1alpha1.HTTPIngressPath{{
+					Splits: []netv1alpha1.IngressBackendSplit{{
+						IngressBackend: netv1alpha1.IngressBackend{
+							ServiceNamespace: testNamespace,
+							ServiceName:      cfgrev.Name,
+							ServicePort:      intstr.FromInt(80),
+						},
+						Percent: 100,
+						AppendHeaders: map[string]string{
+							"Knative-Serving-Revision":  cfgrev.Name,
+							"Knative-Serving-Namespace": testNamespace,
+						},
+					}},
+					AppendHeaders: map[string]string{
+						network.TagHeaderName: "bar",
+					},
+				}},
+			},
+			Visibility: netv1alpha1.IngressVisibilityExternalIP,
+		}, {
+			Hosts: []string{
+				"foo-test-route.test.svc.cluster.local",
+			},
+			HTTP: &netv1alpha1.HTTPIngressRuleValue{
+				Paths: []netv1alpha1.HTTPIngressPath{{
+					Splits: []netv1alpha1.IngressBackendSplit{{
+						IngressBackend: netv1alpha1.IngressBackend{
+							ServiceNamespace: testNamespace,
+							ServiceName:      rev.Name,
+							ServicePort:      intstr.FromInt(80),
+						},
+						Percent: 100,
+						AppendHeaders: map[string]string{
+							"Knative-Serving-Revision":  rev.Name,
+							"Knative-Serving-Namespace": testNamespace,
+						},
+					}},
+					AppendHeaders: map[string]string{
+						network.TagHeaderName: "foo",
+					},
+				}},
+			},
+			Visibility: netv1alpha1.IngressVisibilityClusterLocal,
+		}, {
+			Hosts: []string{
+				"foo-test-route.test.test-domain.dev",
+			},
+			HTTP: &netv1alpha1.HTTPIngressRuleValue{
+				Paths: []netv1alpha1.HTTPIngressPath{{
+					Splits: []netv1alpha1.IngressBackendSplit{{
+						IngressBackend: netv1alpha1.IngressBackend{
+							ServiceNamespace: testNamespace,
+							ServiceName:      rev.Name,
+							ServicePort:      intstr.FromInt(80),
+						},
+						Percent: 100,
+						AppendHeaders: map[string]string{
+							"Knative-Serving-Revision":  rev.Name,
+							"Knative-Serving-Namespace": testNamespace,
+						},
+					}},
+					AppendHeaders: map[string]string{
+						network.TagHeaderName: "foo",
+					},
+				}},
+			},
+			Visibility: netv1alpha1.IngressVisibilityExternalIP,
+		}},
+	}
+
+	if diff := cmp.Diff(expectedSpec, ci.Spec); diff != "" {
+		fmt.Printf("%+v\n", ci.Spec)
+		t.Errorf("Unexpected rule spec diff (-want +got): %v", diff)
+	}
+}
+
 func TestUpdateDomainConfigMap(t *testing.T) {
 	templateCM := corev1.ConfigMap{
 		ObjectMeta: metav1.ObjectMeta{
diff --git a/test/config/config-features.yaml b/test/config/config-features.yaml
new file mode 100644
index 000000000..6336e627b
--- /dev/null
+++ b/test/config/config-features.yaml
@@ -0,0 +1,41 @@
+# Copyright 2020 The Knative Authors
+#
+# Licensed 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
+#
+#     https://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.
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: config-features
+  namespace: knative-serving
+  labels:
+    serving.knative.dev/release: devel
+
+data:
+  _example: |
+    ################################
+    #                              #
+    #    EXAMPLE CONFIGURATION     #
+    #                              #
+    ################################
+
+    # This block is not actually functional configuration,
+    # but serves to illustrate the available configuration
+    # options and document them in a way that is accessible
+    # to users that `kubectl edit` this config map.
+    #
+    # These sample configuration options may be copied out of
+    # this example block and unindented to be in the data block
+    # to actually change the configuration.
+
+    # Indicates whether multi container support is enabled
+    multi-container: "disabled"
diff --git a/third_party/contour-latest/contour.yaml b/third_party/contour-latest/contour.yaml
index b213a814f..898c622f7 100644
--- a/third_party/contour-latest/contour.yaml
+++ b/third_party/contour-latest/contour.yaml
@@ -1432,7 +1432,7 @@ spec:
         # if we change this on each version, you can no longer upgrade
         # just by applying the deployment YAML.
         # See #2423, #2395, #2150, and #2030 for earlier questions about this.
-        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:98c6e286d9052a1f86c61d64a4a43ecaebb5bd3b2370f883e1bf0629c7d80acd
+        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:69f3a2d31b2a14e8cabea4ed38ac83845ff7920c31a0d836a494383307737926
         imagePullPolicy: Always
         command:
         - contour
@@ -1672,7 +1672,7 @@ spec:
         - --contour-key-file=/certs/tls.key
         - --config-path=/config/contour.yaml
         command: ["contour"]
-        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:98c6e286d9052a1f86c61d64a4a43ecaebb5bd3b2370f883e1bf0629c7d80acd
+        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:69f3a2d31b2a14e8cabea4ed38ac83845ff7920c31a0d836a494383307737926
         imagePullPolicy: Always
         name: contour
         ports:
@@ -1764,7 +1764,7 @@ spec:
         args:
         - envoy
         - shutdown-manager
-        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:98c6e286d9052a1f86c61d64a4a43ecaebb5bd3b2370f883e1bf0629c7d80acd
+        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:69f3a2d31b2a14e8cabea4ed38ac83845ff7920c31a0d836a494383307737926
         imagePullPolicy: Always
         lifecycle:
           preStop:
@@ -1840,7 +1840,7 @@ spec:
         - --envoy-key-file=/certs/tls.key
         command:
         - contour
-        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:98c6e286d9052a1f86c61d64a4a43ecaebb5bd3b2370f883e1bf0629c7d80acd
+        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:69f3a2d31b2a14e8cabea4ed38ac83845ff7920c31a0d836a494383307737926
         imagePullPolicy: Always
         name: envoy-initconfig
         volumeMounts:
@@ -3305,7 +3305,7 @@ spec:
         # if we change this on each version, you can no longer upgrade
         # just by applying the deployment YAML.
         # See #2423, #2395, #2150, and #2030 for earlier questions about this.
-        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:98c6e286d9052a1f86c61d64a4a43ecaebb5bd3b2370f883e1bf0629c7d80acd
+        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:69f3a2d31b2a14e8cabea4ed38ac83845ff7920c31a0d836a494383307737926
         imagePullPolicy: Always
         command:
         - contour
@@ -3545,7 +3545,7 @@ spec:
         - --contour-key-file=/certs/tls.key
         - --config-path=/config/contour.yaml
         command: ["contour"]
-        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:98c6e286d9052a1f86c61d64a4a43ecaebb5bd3b2370f883e1bf0629c7d80acd
+        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:69f3a2d31b2a14e8cabea4ed38ac83845ff7920c31a0d836a494383307737926
         imagePullPolicy: Always
         name: contour
         ports:
@@ -3637,7 +3637,7 @@ spec:
         args:
         - envoy
         - shutdown-manager
-        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:98c6e286d9052a1f86c61d64a4a43ecaebb5bd3b2370f883e1bf0629c7d80acd
+        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:69f3a2d31b2a14e8cabea4ed38ac83845ff7920c31a0d836a494383307737926
         imagePullPolicy: Always
         lifecycle:
           preStop:
@@ -3713,7 +3713,7 @@ spec:
         - --envoy-key-file=/certs/tls.key
         command:
         - contour
-        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:98c6e286d9052a1f86c61d64a4a43ecaebb5bd3b2370f883e1bf0629c7d80acd
+        image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:69f3a2d31b2a14e8cabea4ed38ac83845ff7920c31a0d836a494383307737926
         imagePullPolicy: Always
         name: envoy-initconfig
         volumeMounts:
diff --git a/third_party/contour-latest/net-contour.yaml b/third_party/contour-latest/net-contour.yaml
index b1f2d80b5..f2ae8a3e1 100644
--- a/third_party/contour-latest/net-contour.yaml
+++ b/third_party/contour-latest/net-contour.yaml
@@ -35,7 +35,7 @@ metadata:
   namespace: knative-serving
   labels:
     networking.knative.dev/ingress-provider: contour
-    serving.knative.dev/release: "v20200529-fe56655"
+    serving.knative.dev/release: "v20200601-b5d9707"
 data:
   _example: |
     ################################
@@ -94,7 +94,7 @@ spec:
       - name: controller
         # This is the Go import path for the binary that is containerized
         # and substituted here.
-        image: gcr.io/knative-nightly/knative.dev/net-contour/cmd/controller@sha256:5b641e5af854d7ec4cd75f8fe2793f26c4d3d68f408bdcdfc0c633ebdc96ccb9
+        image: gcr.io/knative-nightly/knative.dev/net-contour/cmd/controller@sha256:a00111f41edd33143e10295aa5423fa28a38006262f20a58586ed1eddb161f63
         resources:
           requests:
             cpu: 40m
diff --git a/third_party/kourier-latest/kourier.yaml b/third_party/kourier-latest/kourier.yaml
index 7c2d3803b..a7f4ef466 100644
--- a/third_party/kourier-latest/kourier.yaml
+++ b/third_party/kourier-latest/kourier.yaml
@@ -166,7 +166,7 @@ spec:
         app: 3scale-kourier-control
     spec:
       containers:
-      - image: gcr.io/knative-nightly/knative.dev/net-kourier/cmd/kourier@sha256:22440cb909517838192858eb32596a951daad55af95e3b76475dac76b6800b66
+      - image: gcr.io/knative-nightly/knative.dev/net-kourier/cmd/kourier@sha256:3eaf148ca1da1637a84c39e0aa6423abefdaacacc0a7739ec7a4422b8b4c83a0
         imagePullPolicy: Always
         name: kourier-control
         env: