Repository: knative/client
Pull request #1069 (2020-10-20 02:38:05)

Export test refactor
## Description

Refactor kn service export test to use helper functions from serving repo.

First and third commit - Has only vendor changes
Second commit - Has the test refactor changes

<!--
/lint
-->


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

This pull-request has been approved by: *<a href="https://github.com/knative/client/pull/1069#" title="Author self-approved">itsmurugappan</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **navidshaikh** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @navidshaikh` 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%2Fclient).

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

- **[OWNERS](https://github.com/knative/client/blob/master/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":["navidshaikh"]} -->
knative-metrics-robot: The following is the coverage report on the affected files.
Say `/test pull-knative-client-go-coverage` to re-run this coverage report

File | Old Coverage | New Coverage | Delta
---- |:------------:|:------------:|:-----:
[pkg/kn/commands/service/export.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-go-coverage/1318381235185127428/artifacts/line-cov.html#file0) | 85.2% | 86.1% | 0.9

knative-prow-robot: @itsmurugappan: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-integration-tests | c3e0b9b7ded1dd429120e5fd2aee78cb8f7ebe90 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-integration-tests/1318381235185127425) | `/test pull-knative-client-integration-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_client/1069). [Your PR dashboard](https://gubernator.knative.dev/pr/itsmurugappan).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
knative-metrics-robot: The following is the coverage report on the affected files.
Say `/test pull-knative-client-go-coverage` to re-run this coverage report

File | Old Coverage | New Coverage | Delta
---- |:------------:|:------------:|:-----:
[pkg/kn/commands/service/export.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-go-coverage/1318393566451470338/artifacts/line-cov.html#file0) | 85.2% | 86.1% | 0.9

knative-prow-robot: @itsmurugappan: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-integration-tests | 6397a997378312ad56ada16d69fc93b3bd26b100 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-integration-tests/1318393566451470339) | `/test pull-knative-client-integration-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_client/1069). [Your PR dashboard](https://gubernator.knative.dev/pr/itsmurugappan).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
knative-metrics-robot: The following is the coverage report on the affected files.
Say `/test pull-knative-client-go-coverage` to re-run this coverage report

File | Old Coverage | New Coverage | Delta
---- |:------------:|:------------:|:-----:
[pkg/kn/commands/service/export.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-go-coverage/1318613138463002627/artifacts/line-cov.html#file0) | 85.2% | 86.1% | 0.9

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

This pull-request has been approved by: *<a href="https://github.com/knative/client/pull/1069#" title="Author self-approved">itsmurugappan</a>*, *<a href="https://github.com/knative/client/pull/1069#pullrequestreview-513100290" title="Approved">maximilien</a>*

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

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:

- ~~[OWNERS](https://github.com/knative/client/blob/master/OWNERS)~~ [maximilien]

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":[]} -->
maximilien: @itsmurugappan you need to rebase to pickup latest deps
knative-prow-robot: @itsmurugappan: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
knative-prow-robot: @itsmurugappan: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-integration-tests-latest-release | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-integration-tests-latest-release/1322587831167619072) | `/test pull-knative-client-integration-tests-latest-release`

[Full PR test history](https://gubernator.knative.dev/pr/knative_client/1069). [Your PR dashboard](https://gubernator.knative.dev/pr/itsmurugappan).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
knative-prow-robot: @itsmurugappan: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-integration-tests-latest-release | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-integration-tests-latest-release/1322587831167619072) | `/test pull-knative-client-integration-tests-latest-release`
pull-knative-client-build-tests | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-build-tests/1322587831104704512) | `/test pull-knative-client-build-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_client/1069). [Your PR dashboard](https://gubernator.knative.dev/pr/itsmurugappan).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
knative-prow-robot: @itsmurugappan: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-integration-tests-latest-release | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-integration-tests-latest-release/1322587831167619072) | `/test pull-knative-client-integration-tests-latest-release`
pull-knative-client-build-tests | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-build-tests/1322587831104704512) | `/test pull-knative-client-build-tests`
pull-knative-client-unit-tests | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-unit-tests/1322587831121481728) | `/test pull-knative-client-unit-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_client/1069). [Your PR dashboard](https://gubernator.knative.dev/pr/itsmurugappan).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
knative-prow-robot: @itsmurugappan: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-integration-tests-latest-release | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-integration-tests-latest-release/1322587831167619072) | `/test pull-knative-client-integration-tests-latest-release`
pull-knative-client-build-tests | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-build-tests/1322587831104704512) | `/test pull-knative-client-build-tests`
pull-knative-client-unit-tests | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-unit-tests/1322587831121481728) | `/test pull-knative-client-unit-tests`
pull-knative-client-integration-tests | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-integration-tests/1322587831138258944) | `/test pull-knative-client-integration-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_client/1069). [Your PR dashboard](https://gubernator.knative.dev/pr/itsmurugappan).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
knative-prow-robot: @itsmurugappan: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-integration-tests-latest-release | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-integration-tests-latest-release/1322587831167619072) | `/test pull-knative-client-integration-tests-latest-release`
pull-knative-client-build-tests | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-build-tests/1322587831104704512) | `/test pull-knative-client-build-tests`
pull-knative-client-unit-tests | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-unit-tests/1322587831121481728) | `/test pull-knative-client-unit-tests`
pull-knative-client-integration-tests | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-integration-tests/1322587831138258944) | `/test pull-knative-client-integration-tests`
pull-knative-client-go-coverage | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-go-coverage/1322587831150841856) | `/test pull-knative-client-go-coverage`

[Full PR test history](https://gubernator.knative.dev/pr/knative_client/1069). [Your PR dashboard](https://gubernator.knative.dev/pr/itsmurugappan).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
knative-metrics-robot: The following is the coverage report on the affected files.
Say `/test pull-knative-client-go-coverage` to re-run this coverage report

File | Old Coverage | New Coverage | Delta
---- |:------------:|:------------:|:-----:
[pkg/kn/commands/service/export.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-go-coverage/1322589376466653184/artifacts/line-cov.html#file0) | 85.2% | 86.1% | 0.9

knative-metrics-robot: The following is the coverage report on the affected files.
Say `/test pull-knative-client-go-coverage` to re-run this coverage report

File | Old Coverage | New Coverage | Delta
---- |:------------:|:------------:|:-----:
[pkg/kn/commands/service/export.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-go-coverage/1322590193043116032/artifacts/line-cov.html#file0) | 85.2% | 86.1% | 0.9

knative-prow-robot: @itsmurugappan: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-integration-tests-latest-release | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-integration-tests-latest-release/1322587831167619072) | `/test pull-knative-client-integration-tests-latest-release`
pull-knative-client-integration-tests | 91417f57916994cf5c706282f55631b84fe52968 | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-integration-tests/1322587831138258944) | `/test pull-knative-client-integration-tests`
pull-knative-client-build-tests | a5bbf40fc39a9605d74d95b1fe52190234e3221e | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-build-tests/1322590193017950208) | `/test pull-knative-client-build-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_client/1069). [Your PR dashboard](https://gubernator.knative.dev/pr/itsmurugappan).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
itsmurugappan: /test pull-knative-client-build-tests
knative-prow-robot: @itsmurugappan: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-build-tests | a5bbf40fc39a9605d74d95b1fe52190234e3221e | [link](https://prow.knative.dev/view/gs/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-build-tests/1322599897165205504) | `/test pull-knative-client-build-tests`

[Full PR test history](https://gubernator.knative.dev/pr/knative_client/1069). [Your PR dashboard](https://gubernator.knative.dev/pr/itsmurugappan).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
knative-metrics-robot: The following is the coverage report on the affected files.
Say `/test pull-knative-client-go-coverage` to re-run this coverage report

File | Old Coverage | New Coverage | Delta
---- |:------------:|:------------:|:-----:
[pkg/kn/commands/service/export.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_client/1069/pull-knative-client-go-coverage/1322602301612888064/artifacts/line-cov.html#file0) | 85.2% | 86.1% | 0.9


Patch:
diff --git a/docs/cmd/kn_service.md b/docs/cmd/kn_service.md
index 7243d7e9..27ff8f53 100644
--- a/docs/cmd/kn_service.md
+++ b/docs/cmd/kn_service.md
@@ -27,6 +27,7 @@ kn service
 ### SEE ALSO
 
 * [kn](kn.md)	 - kn manages Knative Serving and Eventing resources
+* [kn service apply](kn_service_apply.md)	 - Apply a service declaration
 * [kn service create](kn_service_create.md)	 - Create a service
 * [kn service delete](kn_service_delete.md)	 - Delete services
 * [kn service describe](kn_service_describe.md)	 - Show details of a service
diff --git a/docs/cmd/kn_service_apply.md b/docs/cmd/kn_service_apply.md
new file mode 100644
index 00000000..8aa6c4cc
--- /dev/null
+++ b/docs/cmd/kn_service_apply.md
@@ -0,0 +1,93 @@
+## kn service apply
+
+Apply a service declaration
+
+### Synopsis
+
+Apply a service declaration
+
+```
+kn service apply NAME
+```
+
+### Examples
+
+```
+
+# Create an initial service with using 'kn service apply', if the service has not
+# been already created
+kn service apply s0 --image knativesamples/helloworld
+
+# Apply the service again which is a no-operation if none of the options changed
+kn service apply s0 --image knativesamples/helloworld
+
+# Add an environment variable to your service. Note, that you have to always fully
+# specify all parameters (in contrast to 'kn service update')
+kn service apply s0 --image knativesamples/helloworld --env foo=bar
+
+# Read the service declaration from a file
+kn service apply s0 --filename my-svc.yml
+
+```
+
+### Options
+
+```
+  -a, --annotation stringArray            Annotations to set for both Service and Revision. name=value; you may provide this flag any number of times to set multiple annotations. To unset, specify the annotation name followed by a "-" (e.g., name-).
+      --annotation-revision stringArray   Revision annotation to set. name=value; you may provide this flag any number of times to set multiple annotations. To unset, specify the annotation name followed by a "-" (e.g., name-). This flag takes precedence over the "annotation" flag.
+      --annotation-service stringArray    Service annotation to set. name=value; you may provide this flag any number of times to set multiple annotations. To unset, specify the annotation name followed by a "-" (e.g., name-). This flag takes precedence over the "annotation" flag.
+      --arg stringArray                   Add argument to the container command. Example: --arg myArg1 --arg --myArg2 --arg myArg3=3. You can use this flag multiple times.
+      --async                             DEPRECATED: please use --no-wait instead. Do not wait for 'service apply' operation to be completed.
+      --autoscale-window string           Duration to look back for making auto-scaling decisions. The service is scaled to zero if no request was received in during that time. (eg: 10s)
+      --cluster-local                     Specify that the service be private. (--no-cluster-local will make the service publicly available)
+      --cmd string                        Specify command to be used as entrypoint instead of default one. Example: --cmd /app/start or --cmd /app/start --arg myArg to pass additional arguments.
+      --concurrency-limit int             Hard Limit of concurrent requests to be processed by a single replica.
+      --concurrency-target int            Recommendation for when to scale up based on the concurrent number of incoming request. Defaults to --concurrency-limit when given.
+      --concurrency-utilization int       Percentage of concurrent requests utilization before scaling up. (default 70)
+  -e, --env stringArray                   Environment variable to set. NAME=value; you may provide this flag any number of times to set multiple environment variables. To unset, specify the environment variable name followed by a "-" (e.g., NAME-).
+      --env-from stringArray              Add environment variables from a ConfigMap (prefix cm: or config-map:) or a Secret (prefix secret:). Example: --env-from cm:myconfigmap or --env-from secret:mysecret. You can use this flag multiple times. To unset a ConfigMap/Secret reference, append "-" to the name, e.g. --env-from cm:myconfigmap-.
+  -f, --filename string                   Create a service from file. The created service can be further modified by combining with other options. For example, -f /path/to/file --env NAME=value adds also an environment variable.
+      --force                             Create service forcefully, replaces existing service if any.
+  -h, --help                              help for apply
+      --image string                      Image to run.
+  -l, --label stringArray                 Labels to set for both Service and Revision. name=value; you may provide this flag any number of times to set multiple labels. To unset, specify the label name followed by a "-" (e.g., name-).
+      --label-revision stringArray        Revision label to set. name=value; you may provide this flag any number of times to set multiple labels. To unset, specify the label name followed by a "-" (e.g., name-). This flag takes precedence over the "label" flag.
+      --label-service stringArray         Service label to set. name=value; you may provide this flag any number of times to set multiple labels. To unset, specify the label name followed by a "-" (e.g., name-). This flag takes precedence over the "label" flag.
+      --limit strings                     The resource requirement limits for this Service. For example, 'cpu=100m,memory=256Mi'. You can use this flag multiple times. To unset a resource limit, append "-" to the resource name, e.g. '--limit memory-'.
+      --limits-cpu string                 DEPRECATED: please use --limit instead. The limits on the requested CPU (e.g., 1000m).
+      --limits-memory string              DEPRECATED: please use --limit instead. The limits on the requested memory (e.g., 1024Mi).
+      --lock-to-digest                    Keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision) (default true)
+      --mount stringArray                 Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
+  -n, --namespace string                  Specify the namespace to operate in.
+      --no-cluster-local                  Do not specify that the service be private. (--no-cluster-local will make the service publicly available) (default true)
+      --no-lock-to-digest                 Do not keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision)
+      --no-wait                           Do not wait for 'service apply' operation to be completed.
+  -p, --port string                       The port where application listens on, in the format 'NAME:PORT', where 'NAME' is optional. Examples: '--port h2c:8080' , '--port 8080'.
+      --pull-secret string                Image pull secret to set. An empty argument ("") clears the pull secret. The referenced secret must exist in the service's namespace.
+      --request strings                   The resource requirement requests for this Service. For example, 'cpu=100m,memory=256Mi'. You can use this flag multiple times. To unset a resource request, append "-" to the resource name, e.g. '--request cpu-'.
+      --requests-cpu string               DEPRECATED: please use --request instead. The requested CPU (e.g., 250m).
+      --requests-memory string            DEPRECATED: please use --request instead. The requested memory (e.g., 64Mi).
+      --revision-name string              The revision name to set. Must start with the service name and a dash as a prefix. Empty revision name will result in the server generating a name for the revision. Accepts golang templates, allowing {{.Service}} for the service name, {{.Generation}} for the generation, and {{.Random [n]}} for n random consonants. (default "{{.Service}}-{{.Random 5}}-{{.Generation}}")
+      --scale int                         Minimum and maximum number of replicas.
+      --scale-init int                    Initial number of replicas with which a service starts. Can be 0 or a positive integer.
+      --scale-max int                     Maximum number of replicas.
+      --scale-min int                     Minimum number of replicas.
+      --service-account string            Service account name to set. An empty argument ("") clears the service account. The referenced service account must exist in the service's namespace.
+      --user int                          The user ID to run the container (e.g., 1001).
+      --volume stringArray                Add a volume from a ConfigMap (prefix cm: or config-map:) or a Secret (prefix secret: or sc:). Example: --volume myvolume=cm:myconfigmap or --volume myvolume=secret:mysecret. You can use this flag multiple times. To unset a ConfigMap/Secret reference, append "-" to the name, e.g. --volume myvolume-.
+      --wait                              Wait for 'service apply' operation to be completed. (default true)
+      --wait-timeout int                  Seconds to wait before giving up on waiting for service to be ready. (default 600)
+```
+
+### Options inherited from parent commands
+
+```
+      --config string       kn configuration file (default: ~/.config/kn/config.yaml)
+      --kubeconfig string   kubectl configuration file (default: ~/.kube/config)
+      --log-http            log http traffic
+```
+
+### SEE ALSO
+
+* [kn service](kn_service.md)	 - Manage Knative services
+
diff --git a/go.mod b/go.mod
index 49832dca..f5efc238 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module knative.dev/client
 go 1.14
 
 require (
+	github.com/google/go-cmp v0.5.2
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/cobra v1.0.1-0.20200715031239-b95db644ed1c
@@ -27,7 +28,6 @@ require (
 // ----------------------------------------------------------------------------------------------
 replace (
 	k8s.io/api => k8s.io/api v0.18.8
-	k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.18.8
 	k8s.io/apimachinery => k8s.io/apimachinery v0.18.8
 	k8s.io/cli-runtime => k8s.io/cli-runtime v0.18.8
 	k8s.io/client-go => k8s.io/client-go v0.18.8
diff --git a/go.sum b/go.sum
index a52bd094..d946011c 100644
--- a/go.sum
+++ b/go.sum
@@ -124,6 +124,7 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L
 github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88=
 github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
 github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
+github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -296,11 +297,16 @@ github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv
 github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
 github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
 github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
+github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-oidc v0.0.0-20180117170138-065b426bd416/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
 github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
+github.com/coreos/go-semver v0.0.0-20180108230905-e214231b295a/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@@ -375,6 +381,7 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
+github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v0.0.0-20200808040245-162e5629780b/go.mod h1:NAJj0yf/KaRKURN6nyi7A9IZydMivZEm9oQLWNjfKDc=
 github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M=
@@ -397,6 +404,7 @@ github.com/fsouza/fake-gcs-server v0.0.0-20180612165233-e85be23bdaa8/go.mod h1:1
 github.com/fsouza/fake-gcs-server v1.19.4/go.mod h1:I0/88nHCASqJJ5M7zVF0zKODkYTcuXFW5J5yajsNJnE=
 github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
 github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
@@ -425,34 +433,41 @@ github.com/go-logr/zapr v0.1.1/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aA
 github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
 github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
 github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
+github.com/go-openapi/analysis v0.17.2/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
 github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
 github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk=
 github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU=
 github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
+github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
 github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
 github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
 github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
 github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
 github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
+github.com/go-openapi/jsonpointer v0.19.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
 github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
 github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
 github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
 github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
 github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
 github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
+github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
 github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
 github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o=
 github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
 github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
+github.com/go-openapi/loads v0.17.2/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
 github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
 github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
 github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs=
 github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk=
 github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
+github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
 github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=
 github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=
 github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
 github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
+github.com/go-openapi/spec v0.17.2/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
 github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
 github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
 github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
@@ -465,11 +480,13 @@ github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+Z
 github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU=
 github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
 github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
+github.com/go-openapi/swag v0.17.2/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
 github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
 github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
 github.com/go-openapi/swag v0.19.7 h1:VRuXN2EnMSsZdauzdss6JBC29YotDqG59BZ+tdlIL1s=
 github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY=
+github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
 github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
 github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
 github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4=
@@ -503,6 +520,7 @@ github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6
 github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
 github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
 github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
+github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -682,9 +700,12 @@ github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM
 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
 github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q=
 github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v0.0.0-20170330212424-2500245aa611/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
 github.com/grpc-ecosystem/grpc-gateway v1.4.1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
 github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
@@ -775,6 +796,7 @@ github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeY
 github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/jonboulle/clockwork v0.0.0-20141017032234-72f9bd7c4e0c/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
 github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@@ -919,6 +941,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c=
+github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
 github.com/nats-io/gnatsd v1.4.1/go.mod h1:nqco77VO78hLCJpIcVfygDP2rPGfsEHkGTUk94uh5DQ=
 github.com/nats-io/go-nats v1.7.0/go.mod h1:+t7RHT5ApZebkrQdnn6AhQJmhJJiKAvJUio1PiiCtj0=
 github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
@@ -1147,6 +1170,7 @@ github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:s
 github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
+github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
 github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
@@ -1158,6 +1182,7 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.0-20180319062004-c439c4fa0937/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
@@ -1293,6 +1318,7 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
 go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opentelemetry.io/otel v0.2.3/go.mod h1:OgNpQOjrlt33Ew6Ds0mGjmcTQg/rhUctsbkRdk/g1fw=
+go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@@ -1303,6 +1329,7 @@ go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/automaxprocs v1.3.0/go.mod h1:9CWT6lKIep8U41DDaPiH6eFscnTyjfTANNQNx6LrIcA=
 go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
+go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
 go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
@@ -1312,6 +1339,7 @@ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.9.2-0.20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
@@ -1326,6 +1354,7 @@ golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnf
 golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1419,6 +1448,7 @@ golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -1711,6 +1741,7 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID
 google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
+google.golang.org/genproto v0.0.0-20170731182057-09f6ed296fc6/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20180608181217-32ee49c4dd80/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -1762,6 +1793,7 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D
 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=
+google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
@@ -1829,9 +1861,11 @@ gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eR
 gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
 gopkg.in/jcmturner/gokrb5.v7 v7.3.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
 gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0-20150622162204-20b71e5b60d7/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/robfig/cron.v2 v2.0.0-20150107220207-be2e0b0deed5/go.mod h1:hiOFpYm0ZJbusNj2ywpbrXowU3G8U6GIQzqn2mw1UIE=
+gopkg.in/square/go-jose.v2 v2.0.0-20180411045311-89060dee6a84/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
 gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
@@ -1840,6 +1874,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
 gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -1868,11 +1903,21 @@ honnef.co/go/tools v0.0.1-2020.1.5 h1:nI5egYTGJakVyOryqLs1cQO5dO0ksin5XXs2pspk75
 honnef.co/go/tools v0.0.1-2020.1.5/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 k8s.io/api v0.18.8 h1:aIKUzJPb96f3fKec2lxtY7acZC9gQNDLVhfSGpxBAC4=
 k8s.io/api v0.18.8/go.mod h1:d/CXqwWv+Z2XEG1LgceeDmHQwpUJhROPx16SlxJgERY=
+k8s.io/apiextensions-apiserver v0.0.0-20190918201827-3de75813f604/go.mod h1:7H8sjDlWQu89yWB3FhZfsLyRCRLuoXoCoY5qtwW1q6I=
+k8s.io/apiextensions-apiserver v0.16.4/go.mod h1:HYQwjujEkXmQNhap2C9YDdIVOSskGZ3et0Mvjcyjbto=
+k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs=
+k8s.io/apiextensions-apiserver v0.17.6/go.mod h1:Z3CHLP3Tha+Rbav7JR3S+ye427UaJkHBomK2c4XtZ3A=
+k8s.io/apiextensions-apiserver v0.18.4/go.mod h1:NYeyeYq4SIpFlPxSAB6jHPIdvu3hL0pc36wuRChybio=
 k8s.io/apiextensions-apiserver v0.18.8/go.mod h1:7f4ySEkkvifIr4+BRrRWriKKIJjPyg9mb/p63dJKnlM=
 k8s.io/apimachinery v0.18.8 h1:jimPrycCqgx2QPearX3to1JePz7wSbVLq+7PdBTTwQ0=
 k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig=
+k8s.io/apiserver v0.0.0-20190918200908-1e17798da8c1/go.mod h1:4FuDU+iKPjdsdQSN3GsEKZLB/feQsj1y9dhhBDVV2Ns=
+k8s.io/apiserver v0.16.4/go.mod h1:kbLJOak655g6W7C+muqu1F76u9wnEycfKMqbVaXIdAc=
 k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg=
+k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo=
 k8s.io/apiserver v0.17.4/go.mod h1:5ZDQ6Xr5MNBxyi3iUZXS84QOhZl+W7Oq2us/29c0j9I=
+k8s.io/apiserver v0.17.6/go.mod h1:sAYqm8hUDNA9aj/TzqwsJoExWrxprKv0tqs/z88qym0=
+k8s.io/apiserver v0.18.4/go.mod h1:q+zoFct5ABNnYkGIaGQ3bcbUNdmPyOCoEBcg51LChY8=
 k8s.io/apiserver v0.18.8/go.mod h1:12u5FuGql8Cc497ORNj79rhPdiXQC4bf53X/skR/1YM=
 k8s.io/cli-runtime v0.18.8 h1:ycmbN3hs7CfkJIYxJAOB10iW7BVPmXGXkfEyiV9NJ+k=
 k8s.io/cli-runtime v0.18.8/go.mod h1:7EzWiDbS9PFd0hamHHVoCY4GrokSTPSL32MA4rzIu0M=
@@ -1883,9 +1928,13 @@ k8s.io/cloud-provider v0.17.4/go.mod h1:XEjKDzfD+b9MTLXQFlDGkk6Ho8SGMpaU8Uugx/KN
 k8s.io/cloud-provider v0.18.8/go.mod h1:cn9AlzMPVIXA4HHLVbgGUigaQlZyHSZ7WAwDEFNrQSs=
 k8s.io/code-generator v0.18.8 h1:lgO1P1wjikEtzNvj7ia+x1VC4svJ28a/r0wnOLhhOTU=
 k8s.io/code-generator v0.18.8/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c=
+k8s.io/component-base v0.0.0-20190918200425-ed2f0867c778/go.mod h1:DFWQCXgXVLiWtzFaS17KxHdlUeUymP7FLxZSkmL9/jU=
+k8s.io/component-base v0.16.4/go.mod h1:GYQ+4hlkEwdlpAp59Ztc4gYuFhdoZqiAJD1unYDJ3FM=
 k8s.io/component-base v0.17.0/go.mod h1:rKuRAokNMY2nn2A6LP/MiwpoaMRHpfRnrPaUJJj1Yoc=
 k8s.io/component-base v0.17.2/go.mod h1:zMPW3g5aH7cHJpKYQ/ZsGMcgbsA/VyhEugF3QT1awLs=
 k8s.io/component-base v0.17.4/go.mod h1:5BRqHMbbQPm2kKu35v3G+CpVq4K0RJKC7TRioF0I9lE=
+k8s.io/component-base v0.17.6/go.mod h1:jgRLWl0B0rOzFNtxQ9E4BphPmDqoMafujdau6AdG2Xo=
+k8s.io/component-base v0.18.4/go.mod h1:7jr/Ef5PGmKwQhyAz/pjByxJbC58mhKAhiaDu0vXfPk=
 k8s.io/component-base v0.18.8/go.mod h1:00frPRDas29rx58pPCxNkhUfPbwajlyyvu8ruNgSErU=
 k8s.io/csi-translation-lib v0.17.0/go.mod h1:HEF7MEz7pOLJCnxabi45IPkhSsE/KmxPQksuCrHKWls=
 k8s.io/csi-translation-lib v0.17.4/go.mod h1:CsxmjwxEI0tTNMzffIAcgR9lX4wOh6AKHdxQrT7L0oo=
@@ -1898,12 +1947,15 @@ k8s.io/gengo v0.0.0-20200205140755-e0e292d8aa12 h1:pZzawYyz6VRNPVYpqGv61LWCimQv1
 k8s.io/gengo v0.0.0-20200205140755-e0e292d8aa12/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
+k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
 k8s.io/klog v0.3.3/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
+k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
 k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/klog/v2 v2.0.0 h1:Foj74zO6RbjjP4hBEKjnYtjjAhGg4jNynUdYF6fJrok=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/kube-openapi v0.0.0-20180731170545-e3762e86a74c/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
+k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=
 k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
@@ -1925,7 +1977,9 @@ k8s.io/test-infra v0.0.0-20200617221206-ea73eaeab7ff/go.mod h1:L3+cRvwftUq8IW1Tr
 k8s.io/test-infra v0.0.0-20200630233406-1dca6122872e/go.mod h1:L3+cRvwftUq8IW1TrHji5m3msnc4uck/7LsE/GR/aZk=
 k8s.io/test-infra v0.0.0-20200803112140-d8aa4e063646/go.mod h1:rtUd2cOFwT0aBma1ld6W40F7PuVVw4ELLSFlz9ZEmv8=
 k8s.io/utils v0.0.0-20181019225348-5e321f9a457c/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
+k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0=
 k8s.io/utils v0.0.0-20190506122338-8fab8cb257d5/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
+k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 k8s.io/utils v0.0.0-20190907131718-3d4f5b7dea0b/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 k8s.io/utils v0.0.0-20200124190032-861946025e34/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
@@ -1985,9 +2039,11 @@ sigs.k8s.io/controller-runtime v0.5.4/go.mod h1:JZUwSMVbxDupo0lTJSSFP5pimEyxGynR
 sigs.k8s.io/controller-runtime v0.6.1/go.mod h1:XRYBPdbf5XJu9kpS84VJiZ7h/u1hF3gEORz0efEja7A=
 sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
 sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
+sigs.k8s.io/structured-merge-diff v0.0.0-20190302045857-e85c7b244fd2/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
 sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
-sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06 h1:zD2IemQ4LmOcAumeiyDWXKUI2SO0NYDe3H6QGvPOVgU=
 sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=
+sigs.k8s.io/structured-merge-diff v1.0.1 h1:LOs1LZWMsz1xs77Phr/pkB4LFaavH7IVq/3+WTN9XTA=
+sigs.k8s.io/structured-merge-diff v1.0.1/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA=
 sigs.k8s.io/structured-merge-diff/v2 v2.0.1/go.mod h1:Wb7vfKAodbKgf6tn1Kl0VvGj7mRH6DGaRcixXEJXTsE=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
diff --git a/pkg/kn/commands/service/apply.go b/pkg/kn/commands/service/apply.go
new file mode 100644
index 00000000..aaf0354c
--- /dev/null
+++ b/pkg/kn/commands/service/apply.go
@@ -0,0 +1,123 @@
+// 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.
+
+package service
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/spf13/cobra"
+	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+
+	"knative.dev/client/pkg/kn/commands"
+	clientservingv1 "knative.dev/client/pkg/serving/v1"
+)
+
+var applyExample = `
+# Create an initial service with using 'kn service apply', if the service has not
+# been already created
+kn service apply s0 --image knativesamples/helloworld
+
+# Apply the service again which is a no-operation if none of the options changed
+kn service apply s0 --image knativesamples/helloworld
+
+# Add an environment variable to your service. Note, that you have to always fully
+# specify all parameters (in contrast to 'kn service update')
+kn service apply s0 --image knativesamples/helloworld --env foo=bar
+
+# Read the service declaration from a file
+kn service apply s0 --filename my-svc.yml
+`
+
+func NewServiceApplyCommand(p *commands.KnParams) *cobra.Command {
+	var applyFlags ConfigurationEditFlags
+	var waitFlags commands.WaitFlags
+
+	serviceApplyCommand := &cobra.Command{
+		Use:     "apply NAME",
+		Short:   "Apply a service declaration",
+		Example: applyExample,
+		RunE: func(cmd *cobra.Command, args []string) (err error) {
+			if len(args) != 1 && applyFlags.Filename == "" {
+				return errors.New("'service apply' requires the service name given as single argument")
+			}
+			name := ""
+			if len(args) == 1 {
+				name = args[0]
+			}
+
+			namespace, err := p.GetNamespace(cmd)
+			if err != nil {
+				return err
+			}
+
+			var service *servingv1.Service
+			applyFlags.RevisionName = ""
+			if applyFlags.Filename == "" {
+				service, err = constructService(cmd, applyFlags, name, namespace)
+			} else {
+				service, err = constructServiceFromFile(cmd, applyFlags, name, namespace)
+			}
+			if err != nil {
+				return err
+			}
+
+			client, err := p.NewServingClient(namespace)
+			if err != nil {
+				return err
+			}
+
+			waitDoing, waitVerb, err := examineServiceForApply(cmd, client, service.Name)
+			if err != nil {
+				return err
+			}
+
+			hasChanged, err := client.ApplyService(service)
+			if err != nil {
+				return err
+			}
+			if !hasChanged {
+				fmt.Fprintf(cmd.OutOrStdout(), "No changes to apply to service '%s'.\n", service.Name)
+
+				return showUrl(client, service.Name, "unchanged", "", cmd.OutOrStdout())
+			}
+			return waitIfRequested(client, service.Name, waitFlags, waitDoing, waitVerb, cmd.OutOrStdout())
+		},
+	}
+	commands.AddNamespaceFlags(serviceApplyCommand.Flags(), false)
+	applyFlags.AddCreateFlags(serviceApplyCommand)
+	waitFlags.AddConditionWaitFlags(serviceApplyCommand, commands.WaitDefaultTimeout, "apply", "service", "ready")
+	return serviceApplyCommand
+}
+
+func examineServiceForApply(cmd *cobra.Command, client clientservingv1.KnServingClient, serviceName string) (string, string, error) {
+	currentService, err := client.GetService(serviceName)
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			return "Creating", "created", nil
+		}
+		return "", "", err
+	}
+
+	annotationMap := currentService.Annotations
+	if annotationMap != nil {
+		if _, ok := annotationMap[corev1.LastAppliedConfigAnnotation]; !ok {
+			fmt.Fprintf(cmd.OutOrStdout(), "Warning: 'kn service apply' should be used only for services created by 'kn service apply'\n")
+		}
+	}
+	return "Applying", "applied", nil
+}
diff --git a/pkg/kn/commands/service/create.go b/pkg/kn/commands/service/create.go
index 1e652459..74c66b9d 100644
--- a/pkg/kn/commands/service/create.go
+++ b/pkg/kn/commands/service/create.go
@@ -145,7 +145,7 @@ func createService(client clientservingv1.KnServingClient, service *servingv1.Se
 		return err
 	}
 
-	return waitIfRequested(client, service, waitFlags, "Creating", "created", out)
+	return waitIfRequested(client, service.Name, waitFlags, "Creating", "created", out)
 }
 
 func replaceService(client clientservingv1.KnServingClient, service *servingv1.Service, waitFlags commands.WaitFlags, out io.Writer) error {
@@ -153,23 +153,23 @@ func replaceService(client clientservingv1.KnServingClient, service *servingv1.S
 	if err != nil {
 		return err
 	}
-	return waitIfRequested(client, service, waitFlags, "Replacing", "replaced", out)
+	return waitIfRequested(client, service.Name, waitFlags, "Replacing", "replaced", out)
 }
 
-func waitIfRequested(client clientservingv1.KnServingClient, service *servingv1.Service, waitFlags commands.WaitFlags, verbDoing string, verbDone string, out io.Writer) error {
+func waitIfRequested(client clientservingv1.KnServingClient, serviceName string, waitFlags commands.WaitFlags, verbDoing string, verbDone string, out io.Writer) error {
 	//TODO: deprecated condition should be removed with --async flag
 	if waitFlags.Async {
 		fmt.Fprintf(out, "\nWARNING: flag --async is deprecated and going to be removed in future release, please use --no-wait instead.\n\n")
-		fmt.Fprintf(out, "Service '%s' %s in namespace '%s'.\n", service.Name, verbDone, client.Namespace())
+		fmt.Fprintf(out, "Service '%s' %s in namespace '%s'.\n", serviceName, verbDone, client.Namespace())
 		return nil
 	}
 	if !waitFlags.Wait {
-		fmt.Fprintf(out, "Service '%s' %s in namespace '%s'.\n", service.Name, verbDone, client.Namespace())
+		fmt.Fprintf(out, "Service '%s' %s in namespace '%s'.\n", serviceName, verbDone, client.Namespace())
 		return nil
 	}
 
-	fmt.Fprintf(out, "%s service '%s' in namespace '%s':\n", verbDoing, service.Name, client.Namespace())
-	return waitForServiceToGetReady(client, service.Name, waitFlags.TimeoutInSeconds, verbDone, out)
+	fmt.Fprintf(out, "%s service '%s' in namespace '%s':\n", verbDoing, serviceName, client.Namespace())
+	return waitForServiceToGetReady(client, serviceName, waitFlags.TimeoutInSeconds, verbDone, out)
 }
 
 func prepareAndUpdateService(client clientservingv1.KnServingClient, service *servingv1.Service) error {
@@ -239,6 +239,10 @@ func serviceExists(client clientservingv1.KnServingClient, name string) (bool, e
 func constructService(cmd *cobra.Command, editFlags ConfigurationEditFlags, name string, namespace string) (*servingv1.Service,
 	error) {
 
+	if name == "" || namespace == "" {
+		return nil, errors.New("internal: no name or namespace provided when constructing a service")
+	}
+
 	service := servingv1.Service{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      name,
diff --git a/pkg/kn/commands/service/service.go b/pkg/kn/commands/service/service.go
index 3a4a273b..72c73a04 100644
--- a/pkg/kn/commands/service/service.go
+++ b/pkg/kn/commands/service/service.go
@@ -42,6 +42,7 @@ func NewServiceCommand(p *commands.KnParams) *cobra.Command {
 	serviceCmd.AddCommand(NewServiceCreateCommand(p))
 	serviceCmd.AddCommand(NewServiceDeleteCommand(p))
 	serviceCmd.AddCommand(NewServiceUpdateCommand(p))
+	serviceCmd.AddCommand(NewServiceApplyCommand(p))
 	serviceCmd.AddCommand(NewServiceExportCommand(p))
 	return serviceCmd
 }
@@ -64,7 +65,7 @@ func showUrl(client clientservingv1.KnServingClient, serviceName string, origina
 	url := service.Status.URL.String()
 
 	newRevision := service.Status.LatestReadyRevisionName
-	if originalRevision != "" && originalRevision == newRevision {
+	if (originalRevision != "" && originalRevision == newRevision) || originalRevision == "unchanged" {
 		fmt.Fprintf(out, "Service '%s' with latest revision '%s' (unchanged) is available at URL:\n%s\n", serviceName, newRevision, url)
 	} else {
 		fmt.Fprintf(out, "Service '%s' %s to latest revision '%s' is available at URL:\n%s\n", serviceName, what, newRevision, url)
diff --git a/pkg/serving/v1/apply.go b/pkg/serving/v1/apply.go
new file mode 100644
index 00000000..616c4377
--- /dev/null
+++ b/pkg/serving/v1/apply.go
@@ -0,0 +1,268 @@
+package v1
+
+import (
+	"context"
+	"strings"
+	"time"
+
+	v1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/serializer"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/jsonmergepatch"
+	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+
+	"knative.dev/client/pkg/util"
+)
+
+// 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.
+
+// Helper methods supporting Apply()
+
+// patch performs a 3-way merge and returns whether the original service has been changed
+// This method uses a simple JSON 3-way merge which has some severe limitations, like that arrays
+// can't be merged. Ideally a strategicpatch merge should be used, which allows a more fine grained
+// way for performing the merge (but this is not supported for custom resources)
+// See issue https://github.com/knative/client/issues/1073 for more details how this method should be
+// improved for a better merge strategy.
+func (cl *knServingClient) patch(modifiedService *servingv1.Service, currentService *servingv1.Service, uOriginalService []byte) (bool, error) {
+	uModifiedService, err := getModifiedConfiguration(modifiedService, true)
+	if err != nil {
+		return false, err
+	}
+	hasChanged, err := cl.patchSimple(currentService, uModifiedService, uOriginalService)
+	for i := 1; i <= 5 && apierrors.IsConflict(err); i++ {
+		if i > 1 {
+			time.Sleep(1 * time.Second)
+		}
+		currentService, err = cl.GetService(currentService.Name)
+		if err != nil {
+			return false, err
+		}
+		hasChanged, err = cl.patchSimple(currentService, uModifiedService, uOriginalService)
+	}
+	return hasChanged, err
+}
+
+func (cl *knServingClient) patchSimple(currentService *servingv1.Service, uModifiedService []byte, uOriginalService []byte) (bool, error) {
+	// Serialize the current configuration of the object from the server.
+	uCurrentService, err := encodeService(currentService)
+	if err != nil {
+		return false, err
+	}
+
+	patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(uOriginalService, uModifiedService, uCurrentService)
+	if err != nil {
+		return false, err
+	}
+
+	if string(patch) == "{}" {
+		return false, nil
+	}
+
+	// Check if the generation has been counted up, only then the backend detected a change
+	savedService, err := cl.patchService(currentService.Name, types.MergePatchType, patch)
+	if err != nil {
+		return false, err
+	}
+	return savedService.Generation != savedService.Status.ObservedGeneration, nil
+}
+
+// patchService patches the given service
+func (cl *knServingClient) patchService(name string, patchType types.PatchType, patch []byte) (*servingv1.Service, error) {
+	service, err := cl.client.Services(cl.namespace).Patch(context.TODO(), name, patchType, patch, metav1.PatchOptions{})
+	if err != nil {
+		return nil, err
+	}
+	err = updateServingGvk(service)
+
+	return service, err
+}
+
+func getOriginalConfiguration(service *servingv1.Service) []byte {
+	annots := service.Annotations
+	if annots == nil {
+		return nil
+	}
+	original, ok := annots[v1.LastAppliedConfigAnnotation]
+	if !ok {
+		return nil
+	}
+	return []byte(original)
+}
+
+func getModifiedConfiguration(service *servingv1.Service, annotate bool) ([]byte, error) {
+
+	// First serialize the object without the annotation to prevent recursion,
+	// then add that serialization to it as the annotation and serialize it again.
+	var uModifiedService []byte
+
+	// Otherwise, use the server side version of the object.
+	// Get the current annotations from the object.
+	annots := service.Annotations
+	if annots == nil {
+		annots = map[string]string{}
+	}
+
+	original := annots[v1.LastAppliedConfigAnnotation]
+	delete(annots, v1.LastAppliedConfigAnnotation)
+	service.Annotations = annots
+
+	uModifiedService, err := encodeService(service)
+	if err != nil {
+		return nil, err
+	}
+
+	if annotate {
+		annots[v1.LastAppliedConfigAnnotation] = strings.TrimRight(string(uModifiedService), "\n")
+
+		service.Annotations = annots
+		uModifiedService, err = encodeService(service)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// Restore the object to its original condition.
+	annots[v1.LastAppliedConfigAnnotation] = original
+	service.Annotations = annots
+	return uModifiedService, nil
+}
+
+func updateLastAppliedAnnotation(service *servingv1.Service) error {
+	annots := service.Annotations
+	if annots == nil {
+		annots = map[string]string{}
+	}
+	lastApplied, err := encodeService(service)
+	if err != nil {
+		return err
+	}
+
+	// Cleanup any trailing newlines
+	annots[v1.LastAppliedConfigAnnotation] = strings.TrimRight(string(lastApplied), "\n")
+
+	service.Annotations = annots
+	return nil
+}
+
+func encodeService(service *servingv1.Service) ([]byte, error) {
+	scheme := runtime.NewScheme()
+	err := servingv1.AddToScheme(scheme)
+	if err != nil {
+		return nil, err
+	}
+	factory := serializer.NewCodecFactory(scheme)
+	encoder := factory.EncoderForVersion(unstructured.UnstructuredJSONScheme, servingv1.SchemeGroupVersion)
+	err = util.UpdateGroupVersionKindWithScheme(service, servingv1.SchemeGroupVersion, scheme)
+	if err != nil {
+		return nil, err
+	}
+
+	serviceUnstructured, err := util.ToUnstructured(service)
+	if err != nil {
+		return nil, err
+	}
+
+	// Remove/adapt service so that it can be used in the apply-annotation
+	cleanupServiceUnstructured(serviceUnstructured)
+
+	return runtime.Encode(encoder, serviceUnstructured)
+}
+
+func cleanupServiceUnstructured(uService *unstructured.Unstructured) {
+	clearCreationTimestamps(uService.Object)
+	removeStatus(uService.Object)
+	removeContainerNameAndResourcesIfNotSet(uService.Object)
+
+}
+
+func removeContainerNameAndResourcesIfNotSet(uService map[string]interface{}) {
+	uContainer := extractUserContainer(uService)
+	if uContainer == nil {
+		return
+	}
+	name, ok := uContainer["name"]
+	if ok && name != "" {
+		delete(uContainer, "name")
+	}
+
+	resources := uContainer["resources"]
+	if resources == nil {
+		return
+	}
+	resourcesMap := resources.(map[string]interface{})
+	if len(resourcesMap) == 0 {
+		delete(uContainer, "resources")
+	}
+}
+
+func extractUserContainer(uService map[string]interface{}) map[string]interface{} {
+	tSpec := extractTemplateSpec(uService)
+	if tSpec == nil {
+		return nil
+	}
+	containers := tSpec["containers"]
+	if len(containers.([]interface{})) == 0 {
+		return nil
+	}
+	return containers.([]interface{})[0].(map[string]interface{})
+}
+
+func removeStatus(uService map[string]interface{}) {
+	delete(uService, "status")
+}
+
+func clearCreationTimestamps(uService map[string]interface{}) {
+	meta := uService["metadata"]
+	if meta != nil {
+		delete(meta.(map[string]interface{}), "creationTimestamp")
+	}
+	template := extractTemplate(uService)
+	if template != nil {
+		meta = template["metadata"]
+		if meta != nil {
+			delete(meta.(map[string]interface{}), "creationTimestamp")
+		}
+	}
+}
+
+func extractTemplateSpec(uService map[string]interface{}) map[string]interface{} {
+	templ := extractTemplate(uService)
+	if templ == nil {
+		return nil
+	}
+	templSpec := templ["spec"]
+	if templSpec == nil {
+		return nil
+	}
+
+	return templSpec.(map[string]interface{})
+}
+
+func extractTemplate(uService map[string]interface{}) map[string]interface{} {
+	spec := uService["spec"]
+	if spec == nil {
+		return nil
+	}
+	templ := spec.(map[string]interface{})["template"]
+	if templ == nil {
+		return nil
+	}
+	return templ.(map[string]interface{})
+}
diff --git a/pkg/serving/v1/client.go b/pkg/serving/v1/client.go
index b464eebf..aa60f6bd 100644
--- a/pkg/serving/v1/client.go
+++ b/pkg/serving/v1/client.go
@@ -16,6 +16,7 @@ package v1
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"time"
 
@@ -68,6 +69,17 @@ type KnServingClient interface {
 	// place.
 	UpdateServiceWithRetry(name string, updateFunc ServiceUpdateFunc, nrRetries int) error
 
+	// Apply a service's definition to the cluster. The full service declaration needs to be provided,
+	// which is different to UpdateService which can also do a partial update. If the given
+	// service does not already exists (identified by name) then the service is create.
+	// If the service exists, then a three-way merge will be performed between the original
+	// configuration given (from the last "apply" operation), the new configuration as given ]
+	// here and the current configuration as found on the cluster.
+	// The returned bool indicates whether the service has been changed or whether this operation
+	// was a no-op
+	// An error can indicate a general error or a conflict that occurred during the three way merge.
+	ApplyService(service *servingv1.Service) (bool, error)
+
 	// Delete a service by name
 	DeleteService(name string, timeout time.Duration) error
 
@@ -267,6 +279,32 @@ func updateServiceWithRetry(cl KnServingClient, name string, updateFunc ServiceU
 	}
 }
 
+// ApplyService applies a service definition that contains the service's targer state
+func (cl *knServingClient) ApplyService(modifiedService *servingv1.Service) (bool, error) {
+	currentService, err := cl.GetService(modifiedService.Name)
+	if err != nil && !apierrors.IsNotFound(err) {
+		return false, err
+	}
+
+	containers := modifiedService.Spec.Template.Spec.Containers
+	if len(containers) == 0 || containers[0].Image == "" && currentService != nil {
+		return false, errors.New("'service apply' requires the image name to run provided with the --image option")
+	}
+
+	// No current service --> create a new service
+	if currentService == nil {
+		err := updateLastAppliedAnnotation(modifiedService)
+		if err != nil {
+			return false, err
+		}
+		return true, cl.CreateService(modifiedService)
+	}
+
+	// Merge with existing service
+	uOriginalService := getOriginalConfiguration(currentService)
+	return cl.patch(modifiedService, currentService, uOriginalService)
+}
+
 // Delete a service by name
 // Param `timeout` represents a duration to wait for a delete op to finish.
 // For `timeout == 0` delete is performed async without any wait.
diff --git a/pkg/serving/v1/client_mock.go b/pkg/serving/v1/client_mock.go
index 8c6d5742..2586b269 100644
--- a/pkg/serving/v1/client_mock.go
+++ b/pkg/serving/v1/client_mock.go
@@ -104,6 +104,16 @@ func (c *MockKnServingClient) UpdateServiceWithRetry(name string, updateFunc Ser
 	return updateServiceWithRetry(c, name, updateFunc, maxRetry)
 }
 
+// Update the given service
+func (sr *ServingRecorder) ApplyService(service interface{}, hasChanged bool, err error) {
+	sr.r.Add("ApplyService", []interface{}{service}, []interface{}{hasChanged, err})
+}
+
+func (c *MockKnServingClient) ApplyService(service *servingv1.Service) (bool, error) {
+	call := c.recorder.r.VerifyCall("ApplyService", service)
+	return call.Result[0].(bool), mock.ErrorOrNil(call.Result[1])
+}
+
 // Delete a service by name
 func (sr *ServingRecorder) DeleteService(name, timeout interface{}, err error) {
 	sr.r.Add("DeleteService", []interface{}{name, timeout}, []interface{}{err})
diff --git a/pkg/util/unstructured.go b/pkg/util/unstructured.go
index 77f2e7a3..ccaa6dc7 100644
--- a/pkg/util/unstructured.go
+++ b/pkg/util/unstructured.go
@@ -32,8 +32,8 @@ func ToUnstructuredList(obj runtime.Object) (*unstructured.UnstructuredList, err
 		if err != nil {
 			return nil, err
 		}
-		for _, item := range items {
-			ud, err := toUnstructured(item)
+		for _, obji := range items {
+			ud, err := ToUnstructured(obji)
 			if err != nil {
 				return nil, err
 			}
@@ -41,7 +41,7 @@ func ToUnstructuredList(obj runtime.Object) (*unstructured.UnstructuredList, err
 		}
 
 	} else {
-		ud, err := toUnstructured(obj)
+		ud, err := ToUnstructured(obj)
 		if err != nil {
 			return nil, err
 		}
@@ -51,7 +51,7 @@ func ToUnstructuredList(obj runtime.Object) (*unstructured.UnstructuredList, err
 
 }
 
-func toUnstructured(obj runtime.Object) (*unstructured.Unstructured, error) {
+func ToUnstructured(obj runtime.Object) (*unstructured.Unstructured, error) {
 	b, err := json.Marshal(obj)
 	if err != nil {
 		return nil, err
diff --git a/vendor/k8s.io/apimachinery/pkg/util/jsonmergepatch/patch.go b/vendor/k8s.io/apimachinery/pkg/util/jsonmergepatch/patch.go
new file mode 100644
index 00000000..e56e1773
--- /dev/null
+++ b/vendor/k8s.io/apimachinery/pkg/util/jsonmergepatch/patch.go
@@ -0,0 +1,160 @@
+/*
+Copyright 2017 The Kubernetes 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.
+*/
+
+package jsonmergepatch
+
+import (
+	"fmt"
+	"reflect"
+
+	"github.com/evanphx/json-patch"
+	"k8s.io/apimachinery/pkg/util/json"
+	"k8s.io/apimachinery/pkg/util/mergepatch"
+)
+
+// Create a 3-way merge patch based-on JSON merge patch.
+// Calculate addition-and-change patch between current and modified.
+// Calculate deletion patch between original and modified.
+func CreateThreeWayJSONMergePatch(original, modified, current []byte, fns ...mergepatch.PreconditionFunc) ([]byte, error) {
+	if len(original) == 0 {
+		original = []byte(`{}`)
+	}
+	if len(modified) == 0 {
+		modified = []byte(`{}`)
+	}
+	if len(current) == 0 {
+		current = []byte(`{}`)
+	}
+
+	addAndChangePatch, err := jsonpatch.CreateMergePatch(current, modified)
+	if err != nil {
+		return nil, err
+	}
+	// Only keep addition and changes
+	addAndChangePatch, addAndChangePatchObj, err := keepOrDeleteNullInJsonPatch(addAndChangePatch, false)
+	if err != nil {
+		return nil, err
+	}
+
+	deletePatch, err := jsonpatch.CreateMergePatch(original, modified)
+	if err != nil {
+		return nil, err
+	}
+	// Only keep deletion
+	deletePatch, deletePatchObj, err := keepOrDeleteNullInJsonPatch(deletePatch, true)
+	if err != nil {
+		return nil, err
+	}
+
+	hasConflicts, err := mergepatch.HasConflicts(addAndChangePatchObj, deletePatchObj)
+	if err != nil {
+		return nil, err
+	}
+	if hasConflicts {
+		return nil, mergepatch.NewErrConflict(mergepatch.ToYAMLOrError(addAndChangePatchObj), mergepatch.ToYAMLOrError(deletePatchObj))
+	}
+	patch, err := jsonpatch.MergePatch(deletePatch, addAndChangePatch)
+	if err != nil {
+		return nil, err
+	}
+
+	var patchMap map[string]interface{}
+	err = json.Unmarshal(patch, &patchMap)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to unmarshal patch for precondition check: %s", patch)
+	}
+	meetPreconditions, err := meetPreconditions(patchMap, fns...)
+	if err != nil {
+		return nil, err
+	}
+	if !meetPreconditions {
+		return nil, mergepatch.NewErrPreconditionFailed(patchMap)
+	}
+
+	return patch, nil
+}
+
+// keepOrDeleteNullInJsonPatch takes a json-encoded byte array and a boolean.
+// It returns a filtered object and its corresponding json-encoded byte array.
+// It is a wrapper of func keepOrDeleteNullInObj
+func keepOrDeleteNullInJsonPatch(patch []byte, keepNull bool) ([]byte, map[string]interface{}, error) {
+	var patchMap map[string]interface{}
+	err := json.Unmarshal(patch, &patchMap)
+	if err != nil {
+		return nil, nil, err
+	}
+	filteredMap, err := keepOrDeleteNullInObj(patchMap, keepNull)
+	if err != nil {
+		return nil, nil, err
+	}
+	o, err := json.Marshal(filteredMap)
+	return o, filteredMap, err
+}
+
+// keepOrDeleteNullInObj will keep only the null value and delete all the others,
+// if keepNull is true. Otherwise, it will delete all the null value and keep the others.
+func keepOrDeleteNullInObj(m map[string]interface{}, keepNull bool) (map[string]interface{}, error) {
+	filteredMap := make(map[string]interface{})
+	var err error
+	for key, val := range m {
+		switch {
+		case keepNull && val == nil:
+			filteredMap[key] = nil
+		case val != nil:
+			switch typedVal := val.(type) {
+			case map[string]interface{}:
+				// Explicitly-set empty maps are treated as values instead of empty patches
+				if len(typedVal) == 0 {
+					if !keepNull {
+						filteredMap[key] = typedVal
+					}
+					continue
+				}
+
+				var filteredSubMap map[string]interface{}
+				filteredSubMap, err = keepOrDeleteNullInObj(typedVal, keepNull)
+				if err != nil {
+					return nil, err
+				}
+
+				// If the returned filtered submap was empty, this is an empty patch for the entire subdict, so the key
+				// should not be set
+				if len(filteredSubMap) != 0 {
+					filteredMap[key] = filteredSubMap
+				}
+
+			case []interface{}, string, float64, bool, int64, nil:
+				// Lists are always replaced in Json, no need to check each entry in the list.
+				if !keepNull {
+					filteredMap[key] = val
+				}
+			default:
+				return nil, fmt.Errorf("unknown type: %v", reflect.TypeOf(typedVal))
+			}
+		}
+	}
+	return filteredMap, nil
+}
+
+func meetPreconditions(patchObj map[string]interface{}, fns ...mergepatch.PreconditionFunc) (bool, error) {
+	// Apply the preconditions to the patch, and return an error if any of them fail.
+	for _, fn := range fns {
+		if !fn(patchObj) {
+			return false, fmt.Errorf("precondition failed for: %v", patchObj)
+		}
+	}
+	return true, nil
+}
diff --git a/vendor/knative.dev/serving/pkg/gc/config.go b/vendor/knative.dev/serving/pkg/gc/config.go
new file mode 100644
index 00000000..97fe2b9a
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/gc/config.go
@@ -0,0 +1,174 @@
+/*
+Copyright 2018 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.
+*/
+
+package gc
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	cm "knative.dev/pkg/configmap"
+	"knative.dev/pkg/controller"
+	"knative.dev/pkg/logging"
+)
+
+const (
+	// ConfigName is the name of the config map for garbage collection.
+	ConfigName = "config-gc"
+
+	// Disabled is the value (-1) used by various config map values to indicate
+	// the setting is disabled.
+	Disabled = -1
+
+	disabled = "disabled"
+)
+
+// Config defines the tunable parameters for Garbage Collection.
+type Config struct {
+	// Delay duration after a revision create before considering it for GC
+	StaleRevisionCreateDelay time.Duration
+	// Timeout since a revision lastPinned before it should be GC'd
+	// This must be longer than the controller resync period
+	StaleRevisionTimeout time.Duration
+	// Minimum number of generations of revisions to keep before considering for GC.
+	StaleRevisionMinimumGenerations int64
+	// Minimum staleness duration before updating lastPinned
+	StaleRevisionLastpinnedDebounce time.Duration
+
+	// Duration from creation when a Revision should be considered active
+	// and exempt from GC. Note that GCMaxStaleRevision may override this if set.
+	// Set Disabled (-1) to disable/ignore duration and always consider active.
+	RetainSinceCreateTime time.Duration
+	// Duration from last active when a Revision should be considered active
+	// and exempt from GC.Note that GCMaxStaleRevision may override this if set.
+	// Set Disabled (-1) to disable/ignore duration and always consider active.
+	RetainSinceLastActiveTime time.Duration
+	// Minimum number of non-active revisions to keep before considering for GC.
+	MinNonActiveRevisions int64
+	// Maximum number of non-active revisions to keep before considering for GC.
+	// regardless of creation or staleness time-bounds.
+	// Set Disabled (-1) to disable/ignore max.
+	MaxNonActiveRevisions int64
+}
+
+func defaultConfig() *Config {
+	return &Config{
+		// V1 GC Settings
+		StaleRevisionCreateDelay:        48 * time.Hour,
+		StaleRevisionTimeout:            15 * time.Hour,
+		StaleRevisionLastpinnedDebounce: 5 * time.Hour,
+		StaleRevisionMinimumGenerations: 20,
+
+		// V2 GC Settings
+		RetainSinceCreateTime:     48 * time.Hour,
+		RetainSinceLastActiveTime: 15 * time.Hour,
+		MinNonActiveRevisions:     20,
+		MaxNonActiveRevisions:     1000,
+	}
+}
+
+// NewConfigFromConfigMapFunc creates a Config from the supplied ConfigMap func.
+func NewConfigFromConfigMapFunc(ctx context.Context) func(configMap *corev1.ConfigMap) (*Config, error) {
+	logger := logging.FromContext(ctx)
+	minRevisionTimeout := controller.GetResyncPeriod(ctx)
+	return func(configMap *corev1.ConfigMap) (*Config, error) {
+		c := defaultConfig()
+
+		var retainCreate, retainActive, max string
+		if err := cm.Parse(configMap.Data,
+			cm.AsDuration("stale-revision-create-delay", &c.StaleRevisionCreateDelay),
+			cm.AsDuration("stale-revision-timeout", &c.StaleRevisionTimeout),
+			cm.AsDuration("stale-revision-lastpinned-debounce", &c.StaleRevisionLastpinnedDebounce),
+			cm.AsInt64("stale-revision-minimum-generations", &c.StaleRevisionMinimumGenerations),
+
+			// v2 settings
+			cm.AsString("retain-since-create-time", &retainCreate),
+			cm.AsString("retain-since-last-active-time", &retainActive),
+			cm.AsInt64("min-non-active-revisions", &c.MinNonActiveRevisions),
+			cm.AsString("max-non-active-revisions", &max),
+		); err != nil {
+			return nil, fmt.Errorf("failed to parse data: %w", err)
+		}
+
+		if c.StaleRevisionMinimumGenerations < 0 {
+			return nil, fmt.Errorf(
+				"stale-revision-minimum-generations must be non-negative, was: %d",
+				c.StaleRevisionMinimumGenerations)
+		}
+		if c.StaleRevisionTimeout-c.StaleRevisionLastpinnedDebounce < minRevisionTimeout {
+			logger.Warnf("Got revision timeout of %v, minimum supported value is %v",
+				c.StaleRevisionTimeout, minRevisionTimeout+c.StaleRevisionLastpinnedDebounce)
+			c.StaleRevisionTimeout = minRevisionTimeout + c.StaleRevisionLastpinnedDebounce
+		}
+
+		// validate V2 settings
+		if err := parseDisabledOrDuration(retainCreate, &c.RetainSinceCreateTime); err != nil {
+			return nil, fmt.Errorf("failed to parse retain-since-create-time: %w", err)
+		}
+		if err := parseDisabledOrDuration(retainActive, &c.RetainSinceLastActiveTime); err != nil {
+			return nil, fmt.Errorf("failed to parse retain-since-last-active-time: %w", err)
+		}
+		if err := parseDisabledOrInt64(max, &c.MaxNonActiveRevisions); err != nil {
+			return nil, fmt.Errorf("failed to parse max-stale-revisions: %w", err)
+		}
+		if c.MinNonActiveRevisions < 0 {
+			return nil, fmt.Errorf("min-non-active-revisions must be non-negative, was: %d", c.MinNonActiveRevisions)
+		}
+		if c.MaxNonActiveRevisions >= 0 && c.MinNonActiveRevisions > c.MaxNonActiveRevisions {
+			return nil, fmt.Errorf("min-non-active-revisions(%d) must be <= max-stale-revisions(%d)", c.MinNonActiveRevisions, c.MaxNonActiveRevisions)
+		}
+		return c, nil
+	}
+}
+
+func parseDisabledOrInt64(val string, toSet *int64) error {
+	switch {
+	case val == "":
+		// keep default value
+	case strings.EqualFold(val, disabled):
+		*toSet = Disabled
+	default:
+		parsed, err := strconv.ParseUint(val, 10, 64)
+		if err != nil {
+			return err
+		}
+		*toSet = int64(parsed)
+	}
+	return nil
+}
+
+func parseDisabledOrDuration(val string, toSet *time.Duration) error {
+	switch {
+	case val == "":
+		// keep default value
+	case strings.EqualFold(val, disabled):
+		*toSet = time.Duration(Disabled)
+	default:
+		parsed, err := time.ParseDuration(val)
+		if err != nil {
+			return err
+		}
+		if parsed < 0 {
+			return fmt.Errorf("must be non-negative")
+		}
+		*toSet = parsed
+	}
+	return nil
+}
diff --git a/vendor/knative.dev/serving/pkg/gc/doc.go b/vendor/knative.dev/serving/pkg/gc/doc.go
new file mode 100644
index 00000000..33d94938
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/gc/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright 2018 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.
+*/
+
+// +k8s:deepcopy-gen=package
+
+// Package gc manages garbage collection of old resources.
+package gc
diff --git a/vendor/knative.dev/serving/pkg/gc/zz_generated.deepcopy.go b/vendor/knative.dev/serving/pkg/gc/zz_generated.deepcopy.go
new file mode 100644
index 00000000..9a3151e1
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/gc/zz_generated.deepcopy.go
@@ -0,0 +1,37 @@
+// +build !ignore_autogenerated
+
+/*
+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.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package gc
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Config) DeepCopyInto(out *Config) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config.
+func (in *Config) DeepCopy() *Config {
+	if in == nil {
+		return nil
+	}
+	out := new(Config)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/vendor/knative.dev/serving/pkg/reconciler/route/config/doc.go b/vendor/knative.dev/serving/pkg/reconciler/route/config/doc.go
new file mode 100644
index 00000000..25512a64
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/route/config/doc.go
@@ -0,0 +1,21 @@
+/*
+Copyright 2018 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.
+*/
+
+// +k8s:deepcopy-gen=package
+
+// Package config holds the typed objects that define the schemas for
+// assorted ConfigMap objects on which the Route controller depends.
+package config
diff --git a/vendor/knative.dev/serving/pkg/reconciler/route/config/domain.go b/vendor/knative.dev/serving/pkg/reconciler/route/config/domain.go
new file mode 100644
index 00000000..1ffb0298
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/route/config/domain.go
@@ -0,0 +1,118 @@
+/*
+Copyright 2018 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.
+*/
+
+package config
+
+import (
+	"strings"
+
+	"github.com/ghodss/yaml"
+	corev1 "k8s.io/api/core/v1"
+
+	netpkg "knative.dev/networking/pkg"
+	"knative.dev/pkg/configmap"
+	"knative.dev/pkg/network"
+	"knative.dev/serving/pkg/apis/serving"
+)
+
+const (
+	// DomainConfigName is the config map name for the domain configuration.
+	DomainConfigName = "config-domain"
+	// DefaultDomain holds the domain that Route's live under by default
+	// when no label selector-based options apply.
+	DefaultDomain = "example.com"
+)
+
+// LabelSelector represents map of {key,value} pairs. A single {key,value} in the
+// map is equivalent to a requirement key == value. The requirements are ANDed.
+type LabelSelector struct {
+	Selector map[string]string `json:"selector,omitempty"`
+}
+
+func (s *LabelSelector) specificity() int {
+	return len(s.Selector)
+}
+
+// Matches returns whether the given labels meet the requirement of the selector.
+func (s *LabelSelector) Matches(labels map[string]string) bool {
+	for label, expectedValue := range s.Selector {
+		value, ok := labels[label]
+		if !ok || expectedValue != value {
+			return false
+		}
+	}
+	return true
+}
+
+// Domain maps domains to routes by matching the domain's
+// label selectors to the route's labels.
+type Domain struct {
+	// Domains map from domain to label selector.  If a route has
+	// labels matching a particular selector, it will use the
+	// corresponding domain.  If multiple selectors match, we choose
+	// the most specific selector.
+	Domains map[string]*LabelSelector
+}
+
+// NewDomainFromConfigMap creates a Domain from the supplied ConfigMap
+func NewDomainFromConfigMap(configMap *corev1.ConfigMap) (*Domain, error) {
+	c := Domain{Domains: map[string]*LabelSelector{}}
+	hasDefault := false
+	for k, v := range configMap.Data {
+		if k == configmap.ExampleKey {
+			continue
+		}
+		labelSelector := &LabelSelector{}
+		err := yaml.Unmarshal([]byte(v), labelSelector)
+		if err != nil {
+			return nil, err
+		}
+		c.Domains[k] = labelSelector
+		if len(labelSelector.Selector) == 0 {
+			hasDefault = true
+		}
+	}
+	if !hasDefault {
+		c.Domains[DefaultDomain] = &LabelSelector{}
+	}
+	return &c, nil
+}
+
+// LookupDomainForLabels returns a domain given a set of labels.
+// Since we reject configuration without a default domain, this should
+// always return a value.
+func (c *Domain) LookupDomainForLabels(labels map[string]string) string {
+	domain := ""
+	specificity := -1
+	// If we see VisibilityLabelKey sets with VisibilityClusterLocal, that
+	// will take precedence and the route will get a Cluster's Domain Name.
+	if labels[netpkg.VisibilityLabelKey] == serving.VisibilityClusterLocal ||
+		labels[serving.VisibilityLabelKeyObsolete] == serving.VisibilityClusterLocal {
+		return "svc." + network.GetClusterDomainName()
+	}
+	for k, selector := range c.Domains {
+		// Ignore if selector doesn't match, or decrease the specificity.
+		if !selector.Matches(labels) || selector.specificity() < specificity {
+			continue
+		}
+		if selector.specificity() > specificity || strings.Compare(k, domain) < 0 {
+			domain = k
+			specificity = selector.specificity()
+		}
+	}
+
+	return domain
+}
diff --git a/vendor/knative.dev/serving/pkg/reconciler/route/config/store.go b/vendor/knative.dev/serving/pkg/reconciler/route/config/store.go
new file mode 100644
index 00000000..82d5f862
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/route/config/store.go
@@ -0,0 +1,121 @@
+/*
+Copyright 2018 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.
+*/
+
+package config
+
+import (
+	"context"
+
+	network "knative.dev/networking/pkg"
+	"knative.dev/pkg/configmap"
+	"knative.dev/pkg/logging"
+	cfgmap "knative.dev/serving/pkg/apis/config"
+	"knative.dev/serving/pkg/gc"
+)
+
+type cfgKey struct{}
+
+// Config is the configuration for the route reconciler.
+// +k8s:deepcopy-gen=false
+type Config struct {
+	Domain   *Domain
+	GC       *gc.Config
+	Network  *network.Config
+	Features *cfgmap.Features
+}
+
+// FromContext obtains a Config injected into the passed context.
+func FromContext(ctx context.Context) *Config {
+	return ctx.Value(cfgKey{}).(*Config)
+}
+
+// FromContextOrDefaults is like FromContext, but when no Config is attached it
+// returns a Config populated with the defaults for each of the Config fields.
+func FromContextOrDefaults(ctx context.Context) *Config {
+	cfg := FromContext(ctx)
+	if cfg == nil {
+		cfg = &Config{}
+	}
+
+	if cfg.Features == nil {
+		cfg.Features, _ = cfgmap.NewFeaturesConfigFromMap(map[string]string{})
+	}
+
+	return cfg
+}
+
+// ToContext stores the configuration Config in the passed context.
+func ToContext(ctx context.Context, c *Config) context.Context {
+	return context.WithValue(ctx, cfgKey{}, c)
+}
+
+// Store is based on configmap.UntypedStore and is used to store and watch for
+// updates to configuration related to routes (currently only config-domain).
+//
+// +k8s:deepcopy-gen=false
+type Store struct {
+	*configmap.UntypedStore
+}
+
+// NewStore creates a configmap.UntypedStore based config store.
+//
+// logger must be non-nil implementation of configmap.Logger (commonly used
+// loggers conform)
+//
+// onAfterStore is a variadic list of callbacks to run
+// after the ConfigMap has been processed and stored.
+//
+// See also: configmap.NewUntypedStore().
+func NewStore(ctx context.Context, onAfterStore ...func(name string, value interface{})) *Store {
+	logger := logging.FromContext(ctx)
+
+	store := &Store{
+		UntypedStore: configmap.NewUntypedStore(
+			"route",
+			logger,
+			configmap.Constructors{
+				DomainConfigName:          NewDomainFromConfigMap,
+				gc.ConfigName:             gc.NewConfigFromConfigMapFunc(ctx),
+				network.ConfigName:        network.NewConfigFromConfigMap,
+				cfgmap.FeaturesConfigName: cfgmap.NewFeaturesConfigFromConfigMap,
+			},
+			onAfterStore...,
+		),
+	}
+
+	return store
+}
+
+// ToContext stores the configuration Store in the passed context.
+func (s *Store) ToContext(ctx context.Context) context.Context {
+	return ToContext(ctx, s.Load())
+}
+
+// Load creates a Config for this store.
+func (s *Store) Load() *Config {
+	config := &Config{
+		Domain:   s.UntypedLoad(DomainConfigName).(*Domain).DeepCopy(),
+		GC:       s.UntypedLoad(gc.ConfigName).(*gc.Config).DeepCopy(),
+		Network:  s.UntypedLoad(network.ConfigName).(*network.Config).DeepCopy(),
+		Features: nil,
+	}
+
+	if featureConfig := s.UntypedLoad(cfgmap.FeaturesConfigName); featureConfig != nil {
+		config.Features = featureConfig.(*cfgmap.Features).DeepCopy()
+	}
+
+	return config
+}
diff --git a/vendor/knative.dev/serving/pkg/reconciler/route/config/zz_generated.deepcopy.go b/vendor/knative.dev/serving/pkg/reconciler/route/config/zz_generated.deepcopy.go
new file mode 100644
index 00000000..86b30c49
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/route/config/zz_generated.deepcopy.go
@@ -0,0 +1,75 @@
+// +build !ignore_autogenerated
+
+/*
+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.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package config
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Domain) DeepCopyInto(out *Domain) {
+	*out = *in
+	if in.Domains != nil {
+		in, out := &in.Domains, &out.Domains
+		*out = make(map[string]*LabelSelector, len(*in))
+		for key, val := range *in {
+			var outVal *LabelSelector
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = new(LabelSelector)
+				(*in).DeepCopyInto(*out)
+			}
+			(*out)[key] = outVal
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Domain.
+func (in *Domain) DeepCopy() *Domain {
+	if in == nil {
+		return nil
+	}
+	out := new(Domain)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LabelSelector) DeepCopyInto(out *LabelSelector) {
+	*out = *in
+	if in.Selector != nil {
+		in, out := &in.Selector, &out.Selector
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LabelSelector.
+func (in *LabelSelector) DeepCopy() *LabelSelector {
+	if in == nil {
+		return nil
+	}
+	out := new(LabelSelector)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/vendor/knative.dev/serving/pkg/reconciler/route/domains/doc.go b/vendor/knative.dev/serving/pkg/reconciler/route/domains/doc.go
new file mode 100644
index 00000000..a61e0a9a
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/route/domains/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2019 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.
+*/
+
+// Package domains holds simple functions for generating domains.
+package domains
diff --git a/vendor/knative.dev/serving/pkg/reconciler/route/domains/domains.go b/vendor/knative.dev/serving/pkg/reconciler/route/domains/domains.go
new file mode 100644
index 00000000..a1cc7fa1
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/route/domains/domains.go
@@ -0,0 +1,134 @@
+/*
+Copyright 2019 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.
+*/
+
+package domains
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"strings"
+	"text/template"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	network "knative.dev/networking/pkg"
+	netv1alpha1 "knative.dev/networking/pkg/apis/networking/v1alpha1"
+	"knative.dev/pkg/apis"
+	pkgnet "knative.dev/pkg/network"
+	"knative.dev/serving/pkg/apis/serving"
+	v1 "knative.dev/serving/pkg/apis/serving/v1"
+	"knative.dev/serving/pkg/reconciler/route/config"
+	"knative.dev/serving/pkg/reconciler/route/resources/labels"
+)
+
+// HTTPScheme is the string representation of http.
+const HTTPScheme string = "http"
+
+// GetAllDomainsAndTags returns all of the domains and tags(including subdomains) associated with a Route
+func GetAllDomainsAndTags(ctx context.Context, r *v1.Route, names []string, visibility map[string]netv1alpha1.IngressVisibility) (map[string]string, error) {
+	domainTagMap := make(map[string]string)
+
+	for _, name := range names {
+		meta := r.ObjectMeta.DeepCopy()
+
+		hostname, err := HostnameFromTemplate(ctx, meta.Name, name)
+		if err != nil {
+			return nil, err
+		}
+
+		labels.SetVisibility(meta, visibility[name] == netv1alpha1.IngressVisibilityClusterLocal)
+
+		subDomain, err := DomainNameFromTemplate(ctx, *meta, hostname)
+		if err != nil {
+			return nil, err
+		}
+		domainTagMap[subDomain] = name
+	}
+	return domainTagMap, nil
+}
+
+// DomainNameFromTemplate generates domain name base on the template specified in the `config-network` ConfigMap.
+// name is the "subdomain" which will be referred as the "name" in the template
+func DomainNameFromTemplate(ctx context.Context, r metav1.ObjectMeta, name string) (string, error) {
+	domainConfig := config.FromContext(ctx).Domain
+	rLabels := r.Labels
+	domain := domainConfig.LookupDomainForLabels(rLabels)
+	annotations := r.Annotations
+	// These are the available properties they can choose from.
+	// We could add more over time - e.g. RevisionName if we thought that
+	// might be of interest to people.
+	data := network.DomainTemplateValues{
+		Name:        name,
+		Namespace:   r.Namespace,
+		Domain:      domain,
+		Annotations: annotations,
+		Labels:      rLabels,
+	}
+
+	networkConfig := config.FromContext(ctx).Network
+	buf := bytes.Buffer{}
+
+	var templ *template.Template
+	// If the route is "cluster local" then don't use the user-defined
+	// domain template, use the default one
+	if rLabels[network.VisibilityLabelKey] == serving.VisibilityClusterLocal ||
+		rLabels[serving.VisibilityLabelKeyObsolete] == serving.VisibilityClusterLocal {
+		templ = template.Must(template.New("domain-template").Parse(
+			network.DefaultDomainTemplate))
+	} else {
+		templ = networkConfig.GetDomainTemplate()
+	}
+
+	if err := templ.Execute(&buf, data); err != nil {
+		return "", fmt.Errorf("error executing the DomainTemplate: %w", err)
+	}
+	return buf.String(), nil
+}
+
+// HostnameFromTemplate generates domain name base on the template specified in the `config-network` ConfigMap.
+// name is the "subdomain" which will be referred as the "name" in the template
+func HostnameFromTemplate(ctx context.Context, name, tag string) (string, error) {
+	if tag == "" {
+		return name, nil
+	}
+	// These are the available properties they can choose from.
+	// We could add more over time - e.g. RevisionName if we thought that
+	// might be of interest to people.
+	data := network.TagTemplateValues{
+		Name: name,
+		Tag:  tag,
+	}
+
+	networkConfig := config.FromContext(ctx).Network
+	buf := bytes.Buffer{}
+	if err := networkConfig.GetTagTemplate().Execute(&buf, data); err != nil {
+		return "", fmt.Errorf("error executing the TagTemplate: %w", err)
+	}
+	return buf.String(), nil
+}
+
+// URL generates the a string representation of a URL.
+func URL(scheme, fqdn string) *apis.URL {
+	return &apis.URL{
+		Scheme: scheme,
+		Host:   fqdn,
+	}
+}
+
+// IsClusterLocal checks if a domain is only visible with cluster.
+func IsClusterLocal(domain string) bool {
+	return strings.HasSuffix(domain, pkgnet.GetClusterDomainName())
+}
diff --git a/vendor/knative.dev/serving/pkg/reconciler/route/resources/labels/doc.go b/vendor/knative.dev/serving/pkg/reconciler/route/resources/labels/doc.go
new file mode 100644
index 00000000..49e42e5b
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/route/resources/labels/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2019 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.
+*/
+
+// Package labels holds simple functions for working with ObjectMeta labels.
+package labels
diff --git a/vendor/knative.dev/serving/pkg/reconciler/route/resources/labels/labels.go b/vendor/knative.dev/serving/pkg/reconciler/route/resources/labels/labels.go
new file mode 100644
index 00000000..865360f4
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/route/resources/labels/labels.go
@@ -0,0 +1,51 @@
+/*
+Copyright 2019 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.
+*/
+
+package labels
+
+import (
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	network "knative.dev/networking/pkg"
+	"knative.dev/serving/pkg/apis/serving"
+)
+
+// IsObjectLocalVisibility returns whether an ObjectMeta is of cluster-local visibility
+func IsObjectLocalVisibility(meta *v1.ObjectMeta) bool {
+	return meta.Labels[network.VisibilityLabelKey] != "" || meta.Labels[serving.VisibilityLabelKeyObsolete] != ""
+}
+
+// SetVisibility sets the visibility on an ObjectMeta
+func SetVisibility(meta *v1.ObjectMeta, isClusterLocal bool) {
+	if isClusterLocal {
+		SetLabel(meta, network.VisibilityLabelKey, serving.VisibilityClusterLocal)
+	} else {
+		DeleteLabel(meta, network.VisibilityLabelKey)
+	}
+}
+
+// SetLabel sets/update the label of the an ObjectMeta
+func SetLabel(meta *v1.ObjectMeta, key, value string) {
+	if meta.Labels == nil {
+		meta.Labels = make(map[string]string, 1)
+	}
+
+	meta.Labels[key] = value
+}
+
+// DeleteLabel removes a label from the ObjectMeta
+func DeleteLabel(meta *v1.ObjectMeta, key string) {
+	delete(meta.Labels, key)
+}
diff --git a/vendor/knative.dev/serving/pkg/reconciler/route/resources/names/doc.go b/vendor/knative.dev/serving/pkg/reconciler/route/resources/names/doc.go
new file mode 100644
index 00000000..42a13f3c
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/route/resources/names/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2018 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.
+*/
+
+// Package names holds simple functions for synthesizing resource names.
+package names
diff --git a/vendor/knative.dev/serving/pkg/reconciler/route/resources/names/names.go b/vendor/knative.dev/serving/pkg/reconciler/route/resources/names/names.go
new file mode 100644
index 00000000..32e4abe4
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/route/resources/names/names.go
@@ -0,0 +1,44 @@
+/*
+Copyright 2018 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.
+*/
+
+package names
+
+import (
+	"knative.dev/pkg/kmeta"
+	"knative.dev/pkg/network"
+)
+
+// K8sService returns the name of the K8sService for a given route.
+func K8sService(route kmeta.Accessor) string {
+	return route.GetName()
+}
+
+// K8sServiceFullname returns the full name of the K8sService for a given route.
+func K8sServiceFullname(route kmeta.Accessor) string {
+	return network.GetServiceHostname(K8sService(route), route.GetNamespace())
+}
+
+// Ingress returns the name for the Ingress
+// child resource for the given Route.
+func Ingress(route kmeta.Accessor) string {
+	return kmeta.ChildName(route.GetName(), "")
+}
+
+// Certificate returns the name for the Certificate
+// child resource for the given Route.
+func Certificate(route kmeta.Accessor) string {
+	return "route-" + string(route.GetUID())
+}
diff --git a/vendor/knative.dev/serving/pkg/reconciler/service/resources/names/doc.go b/vendor/knative.dev/serving/pkg/reconciler/service/resources/names/doc.go
new file mode 100644
index 00000000..42a13f3c
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/service/resources/names/doc.go
@@ -0,0 +1,18 @@
+/*
+Copyright 2018 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.
+*/
+
+// Package names holds simple functions for synthesizing resource names.
+package names
diff --git a/vendor/knative.dev/serving/pkg/reconciler/service/resources/names/names.go b/vendor/knative.dev/serving/pkg/reconciler/service/resources/names/names.go
new file mode 100644
index 00000000..7f3eeccb
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/reconciler/service/resources/names/names.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2018 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.
+*/
+
+package names
+
+import "knative.dev/pkg/kmeta"
+
+// Configuration returns a configuration name based on a given service name.
+func Configuration(service kmeta.Accessor) string {
+	return service.GetName()
+}
+
+// Route returns a route name based on a given service name.
+func Route(service kmeta.Accessor) string {
+	return service.GetName()
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 11beda38..196a13d1 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -109,6 +109,7 @@ github.com/golang/protobuf/ptypes/wrappers
 # github.com/google/btree v1.0.0
 github.com/google/btree
 # github.com/google/go-cmp v0.5.2
+## explicit
 github.com/google/go-cmp/cmp
 github.com/google/go-cmp/cmp/cmpopts
 github.com/google/go-cmp/cmp/internal/diff
@@ -549,6 +550,7 @@ k8s.io/apimachinery/pkg/util/errors
 k8s.io/apimachinery/pkg/util/framer
 k8s.io/apimachinery/pkg/util/intstr
 k8s.io/apimachinery/pkg/util/json
+k8s.io/apimachinery/pkg/util/jsonmergepatch
 k8s.io/apimachinery/pkg/util/mergepatch
 k8s.io/apimachinery/pkg/util/naming
 k8s.io/apimachinery/pkg/util/net
@@ -904,7 +906,14 @@ knative.dev/serving/pkg/autoscaler/config/autoscalerconfig
 knative.dev/serving/pkg/client/clientset/versioned/scheme
 knative.dev/serving/pkg/client/clientset/versioned/typed/serving/v1
 knative.dev/serving/pkg/client/clientset/versioned/typed/serving/v1/fake
+knative.dev/serving/pkg/gc
 knative.dev/serving/pkg/networking
+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
+knative.dev/serving/pkg/reconciler/service/resources/names
+knative.dev/serving/pkg/testing/v1
 knative.dev/serving/test/test_images/grpc-ping
 knative.dev/serving/test/test_images/grpc-ping/proto
 # sigs.k8s.io/kustomize v2.0.3+incompatible
@@ -936,7 +945,6 @@ sigs.k8s.io/structured-merge-diff/v3/value
 ## explicit
 sigs.k8s.io/yaml
 # k8s.io/api => k8s.io/api v0.18.8
-# k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.18.8
 # k8s.io/apimachinery => k8s.io/apimachinery v0.18.8
 # k8s.io/cli-runtime => k8s.io/cli-runtime v0.18.8
 # k8s.io/client-go => k8s.io/client-go v0.18.8


Test patch:
diff --git a/lib/test/service.go b/lib/test/service.go
index 55d05baa..44d6f6d9 100644
--- a/lib/test/service.go
+++ b/lib/test/service.go
@@ -15,18 +15,44 @@
 package test
 
 import (
+	"context"
 	"encoding/json"
 	"strings"
 
 	"gotest.tools/assert"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	clientv1alpha1 "knative.dev/client/pkg/apis/client/v1alpha1"
+	"knative.dev/pkg/kmeta"
+	"knative.dev/pkg/ptr"
 	pkgtest "knative.dev/pkg/test"
+	"knative.dev/serving/pkg/apis/config"
 	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+	servingtest "knative.dev/serving/pkg/testing/v1"
 
 	"knative.dev/client/pkg/util"
 )
 
+// ExpectedServiceListOption enables further configuration of a ServiceList.
+type ExpectedServiceListOption func(*servingv1.ServiceList)
+
+// ExpectedRevisionListOption enables further configuration of a RevisionList.
+type ExpectedRevisionListOption func(*servingv1.RevisionList)
+
+// ExpectedKNExportOption enables further configuration of a Export.
+type ExpectedKNExportOption func(*clientv1alpha1.Export)
+
+var revisionSpec = servingv1.RevisionSpec{
+	PodSpec: corev1.PodSpec{
+		Containers: []corev1.Container{{
+			Image: pkgtest.ImagePath("helloworld"),
+		}},
+		EnableServiceLinks: ptr.Bool(false),
+	},
+	TimeoutSeconds: ptr.Int64(config.DefaultRevisionTimeoutSeconds),
+}
+
 // ServiceCreate verifies given service creation in sync mode and also verifies output
 func ServiceCreate(r *KnRunResultCollector, serviceName string) {
 	out := r.KnTest().Kn().Run("service", "create", serviceName, "--image", pkgtest.ImagePath("helloworld"))
@@ -96,6 +122,7 @@ func ServiceDescribeWithJSONPath(r *KnRunResultCollector, serviceName, jsonpath
 	return out.Stdout
 }
 
+// ValidateServiceResources validates cpu and mem resources
 func ValidateServiceResources(r *KnRunResultCollector, serviceName string, requestsMemory, requestsCPU, limitsMemory, limitsCPU string) {
 	var err error
 	rlist := corev1.ResourceList{}
@@ -123,8 +150,8 @@ func ValidateServiceResources(r *KnRunResultCollector, serviceName string, reque
 	assert.DeepEqual(r.T(), serviceLimitsResourceList, llist)
 }
 
-//GetServiceFromKNServiceDescribe runs the kn service describe command
-//decodes it into a ksvc and returns it.
+// GetServiceFromKNServiceDescribe runs the kn service describe command
+// decodes it into a ksvc and returns it.
 func GetServiceFromKNServiceDescribe(r *KnRunResultCollector, serviceName string) servingv1.Service {
 	out := r.KnTest().Kn().Run("service", "describe", serviceName, "-ojson")
 	data := json.NewDecoder(strings.NewReader(out.Stdout))
@@ -134,3 +161,162 @@ func GetServiceFromKNServiceDescribe(r *KnRunResultCollector, serviceName string
 	assert.NilError(r.T(), err)
 	return service
 }
+
+// BuildServiceListWithOptions returns ServiceList with options provided
+func BuildServiceListWithOptions(options ...ExpectedServiceListOption) *servingv1.ServiceList {
+	list := &servingv1.ServiceList{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "v1",
+			Kind:       "List",
+		},
+	}
+
+	for _, fn := range options {
+		fn(list)
+	}
+
+	return list
+}
+
+// WithService appends the given service to ServiceList
+func WithService(svc *servingv1.Service) ExpectedServiceListOption {
+	return func(list *servingv1.ServiceList) {
+		list.Items = append(list.Items, *svc)
+	}
+}
+
+// BuildRevisionListWithOptions returns RevisionList with options provided
+func BuildRevisionListWithOptions(options ...ExpectedRevisionListOption) *servingv1.RevisionList {
+	list := &servingv1.RevisionList{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "v1",
+			Kind:       "List",
+		},
+	}
+
+	for _, fn := range options {
+		fn(list)
+	}
+
+	return list
+}
+
+// BuildKNExportWithOptions returns Export object with the options provided
+func BuildKNExportWithOptions(options ...ExpectedKNExportOption) *clientv1alpha1.Export {
+	knExport := &clientv1alpha1.Export{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "client.knative.dev/v1alpha1",
+			Kind:       "Export",
+		},
+	}
+
+	for _, fn := range options {
+		fn(knExport)
+	}
+
+	return knExport
+}
+
+// BuildConfigurationSpec builds servingv1.ConfigurationSpec with the options provided
+func BuildConfigurationSpec(co ...servingtest.ConfigOption) *servingv1.ConfigurationSpec {
+	c := &servingv1.Configuration{
+		Spec: servingv1.ConfigurationSpec{
+			Template: servingv1.RevisionTemplateSpec{
+				Spec: *revisionSpec.DeepCopy(),
+			},
+		},
+	}
+	for _, opt := range co {
+		opt(c)
+	}
+	c.SetDefaults(context.Background())
+	return &c.Spec
+}
+
+// BuildServiceWithOptions returns ksvc with options provided
+func BuildServiceWithOptions(name string, so ...servingtest.ServiceOption) *servingv1.Service {
+	svc := servingtest.ServiceWithoutNamespace(name, so...)
+	svc.TypeMeta = metav1.TypeMeta{
+		Kind:       "Service",
+		APIVersion: "serving.knative.dev/v1",
+	}
+	svc.Spec.Template.Spec.Containers[0].Resources = corev1.ResourceRequirements{}
+	return svc
+}
+
+// WithTrafficSpec adds route to ksvc
+func WithTrafficSpec(revisions []string, percentages []int, tags []string) servingtest.ServiceOption {
+	return func(svc *servingv1.Service) {
+		var trafficTargets []servingv1.TrafficTarget
+		for i, rev := range revisions {
+			trafficTargets = append(trafficTargets, servingv1.TrafficTarget{
+				Percent: ptr.Int64(int64(percentages[i])),
+			})
+			if tags[i] != "" {
+				trafficTargets[i].Tag = tags[i]
+			}
+			if rev == "latest" {
+				trafficTargets[i].LatestRevision = ptr.Bool(true)
+			} else {
+				trafficTargets[i].RevisionName = rev
+				trafficTargets[i].LatestRevision = ptr.Bool(false)
+			}
+		}
+		svc.Spec.RouteSpec = servingv1.RouteSpec{
+			Traffic: trafficTargets,
+		}
+	}
+}
+
+// BuildRevision returns Revision object with the options provided
+func BuildRevision(name string, options ...servingtest.RevisionOption) *servingv1.Revision {
+	rev := servingtest.Revision("", name, options...)
+	rev.TypeMeta = metav1.TypeMeta{
+		Kind:       "Revision",
+		APIVersion: "serving.knative.dev/v1",
+	}
+	rev.Spec.PodSpec.Containers[0].Name = config.DefaultUserContainerName
+	rev.Spec.PodSpec.EnableServiceLinks = ptr.Bool(false)
+	rev.ObjectMeta.SelfLink = ""
+	rev.ObjectMeta.Namespace = ""
+	rev.ObjectMeta.UID = ""
+	rev.ObjectMeta.Generation = int64(0)
+	rev.Spec.PodSpec.Containers[0].Resources = corev1.ResourceRequirements{}
+	return rev
+}
+
+// WithRevision appends Revision object to RevisionList
+func WithRevision(rev servingv1.Revision) ExpectedRevisionListOption {
+	return func(list *servingv1.RevisionList) {
+		list.Items = append(list.Items, rev)
+	}
+}
+
+// WithKNRevision appends Revision object RevisionList to Kn Export
+func WithKNRevision(rev servingv1.Revision) ExpectedKNExportOption {
+	return func(export *clientv1alpha1.Export) {
+		export.Spec.Revisions = append(export.Spec.Revisions, rev)
+	}
+}
+
+// WithRevisionEnv adds env variable to Revision object
+func WithRevisionEnv(evs ...corev1.EnvVar) servingtest.RevisionOption {
+	return func(s *servingv1.Revision) {
+		s.Spec.PodSpec.Containers[0].Env = evs
+	}
+}
+
+// WithRevisionImage adds revision image to Revision object
+func WithRevisionImage(image string) servingtest.RevisionOption {
+	return func(s *servingv1.Revision) {
+		s.Spec.PodSpec.Containers[0].Image = image
+	}
+}
+
+// WithRevisionAnnotations adds annotation to revision spec in ksvc
+func WithRevisionAnnotations(annotations map[string]string) servingtest.ServiceOption {
+	return func(service *servingv1.Service) {
+		service.Spec.Template.ObjectMeta.Annotations = kmeta.UnionMaps(
+			service.Spec.Template.ObjectMeta.Annotations, annotations)
+	}
+}
diff --git a/pkg/kn/commands/service/apply_mock_test.go b/pkg/kn/commands/service/apply_mock_test.go
new file mode 100644
index 00000000..1f1dcd10
--- /dev/null
+++ b/pkg/kn/commands/service/apply_mock_test.go
@@ -0,0 +1,187 @@
+// Copyright © 2019 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.
+
+package service
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/pkg/errors"
+	"gotest.tools/assert"
+	v1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+
+	knclient "knative.dev/client/pkg/serving/v1"
+	"knative.dev/client/pkg/util/mock"
+	"knative.dev/client/pkg/wait"
+
+	"knative.dev/client/pkg/util"
+)
+
+func TestServiceApplyCreateMock(t *testing.T) {
+	// New mock client
+	client := knclient.NewMockKnServiceClient(t)
+
+	r := setupServiceApplyRecorder(client, "foo", nil, apierrors.NewNotFound(servingv1.Resource("service"), "foo"), true)
+
+	// Testing:
+	output, err := executeServiceCommand(client, "apply", "foo", "--image", "gcr.io/foo/bar:baz")
+	assert.NilError(t, err)
+	assert.Assert(t, util.ContainsAll(output, "created", "foo", "http://foo.example.com", "Ready"))
+
+	// Validate that all recorded API methods have been called
+	r.Validate()
+}
+
+func TestServiceApplyCreateFromFileMock(t *testing.T) {
+	testWithServiceFiles(t, func(t *testing.T, file string) {
+		for _, testArgs := range [][]string{
+			{"apply", "foo", "--filename", file},
+			{"apply", "--filename", file},
+		} {
+			client := knclient.NewMockKnServiceClient(t)
+
+			r := setupServiceApplyRecorder(client, "foo", nil, apierrors.NewNotFound(servingv1.Resource("service"), "foo"), true)
+
+			// Testing:
+			output, err := executeServiceCommand(client, testArgs...)
+			assert.NilError(t, err)
+			assert.Assert(t, util.ContainsAll(output, "created", "foo", "http://foo.example.com", "Ready"))
+
+			// Validate that all recorded API methods have been called
+			r.Validate()
+		}
+	})
+}
+
+func TestServiceApplyCreateFromFileMockWithoutName(t *testing.T) {
+	testWithServiceFiles(t, func(t *testing.T, file string) {
+		client := knclient.NewMockKnServiceClient(t)
+
+		r := setupServiceApplyRecorder(client, "foo", nil, apierrors.NewNotFound(servingv1.Resource("service"), "foo"), true)
+
+		// Testing:
+		output, err := executeServiceCommand(client, "apply", "foo", "--filename", file)
+		assert.NilError(t, err)
+		assert.Assert(t, util.ContainsAll(output, "created", "foo", "http://foo.example.com", "Ready"))
+
+		// Validate that all recorded API methods have been called
+		r.Validate()
+	})
+}
+
+func TestServiceApplyUpdateMock(t *testing.T) {
+	// New mock client
+	client := knclient.NewMockKnServiceClient(t)
+
+	service := createServiceWithImage("foo", "gcr.io/foo/bar:baz")
+
+	r := setupServiceApplyRecorder(client, "foo", service, nil, true)
+
+	// Testing:
+	output, err := executeServiceCommand(client, "apply", "foo", "--image", "gcr.io/foo/bar:baz")
+	assert.NilError(t, err)
+	assert.Assert(t, util.ContainsAll(output, "applied", "foo", "http://foo.example.com", "Ready"))
+
+	// Validate that all recorded API methods have been called
+	r.Validate()
+}
+
+func TestServiceApplyUpdateUnchanged(t *testing.T) {
+	// New mock client
+	client := knclient.NewMockKnServiceClient(t)
+
+	service := createServiceWithImage("foo", "gcr.io/foo/bar:baz")
+
+	r := setupServiceApplyRecorder(client, "foo", service, nil, false)
+
+	// Testing:
+	output, err := executeServiceCommand(client, "apply", "foo", "--image", "gcr.io/foo/bar:baz")
+	assert.NilError(t, err)
+	assert.Assert(t, util.ContainsAll(output, "No changes", "apply", "foo", "http://foo.example.com"))
+
+	// Validate that all recorded API methods have been called
+	r.Validate()
+}
+
+func TestServiceApplyWithGetError(t *testing.T) {
+	// New mock client
+	client := knclient.NewMockKnServiceClient(t)
+
+	errThrown := errors.New("boom!")
+	r := setupServiceApplyRecorder(client, "foo", nil, errThrown, true)
+
+	_, err := executeServiceCommand(client, "apply", "foo", "--image", "gcr.io/foo/bar:baz")
+	assert.Equal(t, err, errThrown)
+
+	// Validate that all recorded API methods have been called
+	r.Validate()
+}
+
+func setupServiceApplyRecorder(client *knclient.MockKnServingClient, name string, service *servingv1.Service, err error, hasChanged bool) *knclient.ServingRecorder {
+	// Recording:
+	r := client.Recorder()
+	// Check for existing service --> no
+	r.GetService(name, service, err)
+	// Error test
+	if err != nil && !apierrors.IsNotFound(err) {
+		return r
+	}
+
+	// Create service (don't validate given service --> "Any()" arg is allowed)
+	r.ApplyService(func(t *testing.T, a interface{}) {
+		svc := a.(*servingv1.Service)
+		assert.Equal(t, svc.Name, name)
+		setUrl(svc, fmt.Sprintf("http://%s.example.com", name))
+	}, hasChanged, nil)
+
+	// Fetch service for URL
+	r.GetService(name, getServiceWithUrl(name, fmt.Sprintf("http://%s.example.com", name)), nil)
+
+	if !hasChanged {
+		return r
+	}
+	// Wait for service to become ready
+	r.WaitForService(name, mock.Any(), wait.NoopMessageCallback(), nil, time.Second)
+
+	return r
+}
+
+func createServiceWithImage(name string, image string) *servingv1.Service {
+	return &servingv1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: "default",
+		},
+		Spec: servingv1.ServiceSpec{
+			ConfigurationSpec: servingv1.ConfigurationSpec{
+				Template: servingv1.RevisionTemplateSpec{
+					Spec: servingv1.RevisionSpec{
+						PodSpec: v1.PodSpec{
+							Containers: []v1.Container{
+								{
+									Image: image,
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}
diff --git a/pkg/kn/commands/service/create_mock_test.go b/pkg/kn/commands/service/create_mock_test.go
index 277e563e..bbcbf12c 100644
--- a/pkg/kn/commands/service/create_mock_test.go
+++ b/pkg/kn/commands/service/create_mock_test.go
@@ -681,8 +681,12 @@ func TestServiceCreateWithAutoScaleServiceAnnotationsError(t *testing.T) {
 
 func getServiceWithUrl(name string, urlName string) *servingv1.Service {
 	service := servingv1.Service{}
-	url, _ := apis.ParseURL(urlName)
-	service.Status.URL = url
 	service.Name = name
+	setUrl(&service, urlName)
 	return &service
 }
+
+func setUrl(service *servingv1.Service, urlName string) {
+	url, _ := apis.ParseURL(urlName)
+	service.Status.URL = url
+}
diff --git a/pkg/kn/commands/service/create_test.go b/pkg/kn/commands/service/create_test.go
index 6ebd0a89..d155454c 100644
--- a/pkg/kn/commands/service/create_test.go
+++ b/pkg/kn/commands/service/create_test.go
@@ -822,93 +822,61 @@ var serviceJSON = `
   }
 }`
 
-func TestServiceCreateFromYAML(t *testing.T) {
-	tempDir, err := ioutil.TempDir("", "kn-file")
-	defer os.RemoveAll(tempDir)
-	assert.NilError(t, err)
-
-	tempFile := filepath.Join(tempDir, "service.yaml")
-	err = ioutil.WriteFile(tempFile, []byte(serviceYAML), os.FileMode(0666))
-	assert.NilError(t, err)
-
-	action, created, _, err := fakeServiceCreate([]string{
-		"service", "create", "foo", "--filename", tempFile}, false)
-	assert.NilError(t, err)
-	assert.Assert(t, action.Matches("create", "services"))
-
-	assert.Equal(t, created.Name, "foo")
-	assert.Equal(t, created.Spec.Template.Spec.GetContainer().Image, "gcr.io/foo/bar:baz")
-}
-
-func TestServiceCreateFromJSON(t *testing.T) {
-	tempDir, err := ioutil.TempDir("", "kn-file")
-	defer os.RemoveAll(tempDir)
-	assert.NilError(t, err)
-
-	tempFile := filepath.Join(tempDir, "service.json")
-	err = ioutil.WriteFile(tempFile, []byte(serviceJSON), os.FileMode(0666))
-	assert.NilError(t, err)
-
-	action, created, _, err := fakeServiceCreate([]string{
-		"service", "create", "foo", "--filename", tempFile}, false)
-	assert.NilError(t, err)
-	assert.Assert(t, action.Matches("create", "services"))
-
-	assert.Equal(t, created.Name, "foo")
-	assert.Equal(t, created.Spec.Template.Spec.GetContainer().Image, "gcr.io/foo/bar:baz")
+func TestServiceCreateFromFile(t *testing.T) {
+	testWithServiceFiles(t, func(t *testing.T, tempFile string) {
+		for _, testArgs := range [][]string{
+			{
+				"service", "create", "foo", "--filename", tempFile}, {
+				"service", "create", "--filename", tempFile},
+		} {
+			action, created, _, err := fakeServiceCreate(testArgs, false)
+			assert.NilError(t, err)
+			assert.Assert(t, action.Matches("create", "services"))
+
+			assert.Equal(t, created.Name, "foo")
+			assert.Equal(t, created.Spec.Template.Spec.GetContainer().Image, "gcr.io/foo/bar:baz")
+		}
+	})
 }
 
-func TestServiceCreateFromFileWithName(t *testing.T) {
+func testWithServiceFiles(t *testing.T, testFunction func(t *testing.T, file string)) {
 	tempDir, err := ioutil.TempDir("", "kn-file")
 	defer os.RemoveAll(tempDir)
 	assert.NilError(t, err)
 
-	tempFile := filepath.Join(tempDir, "service.yaml")
-	err = ioutil.WriteFile(tempFile, []byte(serviceYAML), os.FileMode(0666))
-	assert.NilError(t, err)
-
-	t.Log("no NAME param provided")
-	action, created, _, err := fakeServiceCreate([]string{
-		"service", "create", "--filename", tempFile}, false)
-	assert.NilError(t, err)
-	assert.Assert(t, action.Matches("create", "services"))
-
-	assert.Equal(t, created.Name, "foo")
-	assert.Equal(t, created.Spec.Template.Spec.GetContainer().Image, "gcr.io/foo/bar:baz")
-
-	t.Log("no service.Name provided in file")
-	err = ioutil.WriteFile(tempFile, []byte(strings.ReplaceAll(serviceYAML, "name: foo", "")), os.FileMode(0666))
-	assert.NilError(t, err)
-	action, created, _, err = fakeServiceCreate([]string{
-		"service", "create", "cli-foo", "--filename", tempFile}, false)
-	assert.NilError(t, err)
-	assert.Assert(t, action.Matches("create", "services"))
-
-	assert.Equal(t, created.Name, "cli-foo")
-	assert.Equal(t, created.Spec.Template.Spec.GetContainer().Image, "gcr.io/foo/bar:baz")
+	for _, d := range []struct {
+		filename string
+		content  string
+	}{
+		{"service.yaml",
+			serviceYAML,
+		},
+		{"service.json",
+			serviceJSON,
+		},
+	} {
+		tempFile := filepath.Join(tempDir, d.filename)
+		err = ioutil.WriteFile(tempFile, []byte(d.content), os.FileMode(0666))
+		assert.NilError(t, err)
+		testFunction(t, tempFile)
+	}
 }
 
 func TestServiceCreateFileNameMismatch(t *testing.T) {
-	tempDir, err := ioutil.TempDir("", "kn-file")
-	assert.NilError(t, err)
-
-	tempFile := filepath.Join(tempDir, "service.json")
-	err = ioutil.WriteFile(tempFile, []byte(serviceJSON), os.FileMode(0666))
-	assert.NilError(t, err)
-
-	t.Log("NAME param nad service.Name differ")
-	_, _, _, err = fakeServiceCreate([]string{
-		"service", "create", "anotherFoo", "--filename", tempFile}, false)
-	assert.Assert(t, err != nil)
-	assert.Assert(t, util.ContainsAllIgnoreCase(err.Error(), "provided", "'anotherFoo'", "name", "match", "from", "file", "'foo'"))
-
-	t.Log("no NAME param & no service.Name provided in file")
-	err = ioutil.WriteFile(tempFile, []byte(strings.ReplaceAll(serviceYAML, "name: foo", "")), os.FileMode(0666))
-	assert.NilError(t, err)
-	_, _, _, err = fakeServiceCreate([]string{
-		"service", "create", "--filename", tempFile}, false)
-	assert.Assert(t, err != nil)
-	assert.Assert(t, util.ContainsAllIgnoreCase(err.Error(), "no", "service", "name", "provided", "parameter", "file"))
+	testWithServiceFiles(t, func(t *testing.T, tempFile string) {
+		_, _, _, err := fakeServiceCreate([]string{
+			"service", "create", "anotherFoo", "--filename", tempFile}, false)
+		assert.Assert(t, err != nil)
+		assert.Assert(t, util.ContainsAllIgnoreCase(err.Error(), "provided", "'anotherFoo'", "name", "match", "from", "file", "'foo'"))
+
+		err = ioutil.WriteFile(tempFile, []byte(strings.ReplaceAll(serviceYAML, "name: foo", "")), os.FileMode(0666))
+		assert.NilError(t, err)
+		_, _, _, err = fakeServiceCreate([]string{
+			"service", "create", "--filename", tempFile}, false)
+		assert.Assert(t, err != nil)
+		assert.Assert(t, util.ContainsAllIgnoreCase(err.Error(), "no", "service", "name", "provided", "parameter", "file"))
+
+	})
 }
 
 func TestServiceCreateFileError(t *testing.T) {
diff --git a/pkg/kn/commands/service/export_test.go b/pkg/kn/commands/service/export_test.go
index ac825a84..f60be43f 100644
--- a/pkg/kn/commands/service/export_test.go
+++ b/pkg/kn/commands/service/export_test.go
@@ -15,28 +15,33 @@
 package service
 
 import (
+	"context"
 	"encoding/json"
 	"testing"
 
 	"gotest.tools/assert"
 
 	v1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	libtest "knative.dev/client/lib/test"
 	clientv1alpha1 "knative.dev/client/pkg/apis/client/v1alpha1"
 	knclient "knative.dev/client/pkg/serving/v1"
 	"knative.dev/client/pkg/util/mock"
 	"knative.dev/pkg/ptr"
 	apiserving "knative.dev/serving/pkg/apis/serving"
 	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+	servingtest "knative.dev/serving/pkg/testing/v1"
 	"sigs.k8s.io/yaml"
 )
 
-type expectedServiceOption func(*servingv1.Service)
-type expectedRevisionOption func(*servingv1.Revision)
-type expectedServiceListOption func(*servingv1.ServiceList)
-type expectedRevisionListOption func(*servingv1.RevisionList)
-type expectedKNExportOption func(*clientv1alpha1.Export)
-type podSpecOption func(*v1.PodSpec)
+var revisionSpec = servingv1.RevisionSpec{
+	PodSpec: v1.PodSpec{
+		Containers: []v1.Container{{
+			Image: "busybox",
+		}},
+		EnableServiceLinks: ptr.Bool(false),
+	},
+	TimeoutSeconds: ptr.Int64(300),
+}
 
 type testCase struct {
 	name             string
@@ -62,11 +67,11 @@ func TestServiceExportError(t *testing.T) {
 func TestServiceExport(t *testing.T) {
 
 	for _, tc := range []testCase{
-		{latestSvc: getServiceWithOptions(getService("foo"), withServicePodSpecOption(withContainer()))},
-		{latestSvc: getServiceWithOptions(getService("foo"), withServicePodSpecOption(withContainer(), withEnv([]v1.EnvVar{{Name: "a", Value: "mouse"}})))},
-		{latestSvc: getServiceWithOptions(getService("foo"), withConfigurationLabels(map[string]string{"a": "mouse"}), withConfigurationAnnotations(map[string]string{"a": "mouse"}), withServicePodSpecOption(withContainer()))},
-		{latestSvc: getServiceWithOptions(getService("foo"), withLabels(map[string]string{"a": "mouse"}), withAnnotations(map[string]string{"a": "mouse"}), withServicePodSpecOption(withContainer()))},
-		{latestSvc: getServiceWithOptions(getService("foo"), withServicePodSpecOption(withContainer(), withVolumeandSecrets("secretName")))},
+		{latestSvc: libtest.BuildServiceWithOptions("foo", servingtest.WithConfigSpec(buildConfiguration()))},
+		{latestSvc: libtest.BuildServiceWithOptions("foo", servingtest.WithConfigSpec(buildConfiguration()), servingtest.WithEnv(v1.EnvVar{Name: "a", Value: "mouse"}))},
+		{latestSvc: libtest.BuildServiceWithOptions("foo", servingtest.WithConfigSpec(buildConfiguration()), libtest.WithRevisionAnnotations(map[string]string{"client.knative.dev/user-image": "busybox:v2"}))},
+		{latestSvc: libtest.BuildServiceWithOptions("foo", servingtest.WithConfigSpec(buildConfiguration()), servingtest.WithServiceLabel("a", "mouse"), servingtest.WithServiceAnnotation("a", "mouse"))},
+		{latestSvc: libtest.BuildServiceWithOptions("foo", servingtest.WithConfigSpec(buildConfiguration()), servingtest.WithVolume("secretName", "/mountpath", volumeSource("secretName")))},
 	} {
 		exportServiceTest(t, &tc)
 	}
@@ -80,181 +85,130 @@ func exportServiceTest(t *testing.T, tc *testCase) {
 	err = yaml.Unmarshal([]byte(output), &actSvc)
 	assert.NilError(t, err)
 
-	stripUnwantedFields(tc.latestSvc)
 	assert.DeepEqual(t, tc.latestSvc, &actSvc)
 }
 
 func TestServiceExportwithMultipleRevisions(t *testing.T) {
 	for _, tc := range []testCase{{
 		name: "test 2 revisions with traffic split",
-		latestSvc: getServiceWithOptions(
-			getService("foo"),
-			withAnnotations(map[string]string{"serving.knative.dev/creator": "ut", "serving.knative.dev/lastModifier": "ut"}),
-			withConfigurationAnnotations(map[string]string{"client.knative.dev/user-image": "gcr.io/foo/bar:baz2"}),
-			withTrafficSplit([]string{"foo-rev-1", ""}, []int{50, 50}, []bool{false, true}),
-			withServicePodSpecOption(withContainer()),
+		latestSvc: libtest.BuildServiceWithOptions(
+			"foo", servingtest.WithConfigSpec(buildConfiguration()),
+			libtest.WithRevisionAnnotations(map[string]string{"client.knative.dev/user-image": "busybox:v2"}),
+			libtest.WithTrafficSpec([]string{"foo-rev-1", "latest"}, []int{50, 50}, []string{"", ""}),
 		),
-		expectedSvcList: getServiceListWithOptions(
-			withServices(
-				getService("foo"),
-				withUnwantedFieldsStripped(),
-				withConfigurationAnnotations(map[string]string{"client.knative.dev/user-image": "gcr.io/foo/bar:baz1"}),
-				withServicePodSpecOption(withContainer()),
-				withServiceRevisionName("foo-rev-1"),
-			),
-			withServices(
-				getService("foo"),
-				withUnwantedFieldsStripped(),
-				withConfigurationAnnotations(map[string]string{"client.knative.dev/user-image": "gcr.io/foo/bar:baz2"}),
-				withServicePodSpecOption(withContainer()),
-				withTrafficSplit([]string{"foo-rev-1", ""}, []int{50, 50}, []bool{false, true}),
-			),
+		expectedSvcList: libtest.BuildServiceListWithOptions(
+			libtest.WithService(libtest.BuildServiceWithOptions("foo", servingtest.WithConfigSpec(buildConfiguration()),
+				libtest.WithRevisionAnnotations(map[string]string{"client.knative.dev/user-image": "busybox:v1"}),
+				servingtest.WithBYORevisionName("foo-rev-1"),
+			)),
+			libtest.WithService(libtest.BuildServiceWithOptions("foo", servingtest.WithConfigSpec(buildConfiguration()),
+				libtest.WithRevisionAnnotations(map[string]string{"client.knative.dev/user-image": "busybox:v2"}),
+				libtest.WithTrafficSpec([]string{"foo-rev-1", "latest"}, []int{50, 50}, []string{"", ""}),
+			)),
 		),
-		revisionList: getRevisionListWithOptions(
-			withRevisions(
-				withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}),
-				withRevisionGeneration("1"),
-				withRevisionAnnotations(map[string]string{"serving.knative.dev/lastPinned": "1111132"}),
-				withRevisionAnnotations(map[string]string{"client.knative.dev/user-image": "gcr.io/foo/bar:baz1"}),
-				withRevisionName("foo-rev-1"),
-				withRevisionPodSpecOption(withContainer()),
-			),
-			withRevisions(
-				withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}),
-				withRevisionGeneration("2"),
-				withRevisionName("foo-rev-2"),
-				withRevisionAnnotations(map[string]string{"client.knative.dev/user-image": "gcr.io/foo/bar:baz2"}),
-				withRevisionPodSpecOption(withContainer()),
-			),
+		revisionList: libtest.BuildRevisionListWithOptions(
+			libtest.WithRevision(*(libtest.BuildRevision("foo-rev-1",
+				servingtest.WithRevisionLabel(apiserving.ServiceLabelKey, "foo"),
+				servingtest.WithRevisionLabel(apiserving.ConfigurationGenerationLabelKey, "1"),
+				servingtest.WithRevisionAnn("client.knative.dev/user-image", "busybox:v1"),
+				servingtest.WithRevisionAnn("serving.knative.dev/lastPinned", "1111132"),
+			))),
+			libtest.WithRevision(*(libtest.BuildRevision("foo-rev-2",
+				servingtest.WithRevisionLabel(apiserving.ServiceLabelKey, "foo"),
+				servingtest.WithRevisionLabel(apiserving.ConfigurationGenerationLabelKey, "1"),
+				servingtest.WithRevisionAnn("client.knative.dev/user-image", "busybox:v2"),
+			))),
 		),
-		expectedKNExport: getKNExportWithOptions(
-			withKNRevisions(
-				withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}),
-				withRevisionName("foo-rev-1"),
-				withRevisionGeneration("1"),
-				withRevisionAnnotations(map[string]string{"client.knative.dev/user-image": "gcr.io/foo/bar:baz1"}),
-				withRevisionPodSpecOption(withContainer()),
-			),
+		expectedKNExport: libtest.BuildKNExportWithOptions(
+			libtest.WithKNRevision(*(libtest.BuildRevision("foo-rev-1",
+				servingtest.WithRevisionLabel(apiserving.ServiceLabelKey, "foo"),
+				servingtest.WithRevisionLabel(apiserving.ConfigurationGenerationLabelKey, "1"),
+				servingtest.WithRevisionAnn("client.knative.dev/user-image", "busybox:v1"),
+			))),
 		),
 	}, {
 		name: "test 2 revisions no traffic split",
-		latestSvc: getServiceWithOptions(
-			getService("foo"),
-			withTrafficSplit([]string{""}, []int{100}, []bool{true}),
-			withServicePodSpecOption(withContainer()),
+		latestSvc: libtest.BuildServiceWithOptions(
+			"foo", servingtest.WithConfigSpec(buildConfiguration()),
+			libtest.WithTrafficSpec([]string{"latest"}, []int{100}, []string{""}),
 		),
-		expectedSvcList: getServiceListWithOptions(
-			withServices(
-				getService("foo"),
-				withUnwantedFieldsStripped(),
-				withServicePodSpecOption(withContainer()),
-				withTrafficSplit([]string{""}, []int{100}, []bool{true}),
-			),
+		expectedSvcList: libtest.BuildServiceListWithOptions(
+			libtest.WithService(libtest.BuildServiceWithOptions(
+				"foo", servingtest.WithConfigSpec(buildConfiguration()),
+				libtest.WithTrafficSpec([]string{"latest"}, []int{100}, []string{""}),
+			)),
 		),
-		revisionList: getRevisionListWithOptions(
-			withRevisions(
-				withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}),
-				withRevisionGeneration("1"),
-				withRevisionName("foo-rev-1"),
-				withRevisionPodSpecOption(withContainer()),
-			),
-			withRevisions(
-				withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}),
-				withRevisionGeneration("2"),
-				withRevisionName("foo-rev-2"),
-				withRevisionPodSpecOption(withContainer()),
-			),
+		revisionList: libtest.BuildRevisionListWithOptions(
+			libtest.WithRevision(*(libtest.BuildRevision("foo-rev-1",
+				servingtest.WithRevisionLabel(apiserving.ConfigurationGenerationLabelKey, "1"),
+				servingtest.WithRevisionLabel(apiserving.ServiceLabelKey, "foo"),
+			))),
+			libtest.WithRevision(*(libtest.BuildRevision("foo-rev-2",
+				servingtest.WithRevisionLabel(apiserving.ConfigurationGenerationLabelKey, "2"),
+				servingtest.WithRevisionLabel(apiserving.ServiceLabelKey, "foo"),
+			))),
 		),
-		expectedKNExport: getKNExportWithOptions(),
+		expectedKNExport: libtest.BuildKNExportWithOptions(),
 	}, {
 		name: "test 3 active revisions with traffic split with no latest revision",
-		latestSvc: getServiceWithOptions(
-			getService("foo"),
-			withTrafficSplit([]string{"foo-rev-1", "foo-rev-2", "foo-rev-3"}, []int{25, 50, 25}, []bool{false, false, false}),
-			withServiceRevisionName("foo-rev-3"),
-			withServicePodSpecOption(
-				withContainer(),
-				withEnv([]v1.EnvVar{{Name: "a", Value: "mouse"}}),
-			),
+		latestSvc: libtest.BuildServiceWithOptions(
+			"foo", servingtest.WithConfigSpec(buildConfiguration()),
+			servingtest.WithEnv(v1.EnvVar{Name: "a", Value: "mouse"}),
+			servingtest.WithBYORevisionName("foo-rev-3"),
+			libtest.WithTrafficSpec([]string{"foo-rev-1", "foo-rev-2", "foo-rev-3"}, []int{25, 50, 25}, []string{"", "", ""}),
 		),
-		expectedSvcList: getServiceListWithOptions(
-			withServices(
-				getService("foo"),
-				withUnwantedFieldsStripped(),
-				withServicePodSpecOption(
-					withContainer(),
-					withEnv([]v1.EnvVar{{Name: "a", Value: "cat"}}),
+		expectedSvcList: libtest.BuildServiceListWithOptions(
+			libtest.WithService(
+				libtest.BuildServiceWithOptions(
+					"foo", servingtest.WithConfigSpec(buildConfiguration()),
+					servingtest.WithEnv(v1.EnvVar{Name: "a", Value: "cat"}),
+					servingtest.WithBYORevisionName("foo-rev-1"),
 				),
-				withServiceRevisionName("foo-rev-1"),
 			),
-			withServices(
-				getService("foo"),
-				withUnwantedFieldsStripped(),
-				withServicePodSpecOption(
-					withContainer(),
-					withEnv([]v1.EnvVar{{Name: "a", Value: "dog"}}),
+			libtest.WithService(
+				libtest.BuildServiceWithOptions(
+					"foo", servingtest.WithConfigSpec(buildConfiguration()),
+					servingtest.WithEnv(v1.EnvVar{Name: "a", Value: "dog"}),
+					servingtest.WithBYORevisionName("foo-rev-2"),
 				),
-				withServiceRevisionName("foo-rev-2"),
 			),
-			withServices(
-				getService("foo"),
-				withUnwantedFieldsStripped(),
-				withServicePodSpecOption(
-					withContainer(),
-					withEnv([]v1.EnvVar{{Name: "a", Value: "mouse"}}),
+			libtest.WithService(
+				libtest.BuildServiceWithOptions(
+					"foo", servingtest.WithConfigSpec(buildConfiguration()),
+					servingtest.WithEnv(v1.EnvVar{Name: "a", Value: "mouse"}),
+					servingtest.WithBYORevisionName("foo-rev-3"),
+					libtest.WithTrafficSpec([]string{"foo-rev-1", "foo-rev-2", "foo-rev-3"}, []int{25, 50, 25}, []string{"", "", ""}),
 				),
-				withServiceRevisionName("foo-rev-3"),
-				withTrafficSplit([]string{"foo-rev-1", "foo-rev-2", "foo-rev-3"}, []int{25, 50, 25}, []bool{false, false, false}),
 			),
 		),
-		revisionList: getRevisionListWithOptions(
-			withRevisions(
-				withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}),
-				withRevisionGeneration("1"),
-				withRevisionName("foo-rev-1"),
-				withRevisionPodSpecOption(
-					withContainer(),
-					withEnv([]v1.EnvVar{{Name: "a", Value: "cat"}}),
-				),
-			),
-			withRevisions(
-				withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}),
-				withRevisionGeneration("2"),
-				withRevisionName("foo-rev-2"),
-				withRevisionPodSpecOption(
-					withContainer(),
-					withEnv([]v1.EnvVar{{Name: "a", Value: "dog"}}),
-				),
-			),
-			withRevisions(
-				withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}),
-				withRevisionGeneration("3"),
-				withRevisionName("foo-rev-3"),
-				withRevisionPodSpecOption(
-					withContainer(),
-					withEnv([]v1.EnvVar{{Name: "a", Value: "mouse"}}),
-				),
-			),
+		revisionList: libtest.BuildRevisionListWithOptions(
+			libtest.WithRevision(*(libtest.BuildRevision("foo-rev-1",
+				servingtest.WithRevisionLabel(apiserving.ConfigurationGenerationLabelKey, "1"),
+				servingtest.WithRevisionLabel(apiserving.ServiceLabelKey, "foo"),
+				libtest.WithRevisionEnv(v1.EnvVar{Name: "a", Value: "cat"}),
+			))),
+			libtest.WithRevision(*(libtest.BuildRevision("foo-rev-2",
+				servingtest.WithRevisionLabel(apiserving.ConfigurationGenerationLabelKey, "2"),
+				servingtest.WithRevisionLabel(apiserving.ServiceLabelKey, "foo"),
+				libtest.WithRevisionEnv(v1.EnvVar{Name: "a", Value: "dog"}),
+			))),
+			libtest.WithRevision(*(libtest.BuildRevision("foo-rev-3",
+				servingtest.WithRevisionLabel(apiserving.ConfigurationGenerationLabelKey, "3"),
+				servingtest.WithRevisionLabel(apiserving.ServiceLabelKey, "foo"),
+				libtest.WithRevisionEnv(v1.EnvVar{Name: "a", Value: "mouse"}),
+			))),
 		),
-		expectedKNExport: getKNExportWithOptions(
-			withKNRevisions(
-				withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}),
-				withRevisionName("foo-rev-1"),
-				withRevisionGeneration("1"),
-				withRevisionPodSpecOption(
-					withContainer(),
-					withEnv([]v1.EnvVar{{Name: "a", Value: "cat"}}),
-				),
-			),
-			withKNRevisions(
-				withRevisionLabels(map[string]string{apiserving.ServiceLabelKey: "foo"}),
-				withRevisionName("foo-rev-2"),
-				withRevisionGeneration("2"),
-				withRevisionPodSpecOption(
-					withContainer(),
-					withEnv([]v1.EnvVar{{Name: "a", Value: "dog"}}),
-				),
-			),
+		expectedKNExport: libtest.BuildKNExportWithOptions(
+			libtest.WithKNRevision(*(libtest.BuildRevision("foo-rev-1",
+				servingtest.WithRevisionLabel(apiserving.ConfigurationGenerationLabelKey, "1"),
+				servingtest.WithRevisionLabel(apiserving.ServiceLabelKey, "foo"),
+				libtest.WithRevisionEnv(v1.EnvVar{Name: "a", Value: "cat"}),
+			))),
+			libtest.WithKNRevision(*(libtest.BuildRevision("foo-rev-2",
+				servingtest.WithRevisionLabel(apiserving.ConfigurationGenerationLabelKey, "2"),
+				servingtest.WithRevisionLabel(apiserving.ServiceLabelKey, "foo"),
+				libtest.WithRevisionEnv(v1.EnvVar{Name: "a", Value: "dog"}),
+			))),
 		),
 	}} {
 		t.Run(tc.name, func(t *testing.T) {
@@ -278,7 +232,6 @@ func exportWithRevisionsTest(t *testing.T, tc *testCase) {
 	output, err := executeServiceExportCommand(t, tc, "export", tc.latestSvc.ObjectMeta.Name, "--with-revisions", "--mode", "export", "-o", "json")
 	assert.NilError(t, err)
 
-	stripUnwantedFields(tc.latestSvc)
 	tc.expectedKNExport.Spec.Service = *tc.latestSvc
 
 	actKNExport := &clientv1alpha1.Export{}
@@ -298,226 +251,22 @@ func executeServiceExportCommand(t *testing.T, tc *testCase, options ...string)
 	return executeServiceCommand(client, options...)
 }
 
-func stripUnwantedFields(svc *servingv1.Service) {
-	svc.ObjectMeta.Namespace = ""
-	svc.Spec.Template.Spec.Containers[0].Resources = v1.ResourceRequirements{}
-	svc.Status = servingv1.ServiceStatus{}
-	svc.ObjectMeta.CreationTimestamp = metav1.Time{}
-	for _, annotation := range IGNORED_SERVICE_ANNOTATIONS {
-		delete(svc.ObjectMeta.Annotations, annotation)
-	}
-	if len(svc.ObjectMeta.Annotations) == 0 {
-		svc.ObjectMeta.Annotations = nil
-	}
-}
-
-func getServiceListWithOptions(options ...expectedServiceListOption) *servingv1.ServiceList {
-	list := &servingv1.ServiceList{
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: "v1",
-			Kind:       "List",
-		},
-	}
-
-	for _, fn := range options {
-		fn(list)
-	}
-
-	return list
-}
-
-func withServices(svc *servingv1.Service, options ...expectedServiceOption) expectedServiceListOption {
-	return func(list *servingv1.ServiceList) {
-		list.Items = append(list.Items, *(getServiceWithOptions(svc, options...)))
-	}
-}
-
-func getRevisionListWithOptions(options ...expectedRevisionListOption) *servingv1.RevisionList {
-	list := &servingv1.RevisionList{
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: "v1",
-			Kind:       "List",
-		},
-	}
-
-	for _, fn := range options {
-		fn(list)
-	}
-
-	return list
-}
-
-func withRevisions(options ...expectedRevisionOption) expectedRevisionListOption {
-	return func(list *servingv1.RevisionList) {
-		list.Items = append(list.Items, getRevisionWithOptions(options...))
-	}
-}
-
-func getKNExportWithOptions(options ...expectedKNExportOption) *clientv1alpha1.Export {
-	knExport := &clientv1alpha1.Export{
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: "client.knative.dev/v1alpha1",
-			Kind:       "Export",
+func buildConfiguration() *servingv1.ConfigurationSpec {
+	c := &servingv1.Configuration{
+		Spec: servingv1.ConfigurationSpec{
+			Template: servingv1.RevisionTemplateSpec{
+				Spec: *revisionSpec.DeepCopy(),
+			},
 		},
 	}
-
-	for _, fn := range options {
-		fn(knExport)
-	}
-
-	return knExport
-}
-
-func withKNRevisions(options ...expectedRevisionOption) expectedKNExportOption {
-	return func(export *clientv1alpha1.Export) {
-		export.Spec.Revisions = append(export.Spec.Revisions, getRevisionWithOptions(options...))
-	}
-}
-
-func getServiceWithOptions(svc *servingv1.Service, options ...expectedServiceOption) *servingv1.Service {
-	svc.TypeMeta = metav1.TypeMeta{
-		Kind:       "service",
-		APIVersion: "serving.knative.dev/v1",
-	}
-
-	for _, fn := range options {
-		fn(svc)
-	}
-
-	return svc
-}
-func withLabels(labels map[string]string) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		svc.ObjectMeta.Labels = labels
-	}
-}
-func withConfigurationLabels(labels map[string]string) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		svc.Spec.Template.ObjectMeta.Labels = labels
-	}
-}
-func withAnnotations(Annotations map[string]string) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		svc.ObjectMeta.Annotations = Annotations
-	}
-}
-func withConfigurationAnnotations(Annotations map[string]string) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		svc.Spec.Template.ObjectMeta.Annotations = Annotations
-	}
-}
-func withServiceRevisionName(name string) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		svc.Spec.Template.ObjectMeta.Name = name
-	}
-}
-func withUnwantedFieldsStripped() expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		svc.ObjectMeta.Namespace = ""
-		svc.Spec.Template.Spec.Containers[0].Resources = v1.ResourceRequirements{}
-		svc.Status = servingv1.ServiceStatus{}
-		svc.ObjectMeta.CreationTimestamp = metav1.Time{}
-	}
-}
-func withTrafficSplit(revisions []string, percentages []int, latest []bool) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		var trafficTargets []servingv1.TrafficTarget
-		for i, rev := range revisions {
-			trafficTargets = append(trafficTargets, servingv1.TrafficTarget{
-				Percent: ptr.Int64(int64(percentages[i])),
-			})
-			if latest[i] {
-				trafficTargets[i].LatestRevision = ptr.Bool(true)
-			} else {
-				trafficTargets[i].RevisionName = rev
-				trafficTargets[i].LatestRevision = ptr.Bool(false)
-			}
-		}
-		svc.Spec.RouteSpec = servingv1.RouteSpec{
-			Traffic: trafficTargets,
-		}
-	}
-}
-func withServicePodSpecOption(options ...podSpecOption) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		svc.Spec.Template.Spec.PodSpec = getPodSpecWithOptions(options...)
-	}
+	c.SetDefaults(context.Background())
+	return &c.Spec
 }
 
-func getRevisionWithOptions(options ...expectedRevisionOption) servingv1.Revision {
-	rev := servingv1.Revision{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "Revision",
-			APIVersion: "serving.knative.dev/v1",
+func volumeSource(secretName string) v1.VolumeSource {
+	return v1.VolumeSource{
+		Secret: &v1.SecretVolumeSource{
+			SecretName: secretName,
 		},
 	}
-	for _, fn := range options {
-		fn(&rev)
-	}
-	return rev
-}
-func withRevisionGeneration(gen string) expectedRevisionOption {
-	return func(rev *servingv1.Revision) {
-		rev.ObjectMeta.Labels[apiserving.ConfigurationGenerationLabelKey] = gen
-	}
-}
-func withRevisionName(name string) expectedRevisionOption {
-	return func(rev *servingv1.Revision) {
-		rev.ObjectMeta.Name = name
-	}
-}
-func withRevisionLabels(labels map[string]string) expectedRevisionOption {
-	return func(rev *servingv1.Revision) {
-		rev.ObjectMeta.Labels = labels
-	}
-}
-func withRevisionAnnotations(Annotations map[string]string) expectedRevisionOption {
-	return func(rev *servingv1.Revision) {
-		rev.ObjectMeta.Annotations = Annotations
-	}
-}
-func withRevisionPodSpecOption(options ...podSpecOption) expectedRevisionOption {
-	return func(rev *servingv1.Revision) {
-		rev.Spec.PodSpec = getPodSpecWithOptions(options...)
-	}
-}
-
-func getPodSpecWithOptions(options ...podSpecOption) v1.PodSpec {
-	spec := v1.PodSpec{}
-	for _, fn := range options {
-		fn(&spec)
-	}
-	return spec
-}
-
-func withEnv(env []v1.EnvVar) podSpecOption {
-	return func(spec *v1.PodSpec) {
-		spec.Containers[0].Env = env
-	}
-}
-func withContainer() podSpecOption {
-	return func(spec *v1.PodSpec) {
-		spec.Containers = append(spec.Containers, v1.Container{Image: "gcr.io/foo/bar:baz"})
-	}
-}
-func withVolumeandSecrets(secretName string) podSpecOption {
-	return func(spec *v1.PodSpec) {
-		spec.Volumes = []v1.Volume{
-			{
-				Name: secretName,
-				VolumeSource: v1.VolumeSource{
-					Secret: &v1.SecretVolumeSource{
-						SecretName: secretName,
-					},
-				},
-			},
-		}
-		spec.Containers[0].VolumeMounts = []v1.VolumeMount{
-			{
-				Name:      secretName,
-				MountPath: "/mount/path",
-				ReadOnly:  true,
-			},
-		}
-	}
 }
diff --git a/pkg/serving/v1/apply_test.go b/pkg/serving/v1/apply_test.go
new file mode 100644
index 00000000..d64093b9
--- /dev/null
+++ b/pkg/serving/v1/apply_test.go
@@ -0,0 +1,233 @@
+// 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.
+
+package v1
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"gotest.tools/assert"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	clienttesting "k8s.io/client-go/testing"
+	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+	"sigs.k8s.io/yaml"
+
+	"knative.dev/client/pkg/util"
+)
+
+func TestApplyServiceWithNoImage(t *testing.T) {
+	_, client := setup()
+	serviceFaulty := newService("faulty-service")
+	_, err := client.ApplyService(serviceFaulty)
+	assert.Assert(t, err != nil)
+	assert.Assert(t, util.ContainsAll(err.Error(), "image name"))
+}
+
+func TestApplyServiceCreate(t *testing.T) {
+	serving, client := setup()
+
+	serviceNew := newServiceWithImage("new-service", "test/image")
+	serving.AddReactor("get", "services",
+		func(a clienttesting.Action) (bool, runtime.Object, error) {
+			name := a.(clienttesting.GetAction).GetName()
+			return true, nil, errors.NewNotFound(servingv1.Resource("service"), name)
+		})
+
+	serving.AddReactor("create", "services",
+		func(a clienttesting.Action) (bool, runtime.Object, error) {
+			assert.Equal(t, testNamespace, a.GetNamespace())
+			return true, serviceNew, nil
+		})
+
+	hasChanged, err := client.ApplyService(serviceNew)
+	assert.NilError(t, err)
+	assert.Assert(t, hasChanged, "service has changed")
+}
+
+func TestApplyServiceUpdate(t *testing.T) {
+	serving, client := setup()
+
+	serviceOld := newServiceWithImage("my-service", "test/image")
+	serviceNew := newServiceWithImage("my-service", "test/new-image")
+	serving.AddReactor("get", "services",
+		func(a clienttesting.Action) (bool, runtime.Object, error) {
+			name := a.(clienttesting.GetAction).GetName()
+			assert.Equal(t, name, "my-service")
+			return true, serviceOld, nil
+		})
+
+	serving.AddReactor("patch", "services",
+		func(a clienttesting.Action) (bool, runtime.Object, error) {
+			serviceNew.Generation = 2
+			serviceNew.Status.ObservedGeneration = 1
+			return true, serviceNew, nil
+		})
+
+	hasChanged, err := client.ApplyService(serviceNew)
+	assert.NilError(t, err)
+	assert.Assert(t, hasChanged, "service has changed")
+}
+
+func newServiceWithImage(name string, image string) *servingv1.Service {
+	svc := newService(name)
+	svc.Spec = servingv1.ServiceSpec{
+		ConfigurationSpec: servingv1.ConfigurationSpec{
+			Template: servingv1.RevisionTemplateSpec{
+				Spec: servingv1.RevisionSpec{
+					PodSpec: corev1.PodSpec{
+						Containers: []corev1.Container{
+							{
+								Image: image,
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	return svc
+}
+
+func TestExtractUserContainer(t *testing.T) {
+	tests := []struct {
+		name    string
+		service string
+		want    string
+	}{
+		{"Simple Service",
+			`
+spec:
+  template:
+    spec:
+      containers:
+        - image: gcr.io/foo/bar:baz
+`,
+			`
+image: gcr.io/foo/bar:baz
+`,
+		},
+		{
+			"No template",
+			`
+spec:
+`,
+			"",
+		}, {
+			"No template spec",
+			`
+spec:
+  template:
+`,
+			"",
+		},
+		{
+			"No template spec containers",
+			`
+spec:
+  template:
+    spec:
+`,
+			"",
+		},
+		{
+			"Empty template spec containers",
+			`
+spec:
+  template:
+    spec:
+      containers: []
+`,
+			"",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var serviceMap map[string]interface{}
+			yaml.Unmarshal([]byte(tt.service), &serviceMap)
+
+			got := extractUserContainer(serviceMap)
+
+			if tt.want == "" {
+				assert.Assert(t, got == nil)
+			} else {
+				var expectedMap map[string]interface{}
+				yaml.Unmarshal([]byte(tt.want), &expectedMap)
+				if !reflect.DeepEqual(got, expectedMap) {
+					t.Errorf("extractUserContainer() = %v, want %v", got, expectedMap)
+				}
+			}
+		})
+	}
+}
+
+func TestCleanupServiceUnstructured(t *testing.T) {
+	tests := []struct {
+		name    string
+		service string
+		want    string
+	}{
+		{"Simple Service with fields to remove",
+			`
+apiVersion: serving.knative.dev/v1
+kind: Service
+metadata:
+  name: foo
+  creationTimestamp: "2020-10-22T08:16:37Z"
+spec:
+  template:
+    metadata:
+      name: "bar"
+      creationTimestamp: null
+    spec:
+      containers:
+      - image: gcr.io/foo/bar:baz
+        name: "bla"
+        resources: {}
+status:
+  observedGeneration: 1
+`,
+			`
+apiVersion: serving.knative.dev/v1
+kind: Service
+metadata:
+  name: foo
+spec:
+  template:
+    metadata:
+      name: "bar"
+    spec:
+      containers:
+      - image: gcr.io/foo/bar:baz
+`,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ud := &unstructured.Unstructured{}
+			assert.NilError(t, yaml.Unmarshal([]byte(tt.service), ud))
+			cleanupServiceUnstructured(ud)
+
+			expectedMap := &unstructured.Unstructured{}
+			yaml.Unmarshal([]byte(tt.want), &expectedMap)
+			if !reflect.DeepEqual(ud, expectedMap) {
+				t.Errorf("cleanupServiceUnstructured(): " + cmp.Diff(ud, expectedMap))
+			}
+		})
+	}
+}
diff --git a/pkg/serving/v1/client_mock_test.go b/pkg/serving/v1/client_mock_test.go
index 10f20b70..7ec9738a 100644
--- a/pkg/serving/v1/client_mock_test.go
+++ b/pkg/serving/v1/client_mock_test.go
@@ -37,6 +37,7 @@ func TestMockKnClient(t *testing.T) {
 	recorder.ListServices(mock.Any(), nil, nil)
 	recorder.CreateService(&servingv1.Service{}, nil)
 	recorder.UpdateService(&servingv1.Service{}, nil)
+	recorder.ApplyService(&servingv1.Service{}, true, nil)
 	recorder.DeleteService("hello", time.Duration(10)*time.Second, nil)
 	recorder.WaitForService("hello", time.Duration(10)*time.Second, wait.NoopMessageCallback(), nil, 10*time.Second)
 	recorder.GetRevision("hello", nil, nil)
@@ -52,6 +53,7 @@ func TestMockKnClient(t *testing.T) {
 	client.ListServices(WithLabel("foo", "bar"))
 	client.CreateService(&servingv1.Service{})
 	client.UpdateService(&servingv1.Service{})
+	client.ApplyService(&servingv1.Service{})
 	client.DeleteService("hello", time.Duration(10)*time.Second)
 	client.WaitForService("hello", time.Duration(10)*time.Second, wait.NoopMessageCallback())
 	client.GetRevision("hello")
diff --git a/test/e2e/service_apply_test.go b/test/e2e/service_apply_test.go
new file mode 100644
index 00000000..5c68ed17
--- /dev/null
+++ b/test/e2e/service_apply_test.go
@@ -0,0 +1,62 @@
+// Copyright 2019 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.
+
+// +build e2e
+// +build !eventing
+
+package e2e
+
+import (
+	"testing"
+
+	"gotest.tools/assert"
+
+	pkgtest "knative.dev/pkg/test"
+
+	"knative.dev/client/lib/test"
+	"knative.dev/client/pkg/util"
+)
+
+func TestServiceApply(t *testing.T) {
+	t.Parallel()
+	it, err := test.NewKnTest()
+	assert.NilError(t, err)
+	defer func() {
+		assert.NilError(t, it.Teardown())
+	}()
+
+	r := test.NewKnRunResultCollector(t, it)
+	defer r.DumpIfFailed()
+
+	t.Log("apply hello service (initially)")
+	result := serviceApply(r, "hello-apply")
+	r.AssertNoError(result)
+	assert.Check(r.T(), util.ContainsAllIgnoreCase(result.Stdout, "creating", "service", "hello-apply", "ready", "http"))
+	t.Log("apply hello service (unchanged)")
+	result = serviceApply(r, "hello-apply")
+	r.AssertNoError(result)
+	assert.Check(r.T(), util.ContainsAllIgnoreCase(result.Stdout, "no changes", "service", "hello-apply", "http"))
+
+	t.Log("apply hello service (update env)")
+	result = serviceApply(r, "hello-apply", "--env", "tik=tok")
+	r.AssertNoError(result)
+	assert.Check(r.T(), util.ContainsAllIgnoreCase(result.Stdout, "applying", "service", "hello-apply", "ready", "http"))
+}
+
+// ServiceApply applies a test service and returns the output
+func serviceApply(r *test.KnRunResultCollector, serviceName string, args ...string) test.KnRunResult {
+	fullArgs := append([]string{}, "service", "apply", serviceName, "--image", pkgtest.ImagePath("helloworld"))
+	fullArgs = append(fullArgs, args...)
+	return r.KnTest().Kn().Run(fullArgs...)
+}
diff --git a/test/e2e/service_export_test.go b/test/e2e/service_export_test.go
index 71ea161a..d15a9bd9 100644
--- a/test/e2e/service_export_test.go
+++ b/test/e2e/service_export_test.go
@@ -25,25 +25,17 @@ import (
 
 	"gotest.tools/assert"
 
-	"k8s.io/apimachinery/pkg/util/intstr"
-	"knative.dev/pkg/ptr"
 	"sigs.k8s.io/yaml"
 
 	"knative.dev/client/lib/test"
 
 	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	clientv1alpha1 "knative.dev/client/pkg/apis/client/v1alpha1"
 	pkgtest "knative.dev/pkg/test"
 	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+	servingtest "knative.dev/serving/pkg/testing/v1"
 )
 
-type expectedServiceOption func(*servingv1.Service)
-type expectedRevisionOption func(*servingv1.Revision)
-type expectedServiceListOption func(*servingv1.ServiceList)
-type expectedKNExportOption func(*clientv1alpha1.Export)
-type podSpecOption func(*corev1.PodSpec)
-
 func TestServiceExport(t *testing.T) {
 	//FIXME: enable once 0.19 is available
 	// see: https://github.com/knative/serving/pull/9685
@@ -64,105 +56,75 @@ func TestServiceExport(t *testing.T) {
 	serviceCreateWithOptions(r, "hello", "--revision-name", "rev1")
 
 	t.Log("export service-revision1 and compare")
-	serviceExport(r, "hello", getServiceWithOptions(
-		withServiceName("hello"),
-		withServiceRevisionName("hello-rev1"),
-		withConfigurationAnnotations(map[string]string{
-			"client.knative.dev/user-image": pkgtest.ImagePath("helloworld"),
-		}),
-		withServicePodSpecOption(withContainer()),
+	serviceExport(r, "hello", test.BuildServiceWithOptions("hello",
+		servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+		servingtest.WithBYORevisionName("hello-rev1"),
+		test.WithRevisionAnnotations(map[string]string{"client.knative.dev/user-image": pkgtest.ImagePath("helloworld")}),
 	), "-o", "json")
 
 	t.Log("update service - add env variable")
 	test.ServiceUpdate(r, "hello", "--env", "a=mouse", "--revision-name", "rev2", "--no-lock-to-digest")
-	serviceExport(r, "hello", getServiceWithOptions(
-		withServiceName("hello"),
-		withServiceRevisionName("hello-rev2"),
-		withServicePodSpecOption(
-			withContainer(),
-			withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}),
-		),
+	serviceExport(r, "hello", test.BuildServiceWithOptions("hello",
+		servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+		servingtest.WithBYORevisionName("hello-rev2"),
+		servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}),
 	), "-o", "json")
 
 	t.Log("export service-revision2 with kubernetes-resources")
-	serviceExportWithServiceList(r, "hello", getServiceListWithOptions(
-		withServices(
-			withServiceName("hello"),
-			withServiceRevisionName("hello-rev2"),
-			withTrafficSplit([]string{"latest"}, []int{100}, []string{""}),
-			withServicePodSpecOption(
-				withContainer(),
-				withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}),
-			),
-		),
+	serviceExportWithServiceList(r, "hello", test.BuildServiceListWithOptions(
+		test.WithService(test.BuildServiceWithOptions("hello",
+			servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+			servingtest.WithBYORevisionName("hello-rev2"),
+			test.WithTrafficSpec([]string{"latest"}, []int{100}, []string{""}),
+			servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}),
+		)),
 	), "--with-revisions", "--mode", "replay", "-o", "yaml")
 
 	t.Log("export service-revision2 with revisions-only")
-	serviceExportWithRevisionList(r, "hello", getServiceWithOptions(
-		withServiceName("hello"),
-		withServiceRevisionName("hello-rev2"),
-		withTrafficSplit([]string{"latest"}, []int{100}, []string{""}),
-		withServicePodSpecOption(
-			withContainer(),
-			withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}),
-		),
-	), getKNExportWithOptions(), "--with-revisions", "--mode", "export", "-o", "yaml")
+	serviceExportWithRevisionList(r, "hello", test.BuildServiceWithOptions("hello",
+		servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+		servingtest.WithBYORevisionName("hello-rev2"),
+		test.WithTrafficSpec([]string{"latest"}, []int{100}, []string{""}),
+		servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}),
+	), test.BuildKNExportWithOptions(), "--with-revisions", "--mode", "export", "-o", "yaml")
 
 	t.Log("update service with tag and split traffic")
 	test.ServiceUpdate(r, "hello", "--tag", "hello-rev1=candidate", "--traffic", "candidate=2%,@latest=98%")
 
 	t.Log("export service-revision2 after tagging kubernetes-resources")
-	serviceExportWithServiceList(r, "hello", getServiceListWithOptions(
-		withServices(
-			withServiceName("hello"),
-			withServiceRevisionName("hello-rev1"),
-			withConfigurationAnnotations(map[string]string{
+	serviceExportWithServiceList(r, "hello", test.BuildServiceListWithOptions(
+		test.WithService(test.BuildServiceWithOptions("hello",
+			servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+			servingtest.WithBYORevisionName("hello-rev1"),
+			test.WithRevisionAnnotations(map[string]string{
 				"client.knative.dev/user-image": pkgtest.ImagePath("helloworld"),
 				"serving.knative.dev/routes":    "hello",
 			}),
-			withServicePodSpecOption(
-				withContainer(),
-			),
-		),
-		withServices(
-			withServiceName("hello"),
-			withServiceRevisionName("hello-rev2"),
-			withTrafficSplit([]string{"latest", "hello-rev1"}, []int{98, 2}, []string{"", "candidate"}),
-			withServicePodSpecOption(
-				withContainer(),
-				withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}),
-			),
-		),
+		)),
+		test.WithService(test.BuildServiceWithOptions("hello",
+			servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+			servingtest.WithBYORevisionName("hello-rev2"),
+			test.WithTrafficSpec([]string{"latest", "hello-rev1"}, []int{98, 2}, []string{"", "candidate"}),
+			servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}),
+		)),
 	), "--with-revisions", "--mode", "replay", "-o", "yaml")
 
 	t.Log("export service-revision2 after tagging with revisions-only")
-	serviceExportWithRevisionList(r, "hello", getServiceWithOptions(
-		withServiceName("hello"),
-		withServiceRevisionName("hello-rev2"),
-		withTrafficSplit([]string{"latest", "hello-rev1"}, []int{98, 2}, []string{"", "candidate"}),
-		withServicePodSpecOption(
-			withContainer(),
-			withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}),
-		),
-	), getKNExportWithOptions(
-		withRevisions(
-			withRevisionName("hello-rev1"),
-			withRevisionAnnotations(
-				map[string]string{
-					"client.knative.dev/user-image": pkgtest.ImagePath("helloworld"),
-					"serving.knative.dev/routes":    "hello",
-				}),
-			withRevisionLabels(
-				map[string]string{
-					"serving.knative.dev/configuration":           "hello",
-					"serving.knative.dev/configurationGeneration": "1",
-					"serving.knative.dev/routingState":            "active",
-					"serving.knative.dev/service":                 "hello",
-				}),
-			withRevisionPodSpecOption(
-				withContainer(),
-			),
-		),
+	serviceExportWithRevisionList(r, "hello", test.BuildServiceWithOptions("hello",
+		servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+		servingtest.WithBYORevisionName("hello-rev2"),
+		test.WithTrafficSpec([]string{"latest", "hello-rev1"}, []int{98, 2}, []string{"", "candidate"}),
+		servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}),
+	), test.BuildKNExportWithOptions(
+		test.WithKNRevision(*(test.BuildRevision("hello-rev1",
+			servingtest.WithRevisionAnn("client.knative.dev/user-image", pkgtest.ImagePath("helloworld")),
+			servingtest.WithRevisionAnn("serving.knative.dev/routes", "hello"),
+			servingtest.WithRevisionLabel("serving.knative.dev/configuration", "hello"),
+			servingtest.WithRevisionLabel("serving.knative.dev/configurationGeneration", "1"),
+			servingtest.WithRevisionLabel("serving.knative.dev/routingState", "active"),
+			servingtest.WithRevisionLabel("serving.knative.dev/service", "hello"),
+			test.WithRevisionImage(pkgtest.ImagePath("helloworld")),
+		))),
 	), "--with-revisions", "--mode", "export", "-o", "yaml")
 
 	t.Log("update service - untag, add env variable, traffic split and system revision name")
@@ -170,148 +132,105 @@ func TestServiceExport(t *testing.T) {
 	test.ServiceUpdate(r, "hello", "--env", "b=cat", "--revision-name", "hello-rev3", "--traffic", "hello-rev1=30,hello-rev2=30,hello-rev3=40")
 
 	t.Log("export service-revision3 with kubernetes-resources")
-	serviceExportWithServiceList(r, "hello", getServiceListWithOptions(
-		withServices(
-			withServiceName("hello"),
-			withConfigurationAnnotations(map[string]string{
+	serviceExportWithServiceList(r, "hello", test.BuildServiceListWithOptions(
+		test.WithService(test.BuildServiceWithOptions("hello",
+			servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+			test.WithRevisionAnnotations(map[string]string{
 				"client.knative.dev/user-image": pkgtest.ImagePath("helloworld"),
 				"serving.knative.dev/routes":    "hello",
 			}),
-			withServiceRevisionName("hello-rev1"),
-			withServicePodSpecOption(
-				withContainer(),
-			),
+			servingtest.WithBYORevisionName("hello-rev1"),
 		),
-		withServices(
-			withServiceName("hello"),
-			withServiceRevisionName("hello-rev2"),
-			withConfigurationAnnotations(map[string]string{
+		),
+		test.WithService(test.BuildServiceWithOptions("hello",
+			servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+			servingtest.WithBYORevisionName("hello-rev2"),
+			test.WithRevisionAnnotations(map[string]string{
 				"serving.knative.dev/routes": "hello",
 			}),
-			withServicePodSpecOption(
-				withContainer(),
-				withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}),
-			),
+			servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}),
 		),
-		withServices(
-			withServiceName("hello"),
-			withServiceRevisionName("hello-rev3"),
-			withTrafficSplit([]string{"hello-rev1", "hello-rev2", "hello-rev3"}, []int{30, 30, 40}, []string{"", "", ""}),
-			withServicePodSpecOption(
-				withContainer(),
-				withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}, {Name: "b", Value: "cat"}}),
-			),
 		),
-	), "--with-revisions", "--mode", "replay", "-o", "yaml")
+		test.WithService(test.BuildServiceWithOptions("hello",
+			servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+			servingtest.WithBYORevisionName("hello-rev3"),
+			test.WithTrafficSpec([]string{"hello-rev1", "hello-rev2", "hello-rev3"}, []int{30, 30, 40}, []string{"", "", ""}),
+			servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}, corev1.EnvVar{Name: "b", Value: "cat"}),
+		),
+		)), "--with-revisions", "--mode", "replay", "-o", "yaml")
 
 	t.Log("export service-revision3 with revisions-only")
-	serviceExportWithRevisionList(r, "hello", getServiceWithOptions(
-		withServiceName("hello"),
-		withServiceRevisionName("hello-rev3"),
-		withTrafficSplit([]string{"hello-rev1", "hello-rev2", "hello-rev3"}, []int{30, 30, 40}, []string{"", "", ""}),
-		withServicePodSpecOption(
-			withContainer(),
-			withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}, {Name: "b", Value: "cat"}}),
-		),
-	), getKNExportWithOptions(
-		withRevisions(
-			withRevisionName("hello-rev1"),
-			withRevisionAnnotations(
-				map[string]string{
-					"client.knative.dev/user-image": pkgtest.ImagePath("helloworld"),
-					"serving.knative.dev/routes":    "hello",
-				}),
-			withRevisionLabels(
-				map[string]string{
-					"serving.knative.dev/configuration":           "hello",
-					"serving.knative.dev/configurationGeneration": "1",
-					"serving.knative.dev/routingState":            "active",
-					"serving.knative.dev/service":                 "hello",
-				}),
-			withRevisionPodSpecOption(
-				withContainer(),
-			),
-		),
-		withRevisions(
-			withRevisionName("hello-rev2"),
-			withRevisionAnnotations(
-				map[string]string{
-					"serving.knative.dev/routes": "hello",
-				}),
-			withRevisionLabels(
-				map[string]string{
-					"serving.knative.dev/configuration":           "hello",
-					"serving.knative.dev/configurationGeneration": "2",
-					"serving.knative.dev/routingState":            "active",
-					"serving.knative.dev/service":                 "hello",
-				}),
-			withRevisionPodSpecOption(
-				withContainer(),
-				withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}),
-			),
-		),
+	serviceExportWithRevisionList(r, "hello", test.BuildServiceWithOptions("hello",
+		servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+		test.WithTrafficSpec([]string{"hello-rev1", "hello-rev2", "hello-rev3"}, []int{30, 30, 40}, []string{"", "", ""}),
+		servingtest.WithBYORevisionName("hello-rev3"),
+		servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}, corev1.EnvVar{Name: "b", Value: "cat"}),
+	), test.BuildKNExportWithOptions(
+		test.WithKNRevision(*(test.BuildRevision("hello-rev1",
+			servingtest.WithRevisionAnn("client.knative.dev/user-image", pkgtest.ImagePath("helloworld")),
+			servingtest.WithRevisionAnn("serving.knative.dev/routes", "hello"),
+			servingtest.WithRevisionLabel("serving.knative.dev/configuration", "hello"),
+			servingtest.WithRevisionLabel("serving.knative.dev/configurationGeneration", "1"),
+			servingtest.WithRevisionLabel("serving.knative.dev/routingState", "active"),
+			servingtest.WithRevisionLabel("serving.knative.dev/service", "hello"),
+			test.WithRevisionImage(pkgtest.ImagePath("helloworld")),
+		))),
+		test.WithKNRevision(*(test.BuildRevision("hello-rev2",
+			servingtest.WithRevisionAnn("serving.knative.dev/routes", "hello"),
+			servingtest.WithRevisionLabel("serving.knative.dev/configuration", "hello"),
+			servingtest.WithRevisionLabel("serving.knative.dev/configurationGeneration", "2"),
+			servingtest.WithRevisionLabel("serving.knative.dev/routingState", "active"),
+			servingtest.WithRevisionLabel("serving.knative.dev/service", "hello"),
+			test.WithRevisionImage(pkgtest.ImagePath("helloworld")),
+			test.WithRevisionEnv(corev1.EnvVar{Name: "a", Value: "mouse"}),
+		))),
 	), "--with-revisions", "--mode", "export", "-o", "yaml")
 
 	t.Log("send all traffic to revision 2")
 	test.ServiceUpdate(r, "hello", "--traffic", "hello-rev2=100")
 
 	t.Log("export kubernetes-resources - all traffic to revision 2")
-	serviceExportWithServiceList(r, "hello", getServiceListWithOptions(
-		withServices(
-			withServiceName("hello"),
-			withServiceRevisionName("hello-rev2"),
-			withConfigurationAnnotations(map[string]string{
+	serviceExportWithServiceList(r, "hello", test.BuildServiceListWithOptions(
+		test.WithService(test.BuildServiceWithOptions("hello",
+			servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+			servingtest.WithBYORevisionName("hello-rev2"),
+			test.WithRevisionAnnotations(map[string]string{
 				"serving.knative.dev/routes": "hello",
 			}),
-			withServicePodSpecOption(
-				withContainer(),
-				withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}),
-			),
+			servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}),
+		),
+		),
+		test.WithService(test.BuildServiceWithOptions("hello",
+			servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+			test.WithTrafficSpec([]string{"hello-rev2"}, []int{100}, []string{""}),
+			servingtest.WithBYORevisionName("hello-rev3"),
+			servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}, corev1.EnvVar{Name: "b", Value: "cat"}),
 		),
-		withServices(
-			withServiceName("hello"),
-			withServiceRevisionName("hello-rev3"),
-			withTrafficSplit([]string{"hello-rev2"}, []int{100}, []string{""}),
-			withServicePodSpecOption(
-				withContainer(),
-				withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}, {Name: "b", Value: "cat"}}),
-			),
 		),
 	), "--with-revisions", "--mode", "replay", "-o", "yaml")
 
 	t.Log("export revisions-only - all traffic to revision 2")
-	serviceExportWithRevisionList(r, "hello", getServiceWithOptions(
-		withServiceName("hello"),
-		withServiceRevisionName("hello-rev3"),
-		withTrafficSplit([]string{"hello-rev2"}, []int{100}, []string{""}),
-		withServicePodSpecOption(
-			withContainer(),
-			withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}, {Name: "b", Value: "cat"}}),
-		),
-	), getKNExportWithOptions(
-		withRevisions(
-			withRevisionName("hello-rev2"),
-			withRevisionAnnotations(map[string]string{
-				"serving.knative.dev/routes": "hello",
-			}),
-			withRevisionLabels(
-				map[string]string{
-					"serving.knative.dev/configuration":           "hello",
-					"serving.knative.dev/configurationGeneration": "2",
-					"serving.knative.dev/routingState":            "active",
-					"serving.knative.dev/service":                 "hello",
-				}),
-			withRevisionPodSpecOption(
-				withContainer(),
-				withEnv([]corev1.EnvVar{{Name: "a", Value: "mouse"}}),
-			),
-		),
+	serviceExportWithRevisionList(r, "hello", test.BuildServiceWithOptions("hello",
+		servingtest.WithConfigSpec(test.BuildConfigurationSpec()),
+		servingtest.WithBYORevisionName("hello-rev3"),
+		test.WithTrafficSpec([]string{"hello-rev2"}, []int{100}, []string{""}),
+		servingtest.WithEnv(corev1.EnvVar{Name: "a", Value: "mouse"}, corev1.EnvVar{Name: "b", Value: "cat"}),
+	), test.BuildKNExportWithOptions(
+		test.WithKNRevision(*(test.BuildRevision("hello-rev2",
+			servingtest.WithRevisionAnn("serving.knative.dev/routes", "hello"),
+			servingtest.WithRevisionLabel("serving.knative.dev/configuration", "hello"),
+			servingtest.WithRevisionLabel("serving.knative.dev/configurationGeneration", "2"),
+			servingtest.WithRevisionLabel("serving.knative.dev/routingState", "active"),
+			servingtest.WithRevisionLabel("serving.knative.dev/service", "hello"),
+			test.WithRevisionImage(pkgtest.ImagePath("helloworld")),
+			test.WithRevisionEnv(corev1.EnvVar{Name: "a", Value: "mouse"}),
+		))),
 	), "--with-revisions", "--mode", "export", "-o", "yaml")
 }
 
 // Private methods
 
-func serviceExport(r *test.KnRunResultCollector, serviceName string, expService servingv1.Service, options ...string) {
+func serviceExport(r *test.KnRunResultCollector, serviceName string, expService *servingv1.Service, options ...string) {
 	command := []string{"service", "export", serviceName}
 	command = append(command, options...)
 	out := r.KnTest().Kn().Run(command...)
@@ -319,7 +238,7 @@ func serviceExport(r *test.KnRunResultCollector, serviceName string, expService
 	r.AssertNoError(out)
 }
 
-func serviceExportWithServiceList(r *test.KnRunResultCollector, serviceName string, expServiceList servingv1.ServiceList, options ...string) {
+func serviceExportWithServiceList(r *test.KnRunResultCollector, serviceName string, expServiceList *servingv1.ServiceList, options ...string) {
 	command := []string{"service", "export", serviceName}
 	command = append(command, options...)
 	out := r.KnTest().Kn().Run(command...)
@@ -327,7 +246,7 @@ func serviceExportWithServiceList(r *test.KnRunResultCollector, serviceName stri
 	r.AssertNoError(out)
 }
 
-func serviceExportWithRevisionList(r *test.KnRunResultCollector, serviceName string, expService servingv1.Service, knExport clientv1alpha1.Export, options ...string) {
+func serviceExportWithRevisionList(r *test.KnRunResultCollector, serviceName string, expService *servingv1.Service, knExport *clientv1alpha1.Export, options ...string) {
 	command := []string{"service", "export", serviceName}
 	command = append(command, options...)
 	out := r.KnTest().Kn().Run(command...)
@@ -337,195 +256,25 @@ func serviceExportWithRevisionList(r *test.KnRunResultCollector, serviceName str
 
 // Private functions
 
-func validateExportedService(t *testing.T, it *test.KnTest, out string, expService servingv1.Service) {
+func validateExportedService(t *testing.T, it *test.KnTest, out string, expService *servingv1.Service) {
 	actSvc := servingv1.Service{}
 	err := json.Unmarshal([]byte(out), &actSvc)
 	assert.NilError(t, err)
-	assert.DeepEqual(t, &expService, &actSvc)
+	assert.DeepEqual(t, expService, &actSvc)
 }
 
-func validateExportedServiceList(t *testing.T, it *test.KnTest, out string, expServiceList servingv1.ServiceList) {
+func validateExportedServiceList(t *testing.T, it *test.KnTest, out string, expServiceList *servingv1.ServiceList) {
 	actSvcList := servingv1.ServiceList{}
 	err := yaml.Unmarshal([]byte(out), &actSvcList)
 	assert.NilError(t, err)
-	assert.DeepEqual(t, &expServiceList, &actSvcList)
+	assert.DeepEqual(t, expServiceList, &actSvcList)
 }
 
-func validateExportedServiceandRevisionList(t *testing.T, it *test.KnTest, out string, expService servingv1.Service, knExport clientv1alpha1.Export) {
+func validateExportedServiceandRevisionList(t *testing.T, it *test.KnTest, out string, expService *servingv1.Service, knExport *clientv1alpha1.Export) {
 	actSvc := clientv1alpha1.Export{}
 	err := yaml.Unmarshal([]byte(out), &actSvc)
 	assert.NilError(t, err)
 
-	knExport.Spec.Service = expService
-	assert.DeepEqual(t, &knExport, &actSvc)
-}
-
-func getServiceListWithOptions(options ...expectedServiceListOption) servingv1.ServiceList {
-	list := servingv1.ServiceList{
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: "v1",
-			Kind:       "List",
-		},
-	}
-
-	for _, fn := range options {
-		fn(&list)
-	}
-	return list
-}
-
-func withServices(options ...expectedServiceOption) expectedServiceListOption {
-	return func(list *servingv1.ServiceList) {
-		list.Items = append(list.Items, getServiceWithOptions(options...))
-	}
-}
-
-func getKNExportWithOptions(options ...expectedKNExportOption) clientv1alpha1.Export {
-	knExport := clientv1alpha1.Export{
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: "client.knative.dev/v1alpha1",
-			Kind:       "Export",
-		},
-	}
-
-	for _, fn := range options {
-		fn(&knExport)
-	}
-
-	return knExport
-}
-
-func withRevisions(options ...expectedRevisionOption) expectedKNExportOption {
-	return func(export *clientv1alpha1.Export) {
-		export.Spec.Revisions = append(export.Spec.Revisions, getRevisionWithOptions(options...))
-	}
-}
-
-func getServiceWithOptions(options ...expectedServiceOption) servingv1.Service {
-	svc := servingv1.Service{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "Service",
-			APIVersion: "serving.knative.dev/v1",
-		},
-	}
-
-	for _, fn := range options {
-		fn(&svc)
-	}
-	svc.Spec.Template.Spec.ContainerConcurrency = ptr.Int64(int64(0))
-	svc.Spec.Template.Spec.TimeoutSeconds = ptr.Int64(int64(300))
-
-	return svc
-}
-func withServiceName(name string) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		svc.ObjectMeta.Name = name
-	}
-}
-func withConfigurationAnnotations(annotations map[string]string) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		svc.Spec.Template.ObjectMeta.Annotations = annotations
-	}
-}
-func withServiceRevisionName(name string) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		svc.Spec.Template.ObjectMeta.Name = name
-	}
-}
-func withTrafficSplit(revisions []string, percentages []int, tags []string) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		var trafficTargets []servingv1.TrafficTarget
-		for i, rev := range revisions {
-			trafficTargets = append(trafficTargets, servingv1.TrafficTarget{
-				Percent: ptr.Int64(int64(percentages[i])),
-			})
-			if tags[i] != "" {
-				trafficTargets[i].Tag = tags[i]
-			}
-			if rev == "latest" {
-				trafficTargets[i].LatestRevision = ptr.Bool(true)
-			} else {
-				trafficTargets[i].RevisionName = rev
-				trafficTargets[i].LatestRevision = ptr.Bool(false)
-			}
-		}
-		svc.Spec.RouteSpec = servingv1.RouteSpec{
-			Traffic: trafficTargets,
-		}
-	}
-}
-func withServicePodSpecOption(options ...podSpecOption) expectedServiceOption {
-	return func(svc *servingv1.Service) {
-		svc.Spec.Template.Spec.PodSpec = getPodSpecWithOptions(options...)
-	}
-}
-func getRevisionWithOptions(options ...expectedRevisionOption) servingv1.Revision {
-	rev := servingv1.Revision{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "Revision",
-			APIVersion: "serving.knative.dev/v1",
-		},
-	}
-	for _, fn := range options {
-		fn(&rev)
-	}
-	rev.Spec.ContainerConcurrency = ptr.Int64(int64(0))
-	rev.Spec.TimeoutSeconds = ptr.Int64(int64(300))
-	return rev
-}
-func withRevisionName(name string) expectedRevisionOption {
-	return func(rev *servingv1.Revision) {
-		rev.ObjectMeta.Name = name
-	}
-}
-func withRevisionLabels(labels map[string]string) expectedRevisionOption {
-	return func(rev *servingv1.Revision) {
-		rev.ObjectMeta.Labels = labels
-	}
-}
-func withRevisionAnnotations(Annotations map[string]string) expectedRevisionOption {
-	return func(rev *servingv1.Revision) {
-		rev.ObjectMeta.Annotations = Annotations
-	}
-}
-func withRevisionPodSpecOption(options ...podSpecOption) expectedRevisionOption {
-	return func(rev *servingv1.Revision) {
-		rev.Spec.PodSpec = getPodSpecWithOptions(options...)
-	}
-}
-
-func getPodSpecWithOptions(options ...podSpecOption) corev1.PodSpec {
-	spec := corev1.PodSpec{}
-	for _, fn := range options {
-		fn(&spec)
-	}
-	// Service links are disabled by default now see https://github.com/knative/serving/pull/9685
-	spec.EnableServiceLinks = ptr.Bool(false)
-	return spec
-}
-
-func withEnv(env []corev1.EnvVar) podSpecOption {
-	return func(spec *corev1.PodSpec) {
-		spec.Containers[0].Env = env
-	}
-}
-
-func withContainer() podSpecOption {
-	return func(spec *corev1.PodSpec) {
-		spec.Containers = []corev1.Container{
-			{
-				Name:      "user-container",
-				Image:     pkgtest.ImagePath("helloworld"),
-				Resources: corev1.ResourceRequirements{},
-				ReadinessProbe: &corev1.Probe{
-					SuccessThreshold: int32(1),
-					Handler: corev1.Handler{
-						TCPSocket: &corev1.TCPSocketAction{
-							Port: intstr.FromInt(0),
-						},
-					},
-				},
-			},
-		}
-	}
+	knExport.Spec.Service = *expService
+	assert.DeepEqual(t, knExport, &actSvc)
 }
diff --git a/vendor/knative.dev/serving/pkg/testing/v1/configuration.go b/vendor/knative.dev/serving/pkg/testing/v1/configuration.go
new file mode 100644
index 00000000..0c7d88b2
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/testing/v1/configuration.go
@@ -0,0 +1,133 @@
+/*
+Copyright 2019 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.
+*/
+
+package v1
+
+import (
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"knative.dev/pkg/kmeta"
+	"knative.dev/pkg/ptr"
+	v1 "knative.dev/serving/pkg/apis/serving/v1"
+)
+
+// ConfigOption enables further configuration of a Configuration.
+type ConfigOption func(*v1.Configuration)
+
+// WithConfigReadinessProbe sets the provided probe to be the readiness
+// probe on the configuration.
+func WithConfigReadinessProbe(p *corev1.Probe) ConfigOption {
+	return func(cfg *v1.Configuration) {
+		cfg.Spec.Template.Spec.Containers[0].ReadinessProbe = p
+	}
+}
+
+// WithConfigImage sets the container image to be the provided string.
+func WithConfigImage(img string) ConfigOption {
+	return func(cfg *v1.Configuration) {
+		cfg.Spec.Template.Spec.Containers[0].Image = img
+	}
+}
+
+// WithConfigDeletionTimestamp sets the DeletionTimestamp on the Config.
+func WithConfigDeletionTimestamp(r *v1.Configuration) {
+	t := metav1.NewTime(time.Unix(1e9, 0))
+	r.ObjectMeta.SetDeletionTimestamp(&t)
+}
+
+// WithConfigContainerConcurrency sets the given Configuration's concurrency.
+func WithConfigContainerConcurrency(cc int64) ConfigOption {
+	return func(cfg *v1.Configuration) {
+		cfg.Spec.GetTemplate().Spec.ContainerConcurrency = &cc
+	}
+}
+
+// WithConfigGeneration sets the generation of the Configuration.
+func WithConfigGeneration(gen int64) ConfigOption {
+	return func(cfg *v1.Configuration) {
+		cfg.Generation = gen
+	}
+}
+
+// WithConfigObservedGen sets the observed generation of the Configuration.
+func WithConfigObservedGen(cfg *v1.Configuration) {
+	cfg.Status.ObservedGeneration = cfg.Generation
+}
+
+// WithLatestCreated initializes the .status.latestCreatedRevisionName to be the name
+// of the latest revision that the Configuration would have created.
+func WithLatestCreated(name string) ConfigOption {
+	return func(cfg *v1.Configuration) {
+		cfg.Status.SetLatestCreatedRevisionName(name)
+	}
+}
+
+// WithLatestReady initializes the .status.latestReadyRevisionName to be the name
+// of the latest revision that the Configuration would have created.
+func WithLatestReady(name string) ConfigOption {
+	return func(cfg *v1.Configuration) {
+		cfg.Status.SetLatestReadyRevisionName(name)
+	}
+}
+
+// MarkRevisionCreationFailed calls .Status.MarkRevisionCreationFailed.
+func MarkRevisionCreationFailed(msg string) ConfigOption {
+	return func(cfg *v1.Configuration) {
+		cfg.Status.MarkRevisionCreationFailed(msg)
+	}
+}
+
+// MarkLatestCreatedFailed calls .Status.MarkLatestCreatedFailed.
+func MarkLatestCreatedFailed(msg string) ConfigOption {
+	return func(cfg *v1.Configuration) {
+		cfg.Status.MarkLatestCreatedFailed(cfg.Status.LatestCreatedRevisionName, msg)
+	}
+}
+
+// WithConfigLabel attaches a particular label to the configuration.
+func WithConfigLabel(key, value string) ConfigOption {
+	return func(config *v1.Configuration) {
+		config.Labels = kmeta.UnionMaps(config.Labels, map[string]string{key: value})
+	}
+}
+
+// WithConfigAnn attaches a particular label to the configuration.
+func WithConfigAnn(key, value string) ConfigOption {
+	return func(config *v1.Configuration) {
+		config.Annotations = kmeta.UnionMaps(config.Annotations, map[string]string{key: value})
+	}
+}
+
+// WithConfigOwnersRemoved clears the owner references of this Configuration.
+func WithConfigOwnersRemoved(cfg *v1.Configuration) {
+	cfg.OwnerReferences = nil
+}
+
+// WithConfigEnv configures the Service to use the provided environment variables.
+func WithConfigEnv(evs ...corev1.EnvVar) ConfigOption {
+	return func(c *v1.Configuration) {
+		c.Spec.Template.Spec.Containers[0].Env = evs
+	}
+}
+
+// WithConfigRevisionTimeoutSeconds sets revision timeout.
+func WithConfigRevisionTimeoutSeconds(revisionTimeoutSeconds int64) ConfigOption {
+	return func(cfg *v1.Configuration) {
+		cfg.Spec.Template.Spec.TimeoutSeconds = ptr.Int64(revisionTimeoutSeconds)
+	}
+}
diff --git a/vendor/knative.dev/serving/pkg/testing/v1/revision.go b/vendor/knative.dev/serving/pkg/testing/v1/revision.go
new file mode 100644
index 00000000..7bd4f124
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/testing/v1/revision.go
@@ -0,0 +1,269 @@
+/*
+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.
+*/
+
+package v1
+
+import (
+	"context"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/clock"
+	"knative.dev/pkg/kmeta"
+	"knative.dev/serving/pkg/apis/serving"
+	v1 "knative.dev/serving/pkg/apis/serving/v1"
+)
+
+// RevisionOption enables further configuration of a Revision.
+type RevisionOption func(*v1.Revision)
+
+// WithRevisionDeletionTimestamp will set the DeletionTimestamp on the Revision.
+func WithRevisionDeletionTimestamp(r *v1.Revision) {
+	t := metav1.NewTime(time.Unix(1e9, 0))
+	r.ObjectMeta.SetDeletionTimestamp(&t)
+}
+
+// WithInitRevConditions calls .Status.InitializeConditions() on a Revision.
+func WithInitRevConditions(r *v1.Revision) {
+	r.Status.InitializeConditions()
+}
+
+// WithRevName sets the name of the revision
+func WithRevName(name string) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.Name = name
+	}
+}
+
+// WithServiceName propagates the given service name to the revision status.
+func WithServiceName(sn string) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.Status.ServiceName = sn
+	}
+}
+
+// MarkResourceNotOwned calls the function of the same name on the Revision's status.
+func MarkResourceNotOwned(kind, name string) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.Status.MarkResourcesAvailableFalse(
+			v1.ReasonNotOwned,
+			v1.ResourceNotOwnedMessage(kind, name),
+		)
+	}
+}
+
+// WithRevContainerConcurrency sets the given Revision's concurrency.
+func WithRevContainerConcurrency(cc int64) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.Spec.ContainerConcurrency = &cc
+	}
+}
+
+// WithLogURL sets the .Status.LogURL to the expected value.
+func WithLogURL(r *v1.Revision) {
+	r.Status.LogURL = "http://logger.io/test-uid"
+}
+
+// WithCreationTimestamp sets the Revision's timestamp to the provided time.
+// TODO(mattmoor): Ideally this could be a more generic Option and use meta.Accessor,
+// but unfortunately Go's type system cannot support that.
+func WithCreationTimestamp(t time.Time) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.ObjectMeta.CreationTimestamp = metav1.Time{Time: t}
+	}
+}
+
+// WithLastPinned updates the "last pinned" annotation to the provided timestamp.
+func WithLastPinned(t time.Time) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.SetLastPinned(t)
+	}
+}
+
+// WithRevisionPreserveAnnotation updates the annotation with preserve key.
+func WithRevisionPreserveAnnotation() RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.Annotations = kmeta.UnionMaps(rev.Annotations,
+			map[string]string{
+				serving.RevisionPreservedAnnotationKey: "true",
+			})
+	}
+}
+
+// WithRoutingStateModified updates the annotation to the provided timestamp.
+func WithRoutingStateModified(t time.Time) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.Annotations = kmeta.UnionMaps(rev.Annotations,
+			map[string]string{
+				serving.RoutingStateModifiedAnnotationKey: t.UTC().Format(time.RFC3339),
+			})
+	}
+}
+
+// WithRoutingState updates the annotation to the provided timestamp.
+func WithRoutingState(s v1.RoutingState) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.SetRoutingState(s, clock.RealClock{})
+	}
+}
+
+// WithRevStatus is a generic escape hatch for creating hard-to-craft
+// status orientations.
+func WithRevStatus(st v1.RevisionStatus) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.Status = st
+	}
+}
+
+// WithImagePullSecrets updates the revision spec ImagePullSecrets to
+// the provided secrets
+func WithImagePullSecrets(secretName string) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{
+			Name: secretName,
+		}}
+	}
+}
+
+// MarkActive calls .Status.MarkActive on the Revision.
+func MarkActive(r *v1.Revision) {
+	r.Status.MarkActiveTrue()
+}
+
+// WithK8sServiceName applies sn to the revision status field.
+func WithK8sServiceName(sn string) RevisionOption {
+	return func(r *v1.Revision) {
+		r.Status.ServiceName = sn
+	}
+}
+
+// MarkInactive calls .Status.MarkInactive on the Revision.
+func MarkInactive(reason, message string) RevisionOption {
+	return func(r *v1.Revision) {
+		r.Status.MarkActiveFalse(reason, message)
+	}
+}
+
+// MarkActivating calls .Status.MarkActivating on the Revision.
+func MarkActivating(reason, message string) RevisionOption {
+	return func(r *v1.Revision) {
+		r.Status.MarkActiveUnknown(reason, message)
+	}
+}
+
+// MarkDeploying calls .Status.MarkDeploying on the Revision.
+func MarkDeploying(reason string) RevisionOption {
+	return func(r *v1.Revision) {
+		r.Status.MarkResourcesAvailableUnknown(reason, "")
+		r.Status.MarkContainerHealthyUnknown(reason, "")
+	}
+}
+
+// MarkProgressDeadlineExceeded calls the method of the same name on the Revision
+// with the message we expect the Revision Reconciler to pass.
+func MarkProgressDeadlineExceeded(message string) RevisionOption {
+	return func(r *v1.Revision) {
+		r.Status.MarkResourcesAvailableFalse(
+			v1.ReasonProgressDeadlineExceeded,
+			message,
+		)
+	}
+}
+
+// MarkContainerMissing calls .Status.MarkContainerMissing on the Revision.
+func MarkContainerMissing(rev *v1.Revision) {
+	rev.Status.MarkContainerHealthyFalse(v1.ReasonContainerMissing, "It's the end of the world as we know it")
+}
+
+// MarkContainerExiting calls .Status.MarkContainerExiting on the Revision.
+func MarkContainerExiting(exitCode int32, message string) RevisionOption {
+	return func(r *v1.Revision) {
+		r.Status.MarkContainerHealthyFalse(v1.ExitCodeReason(exitCode), message)
+	}
+}
+
+// MarkResourcesUnavailable calls .Status.MarkResourcesUnavailable on the Revision.
+func MarkResourcesUnavailable(reason, message string) RevisionOption {
+	return func(r *v1.Revision) {
+		r.Status.MarkResourcesAvailableFalse(reason, message)
+	}
+}
+
+// MarkRevisionReady calls the necessary helpers to make the Revision Ready=True.
+func MarkRevisionReady(r *v1.Revision) {
+	WithInitRevConditions(r)
+	MarkActive(r)
+	r.Status.MarkResourcesAvailableTrue()
+	r.Status.MarkContainerHealthyTrue()
+	r.Status.ObservedGeneration = r.Generation
+}
+
+// WithRevisionLabel attaches a particular label to the revision.
+func WithRevisionLabel(key, value string) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.Labels = kmeta.UnionMaps(rev.Labels, map[string]string{key: value})
+	}
+}
+
+// WithRevisionAnn attaches a particular label to the revision.
+func WithRevisionAnn(key, value string) RevisionOption {
+	return func(rev *v1.Revision) {
+		rev.Annotations = kmeta.UnionMaps(rev.Annotations, map[string]string{key: value})
+	}
+}
+
+// WithContainerStatuses sets the .Status.ContainerStatuses to the Revision.
+func WithContainerStatuses(containerStatus []v1.ContainerStatus) RevisionOption {
+	return func(r *v1.Revision) {
+		r.Status.ContainerStatuses = containerStatus
+	}
+}
+
+// WithRevisionObservedGeneration sets the observed generation on the
+// revision status.
+func WithRevisionObservedGeneration(gen int64) RevisionOption {
+	return func(r *v1.Revision) {
+		r.Status.ObservedGeneration = gen
+	}
+}
+
+// Revision creates a revision object with given ns/name and options.
+func Revision(namespace, name string, ro ...RevisionOption) *v1.Revision {
+	r := &v1.Revision{
+		ObjectMeta: metav1.ObjectMeta{
+			SelfLink:   "/apis/serving/v1/namespaces/test/revisions/" + name,
+			Name:       name,
+			Namespace:  namespace,
+			UID:        "test-uid",
+			Generation: 1,
+		},
+		Spec: v1.RevisionSpec{
+			PodSpec: corev1.PodSpec{
+				Containers: []corev1.Container{{
+					Name:  name,
+					Image: "busybox",
+				}},
+			},
+		},
+	}
+	r.SetDefaults(context.Background())
+
+	for _, opt := range ro {
+		opt(r)
+	}
+	return r
+}
diff --git a/vendor/knative.dev/serving/pkg/testing/v1/route.go b/vendor/knative.dev/serving/pkg/testing/v1/route.go
new file mode 100644
index 00000000..a4c7b1a3
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/testing/v1/route.go
@@ -0,0 +1,294 @@
+/*
+Copyright 2019 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.
+*/
+
+package v1
+
+import (
+	"context"
+	"fmt"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"knative.dev/networking/pkg/apis/networking"
+	netv1alpha1 "knative.dev/networking/pkg/apis/networking/v1alpha1"
+	"knative.dev/pkg/apis"
+	duckv1 "knative.dev/pkg/apis/duck/v1"
+	"knative.dev/pkg/network"
+	"knative.dev/pkg/ptr"
+	v1 "knative.dev/serving/pkg/apis/serving/v1"
+	routenames "knative.dev/serving/pkg/reconciler/route/resources/names"
+)
+
+// RouteOption enables further configuration of a Route.
+type RouteOption func(*v1.Route)
+
+// WithSpecTraffic sets the Route's traffic block to the specified traffic targets.
+func WithSpecTraffic(traffic ...v1.TrafficTarget) RouteOption {
+	return func(r *v1.Route) {
+		r.Spec.Traffic = traffic
+	}
+}
+
+// WithRouteUID sets the Route's UID
+func WithRouteUID(uid types.UID) RouteOption {
+	return func(r *v1.Route) {
+		r.UID = uid
+	}
+}
+
+// WithRouteGeneration sets the route's generation
+func WithRouteGeneration(generation int64) RouteOption {
+	return func(r *v1.Route) {
+		r.Generation = generation
+	}
+}
+
+// WithRouteObservedGeneration sets the route's observed generation to it's generation
+func WithRouteObservedGeneration(r *v1.Route) {
+	r.Status.ObservedGeneration = r.Generation
+}
+
+// WithRouteFinalizer adds the Route finalizer to the Route.
+func WithRouteFinalizer(r *v1.Route) {
+	r.ObjectMeta.Finalizers = append(r.ObjectMeta.Finalizers, "routes.serving.knative.dev")
+}
+
+// WithRouteDeletionTimestamp adds the Route finalizer to the Route.
+func WithRouteDeletionTimestamp(t *metav1.Time) RouteOption {
+	return func(r *v1.Route) {
+		r.ObjectMeta.DeletionTimestamp = t
+	}
+}
+
+// WithConfigTarget sets the Route's traffic block to point at a particular Configuration.
+func WithConfigTarget(config string) RouteOption {
+	return WithSpecTraffic(v1.TrafficTarget{
+		ConfigurationName: config,
+		Percent:           ptr.Int64(100),
+	})
+}
+
+// WithRevTarget sets the Route's traffic block to point at a particular Revision.
+func WithRevTarget(revision string) RouteOption {
+	return WithSpecTraffic(v1.TrafficTarget{
+		RevisionName: revision,
+		Percent:      ptr.Int64(100),
+	})
+}
+
+// WithStatusTraffic sets the Route's status traffic block to the specified traffic targets.
+func WithStatusTraffic(traffic ...v1.TrafficTarget) RouteOption {
+	ctx := apis.WithinStatus(context.Background())
+
+	for _, t := range traffic {
+		if err := t.Validate(ctx); err != nil {
+			panic(err)
+		}
+	}
+
+	return func(r *v1.Route) {
+		r.Status.Traffic = traffic
+	}
+}
+
+// WithRouteOwnersRemoved clears the owner references of this Route.
+func WithRouteOwnersRemoved(r *v1.Route) {
+	r.OwnerReferences = nil
+}
+
+// MarkServiceNotOwned calls the function of the same name on the Service's status.
+func MarkServiceNotOwned(r *v1.Route) {
+	r.Status.MarkServiceNotOwned(routenames.K8sService(r))
+}
+
+// WithURL sets the .Status.Domain field to the prototypical domain.
+func WithURL(r *v1.Route) {
+	r.Status.URL = &apis.URL{
+		Scheme: "http",
+		Host:   fmt.Sprintf("%s.%s.example.com", r.Name, r.Namespace),
+	}
+}
+
+// WithHTTPSDomain sets the .Status.URL field to a https-domain based on the name and namespace.
+func WithHTTPSDomain(r *v1.Route) {
+	r.Status.URL = &apis.URL{
+		Scheme: "https",
+		Host:   fmt.Sprintf("%s.%s.example.com", r.Name, r.Namespace),
+	}
+}
+
+// WithAddress sets the .Status.Address field to the prototypical internal hostname.
+func WithAddress(r *v1.Route) {
+	r.Status.Address = &duckv1.Addressable{
+		URL: &apis.URL{
+			Scheme: "http",
+			Host:   network.GetServiceHostname(r.Name, r.Namespace),
+		},
+	}
+}
+
+// WithAnotherDomain sets the .Status.Domain field to an atypical domain.
+func WithAnotherDomain(r *v1.Route) {
+	r.Status.URL = &apis.URL{
+		Scheme: "http",
+		Host:   fmt.Sprintf("%s.%s.another-example.com", r.Name, r.Namespace),
+	}
+}
+
+// WithLocalDomain sets the .Status.Domain field to use ClusterDomain suffix.
+func WithLocalDomain(r *v1.Route) {
+	r.Status.URL = &apis.URL{
+		Scheme: "http",
+		Host:   network.GetServiceHostname(r.Name, r.Namespace),
+	}
+}
+
+// WithInitRouteConditions initializes the Service's conditions.
+func WithInitRouteConditions(rt *v1.Route) {
+	rt.Status.InitializeConditions()
+}
+
+// WithRouteConditionsAutoTLSDisabled calls MarkTLSNotEnabled with AutoTLSNotEnabledMessage
+// after initialized the Service's conditions.
+func WithRouteConditionsAutoTLSDisabled(rt *v1.Route) {
+	rt.Status.InitializeConditions()
+	rt.Status.MarkTLSNotEnabled(v1.AutoTLSNotEnabledMessage)
+}
+
+// WithRouteConditionsTLSNotEnabledForClusterLocalMessage calls
+// MarkTLSNotEnabled with TLSNotEnabledForClusterLocalMessage after initialized
+// the Service's conditions.
+func WithRouteConditionsTLSNotEnabledForClusterLocalMessage(rt *v1.Route) {
+	rt.Status.InitializeConditions()
+	rt.Status.MarkTLSNotEnabled(v1.TLSNotEnabledForClusterLocalMessage)
+}
+
+// WithRouteConditionsHTTPDowngrade calls MarkHTTPDowngrade after initialized the Service's conditions.
+func WithRouteConditionsHTTPDowngrade(rt *v1.Route) {
+	rt.Status.InitializeConditions()
+	rt.Status.MarkHTTPDowngrade(routenames.Certificate(rt))
+}
+
+// MarkTrafficAssigned calls the method of the same name on .Status
+func MarkTrafficAssigned(r *v1.Route) {
+	r.Status.MarkTrafficAssigned()
+}
+
+// MarkCertificateNotReady calls the method of the same name on .Status
+func MarkCertificateNotReady(r *v1.Route) {
+	r.Status.MarkCertificateNotReady(routenames.Certificate(r))
+}
+
+// MarkCertificateNotOwned calls the method of the same name on .Status
+func MarkCertificateNotOwned(r *v1.Route) {
+	r.Status.MarkCertificateNotOwned(routenames.Certificate(r))
+}
+
+// MarkCertificateReady calls the method of the same name on .Status
+func MarkCertificateReady(r *v1.Route) {
+	r.Status.MarkCertificateReady(routenames.Certificate(r))
+}
+
+// WithReadyCertificateName marks the certificate specified by name as ready.
+func WithReadyCertificateName(name string) func(*v1.Route) {
+	return func(r *v1.Route) {
+		r.Status.MarkCertificateReady(name)
+	}
+}
+
+// MarkIngressReady propagates a Ready=True Ingress status to the Route.
+func MarkIngressReady(r *v1.Route) {
+	r.Status.PropagateIngressStatus(netv1alpha1.IngressStatus{
+		Status: duckv1.Status{
+			Conditions: duckv1.Conditions{{
+				Type:   "Ready",
+				Status: "True",
+			}},
+		},
+	})
+}
+
+// MarkIngressNotConfigured calls the method of the same name on .Status
+func MarkIngressNotConfigured(r *v1.Route) {
+	r.Status.MarkIngressNotConfigured()
+}
+
+// WithPropagatedStatus propagates the given IngressStatus into the routes status.
+func WithPropagatedStatus(status netv1alpha1.IngressStatus) RouteOption {
+	return func(r *v1.Route) {
+		r.Status.PropagateIngressStatus(status)
+	}
+}
+
+// MarkMissingTrafficTarget calls the method of the same name on .Status
+func MarkMissingTrafficTarget(kind, revision string) RouteOption {
+	return func(r *v1.Route) {
+		r.Status.MarkMissingTrafficTarget(kind, revision)
+	}
+}
+
+// MarkConfigurationNotReady calls the method of the same name on .Status
+func MarkConfigurationNotReady(name string) RouteOption {
+	return func(r *v1.Route) {
+		r.Status.MarkConfigurationNotReady(name)
+	}
+}
+
+// MarkConfigurationFailed calls the method of the same name on .Status
+func MarkConfigurationFailed(name string) RouteOption {
+	return func(r *v1.Route) {
+		r.Status.MarkConfigurationFailed(name)
+	}
+}
+
+// WithRouteLabel sets the specified label on the Route.
+func WithRouteLabel(labels map[string]string) RouteOption {
+	return func(r *v1.Route) {
+		r.Labels = labels
+	}
+}
+
+// WithIngressClass sets the ingress class annotation on the Route.
+func WithIngressClass(ingressClass string) RouteOption {
+	return func(r *v1.Route) {
+		if r.Annotations == nil {
+			r.Annotations = make(map[string]string, 1)
+		}
+		r.Annotations[networking.IngressClassAnnotationKey] = ingressClass
+	}
+}
+
+// WithRouteAnnotation sets the specified annotation on the Route.
+func WithRouteAnnotation(annotations map[string]string) RouteOption {
+	return func(r *v1.Route) {
+		r.Annotations = annotations
+	}
+}
+
+// Route creates a route with RouteOptions
+func Route(namespace, name string, ro ...RouteOption) *v1.Route {
+	r := &v1.Route{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+			Name:      name,
+		},
+	}
+	for _, opt := range ro {
+		opt(r)
+	}
+	r.SetDefaults(context.Background())
+	return r
+}
diff --git a/vendor/knative.dev/serving/pkg/testing/v1/service.go b/vendor/knative.dev/serving/pkg/testing/v1/service.go
new file mode 100644
index 00000000..734d122f
--- /dev/null
+++ b/vendor/knative.dev/serving/pkg/testing/v1/service.go
@@ -0,0 +1,481 @@
+/*
+Copyright 2019 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.
+*/
+
+package v1
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"knative.dev/pkg/apis"
+	duckv1 "knative.dev/pkg/apis/duck/v1"
+	"knative.dev/pkg/kmeta"
+	"knative.dev/pkg/network"
+	"knative.dev/pkg/ptr"
+	v1 "knative.dev/serving/pkg/apis/serving/v1"
+	"knative.dev/serving/pkg/reconciler/route/domains"
+	servicenames "knative.dev/serving/pkg/reconciler/service/resources/names"
+)
+
+// ServiceOption enables further configuration of a Service.
+type ServiceOption func(*v1.Service)
+
+// Service creates a service with ServiceOptions
+func Service(name, namespace string, so ...ServiceOption) *v1.Service {
+	s := &v1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+	}
+	for _, opt := range so {
+		opt(s)
+	}
+	return s
+}
+
+// DefaultService creates a service with ServiceOptions and with default values set
+func DefaultService(name, namespace string, so ...ServiceOption) *v1.Service {
+	return Service(name, namespace, append(so, WithServiceDefaults)...)
+}
+
+// ServiceWithoutNamespace creates a service with ServiceOptions but without a specific namespace
+func ServiceWithoutNamespace(name string, so ...ServiceOption) *v1.Service {
+	s := &v1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: name,
+		},
+	}
+	for _, opt := range so {
+		opt(s)
+	}
+	return s
+}
+
+// WithInitSvcConditions initializes the Service's conditions.
+func WithInitSvcConditions(s *v1.Service) {
+	s.Status.InitializeConditions()
+}
+
+// WithServiceObservedGenFailure marks the top level condition as unknown when the reconciler
+// does not set any condition during reconciliation of a new generation.
+func WithServiceObservedGenFailure(s *v1.Service) {
+	condSet := s.GetConditionSet()
+	condSet.Manage(&s.Status).MarkUnknown(condSet.GetTopLevelConditionType(),
+		"NewObservedGenFailure", "unsuccessfully observed a new generation")
+}
+
+// WithConfigSpec configures the Service to use the given config spec
+func WithConfigSpec(config *v1.ConfigurationSpec) ServiceOption {
+	return func(svc *v1.Service) {
+		svc.Spec.ConfigurationSpec = *config
+	}
+}
+
+// WithRouteSpec configures the Service to use the given route spec
+func WithRouteSpec(route v1.RouteSpec) ServiceOption {
+	return func(svc *v1.Service) {
+		svc.Spec.RouteSpec = route
+	}
+}
+
+// WithNamedPort sets the name on the Service's port to the provided name
+func WithNamedPort(name string) ServiceOption {
+	return func(svc *v1.Service) {
+		c := &svc.Spec.Template.Spec.Containers[0]
+		if len(c.Ports) == 1 {
+			c.Ports[0].Name = name
+		} else {
+			c.Ports = []corev1.ContainerPort{{
+				Name: name,
+			}}
+		}
+	}
+}
+
+// WithNumberedPort sets the Service's port number to what's provided.
+func WithNumberedPort(number int32) ServiceOption {
+	return func(svc *v1.Service) {
+		c := &svc.Spec.Template.Spec.Containers[0]
+		if len(c.Ports) == 1 {
+			c.Ports[0].ContainerPort = number
+		} else {
+			c.Ports = []corev1.ContainerPort{{
+				ContainerPort: number,
+			}}
+		}
+	}
+}
+
+// WithServiceDefaults will set the default values on the service.
+func WithServiceDefaults(svc *v1.Service) {
+	svc.SetDefaults(context.Background())
+}
+
+// WithResourceRequirements attaches resource requirements to the service
+func WithResourceRequirements(resourceRequirements corev1.ResourceRequirements) ServiceOption {
+	return func(svc *v1.Service) {
+		svc.Spec.Template.Spec.Containers[0].Resources = resourceRequirements
+	}
+}
+
+// WithServiceAnnotation adds the given annotation to the service.
+func WithServiceAnnotation(k, v string) ServiceOption {
+	return func(svc *v1.Service) {
+		svc.Annotations = kmeta.UnionMaps(svc.Annotations, map[string]string{
+			k: v,
+		})
+	}
+}
+
+// WithServiceAnnotationRemoved adds the given annotation to the service.
+func WithServiceAnnotationRemoved(k string) ServiceOption {
+	return func(svc *v1.Service) {
+		svc.Annotations = kmeta.FilterMap(svc.Annotations, func(s string) bool {
+			return k == s
+		})
+	}
+}
+
+// WithServiceImage sets the container image to be the provided string.
+func WithServiceImage(img string) ServiceOption {
+	return func(svc *v1.Service) {
+		svc.Spec.Template.Spec.Containers[0].Image = img
+	}
+}
+
+// WithServiceTemplateMeta sets the container image to be the provided string.
+func WithServiceTemplateMeta(m metav1.ObjectMeta) ServiceOption {
+	return func(svc *v1.Service) {
+		svc.Spec.Template.ObjectMeta = m
+	}
+}
+
+// WithRevisionTimeoutSeconds sets revision timeout
+func WithRevisionTimeoutSeconds(revisionTimeoutSeconds int64) ServiceOption {
+	return func(service *v1.Service) {
+		service.Spec.Template.Spec.TimeoutSeconds = ptr.Int64(revisionTimeoutSeconds)
+	}
+}
+
+// WithServiceAccountName sets revision service account name
+func WithServiceAccountName(serviceAccountName string) ServiceOption {
+	return func(service *v1.Service) {
+		service.Spec.Template.Spec.ServiceAccountName = serviceAccountName
+	}
+}
+
+// WithContainerConcurrency sets the given Service's concurrency.
+func WithContainerConcurrency(cc int64) ServiceOption {
+	return func(svc *v1.Service) {
+		svc.Spec.Template.Spec.ContainerConcurrency = &cc
+	}
+}
+
+// WithVolume adds a volume to the service
+func WithVolume(name, mountPath string, volumeSource corev1.VolumeSource) ServiceOption {
+	return func(svc *v1.Service) {
+		rt := &svc.Spec.ConfigurationSpec.Template.Spec
+
+		rt.Containers[0].VolumeMounts = append(rt.Containers[0].VolumeMounts,
+			corev1.VolumeMount{
+				Name:      name,
+				MountPath: mountPath,
+			})
+
+		rt.Volumes = append(rt.Volumes, corev1.Volume{
+			Name:         name,
+			VolumeSource: volumeSource,
+		})
+	}
+}
+
+// WithEnv configures the Service to use the provided environment variables.
+func WithEnv(evs ...corev1.EnvVar) ServiceOption {
+	return func(s *v1.Service) {
+		s.Spec.Template.Spec.Containers[0].Env = evs
+	}
+}
+
+// WithEnvFrom configures the Service to use the provided environment variables.
+func WithEnvFrom(evs ...corev1.EnvFromSource) ServiceOption {
+	return func(s *v1.Service) {
+		s.Spec.Template.Spec.Containers[0].EnvFrom = evs
+	}
+}
+
+// WithSecurityContext configures the Service to use the provided security context.
+func WithSecurityContext(sc *corev1.SecurityContext) ServiceOption {
+	return func(s *v1.Service) {
+		s.Spec.Template.Spec.Containers[0].SecurityContext = sc
+	}
+}
+
+// WithWorkingDir configures the Service to use the provided working directory.
+func WithWorkingDir(wd string) ServiceOption {
+	return func(s *v1.Service) {
+		s.Spec.Template.Spec.Containers[0].WorkingDir = wd
+	}
+}
+
+// WithServiceAnnotations adds the supplied annotations to the Service
+func WithServiceAnnotations(annotations map[string]string) ServiceOption {
+	return func(service *v1.Service) {
+		service.Annotations = kmeta.UnionMaps(service.Annotations, annotations)
+	}
+}
+
+// WithConfigAnnotations assigns config annotations to a service
+func WithConfigAnnotations(annotations map[string]string) ServiceOption {
+	return func(service *v1.Service) {
+		service.Spec.Template.Annotations = kmeta.UnionMaps(
+			service.Spec.Template.Annotations, annotations)
+	}
+}
+
+// WithBYORevisionName sets the given name to the config spec
+func WithBYORevisionName(name string) ServiceOption {
+	return func(s *v1.Service) {
+		s.Spec.Template.Name = name
+	}
+}
+
+// WithServiceDeletionTimestamp will set the DeletionTimestamp on the Service.
+func WithServiceDeletionTimestamp(r *v1.Service) {
+	t := metav1.NewTime(time.Unix(1e9, 0))
+	r.ObjectMeta.SetDeletionTimestamp(&t)
+}
+
+// WithNamedRevision configures the Service to use BYO Revision in the
+// template spec and reference that same revision name in the route spec.
+func WithNamedRevision(s *v1.Service) {
+	s.Spec = v1.ServiceSpec{
+		ConfigurationSpec: v1.ConfigurationSpec{
+			Template: v1.RevisionTemplateSpec{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: s.Name + "-byo",
+				},
+				Spec: v1.RevisionSpec{
+					PodSpec: corev1.PodSpec{
+						Containers: []corev1.Container{{
+							Image: "busybox",
+						}},
+					},
+					TimeoutSeconds: ptr.Int64(60),
+				},
+			},
+		},
+		RouteSpec: v1.RouteSpec{
+			Traffic: []v1.TrafficTarget{{
+				RevisionName: s.Name + "-byo",
+				Percent:      ptr.Int64(100),
+			}},
+		},
+	}
+}
+
+// WithOutOfDateConfig reflects the Configuration's readiness in the Service
+// resource.
+func WithOutOfDateConfig(s *v1.Service) {
+	s.Status.MarkConfigurationNotReconciled()
+}
+
+// WithServiceGeneration sets the service's generation
+func WithServiceGeneration(generation int64) ServiceOption {
+	return func(svc *v1.Service) {
+		svc.Status.ObservedGeneration = generation
+	}
+}
+
+// WithServiceLabel attaches a particular label to the service.
+func WithServiceLabel(key, value string) ServiceOption {
+	return func(service *v1.Service) {
+		if service.Labels == nil {
+			service.Labels = make(map[string]string, 1)
+		}
+		service.Labels[key] = value
+	}
+}
+
+// WithServiceObservedGeneration sets the service's observed generation to it's generation
+func WithServiceObservedGeneration(svc *v1.Service) {
+	svc.Status.ObservedGeneration = svc.Generation
+}
+
+// MarkRevisionNameTaken calls the function of the same name on the Service's status
+func MarkRevisionNameTaken(service *v1.Service) {
+	service.Status.MarkRevisionNameTaken(service.Spec.GetTemplate().GetName())
+}
+
+// WithRunLatestRollout configures the Service to use a "runLatest" rollout.
+func WithRunLatestRollout(s *v1.Service) {
+	s.Spec = v1.ServiceSpec{
+		ConfigurationSpec: *configSpec.DeepCopy(),
+	}
+}
+
+// WithReadyRoute reflects the Route's readiness in the Service resource.
+func WithReadyRoute(s *v1.Service) {
+	s.Status.PropagateRouteStatus(&v1.RouteStatus{
+		Status: duckv1.Status{
+			Conditions: duckv1.Conditions{{
+				Type:   "Ready",
+				Status: "True",
+			}},
+		},
+	})
+}
+
+// WithReadyConfig reflects the Configuration's readiness in the Service
+// resource.  This must coincide with the setting of Latest{Created,Ready}
+// to the provided revision name.
+func WithReadyConfig(name string) ServiceOption {
+	return func(s *v1.Service) {
+		s.Status.PropagateConfigurationStatus(&v1.ConfigurationStatus{
+			Status: duckv1.Status{
+				Conditions: duckv1.Conditions{{
+					Type:   "Ready",
+					Status: "True",
+				}},
+			},
+			ConfigurationStatusFields: v1.ConfigurationStatusFields{
+				LatestCreatedRevisionName: name,
+				LatestReadyRevisionName:   name,
+			},
+		})
+	}
+}
+
+// WithSvcStatusDomain propagates the domain name to the status of the Service.
+func WithSvcStatusDomain(s *v1.Service) {
+	n, ns := s.GetName(), s.GetNamespace()
+	s.Status.URL = &apis.URL{
+		Scheme: "http",
+		Host:   fmt.Sprintf("%s.%s.example.com", n, ns),
+	}
+}
+
+// WithSvcStatusAddress updates the service's status with the address.
+func WithSvcStatusAddress(s *v1.Service) {
+	s.Status.Address = &duckv1.Addressable{
+		URL: &apis.URL{
+			Scheme: "http",
+			Host:   network.GetServiceHostname(s.Name, s.Namespace),
+		},
+	}
+}
+
+// WithSvcStatusTraffic sets the Service's status traffic block to the specified traffic targets.
+func WithSvcStatusTraffic(targets ...v1.TrafficTarget) ServiceOption {
+	return func(r *v1.Service) {
+		// Automatically inject URL into TrafficTarget status
+		for _, tt := range targets {
+			tt.URL = domains.URL(domains.HTTPScheme, tt.Tag+".example.com")
+		}
+		r.Status.Traffic = targets
+	}
+}
+
+// WithServiceLatestReadyRevision sets the latest ready revision on the Service's status.
+func WithServiceLatestReadyRevision(lrr string) ServiceOption {
+	return func(s *v1.Service) {
+		s.Status.LatestReadyRevisionName = lrr
+	}
+}
+
+// WithReadinessProbe sets the provided probe to be the readiness
+// probe on the service.
+func WithReadinessProbe(p *corev1.Probe) ServiceOption {
+	return func(s *v1.Service) {
+		s.Spec.Template.Spec.Containers[0].ReadinessProbe = p
+	}
+}
+
+// MarkConfigurationNotReconciled calls the function of the same name on the Service's status.
+func MarkConfigurationNotReconciled(service *v1.Service) {
+	service.Status.MarkConfigurationNotReconciled()
+}
+
+// WithServiceStatusRouteNotReady sets the `RoutesReady` condition on the service to `Unknown`.
+func WithServiceStatusRouteNotReady(s *v1.Service) {
+	s.Status.MarkRouteNotYetReady()
+}
+
+// MarkConfigurationNotOwned calls the function of the same name on the Service's status.
+func MarkConfigurationNotOwned(service *v1.Service) {
+	service.Status.MarkConfigurationNotOwned(servicenames.Configuration(service))
+}
+
+// MarkRouteNotOwned calls the function of the same name on the Service's status.
+func MarkRouteNotOwned(service *v1.Service) {
+	service.Status.MarkRouteNotOwned(servicenames.Route(service))
+}
+
+// WithFailedRoute reflects a Route's failure in the Service resource.
+func WithFailedRoute(reason, message string) ServiceOption {
+	return func(s *v1.Service) {
+		s.Status.PropagateRouteStatus(&v1.RouteStatus{
+			Status: duckv1.Status{
+				Conditions: duckv1.Conditions{{
+					Type:    "Ready",
+					Status:  "False",
+					Reason:  reason,
+					Message: message,
+				}},
+			},
+		})
+	}
+}
+
+// WithFailedConfig reflects the Configuration's failure in the Service
+// resource.  The failing revision's name is reflected in LatestCreated.
+func WithFailedConfig(name, reason, message string) ServiceOption {
+	return func(s *v1.Service) {
+		s.Status.PropagateConfigurationStatus(&v1.ConfigurationStatus{
+			Status: duckv1.Status{
+				Conditions: duckv1.Conditions{{
+					Type:   "Ready",
+					Status: "False",
+					Reason: reason,
+					Message: fmt.Sprintf("Revision %q failed with message: %s.",
+						name, message),
+				}},
+			},
+			ConfigurationStatusFields: v1.ConfigurationStatusFields{
+				LatestCreatedRevisionName: name,
+			},
+		})
+	}
+}
+
+var (
+	// configSpec is the spec used for the different styles of Service rollout.
+	configSpec = v1.ConfigurationSpec{
+		Template: v1.RevisionTemplateSpec{
+			Spec: v1.RevisionSpec{
+				TimeoutSeconds: ptr.Int64(60),
+				PodSpec: corev1.PodSpec{
+					Containers: []corev1.Container{{
+						Image: "busybox",
+					}},
+				},
+			},
+		},
+	}
+)