Repository: knative/pkg
Pull request #1332 (2020-05-13 20:07:48)

Library for writing interactive "shell script" in Go
Advance knative/test-infra#2063

Already used elsewhere

/cc @chaodaiG 
/cc @chizhg 

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

This pull-request has been approved by: *<a href="https://github.com/knative/pkg/pull/1332#" title="Author self-approved">coryrc</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **chizhg**
You can assign the PR to them by writing `/assign @chizhg` in a comment when ready.

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

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

- **[test/OWNERS](https://github.com/knative/pkg/blob/master/test/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["chizhg"]} -->
knative-metrics-robot: The following is the coverage report on the affected files.
Say `/test pull-knative-pkg-go-coverage` to re-run this coverage report

File | Old Coverage | New Coverage | Delta
---- |:------------:|:------------:|:-----:
[test/interactive/command.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_pkg/1332/pull-knative-pkg-go-coverage/1260663181127192577/artifacts/line-cov.html#file0) | Do not exist | 55.0% | 
[test/interactive/docker.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_pkg/1332/pull-knative-pkg-go-coverage/1260663181127192577/artifacts/line-cov.html#file1) | Do not exist | 45.5% | 

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-pkg-go-coverage | d0e045094d36225945c6a487f9de9a14dc861610 | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_pkg/1332/pull-knative-pkg-go-coverage/1260663181127192577) | `/test pull-knative-pkg-go-coverage`

[Full PR test history](https://gubernator.knative.dev/pr/knative_pkg/1332). [Your PR dashboard](https://gubernator.knative.dev/pr/coryrc).

<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-pkg-go-coverage` to re-run this coverage report

File | Old Coverage | New Coverage | Delta
---- |:------------:|:------------:|:-----:
[test/interactive/command.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_pkg/1332/pull-knative-pkg-go-coverage/1260795302353309696/artifacts/line-cov.html#file0) | Do not exist | 63.2% | 
[test/interactive/docker.go](https://storage.cloud.google.com/knative-prow/pr-logs/pull/knative_pkg/1332/pull-knative-pkg-go-coverage/1260795302353309696/artifacts/line-cov.html#file1) | Do not exist | 45.5% | 

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-pkg-go-coverage | 533994a7493168e652a61faefba0cf87aaf218fe | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_pkg/1332/pull-knative-pkg-go-coverage/1260795302353309696) | `/test pull-knative-pkg-go-coverage`

[Full PR test history](https://gubernator.knative.dev/pr/knative_pkg/1332). [Your PR dashboard](https://gubernator.knative.dev/pr/coryrc).

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

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-knative-pkg-go-coverage | 533994a7493168e652a61faefba0cf87aaf218fe | [link](https://prow.knative.dev/view/gcs/knative-prow/pr-logs/pull/knative_pkg/1332/pull-knative-pkg-go-coverage/1260795302353309696) | `/test pull-knative-pkg-go-coverage`

[Full PR test history](https://gubernator.knative.dev/pr/knative_pkg/1332). [Your PR dashboard](https://gubernator.knative.dev/pr/coryrc).

<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 -->

Patch:
diff --git a/apis/duck/v1/kresource_type.go b/apis/duck/v1/kresource_type.go
index 80558428c..78d8ece66 100644
--- a/apis/duck/v1/kresource_type.go
+++ b/apis/duck/v1/kresource_type.go
@@ -22,15 +22,15 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 
 	"knative.dev/pkg/apis"
 )
 
 // KRShaped is an interface for retrieving the duck elements of an arbitrary resource.
 type KRShaped interface {
-	metav1.ObjectMetaAccessor
-
-	GetTypeMeta() *metav1.TypeMeta
+	metav1.Object
+	schema.ObjectKind
 
 	GetStatus() *Status
 
@@ -81,11 +81,6 @@ type KResourceList struct {
 	Items []KResource `json:"items"`
 }
 
-// GetTypeMeta retrieves the ObjectMeta of the KResource. Implements the KRShaped interface.
-func (t *KResource) GetTypeMeta() *metav1.TypeMeta {
-	return &t.TypeMeta
-}
-
 // GetStatus retrieves the status of the KResource. Implements the KRShaped interface.
 func (t *KResource) GetStatus() *Status {
 	return &t.Status
diff --git a/configmap/parse.go b/configmap/parse.go
new file mode 100644
index 000000000..097b38f90
--- /dev/null
+++ b/configmap/parse.go
@@ -0,0 +1,112 @@
+/*
+Copyright 2020 The Knative Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package configmap
+
+import (
+	"strconv"
+	"strings"
+	"time"
+)
+
+// ParseFunc is a function taking ConfigMap data and applying a parse operation to it.
+type ParseFunc func(map[string]string) error
+
+// AsString passes the value at key through into the target, if it exists.
+func AsString(key string, target *string) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			*target = raw
+		}
+		return nil
+	}
+}
+
+// AsBool parses the value at key as a boolean into the target, if it exists.
+func AsBool(key string, target *bool) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			*target = strings.EqualFold(raw, "true")
+		}
+		return nil
+	}
+}
+
+// AsInt32 parses the value at key as an int32 into the target, if it exists.
+func AsInt32(key string, target *int32) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			val, err := strconv.ParseInt(raw, 10, 32)
+			if err != nil {
+				return err
+			}
+			*target = int32(val)
+		}
+		return nil
+	}
+}
+
+// AsInt64 parses the value at key as an int64 into the target, if it exists.
+func AsInt64(key string, target *int64) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			val, err := strconv.ParseInt(raw, 10, 64)
+			if err != nil {
+				return err
+			}
+			*target = val
+		}
+		return nil
+	}
+}
+
+// AsFloat64 parses the value at key as a float64 into the target, if it exists.
+func AsFloat64(key string, target *float64) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			val, err := strconv.ParseFloat(raw, 64)
+			if err != nil {
+				return err
+			}
+			*target = val
+		}
+		return nil
+	}
+}
+
+// AsDuration parses the value at key as a time.Duration into the target, if it exists.
+func AsDuration(key string, target *time.Duration) ParseFunc {
+	return func(data map[string]string) error {
+		if raw, ok := data[key]; ok {
+			val, err := time.ParseDuration(raw)
+			if err != nil {
+				return err
+			}
+			*target = val
+		}
+		return nil
+	}
+}
+
+// Parse parses the given map using the parser functions passed in.
+func Parse(data map[string]string, parsers ...ParseFunc) error {
+	for _, parse := range parsers {
+		if err := parse(data); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/network/transports.go b/network/transports.go
index 019f4ba87..745e6a5a8 100644
--- a/network/transports.go
+++ b/network/transports.go
@@ -18,7 +18,6 @@ package network
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net"
 	"net/http"
@@ -54,8 +53,6 @@ var backOffTemplate = wait.Backoff{
 	Steps:    15,
 }
 
-var errDialTimeout = errors.New("timed out dialing")
-
 // DialWithBackOff executes `net.Dialer.DialContext()` with exponentially increasing
 // dial timeouts. In addition it sleeps with random jitter between tries.
 var DialWithBackOff = NewBackoffDialer(backOffTemplate)
@@ -91,7 +88,7 @@ func dialBackOffHelper(ctx context.Context, network, address string, bo wait.Bac
 		}
 		return c, nil
 	}
-	elapsed := time.Now().Sub(start)
+	elapsed := time.Since(start)
 	return nil, fmt.Errorf("timed out dialing after %.2fs", elapsed.Seconds())
 }
 
diff --git a/reconciler/reconcile_common.go b/reconciler/reconcile_common.go
index 38e3bbe38..8af08f61a 100644
--- a/reconciler/reconcile_common.go
+++ b/reconciler/reconcile_common.go
@@ -29,7 +29,7 @@ const failedGenerationBump = "NewObservedGenFailure"
 func PreProcessReconcile(ctx context.Context, resource duckv1.KRShaped) {
 	newStatus := resource.GetStatus()
 
-	if newStatus.ObservedGeneration != resource.GetObjectMeta().GetGeneration() {
+	if newStatus.ObservedGeneration != resource.GetGeneration() {
 		condSet := resource.GetConditionSet()
 		manager := condSet.Manage(newStatus)
 
@@ -46,7 +46,7 @@ func PostProcessReconcile(ctx context.Context, resource duckv1.KRShaped) {
 
 	// Bump observed generation to denote that we have processed this
 	// generation regardless of success or failure.
-	newStatus.ObservedGeneration = resource.GetObjectMeta().GetGeneration()
+	newStatus.ObservedGeneration = resource.GetGeneration()
 
 	rc := mgr.GetTopLevelCondition()
 	if rc.Reason == failedGenerationBump {


Test patch:
diff --git a/apis/test/example/v1alpha1/fiz_types.go b/apis/test/example/v1alpha1/fiz_types.go
index b87559e2e..8b0fbe472 100644
--- a/apis/test/example/v1alpha1/fiz_types.go
+++ b/apis/test/example/v1alpha1/fiz_types.go
@@ -94,11 +94,6 @@ func (f *ClusterFiz) Validate(ctx context.Context) *apis.FieldError {
 	return nil
 }
 
-// GetTypeMeta retrieves the ObjectMeta of the ClusterFiz. Implements the KRShaped interface.
-func (f *ClusterFiz) GetTypeMeta() *metav1.TypeMeta {
-	return &f.TypeMeta
-}
-
 // GetStatus retrieves the status of the ClusterFiz. Implements the KRShaped interface.
 func (f *ClusterFiz) GetStatus() *duckv1.Status {
 	return &f.Status.Status
diff --git a/apis/test/example/v1alpha1/foo_types.go b/apis/test/example/v1alpha1/foo_types.go
index ebe255341..c681ea796 100644
--- a/apis/test/example/v1alpha1/foo_types.go
+++ b/apis/test/example/v1alpha1/foo_types.go
@@ -93,11 +93,6 @@ func (f *Foo) Validate(ctx context.Context) *apis.FieldError {
 	return nil
 }
 
-// GetTypeMeta retrieves the ObjectMeta of the Foo. Implements the KRShaped interface.
-func (f *Foo) GetTypeMeta() *metav1.TypeMeta {
-	return &f.TypeMeta
-}
-
 // GetStatus retrieves the status of the Foo Implements the KRShaped interface.
 func (f *Foo) GetStatus() *duckv1.Status {
 	return &f.Status.Status
diff --git a/apis/test/pub/v1alpha1/bar_types.go b/apis/test/pub/v1alpha1/bar_types.go
index 826dae3fb..48e4f081b 100644
--- a/apis/test/pub/v1alpha1/bar_types.go
+++ b/apis/test/pub/v1alpha1/bar_types.go
@@ -93,11 +93,6 @@ func (b *Bar) Validate(ctx context.Context) *apis.FieldError {
 	return nil
 }
 
-// GetTypeMeta retrieves the ObjectMeta of the Bar. Implements the KRShaped interface.
-func (b *Bar) GetTypeMeta() *metav1.TypeMeta {
-	return &b.TypeMeta
-}
-
 // GetStatus retrieves the status of the Bar. Implements the KRShaped interface.
 func (b *Bar) GetStatus() *duckv1.Status {
 	return &b.Status.Status
diff --git a/configmap/parse_test.go b/configmap/parse_test.go
new file mode 100644
index 000000000..65b9ab56d
--- /dev/null
+++ b/configmap/parse_test.go
@@ -0,0 +1,128 @@
+/*
+Copyright 2020 The Knative Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package configmap
+
+import (
+	"testing"
+	"time"
+)
+
+type testConfig struct {
+	str string
+	boo bool
+	i32 int32
+	i64 int64
+	f64 float64
+	dur time.Duration
+}
+
+func TestParse(t *testing.T) {
+	tests := []struct {
+		name      string
+		conf      testConfig
+		data      map[string]string
+		want      testConfig
+		expectErr bool
+	}{{
+		name: "all good",
+		data: map[string]string{
+			"test-string":   "foo.bar",
+			"test-bool":     "true",
+			"test-int32":    "1",
+			"test-int64":    "2",
+			"test-float64":  "1.0",
+			"test-duration": "1m",
+		},
+		want: testConfig{
+			str: "foo.bar",
+			boo: true,
+			i32: 1,
+			i64: 2,
+			f64: 1.0,
+			dur: time.Minute,
+		},
+	}, {
+		name: "respect defaults",
+		conf: testConfig{
+			str: "foo.bar",
+			boo: true,
+			i32: 1,
+			i64: 2,
+			f64: 1.0,
+			dur: time.Minute,
+		},
+		want: testConfig{
+			str: "foo.bar",
+			boo: true,
+			i32: 1,
+			i64: 2,
+			f64: 1.0,
+			dur: time.Minute,
+		},
+	}, {
+		name: "bool defaults to false",
+		data: map[string]string{
+			"test-bool": "foo",
+		},
+		want: testConfig{
+			boo: false,
+		},
+	}, {
+		name: "int32 error",
+		data: map[string]string{
+			"test-int32": "foo",
+		},
+		expectErr: true,
+	}, {
+		name: "int64 error",
+		data: map[string]string{
+			"test-int64": "foo",
+		},
+		expectErr: true,
+	}, {
+		name: "float64 error",
+		data: map[string]string{
+			"test-float64": "foo",
+		},
+		expectErr: true,
+	}, {
+		name: "duration error",
+		data: map[string]string{
+			"test-duration": "foo",
+		},
+		expectErr: true,
+	}}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			if err := Parse(test.data,
+				AsString("test-string", &test.conf.str),
+				AsBool("test-bool", &test.conf.boo),
+				AsInt32("test-int32", &test.conf.i32),
+				AsInt64("test-int64", &test.conf.i64),
+				AsFloat64("test-float64", &test.conf.f64),
+				AsDuration("test-duration", &test.conf.dur),
+			); (err == nil) == test.expectErr {
+				t.Fatal("Failed to parse data:", err)
+			}
+
+			if test.conf != test.want {
+				t.Fatalf("parsed = %v, want %v", test.conf, test.want)
+			}
+		})
+	}
+}
diff --git a/kmp/reporters_test.go b/kmp/reporters_test.go
index 29e1b729b..2daaf42d7 100644
--- a/kmp/reporters_test.go
+++ b/kmp/reporters_test.go
@@ -31,7 +31,7 @@ type testStruct struct {
 	Omit        string      `json:"omit,omitempty"`
 	Ignore      string      `json:"-"`
 	Dash        string      `json:"-,"`
-	//lint:ignore SA5008 we actually want to test this broken json options work
+	//nolint:staticcheck // We actually want to test this broken json options work
 	MultiComma string `json:"multi,omitempty,somethingelse"`
 }
 
diff --git a/reconciler/reconcile_common_test.go b/reconciler/reconcile_common_test.go
index b61cc0399..1981e0815 100644
--- a/reconciler/reconcile_common_test.go
+++ b/reconciler/reconcile_common_test.go
@@ -90,7 +90,7 @@ func TestPostProcessReconcileBumpsGeneration(t *testing.T) {
 		t.Errorf("Expected observed generation bump got=%d want=%d", resource.Status.ObservedGeneration, resource.Generation)
 	}
 
-	if krShape.GetStatus().ObservedGeneration != krShape.GetObjectMeta().GetGeneration() {
+	if krShape.GetStatus().ObservedGeneration != krShape.GetGeneration() {
 		t.Errorf("Expected observed generation bump got=%d want=%d", resource.Status.ObservedGeneration, resource.Generation)
 	}
 }
diff --git a/test/e2e_flags.go b/test/e2e_flags.go
index 3dfb2eb3d..bc6f90bff 100644
--- a/test/e2e_flags.go
+++ b/test/e2e_flags.go
@@ -32,7 +32,6 @@ import (
 )
 
 var (
-	klogFlags = flag.NewFlagSet("klog", flag.ExitOnError)
 	// Flags holds the command line flags or defaults for settings in the user's environment.
 	// See EnvironmentFlags for a list of supported fields.
 	Flags = initializeFlags()
diff --git a/test/ha/ha.go b/test/ha/ha.go
new file mode 100644
index 000000000..26326358f
--- /dev/null
+++ b/test/ha/ha.go
@@ -0,0 +1,45 @@
+/*
+Copyright 2020 The Knative Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package ha
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"knative.dev/pkg/test"
+	"knative.dev/pkg/test/logging"
+)
+
+// WaitForNewLeader waits until the holder of the given lease is different from the previousLeader.
+func WaitForNewLeader(client *test.KubeClient, lease, namespace, previousLeader string) (string, error) {
+	span := logging.GetEmitableSpan(context.Background(), "WaitForNewLeader/"+lease)
+	defer span.End()
+	var leader string
+	err := wait.PollImmediate(time.Second, time.Minute, func() (bool, error) {
+		lease, err := client.Kube.CoordinationV1().Leases(namespace).Get(lease, metav1.GetOptions{})
+		if err != nil {
+			return false, fmt.Errorf("error getting lease %s: %w", lease, err)
+		}
+		leader = strings.Split(*lease.Spec.HolderIdentity, "_")[0]
+		return leader != previousLeader, nil
+	})
+	return leader, err
+}
diff --git a/test/interactive/command.go b/test/interactive/command.go
new file mode 100644
index 000000000..edea81ab4
--- /dev/null
+++ b/test/interactive/command.go
@@ -0,0 +1,81 @@
+/*
+Copyright 2020 The Knative Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Helper functions for running interactive CLI sessions from Go
+package interactive
+
+import (
+	"io"
+	"log"
+	"os"
+	"os/exec"
+	"strings"
+)
+
+// could be public so extensions of this can test themselves
+var defaultRun func(*exec.Cmd) error
+
+func init() {
+	defaultRun = func(c *exec.Cmd) error { return c.Run() }
+}
+
+// Command represents running an executable, to make it easy to write interactive "shell scripts" in Go.
+// This routes standard in, out, and error from and to the calling program, intended usually to be a login shell run by a person.
+type Command struct {
+	Name string
+	Args []string
+	// If LogFile is set, anything send to stdout and stderr is tee'd to the file named in LogFile when .Run() is called.
+	LogFile string
+	run     func(*exec.Cmd) error
+}
+
+// NewCommand creates an Command, breaking out .Name and .Args for you
+func NewCommand(cmdAndArgs ...string) Command {
+	if len(cmdAndArgs) < 1 {
+		log.Fatal("NewCommand must have a least the command given")
+	}
+	return Command{cmdAndArgs[0], cmdAndArgs[1:], "", defaultRun}
+}
+
+// Run executes the command with exec.Command(), sets standard in/out/err, and returns the result of exec.Cmd.Run()
+// Will tee stdout&err to .LogFile if set
+func (c Command) Run() error {
+	cmd := exec.Command(c.Name, c.Args...)
+	cmd.Stdin = os.Stdin
+	if len(c.LogFile) > 0 {
+		f, err := os.OpenFile(c.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
+		if err != nil {
+			log.Fatal(err)
+		}
+		cmd.Stdout = io.MultiWriter(os.Stdout, f)
+		cmd.Stderr = io.MultiWriter(os.Stderr, f)
+	} else {
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+	}
+	return c.run(cmd)
+}
+
+// String conforms to Stringer
+func (c Command) String() string {
+	s := c.Name + " " + strings.Join(c.Args, " ")
+	return "Command to run: " + s
+}
+
+// AddArgs appends arguments to the current list of args
+func (c *Command) AddArgs(args ...string) {
+	c.Args = append(c.Args, args...)
+}
diff --git a/test/interactive/command_test.go b/test/interactive/command_test.go
new file mode 100644
index 000000000..9ff310217
--- /dev/null
+++ b/test/interactive/command_test.go
@@ -0,0 +1,67 @@
+/*
+Copyright 2020 The Knative Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package interactive
+
+import (
+	"os/exec"
+	"testing"
+
+	"github.com/davecgh/go-spew/spew"
+)
+
+var (
+	mySpew        *spew.ConfigState
+	arbitraryArgs []string
+)
+
+func init() {
+	mySpew = spew.NewDefaultConfig()
+	mySpew.DisableMethods = true
+	arbitraryArgs = []string{"docker", "run", "something"}
+}
+
+func argsValidator(t *testing.T, args []string) func(c *exec.Cmd) error {
+	return func(c *exec.Cmd) error {
+		for i, arg := range arbitraryArgs {
+			if c.Args[i] != arg {
+				t.Errorf("c.Args[%d] not correct", i)
+			}
+		}
+		t.Log("Validated c.Args")
+		if t.Failed() {
+			mySpew.Sdump(*c)
+		}
+		return nil
+	}
+}
+
+func TestNewCommand(t *testing.T) {
+	cmd := NewCommand(arbitraryArgs...)
+	cmd.run = argsValidator(t, arbitraryArgs)
+	cmd.Run()
+}
+
+func TestAddArgs(t *testing.T) {
+	cmd := NewCommand(arbitraryArgs[0])
+	cmd.AddArgs(arbitraryArgs[1:]...)
+	cmd.run = argsValidator(t, arbitraryArgs)
+	cmd.Run()
+}
+
+func TestCommandStringer(t *testing.T) {
+	t.Log(NewCommand("true"))
+}
diff --git a/test/interactive/docker.go b/test/interactive/docker.go
new file mode 100644
index 000000000..08bbbc9c9
--- /dev/null
+++ b/test/interactive/docker.go
@@ -0,0 +1,114 @@
+/*
+Copyright 2020 The Knative Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Helper functions for running interactive CLI sessions from Go
+package interactive
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"path"
+	"strings"
+)
+
+var defaultDockerCommands []string
+
+func init() {
+	defaultDockerCommands = []string{"docker", "run", "-it", "--rm", "--entrypoint", "bash"}
+}
+
+// Env represents a collection of environment variables and their values
+type Env map[string]string
+
+// Docker is mostly an Command preloaded with arguments which setup Docker for running an image interactively.
+type Docker struct {
+	Command
+}
+
+// PromoteFromEnv pulls the named environment variables from the environment and puts them in the Env.
+// It does not stop on error and returns an error listing all the failed values
+func (e Env) PromoteFromEnv(envVars ...string) error {
+	var err error
+	for _, env := range envVars {
+		v := os.Getenv(env)
+		if v == "" {
+			err = fmt.Errorf("environment variable %q is not set; %v", env, err)
+		} else {
+			e[env] = v
+		}
+	}
+	return err
+}
+
+// Creates an Docker with default Docker command arguments for running interactively
+func NewDocker() Docker {
+	return Docker{NewCommand(defaultDockerCommands...)}
+}
+
+// AddEnv adds arguments so all the environment variables present in e become part of the docker run's environment
+func (d *Docker) AddEnv(e Env) {
+	for k, v := range e {
+		d.AddArgs("-e", fmt.Sprintf("%s=%s", k, v))
+	}
+}
+
+// AddMount add arguments for the --mount command
+func (d *Docker) AddMount(typeStr, source, target string, optAdditionalArgs ...string) {
+	addl := ""
+	if len(optAdditionalArgs) != 0 {
+		addl = "," + strings.Join(optAdditionalArgs, ",")
+	}
+	d.AddArgs("--mount", fmt.Sprintf("type=%s,source=%s,target=%s%s", typeStr, source, target, addl))
+}
+
+// AddRWOverlay mounts a directory into the image at the desired location, but with an overlay
+//  so internal changes do not modify the external directory.
+// externalDirectory probably needs to be an absolute path
+// Returns a function to clean up the mount (but does not delete the directory).
+// Uses sudo and probably only works on Linux
+func (d *Docker) AddRWOverlay(externalDirectory, internalDirectory string) func() {
+	tmpDir, err := ioutil.TempDir("", "overlay")
+	if err != nil {
+		log.Fatal(err)
+	}
+	subDirs := []string{"upper", "work", "overlay"}
+	for _, d := range subDirs {
+		err = os.Mkdir(path.Join(tmpDir, d), 0777)
+		if err != nil {
+			log.Fatal(err)
+		}
+	}
+	overlayDir := path.Join(tmpDir, "overlay")
+	// The options for overlay mount are confusing
+	// You need empty directories for upper and work (and overlay, though you can mount over directories that have files in them if you *want* to...)
+	mount := NewCommand("sudo", "mount", "-t", "overlay", "-o",
+		fmt.Sprintf("lowerdir=%s,upperdir=%s/upper,workdir=%s/work", externalDirectory, tmpDir, tmpDir),
+		"none", overlayDir)
+	// Print command to run so user knows why it is asking for sudo password (if it does)
+	log.Println(mount)
+	if err = mount.Run(); err != nil {
+		log.Fatalf("Unable to create overlay mount, so giving up: %v", err)
+	}
+	d.AddMount("bind", overlayDir, internalDirectory)
+	return func() {
+		// Print command to run so user knows why it is asking for sudo password (if it does)
+		umount := NewCommand("sudo", "umount", overlayDir)
+		log.Println(umount)
+		umount.Run()
+	}
+}
diff --git a/test/interactive/docker_test.go b/test/interactive/docker_test.go
new file mode 100644
index 000000000..4c34cf796
--- /dev/null
+++ b/test/interactive/docker_test.go
@@ -0,0 +1,81 @@
+/*
+Copyright 2020 The Knative Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package interactive
+
+import (
+	"os"
+	"os/exec"
+	"testing"
+)
+
+func TestEnv(t *testing.T) {
+	e := make(Env)
+	err := e.PromoteFromEnv("PWD")
+	if err != nil {
+		t.Error(err)
+		t.Logf("The Environ is:\n%+v\n", os.Environ())
+	}
+	epwd, exists := e["PWD"]
+	if !exists || epwd != os.Getenv("PWD") {
+		t.Errorf("$PWD promotion did not occur correctly: Env='%v'; os.Getenv(\"PWD\")=%s", e, os.Getenv("PWD"))
+	}
+	badName := "GEEZ_I_REALLY_H0PE_TH1S_DOES_NOT_EXIST"
+	// and just in case:
+	for {
+		_, exists := os.LookupEnv(badName)
+		if !exists {
+			break
+		}
+		badName = badName + "z"
+	}
+	err = e.PromoteFromEnv(badName)
+	if err == nil {
+		t.Error("Should have received error promoting non-existent variable")
+	}
+}
+
+func TestAddEnv(t *testing.T) {
+	d := NewDocker()
+	e := make(Env)
+	l := len(defaultDockerCommands)
+	e["env"] = "var"
+	d.AddEnv(e)
+	d.run = func(c *exec.Cmd) error {
+		if c.Args[l] != "-e" || c.Args[l+1] != "env=var" {
+			t.Error("Env var wasn't added correctly")
+			t.Log(mySpew.Sdump(c))
+		}
+		t.Log("Test Over")
+		return nil
+	}
+	d.Run()
+}
+
+func TestAddMount(t *testing.T) {
+	d := NewDocker()
+	l := len(defaultDockerCommands)
+	d.AddMount("bind", "mysource", "mytarget", "other1=banana1", "other2=banana2")
+	d.run = func(c *exec.Cmd) error {
+		if c.Args[l] != "--mount" || c.Args[l+1] != "type=bind,source=mysource,target=mytarget,other1=banana1,other2=banana2" {
+			t.Error("Mount wasn't added correctly")
+			t.Log(mySpew.Sdump(c))
+		}
+		t.Log("Test Over")
+		return nil
+	}
+	d.Run()
+}
diff --git a/test/kube_checks.go b/test/kube_checks.go
index 35bcbd201..61a259ac9 100644
--- a/test/kube_checks.go
+++ b/test/kube_checks.go
@@ -25,8 +25,10 @@ import (
 	"strings"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
+	apierrs "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/wait"
 	k8styped "k8s.io/client-go/kubernetes/typed/core/v1"
@@ -93,6 +95,19 @@ func WaitForPodState(client *KubeClient, inState func(p *corev1.Pod) (bool, erro
 	})
 }
 
+// WaitForPodDeleted waits for the given pod to disappear from the given namespace.
+func WaitForPodDeleted(client *KubeClient, name, namespace string) error {
+	if err := WaitForPodState(client, func(p *corev1.Pod) (bool, error) {
+		// Always return false. We're oly interested in the error which indicates pod deletion or timeout.
+		return false, nil
+	}, name, namespace); err != nil {
+		if !apierrs.IsNotFound(err) {
+			return err
+		}
+	}
+	return nil
+}
+
 // WaitForServiceHasAtLeastOneEndpoint polls the status of the specified Service
 // from client every interval until number of service endpoints = numOfEndpoints
 func WaitForServiceEndpoints(client *KubeClient, svcName string, svcNamespace string, numOfEndpoints int) error {
@@ -121,6 +136,29 @@ func countEndpointsNum(e *corev1.Endpoints) int {
 	return num
 }
 
+// GetEndpointAddresses returns addresses of endpoints for the given service.
+func GetEndpointAddresses(client *KubeClient, svcName, svcNamespace string) ([]string, error) {
+	endpoints, err := client.Kube.CoreV1().Endpoints(svcNamespace).Get(svcName, metav1.GetOptions{})
+	if err != nil || countEndpointsNum(endpoints) == 0 {
+		return nil, fmt.Errorf("no endpoints or error: %w", err)
+	}
+	var hosts []string
+	for _, sub := range endpoints.Subsets {
+		for _, addr := range sub.Addresses {
+			hosts = append(hosts, addr.IP)
+		}
+	}
+	return hosts, nil
+}
+
+// WaitForChangedEndpoints waits until the endpoints for the given service differ from origEndpoints.
+func WaitForChangedEndpoints(client *KubeClient, svcName, svcNamespace string, origEndpoints []string) error {
+	return wait.PollImmediate(1*time.Second, 2*time.Minute, func() (bool, error) {
+		newEndpoints, err := GetEndpointAddresses(client, svcName, svcNamespace)
+		return !cmp.Equal(origEndpoints, newEndpoints), err
+	})
+}
+
 // GetConfigMap gets the configmaps for a given namespace
 func GetConfigMap(client *KubeClient, namespace string) k8styped.ConfigMapInterface {
 	return client.Kube.CoreV1().ConfigMaps(namespace)
@@ -176,3 +214,17 @@ func PodsRunning(podList *corev1.PodList) (bool, error) {
 func PodRunning(pod *corev1.Pod) bool {
 	return pod.Status.Phase == corev1.PodRunning || pod.Status.Phase == corev1.PodSucceeded
 }
+
+// WaitForDeploymentScale waits until the given deployment has the expected scale.
+func WaitForDeploymentScale(client *KubeClient, name, namespace string, scale int) error {
+	return WaitForDeploymentState(
+		client,
+		name,
+		func(d *appsv1.Deployment) (bool, error) {
+			return d.Status.ReadyReplicas == int32(scale), nil
+		},
+		"DeploymentIsScaled",
+		namespace,
+		time.Minute,
+	)
+}
diff --git a/test/logging/example_tlogger_test.go b/test/logging/example_tlogger_test.go
index 1395822fa..7c8660034 100644
--- a/test/logging/example_tlogger_test.go
+++ b/test/logging/example_tlogger_test.go
@@ -34,8 +34,7 @@ var (
 
 func init() { someStruct = testStruct{"hello", 42.0} }
 
-// godoc limitation; this would be named Test*(), of course
-
+// nolint:govet // godoc limitation; this would be named Test*(), of course
 func Example(legacy *testing.T) {
 	// Get our TLogger and ready the cleanup function
 	t, cancel := logging.NewTLogger(legacy)
diff --git a/test/webhook-apicoverage/resourcetree/test_util.go b/test/webhook-apicoverage/resourcetree/test_util.go
index 34fca0545..3b922d44a 100644
--- a/test/webhook-apicoverage/resourcetree/test_util.go
+++ b/test/webhook-apicoverage/resourcetree/test_util.go
@@ -49,11 +49,13 @@ type arrayType struct {
 	baseArr   []bool
 }
 
+//nolint:structcheck,unused // Disable warnings about unused fields.
 type otherType struct {
 	structMap map[string]baseType
 	baseMap   map[string]string
 }
 
+//nolint:structcheck,unused // Disable warnings about unused fields.
 type combinedNodeType struct {
 	b baseType
 	a arrayType