Repository: knative/serving
Pull request #9975 (2020-10-28 21:53:47)

Initial version of rollout code
The code below does the chaining of the revisions and assigns 1% to the new revision.
It does not yet do progressive part aside from that yet (but nothing acts on it, so no harm done either).
Since we remove 0% issues, successive rollouts won't make the annotation expand in an unlimited way.

In the followup PRs I will be expanding the API for the progressive logic and actual implementation thereof.

/assign @tcnghia mattmoor

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

This pull-request has been approved by: *<a href="https://github.com/knative/serving/pull/9975#" 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/reconciler/route/OWNERS](https://github.com/knative/serving/blob/master/pkg/reconciler/route/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/9975?src=pr&el=h1) Report
> Merging [#9975](https://codecov.io/gh/knative/serving/pull/9975?src=pr&el=desc) into [master](https://codecov.io/gh/knative/serving/commit/83fa0f1781965f17ba8b7e8c6970fd9206b84a7e?el=desc) will **increase** coverage by `0.07%`.
> The diff coverage is `100.00%`.

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

```diff
@@            Coverage Diff             @@
##           master    #9975      +/-   ##
==========================================
+ Coverage   87.78%   87.86%   +0.07%     
==========================================
  Files         183      184       +1     
  Lines        8631     8667      +36     
==========================================
+ Hits         7577     7615      +38     
+ Misses        803      801       -2     
  Partials      251      251              
```


| [Impacted Files](https://codecov.io/gh/knative/serving/pull/9975?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/reconciler/revision/resources/queue.go](https://codecov.io/gh/knative/serving/pull/9975/diff?src=pr&el=tree#diff-cGtnL3JlY29uY2lsZXIvcmV2aXNpb24vcmVzb3VyY2VzL3F1ZXVlLmdv) | `100.00% <ø> (ø)` | |
| [pkg/reconciler/route/traffic/rollout.go](https://codecov.io/gh/knative/serving/pull/9975/diff?src=pr&el=tree#diff-cGtnL3JlY29uY2lsZXIvcm91dGUvdHJhZmZpYy9yb2xsb3V0Lmdv) | `100.00% <100.00%> (ø)` | |
| [pkg/activator/net/revision\_backends.go](https://codecov.io/gh/knative/serving/pull/9975/diff?src=pr&el=tree#diff-cGtnL2FjdGl2YXRvci9uZXQvcmV2aXNpb25fYmFja2VuZHMuZ28=) | `91.40% <0.00%> (+0.90%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/knative/serving/pull/9975?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/9975?src=pr&el=footer). Last update [83fa0f1...bdba711](https://codecov.io/gh/knative/serving/pull/9975?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

vagababov: Alright, we chatted offline (or rather online) and agreed to try c=a+b approach to see if the code/api would read better.

Patch:
diff --git a/go.mod b/go.mod
index 6ed420cf3..5769e13f9 100644
--- a/go.mod
+++ b/go.mod
@@ -32,10 +32,10 @@ require (
 	k8s.io/client-go v11.0.1-0.20190805182717-6502b5e7b1b5+incompatible
 	k8s.io/code-generator v0.18.8
 	k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29
-	knative.dev/caching v0.0.0-20201029014134-01b051e4fb9a
+	knative.dev/caching v0.0.0-20201029015435-f2bc7482b39c
 	knative.dev/hack v0.0.0-20201028205534-fe80f1c8af68
-	knative.dev/networking v0.0.0-20201028132534-429a6210295c
-	knative.dev/pkg v0.0.0-20201028171634-6a24d54694a8
+	knative.dev/networking v0.0.0-20201029015534-8ec2eff034d5
+	knative.dev/pkg v0.0.0-20201029122234-6d905b3f84a6
 )
 
 replace (
diff --git a/go.sum b/go.sum
index 986c02779..ccd3f5166 100644
--- a/go.sum
+++ b/go.sum
@@ -63,8 +63,6 @@ contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e
 contrib.go.opencensus.io/exporter/stackdriver v0.12.8/go.mod h1:XyyafDnFOsqoxHJgTFycKZMrRUrPThLh2iYTJF6uoO0=
 contrib.go.opencensus.io/exporter/stackdriver v0.12.9-0.20191108183826-59d068f8d8ff/go.mod h1:XyyafDnFOsqoxHJgTFycKZMrRUrPThLh2iYTJF6uoO0=
 contrib.go.opencensus.io/exporter/stackdriver v0.13.1/go.mod h1:z2tyTZtPmQ2HvWH4cOmVDgtY+1lomfKdbLnkJvZdc8c=
-contrib.go.opencensus.io/exporter/stackdriver v0.13.2 h1:5lKLBwUuq4S6pTbYaBtWmnay3eJfKNS3qL8M8HM5fM4=
-contrib.go.opencensus.io/exporter/stackdriver v0.13.2/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
 contrib.go.opencensus.io/exporter/stackdriver v0.13.4 h1:ksUxwH3OD5sxkjzEqGxNTl+Xjsmu3BnC/300MhSVTSc=
 contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
 contrib.go.opencensus.io/exporter/zipkin v0.1.1 h1:PR+1zWqY8ceXs1qDQQIlgXe+sdiwCf0n32bH4+Epk8g=
@@ -1546,7 +1544,6 @@ golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f h1:Fqb3ao1hUmOR3GkUOg/Y+BadLwykBIzs5q8Ez2SbHyc=
 golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88=
@@ -1659,7 +1656,6 @@ golang.org/x/tools v0.0.0-20200709181711-e327e1019dfe/go.mod h1:njjCfa9FT2d7l9Bc
 golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
 golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3 h1:DywqrEscRX7O2phNjkT0L6lhHKGBoMLCNX+XcAe7t6s=
 golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
@@ -1701,8 +1697,6 @@ google.golang.org/api v0.26.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
 google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
 google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
 google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.31.0 h1:1w5Sz/puhxFo9lTtip2n47k7toB/U2nCqOKNHd3Yrbo=
-google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQWYWo=
 google.golang.org/api v0.34.0 h1:k40adF3uR+6x/+hO5Dh4ZFUqFp67vxvbpafFiJxl10A=
 google.golang.org/api v0.34.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -1764,7 +1758,6 @@ google.golang.org/genproto v0.0.0-20200709005830-7a2ca40e9dc3/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d h1:92D1fum1bJLKSdr11OJ+54YeCMCGYIygTA7R/YZxH5M=
 google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
@@ -1939,17 +1932,16 @@ k8s.io/utils v0.0.0-20200603063816-c1c6865ac451 h1:v8ud2Up6QK1lNOKFgiIVrZdMg7Mpm
 k8s.io/utils v0.0.0-20200603063816-c1c6865ac451/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 knative.dev/caching v0.0.0-20190719140829-2032732871ff/go.mod h1:dHXFU6CGlLlbzaWc32g80cR92iuBSpsslDNBWI8C7eg=
 knative.dev/caching v0.0.0-20200116200605-67bca2c83dfa/go.mod h1:dHXFU6CGlLlbzaWc32g80cR92iuBSpsslDNBWI8C7eg=
-knative.dev/caching v0.0.0-20201029014134-01b051e4fb9a h1:9KckyR/DgQrDzk104l+W7/D5KReVlQmpgo78loHGk+g=
-knative.dev/caching v0.0.0-20201029014134-01b051e4fb9a/go.mod h1:r+x6zHVrYO3z3Ofr+nG2JYvdnGF5c0xpLfy9/FNuLs8=
+knative.dev/caching v0.0.0-20201029015435-f2bc7482b39c h1:UlSMHBBViWBi8wD4RPNLtdSXMfaSSEqlJwfrRRuUbww=
+knative.dev/caching v0.0.0-20201029015435-f2bc7482b39c/go.mod h1:s+IpuPu1pgzRrpFXi8zQ4bketnjwrC31OFgopPhN9f8=
 knative.dev/eventing-contrib v0.6.1-0.20190723221543-5ce18048c08b/go.mod h1:SnXZgSGgMSMLNFTwTnpaOH7hXDzTFtw0J8OmHflNx3g=
 knative.dev/eventing-contrib v0.11.2/go.mod h1:SnXZgSGgMSMLNFTwTnpaOH7hXDzTFtw0J8OmHflNx3g=
-knative.dev/hack v0.0.0-20201027201633-1763a666eb41/go.mod h1:PHt8x8yX5Z9pPquBEfIj0X66f8iWkWfR0S/sarACJrI=
 knative.dev/hack v0.0.0-20201027221733-0d7f2f064b7b h1:tBUkvMhcWZny+7CJTs5Ona8S3OCKJkgAwgZ7mkgWL3Y=
 knative.dev/hack v0.0.0-20201027221733-0d7f2f064b7b/go.mod h1:PHt8x8yX5Z9pPquBEfIj0X66f8iWkWfR0S/sarACJrI=
 knative.dev/hack v0.0.0-20201028205534-fe80f1c8af68 h1:ygwpOK8B0PXurq88GHhym2uxKiiMJ+bRFwFeZHqGJNc=
 knative.dev/hack v0.0.0-20201028205534-fe80f1c8af68/go.mod h1:PHt8x8yX5Z9pPquBEfIj0X66f8iWkWfR0S/sarACJrI=
-knative.dev/networking v0.0.0-20201028132534-429a6210295c h1:3UMmcYV2+tqaV5lURSbOHApqzWiZXegoN9o/EM1/uTU=
-knative.dev/networking v0.0.0-20201028132534-429a6210295c/go.mod h1:196CqlYZecTICLDG5Vww19dI/cAyGsHDKF+LpHKvbC4=
+knative.dev/networking v0.0.0-20201029015534-8ec2eff034d5 h1:FNe0LkW3GJLsXWvcOukL1SbbX9cFRuyx73abEDA2mTc=
+knative.dev/networking v0.0.0-20201029015534-8ec2eff034d5/go.mod h1:GLUFkkEsNCMHSXZDGHG0EFpiDHkWZWDvWJurG3VV8do=
 knative.dev/pkg v0.0.0-20191101194912-56c2594e4f11/go.mod h1:pgODObA1dTyhNoFxPZTTjNWfx6F0aKsKzn+vaT9XO/Q=
 knative.dev/pkg v0.0.0-20191111150521-6d806b998379/go.mod h1:pgODObA1dTyhNoFxPZTTjNWfx6F0aKsKzn+vaT9XO/Q=
 knative.dev/pkg v0.0.0-20200207155214-fef852970f43/go.mod h1:pgODObA1dTyhNoFxPZTTjNWfx6F0aKsKzn+vaT9XO/Q=
@@ -1958,10 +1950,10 @@ knative.dev/pkg v0.0.0-20200505191044-3da93ebb24c2/go.mod h1:Q6sL35DdGs8hIQZKdaC
 knative.dev/pkg v0.0.0-20200515002500-16d7b963416f/go.mod h1:tMOHGbxtRz8zYFGEGpV/bpoTEM1o89MwYFC4YJXl3GY=
 knative.dev/pkg v0.0.0-20200528142800-1c6815d7e4c9/go.mod h1:QgNZTxnwpB/oSpNcfnLVlw+WpEwwyKAvJlvR3hgeltA=
 knative.dev/pkg v0.0.0-20200711004937-22502028e31a/go.mod h1:AqAJV6rYi8IGikDjJ/9ZQd9qKdkXVlesVnVjwx62YB8=
-knative.dev/pkg v0.0.0-20201028121234-58d43f05028e h1:hUiaQjWDH2zye0rdVbkEpXQE4UWETEd+H7TGht7iems=
-knative.dev/pkg v0.0.0-20201028121234-58d43f05028e/go.mod h1:2K9UlkXASBzY9j0aDoOnqs1xsNi+hz/39X6xVap+nlE=
 knative.dev/pkg v0.0.0-20201028171634-6a24d54694a8 h1:mby4d9HiUPk9J0X6hjKD1Gvye1OJSWsX2Y5lzUCGUxA=
 knative.dev/pkg v0.0.0-20201028171634-6a24d54694a8/go.mod h1:gSgUoSIvJZoCHurWwZJQHiVDTvMkAJd6OFe8KwtzdX0=
+knative.dev/pkg v0.0.0-20201029122234-6d905b3f84a6 h1:J7VRiR73Mnc7UeiPo0mCPBIZVYcBBKZO+wPqFc8nlrM=
+knative.dev/pkg v0.0.0-20201029122234-6d905b3f84a6/go.mod h1:G805KH4tlnB6CIVHWIpE82pjx3/+2r9gUqNSOa78tns=
 knative.dev/test-infra v0.0.0-20200407185800-1b88cb3b45a5/go.mod h1:xcdUkMJrLlBswIZqL5zCuBFOC22WIPMQoVX1L35i0vQ=
 knative.dev/test-infra v0.0.0-20200505052144-5ea2f705bb55/go.mod h1:WqF1Azka+FxPZ20keR2zCNtiQA1MP9ZB4BH4HuI+SIU=
 knative.dev/test-infra v0.0.0-20200513011557-d03429a76034/go.mod h1:aMif0KXL4g19YCYwsy4Ocjjz5xgPlseYV+B95Oo4JGE=
diff --git a/pkg/reconciler/route/traffic/rollout.go b/pkg/reconciler/route/traffic/rollout.go
index 32e4afcde..460c392fe 100644
--- a/pkg/reconciler/route/traffic/rollout.go
+++ b/pkg/reconciler/route/traffic/rollout.go
@@ -21,7 +21,11 @@ limitations under the License.
 
 package traffic
 
-import "knative.dev/networking/pkg/apis/networking"
+import (
+	"sort"
+
+	"knative.dev/networking/pkg/apis/networking"
+)
 
 // RolloutAnnotationKey is the annotation key for storing
 // the rollout state in the Annotations of the Kingress or Route.Status.
@@ -33,6 +37,7 @@ const RolloutAnnotationKey = networking.GroupName + "/rollout"
 // There may be several rollouts going on at the same time for the
 // same configuration if there is a tag configured traffic target.
 type Rollout struct {
+	// Configurations are sorted by tag first and within same tag, by configuration name.
 	Configurations []ConfigurationRollout `json:"configurations,omitempty"`
 }
 
@@ -70,3 +75,134 @@ type RevisionRollout struct {
 	// target percentage.
 	Percent int `json:"percent"`
 }
+
+// Step merges this rollout object with the previous state and
+// returns a new Rollout object representing the merged state.
+// At the end of the call the returned object will contain the
+// desired traffic shape.
+// Step will return cur if no previous state was available.
+func (cur *Rollout) Step(prev *Rollout) *Rollout {
+	if prev == nil || len(prev.Configurations) == 0 {
+		return cur
+	}
+
+	// The algorithm below is simplest, but probably not the most performant.
+	// TODO: optimize in the later passes.
+
+	// Map the configs by tag.
+	currConfigs, prevConfigs := map[string][]*ConfigurationRollout{}, map[string][]*ConfigurationRollout{}
+	for i, cfg := range cur.Configurations {
+		currConfigs[cfg.Tag] = append(currConfigs[cfg.Tag], &cur.Configurations[i])
+	}
+	for i, cfg := range prev.Configurations {
+		prevConfigs[cfg.Tag] = append(prevConfigs[cfg.Tag], &prev.Configurations[i])
+	}
+
+	var ret []ConfigurationRollout
+	for t, ccfgs := range currConfigs {
+		pcfgs, ok := prevConfigs[t]
+		// A new tag was added, so we have no previous state to roll from,
+		// thus just add it to the return, we'll rollout to 100% from the get go (and it is
+		// always 100%, since default tag is _always_ there).
+		// So just append to the return list.
+		if !ok {
+			ret = append(ret, *ccfgs[0])
+			continue
+		}
+		// This is basically an intersect algorithm,
+		// It relies on the fact that inputs are sorted.
+		for i, j := 0, 0; i < len(ccfgs) && j < len(pcfgs); {
+			switch {
+			case j >= len(pcfgs):
+				// Those are the new configs that were added during this reconciliation.
+				// So we just copy them to the result.
+				ret = append(ret, *ccfgs[i])
+			case ccfgs[i].ConfigurationName == pcfgs[j].ConfigurationName:
+				// Config might have 0% traffic assigned, if it is a tag only route (i.e.
+				// receives no traffic via default tag).
+				if ccfgs[i].Percent != 0 {
+					ret = append(ret, *stepConfig(ccfgs[i], pcfgs[j]))
+				}
+				i++
+				j++
+			case ccfgs[i].ConfigurationName < pcfgs[j].ConfigurationName:
+				// A new config, has been added. No action for rollout though.
+				ret = append(ret, *ccfgs[i])
+				i++
+			default: // cur > prev.
+				// A config has been removed during this update.
+				// Again, no action for rollout, since this will no longer
+				// be rolling it out (or sending traffic to it overall).
+				j++
+			}
+		}
+	}
+	ro := &Rollout{Configurations: ret}
+	// We need to sort the rollout, since we have map iterations in between,
+	// which are random.
+	sortRollout(ro)
+	return ro
+}
+
+// stepConfig takes previous and goal configuration shapes and returns a new
+// config rollout, after computing the percetage allocations.
+func stepConfig(goal, prev *ConfigurationRollout) *ConfigurationRollout {
+	pc := len(prev.Revisions)
+	ret := &ConfigurationRollout{
+		ConfigurationName: goal.ConfigurationName,
+		Tag:               goal.Tag,
+		Percent:           goal.Percent,
+	}
+	// goal will always have just one revision in the list – the current desired revision.
+	if goal.Revisions[0].RevisionName == prev.Revisions[pc-1].RevisionName {
+		// TODO(vagababov): here would go the logic to compute new percentages for the rollout,
+		// i.e step function, so return value will change, depending on that.
+		// TODO(vagababov): percentage might change, so this should trigger recompute of existing
+		// revision rollouts.
+		ret.Revisions = goal.Revisions
+		return ret
+	}
+
+	// Go backwards and find first revision with traffic assignment > 0.
+	// Reduce it by one, so we can give that 1% to the new revision.
+	// By design we drain newest revision first.
+	for i := len(prev.Revisions) - 1; i >= 0; i-- {
+		if prev.Revisions[i].Percent > 0 {
+			prev.Revisions[i].Percent--
+			break
+		}
+	}
+
+	// Allocate optimistically.
+	out := make([]RevisionRollout, 0, len(prev.Revisions)+1)
+
+	// Copy the non 0% objects over.
+	for _, r := range prev.Revisions {
+		if r.Percent == 0 {
+			// Skip the zeroed out items. This can be the 1%->0% from above, but
+			// generally speaking should not happen otherwise, aside from
+			// users modifying the annotation manually.
+			continue
+		}
+		out = append(out, r)
+	}
+
+	// Append the new revision, to the list of previous ones.
+	// This is how we start the rollout.
+	goalRev := goal.Revisions[0]
+	goalRev.Percent = 1
+	ret.Revisions = append(out, goalRev)
+	return ret
+}
+
+// sortRollout sorts the rollout based on tag so it's consistent
+// from run to run, since input to the process is map iterator.
+func sortRollout(r *Rollout) {
+	sort.Slice(r.Configurations, func(i, j int) bool {
+		// Sort by tag and within tag sort by config name.
+		if r.Configurations[i].Tag == r.Configurations[j].Tag {
+			return r.Configurations[i].ConfigurationName < r.Configurations[j].ConfigurationName
+		}
+		return r.Configurations[i].Tag < r.Configurations[j].Tag
+	})
+}
diff --git a/pkg/reconciler/route/traffic/traffic.go b/pkg/reconciler/route/traffic/traffic.go
index 4a57c7f65..05e6036b9 100644
--- a/pkg/reconciler/route/traffic/traffic.go
+++ b/pkg/reconciler/route/traffic/traffic.go
@@ -19,7 +19,6 @@ package traffic
 import (
 	"context"
 	"errors"
-	"sort"
 
 	corev1 "k8s.io/api/core/v1"
 	apierrs "k8s.io/apimachinery/pkg/api/errors"
@@ -180,18 +179,6 @@ func (cfg *Config) BuildRollout() *Rollout {
 	return rollout
 }
 
-// sortRollout sorts the rollout based on tag so it's consistent
-// from run to run, since input to the process is map iterator.
-func sortRollout(r *Rollout) {
-	sort.Slice(r.Configurations, func(i, j int) bool {
-		// Sort by tag and within tag sort by config name.
-		if r.Configurations[i].Tag == r.Configurations[j].Tag {
-			return r.Configurations[i].ConfigurationName < r.Configurations[j].ConfigurationName
-		}
-		return r.Configurations[i].Tag < r.Configurations[j].Tag
-	})
-}
-
 // buildRolloutForTag builds the current rollout state.
 // It is expected to be invoked after applySpecTraffic.
 // TODO(vagababov): actually deal with rollouts, vs just report desired state.
diff --git a/vendor/knative.dev/pkg/apis/duck/verify.go b/vendor/knative.dev/pkg/apis/duck/verify.go
index cbcc51117..e3f5abab4 100644
--- a/vendor/knative.dev/pkg/apis/duck/verify.go
+++ b/vendor/knative.dev/pkg/apis/duck/verify.go
@@ -79,17 +79,17 @@ func roundTrip(instance interface{}, input, output Populatable) error {
 	// Serialize the input to JSON and deserialize that into the provided instance
 	// of the type that we are checking.
 	if before, err := json.Marshal(input); err != nil {
-		return fmt.Errorf("error serializing duck type %T error: %s", input, err)
+		return fmt.Errorf("error serializing duck type %T error: %w", input, err)
 	} else if err := json.Unmarshal(before, instance); err != nil {
-		return fmt.Errorf("error deserializing duck type %T into %T error: %s", input, instance, err)
+		return fmt.Errorf("error deserializing duck type %T into %T error: %w", input, instance, err)
 	}
 
 	// Serialize the instance we are checking to JSON and deserialize that into the
 	// output resource.
 	if after, err := json.Marshal(instance); err != nil {
-		return fmt.Errorf("error serializing %T error: %s", instance, err)
+		return fmt.Errorf("error serializing %T error: %w", instance, err)
 	} else if err := json.Unmarshal(after, output); err != nil {
-		return fmt.Errorf("error deserializing %T into duck type %T error: %s", instance, output, err)
+		return fmt.Errorf("error deserializing %T into duck type %T error: %w", instance, output, err)
 	}
 
 	return nil
diff --git a/vendor/knative.dev/pkg/controller/controller.go b/vendor/knative.dev/pkg/controller/controller.go
index 8ccee7a14..b0ecb2e9b 100644
--- a/vendor/knative.dev/pkg/controller/controller.go
+++ b/vendor/knative.dev/pkg/controller/controller.go
@@ -582,6 +582,7 @@ func IsPermanentError(err error) bool {
 // Is implements the Is() interface of error. It returns whether the target
 // error can be treated as equivalent to a permanentError.
 func (permanentError) Is(target error) bool {
+	//nolint: errorlint // This check is actually fine.
 	_, ok := target.(permanentError)
 	return ok
 }
diff --git a/vendor/knative.dev/pkg/depcheck/depcheck.go b/vendor/knative.dev/pkg/depcheck/depcheck.go
index 8febf632f..14d5c6674 100644
--- a/vendor/knative.dev/pkg/depcheck/depcheck.go
+++ b/vendor/knative.dev/pkg/depcheck/depcheck.go
@@ -104,7 +104,7 @@ func buildGraph(importpaths ...string) (graph, error) {
 func CheckNoDependency(ip string, banned []string) error {
 	g, err := buildGraph(ip)
 	if err != nil {
-		return fmt.Errorf("buildGraph(queue) = %v", err)
+		return fmt.Errorf("buildGraph(queue) = %w", err)
 	}
 	for _, dip := range banned {
 		if g.contains(dip) {
@@ -136,7 +136,7 @@ func AssertNoDependency(t *testing.T, banned map[string][]string) {
 func CheckOnlyDependencies(ip string, allowed map[string]struct{}) error {
 	g, err := buildGraph(ip)
 	if err != nil {
-		return fmt.Errorf("buildGraph(queue) = %v", err)
+		return fmt.Errorf("buildGraph(queue) = %w", err)
 	}
 	for _, name := range g.order() {
 		if _, ok := allowed[name]; !ok {
diff --git a/vendor/knative.dev/pkg/injection/sharedmain/main.go b/vendor/knative.dev/pkg/injection/sharedmain/main.go
index f9b66efb4..72cab9496 100644
--- a/vendor/knative.dev/pkg/injection/sharedmain/main.go
+++ b/vendor/knative.dev/pkg/injection/sharedmain/main.go
@@ -18,7 +18,9 @@ package sharedmain
 
 import (
 	"context"
+	"errors"
 	"flag"
+	"fmt"
 	"log"
 	"net/http"
 	"os"
@@ -73,12 +75,12 @@ func GetLoggingConfig(ctx context.Context) (*logging.Config, error) {
 	var loggingConfigMap *corev1.ConfigMap
 	// These timeout and retry interval are set by heuristics.
 	// e.g. istio sidecar needs a few seconds to configure the pod network.
+	var lastErr error
 	if err := wait.PollImmediate(1*time.Second, 5*time.Second, func() (bool, error) {
-		var err error
-		loggingConfigMap, err = kubeclient.Get(ctx).CoreV1().ConfigMaps(system.Namespace()).Get(ctx, logging.ConfigMapName(), metav1.GetOptions{})
-		return err == nil || apierrors.IsNotFound(err), nil
+		loggingConfigMap, lastErr = kubeclient.Get(ctx).CoreV1().ConfigMaps(system.Namespace()).Get(ctx, logging.ConfigMapName(), metav1.GetOptions{})
+		return lastErr == nil || apierrors.IsNotFound(lastErr), nil
 	}); err != nil {
-		return nil, err
+		return nil, fmt.Errorf("timed out waiting for the condition: %w", lastErr)
 	}
 	if loggingConfigMap == nil {
 		return logging.NewConfigFromMap(nil)
@@ -241,7 +243,7 @@ func MainWithConfig(ctx context.Context, component string, cfg *rest.Config, cto
 
 	profilingServer.Shutdown(context.Background())
 	// Don't forward ErrServerClosed as that indicates we're already shutting down.
-	if err := eg.Wait(); err != nil && err != http.ErrServerClosed {
+	if err := eg.Wait(); err != nil && !errors.Is(err, http.ErrServerClosed) {
 		logger.Errorw("Error while running server", zap.Error(err))
 	}
 }
diff --git a/vendor/knative.dev/pkg/logging/config.go b/vendor/knative.dev/pkg/logging/config.go
index 5162eec5c..2b5eb696d 100644
--- a/vendor/knative.dev/pkg/logging/config.go
+++ b/vendor/knative.dev/pkg/logging/config.go
@@ -212,7 +212,7 @@ func UpdateLevelFromConfigMap(logger *zap.SugaredLogger, atomicLevel zap.AtomicL
 			// reset to global level
 			loggingCfg, err := zapConfigFromJSON(config.LoggingConfig)
 			switch {
-			case err == errEmptyLoggerConfig:
+			case errors.Is(err, errEmptyLoggerConfig):
 				level = zap.NewAtomicLevel().Level()
 			case err != nil:
 				logger.With(zap.Error(err)).Errorf("Failed to parse logger configuration. "+
diff --git a/vendor/knative.dev/pkg/metrics/opencensus_exporter.go b/vendor/knative.dev/pkg/metrics/opencensus_exporter.go
index 403449e23..eaeac4a56 100644
--- a/vendor/knative.dev/pkg/metrics/opencensus_exporter.go
+++ b/vendor/knative.dev/pkg/metrics/opencensus_exporter.go
@@ -85,7 +85,7 @@ func getOpenCensusSecret(component string, lister SecretFetcher) (*corev1.Secret
 		secret, err = lister("opencensus")
 	}
 	if err != nil {
-		return nil, fmt.Errorf("unable to fetch opencensus secret for %q, cannot use requireSecure=true: %+v", component, err)
+		return nil, fmt.Errorf("unable to fetch opencensus secret for %q, cannot use requireSecure=true: %w", component, err)
 	}
 
 	return secret, nil
diff --git a/vendor/knative.dev/pkg/metrics/stackdriver_exporter.go b/vendor/knative.dev/pkg/metrics/stackdriver_exporter.go
index 101d96bb2..ab14ef266 100644
--- a/vendor/knative.dev/pkg/metrics/stackdriver_exporter.go
+++ b/vendor/knative.dev/pkg/metrics/stackdriver_exporter.go
@@ -363,7 +363,7 @@ func getStackdriverSecret(ctx context.Context, secretFetcher SecretFetcher) (*co
 	}
 
 	if secErr != nil {
-		return nil, fmt.Errorf("error getting Secret [%v] in namespace [%v]: %v", secretName, secretNamespace, secErr)
+		return nil, fmt.Errorf("error getting Secret [%v] in namespace [%v]: %w", secretName, secretNamespace, secErr)
 	}
 
 	return sec, nil
diff --git a/vendor/knative.dev/pkg/network/transports.go b/vendor/knative.dev/pkg/network/transports.go
index 64ce11845..c37b99d57 100644
--- a/vendor/knative.dev/pkg/network/transports.go
+++ b/vendor/knative.dev/pkg/network/transports.go
@@ -18,6 +18,7 @@ package network
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net"
 	"net/http"
@@ -76,7 +77,8 @@ func dialBackOffHelper(ctx context.Context, network, address string, bo wait.Bac
 	for {
 		c, err := dialer.DialContext(ctx, network, address)
 		if err != nil {
-			if err, ok := err.(net.Error); ok && err.Timeout() {
+			var errNet net.Error
+			if errors.As(err, &errNet) && errNet.Timeout() {
 				if bo.Steps < 1 {
 					break
 				}
diff --git a/vendor/knative.dev/pkg/reconciler/events.go b/vendor/knative.dev/pkg/reconciler/events.go
index 54a6e52a2..611ba2dc1 100644
--- a/vendor/knative.dev/pkg/reconciler/events.go
+++ b/vendor/knative.dev/pkg/reconciler/events.go
@@ -75,7 +75,8 @@ var _ error = (*ReconcilerEvent)(nil)
 // Is returns if the target error is a ReconcilerEvent type checking that
 // EventType and Reason match.
 func (e *ReconcilerEvent) Is(target error) bool {
-	if t, ok := target.(*ReconcilerEvent); ok {
+	var t *ReconcilerEvent
+	if errors.As(target, &t) {
 		if t != nil && t.EventType == e.EventType && t.Reason == e.Reason {
 			return true
 		}
diff --git a/vendor/knative.dev/pkg/webhook/webhook.go b/vendor/knative.dev/pkg/webhook/webhook.go
index ba04b8e1a..ba37d385b 100644
--- a/vendor/knative.dev/pkg/webhook/webhook.go
+++ b/vendor/knative.dev/pkg/webhook/webhook.go
@@ -209,7 +209,7 @@ func (wh *Webhook) Run(stop <-chan struct{}) error {
 
 	eg, ctx := errgroup.WithContext(ctx)
 	eg.Go(func() error {
-		if err := server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
+		if err := server.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) {
 			logger.Errorw("ListenAndServeTLS for admission webhook returned error", zap.Error(err))
 			return err
 		}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index e8d3c356a..8aa2e7642 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -951,7 +951,7 @@ k8s.io/utils/buffer
 k8s.io/utils/integer
 k8s.io/utils/pointer
 k8s.io/utils/trace
-# knative.dev/caching v0.0.0-20201029014134-01b051e4fb9a
+# knative.dev/caching v0.0.0-20201029015435-f2bc7482b39c
 ## explicit
 knative.dev/caching/config
 knative.dev/caching/pkg/apis/caching
@@ -975,7 +975,7 @@ knative.dev/caching/pkg/client/listers/caching/v1alpha1
 # knative.dev/hack v0.0.0-20201028205534-fe80f1c8af68
 ## explicit
 knative.dev/hack
-# knative.dev/networking v0.0.0-20201028132534-429a6210295c
+# knative.dev/networking v0.0.0-20201029015534-8ec2eff034d5
 ## explicit
 knative.dev/networking/config
 knative.dev/networking/pkg
@@ -1008,7 +1008,7 @@ knative.dev/networking/test/conformance/ingress
 knative.dev/networking/test/defaultsystem
 knative.dev/networking/test/test_images/grpc-ping/proto
 knative.dev/networking/test/types
-# knative.dev/pkg v0.0.0-20201028171634-6a24d54694a8
+# knative.dev/pkg v0.0.0-20201029122234-6d905b3f84a6
 ## explicit
 knative.dev/pkg/apiextensions/storageversion
 knative.dev/pkg/apiextensions/storageversion/cmd/migrate


Test patch:
diff --git a/pkg/activator/handler/handler_test.go b/pkg/activator/handler/handler_test.go
index 12b184e30..ea35298e5 100644
--- a/pkg/activator/handler/handler_test.go
+++ b/pkg/activator/handler/handler_test.go
@@ -85,7 +85,6 @@ func TestActivationHandler(t *testing.T) {
 		name:      "active endpoint",
 		wantBody:  wantBody,
 		wantCode:  http.StatusOK,
-		wantErr:   nil,
 		throttler: fakeThrottler{},
 	}, {
 		name:      "request error",
@@ -97,13 +96,11 @@ func TestActivationHandler(t *testing.T) {
 		name:      "throttler timeout",
 		wantBody:  context.DeadlineExceeded.Error() + "\n",
 		wantCode:  http.StatusServiceUnavailable,
-		wantErr:   nil,
 		throttler: fakeThrottler{err: context.DeadlineExceeded},
 	}, {
 		name:      "overflow",
 		wantBody:  "pending request queue full\n",
 		wantCode:  http.StatusServiceUnavailable,
-		wantErr:   nil,
 		throttler: fakeThrottler{err: queue.ErrRequestQueueFull},
 	}}
 	for _, test := range tests {
@@ -151,7 +148,7 @@ func TestActivationHandler(t *testing.T) {
 				t.Fatal("Error reading body:", err)
 			}
 			if string(gotBody) != test.wantBody {
-				t.Errorf("Unexpected response body. Response body %q, want %q", gotBody, test.wantBody)
+				t.Errorf("Response body = %q, want: %q", gotBody, test.wantBody)
 			}
 		})
 	}
@@ -183,10 +180,10 @@ func TestActivationHandlerProxyHeader(t *testing.T) {
 	select {
 	case httpReq := <-interceptCh:
 		if got := httpReq.Header.Get(network.ProxyHeaderName); got != activator.Name {
-			t.Errorf("Header %q = %q, want:  %q", network.ProxyHeaderName, got, activator.Name)
+			t.Errorf("Header %q = %q, want: %q", network.ProxyHeaderName, got, activator.Name)
 		}
 	case <-time.After(1 * time.Second):
-		t.Fatal("Timed out waiting for a request to be intercepted")
+		t.Error("Timed out waiting for a request to be intercepted")
 	}
 }
 
@@ -201,16 +198,15 @@ func TestActivationHandlerTraceSpans(t *testing.T) {
 		traceBackend: tracingconfig.Zipkin,
 	}, {
 		name:         "trace disabled",
-		wantSpans:    0,
 		traceBackend: tracingconfig.None,
 	}}
 
+	spanNames := []string{"throttler_try", "/", "activator_proxy"}
 	for _, tc := range testcases {
 		t.Run(tc.name, func(t *testing.T) {
 			// Setup transport
 			fakeRT := activatortest.FakeRoundTripper{
 				RequestResponse: &activatortest.FakeResponse{
-					Err:  nil,
 					Code: http.StatusOK,
 					Body: wantBody,
 				},
@@ -256,13 +252,12 @@ func TestActivationHandlerTraceSpans(t *testing.T) {
 
 			gotSpans := reporter.Flush()
 			if len(gotSpans) != tc.wantSpans {
-				t.Errorf("Got %d spans, expected %d", len(gotSpans), tc.wantSpans)
+				t.Errorf("NumSpans = %d, want: %d", len(gotSpans), tc.wantSpans)
 			}
 
-			spanNames := []string{"throttler_try", "/", "activator_proxy"}
 			for i, spanName := range spanNames[0:tc.wantSpans] {
 				if gotSpans[i].Name != spanName {
-					t.Errorf("Got span %d named %q, expected %q", i, gotSpans[i].Name, spanName)
+					t.Errorf("Span[%d] = %q, expected %q", i, gotSpans[i].Name, spanName)
 				}
 			}
 		})
@@ -303,7 +298,7 @@ func setupConfigStore(t *testing.T, logger *zap.SugaredLogger) *activatorconfig.
 
 func BenchmarkHandler(b *testing.B) {
 	ctx, cancel, _ := rtesting.SetupFakeContextWithCancel(&testing.T{})
-	defer cancel()
+	b.Cleanup(cancel)
 	configStore := setupConfigStore(&testing.T{}, logging.FromContext(ctx))
 
 	// bodyLength is in kilobytes.
@@ -374,15 +369,13 @@ type responseRecorder struct {
 	size atomic.Int32
 }
 
-func (rr *responseRecorder) Flush() {}
-
 func (rr *responseRecorder) Header() http.Header {
 	return http.Header{}
 }
 
 func (rr *responseRecorder) Write(p []byte) (int, error) {
 	rr.size.Add(int32(len(p)))
-	return ioutil.Discard.Write(p)
+	return len(p), nil
 }
 
 func (rr *responseRecorder) WriteHeader(code int) {
diff --git a/pkg/reconciler/route/traffic/rollout_test.go b/pkg/reconciler/route/traffic/rollout_test.go
new file mode 100644
index 000000000..d9bd0281f
--- /dev/null
+++ b/pkg/reconciler/route/traffic/rollout_test.go
@@ -0,0 +1,327 @@
+/*
+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
+
+    http://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.
+*/
+
+// rollout.go contains the types and functions to deal with
+// gradual rollout of the new revision for a configuration target.
+// The types in this file are expected to be serialized as strings
+// and used as annotations for progressive rollout logic.
+
+package traffic
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestRoll(t *testing.T) {
+	tests := []struct {
+		name            string
+		prev, cur, want *Rollout
+	}{{
+		name: "no prev",
+		cur:  &Rollout{},
+		prev: nil,
+		want: &Rollout{},
+	}, {
+		name: "simplest, same",
+		cur: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "let-it-bleed",
+					Percent:      100,
+				}},
+			}},
+		},
+		prev: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "let-it-bleed",
+					Percent:      100,
+				}},
+			}},
+		},
+		want: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "let-it-bleed",
+					Percent:      100,
+				}},
+			}},
+		},
+	}, {
+		name: "simplest, roll",
+		cur: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "let-it-bleed",
+					Percent:      100,
+				}},
+			}},
+		},
+		prev: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "goat-head-soup",
+					Percent:      100,
+				}},
+			}},
+		},
+		want: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "goat-head-soup",
+					Percent:      99,
+				}, {
+					RevisionName: "let-it-bleed",
+					Percent:      1,
+				}},
+			}},
+		},
+	}, {
+		name: "roll with two existing, no deletes",
+		cur: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "sticky-fingers",
+					Percent:      100,
+				}},
+			}},
+		},
+		prev: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "goat-head-soup",
+					Percent:      95,
+				}, {
+					RevisionName: "beggars-banquet",
+					Percent:      5, // 5 should become 4.
+				}},
+			}},
+		},
+		want: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "goat-head-soup",
+					Percent:      95,
+				}, {
+					RevisionName: "beggars-banquet",
+					Percent:      4,
+				}, {
+					RevisionName: "sticky-fingers",
+					Percent:      1,
+				}},
+			}},
+		},
+	}, {
+		name: "roll with delete (two fast successive rolls)",
+		cur: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "between-the-buttons",
+					Percent:      100,
+				}},
+			}},
+		},
+		prev: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "goat-head-soup",
+					Percent:      99,
+				}, {
+					RevisionName: "bridges-to-babylon",
+					Percent:      1,
+				}},
+			}},
+		},
+		want: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "goat-head-soup",
+					Percent:      99,
+				}, {
+					RevisionName: "between-the-buttons",
+					Percent:      1,
+				}},
+			}},
+		},
+	}, {
+		name: "new tag, no roll", // just attached a tag to an existing route.
+		cur: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "black-on-blue",
+					Percent:      100,
+				}},
+			}, {
+				ConfigurationName: "mick",
+				Tag:               "jagger",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "black-on-blue",
+					Percent:      100,
+				}},
+			}},
+		},
+		prev: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "black-on-blue",
+					Percent:      100,
+				}},
+			}},
+		},
+		want: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "black-on-blue",
+					Percent:      100,
+				}},
+			}, {
+				ConfigurationName: "mick",
+				Tag:               "jagger",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "black-on-blue",
+					Percent:      100,
+				}},
+			}},
+		},
+	}, {
+		name: "deleted config, no roll",
+		cur: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "it's-only-rock-n-roll",
+					Percent:      100,
+				}},
+			}},
+		},
+		prev: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "keith",
+				Percent:           50,
+				Revisions: []RevisionRollout{{
+					RevisionName: "black-on-blue",
+					Percent:      50,
+				}},
+			}, {
+				ConfigurationName: "mick",
+				Percent:           50,
+				Revisions: []RevisionRollout{{
+					RevisionName: "it's-only-rock-n-roll",
+					Percent:      50,
+				}},
+			}},
+		},
+		want: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "it's-only-rock-n-roll",
+					Percent:      100,
+				}},
+			}},
+		},
+	}, {
+		name: "new config, no roll",
+		cur: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "keith",
+				Percent:           50,
+				Revisions: []RevisionRollout{{
+					RevisionName: "black-on-blue",
+					Percent:      50,
+				}},
+			}, {
+				ConfigurationName: "mick",
+				Percent:           50,
+				Revisions: []RevisionRollout{{
+					RevisionName: "it's-only-rock-n-roll",
+					Percent:      50,
+				}},
+			}},
+		},
+		prev: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "mick",
+				Percent:           100,
+				Revisions: []RevisionRollout{{
+					RevisionName: "it's-only-rock-n-roll",
+					Percent:      100,
+				}},
+			}},
+		},
+		want: &Rollout{
+			Configurations: []ConfigurationRollout{{
+				ConfigurationName: "keith",
+				Percent:           50,
+				Revisions: []RevisionRollout{{
+					RevisionName: "black-on-blue",
+					Percent:      50,
+				}},
+			}, {
+				ConfigurationName: "mick",
+				Percent:           50,
+				Revisions: []RevisionRollout{{
+					RevisionName: "it's-only-rock-n-roll",
+					Percent:      50,
+				}},
+			}},
+		},
+	}}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			if got, want := tc.cur.Step(tc.prev), tc.want; !cmp.Equal(got, want) {
+				t.Errorf("Wrong rolled rollout, diff(-want,+got):\n%s", cmp.Diff(want, got))
+			}
+		})
+	}
+}
diff --git a/test/config/chaosduck.yaml b/test/config/chaosduck.yaml
index 574a276ed..eda81fa76 100644
--- a/test/config/chaosduck.yaml
+++ b/test/config/chaosduck.yaml
@@ -81,6 +81,8 @@ spec:
         # Disable chaos on the Istio components.
         - "-disable=istio-webhook"
         - "-disable=networking-istio"
+        # Disable chaos on Kourier components.
+        - "-disable=3scale-kourier-control"
         # Disable chaos on webhooks until https://github.com/knative/pkg/issues/1509 is
         # sorted out.
         - "-disable=webhook"
diff --git a/third_party/cert-manager-latest/net-certmanager.yaml b/third_party/cert-manager-latest/net-certmanager.yaml
index 9a50534e6..035115f59 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: "v20201028-6d22680"
+    serving.knative.dev/release: "v20201029-48615be"
     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: "v20201028-6d22680"
+    serving.knative.dev/release: "v20201029-48615be"
 webhooks:
   - admissionReviewVersions:
       - v1beta1
@@ -86,7 +86,7 @@ metadata:
   name: net-certmanager-webhook-certs
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20201028-6d22680"
+    serving.knative.dev/release: "v20201029-48615be"
 
 ---
 # Copyright 2020 The Knative Authors
@@ -109,7 +109,7 @@ metadata:
   name: config-certmanager
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20201028-6d22680"
+    serving.knative.dev/release: "v20201029-48615be"
     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: "v20201028-6d22680"
+    serving.knative.dev/release: "v20201029-48615be"
     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: "v20201028-6d22680"
+        serving.knative.dev/release: "v20201029-48615be"
     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:a156cd7ffd73b7c03f0878b865999f105ce760e32e8d0a7b7cbf511cc8ef3388
+          image: gcr.io/knative-nightly/knative.dev/net-certmanager/cmd/controller@sha256:a8b7c934df2bd19495e34cb40feea830193ae7d6655e2b59250a33eb501f5feb
           resources:
             requests:
               cpu: 30m
@@ -197,6 +197,11 @@ spec:
               value: knative.dev/serving
           securityContext:
             allowPrivilegeEscalation: false
+            readOnlyRootFilesystem: true
+            runAsNonRoot: true
+            capabilities:
+              drop:
+                - all
           ports:
             - name: metrics
               containerPort: 9090
@@ -208,7 +213,7 @@ kind: Service
 metadata:
   labels:
     app: networking-certmanager
-    serving.knative.dev/release: "v20201028-6d22680"
+    serving.knative.dev/release: "v20201029-48615be"
     networking.knative.dev/certificate-provider: cert-manager
   name: networking-certmanager
   namespace: knative-serving
@@ -245,7 +250,7 @@ metadata:
   name: net-certmanager-webhook
   namespace: knative-serving
   labels:
-    serving.knative.dev/release: "v20201028-6d22680"
+    serving.knative.dev/release: "v20201029-48615be"
 spec:
   selector:
     matchLabels:
@@ -258,14 +263,14 @@ spec:
       labels:
         app: net-certmanager-webhook
         role: net-certmanager-webhook
-        serving.knative.dev/release: "v20201028-6d22680"
+        serving.knative.dev/release: "v20201029-48615be"
     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:b45aacfed54aa5701ef4d4f736582a9a295b491d96ca5ab7452dbde4037ead26
+          image: gcr.io/knative-nightly/knative.dev/net-certmanager/cmd/webhook@sha256:b9c8ae97b4e74d400fc6842d24ac40b112504498e509a37aaf055a531486845a
           resources:
             requests:
               cpu: 20m
@@ -289,6 +294,11 @@ spec:
               value: net-certmanager-webhook
           securityContext:
             allowPrivilegeEscalation: false
+            readOnlyRootFilesystem: true
+            runAsNonRoot: true
+            capabilities:
+              drop:
+                - all
           ports:
             - name: metrics
               containerPort: 9090
@@ -319,7 +329,7 @@ metadata:
   namespace: knative-serving
   labels:
     role: net-certmanager-webhook
-    serving.knative.dev/release: "v20201028-6d22680"
+    serving.knative.dev/release: "v20201029-48615be"
 spec:
   ports:
     # Define metrics and profiling for them to be accessible within service meshes.
diff --git a/third_party/contour-latest/contour.yaml b/third_party/contour-latest/contour.yaml
index 4dc434a92..c2b4df555 100644
--- a/third_party/contour-latest/contour.yaml
+++ b/third_party/contour-latest/contour.yaml
@@ -1482,7 +1482,7 @@ spec:
     spec:
       containers:
         - name: contour
-          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:95ad22d44934a7803975ec5eab070f99f40068de24180a9d761f7f326457bd56
+          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:d691d5e3c0a3bf60d796667cb1418ef6a2db806052c16f3f9cbb0579cefc41eb
           imagePullPolicy: Always
           command:
             - contour
@@ -1709,7 +1709,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:95ad22d44934a7803975ec5eab070f99f40068de24180a9d761f7f326457bd56
+          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:d691d5e3c0a3bf60d796667cb1418ef6a2db806052c16f3f9cbb0579cefc41eb
           imagePullPolicy: IfNotPresent
           name: contour
           ports:
@@ -1795,7 +1795,7 @@ spec:
           args:
             - envoy
             - shutdown-manager
-          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:95ad22d44934a7803975ec5eab070f99f40068de24180a9d761f7f326457bd56
+          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:d691d5e3c0a3bf60d796667cb1418ef6a2db806052c16f3f9cbb0579cefc41eb
           imagePullPolicy: IfNotPresent
           lifecycle:
             preStop:
@@ -1871,7 +1871,7 @@ spec:
             - --envoy-key-file=/certs/tls.key
           command:
             - contour
-          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:95ad22d44934a7803975ec5eab070f99f40068de24180a9d761f7f326457bd56
+          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:d691d5e3c0a3bf60d796667cb1418ef6a2db806052c16f3f9cbb0579cefc41eb
           imagePullPolicy: IfNotPresent
           name: envoy-initconfig
           volumeMounts:
@@ -3381,7 +3381,7 @@ spec:
     spec:
       containers:
         - name: contour
-          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:95ad22d44934a7803975ec5eab070f99f40068de24180a9d761f7f326457bd56
+          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:d691d5e3c0a3bf60d796667cb1418ef6a2db806052c16f3f9cbb0579cefc41eb
           imagePullPolicy: Always
           command:
             - contour
@@ -3608,7 +3608,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:95ad22d44934a7803975ec5eab070f99f40068de24180a9d761f7f326457bd56
+          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:d691d5e3c0a3bf60d796667cb1418ef6a2db806052c16f3f9cbb0579cefc41eb
           imagePullPolicy: IfNotPresent
           name: contour
           ports:
@@ -3694,7 +3694,7 @@ spec:
           args:
             - envoy
             - shutdown-manager
-          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:95ad22d44934a7803975ec5eab070f99f40068de24180a9d761f7f326457bd56
+          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:d691d5e3c0a3bf60d796667cb1418ef6a2db806052c16f3f9cbb0579cefc41eb
           imagePullPolicy: IfNotPresent
           lifecycle:
             preStop:
@@ -3770,7 +3770,7 @@ spec:
             - --envoy-key-file=/certs/tls.key
           command:
             - contour
-          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:95ad22d44934a7803975ec5eab070f99f40068de24180a9d761f7f326457bd56
+          image: gcr.io/knative-nightly/github.com/projectcontour/contour/cmd/contour@sha256:d691d5e3c0a3bf60d796667cb1418ef6a2db806052c16f3f9cbb0579cefc41eb
           imagePullPolicy: IfNotPresent
           name: envoy-initconfig
           volumeMounts:
diff --git a/third_party/contour-latest/net-contour.yaml b/third_party/contour-latest/net-contour.yaml
index 859d7e5ae..1618d34d6 100644
--- a/third_party/contour-latest/net-contour.yaml
+++ b/third_party/contour-latest/net-contour.yaml
@@ -34,7 +34,7 @@ metadata:
   namespace: knative-serving
   labels:
     networking.knative.dev/ingress-provider: contour
-    serving.knative.dev/release: "v20201028-881b3b55"
+    serving.knative.dev/release: "v20201029-dd6e0ede"
 data:
   _example: |
     ################################
@@ -105,7 +105,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:213ad784885e22b3c732fe1f0c3151bb3b2751224d7c32d9d3cffa40b6927988
+          image: gcr.io/knative-nightly/knative.dev/net-contour/cmd/controller@sha256:5c016c66ab7017e6927b2b74966cc74f67aebb16e480d4b0de5009902573d487
           resources:
             requests:
               cpu: 40m
@@ -131,5 +131,10 @@ spec:
               containerPort: 8008
           securityContext:
             allowPrivilegeEscalation: false
+            readOnlyRootFilesystem: true
+            runAsNonRoot: true
+            capabilities:
+              drop:
+                - all
 
 ---
diff --git a/vendor/knative.dev/pkg/test/ghutil/client.go b/vendor/knative.dev/pkg/test/ghutil/client.go
index 8f6466eb7..8c14509f1 100644
--- a/vendor/knative.dev/pkg/test/ghutil/client.go
+++ b/vendor/knative.dev/pkg/test/ghutil/client.go
@@ -20,6 +20,7 @@ package ghutil
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"log"
@@ -119,10 +120,11 @@ func (gc *GithubClient) retry(message string, maxRetries int, call func() (*gith
 		if resp, err = call(); nil == err {
 			return resp, nil
 		}
-		switch err := err.(type) {
-		case *github.RateLimitError:
-			gc.waitForRateReset(&err.Rate)
-		default:
+
+		var errRateLimit *github.RateLimitError
+		if errors.As(err, &errRateLimit) {
+			gc.waitForRateReset(&errRateLimit.Rate)
+		} else {
 			return resp, err
 		}
 		log.Printf("error %s: %v. Will retry.\n", message, err)
diff --git a/vendor/knative.dev/pkg/test/gke/client.go b/vendor/knative.dev/pkg/test/gke/client.go
index 443d5fefe..b1713860e 100644
--- a/vendor/knative.dev/pkg/test/gke/client.go
+++ b/vendor/knative.dev/pkg/test/gke/client.go
@@ -45,7 +45,7 @@ type sdkClient struct {
 func NewSDKClient(opts ...option.ClientOption) (SDKOperations, error) {
 	containerService, err := container.NewService(context.Background(), opts...)
 	if err != nil {
-		return nil, fmt.Errorf("failed to create container service: '%v'", err)
+		return nil, fmt.Errorf("failed to create container service: %w", err)
 	}
 	return &sdkClient{containerService}, nil
 }
@@ -110,7 +110,7 @@ func (gsc *sdkClient) ListClustersInProject(project string) ([]*container.Cluste
 	projectFullPath := fmt.Sprintf("projects/%s/locations/-", project)
 	resp, err := gsc.Projects.Locations.Clusters.List(projectFullPath).Do()
 	if err != nil {
-		return clusters, fmt.Errorf("failed to list clusters under project %s: %v", project, err)
+		return clusters, fmt.Errorf("failed to list clusters under project %s: %w", project, err)
 	}
 	return resp.Clusters, nil
 }
diff --git a/vendor/knative.dev/pkg/test/mako/config/benchmark.go b/vendor/knative.dev/pkg/test/mako/config/benchmark.go
index 74bcbe928..3ddff5cdd 100644
--- a/vendor/knative.dev/pkg/test/mako/config/benchmark.go
+++ b/vendor/knative.dev/pkg/test/mako/config/benchmark.go
@@ -51,6 +51,7 @@ func getBenchmark() (*mpb.BenchmarkInfo, error) {
 	if koerr != nil {
 		data, err = ioutil.ReadFile(filepath.Join(configMako, env+".config"))
 		if err != nil {
+			//nolint: errorlint // It's fine not to wrap here.
 			return nil, fmt.Errorf("cannot load both from kodata and from config mako config map: %s, %s", koerr.Error(), err.Error())
 		}
 	}
diff --git a/vendor/knative.dev/pkg/test/spoof/error_checks.go b/vendor/knative.dev/pkg/test/spoof/error_checks.go
index 8a913b99d..e152e11d7 100644
--- a/vendor/knative.dev/pkg/test/spoof/error_checks.go
+++ b/vendor/knative.dev/pkg/test/spoof/error_checks.go
@@ -19,13 +19,20 @@ limitations under the License.
 package spoof
 
 import (
+	"errors"
 	"net"
 	"strings"
 )
 
-func isTCPTimeout(e error) bool {
-	err, ok := e.(net.Error)
-	return err != nil && ok && err.Timeout()
+func isTCPTimeout(err error) bool {
+	if err == nil {
+		return false
+	}
+	var errNet net.Error
+	if !errors.As(err, &errNet) {
+		return false
+	}
+	return errNet.Timeout()
 }
 
 func isDNSError(err error) bool {
diff --git a/vendor/knative.dev/pkg/test/spoof/spoof.go b/vendor/knative.dev/pkg/test/spoof/spoof.go
index f8c5b9498..e69b39c28 100644
--- a/vendor/knative.dev/pkg/test/spoof/spoof.go
+++ b/vendor/knative.dev/pkg/test/spoof/spoof.go
@@ -237,7 +237,8 @@ func (sc *SpoofingClient) logZipkinTrace(spoofResp *Response) {
 
 	json, err := zipkin.JSONTrace(traceID /* We don't know the expected number of spans */, -1, 5*time.Second)
 	if err != nil {
-		if _, ok := err.(*zipkin.TimeoutError); !ok {
+		var errTimeout *zipkin.TimeoutError
+		if !errors.As(err, &errTimeout) {
 			sc.Logf("Error getting zipkin trace: %v", err)
 		}
 	}