Repository: knative/client
Pull request #597 (2020-01-06 16:17:54)

chore(e2e): Namespace specific k8s resources for tests
## Proposed Changes

 Completes #576
 - For ApiServer source: ClusteRole and ClusterRoleBinding name(s)
   are now test-namespace specific. They are cleared in test's tearDown method.
 - Use constant prefix as:
  clusterRolePrefix        = "apiserver-role-"
  clusterRoleBindingPrefix = "apiserver-binding-"
 - Use constant ServiceAccountName as:
  testServiceAccount = "apiserver-sa"
 - Rename methods and cleanup.


/lint

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

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

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: /assign @rhuss 
knative-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative/client/pull/597#pullrequestreview-338959381" title="Approved">maximilien</a>*, *<a href="https://github.com/knative/client/pull/597#" title="Author self-approved">navidshaikh</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,navidshaikh]

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":[]} -->

Patch:
diff --git a/pkg/kn/commands/revision/human_readable_flags.go b/pkg/kn/commands/revision/human_readable_flags.go
index b3bb44b9..65257225 100644
--- a/pkg/kn/commands/revision/human_readable_flags.go
+++ b/pkg/kn/commands/revision/human_readable_flags.go
@@ -23,11 +23,18 @@ import (
 	servingv1alpha1 "knative.dev/serving/pkg/apis/serving/v1alpha1"
 )
 
+const (
+	RevisionTrafficAnnotation = "client.knative.dev/traffic"
+	RevisionTagsAnnotation    = "client.knative.dev/tags"
+)
+
 // RevisionListHandlers adds print handlers for revision list command
 func RevisionListHandlers(h hprinters.PrintHandler) {
 	RevisionColumnDefinitions := []metav1beta1.TableColumnDefinition{
 		{Name: "Name", Type: "string", Description: "Name of the revision.", Priority: 1},
 		{Name: "Service", Type: "string", Description: "Name of the Knative service.", Priority: 1},
+		{Name: "Traffic", Type: "string", Description: "Percentage of traffic assigned to this revision.", Priority: 1},
+		{Name: "Tags", Type: "string", Description: "Set of tags assigned to this revision.", Priority: 1},
 		{Name: "Generation", Type: "string", Description: "Generation of the revision", Priority: 1},
 		{Name: "Age", Type: "string", Description: "Age of the revision.", Priority: 1},
 		{Name: "Conditions", Type: "string", Description: "Conditions describing statuses of the revision.", Priority: 1},
@@ -57,6 +64,8 @@ func printRevisionList(revisionList *servingv1alpha1.RevisionList, options hprin
 func printRevision(revision *servingv1alpha1.Revision, options hprinters.PrintOptions) ([]metav1beta1.TableRow, error) {
 	name := revision.Name
 	service := revision.Labels[serving.ServiceLabelKey]
+	traffic := revision.Annotations[RevisionTrafficAnnotation]
+	tags := revision.Annotations[RevisionTagsAnnotation]
 	generation := revision.Labels[serving.ConfigurationGenerationLabelKey]
 	age := commands.TranslateTimestampSince(revision.CreationTimestamp)
 	conditions := commands.ConditionsValue(revision.Status.Conditions)
@@ -68,6 +77,8 @@ func printRevision(revision *servingv1alpha1.Revision, options hprinters.PrintOp
 	row.Cells = append(row.Cells,
 		name,
 		service,
+		traffic,
+		tags,
 		generation,
 		age,
 		conditions,
diff --git a/pkg/kn/commands/revision/list.go b/pkg/kn/commands/revision/list.go
index 52c0738c..a8fb662f 100644
--- a/pkg/kn/commands/revision/list.go
+++ b/pkg/kn/commands/revision/list.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"sort"
 	"strconv"
+	"strings"
 
 	"knative.dev/serving/pkg/apis/serving"
 
@@ -85,6 +86,23 @@ func NewRevisionListCommand(p *commands.KnParams) *cobra.Command {
 				}
 			}
 
+			// Add revision/service info as annotations: traffic and tags
+			var service *v1alpha1.Service
+			var serviceName string
+			for _, revision := range revisionList.Items {
+				if serviceName == "" || revision.Labels[serving.ServiceLabelKey] != serviceName {
+					serviceName = revision.Labels[serving.ServiceLabelKey]
+					service, err = client.GetService(serviceName)
+					if err != nil {
+						return err
+					}
+				}
+
+				traffic, tags := trafficForRevision(revision.Name, service)
+				revision.Annotations[RevisionTrafficAnnotation] = fmt.Sprintf("%d%%", traffic)
+				revision.Annotations[RevisionTagsAnnotation] = strings.Join(tags, ",")
+			}
+
 			// sort revisionList by configuration generation key
 			sort.SliceStable(revisionList.Items, func(i, j int) bool {
 				a := revisionList.Items[i]
@@ -110,6 +128,7 @@ func NewRevisionListCommand(p *commands.KnParams) *cobra.Command {
 			if err != nil {
 				return err
 			}
+
 			err = printer.PrintObj(revisionList, cmd.OutOrStdout())
 			if err != nil {
 				return err
diff --git a/pkg/kn/commands/revision/list_flags.go b/pkg/kn/commands/revision/list_flags.go
index a22dfb33..7d6fd309 100644
--- a/pkg/kn/commands/revision/list_flags.go
+++ b/pkg/kn/commands/revision/list_flags.go
@@ -26,7 +26,7 @@ import (
 type RevisionListFlags struct {
 	GenericPrintFlags  *genericclioptions.PrintFlags
 	HumanReadableFlags *commands.HumanPrintFlags
-	ServiceRefFlags    ServiceReferenceFlags
+	ServiceRefFlags    *ServiceReferenceFlags
 }
 
 // AllowedFormats is the list of formats in which data can be displayed
@@ -70,6 +70,7 @@ func NewRevisionListFlags() *RevisionListFlags {
 	return &RevisionListFlags{
 		GenericPrintFlags:  genericclioptions.NewPrintFlags(""),
 		HumanReadableFlags: commands.NewHumanPrintFlags(),
+		ServiceRefFlags:    &ServiceReferenceFlags{},
 	}
 }
 


Test patch:
diff --git a/pkg/kn/commands/revision/list_test.go b/pkg/kn/commands/revision/list_test.go
index 48090e40..8a913b8a 100644
--- a/pkg/kn/commands/revision/list_test.go
+++ b/pkg/kn/commands/revision/list_test.go
@@ -29,7 +29,7 @@ import (
 	"knative.dev/client/pkg/util"
 )
 
-var revisionListHeader = []string{"NAME", "SERVICE", "GENERATION", "AGE", "CONDITIONS", "READY", "REASON"}
+var revisionListHeader = []string{"NAME", "SERVICE", "TRAFFIC", "TAGS", "GENERATION", "AGE", "CONDITIONS", "READY", "REASON"}
 
 func fakeRevisionList(args []string, response *v1alpha1.RevisionList) (action client_testing.Action, output []string, err error) {
 	knParams := &commands.KnParams{}
@@ -71,13 +71,13 @@ func TestRevisionListEmptyByName(t *testing.T) {
 }
 
 func TestRevisionListDefaultOutput(t *testing.T) {
-	revision1 := createMockRevisionWithParams("foo-abcd", "foo", "1")
-	revision2 := createMockRevisionWithParams("bar-abcd", "bar", "1")
-	revision3 := createMockRevisionWithParams("foo-wxyz", "foo", "2")
-	revision4 := createMockRevisionWithParams("bar-wxyz", "bar", "2")
+	revision1 := createMockRevisionWithParams("foo-abcd", "foo", "1", "100", "")
+	revision2 := createMockRevisionWithParams("bar-abcd", "bar", "1", "100", "")
+	revision3 := createMockRevisionWithParams("foo-wxyz", "foo", "2", "50", "tag10")
+	revision4 := createMockRevisionWithParams("bar-wxyz", "bar", "2", "0", "tag2")
 	// Validate edge case for catching the sorting issue caused by string comparison
-	revision5 := createMockRevisionWithParams("foo-wxyz", "foo", "10")
-	revision6 := createMockRevisionWithParams("bar-wxyz", "bar", "10")
+	revision5 := createMockRevisionWithParams("foo-wxyz", "foo", "10", "tag1", "tagx")
+	revision6 := createMockRevisionWithParams("bar-wxyz", "bar", "10", "50", "")
 
 	RevisionList := &v1alpha1.RevisionList{Items: []v1alpha1.Revision{
 		*revision1, *revision2, *revision3, *revision4, *revision5, *revision6}}
@@ -98,8 +98,8 @@ func TestRevisionListDefaultOutput(t *testing.T) {
 }
 
 func TestRevisionListDefaultOutputNoHeaders(t *testing.T) {
-	revision1 := createMockRevisionWithParams("foo-abcd", "foo", "2")
-	revision2 := createMockRevisionWithParams("bar-wxyz", "bar", "1")
+	revision1 := createMockRevisionWithParams("foo-abcd", "foo", "2", "100", "")
+	revision2 := createMockRevisionWithParams("bar-wxyz", "bar", "1", "100", "")
 	RevisionList := &v1alpha1.RevisionList{Items: []v1alpha1.Revision{*revision1, *revision2}}
 	action, output, err := fakeRevisionList([]string{"revision", "list", "--no-headers"}, RevisionList)
 	assert.NilError(t, err)
@@ -116,10 +116,10 @@ func TestRevisionListDefaultOutputNoHeaders(t *testing.T) {
 }
 
 func TestRevisionListForService(t *testing.T) {
-	revision1 := createMockRevisionWithParams("foo-abcd", "svc1", "1")
-	revision2 := createMockRevisionWithParams("bar-wxyz", "svc1", "2")
-	revision3 := createMockRevisionWithParams("foo-abcd", "svc2", "1")
-	revision4 := createMockRevisionWithParams("bar-wxyz", "svc2", "2")
+	revision1 := createMockRevisionWithParams("foo-abcd", "svc1", "1", "50", "")
+	revision2 := createMockRevisionWithParams("bar-wxyz", "svc1", "2", "50", "")
+	revision3 := createMockRevisionWithParams("foo-abcd", "svc2", "1", "0", "")
+	revision4 := createMockRevisionWithParams("bar-wxyz", "svc2", "2", "100", "")
 	RevisionList := &v1alpha1.RevisionList{Items: []v1alpha1.Revision{*revision1, *revision2, *revision3, *revision4}}
 	action, output, err := fakeRevisionList([]string{"revision", "list", "-s", "svc1"}, RevisionList)
 	assert.NilError(t, err)
@@ -156,7 +156,7 @@ func TestRevisionListForService(t *testing.T) {
 }
 
 func TestRevisionListOneOutput(t *testing.T) {
-	revision := createMockRevisionWithParams("foo-abcd", "foo", "1")
+	revision := createMockRevisionWithParams("foo-abcd", "foo", "1", "100", "")
 	RevisionList := &v1alpha1.RevisionList{Items: []v1alpha1.Revision{*revision}}
 	action, output, err := fakeRevisionList([]string{"revision", "list", "foo-abcd"}, RevisionList)
 	assert.NilError(t, err)
@@ -176,7 +176,7 @@ func TestRevisionListOutputWithTwoRevName(t *testing.T) {
 	assert.ErrorContains(t, err, "'kn revision list' accepts maximum 1 argument")
 }
 
-func createMockRevisionWithParams(name, svcName, generation string) *v1alpha1.Revision {
+func createMockRevisionWithParams(name, svcName, generation, traffic, tags string) *v1alpha1.Revision {
 	revision := &v1alpha1.Revision{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "Revision",
@@ -189,6 +189,10 @@ func createMockRevisionWithParams(name, svcName, generation string) *v1alpha1.Re
 				serving.ServiceLabelKey:                 svcName,
 				serving.ConfigurationGenerationLabelKey: generation,
 			},
+			Annotations: map[string]string{
+				"client.knative.dev/traffic": traffic,
+				"client.knative.dev/tags":    tags,
+			},
 		},
 	}
 	return revision
diff --git a/test/README.md b/test/README.md
index 9bd787de..4b1f7992 100644
--- a/test/README.md
+++ b/test/README.md
@@ -51,7 +51,7 @@ E2E_TAGS="eventing" test/e2e-tests-local.sh
 To run one e2e test case, e.g. TestBasicWorkflow
 
 ```bash
-go test -v -tags=e2e -count=1 ./e2e -run ^TestBasicWorkflow$
+test/e2e-tests-local.sh -run ^TestBasicWorkflow$
 ```
 
 ### Running tests in short mode
diff --git a/test/e2e/common.go b/test/e2e/common.go
index 34ac974d..19b9503f 100644
--- a/test/e2e/common.go
+++ b/test/e2e/common.go
@@ -47,13 +47,16 @@ const (
 var m sync.Mutex
 
 type e2eTest struct {
-	env env
-	kn  kn
+	env                    env
+	kn                     kn
+	createNamespaceOnSetup bool
+	namespaceCreated       bool
 }
 
 func NewE2eTest(t *testing.T) *e2eTest {
 	return &e2eTest{
-		env: buildEnv(t),
+		env:                    buildEnv(t),
+		createNamespaceOnSetup: true,
 	}
 }
 
@@ -61,8 +64,10 @@ func NewE2eTest(t *testing.T) *e2eTest {
 func (test *e2eTest) Setup(t *testing.T) {
 	test.env.Namespace = fmt.Sprintf("%s%d", test.env.Namespace, getNamespaceCountAndIncrement())
 	test.kn = kn{t, test.env.Namespace, Logger{}}
-	test.CreateTestNamespace(t, test.env.Namespace)
-	time.Sleep(20 * time.Second)
+	if test.createNamespaceOnSetup {
+		test.CreateTestNamespace(t, test.env.Namespace)
+		time.Sleep(20 * time.Second)
+	}
 }
 
 func getNamespaceCountAndIncrement() int {
@@ -83,7 +88,9 @@ func getServiceNameAndIncrement(base string) string {
 
 // Teardown clean up
 func (test *e2eTest) Teardown(t *testing.T) {
-	test.DeleteTestNamespace(t, test.env.Namespace)
+	if test.namespaceCreated {
+		test.DeleteTestNamespace(t, test.env.Namespace)
+	}
 }
 
 // CreateTestNamespace creates and tests a namesspace creation invoking kubectl
@@ -99,6 +106,7 @@ func (test *e2eTest) CreateTestNamespace(t *testing.T, namespace string) {
 	if !matchRegexp(t, expectedOutputRegexp, out) {
 		t.Fatalf("Expected output incorrect, expecting to include:\n%s\n Instead found:\n%s\n", expectedOutputRegexp, out)
 	}
+	test.namespaceCreated = true
 }
 
 // CreateTestNamespace deletes and tests a namesspace deletion invoking kubectl
@@ -106,13 +114,14 @@ func (test *e2eTest) DeleteTestNamespace(t *testing.T, namespace string) {
 	kubectl := kubectl{t, Logger{}}
 	out, err := kubectl.RunWithOpts([]string{"delete", "namespace", namespace}, runOpts{})
 	if err != nil {
-		t.Fatalf(fmt.Sprintf("Error executing 'kubectl delete namespace' command. Error: %s", err.Error()))
+		t.Fatalf("Error executing 'kubectl delete namespace' command. Error: %s", err.Error())
 	}
 
 	expectedOutputRegexp := fmt.Sprintf("namespace?.+%s.+deleted", namespace)
 	if !matchRegexp(t, expectedOutputRegexp, out) {
 		t.Fatalf("Expected output incorrect, expecting to include:\n%s\n Instead found:\n%s\n", expectedOutputRegexp, out)
 	}
+	test.namespaceCreated = false
 }
 
 // WaitForNamespaceDeleted wait until namespace is deleted
@@ -120,7 +129,7 @@ func (test *e2eTest) WaitForNamespaceDeleted(t *testing.T, namespace string) {
 	logger := Logger{}
 	deleted := checkNamespaceDeleted(t, namespace, MaxRetries, logger)
 	if !deleted {
-		t.Fatalf(fmt.Sprintf("Error deleting namespace %s, timed out", namespace))
+		t.Fatalf("Error deleting namespace %s, timed out", namespace)
 	}
 }
 
@@ -216,7 +225,7 @@ func cmdCLIDesc(cli string, args []string) string {
 func matchRegexp(t *testing.T, matchingRegexp, actual string) bool {
 	matched, err := regexp.MatchString(matchingRegexp, actual)
 	if err != nil {
-		t.Fatalf(fmt.Sprintf("Failed to match regexp '%s'. Error: '%s'", matchingRegexp, err.Error()))
+		t.Fatalf("Failed to match regexp '%s'. Error: '%s'", matchingRegexp, err.Error())
 	}
 	return matched
 }
diff --git a/test/e2e/cronjob_test.go b/test/e2e/cronjob_test.go
new file mode 100644
index 00000000..cbfd6a38
--- /dev/null
+++ b/test/e2e/cronjob_test.go
@@ -0,0 +1,81 @@
+// 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 im
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +build e2e
+// +build !serving
+
+package e2e
+
+import (
+	"testing"
+
+	"gotest.tools/assert"
+	"knative.dev/client/pkg/util"
+)
+
+func TestSourceCronJob(t *testing.T) {
+	t.Parallel()
+	test := NewE2eTest(t)
+	test.Setup(t)
+	defer test.Teardown(t)
+
+	test.serviceCreate(t, "testsvc0")
+
+	t.Run("create cronJob sources with a sink to a service", func(t *testing.T) {
+		test.cronJobSourceCreate(t, "testcronjobsource0", "* * * * */1", "ping", "svc:testsvc0")
+	})
+
+	t.Run("delete cronJob sources", func(t *testing.T) {
+		test.cronJobSourceDelete(t, "testcronjobsource0")
+	})
+
+	t.Run("create cronJob source with a missing sink service", func(t *testing.T) {
+		test.cronJobSourceCreateMissingSink(t, "testcronjobsource1", "* * * * */1", "ping", "svc:unknown")
+	})
+
+	t.Run("update cronJob source sink service", func(t *testing.T) {
+		test.cronJobSourceCreate(t, "testcronjobsource2", "* * * * */1", "ping", "svc:testsvc0")
+		test.serviceCreate(t, "testsvc1")
+		test.cronJobSourceUpdateSink(t, "testcronjobsource2", "svc:testsvc1")
+		jpSinkRefNameInSpec := "jsonpath={.spec.sink.ref.name}"
+		out, err := test.getResourceFieldsWithJSONPath(t, "cronjobsource", "testcronjobsource2", jpSinkRefNameInSpec)
+		assert.NilError(t, err)
+		assert.Equal(t, out, "testsvc1")
+	})
+}
+
+func (test *e2eTest) cronJobSourceCreate(t *testing.T, sourceName string, schedule string, data string, sink string) {
+	out, err := test.kn.RunWithOpts([]string{"source", "cronjob", "create", sourceName,
+		"--schedule", schedule, "--data", data, "--sink", sink}, runOpts{NoNamespace: false})
+	assert.NilError(t, err)
+	assert.Check(t, util.ContainsAllIgnoreCase(out, "cronjob", "source", sourceName, "created", "namespace", test.kn.namespace))
+}
+
+func (test *e2eTest) cronJobSourceDelete(t *testing.T, sourceName string) {
+	out, err := test.kn.RunWithOpts([]string{"source", "cronjob", "delete", sourceName}, runOpts{NoNamespace: false})
+	assert.NilError(t, err)
+	assert.Check(t, util.ContainsAllIgnoreCase(out, "cronjob", "source", sourceName, "deleted", "namespace", test.kn.namespace))
+}
+
+func (test *e2eTest) cronJobSourceCreateMissingSink(t *testing.T, sourceName string, schedule string, data string, sink string) {
+	_, err := test.kn.RunWithOpts([]string{"source", "cronjob", "create", sourceName,
+		"--schedule", schedule, "--data", data, "--sink", sink}, runOpts{NoNamespace: false, AllowError: true})
+	assert.ErrorContains(t, err, "services.serving.knative.dev", "not found")
+}
+
+func (test *e2eTest) cronJobSourceUpdateSink(t *testing.T, sourceName string, sink string) {
+	out, err := test.kn.RunWithOpts([]string{"source", "cronjob", "update", sourceName, "--sink", sink}, runOpts{})
+	assert.NilError(t, err)
+	assert.Check(t, util.ContainsAll(out, sourceName, "updated", "namespace", test.kn.namespace))
+}
diff --git a/test/e2e/plugins_test.go b/test/e2e/plugins_test.go
new file mode 100644
index 00000000..d619fbaf
--- /dev/null
+++ b/test/e2e/plugins_test.go
@@ -0,0 +1,189 @@
+// 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 im
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +build e2e
+
+package e2e
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"gotest.tools/assert"
+	"knative.dev/client/pkg/util"
+)
+
+const (
+	TestPluginCode string = `#!/bin/bash
+
+echo "Hello Knative, I'm a Kn plugin"
+echo "  My plugin file is $0"
+echo "  I received arguments: $1 $2 $3 $4"`
+
+	KnConfigDefault string = `plugins-dir: %s
+lookup-plugins: %s`
+
+	FileModeReadable   = 0644
+	FileModeReadWrite  = 0666
+	FileModeExecutable = 0777
+)
+
+func TestPluginWorkflow(t *testing.T) {
+	var (
+		knConfigDir, knPluginsDir, knPluginsDir2  string
+		knConfigPath, knPluginPath, knPluginPath2 string
+		lookupPlugins                             bool
+		err                                       error
+	)
+
+	t.Parallel()
+	test := NewE2eTest(t)
+	setup := func(t *testing.T) {
+		test.createNamespaceOnSetup = false
+		test.Setup(t)
+		defer test.Teardown(t)
+
+		knConfigDir, err = ioutil.TempDir("", "kn-config")
+		assert.NilError(t, err)
+
+		knPluginsDir = filepath.Join(knConfigDir, "plugins")
+		err = os.MkdirAll(knPluginsDir, FileModeExecutable)
+		assert.NilError(t, err)
+
+		knPluginsDir2 = filepath.Join(knConfigDir, "plugins2")
+		err = os.MkdirAll(knPluginsDir2, FileModeExecutable)
+		assert.NilError(t, err)
+
+		knConfigPath = test.createFile(t, "config.yaml", "", knConfigDir, FileModeReadWrite)
+		assert.Assert(t, knConfigPath != "")
+
+		knPluginPath = test.createFile(t, "kn-helloe2e", TestPluginCode, knPluginsDir, FileModeExecutable)
+		assert.Assert(t, knPluginPath != "")
+
+		knPluginPath2 = test.createFile(t, "kn-hello2e2e", TestPluginCode, knPluginsDir2, FileModeExecutable)
+		assert.Assert(t, knPluginPath2 != "")
+	}
+
+	teardown := func(t *testing.T) {
+		err = os.RemoveAll(knConfigDir)
+		assert.NilError(t, err)
+	}
+
+	t.Run("when kn config is empty", func(t *testing.T) {
+		t.Run("when using --plugin-dir", func(t *testing.T) {
+			t.Run("when --plugins-dir has a plugin", func(t *testing.T) {
+				t.Run("when --lookup-plugins is false", func(t *testing.T) {
+					lookupPlugins = false
+
+					setup(t)
+					defer teardown(t)
+
+					assert.Assert(t, lookupPlugins == false)
+					knFlags := []string{fmt.Sprintf("--plugins-dir=%s", knPluginsDir), fmt.Sprintf("--lookup-plugins=%t", lookupPlugins)}
+
+					t.Run("list plugin in --plugins-dir", func(t *testing.T) {
+						test.listPlugin(t, knFlags, []string{knPluginPath}, []string{})
+					})
+
+					t.Run("execute plugin in --plugins-dir", func(t *testing.T) {
+						test.runPlugin(t, knFlags, "helloe2e", []string{"e2e", "test"}, []string{"Hello Knative, I'm a Kn plugin", "I received arguments: e2e"})
+					})
+
+					t.Run("does not list any other plugin in $PATH", func(t *testing.T) {
+						test.listPlugin(t, knFlags, []string{knPluginPath}, []string{knPluginPath2})
+					})
+				})
+
+				t.Run("with --lookup-plugins is true", func(t *testing.T) {
+					lookupPlugins = true
+
+					setup(t)
+					defer teardown(t)
+
+					assert.Assert(t, lookupPlugins == true)
+					knFlags := []string{fmt.Sprintf("--plugins-dir=%s", knPluginsDir), fmt.Sprintf("--lookup-plugins=%t", lookupPlugins)}
+
+					t.Run("list plugin in --plugins-dir", func(t *testing.T) {
+						test.listPlugin(t, knFlags, []string{knPluginPath}, []string{knPluginPath2})
+					})
+
+					t.Run("execute plugin in --plugins-dir", func(t *testing.T) {
+						test.runPlugin(t, knFlags, "helloe2e", []string{}, []string{"Hello Knative, I'm a Kn plugin"})
+					})
+
+					t.Run("when other plugins are in $PATH", func(t *testing.T) {
+						var oldPath string
+
+						setupPath := func(t *testing.T) {
+							oldPath = os.Getenv("PATH")
+							err := os.Setenv("PATH", fmt.Sprintf("%s:%s", oldPath, knPluginsDir2))
+							assert.NilError(t, err)
+						}
+
+						tearDownPath := func(t *testing.T) {
+							err = os.Setenv("PATH", oldPath)
+							assert.NilError(t, err)
+						}
+
+						t.Run("list plugin in $PATH", func(t *testing.T) {
+							setupPath(t)
+							defer tearDownPath(t)
+							test.listPlugin(t, knFlags, []string{knPluginPath, knPluginPath2}, []string{})
+						})
+
+						t.Run("execute plugin in $PATH", func(t *testing.T) {
+							setupPath(t)
+							defer tearDownPath(t)
+							test.runPlugin(t, knFlags, "hello2e2e", []string{}, []string{"Hello Knative, I'm a Kn plugin"})
+						})
+					})
+				})
+			})
+		})
+	})
+}
+
+// Private
+
+func (test *e2eTest) createFile(t *testing.T, fileName, fileContent, filePath string, fileMode os.FileMode) string {
+	file := filepath.Join(filePath, fileName)
+	err := ioutil.WriteFile(file, []byte(fileContent), fileMode)
+	assert.NilError(t, err)
+	return file
+}
+
+func (test *e2eTest) listPlugin(t *testing.T, knFlags []string, expectedPlugins []string, unexpectedPlugins []string) {
+	knArgs := append([]string{}, knFlags...)
+	knArgs = append(knArgs, []string{"plugin", "list"}...)
+
+	out, err := test.kn.RunWithOpts(knArgs, runOpts{NoNamespace: true})
+	assert.NilError(t, err)
+	assert.Check(t, util.ContainsAll(out, expectedPlugins...))
+	assert.Check(t, util.ContainsNone(out, unexpectedPlugins...))
+}
+
+func (test *e2eTest) runPlugin(t *testing.T, knFlags []string, pluginName string, args []string, expectedOutput []string) {
+	knArgs := append([]string{}, knFlags...)
+	knArgs = append(knArgs, pluginName)
+	knArgs = append(knArgs, args...)
+
+	out, err := test.kn.RunWithOpts(knArgs, runOpts{NoNamespace: true})
+	assert.NilError(t, err)
+	for _, output := range expectedOutput {
+		assert.Check(t, util.ContainsAll(out, output))
+	}
+}
diff --git a/test/e2e/source_apiserver_test.go b/test/e2e/source_apiserver_test.go
index d8360c23..1cbacd1c 100644
--- a/test/e2e/source_apiserver_test.go
+++ b/test/e2e/source_apiserver_test.go
@@ -18,23 +18,30 @@
 package e2e
 
 import (
-	"fmt"
+	"strings"
 	"testing"
 
 	"gotest.tools/assert"
 	"knative.dev/client/pkg/util"
 )
 
+const (
+	testServiceAccount = "apiserver-sa"
+	// use following prefix + current namespace to generate ClusterRole and ClusterRoleBinding names
+	clusterRolePrefix        = "apiserver-role-"
+	clusterRoleBindingPrefix = "apiserver-binding-"
+)
+
 func TestSourceApiServer(t *testing.T) {
 	t.Parallel()
 	test := NewE2eTest(t)
 	test.Setup(t)
 	defer func() {
-		test.deleteServiceAccountForApiserver(t, "testsa")
+		test.tearDownForSourceApiServer(t)
 		test.Teardown(t)
 	}()
 
-	test.setupServiceAccountForApiserver(t, "testsa")
+	test.setupForSourceApiServer(t)
 	test.serviceCreate(t, "testsvc0")
 
 	t.Run("create apiserver sources with a sink to a service", func(t *testing.T) {
@@ -82,37 +89,47 @@ func (test *e2eTest) apiServerSourceDelete(t *testing.T, sourceName string) {
 	assert.Check(t, util.ContainsAllIgnoreCase(out, "apiserver", "source", sourceName, "deleted", "namespace", test.kn.namespace))
 }
 
-func (test *e2eTest) setupServiceAccountForApiserver(t *testing.T, name string) {
+func (test *e2eTest) setupForSourceApiServer(t *testing.T) {
 	kubectl := kubectl{t, Logger{}}
 
-	_, err := kubectl.RunWithOpts([]string{"create", "serviceaccount", name, "--namespace", test.kn.namespace}, runOpts{})
+	saCmd := []string{"create", "serviceaccount", testServiceAccount, "--namespace", test.kn.namespace}
+	_, err := kubectl.RunWithOpts(saCmd, runOpts{})
 	if err != nil {
-		t.Fatalf(fmt.Sprintf("Error executing 'kubectl create serviceaccount test-sa'. Error: %s", err.Error()))
+		t.Fatalf("Error executing '%s'. Error: %v", strings.Join(saCmd, " "), err.Error())
 	}
-	_, err = kubectl.RunWithOpts([]string{"create", "clusterrole", "testsa-role", "--verb=get,list,watch", "--resource=events,namespaces"}, runOpts{})
+
+	crCmd := []string{"create", "clusterrole", clusterRolePrefix + test.kn.namespace, "--verb=get,list,watch", "--resource=events,namespaces"}
+	_, err = kubectl.RunWithOpts(crCmd, runOpts{})
 	if err != nil {
-		t.Fatalf(fmt.Sprintf("Error executing 'kubectl create clusterrole testsa-role'. Error: %s", err.Error()))
+		t.Fatalf("Error executing '%s'. Error: %v", strings.Join(crCmd, " "), err.Error())
 	}
-	_, err = kubectl.RunWithOpts([]string{"create", "clusterrolebinding", "testsa-binding", "--clusterrole=testsa-role", "--serviceaccount=" + test.kn.namespace + ":" + name}, runOpts{})
+
+	crbCmd := []string{"create", "clusterrolebinding", clusterRoleBindingPrefix + test.kn.namespace, "--clusterrole=" + clusterRolePrefix + test.kn.namespace, "--serviceaccount=" + test.kn.namespace + ":" + testServiceAccount}
+	_, err = kubectl.RunWithOpts(crbCmd, runOpts{})
 	if err != nil {
-		t.Fatalf(fmt.Sprintf("Error executing 'kubectl create clusterrolebinding testsa-binding'. Error: %s", err.Error()))
+		t.Fatalf("Error executing '%s'. Error: %v", strings.Join(crbCmd, " "), err.Error())
 	}
 }
 
-func (test *e2eTest) deleteServiceAccountForApiserver(t *testing.T, name string) {
+func (test *e2eTest) tearDownForSourceApiServer(t *testing.T) {
 	kubectl := kubectl{t, Logger{}}
 
-	_, err := kubectl.RunWithOpts([]string{"delete", "serviceaccount", name, "--namespace", test.kn.namespace}, runOpts{})
+	saCmd := []string{"delete", "serviceaccount", testServiceAccount, "--namespace", test.kn.namespace}
+	_, err := kubectl.RunWithOpts(saCmd, runOpts{})
 	if err != nil {
-		t.Fatalf(fmt.Sprintf("Error executing 'kubectl delete serviceaccount test-sa'. Error: %s", err.Error()))
+		t.Fatalf("Error executing '%s'. Error: %v", strings.Join(saCmd, " "), err.Error())
 	}
-	_, err = kubectl.RunWithOpts([]string{"delete", "clusterrole", "testsa-role"}, runOpts{})
+
+	crCmd := []string{"delete", "clusterrole", clusterRolePrefix + test.kn.namespace}
+	_, err = kubectl.RunWithOpts(crCmd, runOpts{})
 	if err != nil {
-		t.Fatalf(fmt.Sprintf("Error executing 'kubectl delete clusterrole testsa-role'. Error: %s", err.Error()))
+		t.Fatalf("Error executing '%s'. Error: %v", strings.Join(crCmd, " "), err.Error())
 	}
-	_, err = kubectl.RunWithOpts([]string{"delete", "clusterrolebinding", "testsa-binding"}, runOpts{})
+
+	crbCmd := []string{"delete", "clusterrolebinding", clusterRoleBindingPrefix + test.kn.namespace}
+	_, err = kubectl.RunWithOpts(crbCmd, runOpts{})
 	if err != nil {
-		t.Fatalf(fmt.Sprintf("Error executing 'kubectl delete clusterrolebinding testsa-binding'. Error: %s", err.Error()))
+		t.Fatalf("Error executing '%s'. Error: %v", strings.Join(crbCmd, " "), err.Error())
 	}
 }
 
diff --git a/test/e2e/tekton_test.go b/test/e2e/tekton_test.go
index 4e522db5..60f1efc1 100644
--- a/test/e2e/tekton_test.go
+++ b/test/e2e/tekton_test.go
@@ -35,7 +35,6 @@ const (
 
 func TestTektonPipeline(t *testing.T) {
 	test := NewE2eTest(t)
-	defer test.Teardown(t)
 	test.Setup(t)
 
 	kubectl := kubectl{t, Logger{}}
@@ -51,7 +50,7 @@ func TestTektonPipeline(t *testing.T) {
 	_, err = kubectl.RunWithOpts([]string{"apply", "-n", test.env.Namespace, "-f", basedir + "/kn-deployer-rbac.yaml"}, runOpts{})
 	assert.NilError(t, err)
 
-	_, err = kubectl.RunWithOpts([]string{"apply", "-n", test.env.Namespace, "-f", "https://raw.githubusercontent.com/tektoncd/catalog/master/buildah/buildah.yaml"}, runOpts{})
+	_, err = kubectl.RunWithOpts([]string{"apply", "-n", test.env.Namespace, "-f", basedir + "/buildah.yaml"}, runOpts{})
 	assert.NilError(t, err)
 
 	_, err = kubectl.RunWithOpts([]string{"apply", "-n", test.env.Namespace, "-f", "https://raw.githubusercontent.com/tektoncd/catalog/master/kn/kn.yaml"}, runOpts{})
diff --git a/test/resources/tekton/buildah.yaml b/test/resources/tekton/buildah.yaml
new file mode 100644
index 00000000..1e9635b2
--- /dev/null
+++ b/test/resources/tekton/buildah.yaml
@@ -0,0 +1,66 @@
+# 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.
+# Define a ServiceAccount named kn-deployer-account that has permission to
+# manage Knative services.
+---
+apiVersion: tekton.dev/v1alpha1
+kind: Task
+metadata:
+  name: buildah
+spec:
+  inputs:
+    params:
+    - name: BUILDER_IMAGE
+      description: The location of the buildah builder image.
+      default: quay.io/buildah/stable:v1.11.0
+    - name: DOCKERFILE
+      description: Path to the Dockerfile to build.
+      default: ./Dockerfile
+    - name: TLSVERIFY
+      description: Verify the TLS on the registry endpoint (for push/pull to a non-TLS registry)
+      default: "true"
+
+    resources:
+    - name: source
+      type: git
+
+  outputs:
+    resources:
+    - name: image
+      type: image
+
+  steps:
+  - name: build
+    image: $(inputs.params.BUILDER_IMAGE)
+    workingDir: /workspace/source
+    command: ['buildah', 'bud', '--format=docker', '--tls-verify=$(inputs.params.TLSVERIFY)', '--layers', '-f', '$(inputs.params.DOCKERFILE)', '-t', '$(outputs.resources.image.url)', '.']
+    volumeMounts:
+    - name: varlibcontainers
+      mountPath: /var/lib/containers
+    securityContext:
+      privileged: true
+
+  - name: push
+    image: $(inputs.params.BUILDER_IMAGE)
+    workingDir: /workspace/source
+    command: ['buildah', 'push', '--tls-verify=$(inputs.params.TLSVERIFY)', '$(outputs.resources.image.url)', 'docker://$(outputs.resources.image.url)']
+    volumeMounts:
+    - name: varlibcontainers
+      mountPath: /var/lib/containers
+    securityContext:
+      privileged: true
+
+  volumes:
+  - name: varlibcontainers
+    emptyDir: {}
diff --git a/test/resources/tekton/kn-pipeline-run.yaml b/test/resources/tekton/kn-pipeline-run.yaml
index f08eec27..9a5b671c 100644
--- a/test/resources/tekton/kn-pipeline-run.yaml
+++ b/test/resources/tekton/kn-pipeline-run.yaml
@@ -16,7 +16,7 @@ kind: PipelineRun
 metadata:
   generateName: buildah-build-kn-create-
 spec:
-  serviceAccount: kn-deployer-account
+  serviceAccountName: kn-deployer-account
   pipelineRef:
     name: buildah-build-kn-create
   resources:
@@ -35,4 +35,4 @@ spec:
         - "--force"
         - "--service-account=kn-deployer-account"
         - "--image=$(inputs.resources.image.url)"
-        - "--env=TARGET=Tekton"
\ No newline at end of file
+        - "--env=TARGET=Tekton"
diff --git a/test/resources/tekton/kn-pipeline.yaml b/test/resources/tekton/kn-pipeline.yaml
index 70c583fe..6e1d8bbd 100644
--- a/test/resources/tekton/kn-pipeline.yaml
+++ b/test/resources/tekton/kn-pipeline.yaml
@@ -41,6 +41,8 @@ spec:
     params:
     - name: DOCKERFILE
       value: ./test/test_images/helloworld/Dockerfile
+    - name: BUILDER_IMAGE
+      value: "quay.io/buildah/stable:latest"
   - name: kn-service-create
     taskRef:
       name: kn
@@ -57,4 +59,4 @@ spec:
       value: "gcr.io/knative-nightly/knative.dev/client/cmd/kn"
     - name: ARGS
       value:
-        - "$(params.ARGS)"
\ No newline at end of file
+        - "$(params.ARGS)"
diff --git a/test/tekton-tests.sh b/test/tekton-tests.sh
index 0b79d775..2ccfc375 100755
--- a/test/tekton-tests.sh
+++ b/test/tekton-tests.sh
@@ -28,7 +28,7 @@ export PATH=$PATH:${REPO_ROOT_DIR}
 # Script entry point.
 initialize $@
 
-export TEKTON_VERSION=${TEKTON_VERSION:-v0.8.0}
+export TEKTON_VERSION=${TEKTON_VERSION:-v0.9.2}
 export KN_E2E_NAMESPACE=tkn-kn
 
 header "Running integration tests for Tekton"
diff --git a/test/test_images/helloworld/Dockerfile b/test/test_images/helloworld/Dockerfile
index c59fb767..5aa9bd40 100644
--- a/test/test_images/helloworld/Dockerfile
+++ b/test/test_images/helloworld/Dockerfile
@@ -1,23 +1,26 @@
-# Use the offical Golang image to create a build artifact.
+# Use the official Golang image to create a build artifact.
 # This is based on Debian and sets the GOPATH to /go.
 # https://hub.docker.com/_/golang
-FROM golang:1.12 as builder
+FROM golang:1.13 as builder
+
+# Create and change to the app directory.
+WORKDIR /app
 
 # Copy local code to the container image.
-WORKDIR /go/src/knative.dev/client/test/test_images/helloworld
-COPY ./test/test_images/helloworld/helloworld.go .
+COPY ./test/test_images/helloworld/ .
 
-# Build the command inside the container.
-# (You may fetch or manage dependencies here,
-# either manually or with a tool like "godep".)
-RUN CGO_ENABLED=0 GOOS=linux go build -v -o helloworld
+# Build the binary.
+# -mod=readonly ensures immutable go.mod and go.sum in container builds.
+RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -v -o server
 
-# Use a Docker multi-stage build to create a lean production image.
+# Use the official Alpine image for a lean production container.
+# https://hub.docker.com/_/alpine
 # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
-FROM scratch
+FROM alpine:3
+RUN apk add --no-cache ca-certificates
 
 # Copy the binary to the production image from the builder stage.
-COPY --from=builder /go/src/knative.dev/client/test/test_images/helloworld/helloworld /helloworld
+COPY --from=builder /app/server /server
 
 # Run the web service on container startup.
-CMD ["/helloworld"]
\ No newline at end of file
+CMD ["/server"]
diff --git a/test/test_images/helloworld/go.mod b/test/test_images/helloworld/go.mod
new file mode 100644
index 00000000..1c89f351
--- /dev/null
+++ b/test/test_images/helloworld/go.mod
@@ -0,0 +1,3 @@
+module knative.dev/client/test/test_images/helloworld
+
+go 1.13
diff --git a/test/test_images/helloworld/helloworld.go b/test/test_images/helloworld/helloworld.go
index 11030351..6d0f4bcc 100644
--- a/test/test_images/helloworld/helloworld.go
+++ b/test/test_images/helloworld/helloworld.go
@@ -22,7 +22,7 @@ import (
 )
 
 func handler(w http.ResponseWriter, r *http.Request) {
-	log.Print("Hello world received a request.")
+	log.Print("helloworld: received a request")
 	target := os.Getenv("TARGET")
 	if target == "" {
 		target = "World"
@@ -31,7 +31,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
 }
 
 func main() {
-	log.Print("Hello world sample started.")
+	log.Print("helloworld: starting server...")
 
 	http.HandleFunc("/", handler)
 
@@ -40,5 +40,6 @@ func main() {
 		port = "8080"
 	}
 
+	log.Printf("helloworld: listening on port %s", port)
 	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
 }