Repository: google/kf
Pull request #704 (2019-09-20 23:06:06)

Feature/watcher closures
## Proposed Changes

* Added ability for generators to check conditions and ObservedGenerations to open up #599 

## Release Notes

<!-- kf follows the Keep A Changelog standard for release notes.

https://keepachangelog.com/en/1.0.0/

Changelog entries should be one per line and start with one of the following
words:

`Added` for new features.
`Changed` for changes in existing functionality.
`Deprecated` for soon-to-be removed features.
`Removed` for now removed features.
`Fixed` for any bug fixes.
`Security` in case of vulnerabilities.

If one of the changes is breaking include that as a second word e.g.

  Removed BREAKING support for manifests v1
-->

```release-note

```



Patch:
diff --git a/docs/kf.dev/content/en/docs/developers/develop-and-manage-apps/kf-push/app-manifests/_index.md b/docs/kf.dev/content/en/docs/developers/develop-and-manage-apps/kf-push/app-manifests/_index.md
index f2d7bd6..2176d92 100644
--- a/docs/kf.dev/content/en/docs/developers/develop-and-manage-apps/kf-push/app-manifests/_index.md
+++ b/docs/kf.dev/content/en/docs/developers/develop-and-manage-apps/kf-push/app-manifests/_index.md
@@ -29,16 +29,21 @@ The following fields are valid for objects under `applications`:
 | **services** | string[] | A list of service instance names to automatically bind to the app. |
 | **disk_quota** | quantity | The amount of disk the application should get. Defaults to 1GiB. |
 | **memory** | quantity | The amount of RAM to provide the app. Defaults to 1GiB. |
-| **cpu** | quantity | The amount of CPU to provide the application. Defaults to 0.1 (1/10th of a CPU). |
+| **cpu** † | quantity | The amount of CPU to provide the application. Defaults to 0.1 (1/10th of a CPU). |
 | **instances** | int | The number of instances of the app to run. Defaults to 1. |
-| **min-scale** | int | The minimum number of instances to scale to. Valid only if instances is unset. |
-| **max-scale** | int | The maximum number of instances to scale to. Valid only if instances is unset. Blank means unlimited scaling. |
+| **min-scale** † | int | The minimum number of instances to scale to. Valid only if instances is unset. |
+| **max-scale** † | int | The maximum number of instances to scale to. Valid only if instances is unset. Blank means unlimited scaling. |
 | **routes** | object | A list of routes the app should listen on. See the Route Fields section for more. |
 | **no-route** | boolean | If set to true, the application will not be routable. |
 | **random-route** | boolean | If set to true, the app will be given a random route. |
 | **timeout** | int | The number of seconds to wait for the app to become healthy. |
 | **health-check-type** | string | The type of health-check to use `port`, `none`, or `http`. Default: `port` |
 | **health-check-http-endpoint** | string | The endpoint to target as part of the health-check. Only valid if `health-check-type` is `http`. |
+| **command** | string | The command that starts the app. If supplied, this will be passed to the container entrypoint. |
+| **entrypoint** † | string | Overrides the app container's entrypoint. |
+| **args** † | string[] | Overrides the arguments the app container. |
+
+† Unique to Kf
 
 
 ## Docker Fields
diff --git a/docs/kf.dev/content/en/docs/general-info/kf-cli/commands/kf_push.md b/docs/kf.dev/content/en/docs/general-info/kf-cli/commands/kf_push.md
index 87702fb..1467736 100644
--- a/docs/kf.dev/content/en/docs/general-info/kf-cli/commands/kf_push.md
+++ b/docs/kf.dev/content/en/docs/general-info/kf-cli/commands/kf_push.md
@@ -27,9 +27,12 @@ kf push APP_NAME [flags]
 ### Options
 
 ```
+      --args stringArray            Overwrite the args for the image. Can't be used with the command flag.
   -b, --buildpack string            Skip the 'detect' buildpack step and use the given name.
+  -c, --command string              Startup command for the app, this overrides the default command specified by the web process.
       --container-registry string   Container registry to push sources to. Required for buildpack builds not targeting a Kf space.
       --docker-image string         Docker image to deploy.
+      --entrypoint string           Overwrite the default entrypoint of the image. Can't be used with the command flag.
   -e, --env stringArray             Set environment variables. Multiple can be set by using the flag multiple times (e.g., NAME=VALUE).
       --grpc                        Setup the container to allow application to use gRPC.
   -u, --health-check-type string    Application health check type (http or port, default: port)
diff --git a/pkg/kf/apps/client.go b/pkg/kf/apps/client.go
index 4eefc07..866934e 100644
--- a/pkg/kf/apps/client.go
+++ b/pkg/kf/apps/client.go
@@ -15,7 +15,6 @@
 package apps
 
 import (
-	"fmt"
 	"io"
 
 	v1alpha1 "github.com/google/kf/pkg/apis/kf/v1alpha1"
@@ -91,33 +90,3 @@ func (ac *appsClient) Restage(namespace, name string) (app *v1alpha1.App, err er
 
 	return ac.coreClient.Update(namespace, app)
 }
-
-// ConditionServiceBindingsReady returns true if service bindings are ready and
-// errors if the bindings failed.
-func ConditionServiceBindingsReady(app *v1alpha1.App, apiErr error) (isFinal bool, err error) {
-	if apiErr != nil {
-		return true, apiErr
-	}
-
-	// don't propagate old statuses
-	if app.Generation != app.Status.ObservedGeneration {
-		return false, nil
-	}
-
-	if cond := app.Status.GetCondition(v1alpha1.AppConditionServiceBindingsReady); cond != nil {
-		switch {
-		case cond.IsTrue():
-			return true, nil
-
-		case cond.IsUnknown():
-			return false, nil
-
-		default:
-			// return true and a failrue assuming IsFalse and other statuses can't be
-			// recovered from because they violate the K8s spec
-			return true, fmt.Errorf("checking %s failed, status: %s message: %s reason: %s", cond.Type, cond.Status, cond.Message, cond.Reason)
-		}
-	}
-
-	return false, nil
-}
diff --git a/pkg/kf/apps/client.yml b/pkg/kf/apps/client.yml
index fa26f30..0e30dcb 100644
--- a/pkg/kf/apps/client.yml
+++ b/pkg/kf/apps/client.yml
@@ -6,6 +6,13 @@ kubernetes:
   kind: "App"
   version: "v1alpha1"
   namespaced: true
+  observedGenerationFieldPath: Status.ObservedGeneration
+  conditionsFieldPath: Status.Conditions
+  conditions:
+  - name: Ready
+    ref: v1alpha1.AppConditionReady
+  - name: ServiceBindingsReady
+    ref: v1alpha1.AppConditionServiceBindingsReady
 type: "v1alpha1.App"
 clientType: "cv1alpha1.AppsGetter"
 cf:
diff --git a/pkg/kf/apps/fake/fake_client.go b/pkg/kf/apps/fake/fake_client.go
index 37b153e..9f4e23f 100644
--- a/pkg/kf/apps/fake/fake_client.go
+++ b/pkg/kf/apps/fake/fake_client.go
@@ -267,6 +267,51 @@ func (mr *FakeClientMockRecorder) WaitFor(arg0, arg1, arg2, arg3, arg4 interface
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitFor", reflect.TypeOf((*FakeClient)(nil).WaitFor), arg0, arg1, arg2, arg3, arg4)
 }
 
+// WaitForConditionReadyTrue mocks base method
+func (m *FakeClient) WaitForConditionReadyTrue(arg0 context.Context, arg1, arg2 string, arg3 time.Duration) (*v1alpha1.App, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WaitForConditionReadyTrue", arg0, arg1, arg2, arg3)
+	ret0, _ := ret[0].(*v1alpha1.App)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// WaitForConditionReadyTrue indicates an expected call of WaitForConditionReadyTrue
+func (mr *FakeClientMockRecorder) WaitForConditionReadyTrue(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForConditionReadyTrue", reflect.TypeOf((*FakeClient)(nil).WaitForConditionReadyTrue), arg0, arg1, arg2, arg3)
+}
+
+// WaitForConditionServiceBindingsReadyTrue mocks base method
+func (m *FakeClient) WaitForConditionServiceBindingsReadyTrue(arg0 context.Context, arg1, arg2 string, arg3 time.Duration) (*v1alpha1.App, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WaitForConditionServiceBindingsReadyTrue", arg0, arg1, arg2, arg3)
+	ret0, _ := ret[0].(*v1alpha1.App)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// WaitForConditionServiceBindingsReadyTrue indicates an expected call of WaitForConditionServiceBindingsReadyTrue
+func (mr *FakeClientMockRecorder) WaitForConditionServiceBindingsReadyTrue(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForConditionServiceBindingsReadyTrue", reflect.TypeOf((*FakeClient)(nil).WaitForConditionServiceBindingsReadyTrue), arg0, arg1, arg2, arg3)
+}
+
+// WaitForDeletion mocks base method
+func (m *FakeClient) WaitForDeletion(arg0 context.Context, arg1, arg2 string, arg3 time.Duration) (*v1alpha1.App, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WaitForDeletion", arg0, arg1, arg2, arg3)
+	ret0, _ := ret[0].(*v1alpha1.App)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// WaitForDeletion indicates an expected call of WaitForDeletion
+func (mr *FakeClientMockRecorder) WaitForDeletion(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForDeletion", reflect.TypeOf((*FakeClient)(nil).WaitForDeletion), arg0, arg1, arg2, arg3)
+}
+
 // WaitForE mocks base method
 func (m *FakeClient) WaitForE(arg0 context.Context, arg1, arg2 string, arg3 time.Duration, arg4 apps.ConditionFuncE) (*v1alpha1.App, error) {
 	m.ctrl.T.Helper()
diff --git a/pkg/kf/apps/kfapp.go b/pkg/kf/apps/kfapp.go
index 4f7bb2b..4668cda 100644
--- a/pkg/kf/apps/kfapp.go
+++ b/pkg/kf/apps/kfapp.go
@@ -243,6 +243,34 @@ func (k *KfApp) GetServiceBindings() []v1alpha1.AppSpecServiceBinding {
 	return k.Spec.ServiceBindings
 }
 
+// SetCommand sets the entrypoint for the app.
+func (k *KfApp) SetCommand(entrypoint []string) {
+	k.getOrCreateContainer().Command = entrypoint
+}
+
+// GetCommand gets the entrypoint for the app.
+func (k *KfApp) GetCommand() []string {
+	if container := k.getContainerOrNil(); container != nil {
+		return container.Command
+	}
+
+	return nil
+}
+
+// SetArgs sets the arguments for the app.
+func (k *KfApp) SetArgs(args []string) {
+	k.getOrCreateContainer().Args = args
+}
+
+// GetArgs gets the arguments for the app.
+func (k *KfApp) GetArgs() []string {
+	if container := k.getContainerOrNil(); container != nil {
+		return container.Args
+	}
+
+	return nil
+}
+
 // GetClusterURL gets the internal address of the app or the empty string if
 // unset.
 func (k *KfApp) GetClusterURL() string {
diff --git a/pkg/kf/apps/push-options.yml b/pkg/kf/apps/push-options.yml
index dc0b2b0..0c13854 100644
--- a/pkg/kf/apps/push-options.yml
+++ b/pkg/kf/apps/push-options.yml
@@ -68,4 +68,10 @@ configs:
   - name: ServiceBindings
     type: "[]v1alpha1.AppSpecServiceBinding"
     description: a list of Services to bind to the app
+  - name: Command
+    type: "[]string"
+    description: the app container entrypoint
+  - name: Args
+    type: "[]string"
+    description: the app container arguments
 - name: Deploy
diff --git a/pkg/kf/apps/push.go b/pkg/kf/apps/push.go
index c1aee92..b39bd1d 100644
--- a/pkg/kf/apps/push.go
+++ b/pkg/kf/apps/push.go
@@ -74,6 +74,8 @@ func newApp(appName string, opts ...PushOption) (*v1alpha1.App, error) {
 	app.SetHealthCheck(cfg.HealthCheck)
 	app.Spec.Routes = cfg.Routes
 	app.Spec.ServiceBindings = cfg.ServiceBindings
+	app.SetCommand(cfg.Command)
+	app.SetArgs(cfg.Args)
 
 	if cfg.Grpc {
 		app.SetContainerPorts([]corev1.ContainerPort{{Name: "h2c", ContainerPort: 8080}})
diff --git a/pkg/kf/apps/push_options.go b/pkg/kf/apps/push_options.go
index 9c7d94a..faac431 100644
--- a/pkg/kf/apps/push_options.go
+++ b/pkg/kf/apps/push_options.go
@@ -25,10 +25,14 @@ import (
 )
 
 type pushConfig struct {
+	// Args is the app container arguments
+	Args []string
 	// Buildpack is skip the detect buildpack step and use the given name
 	Buildpack string
 	// CPU is app CPU request
 	CPU *resource.Quantity
+	// Command is the app container entrypoint
+	Command []string
 	// ContainerImage is the container to deploy
 	ContainerImage string
 	// DefaultRouteDomain is Domain for a defaultroute. Only used if a route doesn't already exist
@@ -93,6 +97,12 @@ func (opts PushOptions) Extend(other PushOptions) PushOptions {
 	return out
 }
 
+// Args returns the last set value for Args or the empty value
+// if not set.
+func (opts PushOptions) Args() []string {
+	return opts.toConfig().Args
+}
+
 // Buildpack returns the last set value for Buildpack or the empty value
 // if not set.
 func (opts PushOptions) Buildpack() string {
@@ -105,6 +115,12 @@ func (opts PushOptions) CPU() *resource.Quantity {
 	return opts.toConfig().CPU
 }
 
+// Command returns the last set value for Command or the empty value
+// if not set.
+func (opts PushOptions) Command() []string {
+	return opts.toConfig().Command
+}
+
 // ContainerImage returns the last set value for ContainerImage or the empty value
 // if not set.
 func (opts PushOptions) ContainerImage() string {
@@ -213,6 +229,13 @@ func (opts PushOptions) Stack() string {
 	return opts.toConfig().Stack
 }
 
+// WithPushArgs creates an Option that sets the app container arguments
+func WithPushArgs(val []string) PushOption {
+	return func(cfg *pushConfig) {
+		cfg.Args = val
+	}
+}
+
 // WithPushBuildpack creates an Option that sets skip the detect buildpack step and use the given name
 func WithPushBuildpack(val string) PushOption {
 	return func(cfg *pushConfig) {
@@ -227,6 +250,13 @@ func WithPushCPU(val *resource.Quantity) PushOption {
 	}
 }
 
+// WithPushCommand creates an Option that sets the app container entrypoint
+func WithPushCommand(val []string) PushOption {
+	return func(cfg *pushConfig) {
+		cfg.Command = val
+	}
+}
+
 // WithPushContainerImage creates an Option that sets the container to deploy
 func WithPushContainerImage(val string) PushOption {
 	return func(cfg *pushConfig) {
diff --git a/pkg/kf/apps/zz_generated.client.go b/pkg/kf/apps/zz_generated.client.go
index ca69d49..8c7e59d 100644
--- a/pkg/kf/apps/zz_generated.client.go
+++ b/pkg/kf/apps/zz_generated.client.go
@@ -27,6 +27,7 @@ import (
 
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"knative.dev/pkg/apis"
 	"knative.dev/pkg/kmp"
 )
 
@@ -48,6 +49,11 @@ const (
 	APIVersion = "v1alpha1"
 )
 
+var (
+	ConditionReady                = apis.ConditionType(v1alpha1.AppConditionReady)
+	ConditionServiceBindingsReady = apis.ConditionType(v1alpha1.AppConditionServiceBindingsReady)
+)
+
 // Predicate is a boolean function for a v1alpha1.App.
 type Predicate func(*v1alpha1.App) bool
 
@@ -104,6 +110,29 @@ func (list List) Filter(filter Predicate) (out List) {
 	return
 }
 
+// ObservedGenerationMatchesGeneration is a predicate that returns true if the
+// object's ObservedGeneration matches the genration of the object.
+func ObservedGenerationMatchesGeneration(obj *v1alpha1.App) bool {
+	return obj.Generation == obj.Status.ObservedGeneration
+}
+
+// ExtractConditions converts the native condition types into an apis.Condition
+// array with the Type, Status, Reason, and Message fields intact.
+func ExtractConditions(obj *v1alpha1.App) (extracted []apis.Condition) {
+	for _, cond := range obj.Status.Conditions {
+		// Only copy the following four fields to be compatible with
+		// recommended Kuberntes fields.
+		extracted = append(extracted, apis.Condition{
+			Type:    apis.ConditionType(cond.Type),
+			Status:  cond.Status,
+			Reason:  cond.Reason,
+			Message: cond.Message,
+		})
+	}
+
+	return
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // Client
 ////////////////////////////////////////////////////////////////////////////////
@@ -120,6 +149,11 @@ type Client interface {
 	WaitFor(ctx context.Context, namespace string, name string, interval time.Duration, condition Predicate) (*v1alpha1.App, error)
 	WaitForE(ctx context.Context, namespace string, name string, interval time.Duration, condition ConditionFuncE) (*v1alpha1.App, error)
 
+	// Utility functions
+	WaitForDeletion(ctx context.Context, namespace string, name string, interval time.Duration) (*v1alpha1.App, error)
+	WaitForConditionReadyTrue(ctx context.Context, namespace string, name string, interval time.Duration) (*v1alpha1.App, error)
+	WaitForConditionServiceBindingsReadyTrue(ctx context.Context, namespace string, name string, interval time.Duration) (*v1alpha1.App, error)
+
 	// ClientExtension can be used by the developer to extend the client.
 	ClientExtension
 }
@@ -318,3 +352,60 @@ func wrapPredicate(condition Predicate) ConditionFuncE {
 		return condition(obj), nil
 	}
 }
+
+// WaitForDeletion is a utility function that combines WaitForE with ConditionDeleted.
+func (core *coreClient) WaitForDeletion(ctx context.Context, namespace string, name string, interval time.Duration) (instance *v1alpha1.App, err error) {
+	return core.WaitForE(ctx, namespace, name, interval, ConditionDeleted)
+}
+
+func checkConditionTrue(obj *v1alpha1.App, err error, condition apis.ConditionType) (bool, error) {
+	if err != nil {
+		return true, err
+	}
+
+	// don't propagate old statuses
+	if !ObservedGenerationMatchesGeneration(obj) {
+		return false, nil
+	}
+
+	for _, cond := range ExtractConditions(obj) {
+		if cond.Type == condition {
+			switch {
+			case cond.IsTrue():
+				return true, nil
+
+			case cond.IsUnknown():
+				return false, nil
+
+			default:
+				// return true and a failure assuming IsFalse and other statuses can't be
+				// recovered from because they violate the K8s spec
+				return true, fmt.Errorf("checking %s failed, status: %s message: %s reason: %s", cond.Type, cond.Status, cond.Message, cond.Reason)
+			}
+		}
+	}
+
+	return false, nil
+}
+
+// ConditionReadyTrue is a ConditionFuncE that waits for Condition{Ready v1alpha1.AppConditionReady } to
+// become true and fails with an error if the condition becomes false.
+func ConditionReadyTrue(obj *v1alpha1.App, err error) (bool, error) {
+	return checkConditionTrue(obj, err, ConditionReady)
+}
+
+// WaitForConditionReadyTrue is a utility function that combines WaitForE with ConditionReadyTrue.
+func (core *coreClient) WaitForConditionReadyTrue(ctx context.Context, namespace string, name string, interval time.Duration) (instance *v1alpha1.App, err error) {
+	return core.WaitForE(ctx, namespace, name, interval, ConditionReadyTrue)
+}
+
+// ConditionServiceBindingsReadyTrue is a ConditionFuncE that waits for Condition{ServiceBindingsReady v1alpha1.AppConditionServiceBindingsReady } to
+// become true and fails with an error if the condition becomes false.
+func ConditionServiceBindingsReadyTrue(obj *v1alpha1.App, err error) (bool, error) {
+	return checkConditionTrue(obj, err, ConditionServiceBindingsReady)
+}
+
+// WaitForConditionServiceBindingsReadyTrue is a utility function that combines WaitForE with ConditionServiceBindingsReadyTrue.
+func (core *coreClient) WaitForConditionServiceBindingsReadyTrue(ctx context.Context, namespace string, name string, interval time.Duration) (instance *v1alpha1.App, err error) {
+	return core.WaitForE(ctx, namespace, name, interval, ConditionServiceBindingsReadyTrue)
+}
diff --git a/pkg/kf/commands/apps/app.go b/pkg/kf/commands/apps/app.go
index 6e4cc8e..593c7df 100644
--- a/pkg/kf/commands/apps/app.go
+++ b/pkg/kf/commands/apps/app.go
@@ -23,8 +23,8 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 )
diff --git a/pkg/kf/commands/apps/apps.go b/pkg/kf/commands/apps/apps.go
index ac963e5..68e130c 100644
--- a/pkg/kf/commands/apps/apps.go
+++ b/pkg/kf/commands/apps/apps.go
@@ -22,8 +22,8 @@ import (
 
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 	corev1 "k8s.io/api/core/v1"
 )
diff --git a/pkg/kf/commands/apps/delete.go b/pkg/kf/commands/apps/delete.go
index 930428b..fa636d6 100644
--- a/pkg/kf/commands/apps/delete.go
+++ b/pkg/kf/commands/apps/delete.go
@@ -20,7 +20,7 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
diff --git a/pkg/kf/commands/apps/env.go b/pkg/kf/commands/apps/env.go
index 0303f10..d39e2cb 100644
--- a/pkg/kf/commands/apps/env.go
+++ b/pkg/kf/commands/apps/env.go
@@ -18,8 +18,8 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
diff --git a/pkg/kf/commands/apps/logs.go b/pkg/kf/commands/apps/logs.go
index 709dbfa..c11a14a 100644
--- a/pkg/kf/commands/apps/logs.go
+++ b/pkg/kf/commands/apps/logs.go
@@ -20,8 +20,7 @@ import (
 
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
-	kfi "github.com/google/kf/pkg/kf/internal/kf"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/logs"
 	"github.com/spf13/cobra"
 )
@@ -65,7 +64,7 @@ func NewLogsCommand(p *config.KfParams, tailer logs.Tailer) *cobra.Command {
 				logs.WithTailNumberLines(numberLines),
 				logs.WithTailFollow(shouldFollow),
 			); err != nil {
-				cmd.SilenceUsage = !kfi.ConfigError(err)
+				cmd.SilenceUsage = !utils.ConfigError(err)
 				return fmt.Errorf("failed to tail logs: %s", err)
 			}
 
diff --git a/pkg/kf/commands/apps/proxy.go b/pkg/kf/commands/apps/proxy.go
index b0b01c8..f15e149 100644
--- a/pkg/kf/commands/apps/proxy.go
+++ b/pkg/kf/commands/apps/proxy.go
@@ -22,7 +22,7 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/istio"
 	"github.com/spf13/cobra"
 )
diff --git a/pkg/kf/commands/apps/push.go b/pkg/kf/commands/apps/push.go
index 1eff62d..f98a05b 100644
--- a/pkg/kf/commands/apps/push.go
+++ b/pkg/kf/commands/apps/push.go
@@ -28,8 +28,7 @@ import (
 	"github.com/google/kf/pkg/internal/envutil"
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
-	kfi "github.com/google/kf/pkg/kf/internal/kf"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/manifest"
 	servicebindings "github.com/google/kf/pkg/kf/service-bindings"
 	"github.com/spf13/cobra"
@@ -73,25 +72,28 @@ func NewPushCommand(
 	serviceBindingClient servicebindings.ClientInterface,
 ) *cobra.Command {
 	var (
-		containerRegistry  string
-		sourceImage        string
-		containerImage     string
-		manifestFile       string
-		instances          int
-		minScale           int
-		maxScale           int
-		path               string
-		buildpack          string
-		stack              string
-		envs               []string
-		grpc               bool
-		noManifest         bool
-		noStart            bool
-		healthCheckType    string
-		healthCheckTimeout int
-		memoryRequest      *resource.Quantity
-		storageRequest     *resource.Quantity
-		cpuRequest         *resource.Quantity
+		containerRegistry   string
+		sourceImage         string
+		containerImage      string
+		manifestFile        string
+		instances           int
+		minScale            int
+		maxScale            int
+		path                string
+		buildpack           string
+		stack               string
+		envs                []string
+		grpc                bool
+		noManifest          bool
+		noStart             bool
+		healthCheckType     string
+		healthCheckTimeout  int
+		memoryRequest       *resource.Quantity
+		storageRequest      *resource.Quantity
+		cpuRequest          *resource.Quantity
+		startupCommand      string
+		containerEntrypoint string
+		containerArgs       []string
 
 		// Route Flags
 		rawRoutes         []string
@@ -163,6 +165,9 @@ func NewPushCommand(
 			{
 				overrides.Docker.Image = containerImage
 				overrides.Stack = stack
+				overrides.Command = startupCommand
+				overrides.Args = containerArgs
+				overrides.Entrypoint = containerEntrypoint
 
 				// Read environment variables from cli args
 				envVars, err := envutil.ParseCLIEnvVars(envs)
@@ -298,6 +303,8 @@ func NewPushCommand(
 					apps.WithPushHealthCheck(healthCheck),
 					apps.WithPushRandomRouteDomain(randomRouteDomain),
 					apps.WithPushDefaultRouteDomain(defaultRouteDomain),
+					apps.WithPushCommand(app.CommandEntrypoint()),
+					apps.WithPushArgs(app.CommandArgs()),
 				}
 
 				if app.Docker.Image == "" {
@@ -358,7 +365,7 @@ func NewPushCommand(
 
 				err = pusher.Push(app.Name, pushOpts...)
 
-				cmd.SilenceUsage = !kfi.ConfigError(err)
+				cmd.SilenceUsage = !utils.ConfigError(err)
 
 				if err != nil {
 					return err
@@ -514,6 +521,28 @@ func NewPushCommand(
 		"Use the routes flag to provide multiple HTTP and TCP routes. Each route for this app is created if it does not already exist.",
 	)
 
+	pushCmd.Flags().StringVarP(
+		&startupCommand,
+		"command",
+		"c",
+		"",
+		"Startup command for the app, this overrides the default command specified by the web process.",
+	)
+
+	pushCmd.Flags().StringVar(
+		&containerEntrypoint,
+		"entrypoint",
+		"",
+		"Overwrite the default entrypoint of the image. Can't be used with the command flag.",
+	)
+
+	pushCmd.Flags().StringArrayVar(
+		&containerArgs,
+		"args",
+		nil,
+		"Overwrite the args for the image. Can't be used with the command flag.",
+	)
+
 	return pushCmd
 }
 
diff --git a/pkg/kf/commands/apps/restage.go b/pkg/kf/commands/apps/restage.go
index 9f63f43..1ed2b6b 100644
--- a/pkg/kf/commands/apps/restage.go
+++ b/pkg/kf/commands/apps/restage.go
@@ -20,7 +20,7 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
diff --git a/pkg/kf/commands/apps/restart.go b/pkg/kf/commands/apps/restart.go
index ba48cfb..efa12fc 100644
--- a/pkg/kf/commands/apps/restart.go
+++ b/pkg/kf/commands/apps/restart.go
@@ -20,7 +20,7 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
diff --git a/pkg/kf/commands/apps/scale.go b/pkg/kf/commands/apps/scale.go
index 25fdb86..b341bb7 100644
--- a/pkg/kf/commands/apps/scale.go
+++ b/pkg/kf/commands/apps/scale.go
@@ -22,8 +22,8 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
diff --git a/pkg/kf/commands/apps/set_env.go b/pkg/kf/commands/apps/set_env.go
index 59153ba..5ef6d47 100644
--- a/pkg/kf/commands/apps/set_env.go
+++ b/pkg/kf/commands/apps/set_env.go
@@ -21,7 +21,7 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 	corev1 "k8s.io/api/core/v1"
 )
diff --git a/pkg/kf/commands/apps/start.go b/pkg/kf/commands/apps/start.go
index 9dca7c9..6a8f179 100644
--- a/pkg/kf/commands/apps/start.go
+++ b/pkg/kf/commands/apps/start.go
@@ -21,7 +21,7 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
diff --git a/pkg/kf/commands/apps/stop.go b/pkg/kf/commands/apps/stop.go
index 839c78c..2fd55e5 100644
--- a/pkg/kf/commands/apps/stop.go
+++ b/pkg/kf/commands/apps/stop.go
@@ -21,7 +21,7 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
diff --git a/pkg/kf/commands/apps/unset_env.go b/pkg/kf/commands/apps/unset_env.go
index af3871d..5474887 100644
--- a/pkg/kf/commands/apps/unset_env.go
+++ b/pkg/kf/commands/apps/unset_env.go
@@ -21,7 +21,7 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
diff --git a/pkg/kf/commands/buildpacks/buildpacks.go b/pkg/kf/commands/buildpacks/buildpacks.go
index 2760df9..510e32f 100644
--- a/pkg/kf/commands/buildpacks/buildpacks.go
+++ b/pkg/kf/commands/buildpacks/buildpacks.go
@@ -20,9 +20,8 @@ import (
 
 	"github.com/google/kf/pkg/kf/buildpacks"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
-	"github.com/google/kf/pkg/kf/internal/kf"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
@@ -53,7 +52,7 @@ func NewBuildpacksCommand(p *config.KfParams, l buildpacks.Client) *cobra.Comman
 
 			bps, err := l.List(space.Spec.BuildpackBuild.BuilderImage)
 			if err != nil {
-				cmd.SilenceUsage = !kf.ConfigError(err)
+				cmd.SilenceUsage = !utils.ConfigError(err)
 				return err
 			}
 
diff --git a/pkg/kf/commands/buildpacks/stacks.go b/pkg/kf/commands/buildpacks/stacks.go
index ade7205..96d9d1d 100644
--- a/pkg/kf/commands/buildpacks/stacks.go
+++ b/pkg/kf/commands/buildpacks/stacks.go
@@ -20,9 +20,8 @@ import (
 
 	"github.com/google/kf/pkg/kf/buildpacks"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
-	"github.com/google/kf/pkg/kf/internal/kf"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
@@ -53,7 +52,7 @@ func NewStacksCommand(p *config.KfParams, l buildpacks.Client) *cobra.Command {
 
 			stacks, err := l.Stacks(space.Spec.BuildpackBuild.BuilderImage)
 			if err != nil {
-				cmd.SilenceUsage = !kf.ConfigError(err)
+				cmd.SilenceUsage = !utils.ConfigError(err)
 				return err
 			}
 
diff --git a/pkg/kf/commands/builds/list.go b/pkg/kf/commands/builds/list.go
index 6f05080..c063f17 100644
--- a/pkg/kf/commands/builds/list.go
+++ b/pkg/kf/commands/builds/list.go
@@ -20,8 +20,8 @@ import (
 
 	"github.com/google/kf/pkg/apis/kf/v1alpha1"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/sources"
 	"github.com/spf13/cobra"
 	"k8s.io/apimachinery/pkg/api/meta/table"
diff --git a/pkg/kf/commands/builds/logs.go b/pkg/kf/commands/builds/logs.go
index 0c70a32..493c6d8 100644
--- a/pkg/kf/commands/builds/logs.go
+++ b/pkg/kf/commands/builds/logs.go
@@ -19,7 +19,7 @@ import (
 
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/sources"
 	"github.com/spf13/cobra"
 )
diff --git a/pkg/kf/commands/routes/create_route.go b/pkg/kf/commands/routes/create_route.go
index 8eb5a1f..cea4954 100644
--- a/pkg/kf/commands/routes/create_route.go
+++ b/pkg/kf/commands/routes/create_route.go
@@ -21,7 +21,7 @@ import (
 
 	"github.com/google/kf/pkg/apis/kf/v1alpha1"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/routeclaims"
 	"github.com/spf13/cobra"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/pkg/kf/commands/routes/delete_route.go b/pkg/kf/commands/routes/delete_route.go
index 7014657..0937375 100644
--- a/pkg/kf/commands/routes/delete_route.go
+++ b/pkg/kf/commands/routes/delete_route.go
@@ -21,7 +21,7 @@ import (
 	"github.com/google/kf/pkg/kf/algorithms"
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/routeclaims"
 	"github.com/spf13/cobra"
 )
diff --git a/pkg/kf/commands/routes/map_route.go b/pkg/kf/commands/routes/map_route.go
index a0329ec..2278da2 100644
--- a/pkg/kf/commands/routes/map_route.go
+++ b/pkg/kf/commands/routes/map_route.go
@@ -21,7 +21,7 @@ import (
 	"github.com/google/kf/pkg/apis/kf/v1alpha1"
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
diff --git a/pkg/kf/commands/routes/proxy_route.go b/pkg/kf/commands/routes/proxy_route.go
index cca9679..e66e77e 100644
--- a/pkg/kf/commands/routes/proxy_route.go
+++ b/pkg/kf/commands/routes/proxy_route.go
@@ -21,7 +21,7 @@ import (
 
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/istio"
 	"github.com/spf13/cobra"
 )
diff --git a/pkg/kf/commands/routes/routes.go b/pkg/kf/commands/routes/routes.go
index 9c674d9..98e33ab 100644
--- a/pkg/kf/commands/routes/routes.go
+++ b/pkg/kf/commands/routes/routes.go
@@ -24,8 +24,8 @@ import (
 	"github.com/google/kf/pkg/kf/algorithms"
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/routeclaims"
 	"github.com/google/kf/pkg/kf/routes"
 	"github.com/spf13/cobra"
diff --git a/pkg/kf/commands/routes/unmap_route.go b/pkg/kf/commands/routes/unmap_route.go
index 24a837c..bf63c09 100644
--- a/pkg/kf/commands/routes/unmap_route.go
+++ b/pkg/kf/commands/routes/unmap_route.go
@@ -23,7 +23,7 @@ import (
 	"github.com/google/kf/pkg/kf/algorithms"
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 )
 
diff --git a/pkg/kf/commands/service-bindings/bind-service.go b/pkg/kf/commands/service-bindings/bind-service.go
index a87d551..432f6d9 100644
--- a/pkg/kf/commands/service-bindings/bind-service.go
+++ b/pkg/kf/commands/service-bindings/bind-service.go
@@ -19,7 +19,7 @@ import (
 	"fmt"
 
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	servicebindings "github.com/google/kf/pkg/kf/service-bindings"
 	"github.com/google/kf/pkg/kf/services"
 	"github.com/spf13/cobra"
diff --git a/pkg/kf/commands/service-bindings/bindings.go b/pkg/kf/commands/service-bindings/bindings.go
index 614f4dc..5b2e5e6 100644
--- a/pkg/kf/commands/service-bindings/bindings.go
+++ b/pkg/kf/commands/service-bindings/bindings.go
@@ -20,8 +20,8 @@ import (
 
 	kfv1alpha1 "github.com/google/kf/pkg/apis/kf/v1alpha1"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	servicebindings "github.com/google/kf/pkg/kf/service-bindings"
 	"github.com/spf13/cobra"
 )
diff --git a/pkg/kf/commands/service-bindings/unbind-service.go b/pkg/kf/commands/service-bindings/unbind-service.go
index 6312e44..d4fb3d9 100644
--- a/pkg/kf/commands/service-bindings/unbind-service.go
+++ b/pkg/kf/commands/service-bindings/unbind-service.go
@@ -18,7 +18,7 @@ import (
 	"fmt"
 
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	servicebindings "github.com/google/kf/pkg/kf/service-bindings"
 	"github.com/spf13/cobra"
 )
diff --git a/pkg/kf/commands/service-bindings/vcap_services.go b/pkg/kf/commands/service-bindings/vcap_services.go
index 97b3fa4..8a62e69 100644
--- a/pkg/kf/commands/service-bindings/vcap_services.go
+++ b/pkg/kf/commands/service-bindings/vcap_services.go
@@ -19,7 +19,7 @@ import (
 	"fmt"
 
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
diff --git a/pkg/kf/commands/service-brokers/create-service-broker.go b/pkg/kf/commands/service-brokers/create-service-broker.go
index 7054604..f936a67 100644
--- a/pkg/kf/commands/service-brokers/create-service-broker.go
+++ b/pkg/kf/commands/service-brokers/create-service-broker.go
@@ -19,7 +19,7 @@ import (
 
 	servicecatalogclient "github.com/google/kf/pkg/client/servicecatalog/clientset/versioned"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	servicecatalogv1beta1 "github.com/poy/service-catalog/pkg/apis/servicecatalog/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
diff --git a/pkg/kf/commands/service-brokers/delete-service-broker.go b/pkg/kf/commands/service-brokers/delete-service-broker.go
index 103f6c4..e716c44 100644
--- a/pkg/kf/commands/service-brokers/delete-service-broker.go
+++ b/pkg/kf/commands/service-brokers/delete-service-broker.go
@@ -21,7 +21,7 @@ import (
 	servicecatalogclient "github.com/google/kf/pkg/client/servicecatalog/clientset/versioned"
 	"github.com/google/kf/pkg/kf/commands/config"
 	installutil "github.com/google/kf/pkg/kf/commands/install/util"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/spf13/cobra"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
diff --git a/pkg/kf/commands/services/create-service.go b/pkg/kf/commands/services/create-service.go
index 9e903b1..035092a 100644
--- a/pkg/kf/commands/services/create-service.go
+++ b/pkg/kf/commands/services/create-service.go
@@ -20,8 +20,8 @@ import (
 
 	servicecatalogclient "github.com/google/kf/pkg/client/servicecatalog/clientset/versioned"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/services"
 	servicecatalogv1beta1 "github.com/poy/service-catalog/pkg/apis/servicecatalog/v1beta1"
 	"github.com/spf13/cobra"
diff --git a/pkg/kf/commands/services/delete-service.go b/pkg/kf/commands/services/delete-service.go
index 8762ee1..150ff96 100644
--- a/pkg/kf/commands/services/delete-service.go
+++ b/pkg/kf/commands/services/delete-service.go
@@ -18,7 +18,7 @@ import (
 	"fmt"
 
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/services"
 	"github.com/spf13/cobra"
 )
diff --git a/pkg/kf/commands/services/marketplace.go b/pkg/kf/commands/services/marketplace.go
index a2b23c7..cb20a24 100644
--- a/pkg/kf/commands/services/marketplace.go
+++ b/pkg/kf/commands/services/marketplace.go
@@ -19,8 +19,8 @@ import (
 	"io"
 
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/services"
 	servicecatalog "github.com/poy/service-catalog/pkg/svcat/service-catalog"
 	"github.com/spf13/cobra"
diff --git a/pkg/kf/commands/services/service.go b/pkg/kf/commands/services/service.go
index 18d2191..2ca4670 100644
--- a/pkg/kf/commands/services/service.go
+++ b/pkg/kf/commands/services/service.go
@@ -16,8 +16,8 @@ package services
 
 import (
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/services"
 	"github.com/spf13/cobra"
 )
diff --git a/pkg/kf/commands/services/services.go b/pkg/kf/commands/services/services.go
index e2251c4..ba98e59 100644
--- a/pkg/kf/commands/services/services.go
+++ b/pkg/kf/commands/services/services.go
@@ -22,8 +22,8 @@ import (
 	"github.com/google/kf/pkg/apis/kf/v1alpha1"
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
 	"github.com/google/kf/pkg/kf/describe"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/services"
 	"github.com/spf13/cobra"
 )
diff --git a/pkg/kf/commands/spaces/delete.go b/pkg/kf/commands/spaces/delete.go
index dfa821c..fddaa8a 100644
--- a/pkg/kf/commands/spaces/delete.go
+++ b/pkg/kf/commands/spaces/delete.go
@@ -19,7 +19,7 @@ import (
 
 	"github.com/google/kf/pkg/kf/commands/completion"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/spaces"
 	"github.com/spf13/cobra"
 )
diff --git a/pkg/kf/internal/kf/utils.go b/pkg/kf/internal/kf/utils.go
deleted file mode 100644
index 500cb09..0000000
--- a/pkg/kf/internal/kf/utils.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2019 Google LLC
-//
-// 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
-//
-//     https://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 kf
-
-// ConfigErr is used to indicate that the returned error is due to a user's
-// invalid configuration.
-type ConfigErr struct {
-	// Reason holds the error message.
-	Reason string
-}
-
-// Error implements error.
-func (e ConfigErr) Error() string {
-	return e.Reason
-}
-
-// ConfigError returns true if the error is due to user error.
-func ConfigError(err error) bool {
-	_, ok := err.(ConfigErr)
-	return ok
-}
diff --git a/pkg/kf/internal/tools/clientgen/client.go b/pkg/kf/internal/tools/clientgen/client.go
index adcc632..84dd5eb 100644
--- a/pkg/kf/internal/tools/clientgen/client.go
+++ b/pkg/kf/internal/tools/clientgen/client.go
@@ -18,9 +18,36 @@ package clientgen
 
 import (
 	"bytes"
+	"fmt"
 	"text/template"
 )
 
+type Condition struct {
+	Name string `json:"name"`
+	Ref  string `json:"ref"`
+	Val  string `json:"value"`
+}
+
+func (c *Condition) Definition() string {
+	if c.Ref != "" {
+		return c.Ref
+	}
+
+	return fmt.Sprintf("%q", c.Val)
+}
+
+func (c *Condition) ConditionName() string {
+	return fmt.Sprintf("Condition%s", c.Name)
+}
+
+func (c *Condition) PredicateName() string {
+	return fmt.Sprintf("%sTrue", c.ConditionName())
+}
+
+func (c *Condition) WaitForName() string {
+	return fmt.Sprintf("WaitFor%s", c.PredicateName())
+}
+
 type ClientParams struct {
 	// Package is the package the client will be generated in.
 	Package string `json:"package"`
@@ -39,6 +66,15 @@ type ClientParams struct {
 		// Plural contains the pluralizataion of kind. If blank, default of Kind+"s"
 		// is assumed.
 		Plural string `json:"plural"`
+		// ObservedGenerationField contains the name of the field used for
+		// ObservedGenerations (if the resource suuports it).
+		ObservedGenerationFieldPath string `json:"observedGenerationFieldPath"`
+		// ConditionField contains the name of the field used for
+		// conditions.
+		ConditionsFieldPath string `json:"conditionsFieldPath"`
+		// Conditions holds the names of interesting conditions that will be turned
+		// into predicates.
+		Conditions []Condition `json:"conditions"`
 	} `json:"kubernetes"`
 
 	// CF contains information about this resource from a CF side.
@@ -54,6 +90,17 @@ type ClientParams struct {
 	ClientType string `json:"clientType"`
 }
 
+// SupportsObservedGeneration returns true if the type supports
+// ObservedGeneration fields.
+func (f *ClientParams) SupportsObservedGeneration() bool {
+	return f.Kubernetes.ObservedGenerationFieldPath != ""
+}
+
+// SupportsConditions returns true if the type supports condition fields.
+func (f *ClientParams) SupportsConditions() bool {
+	return f.Kubernetes.ConditionsFieldPath != ""
+}
+
 func (f *ClientParams) Render() ([]byte, error) {
 	buf := &bytes.Buffer{}
 
diff --git a/pkg/kf/internal/tools/clientgen/tmplclient.go b/pkg/kf/internal/tools/clientgen/tmplclient.go
index f7e1b59..f3cb44e 100644
--- a/pkg/kf/internal/tools/clientgen/tmplclient.go
+++ b/pkg/kf/internal/tools/clientgen/tmplclient.go
@@ -29,6 +29,7 @@ var clientTemplate = template.Must(template.New("").Funcs(generator.TemplateFunc
 {{ $nssig := "" }}
 {{ $ns := "" }}
 {{ $nsparam := "" }}
+{{ $type := .Type }}
 
 {{ if .Kubernetes.Namespaced }}
   {{ $nssig = "namespace string," }}
@@ -48,6 +49,12 @@ type Client interface {
 	WaitFor(ctx context.Context, {{ $nssig }} name string, interval time.Duration, condition Predicate) (*{{.Type}}, error)
 	WaitForE(ctx context.Context, {{ $nssig }} name string, interval time.Duration, condition ConditionFuncE) (*{{.Type}}, error)
 
+	// Utility functions
+	WaitForDeletion(ctx context.Context, {{ $nssig }} name string, interval time.Duration) (*{{.Type}}, error)
+	{{ if .SupportsConditions }}{{ range .Kubernetes.Conditions }}{{.WaitForName}}(ctx context.Context, {{ $nssig }} name string, interval time.Duration) (*{{$type}}, error)
+	{{ end }}
+	{{ end }}
+
 	// ClientExtension can be used by the developer to extend the client.
 	ClientExtension
 }
@@ -246,4 +253,56 @@ func wrapPredicate(condition Predicate) ConditionFuncE {
 		return condition(obj), nil
 	}
 }
+
+// WaitForDeletion is a utility function that combines WaitForE with ConditionDeleted.
+func (core *coreClient) WaitForDeletion(ctx context.Context, {{ $nssig }} name string, interval time.Duration) (instance *{{.Type}}, err error) {
+	return core.WaitForE(ctx, {{ $nsparam }} name, interval, ConditionDeleted)
+}
+
+{{ if .SupportsConditions }}
+func checkConditionTrue(obj *{{.Type}}, err error, condition apis.ConditionType) (bool, error) {
+	if err != nil {
+		return true, err
+	}
+
+	{{ if .SupportsObservedGeneration }}// don't propagate old statuses
+	if !ObservedGenerationMatchesGeneration(obj){
+		return false, nil
+	}
+	{{ end }}
+	for _, cond := range ExtractConditions(obj) {
+		if cond.Type == condition {
+			switch {
+			case cond.IsTrue():
+				return true, nil
+
+			case cond.IsUnknown():
+				return false, nil
+
+			default:
+				// return true and a failure assuming IsFalse and other statuses can't be
+				// recovered from because they violate the K8s spec
+				return true, fmt.Errorf("checking %s failed, status: %s message: %s reason: %s", cond.Type, cond.Status, cond.Message, cond.Reason)
+			}
+		}
+	}
+
+	return false, nil
+}
+
+{{ range .Kubernetes.Conditions }}
+// {{.PredicateName}} is a ConditionFuncE that waits for Condition{{.}} to
+// become true and fails with an error if the condition becomes false.
+func {{.PredicateName}}(obj *{{$type}}, err error) (bool, error) {
+	return checkConditionTrue(obj, err, {{.ConditionName}})
+}
+
+// {{.WaitForName}} is a utility function that combines WaitForE with {{.PredicateName}}.
+func (core *coreClient) {{.WaitForName}}(ctx context.Context, {{ $nssig }} name string, interval time.Duration) (instance *{{$type}}, err error) {
+	return core.WaitForE(ctx, {{ $nsparam }} name, interval, {{.PredicateName}})
+}
+{{ end }}
+
+{{ end }}
+
 `))
diff --git a/pkg/kf/internal/tools/clientgen/tmplfunctional.go b/pkg/kf/internal/tools/clientgen/tmplfunctional.go
index 361c76d..5d429c3 100644
--- a/pkg/kf/internal/tools/clientgen/tmplfunctional.go
+++ b/pkg/kf/internal/tools/clientgen/tmplfunctional.go
@@ -34,6 +34,13 @@ const (
 	APIVersion = "{{.Kubernetes.Version}}"
 )
 
+{{ if .SupportsConditions }}
+var (
+	{{ range .Kubernetes.Conditions }}
+	{{.ConditionName}} = apis.ConditionType({{.Definition}}){{ end }}
+)
+{{ end }}
+
 // Predicate is a boolean function for a {{.Type}}.
 type Predicate func(*{{.Type}}) bool
 
@@ -90,4 +97,31 @@ func (list List) Filter(filter Predicate) (out List) {
 	return
 }
 
+
+{{ if .SupportsObservedGeneration }}
+// ObservedGenerationMatchesGeneration is a predicate that returns true if the
+// object's ObservedGeneration matches the genration of the object.
+func ObservedGenerationMatchesGeneration(obj *{{.Type}}) bool {
+	return obj.Generation == obj.{{.Kubernetes.ObservedGenerationFieldPath}}
+}
+{{ end }}
+
+{{ if .SupportsConditions }}
+// ExtractConditions converts the native condition types into an apis.Condition
+// array with the Type, Status, Reason, and Message fields intact.
+func ExtractConditions(obj *{{.Type}}) (extracted []apis.Condition) {
+	for _, cond := range obj.{{.Kubernetes.ConditionsFieldPath}} {
+		// Only copy the following four fields to be compatible with
+		// recommended Kuberntes fields.
+		extracted = append(extracted, apis.Condition{
+			Type:    apis.ConditionType(cond.Type),
+			Status:  cond.Status,
+			Reason:  cond.Reason,
+			Message: cond.Message,
+		})
+	}
+
+	return
+}
+{{ end }}
 `))
diff --git a/pkg/kf/internal/tools/clientgen/tmplheader.go b/pkg/kf/internal/tools/clientgen/tmplheader.go
index a61e280..d3bb7ff 100644
--- a/pkg/kf/internal/tools/clientgen/tmplheader.go
+++ b/pkg/kf/internal/tools/clientgen/tmplheader.go
@@ -37,6 +37,7 @@ import (
 	"time"
 
 	"knative.dev/pkg/kmp"
+	{{ if .SupportsConditions }}"knative.dev/pkg/apis"{{ end }}
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
diff --git a/pkg/kf/commands/utils/utils.go b/pkg/kf/internal/utils/cli/utils.go
similarity index 92%
rename from pkg/kf/commands/utils/utils.go
rename to pkg/kf/internal/utils/cli/utils.go
index 4a8b8a9..7c60bc3 100644
--- a/pkg/kf/commands/utils/utils.go
+++ b/pkg/kf/internal/utils/cli/utils.go
@@ -41,6 +41,24 @@ const (
 	AsyncLogSuffix = "\n(This is an asynchronous operation. See https://github.com/google/kf/issues/599 for updates on work to support synchronous commands.)\n"
 )
 
+// ConfigErr is used to indicate that the returned error is due to a user's
+// invalid configuration.
+type ConfigErr struct {
+	// Reason holds the error message.
+	Reason string
+}
+
+// Error implements error.
+func (e ConfigErr) Error() string {
+	return e.Reason
+}
+
+// ConfigError returns true if the error is due to user error.
+func ConfigError(err error) bool {
+	_, ok := err.(ConfigErr)
+	return ok
+}
+
 // KfParams stores everything needed to interact with the user and Knative.
 type KfParams struct {
 	Output    io.Writer
diff --git a/pkg/kf/manifest/manifest.go b/pkg/kf/manifest/manifest.go
index 7c599d7..177513d 100644
--- a/pkg/kf/manifest/manifest.go
+++ b/pkg/kf/manifest/manifest.go
@@ -15,6 +15,7 @@
 package manifest
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"io"
@@ -43,6 +44,11 @@ type Application struct {
 	CPU             string            `json:"cpu,omitempty"`
 	Instances       *int              `json:"instances,omitempty"`
 
+	// Container command configuration
+	Entrypoint string   `json:"entrypoint,omitempty"`
+	Args       []string `json:"args,omitempty"`
+	Command    string   `json:"command,omitempty"`
+
 	// TODO(#95): These aren't CF proper. How do we expose these in the
 	// manifest?
 	MinScale *int `json:"min-scale,omitempty"`
@@ -198,6 +204,10 @@ for more info.
 		app.Env = envutil.EnvVarsToMap(envutil.DeduplicateEnvVars(combined))
 	}
 
+	if err := app.Validate(context.Background()); err.Error() != "" {
+		return err
+	}
+
 	return nil
 }
 
@@ -211,3 +221,25 @@ func (app *Application) Buildpack() string {
 
 	return app.LegacyBuildpack
 }
+
+// CommandEntrypoint gets an override for the entrypoint of the container.
+func (app *Application) CommandEntrypoint() []string {
+	if app.Entrypoint != "" {
+		return []string{app.Entrypoint}
+	}
+
+	return nil
+}
+
+// CommandArgs returns the container args if they're defined or nil.
+func (app *Application) CommandArgs() []string {
+	if len(app.Args) > 0 {
+		return app.Args
+	}
+
+	if app.Command != "" {
+		return []string{app.Command}
+	}
+
+	return nil
+}
diff --git a/pkg/kf/manifest/manifest_validation.go b/pkg/kf/manifest/manifest_validation.go
new file mode 100644
index 0000000..341c109
--- /dev/null
+++ b/pkg/kf/manifest/manifest_validation.go
@@ -0,0 +1,44 @@
+// Copyright 2019 Google LLC
+//
+// 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
+//
+//     https://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 manifest
+
+import (
+	"context"
+
+	"knative.dev/pkg/apis"
+)
+
+// Validate checks for errors in the Application's fields.
+func (app *Application) Validate(ctx context.Context) (errs *apis.FieldError) {
+	// validate container execution
+	if app.Command != "" {
+		if len(app.Args) > 0 {
+			errs = errs.Also(apis.ErrMultipleOneOf("command", "args"))
+		}
+
+		if app.Entrypoint != "" {
+			errs = errs.Also(apis.ErrMultipleOneOf("entrypoint", "command"))
+		}
+	}
+
+	// validate buildpacks
+	if len(app.Buildpacks) > 0 {
+		if app.LegacyBuildpack != "" {
+			errs = errs.Also(apis.ErrMultipleOneOf("buildpack", "buildpacks"))
+		}
+	}
+
+	return
+}
diff --git a/pkg/kf/routeclaims/fake/fake_client.go b/pkg/kf/routeclaims/fake/fake_client.go
index 762fce8..cbe5485 100644
--- a/pkg/kf/routeclaims/fake/fake_client.go
+++ b/pkg/kf/routeclaims/fake/fake_client.go
@@ -195,6 +195,21 @@ func (mr *FakeClientMockRecorder) WaitFor(arg0, arg1, arg2, arg3, arg4 interface
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitFor", reflect.TypeOf((*FakeClient)(nil).WaitFor), arg0, arg1, arg2, arg3, arg4)
 }
 
+// WaitForDeletion mocks base method
+func (m *FakeClient) WaitForDeletion(arg0 context.Context, arg1, arg2 string, arg3 time.Duration) (*v1alpha1.RouteClaim, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WaitForDeletion", arg0, arg1, arg2, arg3)
+	ret0, _ := ret[0].(*v1alpha1.RouteClaim)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// WaitForDeletion indicates an expected call of WaitForDeletion
+func (mr *FakeClientMockRecorder) WaitForDeletion(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForDeletion", reflect.TypeOf((*FakeClient)(nil).WaitForDeletion), arg0, arg1, arg2, arg3)
+}
+
 // WaitForE mocks base method
 func (m *FakeClient) WaitForE(arg0 context.Context, arg1, arg2 string, arg3 time.Duration, arg4 routeclaims.ConditionFuncE) (*v1alpha1.RouteClaim, error) {
 	m.ctrl.T.Helper()
diff --git a/pkg/kf/routeclaims/zz_generated.client.go b/pkg/kf/routeclaims/zz_generated.client.go
index b03bfe7..37690af 100644
--- a/pkg/kf/routeclaims/zz_generated.client.go
+++ b/pkg/kf/routeclaims/zz_generated.client.go
@@ -25,9 +25,10 @@ import (
 	"strings"
 	"time"
 
+	"knative.dev/pkg/kmp"
+
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"knative.dev/pkg/kmp"
 )
 
 // User defined imports
@@ -120,6 +121,9 @@ type Client interface {
 	WaitFor(ctx context.Context, namespace string, name string, interval time.Duration, condition Predicate) (*v1alpha1.RouteClaim, error)
 	WaitForE(ctx context.Context, namespace string, name string, interval time.Duration, condition ConditionFuncE) (*v1alpha1.RouteClaim, error)
 
+	// Utility functions
+	WaitForDeletion(ctx context.Context, namespace string, name string, interval time.Duration) (*v1alpha1.RouteClaim, error)
+
 	// ClientExtension can be used by the developer to extend the client.
 	ClientExtension
 }
@@ -318,3 +322,8 @@ func wrapPredicate(condition Predicate) ConditionFuncE {
 		return condition(obj), nil
 	}
 }
+
+// WaitForDeletion is a utility function that combines WaitForE with ConditionDeleted.
+func (core *coreClient) WaitForDeletion(ctx context.Context, namespace string, name string, interval time.Duration) (instance *v1alpha1.RouteClaim, err error) {
+	return core.WaitForE(ctx, namespace, name, interval, ConditionDeleted)
+}
diff --git a/pkg/kf/routes/fake/fake_client.go b/pkg/kf/routes/fake/fake_client.go
index 97c55da..fede700 100644
--- a/pkg/kf/routes/fake/fake_client.go
+++ b/pkg/kf/routes/fake/fake_client.go
@@ -195,6 +195,21 @@ func (mr *FakeClientMockRecorder) WaitFor(arg0, arg1, arg2, arg3, arg4 interface
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitFor", reflect.TypeOf((*FakeClient)(nil).WaitFor), arg0, arg1, arg2, arg3, arg4)
 }
 
+// WaitForDeletion mocks base method
+func (m *FakeClient) WaitForDeletion(arg0 context.Context, arg1, arg2 string, arg3 time.Duration) (*v1alpha1.Route, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WaitForDeletion", arg0, arg1, arg2, arg3)
+	ret0, _ := ret[0].(*v1alpha1.Route)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// WaitForDeletion indicates an expected call of WaitForDeletion
+func (mr *FakeClientMockRecorder) WaitForDeletion(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForDeletion", reflect.TypeOf((*FakeClient)(nil).WaitForDeletion), arg0, arg1, arg2, arg3)
+}
+
 // WaitForE mocks base method
 func (m *FakeClient) WaitForE(arg0 context.Context, arg1, arg2 string, arg3 time.Duration, arg4 routes.ConditionFuncE) (*v1alpha1.Route, error) {
 	m.ctrl.T.Helper()
diff --git a/pkg/kf/routes/zz_generated.client.go b/pkg/kf/routes/zz_generated.client.go
index a19c97b..5fcf291 100644
--- a/pkg/kf/routes/zz_generated.client.go
+++ b/pkg/kf/routes/zz_generated.client.go
@@ -25,9 +25,10 @@ import (
 	"strings"
 	"time"
 
+	"knative.dev/pkg/kmp"
+
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"knative.dev/pkg/kmp"
 )
 
 // User defined imports
@@ -120,6 +121,9 @@ type Client interface {
 	WaitFor(ctx context.Context, namespace string, name string, interval time.Duration, condition Predicate) (*v1alpha1.Route, error)
 	WaitForE(ctx context.Context, namespace string, name string, interval time.Duration, condition ConditionFuncE) (*v1alpha1.Route, error)
 
+	// Utility functions
+	WaitForDeletion(ctx context.Context, namespace string, name string, interval time.Duration) (*v1alpha1.Route, error)
+
 	// ClientExtension can be used by the developer to extend the client.
 	ClientExtension
 }
@@ -318,3 +322,8 @@ func wrapPredicate(condition Predicate) ConditionFuncE {
 		return condition(obj), nil
 	}
 }
+
+// WaitForDeletion is a utility function that combines WaitForE with ConditionDeleted.
+func (core *coreClient) WaitForDeletion(ctx context.Context, namespace string, name string, interval time.Duration) (instance *v1alpha1.Route, err error) {
+	return core.WaitForE(ctx, namespace, name, interval, ConditionDeleted)
+}
diff --git a/pkg/kf/service-bindings/client.go b/pkg/kf/service-bindings/client.go
index 9440488..a27365d 100644
--- a/pkg/kf/service-bindings/client.go
+++ b/pkg/kf/service-bindings/client.go
@@ -140,7 +140,7 @@ func (c *Client) List(opts ...ListOption) ([]servicecatalogv1beta1.ServiceBindin
 
 // WaitForBindings waits for bindings to all be ready or fail on the given app.
 func (c *Client) WaitForBindings(ctx context.Context, namespace, appName string) error {
-	_, err := c.appsClient.WaitForE(ctx, namespace, appName, 2*time.Second, apps.ConditionServiceBindingsReady)
+	_, err := c.appsClient.WaitForConditionServiceBindingsReadyTrue(ctx, namespace, appName, 2*time.Second)
 	return err
 }
 
diff --git a/pkg/kf/sources/fake/fake_client.go b/pkg/kf/sources/fake/fake_client.go
index 75f6ad6..3715976 100644
--- a/pkg/kf/sources/fake/fake_client.go
+++ b/pkg/kf/sources/fake/fake_client.go
@@ -225,6 +225,21 @@ func (mr *FakeClientMockRecorder) WaitFor(arg0, arg1, arg2, arg3, arg4 interface
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitFor", reflect.TypeOf((*FakeClient)(nil).WaitFor), arg0, arg1, arg2, arg3, arg4)
 }
 
+// WaitForDeletion mocks base method
+func (m *FakeClient) WaitForDeletion(arg0 context.Context, arg1, arg2 string, arg3 time.Duration) (*v1alpha1.Source, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WaitForDeletion", arg0, arg1, arg2, arg3)
+	ret0, _ := ret[0].(*v1alpha1.Source)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// WaitForDeletion indicates an expected call of WaitForDeletion
+func (mr *FakeClientMockRecorder) WaitForDeletion(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForDeletion", reflect.TypeOf((*FakeClient)(nil).WaitForDeletion), arg0, arg1, arg2, arg3)
+}
+
 // WaitForE mocks base method
 func (m *FakeClient) WaitForE(arg0 context.Context, arg1, arg2 string, arg3 time.Duration, arg4 sources.ConditionFuncE) (*v1alpha1.Source, error) {
 	m.ctrl.T.Helper()
diff --git a/pkg/kf/sources/zz_generated.client.go b/pkg/kf/sources/zz_generated.client.go
index b0e02f2..ad4939c 100644
--- a/pkg/kf/sources/zz_generated.client.go
+++ b/pkg/kf/sources/zz_generated.client.go
@@ -25,9 +25,10 @@ import (
 	"strings"
 	"time"
 
+	"knative.dev/pkg/kmp"
+
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"knative.dev/pkg/kmp"
 )
 
 // User defined imports
@@ -120,6 +121,9 @@ type Client interface {
 	WaitFor(ctx context.Context, namespace string, name string, interval time.Duration, condition Predicate) (*v1alpha1.Source, error)
 	WaitForE(ctx context.Context, namespace string, name string, interval time.Duration, condition ConditionFuncE) (*v1alpha1.Source, error)
 
+	// Utility functions
+	WaitForDeletion(ctx context.Context, namespace string, name string, interval time.Duration) (*v1alpha1.Source, error)
+
 	// ClientExtension can be used by the developer to extend the client.
 	ClientExtension
 }
@@ -318,3 +322,8 @@ func wrapPredicate(condition Predicate) ConditionFuncE {
 		return condition(obj), nil
 	}
 }
+
+// WaitForDeletion is a utility function that combines WaitForE with ConditionDeleted.
+func (core *coreClient) WaitForDeletion(ctx context.Context, namespace string, name string, interval time.Duration) (instance *v1alpha1.Source, err error) {
+	return core.WaitForE(ctx, namespace, name, interval, ConditionDeleted)
+}
diff --git a/pkg/kf/spaces/fake/fake_client.go b/pkg/kf/spaces/fake/fake_client.go
index fe33c7d..c4af8d6 100644
--- a/pkg/kf/spaces/fake/fake_client.go
+++ b/pkg/kf/spaces/fake/fake_client.go
@@ -194,6 +194,21 @@ func (mr *FakeClientMockRecorder) WaitFor(arg0, arg1, arg2, arg3 interface{}) *g
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitFor", reflect.TypeOf((*FakeClient)(nil).WaitFor), arg0, arg1, arg2, arg3)
 }
 
+// WaitForDeletion mocks base method
+func (m *FakeClient) WaitForDeletion(arg0 context.Context, arg1 string, arg2 time.Duration) (*v1alpha1.Space, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "WaitForDeletion", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*v1alpha1.Space)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// WaitForDeletion indicates an expected call of WaitForDeletion
+func (mr *FakeClientMockRecorder) WaitForDeletion(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForDeletion", reflect.TypeOf((*FakeClient)(nil).WaitForDeletion), arg0, arg1, arg2)
+}
+
 // WaitForE mocks base method
 func (m *FakeClient) WaitForE(arg0 context.Context, arg1 string, arg2 time.Duration, arg3 spaces.ConditionFuncE) (*v1alpha1.Space, error) {
 	m.ctrl.T.Helper()
diff --git a/pkg/kf/spaces/zz_generated.client.go b/pkg/kf/spaces/zz_generated.client.go
index e5532b4..3cd73a0 100644
--- a/pkg/kf/spaces/zz_generated.client.go
+++ b/pkg/kf/spaces/zz_generated.client.go
@@ -25,9 +25,10 @@ import (
 	"strings"
 	"time"
 
+	"knative.dev/pkg/kmp"
+
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"knative.dev/pkg/kmp"
 )
 
 // User defined imports
@@ -120,6 +121,9 @@ type Client interface {
 	WaitFor(ctx context.Context, name string, interval time.Duration, condition Predicate) (*v1alpha1.Space, error)
 	WaitForE(ctx context.Context, name string, interval time.Duration, condition ConditionFuncE) (*v1alpha1.Space, error)
 
+	// Utility functions
+	WaitForDeletion(ctx context.Context, name string, interval time.Duration) (*v1alpha1.Space, error)
+
 	// ClientExtension can be used by the developer to extend the client.
 	ClientExtension
 }
@@ -318,3 +322,8 @@ func wrapPredicate(condition Predicate) ConditionFuncE {
 		return condition(obj), nil
 	}
 }
+
+// WaitForDeletion is a utility function that combines WaitForE with ConditionDeleted.
+func (core *coreClient) WaitForDeletion(ctx context.Context, name string, interval time.Duration) (instance *v1alpha1.Space, err error) {
+	return core.WaitForE(ctx, name, interval, ConditionDeleted)
+}
diff --git a/pkg/reconciler/route/resources/virtual_service.go b/pkg/reconciler/route/resources/virtual_service.go
index ab321ba..ac19ca0 100644
--- a/pkg/reconciler/route/resources/virtual_service.go
+++ b/pkg/reconciler/route/resources/virtual_service.go
@@ -72,7 +72,7 @@ func MakeVirtualService(claims []*v1alpha1.RouteClaim, routes []*v1alpha1.Route)
 	for _, route := range claims {
 		urlPath := route.Spec.RouteSpecFields.Path
 
-		httpRoute, err := buildHTTPRoute(namespace, urlPath, nil)
+		httpRoute, err := buildHTTPRoute(hostDomain, namespace, urlPath, nil)
 		if err != nil {
 			return nil, err
 		}
@@ -91,7 +91,8 @@ func MakeVirtualService(claims []*v1alpha1.RouteClaim, routes []*v1alpha1.Route)
 		if route.Spec.AppName != "" {
 			appNames = append(appNames, route.Spec.AppName)
 		}
-		httpRoute, err := buildHTTPRoute(namespace, urlPath, appNames)
+
+		httpRoute, err := buildHTTPRoute(hostDomain, namespace, urlPath, appNames)
 		if err != nil {
 			return nil, err
 		}
@@ -128,7 +129,7 @@ func MakeVirtualService(claims []*v1alpha1.RouteClaim, routes []*v1alpha1.Route)
 	}, nil
 }
 
-func buildHTTPRoute(namespace, urlPath string, appNames []string) ([]networking.HTTPRoute, error) {
+func buildHTTPRoute(hostDomain, namespace, urlPath string, appNames []string) ([]networking.HTTPRoute, error) {
 	var pathMatchers []networking.HTTPMatchRequest
 	urlPath = path.Join("/", urlPath, "/")
 	regexpPath, err := v1alpha1.BuildPathRegexp(urlPath)
@@ -153,6 +154,17 @@ func buildHTTPRoute(namespace, urlPath string, appNames []string) ([]networking.
 			Rewrite: &networking.HTTPRewrite{
 				Authority: network.GetServiceHostname(appName, namespace),
 			},
+			Headers: &networking.Headers{
+				Request: &networking.HeaderOperations{
+					Add: map[string]string{
+						// Set forwarding headers so the app gets the real hostname it's serving
+						// at rather than the internal one:
+						// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded1
+						"X-Forwarded-Host": hostDomain,
+						"Forwarded":        fmt.Sprintf("host=%s", hostDomain),
+					},
+				},
+			},
 		})
 	}
 


Test patch:
diff --git a/pkg/kf/apps/client_test.go b/pkg/kf/apps/client_test.go
deleted file mode 100644
index be0e859..0000000
--- a/pkg/kf/apps/client_test.go
+++ /dev/null
@@ -1,107 +0,0 @@
-// Copyright 2019 Google LLC
-//
-// 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
-//
-//     https://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 apps
-
-import (
-	"errors"
-	"testing"
-
-	v1alpha1 "github.com/google/kf/pkg/apis/kf/v1alpha1"
-	"github.com/google/kf/pkg/kf/testutil"
-	duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1"
-)
-
-func TestConditionServiceBindingsReady(t *testing.T) {
-	cases := map[string]struct {
-		app    v1alpha1.App
-		apiErr error
-
-		expectDone bool
-		expectErr  error
-	}{
-		"api error": {
-			apiErr:     errors.New("api-error"),
-			expectErr:  errors.New("api-error"),
-			expectDone: true,
-		},
-		"mismatch generation": {
-			app: v1alpha1.App{
-				Status: v1alpha1.AppStatus{
-					Status: duckv1beta1.Status{
-						ObservedGeneration: 3,
-					},
-				},
-			},
-			expectDone: false,
-			expectErr:  nil,
-		},
-		"missing status": {
-			app: v1alpha1.App{
-				Status: v1alpha1.AppStatus{
-					Status: duckv1beta1.Status{
-						Conditions: duckv1beta1.Conditions{},
-					},
-				},
-			},
-			expectDone: false,
-		},
-		"unknown status causes retry": {
-			app: v1alpha1.App{
-				Status: v1alpha1.AppStatus{
-					Status: duckv1beta1.Status{
-						Conditions: duckv1beta1.Conditions{
-							{Type: v1alpha1.AppConditionServiceBindingsReady, Status: "Unknown"},
-						},
-					},
-				},
-			},
-			expectDone: false,
-		},
-		"good status completes without error": {
-			app: v1alpha1.App{
-				Status: v1alpha1.AppStatus{
-					Status: duckv1beta1.Status{
-						Conditions: duckv1beta1.Conditions{
-							{Type: v1alpha1.AppConditionServiceBindingsReady, Status: "True"},
-						},
-					},
-				},
-			},
-			expectDone: true,
-		},
-		"bad status completes with error": {
-			app: v1alpha1.App{
-				Status: v1alpha1.AppStatus{
-					Status: duckv1beta1.Status{
-						Conditions: duckv1beta1.Conditions{
-							{Type: v1alpha1.AppConditionServiceBindingsReady, Status: "False", Message: "ShortMessage", Reason: "longer description"},
-						},
-					},
-				},
-			},
-			expectDone: true,
-			expectErr:  errors.New("checking ServiceBindingsReady failed, status: False message: ShortMessage reason: longer description"),
-		},
-	}
-
-	for tn, tc := range cases {
-		t.Run(tn, func(t *testing.T) {
-			actualDone, actualErr := ConditionServiceBindingsReady(&tc.app, tc.apiErr)
-
-			testutil.AssertErrorsEqual(t, tc.expectErr, actualErr)
-			testutil.AssertEqual(t, "done", tc.expectDone, actualDone)
-		})
-	}
-}
diff --git a/pkg/kf/apps/kfapp_test.go b/pkg/kf/apps/kfapp_test.go
index dfee92c..a0fa342 100644
--- a/pkg/kf/apps/kfapp_test.go
+++ b/pkg/kf/apps/kfapp_test.go
@@ -246,3 +246,25 @@ func ExampleKfApp_GetClusterURL() {
 
 	// Output: http://app-a.some-namespace.svc.cluster.local
 }
+
+func ExampleKfApp_GetArgs() {
+	myApp := NewKfApp()
+	fmt.Printf("Default: %v\n", myApp.GetArgs())
+
+	myApp.SetArgs([]string{"arg1", "arg2"})
+	fmt.Printf("After set: %v\n", myApp.GetArgs())
+
+	// Output: Default: []
+	// After set: [arg1 arg2]
+}
+
+func ExampleKfApp_GetCommand() {
+	myApp := NewKfApp()
+	fmt.Printf("Default: %v\n", myApp.GetCommand())
+
+	myApp.SetCommand([]string{"/bin/bash", "-x"})
+	fmt.Printf("After set: %v\n", myApp.GetCommand())
+
+	// Output: Default: []
+	// After set: [/bin/bash -x]
+}
diff --git a/pkg/kf/commands/apps/apps_test.go b/pkg/kf/commands/apps/apps_test.go
index a7d7b08..16792ee 100644
--- a/pkg/kf/commands/apps/apps_test.go
+++ b/pkg/kf/commands/apps/apps_test.go
@@ -24,7 +24,7 @@ import (
 	v1alpha1 "github.com/google/kf/pkg/apis/kf/v1alpha1"
 	"github.com/google/kf/pkg/kf/apps/fake"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/testutil"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/pkg/kf/commands/apps/delete_test.go b/pkg/kf/commands/apps/delete_test.go
index 38abad9..953be72 100644
--- a/pkg/kf/commands/apps/delete_test.go
+++ b/pkg/kf/commands/apps/delete_test.go
@@ -23,7 +23,7 @@ import (
 	"github.com/golang/mock/gomock"
 	"github.com/google/kf/pkg/kf/apps/fake"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 )
 
 func TestDeleteCommand(t *testing.T) {
diff --git a/pkg/kf/commands/apps/logs_test.go b/pkg/kf/commands/apps/logs_test.go
index b1df86c..5124619 100644
--- a/pkg/kf/commands/apps/logs_test.go
+++ b/pkg/kf/commands/apps/logs_test.go
@@ -22,8 +22,7 @@ import (
 
 	"github.com/golang/mock/gomock"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
-	"github.com/google/kf/pkg/kf/internal/kf"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/logs"
 	"github.com/google/kf/pkg/kf/logs/fake"
 	"github.com/google/kf/pkg/kf/testutil"
@@ -69,7 +68,7 @@ func TestLogsCommand(t *testing.T) {
 			Setup: func(t *testing.T, fake *fake.FakeTailer) {
 				fake.EXPECT().
 					Tail(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
-					Return(kf.ConfigErr{Reason: "some-error"})
+					Return(utils.ConfigErr{Reason: "some-error"})
 			},
 			Assert: func(t *testing.T, cmd *cobra.Command, err error) {
 				testutil.AssertEqual(t, "SilenceUsage", false, cmd.SilenceUsage)
diff --git a/pkg/kf/commands/apps/push_test.go b/pkg/kf/commands/apps/push_test.go
index 7534e63..56efa61 100644
--- a/pkg/kf/commands/apps/push_test.go
+++ b/pkg/kf/commands/apps/push_test.go
@@ -28,7 +28,7 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	appsfake "github.com/google/kf/pkg/kf/apps/fake"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	svbFake "github.com/google/kf/pkg/kf/service-bindings/fake"
 	"github.com/google/kf/pkg/kf/testutil"
 	"github.com/poy/service-catalog/pkg/apis/servicecatalog/v1beta1"
@@ -93,6 +93,9 @@ func TestPushCommand(t *testing.T) {
 				"-u", "http",
 				"-t", "28",
 				"-s", "cflinuxfs3",
+				"--entrypoint", "start-web.sh",
+				"--args", "a",
+				"--args", "b",
 			},
 			wantImagePrefix: "some-reg.io/src-some-namespace-example-app",
 			srcImageBuilder: func(dir, srcImage string, rebase bool) error {
@@ -108,6 +111,8 @@ func TestPushCommand(t *testing.T) {
 				apps.WithPushEnvironmentVariables(map[string]string{"env1": "val1", "env2": "val2"}),
 				apps.WithPushNoStart(true),
 				apps.WithPushExactScale(intPtr(1)),
+				apps.WithPushArgs([]string{"a", "b"}),
+				apps.WithPushCommand([]string{"start-web.sh"}),
 				apps.WithPushHealthCheck(&corev1.Probe{
 					TimeoutSeconds: 28,
 					Handler: corev1.Handler{
@@ -547,6 +552,8 @@ func TestPushCommand(t *testing.T) {
 					testutil.AssertEqual(t, "health check", expectOpts.HealthCheck(), actualOpts.HealthCheck())
 					testutil.AssertEqual(t, "default route", expectOpts.DefaultRouteDomain(), actualOpts.DefaultRouteDomain())
 					testutil.AssertEqual(t, "random route", expectOpts.RandomRouteDomain(), actualOpts.RandomRouteDomain())
+					testutil.AssertEqual(t, "command", expectOpts.Command(), actualOpts.Command())
+					testutil.AssertEqual(t, "args", expectOpts.Args(), actualOpts.Args())
 
 					if !strings.HasPrefix(actualOpts.SourceImage(), tc.wantImagePrefix) {
 						t.Errorf("Wanted srcImage to start with %s got: %s", tc.wantImagePrefix, actualOpts.SourceImage())
diff --git a/pkg/kf/commands/apps/set_env_test.go b/pkg/kf/commands/apps/set_env_test.go
index 1d750b9..98f3e36 100644
--- a/pkg/kf/commands/apps/set_env_test.go
+++ b/pkg/kf/commands/apps/set_env_test.go
@@ -25,7 +25,7 @@ import (
 	"github.com/google/kf/pkg/kf/apps"
 	"github.com/google/kf/pkg/kf/apps/fake"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/testutil"
 )
 
diff --git a/pkg/kf/commands/apps/unset_env_test.go b/pkg/kf/commands/apps/unset_env_test.go
index 44734e4..0f89f0f 100644
--- a/pkg/kf/commands/apps/unset_env_test.go
+++ b/pkg/kf/commands/apps/unset_env_test.go
@@ -19,7 +19,7 @@ import (
 	"errors"
 	"testing"
 
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 
 	"github.com/golang/mock/gomock"
 	"github.com/google/kf/pkg/internal/envutil"
diff --git a/pkg/kf/commands/buildpacks/buildpacks_test.go b/pkg/kf/commands/buildpacks/buildpacks_test.go
index 9555087..c500e54 100644
--- a/pkg/kf/commands/buildpacks/buildpacks_test.go
+++ b/pkg/kf/commands/buildpacks/buildpacks_test.go
@@ -24,7 +24,7 @@ import (
 	"github.com/google/kf/pkg/kf/buildpacks/fake"
 	cbuildpacks "github.com/google/kf/pkg/kf/commands/buildpacks"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/testutil"
 )
 
diff --git a/pkg/kf/commands/buildpacks/stacks_test.go b/pkg/kf/commands/buildpacks/stacks_test.go
index b4ac11a..2d0f2ff 100644
--- a/pkg/kf/commands/buildpacks/stacks_test.go
+++ b/pkg/kf/commands/buildpacks/stacks_test.go
@@ -23,7 +23,7 @@ import (
 	"github.com/google/kf/pkg/kf/buildpacks/fake"
 	cbuildpacks "github.com/google/kf/pkg/kf/commands/buildpacks"
 	"github.com/google/kf/pkg/kf/commands/config"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/testutil"
 )
 
diff --git a/pkg/kf/commands/routes/create_route_test.go b/pkg/kf/commands/routes/create_route_test.go
index fd56c66..0545c59 100644
--- a/pkg/kf/commands/routes/create_route_test.go
+++ b/pkg/kf/commands/routes/create_route_test.go
@@ -23,7 +23,7 @@ import (
 	"github.com/google/kf/pkg/apis/kf/v1alpha1"
 	"github.com/google/kf/pkg/kf/commands/config"
 	"github.com/google/kf/pkg/kf/commands/routes"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	routesfake "github.com/google/kf/pkg/kf/routeclaims/fake"
 	"github.com/google/kf/pkg/kf/testutil"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/pkg/kf/commands/routes/delete_route_test.go b/pkg/kf/commands/routes/delete_route_test.go
index 74e73a2..01bb97b 100644
--- a/pkg/kf/commands/routes/delete_route_test.go
+++ b/pkg/kf/commands/routes/delete_route_test.go
@@ -25,7 +25,7 @@ import (
 	fakeapp "github.com/google/kf/pkg/kf/apps/fake"
 	"github.com/google/kf/pkg/kf/commands/config"
 	"github.com/google/kf/pkg/kf/commands/routes"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	fakerouteclaims "github.com/google/kf/pkg/kf/routeclaims/fake"
 	"github.com/google/kf/pkg/kf/testutil"
 )
diff --git a/pkg/kf/commands/routes/map_route_test.go b/pkg/kf/commands/routes/map_route_test.go
index fa68fa1..eecc7c2 100644
--- a/pkg/kf/commands/routes/map_route_test.go
+++ b/pkg/kf/commands/routes/map_route_test.go
@@ -25,7 +25,7 @@ import (
 	appsfake "github.com/google/kf/pkg/kf/apps/fake"
 	"github.com/google/kf/pkg/kf/commands/config"
 	"github.com/google/kf/pkg/kf/commands/routes"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/testutil"
 )
 
diff --git a/pkg/kf/commands/routes/unmap_route_test.go b/pkg/kf/commands/routes/unmap_route_test.go
index 50f331b..32c92fc 100644
--- a/pkg/kf/commands/routes/unmap_route_test.go
+++ b/pkg/kf/commands/routes/unmap_route_test.go
@@ -25,7 +25,7 @@ import (
 	"github.com/google/kf/pkg/kf/apps/fake"
 	"github.com/google/kf/pkg/kf/commands/config"
 	"github.com/google/kf/pkg/kf/commands/routes"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/testutil"
 )
 
diff --git a/pkg/kf/commands/service-bindings/bind-service_test.go b/pkg/kf/commands/service-bindings/bind-service_test.go
index db1012f..a2087cc 100644
--- a/pkg/kf/commands/service-bindings/bind-service_test.go
+++ b/pkg/kf/commands/service-bindings/bind-service_test.go
@@ -20,7 +20,7 @@ import (
 
 	"github.com/golang/mock/gomock"
 	servicebindingscmd "github.com/google/kf/pkg/kf/commands/service-bindings"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	servicebindings "github.com/google/kf/pkg/kf/service-bindings"
 	"github.com/google/kf/pkg/kf/service-bindings/fake"
 	"github.com/google/kf/pkg/kf/testutil"
diff --git a/pkg/kf/commands/service-bindings/bindings_test.go b/pkg/kf/commands/service-bindings/bindings_test.go
index be8fdf5..ab9e455 100644
--- a/pkg/kf/commands/service-bindings/bindings_test.go
+++ b/pkg/kf/commands/service-bindings/bindings_test.go
@@ -20,7 +20,7 @@ import (
 
 	"github.com/golang/mock/gomock"
 	servicebindingscmd "github.com/google/kf/pkg/kf/commands/service-bindings"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	servicebindings "github.com/google/kf/pkg/kf/service-bindings"
 	"github.com/google/kf/pkg/kf/service-bindings/fake"
 	"github.com/google/kf/pkg/kf/testutil"
diff --git a/pkg/kf/commands/service-bindings/unbind-service_test.go b/pkg/kf/commands/service-bindings/unbind-service_test.go
index 21627ae..70e5212 100644
--- a/pkg/kf/commands/service-bindings/unbind-service_test.go
+++ b/pkg/kf/commands/service-bindings/unbind-service_test.go
@@ -16,7 +16,7 @@ package servicebindings_test
 
 import (
 	"errors"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"testing"
 
 	"github.com/golang/mock/gomock"
diff --git a/pkg/kf/commands/service-bindings/vcap_services_test.go b/pkg/kf/commands/service-bindings/vcap_services_test.go
index fc95d9e..112fab5 100644
--- a/pkg/kf/commands/service-bindings/vcap_services_test.go
+++ b/pkg/kf/commands/service-bindings/vcap_services_test.go
@@ -23,7 +23,7 @@ import (
 
 	"github.com/google/kf/pkg/kf/commands/config"
 	servicebindings "github.com/google/kf/pkg/kf/commands/service-bindings"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/testutil"
 	corev1 "k8s.io/api/core/v1"
 	k8sfake "k8s.io/client-go/kubernetes/fake"
diff --git a/pkg/kf/commands/services/create-service_test.go b/pkg/kf/commands/services/create-service_test.go
index 856c064..5e01b15 100644
--- a/pkg/kf/commands/services/create-service_test.go
+++ b/pkg/kf/commands/services/create-service_test.go
@@ -24,7 +24,7 @@ import (
 	servicecatalogclientfake "github.com/google/kf/pkg/client/servicecatalog/clientset/versioned/fake"
 	"github.com/google/kf/pkg/kf/commands/config"
 	servicescmd "github.com/google/kf/pkg/kf/commands/services"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/testutil"
 	servicecatalogv1beta1 "github.com/poy/service-catalog/pkg/apis/servicecatalog/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/pkg/kf/commands/services/delete-service_test.go b/pkg/kf/commands/services/delete-service_test.go
index d570e31..65f2ed1 100644
--- a/pkg/kf/commands/services/delete-service_test.go
+++ b/pkg/kf/commands/services/delete-service_test.go
@@ -20,7 +20,7 @@ import (
 
 	"github.com/golang/mock/gomock"
 	servicescmd "github.com/google/kf/pkg/kf/commands/services"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/services"
 	"github.com/google/kf/pkg/kf/services/fake"
 	"github.com/google/kf/pkg/kf/testutil"
diff --git a/pkg/kf/commands/services/marketplace_test.go b/pkg/kf/commands/services/marketplace_test.go
index 79a9dc9..f26b91c 100644
--- a/pkg/kf/commands/services/marketplace_test.go
+++ b/pkg/kf/commands/services/marketplace_test.go
@@ -20,7 +20,7 @@ import (
 
 	"github.com/golang/mock/gomock"
 	servicescmd "github.com/google/kf/pkg/kf/commands/services"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/services"
 	"github.com/google/kf/pkg/kf/services/fake"
 	"github.com/google/kf/pkg/kf/testutil"
diff --git a/pkg/kf/commands/services/service_test.go b/pkg/kf/commands/services/service_test.go
index 5c59c85..0019ede 100644
--- a/pkg/kf/commands/services/service_test.go
+++ b/pkg/kf/commands/services/service_test.go
@@ -20,7 +20,7 @@ import (
 
 	"github.com/golang/mock/gomock"
 	servicescmd "github.com/google/kf/pkg/kf/commands/services"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/services"
 	"github.com/google/kf/pkg/kf/services/fake"
 	"github.com/google/kf/pkg/kf/testutil"
diff --git a/pkg/kf/commands/services/services_test.go b/pkg/kf/commands/services/services_test.go
index 59f3760..052e0e6 100644
--- a/pkg/kf/commands/services/services_test.go
+++ b/pkg/kf/commands/services/services_test.go
@@ -23,7 +23,7 @@ import (
 	fakeapps "github.com/google/kf/pkg/kf/apps/fake"
 	"github.com/google/kf/pkg/kf/commands/config"
 	servicescmd "github.com/google/kf/pkg/kf/commands/services"
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/services"
 	"github.com/google/kf/pkg/kf/services/fake"
 	"github.com/google/kf/pkg/kf/testutil"
diff --git a/pkg/kf/internal/tools/clientgen/gentest/client.go b/pkg/kf/internal/tools/clientgen/gentest/client.go
index 140f75f..ba5cb94 100644
--- a/pkg/kf/internal/tools/clientgen/gentest/client.go
+++ b/pkg/kf/internal/tools/clientgen/gentest/client.go
@@ -26,7 +26,7 @@ type ClientExtension interface {
 
 // NewExampleClient creates an example client with a mutator and membership
 // validator that filter based on a label.
-func NewExampleClient(mockK8s v1.SecretsGetter) Client {
+func NewExampleClient(mockK8s v1.PodsGetter) Client {
 	return &coreClient{
 		kclient:      mockK8s,
 		upsertMutate: LabelSetMutator(map[string]string{"is-a": "OperatorConfig"}),
@@ -34,7 +34,7 @@ func NewExampleClient(mockK8s v1.SecretsGetter) Client {
 }
 
 func LabelSetMutator(labels map[string]string) Mutator {
-	return func(obj *corev1.Secret) error {
+	return func(obj *corev1.Pod) error {
 		if obj.Labels == nil {
 			obj.Labels = make(map[string]string)
 		}
diff --git a/pkg/kf/internal/tools/clientgen/gentest/client.yml b/pkg/kf/internal/tools/clientgen/gentest/client.yml
index 8d296a5..2e54e8a 100644
--- a/pkg/kf/internal/tools/clientgen/gentest/client.yml
+++ b/pkg/kf/internal/tools/clientgen/gentest/client.yml
@@ -3,10 +3,18 @@
 package: gentest
 imports: {"k8s.io/api/core/v1":"v1", "k8s.io/client-go/kubernetes/typed/core/v1": "cv1"}
 kubernetes:
-  kind: "Secret"
+  kind: "Pod"
   version: "v1"
   namespaced: true
-type: "v1.Secret"
-clientType: "cv1.SecretsGetter"
+  # pod doesn't have an observedgeneration, but we want to exercise the code
+  observedGenerationFieldPath: Generation
+  conditionsFieldPath: Status.Conditions
+  conditions:
+  - name: Ready
+    value: Ready
+  - name: Initialized
+    ref: v1.PodInitialized
+type: "v1.Pod"
+clientType: "cv1.PodsGetter"
 cf:
   name: "OperatorConfig"
diff --git a/pkg/kf/internal/tools/clientgen/gentest/client_test.go b/pkg/kf/internal/tools/clientgen/gentest/client_test.go
index 39a22a6..090bbe2 100644
--- a/pkg/kf/internal/tools/clientgen/gentest/client_test.go
+++ b/pkg/kf/internal/tools/clientgen/gentest/client_test.go
@@ -34,15 +34,15 @@ import (
 )
 
 func ExampleList_Filter() {
-	first := v1.Secret{}
+	first := v1.Pod{}
 	first.Name = "ok"
 
-	second := v1.Secret{}
+	second := v1.Pod{}
 	second.Name = "name-too-long-to-pass"
 
 	list := List{first, second}
 
-	filtered := list.Filter(func(s *v1.Secret) bool {
+	filtered := list.Filter(func(s *v1.Pod) bool {
 		return len(s.Name) < 8
 	})
 
@@ -56,7 +56,7 @@ func ExampleList_Filter() {
 }
 
 func ExampleLabelSetMutator() {
-	out := &v1.Secret{}
+	out := &v1.Pod{}
 	managedAdder := LabelSetMutator(map[string]string{"managed-by": "kf"})
 
 	managedAdder(out)
@@ -70,33 +70,33 @@ func TestClient_invariant(t *testing.T) {
 	// write operations.
 	mockK8s := testclient.NewSimpleClientset().CoreV1()
 
-	secretsClient := NewExampleClient(mockK8s)
+	client := NewExampleClient(mockK8s)
 
 	t.Run("create", func(t *testing.T) {
-		good := &v1.Secret{}
+		good := &v1.Pod{}
 		good.Name = "created-through-client"
 
-		if _, err := secretsClient.Create("default", good); err != nil {
+		if _, err := client.Create("default", good); err != nil {
 			t.Fatal(err)
 		}
 	})
 
 	t.Run("list", func(t *testing.T) {
-		out, err := secretsClient.List("default")
+		out, err := client.List("default")
 		testutil.AssertNil(t, "list err", err)
 
-		testutil.AssertEqual(t, "secret count", 1, len(out))
+		testutil.AssertEqual(t, "object count", 1, len(out))
 	})
 
 	t.Run("transform", func(t *testing.T) {
-		transformResult, err := secretsClient.Transform("default", "created-through-client", func(s *v1.Secret) error {
+		transformResult, err := client.Transform("default", "created-through-client", func(s *v1.Pod) error {
 			s.Labels["mutated"] = "true"
 
 			return nil
 		})
 		testutil.AssertNil(t, "transform err", err)
 
-		modified, err := secretsClient.Get("default", "created-through-client")
+		modified, err := client.Get("default", "created-through-client")
 		testutil.AssertNil(t, "get err", err)
 
 		testutil.AssertEqual(t, "mutated label", "true", modified.Labels["mutated"])
@@ -112,22 +112,22 @@ func TestClient_Delete(t *testing.T) {
 		Name      string
 		Options   []DeleteOption
 		ExpectErr error
-		Setup     func(mockK8s cv1.SecretsGetter)
+		Setup     func(mockK8s cv1.PodsGetter)
 	}{
-		"secret does not exist": {
+		"object does not exist": {
 			Namespace: "default",
-			Name:      "some-secret",
+			Name:      "some-object",
 			Options:   []DeleteOption{},
-			ExpectErr: errors.New(`couldn't delete the OperatorConfig with the name "some-secret": secrets "some-secret" not found`),
+			ExpectErr: errors.New(`couldn't delete the OperatorConfig with the name "some-object": pods "some-object" not found`),
 		},
-		"secret exists": {
+		"object exists": {
 			Namespace: "my-namespace",
-			Name:      "some-secret",
+			Name:      "some-object",
 			Options:   []DeleteOption{},
-			Setup: func(mockK8s cv1.SecretsGetter) {
-				secret := &v1.Secret{}
-				secret.Name = "some-secret"
-				mockK8s.Secrets("my-namespace").Create(secret)
+			Setup: func(mockK8s cv1.PodsGetter) {
+				obj := &v1.Pod{}
+				obj.Name = "some-object"
+				mockK8s.Pods("my-namespace").Create(obj)
 			},
 		},
 	}
@@ -140,25 +140,25 @@ func TestClient_Delete(t *testing.T) {
 				tc.Setup(mockK8s)
 			}
 
-			secretsClient := coreClient{
+			client := coreClient{
 				kclient: mockK8s,
 			}
 
-			actualErr := secretsClient.Delete(tc.Namespace, tc.Name, tc.Options...)
+			actualErr := client.Delete(tc.Namespace, tc.Name, tc.Options...)
 			if tc.ExpectErr != nil || actualErr != nil {
 				testutil.AssertErrorsEqual(t, tc.ExpectErr, actualErr)
 
 				return
 			}
 
-			secrets, err := mockK8s.Secrets(tc.Namespace).List(metav1.ListOptions{})
+			objects, err := mockK8s.Pods(tc.Namespace).List(metav1.ListOptions{})
 			if err != nil {
 				t.Fatal(err)
 			}
 
-			for _, s := range secrets.Items {
+			for _, s := range objects.Items {
 				if s.Name == tc.Name {
-					t.Fatal("The secret wasn't deleted")
+					t.Fatal("The object wasn't deleted")
 				}
 			}
 		})
@@ -166,55 +166,55 @@ func TestClient_Delete(t *testing.T) {
 }
 
 func TestClient_Upsert(t *testing.T) {
-	fakeSecret := func(name string, value string) *v1.Secret {
-		s := &v1.Secret{}
+	fakePod := func(name string, hostname string) *v1.Pod {
+		s := &v1.Pod{}
 		s.Name = name
-		s.StringData = map[string]string{"value": value}
-
+		s.Spec.Hostname = hostname
 		return s
 	}
+
 	cases := map[string]struct {
 		Namespace   string
-		PreExisting []*v1.Secret
-		ToUpsert    *v1.Secret
+		PreExisting []*v1.Pod
+		ToUpsert    *v1.Pod
 		Merger      Merger
 		ExpectErr   error
-		Validate    func(t *testing.T, mockK8s cv1.SecretsGetter)
+		Validate    func(t *testing.T, mockK8s cv1.PodsGetter)
 	}{
 		"inserts if not found": {
 			Namespace: "default",
-			ToUpsert:  fakeSecret("foo", "new"),
+			ToUpsert:  fakePod("foo", "new"),
 			Merger:    nil, // should not be called
-			Validate: func(t *testing.T, mockK8s cv1.SecretsGetter) {
-				secret, err := mockK8s.Secrets("default").Get("foo", metav1.GetOptions{})
-				testutil.AssertNil(t, "secrets err", err)
-				testutil.AssertEqual(t, "value", "new", secret.StringData["value"])
+			Validate: func(t *testing.T, mockK8s cv1.PodsGetter) {
+				obj, err := mockK8s.Pods("default").Get("foo", metav1.GetOptions{})
+				testutil.AssertNil(t, "get err", err)
+				testutil.AssertEqual(t, "value", "new", obj.Spec.Hostname)
 			},
 		},
 		"update if found": {
-			PreExisting: []*v1.Secret{fakeSecret("foo", "old")},
+			PreExisting: []*v1.Pod{fakePod("foo", "old")},
 			Namespace:   "testing",
-			ToUpsert:    fakeSecret("foo", "new"),
-			Merger:      func(n, o *v1.Secret) *v1.Secret { return n },
-			Validate: func(t *testing.T, mockK8s cv1.SecretsGetter) {
-				secret, err := mockK8s.Secrets("testing").Get("foo", metav1.GetOptions{})
-				testutil.AssertNil(t, "secrets err", err)
-				testutil.AssertEqual(t, "value", "new", secret.StringData["value"])
+			ToUpsert:    fakePod("foo", "new"),
+			Merger:      func(n, o *v1.Pod) *v1.Pod { return n },
+			Validate: func(t *testing.T, mockK8s cv1.PodsGetter) {
+				obj, err := mockK8s.Pods("testing").Get("foo", metav1.GetOptions{})
+				testutil.AssertNil(t, "get err", err)
+				testutil.AssertEqual(t, "value", "new", obj.Spec.Hostname)
 			},
 		},
 		"calls merge with right order": {
 			Namespace:   "default",
-			PreExisting: []*v1.Secret{fakeSecret("foo", "old")},
-			ToUpsert:    fakeSecret("foo", "new"),
-			Merger: func(n, o *v1.Secret) *v1.Secret {
-				n.StringData["value"] = n.StringData["value"] + "-" + o.StringData["value"]
+			PreExisting: []*v1.Pod{fakePod("foo", "old")},
+			ToUpsert:    fakePod("foo", "new"),
+			Merger: func(n, o *v1.Pod) *v1.Pod {
+				n.Spec.Hostname = n.Spec.Hostname + "-" + o.Spec.Hostname
 				return n
 			},
 			ExpectErr: nil,
-			Validate: func(t *testing.T, mockK8s cv1.SecretsGetter) {
-				secret, err := mockK8s.Secrets("default").Get("foo", metav1.GetOptions{})
-				testutil.AssertNil(t, "secrets err", err)
-				testutil.AssertEqual(t, "value", "new-old", secret.StringData["value"])
+			Validate: func(t *testing.T, mockK8s cv1.PodsGetter) {
+				obj, err := mockK8s.Pods("default").Get("foo", metav1.GetOptions{})
+				testutil.AssertNil(t, "get err", err)
+				testutil.AssertEqual(t, "value", "new-old", obj.Spec.Hostname)
 			},
 		},
 	}
@@ -223,14 +223,14 @@ func TestClient_Upsert(t *testing.T) {
 		t.Run(tn, func(t *testing.T) {
 			mockK8s := testclient.NewSimpleClientset().CoreV1()
 
-			secretsClient := NewExampleClient(mockK8s)
+			client := NewExampleClient(mockK8s)
 
 			for _, v := range tc.PreExisting {
-				_, err := secretsClient.Create(tc.Namespace, v)
-				testutil.AssertNil(t, "creating preexisting secrets", err)
+				_, err := client.Create(tc.Namespace, v)
+				testutil.AssertNil(t, "creating preexisting objects", err)
 			}
 
-			_, actualErr := secretsClient.Upsert(tc.Namespace, tc.ToUpsert, tc.Merger)
+			_, actualErr := client.Upsert(tc.Namespace, tc.ToUpsert, tc.Merger)
 			if tc.ExpectErr != nil || actualErr != nil {
 				testutil.AssertErrorsEqual(t, tc.ExpectErr, actualErr)
 
@@ -245,29 +245,29 @@ func TestClient_Upsert(t *testing.T) {
 }
 
 func ExampleDiffWrapper_noDiff() {
-	secret := &v1.Secret{}
+	obj := &v1.Pod{}
 
-	wrapper := DiffWrapper(os.Stdout, func(s *v1.Secret) error {
-		// don't mutate the secret
+	wrapper := DiffWrapper(os.Stdout, func(s *v1.Pod) error {
+		// don't mutate the object
 		return nil
 	})
 
-	wrapper(secret)
+	wrapper(obj)
 
 	// Output: No changes
 }
 
 func ExampleDiffWrapper_changes() {
-	secret := &v1.Secret{}
-	secret.Type = "opaque"
+	obj := &v1.Pod{}
+	obj.Spec.Hostname = "opaque"
 
 	contents := &bytes.Buffer{}
-	wrapper := DiffWrapper(contents, func(s *v1.Secret) error {
-		s.Type = "docker-creds"
+	wrapper := DiffWrapper(contents, func(obj *v1.Pod) error {
+		obj.Spec.Hostname = "docker-creds"
 		return nil
 	})
 
-	fmt.Println("Error:", wrapper(secret))
+	fmt.Println("Error:", wrapper(obj))
 	firstLine := strings.Split(contents.String(), "\n")[0]
 	fmt.Println("First line:", firstLine)
 
@@ -276,13 +276,13 @@ func ExampleDiffWrapper_changes() {
 }
 
 func ExampleDiffWrapper_err() {
-	secret := &v1.Secret{}
+	obj := &v1.Pod{}
 
-	wrapper := DiffWrapper(os.Stdout, func(s *v1.Secret) error {
+	wrapper := DiffWrapper(os.Stdout, func(_ *v1.Pod) error {
 		return errors.New("some-error")
 	})
 
-	fmt.Println(wrapper(secret))
+	fmt.Println(wrapper(obj))
 
 	// Output: some-error
 }
@@ -295,7 +295,7 @@ func TestConditionDeleted(t *testing.T) {
 		wantErr  error
 	}{
 		"not found error": {
-			apiErr:   apierrors.NewNotFound(schema.GroupResource{}, "my-secret"),
+			apiErr:   apierrors.NewNotFound(schema.GroupResource{}, "my-obj"),
 			wantDone: true,
 		},
 		"nil error": {
@@ -321,9 +321,9 @@ func TestConditionDeleted(t *testing.T) {
 
 func ExampleClient_WaitForE_conditionDeleted() {
 	mockK8s := testclient.NewSimpleClientset().CoreV1()
-	secretsClient := NewExampleClient(mockK8s)
+	client := NewExampleClient(mockK8s)
 
-	instance, err := secretsClient.WaitForE(context.Background(), "default", "secret-name", 1*time.Second, ConditionDeleted)
+	instance, err := client.WaitForE(context.Background(), "default", "obj-name", 1*time.Second, ConditionDeleted)
 	fmt.Println("Instance:", instance)
 	fmt.Println("Error:", err)
 
@@ -333,13 +333,13 @@ func ExampleClient_WaitForE_conditionDeleted() {
 
 func ExampleClient_WaitForE_timeout() {
 	mockK8s := testclient.NewSimpleClientset().CoreV1()
-	secretsClient := NewExampleClient(mockK8s)
+	client := NewExampleClient(mockK8s)
 
 	ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
 	defer cancel()
 
 	called := 0
-	instance, err := secretsClient.WaitForE(ctx, "default", "secret-name", 100*time.Millisecond, func(_ *v1.Secret, _ error) (bool, error) {
+	instance, err := client.WaitForE(ctx, "default", "object-name", 100*time.Millisecond, func(_ *v1.Pod, _ error) (bool, error) {
 		called++
 		return false, nil
 	})
@@ -383,7 +383,7 @@ func TestWrapPredicate(t *testing.T) {
 	for tn, tc := range cases {
 		t.Run(tn, func(t *testing.T) {
 			called := false
-			wrapped := wrapPredicate(func(_ *v1.Secret) bool {
+			wrapped := wrapPredicate(func(_ *v1.Pod) bool {
 				called = true
 				return tc.predicateResponse
 			})
@@ -395,3 +395,72 @@ func TestWrapPredicate(t *testing.T) {
 		})
 	}
 }
+
+func TestCheckConditionTrue(t *testing.T) {
+	cases := map[string]struct {
+		conditions []v1.PodCondition
+		err        error
+
+		wantDone bool
+		wantErr  error
+	}{
+		"error supplied": {
+			err: errors.New("test"),
+
+			wantDone: true,
+			wantErr:  errors.New("test"),
+		},
+		"condition missing": {
+			conditions: []v1.PodCondition{},
+			wantDone:   false,
+			wantErr:    nil,
+		},
+		"condition unknown": {
+			conditions: []v1.PodCondition{
+				{
+					Type:   v1.PodConditionType(ConditionReady),
+					Status: v1.ConditionUnknown,
+				},
+			},
+			wantDone: false,
+			wantErr:  nil,
+		},
+		"condition true": {
+			conditions: []v1.PodCondition{
+				{
+					Type:   v1.PodConditionType(ConditionReady),
+					Status: v1.ConditionTrue,
+				},
+			},
+			wantDone: true,
+			wantErr:  nil,
+		},
+		"condition false": {
+			conditions: []v1.PodCondition{
+				{
+					Type:    v1.PodConditionType(ConditionReady),
+					Status:  v1.ConditionFalse,
+					Message: "SomeMessage",
+					Reason:  "SomeReason",
+				},
+			},
+			wantDone: true,
+			wantErr:  errors.New("checking Ready failed, status: False message: SomeMessage reason: SomeReason"),
+		},
+	}
+
+	for tn, tc := range cases {
+		t.Run(tn, func(t *testing.T) {
+			pod := &v1.Pod{
+				Status: v1.PodStatus{
+					Conditions: tc.conditions,
+				},
+			}
+
+			actualDone, actualErr := checkConditionTrue(pod, tc.err, ConditionReady)
+
+			testutil.AssertErrorsEqual(t, tc.wantErr, actualErr)
+			testutil.AssertEqual(t, "done", tc.wantDone, actualDone)
+		})
+	}
+}
diff --git a/pkg/kf/internal/tools/clientgen/gentest/zz_generated.client.go b/pkg/kf/internal/tools/clientgen/gentest/zz_generated.client.go
index a542713..c56c9c9 100644
--- a/pkg/kf/internal/tools/clientgen/gentest/zz_generated.client.go
+++ b/pkg/kf/internal/tools/clientgen/gentest/zz_generated.client.go
@@ -27,6 +27,7 @@ import (
 
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"knative.dev/pkg/apis"
 	"knative.dev/pkg/kmp"
 )
 
@@ -42,22 +43,27 @@ import (
 
 const (
 	// Kind contains the kind for the backing Kubernetes API.
-	Kind = "Secret"
+	Kind = "Pod"
 
 	// APIVersion contains the version for the backing Kubernetes API.
 	APIVersion = "v1"
 )
 
-// Predicate is a boolean function for a v1.Secret.
-type Predicate func(*v1.Secret) bool
+var (
+	ConditionReady       = apis.ConditionType("Ready")
+	ConditionInitialized = apis.ConditionType(v1.PodInitialized)
+)
+
+// Predicate is a boolean function for a v1.Pod.
+type Predicate func(*v1.Pod) bool
 
-// Mutator is a function that changes v1.Secret.
-type Mutator func(*v1.Secret) error
+// Mutator is a function that changes v1.Pod.
+type Mutator func(*v1.Pod) error
 
 // DiffWrapper wraps a mutator and prints out the diff between the original object
 // and the one it returns if there's no error.
 func DiffWrapper(w io.Writer, mutator Mutator) Mutator {
-	return func(mutable *v1.Secret) error {
+	return func(mutable *v1.Pod) error {
 		before := mutable.DeepCopy()
 
 		if err := mutator(mutable); err != nil {
@@ -70,9 +76,9 @@ func DiffWrapper(w io.Writer, mutator Mutator) Mutator {
 	}
 }
 
-// FormatDiff creates a diff between two v1.Secrets and writes it to the given
+// FormatDiff creates a diff between two v1.Pods and writes it to the given
 // writer.
-func FormatDiff(w io.Writer, leftName, rightName string, left, right *v1.Secret) {
+func FormatDiff(w io.Writer, leftName, rightName string, left, right *v1.Pod) {
 	diff, err := kmp.SafeDiff(left, right)
 	switch {
 	case err != nil:
@@ -90,8 +96,8 @@ func FormatDiff(w io.Writer, leftName, rightName string, left, right *v1.Secret)
 	}
 }
 
-// List represents a collection of v1.Secret.
-type List []v1.Secret
+// List represents a collection of v1.Pod.
+type List []v1.Pod
 
 // Filter returns a new list items for which the predicates fails removed.
 func (list List) Filter(filter Predicate) (out List) {
@@ -104,32 +110,60 @@ func (list List) Filter(filter Predicate) (out List) {
 	return
 }
 
+// ObservedGenerationMatchesGeneration is a predicate that returns true if the
+// object's ObservedGeneration matches the genration of the object.
+func ObservedGenerationMatchesGeneration(obj *v1.Pod) bool {
+	return obj.Generation == obj.Generation
+}
+
+// ExtractConditions converts the native condition types into an apis.Condition
+// array with the Type, Status, Reason, and Message fields intact.
+func ExtractConditions(obj *v1.Pod) (extracted []apis.Condition) {
+	for _, cond := range obj.Status.Conditions {
+		// Only copy the following four fields to be compatible with
+		// recommended Kuberntes fields.
+		extracted = append(extracted, apis.Condition{
+			Type:    apis.ConditionType(cond.Type),
+			Status:  cond.Status,
+			Reason:  cond.Reason,
+			Message: cond.Message,
+		})
+	}
+
+	return
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // Client
 ////////////////////////////////////////////////////////////////////////////////
 
-// Client is the interface for interacting with v1.Secret types as OperatorConfig CF style objects.
+// Client is the interface for interacting with v1.Pod types as OperatorConfig CF style objects.
 type Client interface {
-	Create(namespace string, obj *v1.Secret, opts ...CreateOption) (*v1.Secret, error)
-	Update(namespace string, obj *v1.Secret, opts ...UpdateOption) (*v1.Secret, error)
-	Transform(namespace string, name string, transformer Mutator) (*v1.Secret, error)
-	Get(namespace string, name string, opts ...GetOption) (*v1.Secret, error)
+	Create(namespace string, obj *v1.Pod, opts ...CreateOption) (*v1.Pod, error)
+	Update(namespace string, obj *v1.Pod, opts ...UpdateOption) (*v1.Pod, error)
+	Transform(namespace string, name string, transformer Mutator) (*v1.Pod, error)
+	Get(namespace string, name string, opts ...GetOption) (*v1.Pod, error)
 	Delete(namespace string, name string, opts ...DeleteOption) error
-	List(namespace string, opts ...ListOption) ([]v1.Secret, error)
-	Upsert(namespace string, newObj *v1.Secret, merge Merger) (*v1.Secret, error)
-	WaitFor(ctx context.Context, namespace string, name string, interval time.Duration, condition Predicate) (*v1.Secret, error)
-	WaitForE(ctx context.Context, namespace string, name string, interval time.Duration, condition ConditionFuncE) (*v1.Secret, error)
+	List(namespace string, opts ...ListOption) ([]v1.Pod, error)
+	Upsert(namespace string, newObj *v1.Pod, merge Merger) (*v1.Pod, error)
+	WaitFor(ctx context.Context, namespace string, name string, interval time.Duration, condition Predicate) (*v1.Pod, error)
+	WaitForE(ctx context.Context, namespace string, name string, interval time.Duration, condition ConditionFuncE) (*v1.Pod, error)
+
+	// Utility functions
+	WaitForDeletion(ctx context.Context, namespace string, name string, interval time.Duration) (*v1.Pod, error)
+	WaitForConditionReadyTrue(ctx context.Context, namespace string, name string, interval time.Duration) (*v1.Pod, error)
+	WaitForConditionInitializedTrue(ctx context.Context, namespace string, name string, interval time.Duration) (*v1.Pod, error)
 
 	// ClientExtension can be used by the developer to extend the client.
 	ClientExtension
 }
 
 type coreClient struct {
-	kclient      cv1.SecretsGetter
+	kclient      cv1.PodsGetter
 	upsertMutate Mutator
 }
 
-func (core *coreClient) preprocessUpsert(obj *v1.Secret) error {
+func (core *coreClient) preprocessUpsert(obj *v1.Pod) error {
 	if core.upsertMutate == nil {
 		return nil
 	}
@@ -137,30 +171,30 @@ func (core *coreClient) preprocessUpsert(obj *v1.Secret) error {
 	return core.upsertMutate(obj)
 }
 
-// Create inserts the given v1.Secret into the cluster.
+// Create inserts the given v1.Pod into the cluster.
 // The value to be inserted will be preprocessed and validated before being sent.
-func (core *coreClient) Create(namespace string, obj *v1.Secret, opts ...CreateOption) (*v1.Secret, error) {
+func (core *coreClient) Create(namespace string, obj *v1.Pod, opts ...CreateOption) (*v1.Pod, error) {
 	if err := core.preprocessUpsert(obj); err != nil {
 		return nil, err
 	}
 
-	return core.kclient.Secrets(namespace).Create(obj)
+	return core.kclient.Pods(namespace).Create(obj)
 }
 
 // Update replaces the existing object in the cluster with the new one.
 // The value to be inserted will be preprocessed and validated before being sent.
-func (core *coreClient) Update(namespace string, obj *v1.Secret, opts ...UpdateOption) (*v1.Secret, error) {
+func (core *coreClient) Update(namespace string, obj *v1.Pod, opts ...UpdateOption) (*v1.Pod, error) {
 	if err := core.preprocessUpsert(obj); err != nil {
 		return nil, err
 	}
 
-	return core.kclient.Secrets(namespace).Update(obj)
+	return core.kclient.Pods(namespace).Update(obj)
 }
 
 // Transform performs a read/modify/write on the object with the given name
 // and returns the updated object. Transform manages the options for the Get and
 // Update calls.
-func (core *coreClient) Transform(namespace string, name string, mutator Mutator) (*v1.Secret, error) {
+func (core *coreClient) Transform(namespace string, name string, mutator Mutator) (*v1.Pod, error) {
 	obj, err := core.Get(namespace, name)
 	if err != nil {
 		return nil, err
@@ -176,8 +210,8 @@ func (core *coreClient) Transform(namespace string, name string, mutator Mutator
 // Get retrieves an existing object in the cluster with the given name.
 // The function will return an error if an object is retrieved from the cluster
 // but doesn't pass the membership test of this client.
-func (core *coreClient) Get(namespace string, name string, opts ...GetOption) (*v1.Secret, error) {
-	res, err := core.kclient.Secrets(namespace).Get(name, metav1.GetOptions{})
+func (core *coreClient) Get(namespace string, name string, opts ...GetOption) (*v1.Pod, error) {
+	res, err := core.kclient.Pods(namespace).Get(name, metav1.GetOptions{})
 	if err != nil {
 		return nil, fmt.Errorf("couldn't get the OperatorConfig with the name %q: %v", name, err)
 	}
@@ -190,7 +224,7 @@ func (core *coreClient) Get(namespace string, name string, opts ...GetOption) (*
 func (core *coreClient) Delete(namespace string, name string, opts ...DeleteOption) error {
 	cfg := DeleteOptionDefaults().Extend(opts).toConfig()
 
-	if err := core.kclient.Secrets(namespace).Delete(name, cfg.ToDeleteOptions()); err != nil {
+	if err := core.kclient.Pods(namespace).Delete(name, cfg.ToDeleteOptions()); err != nil {
 		return fmt.Errorf("couldn't delete the OperatorConfig with the name %q: %v", name, err)
 	}
 
@@ -210,10 +244,10 @@ func (cfg deleteConfig) ToDeleteOptions() *metav1.DeleteOptions {
 
 // List gets objects in the cluster and filters the results based on the
 // internal membership test.
-func (core *coreClient) List(namespace string, opts ...ListOption) ([]v1.Secret, error) {
+func (core *coreClient) List(namespace string, opts ...ListOption) ([]v1.Pod, error) {
 	cfg := ListOptionDefaults().Extend(opts).toConfig()
 
-	res, err := core.kclient.Secrets(namespace).List(cfg.ToListOptions())
+	res, err := core.kclient.Pods(namespace).List(cfg.ToListOptions())
 	if err != nil {
 		return nil, fmt.Errorf("couldn't list OperatorConfigs: %v", err)
 	}
@@ -234,11 +268,11 @@ func (cfg listConfig) ToListOptions() (resp metav1.ListOptions) {
 }
 
 // Merger is a type to merge an existing value with a new one.
-type Merger func(newObj, oldObj *v1.Secret) *v1.Secret
+type Merger func(newObj, oldObj *v1.Pod) *v1.Pod
 
 // Upsert inserts the object into the cluster if it doesn't already exist, or else
 // calls the merge function to merge the existing and new then performs an Update.
-func (core *coreClient) Upsert(namespace string, newObj *v1.Secret, merge Merger) (*v1.Secret, error) {
+func (core *coreClient) Upsert(namespace string, newObj *v1.Pod, merge Merger) (*v1.Pod, error) {
 	// NOTE: the field selector may be ignored by some Kubernetes resources
 	// so we double check down below.
 	existing, err := core.List(namespace, WithListFieldSelector(map[string]string{"metadata.name": newObj.Name}))
@@ -257,7 +291,7 @@ func (core *coreClient) Upsert(namespace string, newObj *v1.Secret, merge Merger
 
 // WaitFor is a convenience wrapper for WaitForE that fails if the error
 // passed is non-nil. It allows the use of Predicates instead of ConditionFuncE.
-func (core *coreClient) WaitFor(ctx context.Context, namespace string, name string, interval time.Duration, condition Predicate) (*v1.Secret, error) {
+func (core *coreClient) WaitFor(ctx context.Context, namespace string, name string, interval time.Duration, condition Predicate) (*v1.Pod, error) {
 	return core.WaitForE(ctx, namespace, name, interval, wrapPredicate(condition))
 }
 
@@ -267,19 +301,19 @@ func (core *coreClient) WaitFor(ctx context.Context, namespace string, name stri
 //
 // This function MAY retrieve a nil instance and an apiErr. It's up to the
 // function to decide how to handle the apiErr.
-type ConditionFuncE func(instance *v1.Secret, apiErr error) (done bool, err error)
+type ConditionFuncE func(instance *v1.Pod, apiErr error) (done bool, err error)
 
 // WaitForE polls for the given object every interval until the condition
 // function becomes done or the timeout expires. The first poll occurs
 // immediately after the function is invoked.
 //
 // The function polls infinitely if no timeout is supplied.
-func (core *coreClient) WaitForE(ctx context.Context, namespace string, name string, interval time.Duration, condition ConditionFuncE) (instance *v1.Secret, err error) {
+func (core *coreClient) WaitForE(ctx context.Context, namespace string, name string, interval time.Duration, condition ConditionFuncE) (instance *v1.Pod, err error) {
 	var done bool
 	tick := time.Tick(interval)
 
 	for {
-		instance, err = core.kclient.Secrets(namespace).Get(name, metav1.GetOptions{})
+		instance, err = core.kclient.Pods(namespace).Get(name, metav1.GetOptions{})
 		if done, err = condition(instance, err); done {
 			return
 		}
@@ -295,7 +329,7 @@ func (core *coreClient) WaitForE(ctx context.Context, namespace string, name str
 
 // ConditionDeleted is a ConditionFuncE that succeeds if the error returned by
 // the cluster was a not found error.
-func ConditionDeleted(_ *v1.Secret, apiErr error) (bool, error) {
+func ConditionDeleted(_ *v1.Pod, apiErr error) (bool, error) {
 	if apiErr != nil {
 		if apierrors.IsNotFound(apiErr) {
 			apiErr = nil
@@ -310,7 +344,7 @@ func ConditionDeleted(_ *v1.Secret, apiErr error) (bool, error) {
 // wrapPredicate converts a predicate to a ConditionFuncE that fails if the
 // error is not nil
 func wrapPredicate(condition Predicate) ConditionFuncE {
-	return func(obj *v1.Secret, err error) (bool, error) {
+	return func(obj *v1.Pod, err error) (bool, error) {
 		if err != nil {
 			return true, err
 		}
@@ -318,3 +352,60 @@ func wrapPredicate(condition Predicate) ConditionFuncE {
 		return condition(obj), nil
 	}
 }
+
+// WaitForDeletion is a utility function that combines WaitForE with ConditionDeleted.
+func (core *coreClient) WaitForDeletion(ctx context.Context, namespace string, name string, interval time.Duration) (instance *v1.Pod, err error) {
+	return core.WaitForE(ctx, namespace, name, interval, ConditionDeleted)
+}
+
+func checkConditionTrue(obj *v1.Pod, err error, condition apis.ConditionType) (bool, error) {
+	if err != nil {
+		return true, err
+	}
+
+	// don't propagate old statuses
+	if !ObservedGenerationMatchesGeneration(obj) {
+		return false, nil
+	}
+
+	for _, cond := range ExtractConditions(obj) {
+		if cond.Type == condition {
+			switch {
+			case cond.IsTrue():
+				return true, nil
+
+			case cond.IsUnknown():
+				return false, nil
+
+			default:
+				// return true and a failure assuming IsFalse and other statuses can't be
+				// recovered from because they violate the K8s spec
+				return true, fmt.Errorf("checking %s failed, status: %s message: %s reason: %s", cond.Type, cond.Status, cond.Message, cond.Reason)
+			}
+		}
+	}
+
+	return false, nil
+}
+
+// ConditionReadyTrue is a ConditionFuncE that waits for Condition{Ready  Ready} to
+// become true and fails with an error if the condition becomes false.
+func ConditionReadyTrue(obj *v1.Pod, err error) (bool, error) {
+	return checkConditionTrue(obj, err, ConditionReady)
+}
+
+// WaitForConditionReadyTrue is a utility function that combines WaitForE with ConditionReadyTrue.
+func (core *coreClient) WaitForConditionReadyTrue(ctx context.Context, namespace string, name string, interval time.Duration) (instance *v1.Pod, err error) {
+	return core.WaitForE(ctx, namespace, name, interval, ConditionReadyTrue)
+}
+
+// ConditionInitializedTrue is a ConditionFuncE that waits for Condition{Initialized v1.PodInitialized } to
+// become true and fails with an error if the condition becomes false.
+func ConditionInitializedTrue(obj *v1.Pod, err error) (bool, error) {
+	return checkConditionTrue(obj, err, ConditionInitialized)
+}
+
+// WaitForConditionInitializedTrue is a utility function that combines WaitForE with ConditionInitializedTrue.
+func (core *coreClient) WaitForConditionInitializedTrue(ctx context.Context, namespace string, name string, interval time.Duration) (instance *v1.Pod, err error) {
+	return core.WaitForE(ctx, namespace, name, interval, ConditionInitializedTrue)
+}
diff --git a/pkg/kf/commands/utils/prefix_filter.go b/pkg/kf/internal/utils/cli/prefix_filter.go
similarity index 100%
rename from pkg/kf/commands/utils/prefix_filter.go
rename to pkg/kf/internal/utils/cli/prefix_filter.go
diff --git a/pkg/kf/commands/utils/prefix_filter_test.go b/pkg/kf/internal/utils/cli/prefix_filter_test.go
similarity index 96%
rename from pkg/kf/commands/utils/prefix_filter_test.go
rename to pkg/kf/internal/utils/cli/prefix_filter_test.go
index 794fcd2..781c618 100644
--- a/pkg/kf/commands/utils/prefix_filter_test.go
+++ b/pkg/kf/internal/utils/cli/prefix_filter_test.go
@@ -19,7 +19,7 @@ import (
 	"io"
 	"testing"
 
-	"github.com/google/kf/pkg/kf/commands/utils"
+	utils "github.com/google/kf/pkg/kf/internal/utils/cli"
 	"github.com/google/kf/pkg/kf/testutil"
 )
 
diff --git a/pkg/kf/manifest/manifest_test.go b/pkg/kf/manifest/manifest_test.go
index 1b627fa..c0648af 100644
--- a/pkg/kf/manifest/manifest_test.go
+++ b/pkg/kf/manifest/manifest_test.go
@@ -98,6 +98,38 @@ applications:
 				},
 			},
 		},
+		"cf style command": {
+			fileContent: `---
+applications:
+- name: CUSTOM_START
+  command: rake run $VAR
+`,
+			expected: &manifest.Manifest{
+				Applications: []manifest.Application{
+					{
+						Name:    "CUSTOM_START",
+						Command: "rake run $VAR",
+					},
+				},
+			},
+		},
+		"docker style command": {
+			fileContent: `---
+applications:
+- name: CUSTOM_START
+  entrypoint: python
+  args: ["-m", "SimpleHTTPServer"]
+`,
+			expected: &manifest.Manifest{
+				Applications: []manifest.Application{
+					{
+						Name:       "CUSTOM_START",
+						Entrypoint: "python",
+						Args:       []string{"-m", "SimpleHTTPServer"},
+					},
+				},
+			},
+		},
 	}
 
 	for tn, tc := range cases {
@@ -218,3 +250,35 @@ func ExampleApplication_Buildpack() {
 	// One: java
 	// Two: maven,java
 }
+
+func ExampleApplication_CommandArgs() {
+	app := manifest.Application{}
+	fmt.Printf("Blank: %v\n", app.CommandArgs())
+
+	app = manifest.Application{
+		Command: "start.sh && exit 1",
+	}
+	fmt.Printf("Command: %v\n", app.CommandArgs())
+
+	app = manifest.Application{
+		Args: []string{"-m", "SimpleHTTPServer"},
+	}
+	fmt.Printf("Args: %v\n", app.CommandArgs())
+
+	// Output: Blank: []
+	// Command: [start.sh && exit 1]
+	// Args: [-m SimpleHTTPServer]
+}
+
+func ExampleApplication_CommandEntrypoint() {
+	app := manifest.Application{}
+	fmt.Printf("Blank: %v\n", app.CommandEntrypoint())
+
+	app = manifest.Application{
+		Entrypoint: "python",
+	}
+	fmt.Printf("Entrypoint: %v\n", app.CommandEntrypoint())
+
+	// Output: Blank: []
+	// Entrypoint: [python]
+}
diff --git a/pkg/kf/manifest/manifest_validation_test.go b/pkg/kf/manifest/manifest_validation_test.go
new file mode 100644
index 0000000..960bf99
--- /dev/null
+++ b/pkg/kf/manifest/manifest_validation_test.go
@@ -0,0 +1,69 @@
+// Copyright 2019 Google LLC
+//
+// 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
+//
+//     https://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 manifest
+
+import (
+	"context"
+	"testing"
+
+	"github.com/google/kf/pkg/kf/testutil"
+	"knative.dev/pkg/apis"
+)
+
+func TestApplication_Validation(t *testing.T) {
+	cases := map[string]struct {
+		spec Application
+		want *apis.FieldError
+	}{
+		"valid": {
+			spec: Application{},
+		},
+		"entrypoint and args": {
+			spec: Application{
+				Entrypoint: "python",
+				Args:       []string{"-m", "SimpleHTTPServer"},
+			},
+		},
+		"command and args": {
+			spec: Application{
+				Command: "python",
+				Args:    []string{"-m", "SimpleHTTPServer"},
+			},
+			want: apis.ErrMultipleOneOf("args", "command"),
+		},
+		"entrypoint and command": {
+			spec: Application{
+				Entrypoint: "/lifecycle/launcher",
+				Command:    "python",
+			},
+			want: apis.ErrMultipleOneOf("entrypoint", "command"),
+		},
+		"buildpack and buildpacks": {
+			spec: Application{
+				LegacyBuildpack: "default",
+				Buildpacks:      []string{"java", "node"},
+			},
+			want: apis.ErrMultipleOneOf("buildpack", "buildpacks"),
+		},
+	}
+
+	for tn, tc := range cases {
+		t.Run(tn, func(t *testing.T) {
+			got := tc.spec.Validate(context.Background())
+
+			testutil.AssertEqual(t, "validation errors", tc.want.Error(), got.Error())
+		})
+	}
+}
diff --git a/pkg/reconciler/route/resources/virtual_service_test.go b/pkg/reconciler/route/resources/virtual_service_test.go
index 061903d..e11f0bc 100644
--- a/pkg/reconciler/route/resources/virtual_service_test.go
+++ b/pkg/reconciler/route/resources/virtual_service_test.go
@@ -29,6 +29,30 @@ import (
 	networking "knative.dev/pkg/apis/istio/v1alpha3"
 )
 
+func makeRouteSpecFields(host, domain, path string) v1alpha1.RouteSpecFields {
+	return v1alpha1.RouteSpecFields{
+		Hostname: host,
+		Domain:   domain,
+		Path:     path,
+	}
+}
+
+func makeRoute(host, domain, path string) *v1alpha1.Route {
+	return &v1alpha1.Route{
+		Spec: v1alpha1.RouteSpec{
+			RouteSpecFields: makeRouteSpecFields(host, domain, path),
+		},
+	}
+}
+
+func makeRouteClaim(host, domain, path string) *v1alpha1.RouteClaim {
+	return &v1alpha1.RouteClaim{
+		Spec: v1alpha1.RouteClaimSpec{
+			RouteSpecFields: makeRouteSpecFields(host, domain, path),
+		},
+	}
+}
+
 func TestMakeVirtualService(t *testing.T) {
 	t.Parallel()
 
@@ -50,11 +74,7 @@ func TestMakeVirtualService(t *testing.T) {
 						Labels:    map[string]string{"a": "1", "b": "2"},
 					},
 					Spec: v1alpha1.RouteClaimSpec{
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "some-host",
-							Domain:   "example.com",
-							Path:     "some-path",
-						},
+						RouteSpecFields: makeRouteSpecFields("some-host", "example.com", "some-path"),
 					},
 				},
 			},
@@ -65,15 +85,7 @@ func TestMakeVirtualService(t *testing.T) {
 					Kind:       "VirtualService",
 				}, v.TypeMeta)
 
-				route := &v1alpha1.Route{
-					Spec: v1alpha1.RouteSpec{
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "some-host",
-							Domain:   "example.com",
-							Path:     "/some-path",
-						},
-					},
-				}
+				route := makeRoute("some-host", "example.com", "/some-path")
 
 				testutil.AssertEqual(t, "ObjectMeta", metav1.ObjectMeta{
 					Name:      v1alpha1.GenerateName(route.Spec.Hostname, route.Spec.Domain),
@@ -96,11 +108,7 @@ func TestMakeVirtualService(t *testing.T) {
 			Claims: []*v1alpha1.RouteClaim{
 				{
 					Spec: v1alpha1.RouteClaimSpec{
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "some-host",
-							Domain:   "example.com",
-							Path:     "/some-path",
-						},
+						RouteSpecFields: makeRouteSpecFields("some-host", "example.com", "/some-path"),
 					},
 				},
 			},
@@ -117,26 +125,10 @@ func TestMakeVirtualService(t *testing.T) {
 		},
 		"Route": {
 			Claims: []*v1alpha1.RouteClaim{
-				{
-					Spec: v1alpha1.RouteClaimSpec{
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "some-host",
-							Domain:   "example.com",
-							Path:     "/some-other-path",
-						},
-					},
-				},
+				makeRouteClaim("some-host", "example.com", "/some-other-path"),
 			},
 			Routes: []*v1alpha1.Route{
-				{
-					Spec: v1alpha1.RouteSpec{
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "some-host",
-							Domain:   "example.com",
-							Path:     "/some-path",
-						},
-					},
-				},
+				makeRoute("some-host", "example.com", "/some-path"),
 			},
 			Assert: func(t *testing.T, v *networking.VirtualService, err error) {
 				testutil.AssertNil(t, "err", err)
@@ -159,23 +151,15 @@ func TestMakeVirtualService(t *testing.T) {
 						Namespace: "some-namespace",
 					},
 					Spec: v1alpha1.RouteClaimSpec{
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "some-host",
-							Domain:   "example.com",
-							Path:     "/some-path",
-						},
+						RouteSpecFields: makeRouteSpecFields("some-host", "example.com", "/some-path"),
 					},
 				},
 			},
 			Routes: []*v1alpha1.Route{
 				{
 					Spec: v1alpha1.RouteSpec{
-						AppName: "some-app-name",
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "some-host",
-							Domain:   "example.com",
-							Path:     "/some-path",
-						},
+						AppName:         "some-app-name",
+						RouteSpecFields: makeRouteSpecFields("some-host", "example.com", "/some-path"),
 					},
 				},
 			},
@@ -195,15 +179,7 @@ func TestMakeVirtualService(t *testing.T) {
 		},
 		"when there aren't any bound services, setup fault to 503": {
 			Claims: []*v1alpha1.RouteClaim{
-				{
-					Spec: v1alpha1.RouteClaimSpec{
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "some-host",
-							Domain:   "example.com",
-							Path:     "/some-path",
-						},
-					},
-				},
+				makeRouteClaim("some-host", "example.com", "/some-path"),
 			},
 			Assert: func(t *testing.T, v *networking.VirtualService, err error) {
 				testutil.AssertNil(t, "err", err)
@@ -223,11 +199,7 @@ func TestMakeVirtualService(t *testing.T) {
 						Namespace: "some-namespace",
 					},
 					Spec: v1alpha1.RouteClaimSpec{
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "some-host",
-							Domain:   "example.com",
-							Path:     "/some-path",
-						},
+						RouteSpecFields: makeRouteSpecFields("some-host", "example.com", "/some-path"),
 					},
 				},
 			},
@@ -237,64 +209,56 @@ func TestMakeVirtualService(t *testing.T) {
 						Namespace: "some-namespace",
 					},
 					Spec: v1alpha1.RouteSpec{
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "some-host",
-							Domain:   "example.com",
-							Path:     "/some-path",
-						},
-						AppName: "ksvc-1",
+						RouteSpecFields: makeRouteSpecFields("some-host", "example.com", "/some-path"),
+						AppName:         "ksvc-1",
 					},
 				},
 			},
 			Assert: func(t *testing.T, v *networking.VirtualService, err error) {
-				testutil.AssertNil(t, "err", err)
-				testutil.AssertEqual(t, "HTTP len", 1, len(v.Spec.HTTP))
-				testutil.AssertEqual(t, "HTTP route destination", networking.HTTPRouteDestination{
-					Destination: networking.Destination{
-						Host: "istio-ingressgateway.istio-system.svc.cluster.local",
+				expectedHTTP := []networking.HTTPRoute{
+					{
+						Match: []networking.HTTPMatchRequest{
+							{URI: &istio.StringMatch{Regex: "^/some-path(/.*)?"}},
+						},
+						Rewrite: &networking.HTTPRewrite{
+							Authority: network.GetServiceHostname("ksvc-1", "some-namespace"),
+						},
+						Route: []networking.HTTPRouteDestination{
+							{
+								Destination: networking.Destination{Host: "istio-ingressgateway.istio-system.svc.cluster.local"},
+								Weight:      100,
+							},
+						},
+						Headers: &networking.Headers{
+							Request: &networking.HeaderOperations{
+								Add: map[string]string{
+									"X-Forwarded-Host": "some-host.example.com",
+									"Forwarded":        "host=some-host.example.com",
+								},
+							},
+						},
 					},
-					Weight: 100,
-				}, v.Spec.HTTP[0].Route[0])
-				testutil.AssertEqual(t, "HTTP Rewrite", &networking.HTTPRewrite{
-					Authority: network.GetServiceHostname("ksvc-1", "some-namespace"),
-				}, v.Spec.HTTP[0].Rewrite)
-				testutil.AssertEqual(t, "HTTP Match len", 1, len(v.Spec.HTTP[0].Match))
-				testutil.AssertEqual(t, "HTTP Match", "^/some-path(/.*)?", v.Spec.HTTP[0].Match[0].URI.Regex)
+				}
+
+				testutil.AssertNil(t, "err", err)
+				testutil.AssertEqual(t, "HTTP", expectedHTTP, v.Spec.HTTP)
 			},
 		},
 		"Hosts with subdomain": {
 			Claims: []*v1alpha1.RouteClaim{
-				{
-					Spec: v1alpha1.RouteClaimSpec{
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "some-host",
-							Domain:   "example.com",
-							Path:     "/some-path",
-						},
-					},
-				},
+				makeRouteClaim("some-host", "example.com", "/some-path"),
 			},
 			Assert: func(t *testing.T, v *networking.VirtualService, err error) {
 				testutil.AssertNil(t, "err", err)
-				testutil.AssertEqual(t, "Hosts len", 1, len(v.Spec.Hosts))
 				testutil.AssertEqual(t, "Hosts", []string{"some-host.example.com"}, v.Spec.Hosts)
 			},
 		},
 		"Hosts without subdomain": {
 			Claims: []*v1alpha1.RouteClaim{
-				{
-					Spec: v1alpha1.RouteClaimSpec{
-						RouteSpecFields: v1alpha1.RouteSpecFields{
-							Hostname: "",
-							Domain:   "example.com",
-							Path:     "/some-path",
-						},
-					},
-				},
+				makeRouteClaim("", "example.com", "/some-path"),
 			},
 			Assert: func(t *testing.T, v *networking.VirtualService, err error) {
 				testutil.AssertNil(t, "err", err)
-				testutil.AssertEqual(t, "Hosts len", 1, len(v.Spec.Hosts))
 				testutil.AssertEqual(t, "Hosts", []string{"example.com"}, v.Spec.Hosts)
 			},
 		},
@@ -307,43 +271,17 @@ func TestMakeVirtualService(t *testing.T) {
 }
 
 func ExampleMakeVirtualService() {
-	vs, err := resources.MakeVirtualService([]*v1alpha1.RouteClaim{
-		{
-			Spec: v1alpha1.RouteClaimSpec{
-				RouteSpecFields: v1alpha1.RouteSpecFields{
-					Hostname: "some-host",
-					Domain:   "example.com/",
-				},
-			},
-		},
-	}, []*v1alpha1.Route{
-		{
-			Spec: v1alpha1.RouteSpec{
-				RouteSpecFields: v1alpha1.RouteSpecFields{
-					Hostname: "some-host",
-					Domain:   "example.com/",
-				},
-			},
-		},
-		{
-			Spec: v1alpha1.RouteSpec{
-				RouteSpecFields: v1alpha1.RouteSpecFields{
-					Hostname: "some-host",
-					Domain:   "example.com",
-					Path:     "/some-path-1",
-				},
-			},
-		},
-		{
-			Spec: v1alpha1.RouteSpec{
-				RouteSpecFields: v1alpha1.RouteSpecFields{
-					Hostname: "some-host",
-					Domain:   "example.com",
-					Path:     "/some-path-2",
-				},
-			},
-		},
-	})
+	claims := []*v1alpha1.RouteClaim{
+		makeRouteClaim("some-host", "example.com/", ""),
+	}
+
+	routes := []*v1alpha1.Route{
+		makeRoute("some-host", "example.com/", ""),
+		makeRoute("some-host", "example.com/", "/some-path-1"),
+		makeRoute("some-host", "example.com/", "/some-path-2"),
+	}
+
+	vs, err := resources.MakeVirtualService(claims, routes)
 	if err != nil {
 		panic(err)
 	}