Repository: knative/serving
Pull request #10201 (2020-11-25 21:19:33)

Remove the error from release.
It is not checked right now, nor there is an easy way to check it (also
what to do with it even if it were checked?)

In relality this is a programming error most likely, so let's panic to
ensure it's noticed and corrected

/assign @julz @markusthoemmes 

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

This pull-request has been approved by: *<a href="https://github.com/knative/serving/pull/10201#" title="Author self-approved">vagababov</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

- ~~[pkg/queue/OWNERS](https://github.com/knative/serving/blob/master/pkg/queue/OWNERS)~~ [vagababov]

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":[]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/knative/serving/pull/10201?src=pr&el=h1) Report
> Merging [#10201](https://codecov.io/gh/knative/serving/pull/10201?src=pr&el=desc) (85e48f5) into [master](https://codecov.io/gh/knative/serving/commit/dfbd645d0ad4cab5b32697b7fbf2421211f6d36c?el=desc) (dfbd645) will **decrease** coverage by `0.06%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/knative/serving/pull/10201/graphs/tree.svg?width=650&height=150&src=pr&token=MPpDfAj4Ui)](https://codecov.io/gh/knative/serving/pull/10201?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master   #10201      +/-   ##
==========================================
- Coverage   88.18%   88.11%   -0.07%     
==========================================
  Files         184      184              
  Lines        8611     8611              
==========================================
- Hits         7594     7588       -6     
- Misses        783      787       +4     
- Partials      234      236       +2     
```


| [Impacted Files](https://codecov.io/gh/knative/serving/pull/10201?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/queue/breaker.go](https://codecov.io/gh/knative/serving/pull/10201/diff?src=pr&el=tree#diff-cGtnL3F1ZXVlL2JyZWFrZXIuZ28=) | `96.42% <100.00%> (ø)` | |
| [pkg/reconciler/configuration/configuration.go](https://codecov.io/gh/knative/serving/pull/10201/diff?src=pr&el=tree#diff-cGtnL3JlY29uY2lsZXIvY29uZmlndXJhdGlvbi9jb25maWd1cmF0aW9uLmdv) | `86.71% <0.00%> (-1.57%)` | :arrow_down: |
| [pkg/reconciler/autoscaling/kpa/scaler.go](https://codecov.io/gh/knative/serving/pull/10201/diff?src=pr&el=tree#diff-cGtnL3JlY29uY2lsZXIvYXV0b3NjYWxpbmcva3BhL3NjYWxlci5nbw==) | `88.57% <0.00%> (-1.43%)` | :arrow_down: |
| [pkg/activator/net/revision\_backends.go](https://codecov.io/gh/knative/serving/pull/10201/diff?src=pr&el=tree#diff-cGtnL2FjdGl2YXRvci9uZXQvcmV2aXNpb25fYmFja2VuZHMuZ28=) | `90.49% <0.00%> (-0.91%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/knative/serving/pull/10201?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/knative/serving/pull/10201?src=pr&el=footer). Last update [dfbd645...6fedb63](https://codecov.io/gh/knative/serving/pull/10201?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/pkg/queue/breaker.go b/pkg/queue/breaker.go
index c81efc296..0d89e6572 100644
--- a/pkg/queue/breaker.go
+++ b/pkg/queue/breaker.go
@@ -238,13 +238,13 @@ func (s *semaphore) acquire(ctx context.Context) error {
 // release releases capacity in the semaphore.
 // If the semaphore capacity was reduced in between and as a result inFlight is greater
 // than capacity, we don't wake up goroutines as they'd not get any capacity anyway.
-func (s *semaphore) release() error {
+func (s *semaphore) release() {
 	for {
 		old := s.state.Load()
 		capacity, in := unpack(old)
 
 		if in == 0 {
-			return ErrRelease
+			panic("release and acquire are not paired")
 		}
 
 		in--
@@ -260,7 +260,7 @@ func (s *semaphore) release() error {
 					// can take, which is guaranteed to be enough.
 				}
 			}
-			return nil
+			return
 		}
 	}
 }
diff --git a/pkg/reconciler/domainmapping/reconciler.go b/pkg/reconciler/domainmapping/reconciler.go
index 261350004..a76d05fe9 100644
--- a/pkg/reconciler/domainmapping/reconciler.go
+++ b/pkg/reconciler/domainmapping/reconciler.go
@@ -84,14 +84,14 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, dm *v1alpha1.DomainMappi
 	}
 
 	// Resolve the spec.Ref to a URI following the Addressable contract.
-	target, err := r.resolveRef(ctx, dm)
+	targetHost, targetBackendSvc, err := r.resolveRef(ctx, dm)
 	if err != nil {
 		return err
 	}
 
 	// Reconcile the Ingress resource corresponding to the requested Mapping.
-	logger.Debugf("Mapping %s to %s/%s", url, dm.Spec.Ref.Namespace, dm.Spec.Ref.Name)
-	desired := resources.MakeIngress(dm, target.Host, ingressClass)
+	logger.Debugf("Mapping %s to ref %s/%s (host: %q, svc: %q)", url, dm.Spec.Ref.Namespace, dm.Spec.Ref.Name, targetHost, targetBackendSvc)
+	desired := resources.MakeIngress(dm, targetBackendSvc, targetHost, ingressClass)
 	ingress, err := r.reconcileIngress(ctx, dm, desired)
 	if err != nil {
 		return err
@@ -138,26 +138,43 @@ func (r *Reconciler) reconcileIngress(ctx context.Context, dm *v1alpha1.DomainMa
 	return ingress, err
 }
 
-func (r *Reconciler) resolveRef(ctx context.Context, dm *v1alpha1.DomainMapping) (*apis.URL, error) {
+func (r *Reconciler) resolveRef(ctx context.Context, dm *v1alpha1.DomainMapping) (host, backendSvc string, err error) {
 	resolved, err := r.resolver.URIFromKReference(ctx, &dm.Spec.Ref, dm)
 	if err != nil {
 		dm.Status.MarkReferenceNotResolved(err.Error())
-		return nil, fmt.Errorf("resolving reference: %w", err)
+		return "", "", fmt.Errorf("resolving reference: %w", err)
 	}
 
+	// Since we do not support path-based routing in domain mappings, we cannot
+	// support target references that contain a path.
 	if resolved.Path != "" {
 		dm.Status.MarkReferenceNotResolved(fmt.Sprintf("resolved URI %q contains a path", resolved))
-		return nil, fmt.Errorf("resolved URI %q contains a path", resolved)
+		return "", "", fmt.Errorf("resolved URI %q contains a path", resolved)
 	}
 
-	suffix := ".svc." + network.GetClusterDomainName()
-	if !strings.HasSuffix(resolved.Host, suffix) {
-		dm.Status.MarkReferenceNotResolved(fmt.Sprintf("resolved URI %q must end in %q", resolved, suffix))
-		return nil, fmt.Errorf("resolved URI %q must end in %q", resolved, suffix)
+	// The resolved hostname must be of the form {name}.{namespace}.svc.{suffix},
+	// which is the standard DNS address given by kubernetes to services. We will
+	// use `name` from this resolved hostname to determine the backend service
+	// name for the KIngress.
+	// TODO(julz) in the future we may support addressables that are not created
+	// from Services, in which case we would need to dynamically create the
+	// an ExternalName Service for the KIngress to use.
+	requiredSuffix := ".svc." + network.GetClusterDomainName()
+	parts := strings.Split(strings.TrimSuffix(resolved.Host, requiredSuffix), ".")
+	if !strings.HasSuffix(resolved.Host, requiredSuffix) || len(parts) != 2 {
+		dm.Status.MarkReferenceNotResolved(fmt.Sprintf("resolved URI %q must be of the form {name}.{namespace}%s", resolved, requiredSuffix))
+		return "", "", fmt.Errorf("resolved URI %q must be of the form {name}.{namespace}%s", resolved, requiredSuffix)
+	}
+
+	// If the namespace part of the target isn't the same as the DomainMapping
+	// we'd need to create a cross-namespace KIngress, which isn't supported.
+	if parts[1] != dm.Namespace {
+		dm.Status.MarkReferenceNotResolved(fmt.Sprintf("resolved URI %q must be in same namespace as DomainMapping", resolved))
+		return "", "", fmt.Errorf("resolved URI %q must be in same namespace as DomainMapping", resolved)
 	}
 
 	dm.Status.MarkReferenceResolved()
-	return resolved, nil
+	return resolved.Host, parts[0], nil
 }
 
 func (r *Reconciler) reconcileDomainClaim(ctx context.Context, dm *v1alpha1.DomainMapping) error {
diff --git a/pkg/reconciler/domainmapping/resources/ingress.go b/pkg/reconciler/domainmapping/resources/ingress.go
index 2e55ab677..99411dc59 100644
--- a/pkg/reconciler/domainmapping/resources/ingress.go
+++ b/pkg/reconciler/domainmapping/resources/ingress.go
@@ -28,13 +28,12 @@ import (
 	servingv1alpha1 "knative.dev/serving/pkg/apis/serving/v1alpha1"
 )
 
-// MakeIngress creates an Ingress object for a DomainMapping.
-func MakeIngress(dm *servingv1alpha1.DomainMapping, hostName, ingressClass string) *netv1alpha1.Ingress {
-	var (
-		targetServiceName      = dm.Spec.Ref.Name
-		targetServiceNamespace = dm.Spec.Ref.Namespace
-	)
-
+// MakeIngress creates an Ingress object for a DomainMapping.  The Ingress is
+// always created in the same namespace as the DomainMapping, and the ingress
+// backend is always in the same namespace also (as this is required by
+// KIngress).  The created ingress will contain a RewriteHost rule to cause the
+// given hostName to be used as the host.
+func MakeIngress(dm *servingv1alpha1.DomainMapping, backendServiceName, hostName, ingressClass string) *netv1alpha1.Ingress {
 	return &netv1alpha1.Ingress{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      kmeta.ChildName(dm.GetName(), ""),
@@ -59,8 +58,8 @@ func MakeIngress(dm *servingv1alpha1.DomainMapping, hostName, ingressClass strin
 								network.OriginalHostHeader: dm.Name,
 							},
 							IngressBackend: netv1alpha1.IngressBackend{
-								ServiceName:      targetServiceName,
-								ServiceNamespace: targetServiceNamespace,
+								ServiceNamespace: dm.Namespace,
+								ServiceName:      backendServiceName,
 								ServicePort:      intstr.FromInt(80),
 							},
 						}},
diff --git a/pkg/reconciler/route/traffic/rollout.go b/pkg/reconciler/route/traffic/rollout.go
index 02eb3df9c..1c790481e 100644
--- a/pkg/reconciler/route/traffic/rollout.go
+++ b/pkg/reconciler/route/traffic/rollout.go
@@ -60,6 +60,11 @@ type ConfigurationRollout struct {
 	// the Rollout shoud be complete.
 	Deadline int `json:"deadline,omitempty"`
 
+	// StartTime is the Unix timestamp by when (+/- reconcile precision)
+	// the Rollout has started.
+	// This is required to compute step time and deadline.
+	StartTime int `json:"starttime,omitempty"`
+
 	// LastStepTimeStamp is the Unix timestamp when the last
 	// rollout step was performed.
 	LastStepTime int `json:"lastStep,omitempty"`
@@ -80,6 +85,13 @@ type RevisionRollout struct {
 	Percent int `json:"percent"`
 }
 
+// Done returns true if there is no active rollout going on
+// for the configuration.
+func (cr *ConfigurationRollout) Done() bool {
+	// Zero or just one revision.
+	return len(cr.Revisions) < 2
+}
+
 // Validate validates current rollout for inconsistencies.
 // This is expected to be invoked after annotation deserialization.
 // If it returns false — the deserialized object should be discarded.
@@ -185,6 +197,8 @@ func (cur *Rollout) Step(prev *Rollout) *Rollout {
 // the older revisions.
 func adjustPercentage(goal int, cr *ConfigurationRollout) {
 	switch diff := goal - cr.Percent; {
+	case goal == 0:
+		cr.Revisions = nil // No traffic, no rollout.
 	case diff > 0:
 		cr.Revisions[len(cr.Revisions)-1].Percent += diff
 	case diff < 0:
@@ -235,6 +249,7 @@ func stepConfig(goal, prev *ConfigurationRollout) *ConfigurationRollout {
 			ret.Deadline = prev.Deadline
 			ret.LastStepTime = prev.LastStepTime
 			ret.StepDuration = prev.StepDuration
+			ret.StartTime = prev.StartTime
 		}
 		return ret
 	}


Test patch:
diff --git a/pkg/queue/breaker_test.go b/pkg/queue/breaker_test.go
index ce8ead764..5a6539fce 100644
--- a/pkg/queue/breaker_test.go
+++ b/pkg/queue/breaker_test.go
@@ -18,7 +18,6 @@ package queue
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"testing"
 	"time"
@@ -275,12 +274,22 @@ func TestSemaphoreAcquireHasCapacity(t *testing.T) {
 func TestSemaphoreRelease(t *testing.T) {
 	sem := newSemaphore(1, 1)
 	sem.acquire(context.Background())
-	if err := sem.release(); err != nil {
-		t.Errorf("release = %v; want: %v", err, nil)
-	}
-	if err := sem.release(); !errors.Is(err, ErrRelease) {
-		t.Errorf("release = %v; want: %v", err, ErrRelease)
-	}
+	func() {
+		defer func() {
+			if e := recover(); e != nil {
+				t.Error("Expected no panic, got message:", e)
+			}
+			sem.release()
+		}()
+	}()
+	func() {
+		defer func() {
+			if e := recover(); e == nil {
+				t.Error("Expected panic, but got none")
+			}
+		}()
+		sem.release()
+	}()
 }
 
 func TestSemaphoreUpdateCapacity(t *testing.T) {
diff --git a/pkg/reconciler/domainmapping/resources/ingress_test.go b/pkg/reconciler/domainmapping/resources/ingress_test.go
index b1261fe0e..c3120d451 100644
--- a/pkg/reconciler/domainmapping/resources/ingress_test.go
+++ b/pkg/reconciler/domainmapping/resources/ingress_test.go
@@ -48,7 +48,7 @@ func TestMakeIngress(t *testing.T) {
 		},
 	}
 
-	got := MakeIngress(dm, "the-rewrite-host", "the-ingress-class")
+	got := MakeIngress(dm, "the-target-svc", "the-rewrite-host", "the-ingress-class")
 
 	want := &netv1alpha1.Ingress{
 		ObjectMeta: metav1.ObjectMeta{
@@ -73,7 +73,7 @@ func TestMakeIngress(t *testing.T) {
 								network.OriginalHostHeader: "mapping.com",
 							},
 							IngressBackend: netv1alpha1.IngressBackend{
-								ServiceName:      "the-name",
+								ServiceName:      "the-target-svc",
 								ServiceNamespace: "the-namespace",
 								ServicePort:      intstr.FromInt(80),
 							},
diff --git a/pkg/reconciler/domainmapping/table_test.go b/pkg/reconciler/domainmapping/table_test.go
index f101f007f..37d5bf42c 100644
--- a/pkg/reconciler/domainmapping/table_test.go
+++ b/pkg/reconciler/domainmapping/table_test.go
@@ -64,7 +64,7 @@ func TestReconcile(t *testing.T) {
 		Name: "first reconcile",
 		Key:  "default/first-reconcile.com",
 		Objects: []runtime.Object{
-			ksvc("default", "target", "the-target-svc.svc.cluster.local", ""),
+			ksvc("default", "target", "the-target-svc.default.svc.cluster.local", ""),
 			domainMapping("default", "first-reconcile.com", withRef("default", "target")),
 		},
 		WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
@@ -81,7 +81,7 @@ func TestReconcile(t *testing.T) {
 		SkipNamespaceValidation: true, // allow creation of ClusterDomainClaim.
 		WantCreates: []runtime.Object{
 			resources.MakeDomainClaim(domainMapping("default", "first-reconcile.com", withRef("default", "target"))),
-			resources.MakeIngress(domainMapping("default", "first-reconcile.com", withRef("default", "target")), "the-target-svc.svc.cluster.local", "the-ingress-class"),
+			resources.MakeIngress(domainMapping("default", "first-reconcile.com", withRef("default", "target")), "the-target-svc", "the-target-svc.default.svc.cluster.local", "the-ingress-class"),
 		},
 		WantEvents: []string{
 			Eventf(corev1.EventTypeNormal, "Created", "Created Ingress %q", "first-reconcile.com"),
@@ -150,7 +150,7 @@ func TestReconcile(t *testing.T) {
 				withAddress("http", "first-reconcile.com"),
 				withInitDomainMappingConditions,
 				withDomainClaimed,
-				withReferenceNotResolved(`resolved URI "http://notasvc.cluster.local" must end in ".svc.cluster.local"`),
+				withReferenceNotResolved(`resolved URI "http://notasvc.cluster.local" must be of the form {name}.{namespace}.svc.cluster.local`),
 			),
 		}},
 		SkipNamespaceValidation: true, // allow creation of ClusterDomainClaim.
@@ -158,13 +158,38 @@ func TestReconcile(t *testing.T) {
 			resources.MakeDomainClaim(domainMapping("default", "first-reconcile.com", withRef("default", "target"))),
 		},
 		WantEvents: []string{
-			Eventf(corev1.EventTypeWarning, "InternalError", `resolved URI "http://notasvc.cluster.local" must end in ".svc.cluster.local"`),
+			Eventf(corev1.EventTypeWarning, "InternalError", `resolved URI "http://notasvc.cluster.local" must be of the form {name}.{namespace}.svc.cluster.local`),
+		},
+	}, {
+		Name: "first reconcile, resolved URL in wrong namespace",
+		Key:  "default/first-reconcile.com",
+		Objects: []runtime.Object{
+			ksvc("default", "target", "name.anothernamespace.svc.cluster.local", ""),
+			domainMapping("default", "first-reconcile.com", withRef("default", "target")),
+		},
+		WantErr: true,
+		WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
+			Object: domainMapping("default", "first-reconcile.com",
+				withRef("default", "target"),
+				withURL("http", "first-reconcile.com"),
+				withAddress("http", "first-reconcile.com"),
+				withInitDomainMappingConditions,
+				withDomainClaimed,
+				withReferenceNotResolved(`resolved URI "http://name.anothernamespace.svc.cluster.local" must be in same namespace as DomainMapping`),
+			),
+		}},
+		SkipNamespaceValidation: true, // allow creation of ClusterDomainClaim.
+		WantCreates: []runtime.Object{
+			resources.MakeDomainClaim(domainMapping("default", "first-reconcile.com", withRef("default", "target"))),
+		},
+		WantEvents: []string{
+			Eventf(corev1.EventTypeWarning, "InternalError", `resolved URI "http://name.anothernamespace.svc.cluster.local" must be in same namespace as DomainMapping`),
 		},
 	}, {
 		Name: "first reconcile, pre-owned domain claim",
 		Key:  "default/first-reconcile.com",
 		Objects: []runtime.Object{
-			ksvc("default", "target", "the-target-svc.svc.cluster.local", ""),
+			ksvc("default", "target", "the-target-svc.default.svc.cluster.local", ""),
 			domainMapping("default", "first-reconcile.com", withRef("default", "target")),
 			resources.MakeDomainClaim(domainMapping("default", "first-reconcile.com", withRef("default", "target"))),
 		},
@@ -180,7 +205,7 @@ func TestReconcile(t *testing.T) {
 			),
 		}},
 		WantCreates: []runtime.Object{
-			resources.MakeIngress(domainMapping("default", "first-reconcile.com", withRef("default", "target")), "the-target-svc.svc.cluster.local", "the-ingress-class"),
+			resources.MakeIngress(domainMapping("default", "first-reconcile.com", withRef("default", "target")), "the-target-svc", "the-target-svc.default.svc.cluster.local", "the-ingress-class"),
 		},
 		WantEvents: []string{
 			Eventf(corev1.EventTypeNormal, "Created", "Created Ingress %q", "first-reconcile.com"),
@@ -239,7 +264,7 @@ func TestReconcile(t *testing.T) {
 		Name: "reconcile with ingressClass annotation",
 		Key:  "default/ingressclass.first-reconcile.com",
 		Objects: []runtime.Object{
-			ksvc("default", "target", "the-target-svc.svc.cluster.local", ""),
+			ksvc("default", "target", "the-target-svc.default.svc.cluster.local", ""),
 			domainMapping("default", "ingressclass.first-reconcile.com", withRef("default", "target"),
 				withAnnotations(map[string]string{
 					networking.IngressClassAnnotationKey: "overridden-ingress-class",
@@ -264,7 +289,7 @@ func TestReconcile(t *testing.T) {
 		WantCreates: []runtime.Object{
 			resources.MakeDomainClaim(domainMapping("default", "ingressclass.first-reconcile.com", withRef("default", "target"))),
 			resources.MakeIngress(domainMapping("default", "ingressclass.first-reconcile.com", withRef("default", "target")),
-				"the-target-svc.svc.cluster.local", "overridden-ingress-class"),
+				"the-target-svc", "the-target-svc.default.svc.cluster.local", "overridden-ingress-class"),
 		},
 		WantEvents: []string{
 			Eventf(corev1.EventTypeNormal, "Created", "Created Ingress %q", "ingressclass.first-reconcile.com"),
@@ -273,10 +298,10 @@ func TestReconcile(t *testing.T) {
 		Name: "reconcile changed ref",
 		Key:  "default/ingress-exists.org",
 		Objects: []runtime.Object{
-			ksvc("default", "changed", "the-target-svc.svc.cluster.local", ""),
+			ksvc("default", "changed", "changed.default.svc.cluster.local", ""),
 			domainMapping("default", "ingress-exists.org", withRef("default", "changed")),
-			resources.MakeIngress(domainMapping("default", "ingress-exists.org", withRef("default", "target")), "the-target-svc.svc.cluster.local", "the-ingress-class"),
-			resources.MakeDomainClaim(domainMapping("default", "ingress-exists.org", withRef("default", "target"))),
+			resources.MakeIngress(domainMapping("default", "ingress-exists.org", withRef("default", "changed")), "previous", "previous.default.svc.cluster.local", "the-ingress-class"),
+			resources.MakeDomainClaim(domainMapping("default", "ingress-exists.org", withRef("default", "changed"))),
 		},
 		WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
 			Object: domainMapping("default", "ingress-exists.org",
@@ -296,7 +321,7 @@ func TestReconcile(t *testing.T) {
 		Name: "reconcile failed ingress",
 		Key:  "default/ingress-failed.me",
 		Objects: []runtime.Object{
-			ksvc("default", "failed", "the-target-svc.svc.cluster.local", ""),
+			ksvc("default", "failed", "failed.default.svc.cluster.local", ""),
 			domainMapping("default", "ingress-failed.me",
 				withRef("default", "failed"),
 				withURL("http", "ingress-failed.me"),
@@ -305,7 +330,7 @@ func TestReconcile(t *testing.T) {
 			ingress(domainMapping("default", "ingress-failed.me", withRef("default", "failed")), "the-ingress-class",
 				WithLoadbalancerFailed("fell over", "hurt myself"),
 			),
-			resources.MakeDomainClaim(domainMapping("default", "ingress-failed.me", withRef("default", "target"))),
+			resources.MakeDomainClaim(domainMapping("default", "ingress-failed.me", withRef("default", "failed"))),
 		},
 		WantStatusUpdates: []clientgotesting.UpdateActionImpl{{
 			Object: domainMapping("default", "ingress-failed.me",
@@ -315,14 +340,14 @@ func TestReconcile(t *testing.T) {
 				withInitDomainMappingConditions,
 				withDomainClaimed,
 				withReferenceResolved,
-				withPropagatedStatus(ingress(domainMapping("default", "ingress-failed.me"), "", WithLoadbalancerFailed("fell over", "hurt myself")).Status),
+				withPropagatedStatus(ingress(domainMapping("default", "failed.default.svc.cluster.local"), "", WithLoadbalancerFailed("fell over", "hurt myself")).Status),
 			),
 		}},
 	}, {
 		Name: "reconcile unknown ingress",
 		Key:  "default/ingress-unknown.me",
 		Objects: []runtime.Object{
-			ksvc("default", "unknown", "the-target-svc.svc.cluster.local", ""),
+			ksvc("default", "unknown", "unknown.default.svc.cluster.local", ""),
 			domainMapping("default", "ingress-unknown.me", withRef("default", "unknown"),
 				withRef("default", "unknown"),
 				withURL("http", "ingress-unknown.me"),
@@ -348,7 +373,7 @@ func TestReconcile(t *testing.T) {
 		Name: "reconcile ready ingress",
 		Key:  "default/ingress-ready.me",
 		Objects: []runtime.Object{
-			ksvc("default", "ready", "the-target-svc.svc.cluster.local", ""),
+			ksvc("default", "ready", "ready.default.svc.cluster.local", ""),
 			domainMapping("default", "ingress-ready.me",
 				withRef("default", "ready"),
 				withURL("http", "ingress-ready.me"),
@@ -377,7 +402,7 @@ func TestReconcile(t *testing.T) {
 			InduceFailure("create", "ingresses"),
 		},
 		Objects: []runtime.Object{
-			ksvc("default", "cantcreate", "the-target-svc.svc.cluster.local", ""),
+			ksvc("default", "cantcreate", "cantcreate.default.svc.cluster.local", ""),
 			domainMapping("default", "cantcreate.this",
 				withRef("default", "cantcreate"),
 				withURL("http", "cantcreate.this"),
@@ -418,7 +443,7 @@ func TestReconcile(t *testing.T) {
 			InduceFailure("update", "ingresses"),
 		},
 		Objects: []runtime.Object{
-			ksvc("default", "cantupdate", "the-target-svc.svc.cluster.local", ""),
+			ksvc("default", "cantupdate", "cantupdate.default.svc.cluster.local", ""),
 			domainMapping("default", "cantupdate.this",
 				withRef("default", "cantupdate"),
 				withURL("http", "cantupdate.this"),
@@ -569,7 +594,7 @@ func withObservedGeneration(dm *v1alpha1.DomainMapping) {
 }
 
 func ingress(dm *v1alpha1.DomainMapping, ingressClass string, opt ...IngressOption) *netv1alpha1.Ingress {
-	ing := resources.MakeIngress(dm, "the-target-svc.svc.cluster.local", ingressClass)
+	ing := resources.MakeIngress(dm, dm.Spec.Ref.Name, dm.Spec.Ref.Name+"."+dm.Spec.Ref.Namespace+".svc.cluster.local", ingressClass)
 	for _, o := range opt {
 		o(ing)
 	}
diff --git a/pkg/reconciler/route/traffic/rollout_test.go b/pkg/reconciler/route/traffic/rollout_test.go
index 5ae626f63..447e97aad 100644
--- a/pkg/reconciler/route/traffic/rollout_test.go
+++ b/pkg/reconciler/route/traffic/rollout_test.go
@@ -22,9 +22,11 @@ limitations under the License.
 package traffic
 
 import (
+	"encoding/json"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 )
 
 func TestStep(t *testing.T) {
@@ -77,6 +79,7 @@ func TestStep(t *testing.T) {
 				Deadline:     2006, // <- Those should be ignored.
 				LastStepTime: 2009,
 				StepDuration: 2020,
+				StartTime:    2004,
 			}},
 		},
 		prev: &Rollout{
@@ -90,6 +93,7 @@ func TestStep(t *testing.T) {
 				Deadline:     1982, // <- Those should be copied.
 				LastStepTime: 1984,
 				StepDuration: 1988,
+				StartTime:    1955,
 			}},
 		},
 		want: &Rollout{
@@ -103,6 +107,7 @@ func TestStep(t *testing.T) {
 				Deadline:     1982,
 				LastStepTime: 1984,
 				StepDuration: 1988,
+				StartTime:    1955,
 			}},
 		},
 	}, {
@@ -673,7 +678,7 @@ func TestStep(t *testing.T) {
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
 			got := tc.cur.Step(tc.prev)
-			if want := tc.want; !cmp.Equal(got, want) {
+			if want := tc.want; !cmp.Equal(got, want, cmpopts.EquateEmpty()) {
 				t.Errorf("Wrong rolled rollout, diff(-want,+got):\n%s", cmp.Diff(want, got))
 			}
 			if !got.Validate() {
@@ -798,12 +803,11 @@ func TestAdjustPercentage(t *testing.T) {
 				Percent: 60,
 			}},
 		},
-		want: []RevisionRollout{},
 	}}
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
 			adjustPercentage(tc.goal, tc.prev)
-			if got, want := tc.prev.Revisions, tc.want; !cmp.Equal(got, want) {
+			if got, want := tc.prev.Revisions, tc.want; !cmp.Equal(got, want, cmpopts.EquateEmpty()) {
 				t.Errorf("Rollout Mistmatch(-want,+got):\n%s", cmp.Diff(want, got))
 			}
 		})
@@ -884,3 +888,84 @@ func TestValidateFailures(t *testing.T) {
 	}
 
 }
+
+func TestConfigDone(t *testing.T) {
+	r := &Rollout{
+		Configurations: []ConfigurationRollout{{
+			ConfigurationName: "one",
+			Percent:           100,
+			Revisions: []RevisionRollout{{
+				RevisionName: "roy",
+				Percent:      100,
+			}},
+		}, {
+			ConfigurationName: "no",
+			Percent:           0,
+			Revisions:         []RevisionRollout{},
+		}, {
+			ConfigurationName: "many",
+			Percent:           100,
+			Revisions: []RevisionRollout{{
+				RevisionName: "black-on-blue",
+				Percent:      83,
+			}, {
+				RevisionName: "flowers",
+				Percent:      17,
+			}},
+		}},
+	}
+	if !r.Configurations[0].Done() {
+		t.Error("Single revision rollout is not `Done`")
+	}
+	if !r.Configurations[1].Done() {
+		t.Error("Zero revisions rollout is not `Done`")
+	}
+	if r.Configurations[2].Done() {
+		t.Error("Many revisions rollout is `Done`")
+	}
+}
+
+func TestJSONRoundtrip(t *testing.T) {
+	orig := &Rollout{
+		Configurations: []ConfigurationRollout{{
+			ConfigurationName: "one",
+			Percent:           100,
+			Revisions: []RevisionRollout{{
+				RevisionName: "roy",
+				Percent:      100,
+			}},
+			StartTime:    1955,
+			Deadline:     2006,
+			LastStepTime: 1988,
+			StepDuration: 1984,
+		}, {
+			ConfigurationName: "no",
+			Percent:           0,
+			Revisions:         []RevisionRollout{},
+		}, {
+			ConfigurationName: "many",
+			Percent:           100,
+			Revisions: []RevisionRollout{{
+				RevisionName: "black-on-blue",
+				Percent:      83,
+			}, {
+				RevisionName: "flowers",
+				Percent:      17,
+			}},
+		}},
+	}
+
+	ss, err := json.Marshal(orig)
+	if err != nil {
+		t.Fatal("Error serializing the rollout:", err)
+	}
+	deserialized := &Rollout{}
+	err = json.Unmarshal(ss, deserialized)
+	if err != nil {
+		t.Fatal("Error deserializing proper JSON:", err)
+	}
+	if !cmp.Equal(deserialized, orig, cmpopts.EquateEmpty()) {
+		t.Errorf("JSON roundtrip mismatch:(-want,+got)\n%s",
+			cmp.Diff(orig, deserialized, cmpopts.EquateEmpty()))
+	}
+}
diff --git a/third_party/cert-manager-latest/net-certmanager.yaml b/third_party/cert-manager-latest/net-certmanager.yaml
index 413d5d173..70f3b92a4 100644
--- a/third_party/cert-manager-latest/net-certmanager.yaml
+++ b/third_party/cert-manager-latest/net-certmanager.yaml
@@ -18,7 +18,7 @@ metadata:
   # These are the permissions needed by the `cert-manager` `Certificate` implementation.
   name: knative-serving-certmanager
   labels:
-    serving.knative.dev/release: "v20201123-e9883b4"
+    serving.knative.dev/release: "v20201125-ee5fc18"
     serving.knative.dev/controller: "true"
     networking.knative.dev/certificate-provider: cert-manager
 rules:
@@ -49,7 +49,7 @@ kind: ValidatingWebhookConfiguration
 metadata:
   name: config.webhook.net-certmanager.networking.internal.knative.dev
   labels:
-    serving.knative.dev/release: "v20201123-e9883b4"
+    serving.knative.dev/release: "v20201125-ee5fc18"
 webhooks:
   - admissionReviewVersions:
       - v1beta1
@@ -86,7 +86,7 @@ metadata:
   name: net-certmanager-webhook-certs
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20201123-e9883b4"
+    serving.knative.dev/release: "v20201125-ee5fc18"
 
 ---
 # Copyright 2020 The Knative Authors
@@ -109,7 +109,7 @@ metadata:
   name: config-certmanager
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20201123-e9883b4"
+    serving.knative.dev/release: "v20201125-ee5fc18"
     networking.knative.dev/certificate-provider: cert-manager
 data:
   _example: |
@@ -156,7 +156,7 @@ metadata:
   name: networking-certmanager
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20201123-e9883b4"
+    serving.knative.dev/release: "v20201125-ee5fc18"
     networking.knative.dev/certificate-provider: cert-manager
 spec:
   selector:
@@ -168,14 +168,14 @@ spec:
         cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
       labels:
         app: networking-certmanager
-        serving.knative.dev/release: "v20201123-e9883b4"
+        serving.knative.dev/release: "v20201125-ee5fc18"
     spec:
       serviceAccountName: controller
       containers:
         - name: networking-certmanager
           # This is the Go import path for the binary that is containerized
           # and substituted here.
-          image: gcr.io/knative-nightly/knative.dev/net-certmanager/cmd/controller@sha256:6d41cdcc90749699a1290a15cba33fb5589a9ab0e689e1e1e44533958dd9f1fc
+          image: gcr.io/knative-nightly/knative.dev/net-certmanager/cmd/controller@sha256:4ac5f96218fe6f9ae768d5986320be888657ae9410a0f5ca4d6078ba483187eb
           resources:
             requests:
               cpu: 30m
@@ -213,7 +213,7 @@ kind: Service
 metadata:
   labels:
     app: networking-certmanager
-    serving.knative.dev/release: "v20201123-e9883b4"
+    serving.knative.dev/release: "v20201125-ee5fc18"
     networking.knative.dev/certificate-provider: cert-manager
   name: networking-certmanager
   namespace: knative-serving
@@ -250,7 +250,7 @@ metadata:
   name: net-certmanager-webhook
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20201123-e9883b4"
+    serving.knative.dev/release: "v20201125-ee5fc18"
 spec:
   selector:
     matchLabels:
@@ -263,14 +263,14 @@ spec:
       labels:
         app: net-certmanager-webhook
         role: net-certmanager-webhook
-        serving.knative.dev/release: "v20201123-e9883b4"
+        serving.knative.dev/release: "v20201125-ee5fc18"
     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-certmanager/cmd/webhook@sha256:2b7c3b36d98d2cfa9b5c4a9fe4ccc726755f08b33b4f415d7e997d760a6d0590
+          image: gcr.io/knative-nightly/knative.dev/net-certmanager/cmd/webhook@sha256:60a35390498ecc84c7dc5a612c48cbe3ef3fa9f19c191b4f6068a4da23d5b5d1
           resources:
             requests:
               cpu: 20m
@@ -329,7 +329,7 @@ metadata:
   namespace: knative-serving
   labels:
     role: net-certmanager-webhook
-    serving.knative.dev/release: "v20201123-e9883b4"
+    serving.knative.dev/release: "v20201125-ee5fc18"
 spec:
   ports:
     # Define metrics and profiling for them to be accessible within service meshes.