Repository: knative/client
Pull request #660 (2020-02-12 08:48:05)

feature(service update): Add generic mechanism to include retries on conflicts
Move the retry logic for retrying in case of a resource conflict into the KnClient.

This is wip (e.g. unit tests are not working yet), but demonstrate how to move the retry logic into the client. If we decided that its worth the effort, I will update the tests, too.

/hold

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

This pull-request has been approved by: *<a href="https://github.com/knative/client/pull/660#" title="Author self-approved">rhuss</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)~~ [rhuss]

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":[]} -->
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/serving/v1/client.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-go-coverage/1227514762514927617/artifacts/line-cov.html#file0) | 82.7% | 74.8% | -7.9
[pkg/serving/v1/client_mock.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-go-coverage/1227514762514927617/artifacts/line-cov.html#file1) | 94.9% | 90.3% | -4.6

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-unit-tests | 6b6123429b67a2392e52d8559719567381712506 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-unit-tests/1227514762510733313) | `/test pull-knative-client-unit-tests`

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

<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: @rhuss: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-unit-tests | 6b6123429b67a2392e52d8559719567381712506 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-unit-tests/1227514762510733313) | `/test pull-knative-client-unit-tests`
pull-knative-client-integration-tests | 6b6123429b67a2392e52d8559719567381712506 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-integration-tests/1227514762514927616) | `/test pull-knative-client-integration-tests`

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

<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 -->
dsimansk: @rhuss just a little side note, you should rebase on master to get the latest unit tests change. (I was bit by it already :)).
knative-prow-robot: @rhuss: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-unit-tests | 6b6123429b67a2392e52d8559719567381712506 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-unit-tests/1227514762510733313) | `/test pull-knative-client-unit-tests`
pull-knative-client-integration-tests | 6b6123429b67a2392e52d8559719567381712506 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-integration-tests/1227514762514927616) | `/test pull-knative-client-integration-tests`
pull-knative-client-integration-tests-latest-release | 6b6123429b67a2392e52d8559719567381712506 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-integration-tests-latest-release/1227514762510733312) | `/test pull-knative-client-integration-tests-latest-release`

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

<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/serving/v1/client.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-go-coverage/1227528731900252163/artifacts/line-cov.html#file0) | 82.7% | 74.8% | -7.9
[pkg/serving/v1/client_mock.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-go-coverage/1227528731900252163/artifacts/line-cov.html#file1) | 94.9% | 90.3% | -4.6

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-integration-tests | 6b6123429b67a2392e52d8559719567381712506 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-integration-tests/1227514762514927616) | `/test pull-knative-client-integration-tests`
pull-knative-client-integration-tests-latest-release | 6b6123429b67a2392e52d8559719567381712506 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-integration-tests-latest-release/1227514762510733312) | `/test pull-knative-client-integration-tests-latest-release`
pull-knative-client-unit-tests | 89d4297f720d0117efdbebabbe983323b306c682 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-unit-tests/1227528731904446464) | `/test pull-knative-client-unit-tests`

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

<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: @rhuss: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-integration-tests | 6b6123429b67a2392e52d8559719567381712506 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-integration-tests/1227514762514927616) | `/test pull-knative-client-integration-tests`
pull-knative-client-unit-tests | 89d4297f720d0117efdbebabbe983323b306c682 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-unit-tests/1227528731904446464) | `/test pull-knative-client-unit-tests`
pull-knative-client-integration-tests-latest-release | 89d4297f720d0117efdbebabbe983323b306c682 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-integration-tests-latest-release/1227528731900252161) | `/test pull-knative-client-integration-tests-latest-release`

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

<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: @rhuss: The following tests **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-client-unit-tests | 89d4297f720d0117efdbebabbe983323b306c682 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-unit-tests/1227528731904446464) | `/test pull-knative-client-unit-tests`
pull-knative-client-integration-tests-latest-release | 89d4297f720d0117efdbebabbe983323b306c682 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-integration-tests-latest-release/1227528731900252161) | `/test pull-knative-client-integration-tests-latest-release`
pull-knative-client-integration-tests | 89d4297f720d0117efdbebabbe983323b306c682 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_client/660/pull-knative-client-integration-tests/1227528731900252160) | `/test pull-knative-client-integration-tests`

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

<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 -->
navidshaikh: +1

Patch:
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 1d9d0a44..c7d3bf4c 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -20,6 +20,10 @@
 |===
 | | Description | PR
 
+| 🧽
+| Support multiple revisions on `kn revision delete`
+| https://github.com/knative/client/pull/657[#657]
+
 | 🎁
 | Add human readable `kn route describe`
 | https://github.com/knative/client/pull/643[#643]
@@ -45,6 +49,11 @@
 | remove unecessary `sync` parameter in setup call
 | https://github.com/knative/client/pull/656[#656]
 
+| 🐛
+| Fix `--image` flag to only allow single occurence
+| https://github.com/knative/client/pull/647[#647]
+|===
+
 ## v0.12.0 (2020-01-29)
 
 [cols="1,10,3", options="header", width="100%"]
diff --git a/pkg/kn/commands/flags/sink.go b/pkg/kn/commands/flags/sink.go
index ba442ae3..2a6ce98f 100644
--- a/pkg/kn/commands/flags/sink.go
+++ b/pkg/kn/commands/flags/sink.go
@@ -105,3 +105,18 @@ func parseSink(sink string) (string, string) {
 		return parts[0], parts[1]
 	}
 }
+
+// SinkToString prepares a sink for list output
+func SinkToString(sink duckv1.Destination) string {
+	if sink.Ref != nil {
+		if sink.Ref.Kind == "Service" {
+			return fmt.Sprintf("svc:%s", sink.Ref.Name)
+		} else {
+			return fmt.Sprintf("%s:%s", strings.ToLower(sink.Ref.Kind), sink.Ref.Name)
+		}
+	}
+	if sink.URI != nil {
+		return sink.URI.String()
+	}
+	return ""
+}
diff --git a/pkg/kn/commands/revision/delete.go b/pkg/kn/commands/revision/delete.go
index eaa3459b..3a856004 100644
--- a/pkg/kn/commands/revision/delete.go
+++ b/pkg/kn/commands/revision/delete.go
@@ -32,8 +32,8 @@ func NewRevisionDeleteCommand(p *commands.KnParams) *cobra.Command {
   # Delete a revision 'svc1-abcde' in default namespace
   kn revision delete svc1-abcde`,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			if len(args) != 1 {
-				return errors.New("'revision delete' requires the revision name given as single argument")
+			if len(args) < 1 {
+				return errors.New("'kn revision delete' requires one or more revision name")
 			}
 			namespace, err := p.GetNamespace(cmd)
 			if err != nil {
@@ -43,11 +43,15 @@ func NewRevisionDeleteCommand(p *commands.KnParams) *cobra.Command {
 			if err != nil {
 				return err
 			}
-			err = client.DeleteRevision(args[0])
-			if err != nil {
-				return err
+
+			for _, name := range args {
+				err = client.DeleteRevision(name)
+				if err != nil {
+					fmt.Fprintf(cmd.OutOrStdout(), "%s.\n", err)
+				} else {
+					fmt.Fprintf(cmd.OutOrStdout(), "Revision '%s' deleted in namespace '%s'.\n", name, namespace)
+				}
 			}
-			fmt.Fprintf(cmd.OutOrStdout(), "Revision '%s' successfully deleted in namespace '%s'.\n", args[0], namespace)
 			return nil
 		},
 	}
diff --git a/pkg/kn/commands/service/configuration_edit_flags.go b/pkg/kn/commands/service/configuration_edit_flags.go
index 25753a85..8731eec0 100644
--- a/pkg/kn/commands/service/configuration_edit_flags.go
+++ b/pkg/kn/commands/service/configuration_edit_flags.go
@@ -30,7 +30,7 @@ import (
 
 type ConfigurationEditFlags struct {
 	// Direct field manipulation
-	Image   string
+	Image   uniqueStringArg
 	Env     []string
 	EnvFrom []string
 	Mount   []string
@@ -67,6 +67,25 @@ type ResourceFlags struct {
 	Memory string
 }
 
+// -- uniqueStringArg Value
+// Custom implementation of flag.Value interface to prevent multiple value assignment.
+// Useful to enforce unique use of flags, e.g. --image.
+type uniqueStringArg string
+
+func (s *uniqueStringArg) Set(val string) error {
+	if len(*s) > 0 {
+		return errors.New("can be provided only once")
+	}
+	*s = uniqueStringArg(val)
+	return nil
+}
+
+func (s *uniqueStringArg) Type() string {
+	return "string"
+}
+
+func (s *uniqueStringArg) String() string { return string(*s) }
+
 // markFlagMakesRevision indicates that a flag will create a new revision if you
 // set it.
 func (p *ConfigurationEditFlags) markFlagMakesRevision(f string) {
@@ -75,7 +94,7 @@ func (p *ConfigurationEditFlags) markFlagMakesRevision(f string) {
 
 // addSharedFlags adds the flags common between create & update.
 func (p *ConfigurationEditFlags) addSharedFlags(command *cobra.Command) {
-	command.Flags().StringVar(&p.Image, "image", "", "Image to run.")
+	command.Flags().VarP(&p.Image, "image", "", "Image to run.")
 	p.markFlagMakesRevision("image")
 	command.Flags().StringArrayVarP(&p.Env, "env", "e", []string{},
 		"Environment variable to set. NAME=value; you may provide this flag "+
@@ -251,7 +270,7 @@ func (p *ConfigurationEditFlags) Apply(
 	}
 	imageSet := false
 	if cmd.Flags().Changed("image") {
-		err = servinglib.UpdateImage(template, p.Image)
+		err = servinglib.UpdateImage(template, p.Image.String())
 		if err != nil {
 			return err
 		}
diff --git a/pkg/kn/commands/service/update.go b/pkg/kn/commands/service/update.go
index 46051a51..c5d419e9 100644
--- a/pkg/kn/commands/service/update.go
+++ b/pkg/kn/commands/service/update.go
@@ -19,7 +19,6 @@ import (
 	"fmt"
 
 	"github.com/spf13/cobra"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
 
 	"knative.dev/client/pkg/kn/commands/flags"
 	"knative.dev/client/pkg/kn/traffic"
@@ -74,15 +73,12 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {
 				return err
 			}
 
-			var retries = 0
-			for {
-				name := args[0]
-				service, err := client.GetService(name)
-				if err != nil {
-					return err
-				}
-				service = service.DeepCopy()
-				latestRevisionBeforeUpdate := service.Status.LatestReadyRevisionName
+			// Use to store the latest revision name
+			var latestRevisionBeforeUpdate string
+			name := args[0]
+
+			updateFunc := func(service *servingv1.Service) (*servingv1.Service, error) {
+				latestRevisionBeforeUpdate = service.Status.LatestReadyRevisionName
 				var baseRevision *servingv1.Revision
 				if !cmd.Flags().Changed("image") && editFlags.LockToDigest {
 					baseRevision, err = client.GetBaseRevision(service)
@@ -92,48 +88,46 @@ func NewServiceUpdateCommand(p *commands.KnParams) *cobra.Command {
 				}
 				err = editFlags.Apply(service, baseRevision, cmd)
 				if err != nil {
-					return err
+					return nil, err
 				}
 
 				if trafficFlags.Changed(cmd) {
 					traffic, err := traffic.Compute(cmd, service.Spec.Traffic, &trafficFlags)
 					if err != nil {
-						return err
+						return nil, err
 					}
 
 					service.Spec.Traffic = traffic
 				}
+				return service, nil
+			}
+
+			// Do the actual update with retry in case of conflicts
+			err = client.UpdateServiceWithRetry(name, updateFunc, MaxUpdateRetries)
+			if err != nil {
+				return err
+			}
 
-				err = client.UpdateService(service)
+			out := cmd.OutOrStdout()
+			//TODO: deprecated condition should be once --async is gone
+			if !waitFlags.Async && !waitFlags.NoWait {
+				fmt.Fprintf(out, "Updating Service '%s' in namespace '%s':\n", args[0], namespace)
+				fmt.Fprintln(out, "")
+				err := waitForService(client, name, out, waitFlags.TimeoutInSeconds)
 				if err != nil {
-					// Retry to update when a resource version conflict exists
-					if apierrors.IsConflict(err) && retries < MaxUpdateRetries {
-						retries++
-						continue
-					}
 					return err
 				}
-
-				out := cmd.OutOrStdout()
-				//TODO: deprecated condition should be once --async is gone
-				if !waitFlags.Async && !waitFlags.NoWait {
-					fmt.Fprintf(out, "Updating Service '%s' in namespace '%s':\n", args[0], namespace)
-					fmt.Fprintln(out, "")
-					err := waitForService(client, name, out, waitFlags.TimeoutInSeconds)
-					if err != nil {
-						return err
-					}
-					fmt.Fprintln(out, "")
-					return showUrl(client, name, latestRevisionBeforeUpdate, "updated", out)
-				} else {
-					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' updated in namespace '%s'.\n", args[0], namespace)
+				fmt.Fprintln(out, "")
+				return showUrl(client, name, latestRevisionBeforeUpdate, "updated", out)
+			} else {
+				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")
 				}
-
-				return nil
+				fmt.Fprintf(out, "Service '%s' updated in namespace '%s'.\n", args[0], namespace)
 			}
+
+			return nil
+
 		},
 		PreRunE: func(cmd *cobra.Command, args []string) error {
 			return preCheck(cmd, args)
diff --git a/pkg/kn/commands/source/apiserver/flags.go b/pkg/kn/commands/source/apiserver/flags.go
index 98f1cd33..0815214c 100644
--- a/pkg/kn/commands/source/apiserver/flags.go
+++ b/pkg/kn/commands/source/apiserver/flags.go
@@ -165,6 +165,7 @@ func APIServerSourceListHandlers(h hprinters.PrintHandler) {
 		{Name: "Name", Type: "string", Description: "Name of the ApiServer source", Priority: 1},
 		{Name: "Resources", Type: "string", Description: "Event resources configured for the ApiServer source", Priority: 1},
 		{Name: "Sink", Type: "string", Description: "Sink of the ApiServer source", Priority: 1},
+		{Name: "Age", Type: "string", Description: "Age of the ApiServer source", Priority: 1},
 		{Name: "Conditions", Type: "string", Description: "Ready state conditions", Priority: 1},
 		{Name: "Ready", Type: "string", Description: "Ready state of the ApiServer source", Priority: 1},
 		{Name: "Reason", Type: "string", Description: "Reason if state is not Ready", Priority: 1},
@@ -180,6 +181,7 @@ func printSource(source *v1alpha1.ApiServerSource, options hprinters.PrintOption
 	}
 
 	name := source.Name
+	age := commands.TranslateTimestampSince(source.CreationTimestamp)
 	conditions := commands.ConditionsValue(source.Status.Conditions)
 	ready := commands.ReadyCondition(source.Status.Conditions)
 	reason := strings.TrimSpace(commands.NonReadyConditionReason(source.Status.Conditions))
@@ -206,7 +208,7 @@ func printSource(source *v1alpha1.ApiServerSource, options hprinters.PrintOption
 		row.Cells = append(row.Cells, source.Namespace)
 	}
 
-	row.Cells = append(row.Cells, name, strings.Join(resources[:], ","), sink, conditions, ready, reason)
+	row.Cells = append(row.Cells, name, strings.Join(resources[:], ","), sink, age, conditions, ready, reason)
 	return []metav1beta1.TableRow{row}, nil
 }
 
diff --git a/pkg/kn/commands/source/binding/binding.go b/pkg/kn/commands/source/binding/binding.go
index 34c0ee00..b20e0c0d 100644
--- a/pkg/kn/commands/source/binding/binding.go
+++ b/pkg/kn/commands/source/binding/binding.go
@@ -23,7 +23,6 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/tools/clientcmd"
 	"knative.dev/eventing/pkg/client/clientset/versioned/typed/sources/v1alpha1"
-	duckv1 "knative.dev/pkg/apis/duck/v1"
 	"knative.dev/pkg/tracker"
 
 	"knative.dev/client/pkg/kn/commands"
@@ -133,23 +132,6 @@ func subjectToString(ref tracker.Reference) string {
 	return ret
 }
 
-// sinkToString prepares a sink for list output
-// This is kept here until we have moved everything to duckv1 (currently the other sources
-// are still on duckv1beta1)
-func sinkToString(sink duckv1.Destination) string {
-	if sink.Ref != nil {
-		if sink.Ref.Kind == "Service" {
-			return fmt.Sprintf("svc:%s", sink.Ref.Name)
-		} else {
-			return fmt.Sprintf("%s:%s", sink.Ref.Kind, sink.Ref.Name)
-		}
-	}
-	if sink.URI != nil {
-		return sink.URI.String()
-	}
-	return ""
-}
-
 // updateCeOverrides updates the values of the --ce-override flags if given
 func updateCeOverrides(bindingFlags bindingUpdateFlags, bindingBuilder *clientsourcesv1alpha1.SinkBindingBuilder) error {
 	if bindingFlags.ceOverrides != nil {
diff --git a/pkg/kn/commands/source/binding/flags.go b/pkg/kn/commands/source/binding/flags.go
index 12ef776e..e8b38884 100644
--- a/pkg/kn/commands/source/binding/flags.go
+++ b/pkg/kn/commands/source/binding/flags.go
@@ -20,6 +20,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 
 	"knative.dev/client/pkg/kn/commands"
+	"knative.dev/client/pkg/kn/commands/flags"
 	hprinters "knative.dev/client/pkg/printers"
 
 	"knative.dev/eventing/pkg/apis/sources/v1alpha1"
@@ -41,6 +42,7 @@ func BindingListHandlers(h hprinters.PrintHandler) {
 		{Name: "Name", Type: "string", Description: "Name of sink binding", Priority: 1},
 		{Name: "Subject", Type: "string", Description: "Subject part of binding", Priority: 1},
 		{Name: "Sink", Type: "string", Description: "Sink part of binding", Priority: 1},
+		{Name: "Age", Type: "string", Description: "Age of binding", Priority: 1},
 		{Name: "Conditions", Type: "string", Description: "Ready state conditions", Priority: 1},
 		{Name: "Ready", Type: "string", Description: "Ready state of the sink binding", Priority: 1},
 		{Name: "Reason", Type: "string", Description: "Reason if state is not Ready", Priority: 1},
@@ -57,7 +59,8 @@ func printSinkBinding(binding *v1alpha1.SinkBinding, options hprinters.PrintOpti
 
 	name := binding.Name
 	subject := subjectToString(binding.Spec.Subject)
-	sink := sinkToString(binding.Spec.Sink)
+	sink := flags.SinkToString(binding.Spec.Sink)
+	age := commands.TranslateTimestampSince(binding.CreationTimestamp)
 	conditions := commands.ConditionsValue(binding.Status.Conditions)
 	ready := commands.ReadyCondition(binding.Status.Conditions)
 	reason := commands.NonReadyConditionReason(binding.Status.Conditions)
@@ -66,7 +69,7 @@ func printSinkBinding(binding *v1alpha1.SinkBinding, options hprinters.PrintOpti
 		row.Cells = append(row.Cells, binding.Namespace)
 	}
 
-	row.Cells = append(row.Cells, name, subject, sink, conditions, ready, reason)
+	row.Cells = append(row.Cells, name, subject, sink, age, conditions, ready, reason)
 	return []metav1beta1.TableRow{row}, nil
 }
 
diff --git a/pkg/kn/commands/source/cronjob/flags.go b/pkg/kn/commands/source/cronjob/flags.go
index 564a7c39..cb100fb0 100644
--- a/pkg/kn/commands/source/cronjob/flags.go
+++ b/pkg/kn/commands/source/cronjob/flags.go
@@ -45,6 +45,7 @@ func CronJobSourceListHandlers(h hprinters.PrintHandler) {
 		{Name: "Name", Type: "string", Description: "Name of the CronJob source", Priority: 1},
 		{Name: "Schedule", Type: "string", Description: "Schedule of the CronJob source", Priority: 1},
 		{Name: "Sink", Type: "string", Description: "Sink of the CronJob source", Priority: 1},
+		{Name: "Age", Type: "string", Description: "Age of the CronJob source", Priority: 1},
 		{Name: "Conditions", Type: "string", Description: "Ready state conditions", Priority: 1},
 		{Name: "Ready", Type: "string", Description: "Ready state of the CronJob source", Priority: 1},
 		{Name: "Reason", Type: "string", Description: "Reason if state is not Ready", Priority: 1},
@@ -61,6 +62,7 @@ func printSource(source *v1alpha1.CronJobSource, options hprinters.PrintOptions)
 
 	name := source.Name
 	schedule := source.Spec.Schedule
+	age := commands.TranslateTimestampSince(source.CreationTimestamp)
 	conditions := commands.ConditionsValue(source.Status.Conditions)
 	ready := commands.ReadyCondition(source.Status.Conditions)
 	reason := commands.NonReadyConditionReason(source.Status.Conditions)
@@ -83,7 +85,7 @@ func printSource(source *v1alpha1.CronJobSource, options hprinters.PrintOptions)
 		row.Cells = append(row.Cells, source.Namespace)
 	}
 
-	row.Cells = append(row.Cells, name, schedule, sink, conditions, ready, reason)
+	row.Cells = append(row.Cells, name, schedule, sink, age, conditions, ready, reason)
 	return []metav1beta1.TableRow{row}, nil
 }
 
diff --git a/pkg/kn/commands/source/human_readable_flags.go b/pkg/kn/commands/source/human_readable_flags.go
index 11f8b917..e7548519 100644
--- a/pkg/kn/commands/source/human_readable_flags.go
+++ b/pkg/kn/commands/source/human_readable_flags.go
@@ -26,10 +26,11 @@ import (
 )
 
 var sourceTypeDescription = map[string]string{
-	"ApiServerSource": "Kubernetes API Server events source",
-	"ContainerSource": "Container events source",
-	"CronJobSource":   "CronJob events source",
-	"SinkBinding":     "Binding Pattern for ContainerSource",
+	"ApiServerSource": "Watch and send Kubernetes API events to a sink",
+	"ContainerSource": "Connect a custom container image to a sink",
+	"CronJobSource":   "Send periodically constant data to a sink",
+	"SinkBinding":     "Binding for connecting a PodSpecable to a sink",
+	"PingSource":      "Send periodically ping events to a sink",
 }
 
 func getSourceTypeDescription(kind string) string {
diff --git a/pkg/kn/commands/trigger/list_flags.go b/pkg/kn/commands/trigger/list_flags.go
index 3c65041d..741ea056 100644
--- a/pkg/kn/commands/trigger/list_flags.go
+++ b/pkg/kn/commands/trigger/list_flags.go
@@ -20,6 +20,7 @@ import (
 	metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"knative.dev/client/pkg/kn/commands"
+	"knative.dev/client/pkg/kn/commands/flags"
 	hprinters "knative.dev/client/pkg/printers"
 	eventing "knative.dev/eventing/pkg/apis/eventing/v1alpha1"
 )
@@ -27,12 +28,14 @@ import (
 // TriggerListHandlers handles printing human readable table for `kn source list-types` command's output
 func TriggerListHandlers(h hprinters.PrintHandler) {
 	sourceTypesColumnDefinitions := []metav1beta1.TableColumnDefinition{
-		{Name: "Namespace", Type: "string", Description: "Namespace of the trigger", Priority: 0},
-		{Name: "Name", Type: "string", Description: "Name of the trigger", Priority: 1},
-		{Name: "Broker", Type: "string", Description: "Name of the broker", Priority: 1},
-		{Name: "Subscriber_URI", Type: "string", Description: "URI of the subscriber", Priority: 1},
-		{Name: "Ready", Type: "string", Description: "Ready condition status of the trigger", Priority: 1},
-		{Name: "Reason", Type: "string", Description: "Reason for non-ready condition of the trigger", Priority: 1},
+		{Name: "Namespace", Type: "string", Description: "Namespace of the trigger.", Priority: 0},
+		{Name: "Name", Type: "string", Description: "Name of the trigger.", Priority: 1},
+		{Name: "Broker", Type: "string", Description: "Name of the broker.", Priority: 1},
+		{Name: "Sink", Type: "string", Description: "Sink for events, i.e. the subscriber.", Priority: 1},
+		{Name: "Age", Type: "string", Description: "Age of the trigger.", Priority: 1},
+		{Name: "Conditions", Type: "string", Description: "Ready state conditions.", Priority: 1},
+		{Name: "Ready", Type: "string", Description: "Ready condition status of the trigger.", Priority: 1},
+		{Name: "Reason", Type: "string", Description: "Reason for non-ready condition of the trigger.", Priority: 1},
 	}
 	h.TableHandler(sourceTypesColumnDefinitions, printTrigger)
 	h.TableHandler(sourceTypesColumnDefinitions, printTriggerList)
@@ -42,7 +45,9 @@ func TriggerListHandlers(h hprinters.PrintHandler) {
 func printTrigger(trigger *eventing.Trigger, options hprinters.PrintOptions) ([]metav1beta1.TableRow, error) {
 	name := trigger.Name
 	broker := trigger.Spec.Broker
-	uri := trigger.Status.SubscriberURI
+	sink := flags.SinkToString(trigger.Spec.Subscriber)
+	age := commands.TranslateTimestampSince(trigger.CreationTimestamp)
+	conditions := commands.ConditionsValue(trigger.Status.Conditions)
 	ready := commands.ReadyCondition(trigger.Status.Conditions)
 	reason := commands.NonReadyConditionReason(trigger.Status.Conditions)
 
@@ -57,7 +62,9 @@ func printTrigger(trigger *eventing.Trigger, options hprinters.PrintOptions) ([]
 	row.Cells = append(row.Cells,
 		name,
 		broker,
-		uri,
+		sink,
+		age,
+		conditions,
 		ready,
 		reason)
 	return []metav1beta1.TableRow{row}, nil
diff --git a/pkg/kn/commands/trigger/trigger.go b/pkg/kn/commands/trigger/trigger.go
index d6dfd9de..7338077d 100644
--- a/pkg/kn/commands/trigger/trigger.go
+++ b/pkg/kn/commands/trigger/trigger.go
@@ -20,6 +20,11 @@ import (
 	"knative.dev/client/pkg/kn/commands"
 )
 
+const (
+	// How often to retry in case of an optimistic lock error when replacing a trigger (--force)
+	MaxUpdateRetries = 3
+)
+
 // NewTriggerCommand to create trigger command group
 func NewTriggerCommand(p *commands.KnParams) *cobra.Command {
 	triggerCmd := &cobra.Command{
diff --git a/pkg/kn/commands/trigger/update.go b/pkg/kn/commands/trigger/update.go
index 1a7c752c..fa5a036e 100644
--- a/pkg/kn/commands/trigger/update.go
+++ b/pkg/kn/commands/trigger/update.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 
 	"github.com/spf13/cobra"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 
 	"knative.dev/eventing/pkg/apis/eventing/v1alpha1"
 	duckv1 "knative.dev/pkg/apis/duck/v1"
@@ -68,43 +69,52 @@ func NewTriggerUpdateCommand(p *commands.KnParams) *cobra.Command {
 				return err
 			}
 
-			trigger, err := eventingClient.GetTrigger(name)
-			if err != nil {
-				return err
-			}
+			var retries = 0
+			for {
+				trigger, err := eventingClient.GetTrigger(name)
+				if err != nil {
+					return err
+				}
 
-			b := client_v1alpha1.NewTriggerBuilderFromExisting(trigger)
+				b := client_v1alpha1.NewTriggerBuilderFromExisting(trigger)
 
-			if cmd.Flags().Changed("broker") {
-				return fmt.Errorf(
-					"cannot update trigger '%s' because broker is immutable", name)
-			}
-			if cmd.Flags().Changed("filter") {
-				updated, removed, err := triggerUpdateFlags.GetUpdateFilters()
-				if err != nil {
+				if cmd.Flags().Changed("broker") {
 					return fmt.Errorf(
-						"cannot update trigger '%s' because %s", name, err)
+						"cannot update trigger '%s' because broker is immutable", name)
 				}
-				existing := extractFilters(trigger)
-				b.Filters(existing.Merge(updated).Remove(removed))
-			}
-			if cmd.Flags().Changed("sink") {
-				destination, err := sinkFlags.ResolveSink(dynamicClient, namespace)
+				if cmd.Flags().Changed("filter") {
+					updated, removed, err := triggerUpdateFlags.GetUpdateFilters()
+					if err != nil {
+						return fmt.Errorf(
+							"cannot update trigger '%s' because %s", name, err)
+					}
+					existing := extractFilters(trigger)
+					b.Filters(existing.Merge(updated).Remove(removed))
+				}
+				if cmd.Flags().Changed("sink") {
+					destination, err := sinkFlags.ResolveSink(dynamicClient, namespace)
+					if err != nil {
+						return err
+					}
+					b.Subscriber(&duckv1.Destination{
+						Ref: destination.Ref,
+						URI: destination.URI,
+					})
+				}
+				err = eventingClient.UpdateTrigger(b.Build())
 				if err != nil {
+					if apierrors.IsConflict(err) && retries < MaxUpdateRetries {
+						retries++
+						continue
+					}
 					return err
 				}
-				b.Subscriber(&duckv1.Destination{
-					Ref: destination.Ref,
-					URI: destination.URI,
-				})
-			}
-			err = eventingClient.UpdateTrigger(b.Build())
-			if err == nil {
 				fmt.Fprintf(cmd.OutOrStdout(), "Trigger '%s' updated in namespace '%s'.\n", name, namespace)
+				return nil
 			}
-			return err
 		},
 	}
+
 	commands.AddNamespaceFlags(cmd.Flags(), false)
 	triggerUpdateFlags.Add(cmd)
 	sinkFlags.Add(cmd)
diff --git a/pkg/kn/core/root.go b/pkg/kn/core/root.go
index c6ea75d8..4f5700cd 100644
--- a/pkg/kn/core/root.go
+++ b/pkg/kn/core/root.go
@@ -152,6 +152,9 @@ func NewKnCommand(params ...commands.KnParams) *cobra.Command {
 	rootCmd.AddCommand(source.NewSourceCommand(p))
 	rootCmd.AddCommand(trigger.NewTriggerCommand(p))
 
+	// Initialize default `help` cmd early to prevent unknown command errors
+	rootCmd.InitDefaultHelpCmd()
+
 	// Deal with empty and unknown sub command groups
 	EmptyAndUnknownSubCommands(rootCmd)
 
diff --git a/pkg/serving/v1/client.go b/pkg/serving/v1/client.go
index b0fad1be..f337f66e 100644
--- a/pkg/serving/v1/client.go
+++ b/pkg/serving/v1/client.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"time"
 
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/fields"
 	"knative.dev/pkg/apis"
 	"knative.dev/serving/pkg/client/clientset/versioned/scheme"
@@ -37,6 +38,10 @@ import (
 	"knative.dev/client/pkg/errors"
 )
 
+// Func signature for an updating function which returns the updated service object
+// or an error
+type serviceUpdateFunc func(origService *servingv1.Service) (*servingv1.Service, error)
+
 // Kn interface to serving. All methods are relative to the
 // namespace specified during construction
 type KnServingClient interface {
@@ -53,9 +58,15 @@ type KnServingClient interface {
 	// Create a new service
 	CreateService(service *servingv1.Service) error
 
-	// Update the given service
+	// UpdateService updates the given service. For a more robust variant with automatic
+	// conflict resolution see UpdateServiceWithRetry
 	UpdateService(service *servingv1.Service) error
 
+	// UpdateServiceWithRetry updates service and retries if there is a version conflict.
+	// The updateFunc receives a deep copy of the existing service and can add update it in
+	// place.
+	UpdateServiceWithRetry(name string, updateFunc serviceUpdateFunc, nrRetries int) error
+
 	// Delete a service by name
 	DeleteService(name string) error
 
@@ -207,6 +218,37 @@ func (cl *knServingClient) UpdateService(service *servingv1.Service) error {
 	return updateServingGvk(service)
 }
 
+// Update the given service with a retry in case of a conflict
+func (cl *knServingClient) UpdateServiceWithRetry(name string, updateFunc serviceUpdateFunc, nrRetries int) error {
+	return updateServiceWithRetry(cl, name, updateFunc, nrRetries)
+}
+
+// Extracted to be usable with the Mocking client
+func updateServiceWithRetry(cl KnServingClient, name string, updateFunc serviceUpdateFunc, nrRetries int) error {
+	var retries = 0
+	for {
+		service, err := cl.GetService(name)
+		if err != nil {
+			return err
+		}
+		updatedService, err := updateFunc(service.DeepCopy())
+		if err != nil {
+			return err
+		}
+
+		err = cl.UpdateService(updatedService)
+		if err != nil {
+			// Retry to update when a resource version conflict exists
+			if apierrors.IsConflict(err) && retries < nrRetries {
+				retries++
+				continue
+			}
+			return err
+		}
+		return nil
+	}
+}
+
 // Delete a service by name
 func (cl *knServingClient) DeleteService(serviceName string) error {
 	err := cl.client.Services(cl.namespace).Delete(
diff --git a/pkg/serving/v1/client_mock.go b/pkg/serving/v1/client_mock.go
index a2a7374e..b0000d76 100644
--- a/pkg/serving/v1/client_mock.go
+++ b/pkg/serving/v1/client_mock.go
@@ -100,6 +100,11 @@ func (c *MockKnServingClient) UpdateService(service *servingv1.Service) error {
 	return mock.ErrorOrNil(call.Result[0])
 }
 
+// Delegate to shared retry method
+func (c *MockKnServingClient) UpdateServiceWithRetry(name string, updateFunc serviceUpdateFunc, maxRetry int) error {
+	return updateServiceWithRetry(c, name, updateFunc, maxRetry)
+}
+
 // Delete a service by name
 func (sr *ServingRecorder) DeleteService(name interface{}, err error) {
 	sr.r.Add("DeleteService", []interface{}{name}, []interface{}{err})
diff --git a/pkg/wait/wait_for_ready.go b/pkg/wait/wait_for_ready.go
index d5a5c563..e4c51776 100644
--- a/pkg/wait/wait_for_ready.go
+++ b/pkg/wait/wait_for_ready.go
@@ -41,7 +41,8 @@ type waitForReadyConfig struct {
 type WaitForReady interface {
 
 	// Wait on resource the resource with this name until a given timeout
-	// and write event messages for unknown event to the status writer
+	// and write event messages for unknown event to the status writer.
+	// Returns an error (if any) and the overall time it took to wait
 	Wait(name string, timeout time.Duration, msgCallback MessageCallback) (error, time.Duration)
 }
 
@@ -161,9 +162,10 @@ func (w *waitForReadyConfig) waitForReadyCondition(start time.Time, name string,
 						// to a true condition even after the condition went to false. If this is not the case within
 						// this window, then an error is returned.
 						// If there is already a timer running, we just log.
-						if errorTimer != nil {
+						if errorTimer == nil {
+							err := fmt.Errorf("%s: %s", cond.Reason, cond.Message)
 							errorTimer = time.AfterFunc(errorWindow, func() {
-								errChan <- fmt.Errorf("%s: %s", cond.Reason, cond.Message)
+								errChan <- err
 							})
 						}
 					}


Test patch:
diff --git a/pkg/kn/commands/revision/delete_test.go b/pkg/kn/commands/revision/delete_test.go
index 10b1ff74..53ac70dd 100644
--- a/pkg/kn/commands/revision/delete_test.go
+++ b/pkg/kn/commands/revision/delete_test.go
@@ -60,3 +60,22 @@ func TestRevisionDelete(t *testing.T) {
 	}
 	assert.Check(t, util.ContainsAll(output, "Revision", revName, "deleted", "namespace", commands.FakeNamespace))
 }
+
+func TestMultipleRevisionDelete(t *testing.T) {
+	revName1 := "foo-12345"
+	revName2 := "foo-67890"
+	revName3 := "foo-abcde"
+	action, _, output, err := fakeRevisionDelete([]string{"revision", "delete", revName1, revName2, revName3})
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	if action == nil {
+		t.Errorf("No action")
+	} else if !action.Matches("delete", "revisions") {
+		t.Errorf("Bad action %v", action)
+	}
+	assert.Check(t, util.ContainsAll(output, "Revision", revName1, "deleted", "namespace", commands.FakeNamespace))
+	assert.Check(t, util.ContainsAll(output, "Revision", revName2, "deleted", "namespace", commands.FakeNamespace))
+	assert.Check(t, util.ContainsAll(output, "Revision", revName3, "deleted", "namespace", commands.FakeNamespace))
+}
diff --git a/pkg/kn/commands/service/create_test.go b/pkg/kn/commands/service/create_test.go
index 47f96a5d..6eb522ac 100644
--- a/pkg/kn/commands/service/create_test.go
+++ b/pkg/kn/commands/service/create_test.go
@@ -29,6 +29,7 @@ import (
 
 	"knative.dev/client/pkg/kn/commands"
 	servinglib "knative.dev/client/pkg/serving"
+	"knative.dev/client/pkg/util"
 	"knative.dev/client/pkg/wait"
 
 	corev1 "k8s.io/api/core/v1"
@@ -135,6 +136,13 @@ func TestServiceCreateImage(t *testing.T) {
 	}
 }
 
+func TestServiceCreateWithMultipleImages(t *testing.T) {
+	_, _, _, err := fakeServiceCreate([]string{
+		"service", "create", "foo", "--image", "gcr.io/foo/bar:baz", "--image", "gcr.io/bar/foo:baz", "--no-wait"}, false)
+
+	assert.Assert(t, util.ContainsAll(err.Error(), "\"--image\"", "\"gcr.io/bar/foo:baz\"", "flag", "once"))
+}
+
 func TestServiceCreateImageSync(t *testing.T) {
 	action, created, output, err := fakeServiceCreate([]string{
 		"service", "create", "foo", "--image", "gcr.io/foo/bar:baz"}, false)
diff --git a/pkg/kn/commands/service/service_update_mock_test.go b/pkg/kn/commands/service/service_update_mock_test.go
index 5bd43f07..1f9688e8 100644
--- a/pkg/kn/commands/service/service_update_mock_test.go
+++ b/pkg/kn/commands/service/service_update_mock_test.go
@@ -25,7 +25,6 @@ import (
 
 	clientserving "knative.dev/client/pkg/serving"
 	clientservingv1 "knative.dev/client/pkg/serving/v1"
-
 	"knative.dev/client/pkg/util"
 )
 
@@ -52,10 +51,7 @@ func TestServiceUpdateEnvMock(t *testing.T) {
 	template.Annotations = map[string]string{clientserving.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
 
 	r := client.Recorder()
-	r.GetService("foo", nil, errors.NewNotFound(servingv1.Resource("service"), "foo"))
-	r.CreateService(service, nil)
-	r.GetService("foo", service, nil)
-	r.UpdateService(updated, nil)
+	recordServiceUpdateWithSuccess(r, "foo", service, updated)
 
 	output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "-e", "a=mouse", "--env", "b=cookie", "--env=empty", "--no-wait", "--revision-name=")
 	assert.NilError(t, err)
@@ -100,10 +96,7 @@ func TestServiceUpdateAnnotationsMock(t *testing.T) {
 	}
 
 	r := client.Recorder()
-	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
-	r.CreateService(newService, nil)
-	r.GetService(svcName, newService, nil)
-	r.UpdateService(updatedService, nil)
+	recordServiceUpdateWithSuccess(r, svcName, newService, updatedService)
 
 	output, err := executeServiceCommand(client,
 		"create", svcName, "--image", "gcr.io/foo/bar:baz",
@@ -127,6 +120,13 @@ func TestServiceUpdateAnnotationsMock(t *testing.T) {
 	r.Validate()
 }
 
+func recordServiceUpdateWithSuccess(r *clientservingv1.ServingRecorder, svcName string, newService *servingv1.Service, updatedService *servingv1.Service) {
+	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
+	r.CreateService(newService, nil)
+	r.GetService(svcName, newService, nil)
+	r.UpdateService(updatedService, nil)
+}
+
 func TestServiceUpdateEnvFromAddingWithConfigMap(t *testing.T) {
 	client := clientservingv1.NewMockKnServiceClient(t)
 	svcName := "svc1"
@@ -170,10 +170,7 @@ func TestServiceUpdateEnvFromAddingWithConfigMap(t *testing.T) {
 	}
 
 	r := client.Recorder()
-	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
-	r.CreateService(newService, nil)
-	r.GetService(svcName, newService, nil)
-	r.UpdateService(updatedService, nil)
+	recordServiceUpdateWithSuccess(r, svcName, newService, updatedService)
 
 	output, err := executeServiceCommand(client,
 		"create", svcName, "--image", "gcr.io/foo/bar:baz",
@@ -275,10 +272,7 @@ func TestServiceUpdateEnvFromRemovalWithConfigMap(t *testing.T) {
 	template.Spec.Containers[0].EnvFrom = nil
 
 	r := client.Recorder()
-	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
-	r.CreateService(newService, nil)
-	r.GetService(svcName, newService, nil)
-	r.UpdateService(updatedService1, nil)
+	recordServiceUpdateWithSuccess(r, svcName, newService, updatedService1)
 	r.GetService(svcName, updatedService1, nil)
 	//r.UpdateService(updatedService2, nil) // since an error happens, update is not triggered here
 	r.GetService(svcName, updatedService2, nil)
@@ -454,10 +448,7 @@ func TestServiceUpdateEnvFromExistingWithConfigMap(t *testing.T) {
 	}
 
 	r := client.Recorder()
-	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
-	r.CreateService(newService, nil)
-	r.GetService(svcName, newService, nil)
-	r.UpdateService(updatedService, nil)
+	recordServiceUpdateWithSuccess(r, svcName, newService, updatedService)
 
 	output, err := executeServiceCommand(client,
 		"create", svcName, "--image", "gcr.io/foo/bar:baz",
@@ -523,10 +514,7 @@ func TestServiceUpdateEnvFromAddingWithSecret(t *testing.T) {
 	}
 
 	r := client.Recorder()
-	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
-	r.CreateService(newService, nil)
-	r.GetService(svcName, newService, nil)
-	r.UpdateService(updatedService, nil)
+	recordServiceUpdateWithSuccess(r, svcName, newService, updatedService)
 
 	output, err := executeServiceCommand(client,
 		"create", svcName, "--image", "gcr.io/foo/bar:baz",
@@ -733,10 +721,7 @@ func TestServiceUpdateEnvFromExistingWithSecret(t *testing.T) {
 	}
 
 	r := client.Recorder()
-	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
-	r.CreateService(newService, nil)
-	r.GetService(svcName, newService, nil)
-	r.UpdateService(updatedService, nil)
+	recordServiceUpdateWithSuccess(r, svcName, newService, updatedService)
 
 	output, err := executeServiceCommand(client,
 		"create", svcName, "--image", "gcr.io/foo/bar:baz",
@@ -835,10 +820,7 @@ func TestServiceUpdateWithAddingVolume(t *testing.T) {
 	}
 
 	r := client.Recorder()
-	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
-	r.CreateService(newService, nil)
-	r.GetService(svcName, newService, nil)
-	r.UpdateService(updatedService, nil)
+	recordServiceUpdateWithSuccess(r, svcName, newService, updatedService)
 
 	output, err := executeServiceCommand(client,
 		"create", svcName, "--image", "gcr.io/foo/bar:baz",
@@ -937,10 +919,7 @@ func TestServiceUpdateWithUpdatingVolume(t *testing.T) {
 	}
 
 	r := client.Recorder()
-	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
-	r.CreateService(newService, nil)
-	r.GetService(svcName, newService, nil)
-	r.UpdateService(updatedService, nil)
+	recordServiceUpdateWithSuccess(r, svcName, newService, updatedService)
 
 	output, err := executeServiceCommand(client,
 		"create", svcName, "--image", "gcr.io/foo/bar:baz",
@@ -1041,10 +1020,7 @@ func TestServiceUpdateWithRemovingVolume(t *testing.T) {
 	}
 
 	r := client.Recorder()
-	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
-	r.CreateService(newService, nil)
-	r.GetService(svcName, newService, nil)
-	r.UpdateService(updatedService, nil)
+	recordServiceUpdateWithSuccess(r, svcName, newService, updatedService)
 
 	output, err := executeServiceCommand(client,
 		"create", svcName, "--image", "gcr.io/foo/bar:baz",
@@ -1119,10 +1095,7 @@ func TestServiceUpdateWithAddingMount(t *testing.T) {
 	}
 
 	r := client.Recorder()
-	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
-	r.CreateService(newService, nil)
-	r.GetService(svcName, newService, nil)
-	r.UpdateService(updatedService, nil)
+	recordServiceUpdateWithSuccess(r, svcName, newService, updatedService)
 
 	output, err := executeServiceCommand(client,
 		"create", svcName, "--image", "gcr.io/foo/bar:baz",
@@ -1227,10 +1200,7 @@ func TestServiceUpdateWithUpdatingMount(t *testing.T) {
 	}
 
 	r := client.Recorder()
-	r.GetService(svcName, nil, errors.NewNotFound(servingv1.Resource("service"), svcName))
-	r.CreateService(newService, nil)
-	r.GetService(svcName, newService, nil)
-	r.UpdateService(updatedService, nil)
+	recordServiceUpdateWithSuccess(r, svcName, newService, updatedService)
 
 	output, err := executeServiceCommand(client,
 		"create", svcName, "--image", "gcr.io/foo/bar:baz",
diff --git a/pkg/kn/commands/service/update_test.go b/pkg/kn/commands/service/update_test.go
index 868dc271..a04fd0e9 100644
--- a/pkg/kn/commands/service/update_test.go
+++ b/pkg/kn/commands/service/update_test.go
@@ -191,6 +191,14 @@ func TestServiceUpdateImage(t *testing.T) {
 	}
 }
 
+func TestServiceUpdateWithMultipleImages(t *testing.T) {
+	orig := newEmptyService()
+	_, _, _, err := fakeServiceUpdate(orig, []string{
+		"service", "create", "foo", "--image", "gcr.io/foo/bar:baz", "--image", "gcr.io/bar/foo:baz", "--no-wait"})
+
+	assert.Assert(t, util.ContainsAll(err.Error(), "\"--image\"", "\"gcr.io/bar/foo:baz\"", "flag", "once"))
+}
+
 func TestServiceUpdateCommand(t *testing.T) {
 	orig := newEmptyService()
 
diff --git a/pkg/kn/commands/source/apiserver/list_test.go b/pkg/kn/commands/source/apiserver/list_test.go
index ea3ec354..652563f6 100644
--- a/pkg/kn/commands/source/apiserver/list_test.go
+++ b/pkg/kn/commands/source/apiserver/list_test.go
@@ -36,7 +36,7 @@ func TestListAPIServerSource(t *testing.T) {
 
 	out, err := executeAPIServerSourceCommand(apiServerClient, nil, "list")
 	assert.NilError(t, err, "sources should be listed")
-	util.ContainsAll(out, "NAME", "RESOURCES", "SINK", "CONDITIONS", "READY", "REASON")
+	util.ContainsAll(out, "NAME", "RESOURCES", "SINK", "AGE", "CONDITIONS", "READY", "REASON")
 	util.ContainsAll(out, "testsource", "Eventing:v1:false", "mysvc")
 
 	apiServerRecorder.Validate()
@@ -52,7 +52,7 @@ func TestListAPIServerSourceEmpty(t *testing.T) {
 
 	out, err := executeAPIServerSourceCommand(apiServerClient, nil, "list")
 	assert.NilError(t, err, "Sources should be listed")
-	util.ContainsNone(out, "NAME", "RESOURCES", "SINK", "CONDITIONS", "READY", "REASON")
+	util.ContainsNone(out, "NAME", "RESOURCES", "SINK", "AGE", "CONDITIONS", "READY", "REASON")
 	util.ContainsAll(out, "No", "ApiServer", "source", "found")
 
 	apiServerRecorder.Validate()
diff --git a/pkg/kn/commands/source/binding/list_test.go b/pkg/kn/commands/source/binding/list_test.go
index a8415eac..7367bea6 100644
--- a/pkg/kn/commands/source/binding/list_test.go
+++ b/pkg/kn/commands/source/binding/list_test.go
@@ -38,7 +38,7 @@ func TestListSimple(t *testing.T) {
 
 	out, err := executeSinkBindingCommand(bindingClient, nil, "list")
 	assert.NilError(t, err, "Sources should be listed")
-	util.ContainsAll(out, "NAME", "SUBJECT", "SINK", "CONDITIONS", "READY", "REASON")
+	util.ContainsAll(out, "NAME", "SUBJECT", "SINK", "AGE", "CONDITIONS", "READY", "REASON")
 	util.ContainsAll(out, "testbinding", "deployment:apps/v1:mydeploy", "mysvc")
 
 	bindingRecorder.Validate()
@@ -53,7 +53,7 @@ func TestListEmpty(t *testing.T) {
 
 	out, err := executeSinkBindingCommand(bindingClient, nil, "list")
 	assert.NilError(t, err, "Sources should be listed")
-	util.ContainsNone(out, "NAME", "SUBJECT", "SINK", "CONDITIONS", "READY", "REASON")
+	util.ContainsNone(out, "NAME", "SUBJECT", "SINK", "AGE", "CONDITIONS", "READY", "REASON")
 	util.ContainsAll(out, "No", "sink binding", "found")
 
 	bindingRecorder.Validate()
diff --git a/pkg/kn/commands/source/cronjob/list_test.go b/pkg/kn/commands/source/cronjob/list_test.go
index 9015ba13..a99a3ec7 100644
--- a/pkg/kn/commands/source/cronjob/list_test.go
+++ b/pkg/kn/commands/source/cronjob/list_test.go
@@ -36,7 +36,7 @@ func TestListCronJobSource(t *testing.T) {
 
 	out, err := executeCronJobSourceCommand(cronjobClient, nil, "list")
 	assert.NilError(t, err, "Sources should be listed")
-	util.ContainsAll(out, "NAME", "SCHEDULE", "SINK", "CONDITIONS", "READY", "REASON")
+	util.ContainsAll(out, "NAME", "SCHEDULE", "SINK", "AGE", "CONDITIONS", "READY", "REASON")
 	util.ContainsAll(out, "testsource", "* * * * */2", "mysvc")
 
 	cronJobRecorder.Validate()
@@ -52,7 +52,7 @@ func TestListCronJobSourceEmpty(t *testing.T) {
 
 	out, err := executeCronJobSourceCommand(cronjobClient, nil, "list")
 	assert.NilError(t, err, "Sources should be listed")
-	util.ContainsNone(out, "NAME", "SCHEDULE", "SINK", "CONDITIONS", "READY", "REASON")
+	util.ContainsNone(out, "NAME", "SCHEDULE", "SINK", "AGE", "CONDITIONS", "READY", "REASON")
 	util.ContainsAll(out, "No", "CronJob", "source", "found")
 
 	cronJobRecorder.Validate()
diff --git a/pkg/kn/commands/trigger/list_test.go b/pkg/kn/commands/trigger/list_test.go
index 5c290162..b4e735c6 100644
--- a/pkg/kn/commands/trigger/list_test.go
+++ b/pkg/kn/commands/trigger/list_test.go
@@ -49,7 +49,7 @@ func TestTriggerList(t *testing.T) {
 	assert.NilError(t, err)
 
 	outputLines := strings.Split(output, "\n")
-	assert.Check(t, util.ContainsAll(outputLines[0], "NAME", "BROKER", "SUBSCRIBER_URI", "READY", "REASON"))
+	assert.Check(t, util.ContainsAll(outputLines[0], "NAME", "BROKER", "SINK", "AGE", "CONDITIONS", "READY", "REASON"))
 	assert.Check(t, util.ContainsAll(outputLines[1], "trigger1", "mybroker1", "mysink"))
 	assert.Check(t, util.ContainsAll(outputLines[2], "trigger2", "mybroker2", "mysink"))
 	assert.Check(t, util.ContainsAll(outputLines[3], "trigger3", "mybroker3", "mysink"))
@@ -90,7 +90,7 @@ func TestTriggerListAllNamespace(t *testing.T) {
 	assert.NilError(t, err)
 
 	outputLines := strings.Split(output, "\n")
-	assert.Check(t, util.ContainsAll(outputLines[0], "NAMESPACE", "NAME", "BROKER", "SUBSCRIBER_URI", "READY", "REASON"))
+	assert.Check(t, util.ContainsAll(outputLines[0], "NAMESPACE", "NAME", "BROKER", "SINK", "AGE", "CONDITIONS", "READY", "REASON"))
 	assert.Check(t, util.ContainsAll(outputLines[1], "default1", "trigger1", "mybroker1", "mysink"))
 	assert.Check(t, util.ContainsAll(outputLines[2], "default2", "trigger2", "mybroker2", "mysink"))
 	assert.Check(t, util.ContainsAll(outputLines[3], "default3", "trigger3", "mybroker3", "mysink"))
diff --git a/pkg/wait/wait_for_ready_test.go b/pkg/wait/wait_for_ready_test.go
index ad80ad19..6ff89d8c 100644
--- a/pkg/wait/wait_for_ready_test.go
+++ b/pkg/wait/wait_for_ready_test.go
@@ -30,7 +30,7 @@ import (
 type waitForReadyTestCase struct {
 	events           []watch.Event
 	timeout          time.Duration
-	errorExpected    bool
+	errorText        string
 	messagesExpected []string
 }
 
@@ -54,12 +54,16 @@ func TestAddWaitForReady(t *testing.T) {
 		})
 		close(fakeWatchApi.eventChan)
 
-		if !tc.errorExpected && err != nil {
+		if tc.errorText == "" && err != nil {
 			t.Errorf("%d: Error received %v", i, err)
 			continue
 		}
-		if tc.errorExpected && err == nil {
-			t.Errorf("%d: No error but expected one", i)
+		if tc.errorText != "" {
+			if err == nil {
+				t.Errorf("%d: No error but expected one", i)
+			} else {
+				assert.ErrorContains(t, err, tc.errorText)
+			}
 		}
 
 		// check messages
@@ -75,20 +79,34 @@ func TestAddWaitForReady(t *testing.T) {
 // Test cases which consists of a series of events to send and the expected behaviour.
 func prepareTestCases(name string) []waitForReadyTestCase {
 	return []waitForReadyTestCase{
-		tc(peNormal, name, false),
-		tc(peError, name, true),
-		tc(peWrongGeneration, name, true),
-		tc(peTimeout, name, true),
-		tc(peReadyFalseWithinErrorWindow, name, false),
+		errorTest(name),
+		tc(peNormal, name, time.Second, ""),
+		tc(peWrongGeneration, name, 1*time.Second, "timeout"),
+		tc(peTimeout, name, time.Second, "timeout"),
+		tc(peReadyFalseWithinErrorWindow, name, time.Second, ""),
 	}
 }
 
-func tc(f func(name string) (evts []watch.Event, nrMessages int), name string, isError bool) waitForReadyTestCase {
+func errorTest(name string) waitForReadyTestCase {
+	events := []watch.Event{
+		{watch.Added, CreateTestServiceWithConditions(name, corev1.ConditionUnknown, corev1.ConditionUnknown, "", "msg1")},
+		{watch.Modified, CreateTestServiceWithConditions(name, corev1.ConditionFalse, corev1.ConditionTrue, "FakeError", "Test Error")},
+	}
+
+	return waitForReadyTestCase{
+		events:           events,
+		timeout:          3 * time.Second,
+		errorText:        "FakeError",
+		messagesExpected: []string{"msg1", "Test Error"},
+	}
+}
+
+func tc(f func(name string) (evts []watch.Event, nrMessages int), name string, timeout time.Duration, errorTxt string) waitForReadyTestCase {
 	events, nrMsgs := f(name)
 	return waitForReadyTestCase{
 		events,
-		time.Second,
-		isError,
+		timeout,
+		errorTxt,
 		pMessages(nrMsgs),
 	}
 }
@@ -110,14 +128,6 @@ func peNormal(name string) ([]watch.Event, int) {
 	}, len(messages)
 }
 
-func peError(name string) ([]watch.Event, int) {
-	messages := pMessages(1)
-	return []watch.Event{
-		{watch.Added, CreateTestServiceWithConditions(name, corev1.ConditionUnknown, corev1.ConditionUnknown, "", messages[0])},
-		{watch.Modified, CreateTestServiceWithConditions(name, corev1.ConditionFalse, corev1.ConditionTrue, "FakeError", "")},
-	}, len(messages)
-}
-
 func peTimeout(name string) ([]watch.Event, int) {
 	messages := pMessages(1)
 	return []watch.Event{
diff --git a/test/e2e/common.go b/test/e2e/common.go
index 1b5d1759..bdd322a4 100644
--- a/test/e2e/common.go
+++ b/test/e2e/common.go
@@ -225,8 +225,10 @@ func runCLIWithOpts(cli string, args []string, opts runOpts, logger Logger) (str
 	err := cmd.Run()
 	if err != nil {
 		err = fmt.Errorf("Execution error: stderr: '%s' error: '%s'", stderr.String(), err)
-
 		if !opts.AllowError {
+			fmt.Println("================[[Error]]=================")
+			fmt.Println(stdout.String())
+			dumpKsvc(args, logger)
 			logger.Fatalf("Failed to successfully execute '%s': %v", cmdCLIDesc(cli, args), err)
 		}
 	}
@@ -234,6 +236,20 @@ func runCLIWithOpts(cli string, args []string, opts runOpts, logger Logger) (str
 	return stdout.String(), err
 }
 
+func dumpKsvc(args []string, logger Logger) {
+	if args[0] == "service" {
+		opts := runOpts{AllowError: true}
+		ns := args[len(args)-1]
+		out, err := runCLIWithOpts("kubectl", []string{"-n", ns, "describe", "ksvc", args[2]}, opts, logger)
+		fmt.Println(err)
+		fmt.Println(out)
+		out, err = runCLIWithOpts("kubectl", []string{"-n", ns, "get", "ksvc", args[2], "-oyaml"}, opts, logger)
+		fmt.Println(err)
+		fmt.Println(out)
+		fmt.Println("================[[Error]]=================")
+	}
+}
+
 func cmdCLIDesc(cli string, args []string) string {
 	return fmt.Sprintf("%s %s", cli, strings.Join(args, " "))
 }
diff --git a/test/e2e/revision_test.go b/test/e2e/revision_test.go
index 534636a7..ea765e5a 100644
--- a/test/e2e/revision_test.go
+++ b/test/e2e/revision_test.go
@@ -38,7 +38,8 @@ func TestRevision(t *testing.T) {
 	})
 
 	t.Run("describe revision from hello service with print flags", func(t *testing.T) {
-		test.revisionDescribeWithPrintFlags(t, "hello")
+		revName := test.findRevision(t, "hello")
+		test.revisionDescribeWithPrintFlags(t, revName)
 	})
 
 	t.Run("update hello service and increase the count of configuration generation", func(t *testing.T) {
@@ -50,7 +51,19 @@ func TestRevision(t *testing.T) {
 	})
 
 	t.Run("delete latest revision from hello service and return no error", func(t *testing.T) {
-		test.revisionDelete(t, "hello")
+		revName := test.findRevision(t, "hello")
+		test.revisionDelete(t, revName)
+	})
+
+	t.Run("delete three revisions with one revision a nonexistent", func(t *testing.T) {
+		// increase count to 2 revisions
+		test.serviceUpdate(t, "hello", []string{"--env", "TARGET=kn", "--port", "8888"})
+
+		existRevision1 := test.findRevisionByGeneration(t, "hello", 1)
+		existRevision2 := test.findRevisionByGeneration(t, "hello", 2)
+		nonexistRevision := "hello-nonexist"
+
+		test.revisionMultipleDelete(t, []string{existRevision1, existRevision2, nonexistRevision})
 	})
 
 	t.Run("delete hello service and return no error", func(t *testing.T) {
@@ -76,18 +89,31 @@ func (test *e2eTest) revisionListWithService(t *testing.T, serviceNames ...strin
 	}
 }
 
-func (test *e2eTest) revisionDelete(t *testing.T, serviceName string) {
-	revName := test.findRevision(t, serviceName)
-
+func (test *e2eTest) revisionDelete(t *testing.T, revName string) {
 	out, err := test.kn.RunWithOpts([]string{"revision", "delete", revName}, runOpts{})
 	assert.NilError(t, err)
 
 	assert.Check(t, util.ContainsAll(out, "Revision", revName, "deleted", "namespace", test.kn.namespace))
 }
 
-func (test *e2eTest) revisionDescribeWithPrintFlags(t *testing.T, serviceName string) {
-	revName := test.findRevision(t, serviceName)
+func (test *e2eTest) revisionMultipleDelete(t *testing.T, revisionNames []string) {
+	existRevision1 := revisionNames[0]
+	existRevision2 := revisionNames[1]
+	nonexistRevision := revisionNames[2]
+
+	out, err := test.kn.RunWithOpts([]string{"revision", "list"}, runOpts{NoNamespace: false})
+	assert.NilError(t, err)
+	assert.Check(t, strings.Contains(out, existRevision1), "Required revision1 does not exist")
+	assert.Check(t, strings.Contains(out, existRevision2), "Required revision2 does not exist")
+
+	out, err = test.kn.RunWithOpts([]string{"revision", "delete", existRevision1, existRevision2, nonexistRevision}, runOpts{NoNamespace: false})
+
+	assert.Check(t, util.ContainsAll(out, "Revision", existRevision1, "deleted", "namespace", test.kn.namespace), "Failed to get 'deleted' first revision message")
+	assert.Check(t, util.ContainsAll(out, "Revision", existRevision2, "deleted", "namespace", test.kn.namespace), "Failed to get 'deleted' second revision message")
+	assert.Check(t, util.ContainsAll(out, "revisions.serving.knative.dev", nonexistRevision, "not found"), "Failed to get 'not found' error")
+}
 
+func (test *e2eTest) revisionDescribeWithPrintFlags(t *testing.T, revName string) {
 	out, err := test.kn.RunWithOpts([]string{"revision", "describe", revName, "-o=name"}, runOpts{})
 	assert.NilError(t, err)
 
diff --git a/test/e2e/trigger_test.go b/test/e2e/trigger_test.go
new file mode 100644
index 00000000..ff4e5495
--- /dev/null
+++ b/test/e2e/trigger_test.go
@@ -0,0 +1,126 @@
+// 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 im
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +build e2e
+// +build !serving
+
+package e2e
+
+import (
+	"strings"
+	"testing"
+	"time"
+
+	"gotest.tools/assert"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"knative.dev/client/pkg/util"
+)
+
+func TestBrokerTrigger(t *testing.T) {
+	t.Parallel()
+	test := NewE2eTest(t)
+	test.Setup(t)
+	defer test.Teardown(t)
+
+	err := test.lableNamespaceForDefaultBroker(t)
+	assert.NilError(t, err)
+	test.serviceCreate(t, "sinksvc0")
+	test.serviceCreate(t, "sinksvc1")
+
+	t.Run("create triggers and list them", func(t *testing.T) {
+		test.triggerCreate(t, "trigger1", []string{"a=b"}, "sinksvc0")
+		test.triggerCreate(t, "trigger2", []string{"type=knative.dev.bar", "source=cronjob"}, "sinksvc1")
+		test.verifyTriggerList(t, []string{"trigger1", "trigger2"})
+	})
+
+	t.Run("create a trigger and delete it", func(t *testing.T) {
+		test.triggerCreate(t, "deltrigger", []string{"a=b"}, "sinksvc0")
+		test.triggerDelete(t, "deltrigger")
+		test.verifyTriggerNotfound(t, "deltrigger")
+	})
+
+	t.Run("create a trigger, describe and update it", func(t *testing.T) {
+		test.triggerCreate(t, "updtrigger", []string{"a=b"}, "sinksvc0")
+		test.verifyTriggerDescribe(t, "updtrigger", []string{"a", "b"}, "default", "sinksvc0")
+		test.triggerUpdate(t, "updtrigger", "type=knative.dev.bar", "sinksvc1")
+		test.verifyTriggerDescribe(t, "updtrigger", []string{"a", "b", "type", "knative.dev.bar"}, "default", "sinksvc1")
+	})
+
+	t.Run("create trigger with error return", func(t *testing.T) {
+		test.triggerCreateMissingSink(t, "errtrigger", "notfound")
+	})
+}
+
+func (test *e2eTest) lableNamespaceForDefaultBroker(t *testing.T) error {
+	kubectl := kubectl{t, Logger{}}
+
+	_, err := kubectl.RunWithOpts([]string{"label", "namespace", test.kn.namespace, "knative-eventing-injection=enabled"}, runOpts{})
+	if err != nil {
+		t.Fatalf("Error executing 'kubectl label namespace %s knative-eventing-injection=enabled'. Error: %s", test.kn.namespace, err.Error())
+	}
+
+	return wait.PollImmediate(10*time.Second, 5*time.Minute, func() (bool, error) {
+		out, err := kubectl.RunWithOpts([]string{"get", "broker", "-n", test.kn.namespace, "-o=jsonpath='{.items[0].status.conditions[?(@.type==\"Ready\")].status}'"}, runOpts{AllowError: true})
+		if err != nil {
+			return false, nil
+		} else {
+			return strings.Contains(out, "True"), nil
+		}
+	})
+}
+
+func (test *e2eTest) triggerCreate(t *testing.T, name string, filters []string, sinksvc string) {
+	args := []string{"trigger", "create", name, "--broker", "default", "--sink", "svc:" + sinksvc}
+	for _, v := range filters {
+		args = append(args, "--filter", v)
+	}
+	out, err := test.kn.RunWithOpts(args, runOpts{NoNamespace: false})
+	assert.NilError(t, err)
+	assert.Check(t, util.ContainsAllIgnoreCase(out, "Trigger", name, "created", "namespace", test.kn.namespace))
+}
+
+func (test *e2eTest) triggerCreateMissingSink(t *testing.T, name string, sinksvc string) {
+	_, err := test.kn.RunWithOpts([]string{"trigger", "create", name, "--broker", "default", "--sink", "svc:" + sinksvc}, runOpts{NoNamespace: false, AllowError: true})
+	assert.ErrorContains(t, err, "services.serving.knative.dev", "not found")
+}
+
+func (test *e2eTest) triggerDelete(t *testing.T, name string) {
+	out, err := test.kn.RunWithOpts([]string{"trigger", "delete", name}, runOpts{NoNamespace: false})
+	assert.NilError(t, err)
+	assert.Check(t, util.ContainsAllIgnoreCase(out, "Trigger", name, "deleted", "namespace", test.kn.namespace))
+}
+
+func (test *e2eTest) triggerUpdate(t *testing.T, name string, filter string, sinksvc string) {
+	out, err := test.kn.RunWithOpts([]string{"trigger", "update", name, "--filter", filter, "--sink", "svc:" + sinksvc}, runOpts{NoNamespace: false})
+	assert.NilError(t, err)
+	assert.Check(t, util.ContainsAllIgnoreCase(out, "Trigger", name, "updated", "namespace", test.kn.namespace))
+}
+
+func (test *e2eTest) verifyTriggerList(t *testing.T, triggers []string) {
+	out, err := test.kn.RunWithOpts([]string{"trigger", "list"}, runOpts{NoNamespace: false})
+	assert.NilError(t, err)
+	assert.Check(t, util.ContainsAllIgnoreCase(out, triggers...))
+}
+
+func (test *e2eTest) verifyTriggerDescribe(t *testing.T, name string, filters []string, broker string, sink string) {
+	out, err := test.kn.RunWithOpts([]string{"trigger", "describe", name}, runOpts{NoNamespace: false})
+	assert.NilError(t, err)
+	assert.Check(t, util.ContainsAllIgnoreCase(out, filters...))
+	assert.Check(t, util.ContainsAllIgnoreCase(out, name, broker, sink))
+}
+
+func (test *e2eTest) verifyTriggerNotfound(t *testing.T, name string) {
+	_, err := test.kn.RunWithOpts([]string{"trigger", "describe", name}, runOpts{NoNamespace: false, AllowError: true})
+	assert.ErrorContains(t, err, name, "not found")
+}