Repository: knative-sandbox/kn-plugin-func
Pull request #1193 (2022-08-25 13:20:02)

Oncluster build initial e2e set of tests
<!-- Thanks for sending a pull request! -->

# Changes

This PR contains an intial set of tests for On Cluster builds according to #856 

- :gift: E2E on cluster build tests

/kind enhancement

The following is covered by this PR

- [x] Deploy on cluster a default function from a public git repository, from a default branch
- [x] Deploy on cluster a default function from a public git repository, from a feature branch
- [x] Deploy on cluster a default function from a public git repository, from a commit hash
- [x] Deploy on cluster a default function from a public git repository, from a tag revision
- [x] Deploy on cluster a default function located in a specific directory from a public git repository, from main branch
- [x] Updated changes to a function get deployed
- [x] Deploy on cluster a default function for each language: node, python, quarkus, springboot, typescript
- [ ] Deploy on cluster a fuction from a public GitHub repository
- [ ] Deploy on cluster a fuction from a public GitLab repository
- [ ] Deploy on cluster a default function from a Private git repository using basic http auth
- [ ] Deploy on cluster a default function from a Private git repository using ssh auth
- [x] Triggers on cluster build from cli when func.yaml indicates local
- [x] Triggers on cluster build from cli when func.yaml indicates local from a specific branch
- [x] Triggers on cluster build from cli when func.yaml indicates local from a when a functions is on a specific directory
- [x] Triggers local build from cli when func.yaml indicates build from git
- [x] Ensure all resources created by on cluster build are deleted when the functions is deleted


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

This pull-request has been approved by: *<a href="https://github.com/knative-sandbox/kn-plugin-func/pull/1193#" title="Author self-approved">jrangelramos</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign navidshaikh for approval by writing `/assign @navidshaikh` in a comment. For more information see:[The Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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

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

- **[OWNERS](https://github.com/knative-sandbox/kn-plugin-func/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["navidshaikh"]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) Report
> Merging [#1193](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (5aa7ff6) into [main](https://codecov.io/gh/knative-sandbox/kn-plugin-func/commit/3ac5d468bfdbb326375ebc5bc640729b59fec7f2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (3ac5d46) will **increase** coverage by `1.73%`.
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff             @@
##             main    #1193      +/-   ##
==========================================
+ Coverage   46.63%   48.36%   +1.73%     
==========================================
  Files          67       67              
  Lines        8880     9000     +120     
==========================================
+ Hits         4141     4353     +212     
+ Misses       4345     4248      -97     
- Partials      394      399       +5     
```


| [Impacted Files](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) | Coverage Δ | |
|---|---|---|
| [buildpacks/builder.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-YnVpbGRwYWNrcy9idWlsZGVyLmdv) | `35.77% <0.00%> (-6.47%)` | :arrow_down: |
| [s2i/builder.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-czJpL2J1aWxkZXIuZ28=) | `61.04% <0.00%> (-3.19%)` | :arrow_down: |
| [cmd/client.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y21kL2NsaWVudC5nbw==) | `78.82% <0.00%> (ø)` | |
| [cmd/config.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y21kL2NvbmZpZy5nbw==) | `30.08% <0.00%> (ø)` | |
| [cmd/deploy.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y21kL2RlcGxveS5nbw==) | `59.26% <0.00%> (ø)` | |
| [cmd/format.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y21kL2Zvcm1hdC5nbw==) | `0.00% <0.00%> (ø)` | |
| [cmd/completion\_util.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y21kL2NvbXBsZXRpb25fdXRpbC5nbw==) | `0.00% <0.00%> (ø)` | |
| [function\_builder.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-ZnVuY3Rpb25fYnVpbGRlci5nbw==) | | |
| [builders/builders.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-YnVpbGRlcnMvYnVpbGRlcnMuZ28=) | `84.21% <0.00%> (ø)` | |
| [cmd/run.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y21kL3J1bi5nbw==) | `72.80% <0.00%> (+0.21%)` | :arrow_up: |
| ... and [6 more](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1193/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox)

knative-prow-robot: @jrangelramos: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
knative-prow[bot]: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/knative-sandbox/kn-plugin-func/pull/1193#" title="Author self-approved">jrangelramos</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

- ~~[OWNERS](https://github.com/knative-sandbox/kn-plugin-func/blob/main/OWNERS)~~ [jrangelramos]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
jrangelramos: /hold for other to approve

Patch:
diff --git a/Makefile b/Makefile
index 7e3f36a0..303d8da9 100644
--- a/Makefile
+++ b/Makefile
@@ -149,6 +149,9 @@ test-e2e: ## Run end-to-end tests using an available cluster.
 test-e2e-runtime: ## Run end-to-end lifecycle tests using an available cluster for a single runtime.
 	./test/e2e_lifecycle_tests.sh $(runtime)
 
+test-e2e-on-cluster: ## Run end-to-end on-cluster build tests using an available cluster.
+	./test/e2e_oncluster_tests.sh
+
 ######################
 ##@ Release Artifacts
 ######################
diff --git a/client.go b/client.go
index 686bc6d8..55e95692 100644
--- a/client.go
+++ b/client.go
@@ -368,10 +368,10 @@ func WithRepository(uri string) Option {
 	}
 }
 
-// WithRegistry sets the default registry which is consulted when an image name/tag
-// is not explocitly provided.  Can be fully qualified, including the registry
-// (ex: 'quay.io/myname') or simply the namespace 'myname' which indicates the
-// the use of the default registry.
+// WithRegistry sets the default registry which is consulted when an image
+// name is not explicitly provided.  Can be fully qualified, including the
+// registry and namespace (ex: 'quay.io/myname') or simply the namespace
+// (ex: 'myname').
 func WithRegistry(registry string) Option {
 	return func(c *Client) {
 		c.registry = registry
@@ -410,6 +410,12 @@ func (c *Client) Instances() *Instances {
 	return c.instances
 }
 
+// Repository accessor returns the default registry for use when building
+// Functions which do not specify Registry or Image name explicitly.
+func (c *Client) Registry() string {
+	return c.registry
+}
+
 // Runtimes available in totality.
 // Not all repository/template combinations necessarily exist,
 // and further validation is performed when a template+runtime is chosen.
@@ -622,9 +628,19 @@ func (c *Client) Build(ctx context.Context, path string) (err error) {
 		return
 	}
 
-	// Derive Image from the path (precedence is given to extant config)
-	if f.Image, err = DerivedImage(path, c.registry); err != nil {
-		return
+	// Default function registry to the client's global registry
+	if f.Registry == "" {
+		f.Registry = c.registry
+	}
+
+	// If no image name has been yet defined (not yet built/deployed), calculate.
+	// Image name is stored on the function for later use by deploy, etc.
+	// TODO: write this to .func/build instead, and populate f.Image on deploy
+	// such that local builds do not dirty the work tree.
+	if f.Image == "" {
+		if f.Image, err = f.ImageName(); err != nil {
+			return
+		}
 	}
 
 	if err = c.builder.Build(ctx, f); err != nil {
diff --git a/cmd/build.go b/cmd/build.go
index 8950e673..25d13c93 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -76,45 +76,6 @@ and the image name is stored in the configuration file.
 	return cmd
 }
 
-// ValidateBuilder ensures that the given builder is one that the CLI
-// knows how to instantiate, returning a builkder.ErrUnknownBuilder otherwise.
-func ValidateBuilder(name string) (err error) {
-	for _, known := range KnownBuilders() {
-		if name == known {
-			return
-		}
-	}
-	return builders.ErrUnknownBuilder{Name: name, Known: KnownBuilders()}
-}
-
-func ValidNamespaceAndRegistry(path string) survey.Validator {
-	return func(val interface{}) error {
-
-		// if the value passed in is the zero value of the appropriate type
-		if len(val.(string)) == 0 {
-			return errors.New("Value is required")
-		}
-
-		_, err := fn.DerivedImage(path, val.(string)) //image can be derived without any error
-
-		if err != nil {
-			return fmt.Errorf("invalid registry [%q]: %w", val.(string), err)
-		}
-		return nil
-	}
-}
-
-// KnownBuilders are a typed string slice of builder short names which this
-// CLI understands.  Includes a customized String() representation intended
-// for use in flags and help text.
-func KnownBuilders() builders.Known {
-	// The set of builders supported by this CLI will likely always equate to
-	// the set of builders enumerated in the builders pacakage.
-	// However, future third-party integrations may support less than, or more
-	// builders, and certain environmental considerations may alter this list.
-	return builders.All()
-}
-
 func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err error) {
 	config, err := newBuildConfig().Prompt()
 	if err != nil {
@@ -124,63 +85,31 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro
 		return
 	}
 
-	function, err := functionWithOverrides(config.Path, functionOverrides{Image: config.Image})
+	// Load the Function at path, and if it is initialized, update it with
+	// pertinent values from the config.
+	f, err := fn.NewFunction(config.Path)
 	if err != nil {
 		return
 	}
-
-	// Check if the function has been initialized
-	if !function.Initialized() {
+	if !f.Initialized() {
 		return fmt.Errorf("'%v' does not contain an initialized function", config.Path)
 	}
-
-	// If a registry name was provided as a command line flag, it should be validated
 	if config.Registry != "" {
-		err = ValidNamespaceAndRegistry(config.Path)(config.Registry)
-		if err != nil {
-			return
-		}
-	} else if function.Registry != "" {
-		// Otherwise, if a registry name was provided in func.config, set that value
-		// for the command configuration and validate it
-		config.Registry = function.Registry
-		err = ValidNamespaceAndRegistry(config.Path)(config.Registry)
-		if err != nil {
-			return
-		}
+		f.Registry = config.Registry
 	}
-
-	// If the function does not yet have an image name and one was not provided on the command line
-	if function.Image == "" && config.Image == "" {
-		//  AND a --registry was not provided, then we need to
-		// prompt for a registry from which we can derive an image name.
-		if config.Registry == "" {
-			fmt.Println("A registry for function images is required. For example, 'docker.io/tigerteam'.")
-
-			err = survey.AskOne(
-				&survey.Input{Message: "Registry for function images:"},
-				&config.Registry, survey.WithValidator(ValidNamespaceAndRegistry(config.Path)))
-			if err != nil {
-				if errors.Is(err, terminal.InterruptErr) {
-					return nil
-				}
-				return
-			}
-			fmt.Println("Note: building a function the first time will take longer than subsequent builds")
-		}
-
-		// We have the registry, so let's use it to derive the function image name
-		config.Image = deriveImage(config.Image, config.Registry, config.Path)
-
-		function.Image = config.Image
+	if config.Image != "" {
+		f.Image = config.Image
+	}
+	if config.Builder != "" {
+		f.Builder = config.Builder
 	}
 
 	// Choose a builder based on the value of the --builder flag
 	var builder fn.Builder
-	if function.Builder == "" || cmd.Flags().Changed("builder") {
-		function.Builder = config.Builder
+	if f.Builder == "" || cmd.Flags().Changed("builder") {
+		f.Builder = config.Builder
 	} else {
-		config.Builder = function.Builder
+		config.Builder = f.Builder
 	}
 	if err = ValidateBuilder(config.Builder); err != nil {
 		return err
@@ -200,28 +129,38 @@ func runBuild(cmd *cobra.Command, _ []string, newClient ClientFactory) (err erro
 			s2i.WithPlatform(config.Platform))
 	}
 
-	// All set, let's write changes in the config to the disk
-	err = function.Write()
-	if err != nil {
-		return
-	}
-
 	// Use the user-provided builder image, if supplied
 	if config.BuilderImage != "" {
-		function.BuilderImages[config.Builder] = config.BuilderImage
+		f.BuilderImages[config.Builder] = config.BuilderImage
 	}
 
-	// Create a client using the registry defined in config plus any additional
-	// options provided (such as mocks for testing)
 	client, done := newClient(ClientConfig{Verbose: config.Verbose},
 		fn.WithRegistry(config.Registry),
 		fn.WithBuilder(builder))
 	defer done()
 
-	err = client.Build(cmd.Context(), config.Path)
-	if err == nil && config.Push {
+	// Default Client Registry, Function Registry or explicit Image is required
+	if client.Registry() == "" && f.Registry == "" && f.Image == "" {
+		// It is not necessary that we validate here, since the client API has
+		// its own validation, but it does give us the opportunity to show a very
+		// cli-specific and detailed error message.
+		return ErrRegistryRequired
+	}
+
+	// This preemptive write call will be unnecessary when the API is updated
+	// to use Function instances rather than file paths. For now it must write
+	// even if the command fails later.  Not ideal.
+	if err = f.Write(); err != nil {
+		return
+	}
+
+	if err = client.Build(cmd.Context(), config.Path); err != nil {
+		return
+	}
+	if config.Push {
 		err = client.Push(cmd.Context(), config.Path)
 	}
+
 	return
 }
 
@@ -278,12 +217,11 @@ func newBuildConfig() buildConfig {
 // Skipped if not in an interactive terminal (non-TTY), or if --confirm false (agree to
 // all prompts) was set (default).
 func (c buildConfig) Prompt() (buildConfig, error) {
-	imageName := deriveImage(c.Image, c.Registry, c.Path)
 	if !interactiveTerminal() || !c.Confirm {
 		return c, nil
 	}
 
-	bc := buildConfig{Verbose: c.Verbose}
+	imageName := deriveImage(c.Image, c.Registry, c.Path)
 
 	var qs = []*survey.Question{
 		{
@@ -292,7 +230,6 @@ func (c buildConfig) Prompt() (buildConfig, error) {
 				Message: "Project path:",
 				Default: c.Path,
 			},
-			Validate: survey.Required,
 		},
 		{
 			Name: "image",
@@ -300,10 +237,28 @@ func (c buildConfig) Prompt() (buildConfig, error) {
 				Message: "Full image name (e.g. quay.io/boson/node-sample):",
 				Default: imageName,
 			},
-			Validate: survey.Required,
 		},
 	}
-	err := survey.Ask(qs, &bc)
+	err := survey.Ask(qs, &c)
+	if err != nil {
+		return c, err
+	}
 
-	return bc, err
+	// if the result of imageName is empty (unset, underivable),
+	// and the value of c.Image is empty (none provided explicitly by flag, env
+	// variable or prompt), then try one more time to get enough to to derive an
+	// image name: explicitly ask for registry.
+	if imageName == "" && c.Image == "" {
+		qs = []*survey.Question{
+			{
+				Name: "registry",
+				Prompt: &survey.Input{
+					Message: "Registry for function image:",
+					Default: c.Registry, // This should be innefectual
+				},
+			},
+		}
+	}
+	err = survey.Ask(qs, &c)
+	return c, err
 }
diff --git a/cmd/deploy.go b/cmd/deploy.go
index e05efbb8..44bc14d6 100644
--- a/cmd/deploy.go
+++ b/cmd/deploy.go
@@ -107,6 +107,8 @@ func runDeploy(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err
 	}
 
 	//if --image contains '@', validate image digest and disable build and push if not set, otherwise return an error
+	// TODO(lkingland): this logic could be assimilated into the Deploy Config
+	// struct and it's constructor.
 	imageSplit := strings.Split(config.Image, "@")
 	imageDigestProvided := false
 
@@ -117,23 +119,30 @@ func runDeploy(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err
 		imageDigestProvided = true
 	}
 
-	function, err := functionWithOverrides(config.Path, functionOverrides{Namespace: config.Namespace, Image: config.Image})
+	// Load the function, and if it exists (path initialized as a function), merge
+	// in any updates from flags/env vars (namespace, explicit image name, envs).
+	f, err := fn.NewFunction(config.Path)
 	if err != nil {
 		return
 	}
-
-	// save image digest if provided in --image
-	if imageDigestProvided {
-		function.ImageDigest = imageSplit[1]
+	if !f.Initialized() {
+		return fmt.Errorf("'%v' does not contain an initialized function", config.Path)
+	}
+	if config.Registry != "" {
+		f.Registry = config.Registry
+	}
+	if config.Image != "" {
+		f.Image = config.Image
+	}
+	if config.Builder != "" {
+		f.Builder = config.Builder
 	}
 
-	// add ns to func.yaml on first deploy and warn if current context differs from func.yaml
-	function.Namespace, err = checkNamespaceDeploy(function.Namespace, config.Namespace)
+	f.Namespace, err = checkNamespaceDeploy(f.Namespace, config.Namespace)
 	if err != nil {
 		return
 	}
-
-	function.Envs, _, err = mergeEnvs(function.Envs, config.EnvToUpdate, config.EnvToRemove)
+	f.Envs, _, err = mergeEnvs(f.Envs, config.EnvToUpdate, config.EnvToRemove)
 	if err != nil {
 		return
 	}
@@ -147,43 +156,21 @@ func runDeploy(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err
 			return err
 		}
 	} else {
-		currentBuildType = function.BuildType
-	}
-
-	// Check if the function has been initialized
-	if !function.Initialized() {
-		return fmt.Errorf("the given path '%v' does not contain an initialized function. Please create one at this path before deploying", config.Path)
+		currentBuildType = f.BuildType
 	}
 
-	// If the function does not yet have an image name and one was not provided on the command line
-	if function.Image == "" && currentBuildType != "disabled" {
-		//  AND a --registry was not provided, then we need to
-		// prompt for a registry from which we can derive an image name.
-		if config.Registry == "" {
-			fmt.Println("A registry for function images is required. For example, 'docker.io/tigerteam'.")
-
-			err = survey.AskOne(
-				&survey.Input{Message: "Registry for function images:"},
-				&config.Registry, survey.WithValidator(survey.Required))
-			if err != nil {
-				if err == terminal.InterruptErr {
-					return nil
-				}
-				return
-			}
-		}
-
-		// We have the registry, so let's use it to derive the function image name
-		config.Image = deriveImage(config.Image, config.Registry, config.Path)
-		function.Image = config.Image
+	if imageDigestProvided {
+		// TODO(lkingland):  This could instead be part of the config, relying on
+		// zero values rather than a flag indicating "Image Digest was Provided"
+		f.ImageDigest = imageSplit[1] // save image digest if provided in --image
 	}
 
 	// Choose a builder based on the value of the --builder flag
 	var builder fn.Builder
-	if function.Builder == "" || cmd.Flags().Changed("builder") {
-		function.Builder = config.Builder
+	if f.Builder == "" || cmd.Flags().Changed("builder") {
+		f.Builder = config.Builder
 	} else {
-		config.Builder = function.Builder
+		config.Builder = f.Builder
 	}
 	if err = ValidateBuilder(config.Builder); err != nil {
 		return err
@@ -198,34 +185,28 @@ func runDeploy(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err
 		builder = s2i.NewBuilder(s2i.WithVerbose(config.Verbose), s2i.WithPlatform(config.Platform))
 	}
 
-	// All set, let's write changes in the config to the disk
-	err = function.Write()
-	if err != nil {
-		return
-	}
-
-	// Default config namespace is the function's namespace
-	if config.Namespace == "" {
-		config.Namespace = function.Namespace
-	}
-
-	// if registry was not changed via command line flag meaning it's empty
-	// keep the same registry by setting the config.registry to empty otherwise
-	// trust viper to override the env variable with the given flag if both are specified
-	if regFlag, _ := cmd.Flags().GetString("registry"); regFlag == "" {
-		config.Registry = ""
-	}
-
 	// Use the user-provided builder image, if supplied
 	if config.BuilderImage != "" {
-		function.BuilderImages[config.Builder] = config.BuilderImage
+		f.BuilderImages[config.Builder] = config.BuilderImage
 	}
 
-	client, done := newClient(ClientConfig{Namespace: config.Namespace, Verbose: config.Verbose},
+	client, done := newClient(ClientConfig{Namespace: f.Namespace, Verbose: config.Verbose},
 		fn.WithRegistry(config.Registry),
 		fn.WithBuilder(builder))
 	defer done()
 
+	// Default Client Registry, Function Registry or explicit Image required
+	if client.Registry() == "" && f.Registry == "" && f.Image == "" {
+		return ErrRegistryRequired
+	}
+
+	// NOTE: curently need to preemptively write out function state until
+	// the API is updated to use instances, at which point we will only
+	// write on success.
+	if err = f.Write(); err != nil {
+		return
+	}
+
 	switch currentBuildType {
 	case fn.BuildTypeLocal, "":
 		if config.GitURL != "" || config.GitDir != "" || config.GitBranch != "" {
@@ -235,7 +216,7 @@ func runDeploy(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err
 			return err
 		}
 	case fn.BuildTypeGit:
-		git := function.Git
+		git := f.Git
 
 		if config.GitURL != "" {
 			git.URL = &config.GitURL
@@ -270,6 +251,28 @@ func runDeploy(cmd *cobra.Command, _ []string, newClient ClientFactory) (err err
 	return client.Deploy(cmd.Context(), config.Path)
 }
 
+// ValidateBuilder ensures that the given builder is one that the CLI
+// knows how to instantiate, returning a builkder.ErrUnknownBuilder otherwise.
+func ValidateBuilder(name string) (err error) {
+	for _, known := range KnownBuilders() {
+		if name == known {
+			return
+		}
+	}
+	return builders.ErrUnknownBuilder{Name: name, Known: KnownBuilders()}
+}
+
+// KnownBuilders are a typed string slice of builder short names which this
+// CLI understands.  Includes a customized String() representation intended
+// for use in flags and help text.
+func KnownBuilders() builders.Known {
+	// The set of builders supported by this CLI will likely always equate to
+	// the set of builders enumerated in the builders pacakage.
+	// However, future third-party integrations may support less than, or more
+	// builders, and certain environmental considerations may alter this list.
+	return builders.All()
+}
+
 func newPromptForCredentials(in io.Reader, out, errOut io.Writer) func(registry string) (docker.Credentials, error) {
 	firstTime := true
 	return func(registry string) (docker.Credentials, error) {
@@ -399,23 +402,9 @@ type deployConfig struct {
 	// (~/.kube/config) in the case of Kubernetes.
 	Namespace string
 
-	// Path of the function implementation on local disk. Defaults to current
-	// working directory of the process.
-	Path string
-
-	// Verbose logging.
-	Verbose bool
-
-	// Confirm: confirm values arrived upon from environment plus flags plus defaults,
-	// with interactive prompting (only applicable when attached to a TTY).
-	Confirm bool
-
 	// Build the associated function before deploying.
 	BuildType string
 
-	// Push function image to the registry before deploying.
-	Push bool
-
 	// Envs passed via cmd to be added/updated
 	EnvToUpdate *util.OrderedMap
 
@@ -450,11 +439,7 @@ func newDeployConfig(cmd *cobra.Command) (deployConfig, error) {
 	return deployConfig{
 		buildConfig: newBuildConfig(),
 		Namespace:   viper.GetString("namespace"),
-		Path:        getPathFlag(),
-		Verbose:     viper.GetBool("verbose"), // defined on root
-		Confirm:     viper.GetBool("confirm"),
 		BuildType:   buildType,
-		Push:        viper.GetBool("push"),
 		EnvToUpdate: envToUpdate,
 		EnvToRemove: envToRemove,
 		GitURL:      viper.GetString("git-url"),
@@ -472,52 +457,47 @@ func (c deployConfig) Prompt() (deployConfig, error) {
 	}
 
 	var qs = []*survey.Question{
+		{
+			Name: "path",
+			Prompt: &survey.Input{
+				Message: "Project path:",
+				Default: c.Path,
+			},
+		},
 		{
 			Name: "registry",
 			Prompt: &survey.Input{
 				Message: "Registry for function images:",
-				Default: c.buildConfig.Registry,
+				Default: c.Registry,
 			},
-			Validate: survey.Required,
 		},
+	}
+	if err := survey.Ask(qs, &c); err != nil {
+		return c, err
+	}
+
+	// calculate imageName with potentially new registry/path
+	imageName := deriveImage(c.Image, c.Registry, c.Path)
+
+	qs = []*survey.Question{
 		{
-			Name: "namespace",
+			Name: "image",
 			Prompt: &survey.Input{
-				Message: "Namespace:",
-				Default: c.Namespace,
+				Message: "Full image name (e.g. quay.io/boson/node-sample):",
+				Default: imageName,
 			},
 		},
 		{
-			Name: "path",
+			Name: "namespace",
 			Prompt: &survey.Input{
-				Message: "Project path:",
-				Default: c.Path,
+				Message: "Namespace into which the function is (re)deployed",
+				Default: c.Namespace,
 			},
-			Validate: survey.Required,
 		},
 	}
-	answers := struct {
-		Registry  string
-		Namespace string
-		Path      string
-	}{}
-	err := survey.Ask(qs, &answers)
-	if err != nil {
-		return deployConfig{}, err
-	}
+	err := survey.Ask(qs, &c)
 
-	dc := deployConfig{
-		buildConfig: buildConfig{
-			Registry: answers.Registry,
-		},
-		Namespace: answers.Namespace,
-		Path:      answers.Path,
-		Verbose:   c.Verbose,
-	}
-
-	dc.Image = deriveImage(dc.Image, dc.Registry, dc.Path)
-
-	return dc, nil
+	return c, err
 }
 
 // ErrInvalidBuildType indicates that the passed build type was invalid.
@@ -563,20 +543,39 @@ func parseImageDigest(imageSplit []string, config deployConfig, cmd *cobra.Comma
 // checkNamespaceDeploy checks current namespace against func.yaml and warns if its different
 // or sets namespace to be written in func.yaml if its the first deployment
 func checkNamespaceDeploy(funcNamespace string, confNamespace string) (string, error) {
-	currNamespace, err := k8s.GetNamespace("")
-	if err != nil {
-		return funcNamespace, err
+	var namespace string
+	var err error
+
+	// Choose target namespace
+	if confNamespace != "" {
+		namespace = confNamespace // --namespace takes precidence
+	} else if funcNamespace != "" {
+		namespace = funcNamespace // value from previous deployment (func.yaml) 2nd priority
+	} else {
+		// Try to derive a default from the current k8s context, if any.
+		namespace, err = k8s.GetNamespace("")
+		if err != nil {
+			return "", nil // configured k8s environment not required
+		}
 	}
 
-	// If ns exists in func.yaml & NOT given via CLI (--namespace flag) & current ns does NOT match func.yaml ns
-	if funcNamespace != "" && confNamespace == "" && (currNamespace != funcNamespace) {
-		fmt.Fprintf(os.Stderr, "Warning: Current namespace '%s' does not match namespace '%s' in func.yaml. Function is deployed at '%s' namespace\n", currNamespace, funcNamespace, funcNamespace)
+	currNamespace, err := k8s.GetNamespace("")
+	if err == nil && namespace != currNamespace {
+		fmt.Fprintf(os.Stderr, "Warning: Current namespace '%s' does not match function which is deployed at '%s' namespace\n", currNamespace, funcNamespace)
 	}
 
-	// Add current namespace to func.yaml if it is NOT set yet & NOT given via --namespace.
-	if funcNamespace == "" {
-		funcNamespace = currNamespace
+	if funcNamespace != "" && namespace != funcNamespace {
+		fmt.Fprintf(os.Stderr, "Warning: New namespace '%s' does not match current namespace '%s'\n", namespace, funcNamespace)
 	}
 
-	return funcNamespace, nil
+	return namespace, nil
 }
+
+var ErrRegistryRequired = errors.New(`A container registry is required.  For example:
+--registry docker.io/myusername
+
+For more advanced usage, it is also possible to specify the exact image to use. For example:
+
+--image docker.io/myusername/myfunc:latest
+
+To run the command in an interactive mode, use --confirm (-c)`)
diff --git a/cmd/root.go b/cmd/root.go
index b45b1594..102d688f 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -160,38 +160,6 @@ func bindEnv(flags ...string) bindFunc {
 	}
 }
 
-type functionOverrides struct {
-	Image     string
-	Namespace string
-}
-
-// functionWithOverrides sets the namespace and image strings for the
-// function project at root, if provided, and returns the function
-// configuration values.
-// Please note that When this function is called, the overrides are not persisted.
-func functionWithOverrides(root string, overrides functionOverrides) (f fn.Function, err error) {
-	f, err = fn.NewFunction(root)
-	if err != nil {
-		return
-	}
-
-	overrideMapping := []struct {
-		src  string
-		dest *string
-	}{
-		{overrides.Image, &f.Image},
-		{overrides.Namespace, &f.Namespace},
-	}
-
-	for _, m := range overrideMapping {
-		if m.src != "" {
-			*m.dest = m.src
-		}
-	}
-
-	return
-}
-
 // deriveName returns the explicit value (if provided) or attempts to derive
 // from the given path.  Path is defaulted to current working directory, where
 // a function configuration, if it exists and contains a name, is used.
@@ -232,9 +200,9 @@ func deriveNameAndAbsolutePathFromPath(path string) (string, string) {
 	return pathParts[len(pathParts)-1], absPath
 }
 
-// deriveImage returns the same image name which will be used if no explicit
-// image is provided.  I.e. derived from the configured registry (registry
-// plus username) and the function's name.
+// deriveImage returns the same image name which will be used.
+// I.e. if the explicit name is empty, derive one from the configured registry
+// (registry plus username) and the function's name.
 //
 // This is calculated preemptively here in the CLI (prior to invoking the
 // client), only in order to provide information to the user via the prompt.
@@ -265,8 +233,10 @@ func deriveImage(explicitImage, defaultRegistry, path string) string {
 	if f.Image != "" {
 		return f.Image // use value previously provided or derived.
 	}
-	derivedValue, _ := fn.DerivedImage(path, defaultRegistry)
-	return derivedValue // Use the func system's derivation logic.
+	// Use the func system's derivation logic.
+	// Errors deriving result in an empty return
+	derivedValue, _ := f.ImageName()
+	return derivedValue
 }
 
 func envFromCmd(cmd *cobra.Command) (*util.OrderedMap, []string, error) {
diff --git a/cmd/run.go b/cmd/run.go
index 55669c46..0847e38d 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -70,10 +70,15 @@ func runRun(cmd *cobra.Command, args []string, newClient ClientFactory) (err err
 		return
 	}
 
+	// Load the Function, and if it exists (path initialized as a Function), merge
+	// in any env vars from the context and save.
 	function, err := fn.NewFunction(config.Path)
 	if err != nil {
 		return
 	}
+	if !function.Initialized() {
+		return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
+	}
 
 	var updated int
 	function.Envs, updated, err = mergeEnvs(function.Envs, config.EnvToUpdate, config.EnvToRemove)
@@ -87,11 +92,6 @@ func runRun(cmd *cobra.Command, args []string, newClient ClientFactory) (err err
 		}
 	}
 
-	// Check if the function has been initialized
-	if !function.Initialized() {
-		return fmt.Errorf("the given path '%v' does not contain an initialized function", config.Path)
-	}
-
 	// Client for use running (and potentially building), using the config
 	// gathered plus any additional option overrieds (such as for providing
 	// mocks when testing for builder and runner)
diff --git a/function.go b/function.go
index 802fa8eb..c1558e96 100644
--- a/function.go
+++ b/function.go
@@ -344,73 +344,6 @@ func (f Function) ImageWithDigest() string {
 	return part1 + strings.Split(part2, ":")[0] + "@" + f.ImageDigest
 }
 
-// DerivedImage returns the derived image name (OCI container tag) of the
-// function whose source is at root, with the default registry for when
-// the image has to be calculated (derived).
-// The following are equivalent due to the use of DefaultRegistry:
-// registry:  docker.io/myname
-//            myname
-// A full image name consists of registry, image name and tag.
-// in form [registry]/[image-name]:[tag]
-// example docker.io/alice/my.example.func:latest
-// Default if not provided is --registry (a required global setting)
-// followed by the provided (or derived) image name.
-// TODO: this calculated field should probably be generated on instantiation
-// to avoid confusion.
-func DerivedImage(root, registry string) (image string, err error) {
-	f, err := NewFunction(root)
-	if err != nil {
-		// an inability to load the function means it is not yet initialized
-		// We could try to be smart here and fall through to the function name
-		// deriviation logic, but that's likely to be confusing.  Better to
-		// stay simple and say that derivation of Image depends on first having
-		// the function initialized.
-		return
-	}
-
-	// If the function has already had image populated
-	// and a new registry hasn't been provided, use this pre-calculated value.
-	if f.Image != "" && f.Registry == registry {
-		image = f.Image
-		return
-	}
-
-	// registry is currently required until such time as we support
-	// pushing to an implicitly-available in-cluster registry by default.
-	if registry == "" {
-		err = errors.New("registry name is required")
-		return
-	}
-
-	// If the function loaded, and there is not yet an Image set, then this is
-	// the first build and no explicit image override was specified.  We should
-	// therefore derive the image tag from the defined registry and name.
-	// form:    [registry]/[user]/[function]:latest
-	// example: quay.io/alice/my.function.name:latest
-	// Also nested namespaces should be supported:
-	// form:    [registry]/[parent]/[user]/[function]:latest
-	// example: quay.io/project/alice/my.function.name:latest
-	registry = strings.Trim(registry, "/") // too defensive?
-	registryTokens := strings.Split(registry, "/")
-	if len(registryTokens) == 1 {
-		//namespace provided only 'alice'
-		image = DefaultRegistry + "/" + registry + "/" + f.Name
-	} else if len(registryTokens) == 2 || len(registryTokens) == 3 {
-		// registry/namespace provided `quay.io/alice` or registry/parent-namespace/namespace provided `quay.io/project/alice`
-		image = registry + "/" + f.Name
-	} else if len(registryTokens) > 3 { // the name of the image is also provided `quay.io/alice/my.function.name`
-		err = fmt.Errorf("registry should be either 'namespace', 'registry/namespace' or 'registry/parent/namespace', the name of the image will be derived from the function name.")
-		return
-	}
-
-	// Explicitly append :latest.  We currently expect source control to drive
-	// versioning, rather than rely on Docker Hub tags with explicit version
-	// numbers, as is seen in many serverless solutions.  This will be updated
-	// to branch name when we add source-driven canary/ bluegreen deployments.
-	image = image + ":latest"
-	return
-}
-
 // assertEmptyRoot ensures that the directory is empty enough to be used for
 // initializing a new function.
 func assertEmptyRoot(path string) (err error) {
@@ -433,6 +366,50 @@ func assertEmptyRoot(path string) (err error) {
 	return
 }
 
+// ImageName returns a full image name (OCI container tag) for the
+// Function based off of the Function's `Registry` member plus `Name`.
+// Used to calculate the final value for .Image when none is provided
+// explicitly.
+//
+// form:    [registry]/[user]/[function]:latest
+// example: quay.io/alice/my.function.name:latest
+//
+// Also nested namespaces should be supported:
+// form:    [registry]/[parent]/[user]/[function]:latest
+// example: quay.io/project/alice/my.function.name:latest
+//
+// Registry values which only contain a single token are presumed to
+// indicate the namespace at the default registry.
+func (f Function) ImageName() (image string, err error) {
+	if f.Registry == "" {
+		return "", errors.New("registry is required")
+	}
+	if f.Name == "" {
+		return "", errors.New("name is required")
+	}
+
+	f.Registry = strings.Trim(f.Registry, "/") // too defensive?
+
+	registryTokens := strings.Split(f.Registry, "/")
+	if len(registryTokens) == 1 { // only namespace provided: ex. 'alice'
+		image = DefaultRegistry + "/" + f.Registry + "/" + f.Name
+	} else if len(registryTokens) == 2 || len(registryTokens) == 3 {
+		// registry/namespace ('quay.io/alice') or
+		// registry/parent-namespace/namespace ('quay.io/project/alice') provided
+		image = f.Registry + "/" + f.Name
+	} else if len(registryTokens) > 3 { // the name of the image is also provided `quay.io/alice/my.function.name`
+		return "", fmt.Errorf("registry should be either 'namespace', 'registry/namespace' or 'registry/parent/namespace', the name of the image will be derived from the function name.")
+	}
+
+	// Explicitly append :latest tag.  We expect source control to drive
+	// versioning, rather than rely on image tags with explicitly pinned version
+	// numbers, as is seen in many serverless solutions.  This will be updated
+	// to branch name when we add source-driven canary/ bluegreen deployments.
+
+	// For pinning to an exact container image, see ImageWithDigest
+	return image + ":latest", nil
+}
+
 // contentiousFiles are files which, if extant, preclude the creation of a
 // function rooted in the given directory.
 var contentiousFiles = []string{
diff --git a/hack/tekton.sh b/hack/tekton.sh
new file mode 100755
index 00000000..824b5b4f
--- /dev/null
+++ b/hack/tekton.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+
+# 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.
+
+#
+# Install Tekton and required tasks in the cluster
+#
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+export TERM="${TERM:-dumb}"
+
+tekton_release="previous/v0.38.3"
+git_clone_release="0.4"
+source_path="https://raw.githubusercontent.com/knative-sandbox/kn-plugin-func/main"
+namespace="${NAMESPACE:-default}"
+
+tekton() {
+  echo "Installing Tekton..."
+  kubectl apply -f https://storage.googleapis.com/tekton-releases/pipeline/${tekton_release}/release.yaml
+  sleep 10
+  kubectl wait pod --for=condition=Ready --timeout=180s -n tekton-pipelines -l "app=tekton-pipelines-controller"
+
+  kubectl create clusterrolebinding ${namespace}:knative-serving-namespaced-admin \
+  --clusterrole=knative-serving-namespaced-admin  --serviceaccount=${namespace}:default
+}
+
+tekton_tasks() {
+  echo "Creating Pipeline tasks..."
+  kubectl apply -f https://raw.githubusercontent.com/tektoncd/catalog/master/task/git-clone/${git_clone_release}/git-clone.yaml
+  kubectl apply -f ${source_path}/pipelines/resources/tekton/task/func-buildpacks/0.1/func-buildpacks.yaml
+  kubectl apply -f ${source_path}/pipelines/resources/tekton/task/func-deploy/0.1/func-deploy.yaml
+}
+
+tekton
+tekton_tasks
+
+echo Done


Test patch:
diff --git a/.github/workflows/test-e2e-oncluster-runtime.yaml b/.github/workflows/test-e2e-oncluster-runtime.yaml
new file mode 100644
index 00000000..8355fd4b
--- /dev/null
+++ b/.github/workflows/test-e2e-oncluster-runtime.yaml
@@ -0,0 +1,29 @@
+name: Func E2E OnCluster RT Test
+
+on: [pull_request]
+
+jobs:
+  test:
+    name: On Cluster RT Test
+    strategy:
+      matrix:
+        go: [1.17.x]
+        os: ["ubuntu-latest"]
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-go@v2
+        with:
+          go-version: ${{ matrix.go }}
+      - name: Install Binaries
+        run: ./hack/binaries.sh
+      - name: Allocate Cluster
+        run: ./hack/allocate.sh
+      - name: Deploy Tekton
+        run: ./hack/tekton.sh
+      - name: Deploy Test Git Server
+        run: ./test/gitserver.sh
+      - name: E2E On Cluster Test (Runtimes)
+        env:
+          TEST_TAGS: runtime
+        run: make && make test-e2e-on-cluster
diff --git a/.github/workflows/test-e2e-oncluster.yaml b/.github/workflows/test-e2e-oncluster.yaml
new file mode 100644
index 00000000..d9cc660f
--- /dev/null
+++ b/.github/workflows/test-e2e-oncluster.yaml
@@ -0,0 +1,29 @@
+name: Func E2E OnCluster Test
+
+on: [pull_request]
+
+jobs:
+  test:
+    name: On Cluster Test
+    strategy:
+      matrix:
+        go: [1.17.x]
+        os: ["ubuntu-latest"]
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-go@v2
+        with:
+          go-version: ${{ matrix.go }}
+      - name: Install Binaries
+        run: ./hack/binaries.sh
+      - name: Allocate Cluster
+        run: ./hack/allocate.sh
+      - name: Deploy Tekton
+        run: ./hack/tekton.sh
+      - name: Deploy Test Git Server
+        run: ./test/gitserver.sh
+      - name: E2E On Cluster Test
+        env:
+          E2E_RUNTIMES: ""
+        run: make && make test-e2e-on-cluster
diff --git a/client_test.go b/client_test.go
index 26566cea..f61ac95e 100644
--- a/client_test.go
+++ b/client_test.go
@@ -365,10 +365,9 @@ func TestClient_New_RegistryRequired(t *testing.T) {
 	}
 }
 
-// TestClient_New_ImageNameDerived ensures that the full image (tag) of the resultant OCI
-// container is populated based of a derivation using configured registry
-// plus the service name.
-func TestClient_New_ImageNameDerived(t *testing.T) {
+// TestClient_New_ImageNamePopulated ensures that the full image (tag) of the
+// resultant OCI container is populated.
+func TestClient_New_ImageNamePopulated(t *testing.T) {
 	// Create the root function directory
 	root := "testdata/example.com/testDeriveImage"
 	defer Using(t, root)()
@@ -385,10 +384,16 @@ func TestClient_New_ImageNameDerived(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	// In form: [Default Registry]/[Registry Namespace]/[Service Name]:latest
-	expected := TestRegistry + "/" + f.Name + ":latest"
-	if f.Image != expected {
-		t.Fatalf("expected image '%v' got '%v'", expected, f.Image)
+	// This opaque-box unit test ensures NewImageTag is invoked and applied.
+	// See the Test_NewImageTag clear-box unit test for an in-depth exploration of
+	// how the values of image and registry are treated to create, by default:
+	//   [Default Registry]/[Registry Namespace]/[Service Name]:latest
+	imageTag, err := f.ImageName()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if f.Image != imageTag {
+		t.Fatalf("expected image '%v' got '%v'", imageTag, f.Image)
 	}
 }
 
@@ -680,6 +685,71 @@ func TestClient_Update(t *testing.T) {
 	}
 }
 
+// TestClient_Deploy_RegistryUpdate ensures that deploying a Function updates
+// its image member on initial deploy, and on subsequent deploys only
+// if reset to it zero value.
+func TestClient_Deploy_RegistryUpdate(t *testing.T) {
+	root, rm := Mktemp(t)
+	defer rm()
+	client := fn.New(fn.WithRegistry("example.com/alice"))
+
+	// New runs build and deploy, thus the initial instantiation should result in
+	// the member being populated from the client's registry and function name.
+	if err := client.New(context.Background(), fn.Function{Runtime: "go", Name: "f", Root: root}); err != nil {
+		t.Fatal(err)
+	}
+	f, err := fn.NewFunction(root)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if f.Image != "example.com/alice/f:latest" {
+		t.Error("image name was not initially set")
+	}
+
+	// Updating the registry and performing a subsequent update should not result
+	// in the image member being updated to the new value: registry is only used
+	// when calculating a nonexistent value
+	f.Registry = "example.com/bob"
+	if err := f.Write(); err != nil {
+		t.Fatal(err)
+	}
+	if err := client.Build(context.Background(), root); err != nil {
+		t.Fatal(err)
+	}
+	if err := client.Deploy(context.Background(), root); err != nil {
+		t.Fatal(err)
+	}
+	expected := "example.com/alice/f:latest"
+	f, err = fn.NewFunction(root) // reload and check
+	if err != nil {
+		t.Fatal(err)
+	}
+	if f.Image != expected { // NOT changed to bob
+		t.Errorf("expected image name to stay '%v' and not be updated, but got '%v'", expected, f.Image)
+	}
+
+	// Reset the value of .Image to default "" and ensure this triggers recalc.
+	f.Image = ""
+	f.Registry = "example.com/bob"
+	if err := f.Write(); err != nil {
+		t.Fatal(err)
+	}
+	if err := client.Build(context.Background(), root); err != nil {
+		t.Fatal(err)
+	}
+	if err := client.Deploy(context.Background(), root); err != nil {
+		t.Fatal(err)
+	}
+	expected = "example.com/bob/f:latest"
+	f, err = fn.NewFunction(root)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if f.Image != expected { // DOES change to bob
+		t.Errorf("expected image name to stay '%v' and not be updated, but got '%v'", expected, f.Image)
+	}
+}
+
 // TestClient_Remove_ByPath ensures that the remover is invoked to remove
 // the function with the name of the function at the provided root.
 func TestClient_Remove_ByPath(t *testing.T) {
@@ -906,6 +976,83 @@ func TestClient_List_OutsideRoot(t *testing.T) {
 	}
 }
 
+// TestClient_Deploy_Image ensures that initilally the function's image
+// member has no value (not initially deployed); the value is populated
+// upon deployment with a value derived from the function's name and currently
+// effective client registry; that the value of f.Image will take precidence
+// over .Registry, which is used to calculate a default value for image.
+func TestClient_Deploy_Image(t *testing.T) {
+	root, rm := Mktemp(t)
+	defer rm()
+
+	client := fn.New(
+		fn.WithBuilder(mock.NewBuilder()),
+		fn.WithDeployer(mock.NewDeployer()),
+		fn.WithRegistry("example.com/alice"))
+
+	err := client.Create(fn.Function{Name: "myfunc", Runtime: "go", Root: root})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	f, err := fn.NewFunction(root)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Upon initial creation, the value of .Image is empty
+	if f.Image != "" {
+		t.Fatalf("new function should have no image, got '%v'", f.Image)
+	}
+
+	// Upon deployment, the function should be populated;
+	if err = client.Build(context.Background(), root); err != nil {
+		t.Fatal(err)
+	}
+	if err = client.Deploy(context.Background(), root); err != nil {
+		t.Fatal(err)
+	}
+	f, err = fn.NewFunction(root)
+	if err != nil {
+		t.Fatal(err)
+	}
+	expected := "example.com/alice/myfunc:latest"
+	if f.Image != expected {
+		t.Fatalf("expected image '%v', got '%v'", expected, f.Image)
+	}
+	expected = "example.com/alice"
+	if f.Registry != "example.com/alice" {
+		t.Fatalf("expected registry '%v', got '%v'", expected, f.Registry)
+	}
+
+	// The value of .Image always takes precidence
+	f.Image = "registry2.example.com/bob/myfunc:latest"
+	if err = f.Write(); err != nil {
+		t.Fatal(err)
+	}
+	if err = client.Build(context.Background(), root); err != nil {
+		t.Fatal(err)
+	}
+	if err = client.Deploy(context.Background(), root); err != nil {
+		t.Fatal(err)
+	}
+	expected = "registry2.example.com/bob/myfunc:latest"
+	if f.Image != expected {
+		t.Fatalf("expected image '%v', got '%v'", expected, f.Image)
+	}
+	expected = "example.com/alice"
+	if f.Registry != "example.com/alice" {
+		// Note that according to current logic, the function's defined registry
+		// may be inaccurate.  Consider an initial deploy to registryA, followed by
+		// an explicit mutaiton of the function's .Image member.
+		// This could either remain as a documented nuance:
+		//   'The value of f.Registry is only used in the event an image name
+		//    need be derived (f.Image =="")
+		// Or we could update .Registry to always be in sync by parsing the .Image
+		t.Fatalf("expected registry '%v', got '%v'", expected, f.Registry)
+	}
+}
+
 // TestClient_Deploy_UnbuiltErrors ensures that a call to deploy a function which was not
 // fully created (ie. was only initialized, not actually built and deploys)
 // yields an expected, and informative, error.
diff --git a/cmd/build_test.go b/cmd/build_test.go
index 78af8fe9..ee6a53be 100644
--- a/cmd/build_test.go
+++ b/cmd/build_test.go
@@ -1,15 +1,11 @@
 package cmd
 
 import (
-	"context"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"testing"
 
-	"github.com/spf13/cobra"
 	fn "knative.dev/kn-plugin-func"
-	"knative.dev/kn-plugin-func/builders"
 	"knative.dev/kn-plugin-func/mock"
 	. "knative.dev/kn-plugin-func/testing"
 )
@@ -50,79 +46,41 @@ func TestBuild_ImageFlag(t *testing.T) {
 	}
 }
 
-// TestBuild_InvalidRegistry ensures that running build specifying the name of the
-// registry explicitly as an argument invokes the registry validation code.
-func TestBuild_InvalidRegistry(t *testing.T) {
-	var (
-		args    = []string{"--registry", "foo/bar/foobar/boofar"} // provide an invalid registry name
-		builder = mock.NewBuilder()                               // with a mock builder
-	)
-
-	// Run this test in a temporary directory
-	defer Fromtemp(t)()
-	// Write a func.yaml config which does not specify an image
-	funcYaml := `name: testymctestface
-namespace: ""
-runtime: go
-image: ""
-imageDigest: ""
-builder: quay.io/boson/faas-go-builder
-builders:
-  default: quay.io/boson/faas-go-builder
-envs: []
-annotations: {}
-labels: []
-created: 2021-01-01T00:00:00+00:00
-`
-	if err := ioutil.WriteFile("func.yaml", []byte(funcYaml), 0600); err != nil {
-		t.Fatal(err)
-	}
-
-	// Create build command that will use a mock builder.
-	cmd := NewBuildCmd(NewClientFactory(func() *fn.Client {
-		return fn.New(fn.WithBuilder(builder))
-	}))
+// TestBuild_RegistryOrImageRequired ensures that when no registry or image are
+// provided, and the client has not been instantiated with a default registry,
+// an ErrRegistryRequired is received.
+func TestBuild_RegistryOrImageRequired(t *testing.T) {
+	testRegistryOrImageRequired(NewBuildCmd, t)
+}
 
-	// Execute the command
-	cmd.SetArgs(args)
-	err := cmd.Execute()
-	if err == nil {
-		t.Fatal("Expected error")
-	}
+// TestBuild_ImageAndRegistry
+func TestBuild_ImageAndRegistry(t *testing.T) {
+	testRegistryOrImageRequired(NewBuildCmd, t)
 }
 
-// TestBuild_registryConfigurationInYaml tests that a build will execute successfully
-// when there is no registry provided on the command line, but one exists in func.yaml
-func TestBuild_registryConfigurationInYaml(t *testing.T) {
-	var (
-		builder = mock.NewBuilder() // with a mock builder
-	)
+// TestBuild_InvalidRegistry ensures that providing an invalid resitry
+// fails with the expected error.
+func TestBuild_InvalidRegistry(t *testing.T) {
+	testInvalidRegistry(NewBuildCmd, t)
+}
 
-	// Run this test in a temporary directory
-	defer Fromtemp(t)()
-	// Write a func.yaml config which does not specify an image
-	// but does specify a registry
-	funcYaml := `name: registrytest
-namespace: ""
-runtime: go
-image: ""
-registry: quay.io/boson/foo
-created: 2021-01-01T00:00:00+00:00
-`
-	if err := ioutil.WriteFile("func.yaml", []byte(funcYaml), 0600); err != nil {
-		t.Fatal(err)
-	}
+// TestBuild_RegistryLoads ensures that a function with a defined registry
+// will use this when recalculating .Image on build when no --image is
+// explicitly provided.
+func TestBuild_RegistryLoads(t *testing.T) {
+	testRegistryLoads(NewBuildCmd, t)
+}
 
-	// Create build command that will use a mock builder.
-	cmd := NewBuildCmd(NewClientFactory(func() *fn.Client {
-		return fn.New(fn.WithBuilder(builder), fn.WithRegistry("quay.io/boson/foo"))
-	}))
+// TestBuild_BuilderPersists ensures that the builder chosen is read from
+// the function by default, and is able to be overridden by flags/env vars.
+func TestBuild_BuilderPersists(t *testing.T) {
+	testBuilderPersists(NewBuildCmd, t)
+}
 
-	// Execute the command
-	err := cmd.Execute()
-	if err != nil {
-		t.Fatal(err)
-	}
+// TestBuild_ValidateBuilder ensures that the validation function correctly
+// identifies valid and invalid builder short names.
+func TestBuild_BuilderValidated(t *testing.T) {
+	testBuilderValidated(NewBuildCmd, t)
 }
 
 func TestBuild_runBuild(t *testing.T) {
@@ -223,119 +181,3 @@ created: 2009-11-10 23:00:00`,
 		})
 	}
 }
-
-func testBuilderPersistence(t *testing.T, testRegistry string, cmdBuilder func(ClientFactory) *cobra.Command) {
-	//add this to work with all other tests in deploy_test.go
-	defer WithEnvVar(t, "KUBECONFIG", fmt.Sprintf("%s/testdata/kubeconfig_deploy_namespace", cwd()))()
-
-	root, rm := Mktemp(t)
-	defer rm()
-
-	client := fn.New(fn.WithRegistry(testRegistry))
-
-	f := fn.Function{Runtime: "go", Root: root, Name: "myfunc", Registry: testRegistry}
-
-	if err := client.New(context.Background(), f); err != nil {
-		t.Fatal(err)
-	}
-
-	cmd := cmdBuilder(NewClientFactory(func() *fn.Client {
-		return client
-	}))
-
-	cmd.SetArgs([]string{"--registry", testRegistry})
-	if err := cmd.Execute(); err != nil {
-		t.Fatal(err)
-	}
-
-	var err error
-	// Assert the function has persisted a value of builder (has a default)
-	if f, err = fn.NewFunction(root); err != nil {
-		t.Fatal(err)
-	}
-	if f.Builder == "" {
-		t.Fatal("value of builder not persisted using a flag default")
-	}
-
-	// Build the function, specifying a Builder
-	cmd.SetArgs([]string{"--builder=s2i"})
-	if err := cmd.Execute(); err != nil {
-		t.Fatal(err)
-	}
-	// Assert the function has persisted the value of builder
-	if f, err = fn.NewFunction(root); err != nil {
-		t.Fatal(err)
-	}
-	if f.Builder != builders.S2I {
-		t.Fatal("value of builder flag not persisted when provided")
-	}
-	// Build the function without specifying a Builder
-	cmd = cmdBuilder(NewClientFactory(func() *fn.Client {
-		return client
-	}))
-	cmd.SetArgs([]string{"--registry", testRegistry})
-
-	if err := cmd.Execute(); err != nil {
-		t.Fatal(err)
-	}
-
-	// Assert the function has retained its original value
-	if f, err = fn.NewFunction(root); err != nil {
-		t.Fatal(err)
-	}
-
-	if f.Builder != builders.S2I {
-		t.Fatal("value of builder updated when not provided")
-	}
-
-	// Build the function again using a different builder
-	cmd.SetArgs([]string{"--builder=pack"})
-	if err := cmd.Execute(); err != nil {
-		t.Fatal(err)
-	}
-
-	// Assert the function has persisted the new value
-	if f, err = fn.NewFunction(root); err != nil {
-		t.Fatal(err)
-	}
-	if f.Builder != builders.Pack {
-		t.Fatal("value of builder flag not persisted on subsequent build")
-	}
-
-	// Build the function, specifying a platform with "pack" Builder
-	cmd.SetArgs([]string{"--platform", "linux"})
-	if err := cmd.Execute(); err == nil {
-		t.Fatal("Expected error")
-	}
-
-	// Set an invalid builder
-	cmd.SetArgs([]string{"--builder", "invalid"})
-	if err := cmd.Execute(); err == nil {
-		t.Fatal("Expected error")
-	}
-}
-
-// TestBuild_BuilderPersistence ensures that the builder chosen is read from
-// the function by default, and is able to be overridden by flags/env vars.
-func TestBuild_BuilderPersistence(t *testing.T) {
-	testBuilderPersistence(t, "docker.io/tigerteam", NewBuildCmd)
-}
-
-// TestBuild_ValidateBuilder ensures that the validation function correctly
-// identifies valid and invalid builder short names.
-func Test_ValidateBuilder(t *testing.T) {
-	for _, name := range builders.All() {
-		if err := ValidateBuilder(name); err != nil {
-			t.Fatalf("expected builder '%v' to be valid, but got error: %v", name, err)
-		}
-	}
-
-	// This CLI creates no builders beyond those in the core reposiory.  Other
-	// users of the client library may provide their own named implementation of
-	// the fn.Builder interface. Those would have a different set of valid
-	// builders.
-
-	if err := ValidateBuilder("invalid"); err == nil {
-		t.Fatalf("did not get expected error validating an invalid builder name")
-	}
-}
diff --git a/cmd/deploy_test.go b/cmd/deploy_test.go
index 996fb346..a4c01cb7 100644
--- a/cmd/deploy_test.go
+++ b/cmd/deploy_test.go
@@ -2,17 +2,350 @@ package cmd
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
 	"testing"
 
 	"github.com/ory/viper"
+	"github.com/spf13/cobra"
 	"k8s.io/utils/pointer"
 	fn "knative.dev/kn-plugin-func"
+	"knative.dev/kn-plugin-func/builders"
 	"knative.dev/kn-plugin-func/mock"
 	. "knative.dev/kn-plugin-func/testing"
 )
 
+const TestRegistry = "example.com/alice"
+
+type commandConstructor func(ClientFactory) *cobra.Command
+
+// TestDeploy_RegistryOrImageRequired ensures that when no registry or image are
+// provided (or exist on the function already), and the client has not been
+// instantiated with a default registry, an ErrRegistryRequired is received.
+func TestDeploy_RegistryOrImageRequired(t *testing.T) {
+	testRegistryOrImageRequired(NewDeployCmd, t) // shared with build
+}
+
+func testRegistryOrImageRequired(cmdFn commandConstructor, t *testing.T) {
+	t.Helper()
+	root, rm := Mktemp(t)
+	defer rm()
+
+	if err := fn.New().Create(fn.Function{Runtime: "go", Root: root}); err != nil {
+		t.Fatal(err)
+	}
+
+	cmd := cmdFn(NewClientFactory(func() *fn.Client {
+		return fn.New()
+	}))
+
+	// If neither --registry nor --image are provided, and the client was not
+	// instantiated with a default registry, a ErrRegistryRequired is expected.
+	cmd.SetArgs([]string{}) // this explicit clearing of args may not be necessary
+	if err := cmd.Execute(); err != nil {
+		if !errors.Is(err, ErrRegistryRequired) {
+			t.Fatalf("expected ErrRegistryRequired, got error: %v", err)
+		}
+	}
+
+	// earlire test covers the --registry only case, test here that --image
+	// also succeeds.
+	cmd.SetArgs([]string{"--image=example.com/alice/myfunc"})
+	if err := cmd.Execute(); err != nil {
+		t.Fatal(err)
+	}
+}
+
+// TestBuild_ImageAndRegistry ensures that image is derived when --registry
+// is provided without --image; that --image is used if provided; that when
+// both are provided, they are both passed to the deployer; and that these
+// values are persisted.
+func TestDeploy_ImageAndRegistry(t *testing.T) {
+	testImageAndRegistry(NewDeployCmd, t)
+}
+
+func testImageAndRegistry(cmdFn commandConstructor, t *testing.T) {
+	t.Helper()
+	root, rm := Mktemp(t)
+	defer rm()
+
+	if err := fn.New().Create(fn.Function{Runtime: "go", Root: root}); err != nil {
+		t.Fatal(err)
+	}
+
+	var (
+		builder  = mock.NewBuilder()
+		deployer = mock.NewDeployer()
+		cmd      = cmdFn(NewClientFactory(func() *fn.Client {
+			return fn.New(
+				fn.WithBuilder(builder),
+				fn.WithDeployer(deployer),
+				fn.WithRegistry(TestRegistry))
+		}))
+	)
+
+	// If only --registry is provided:
+	// the resultant Function should have the registry populated and image
+	// derived from the name.
+	cmd.SetArgs([]string{"--registry=example.com/alice"})
+	deployer.DeployFn = func(f fn.Function) error {
+		if f.Registry != "example.com/alice" {
+			t.Fatal("registry flag not provided to deployer")
+		}
+		return nil
+	}
+	if err := cmd.Execute(); err != nil {
+		t.Fatal(err)
+	}
+
+	// If only --image is provided:
+	// the deploy should not fail, and the resultant Function should have the
+	// Image member set to what was explicitly provided via the --image flag
+	// (not a derived name)
+	cmd.SetArgs([]string{"--image=example.com/alice/myfunc"})
+	deployer.DeployFn = func(f fn.Function) error {
+		if f.Image != "example.com/alice/myfunc" {
+			t.Fatalf("deployer expected f.Image 'example.com/alice/myfunc', got '%v'", f.Image)
+		}
+		return nil
+	}
+	if err := cmd.Execute(); err != nil {
+		t.Fatal(err)
+	}
+
+	// If both --registry and --image are provided:
+	// they should both be plumbed through such that downstream agents (deployer
+	// in this case) see them set on the Function and can act accordingly.
+	cmd.SetArgs([]string{"--registry=example.com/alice", "--image=example.com/alice/subnamespace/myfunc"})
+	deployer.DeployFn = func(f fn.Function) error {
+		if f.Registry != "example.com/alice" {
+			t.Fatal("registry flag value not seen on the Function by the deployer")
+		}
+		if f.Image != "example.com/alice/subnamespace/myfunc" {
+			t.Fatal("image flag value not seen on the Function by deployer")
+		}
+		return nil
+	}
+	if err := cmd.Execute(); err != nil {
+		t.Fatal(err)
+	}
+
+	// TODO it may be cleaner to error if both registry and image are provided,
+	// allowing deployer implementations to avoid arbitration logic.
+}
+
+// TestDepoy_InvalidRegistry ensures that providing an invalid registry
+// fails with the expected error.
+func TestDeploy_InvalidRegistry(t *testing.T) {
+	testInvalidRegistry(NewDeployCmd, t)
+}
+
+func testInvalidRegistry(cmdFn commandConstructor, t *testing.T) {
+	t.Helper()
+	root, rm := Mktemp(t)
+	defer rm()
+
+	f := fn.Function{
+		Root:    root,
+		Name:    "myFunc",
+		Runtime: "go",
+	}
+	if err := fn.New().Create(f); err != nil {
+		t.Fatal(err)
+	}
+
+	cmd := cmdFn(NewClientFactory(func() *fn.Client {
+		return fn.New()
+	}))
+
+	cmd.SetArgs([]string{"--registry=foo/bar/invald/myfunc"})
+
+	if err := cmd.Execute(); err == nil {
+		// TODO: typed ErrInvalidRegistry
+		t.Fatal("invalid registry did not generate expected error")
+	}
+}
+
+// TestDeploy_RegistryLoads ensures that a function with a defined registry
+// will use this when recalculating .Image on deploy when no --image is
+// explicitly provided.
+func TestDeploy_RegistryLoads(t *testing.T) {
+	testRegistryLoads(NewDeployCmd, t)
+}
+
+func testRegistryLoads(cmdFn commandConstructor, t *testing.T) {
+	t.Helper()
+	root, rm := Mktemp(t)
+	defer rm()
+
+	f := fn.Function{
+		Root:     root,
+		Name:     "myFunc",
+		Runtime:  "go",
+		Registry: "example.com/alice",
+	}
+	if err := fn.New().Create(f); err != nil {
+		t.Fatal(err)
+	}
+
+	cmd := cmdFn(NewClientFactory(func() *fn.Client {
+		return fn.New(
+			fn.WithBuilder(mock.NewBuilder()),
+			fn.WithDeployer(mock.NewDeployer()))
+	}))
+	cmd.SetArgs([]string{})
+
+	if err := cmd.Execute(); err != nil {
+		t.Fatal(err)
+	}
+
+	f, err := fn.NewFunction(root)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if f.Image != "example.com/alice/myFunc:latest" {
+		t.Fatalf("unexpected image name: %v", f.Image)
+	}
+}
+
+// TestDeploy_BuilderPersists ensures that the builder chosen is read from
+// the function by default, and is able to be overridden by flags/env vars.
+func TestDeploy_BuilderPersists(t *testing.T) {
+	testBuilderPersists(NewDeployCmd, t)
+}
+
+func testBuilderPersists(cmdFn commandConstructor, t *testing.T) {
+	t.Helper()
+	root, rm := Mktemp(t)
+	defer rm()
+
+	if err := fn.New().Create(fn.Function{Runtime: "go", Root: root}); err != nil {
+		t.Fatal(err)
+	}
+	cmd := cmdFn(NewClientFactory(func() *fn.Client {
+		return fn.New(fn.WithRegistry(TestRegistry))
+	}))
+	cmd.SetArgs([]string{})
+	if err := cmd.Execute(); err != nil {
+		t.Fatal(err)
+	}
+
+	var err error
+	var f fn.Function
+
+	// Assert the function has persisted a value of builder (has a default)
+	if f, err = fn.NewFunction(root); err != nil {
+		t.Fatal(err)
+	}
+	if f.Builder == "" {
+		t.Fatal("value of builder not persisted using a flag default")
+	}
+
+	// Build the function, specifying a Builder
+	viper.Reset()
+	cmd.SetArgs([]string{"--builder=s2i"})
+	if err := cmd.Execute(); err != nil {
+		t.Fatal(err)
+	}
+	// Assert the function has persisted the value of builder
+	if f, err = fn.NewFunction(root); err != nil {
+		t.Fatal(err)
+	}
+	if f.Builder != builders.S2I {
+		t.Fatal("value of builder flag not persisted when provided")
+	}
+
+	// Build the function again without specifying a Builder
+	viper.Reset()
+	cmd.SetArgs([]string{})
+	if err := cmd.Execute(); err != nil {
+		t.Fatal(err)
+	}
+
+	// Assert the function has retained its original value
+	// (was not reset back to a flag default)
+	if f, err = fn.NewFunction(root); err != nil {
+		t.Fatal(err)
+	}
+	if f.Builder != builders.S2I {
+		t.Fatal("value of builder updated when not provided")
+	}
+
+	// Build the function again using a different builder
+	viper.Reset()
+	cmd.SetArgs([]string{"--builder=pack"})
+	if err := cmd.Execute(); err != nil {
+		t.Fatal(err)
+	}
+
+	// Assert the function has persisted the new value
+	if f, err = fn.NewFunction(root); err != nil {
+		t.Fatal(err)
+	}
+	if f.Builder != builders.Pack {
+		t.Fatal("value of builder flag not persisted on subsequent build")
+	}
+
+	// Build the function, specifying a platform with "pack" Builder
+	cmd.SetArgs([]string{"--platform", "linux"})
+	if err := cmd.Execute(); err == nil {
+		t.Fatal("Expected error")
+	}
+
+	// Set an invalid builder
+	cmd.SetArgs([]string{"--builder", "invalid"})
+	if err := cmd.Execute(); err == nil {
+		t.Fatal("Expected error")
+	}
+}
+
+// TestDeploy_BuilderValidated ensures that the validation function correctly
+// identifies valid and invalid builder short names.
+func TestDeploy_BuilderValidated(t *testing.T) {
+	testBuilderValidated(NewDeployCmd, t)
+}
+
+func testBuilderValidated(cmdFn commandConstructor, t *testing.T) {
+	t.Helper()
+	root, rm := Mktemp(t)
+	defer rm()
+
+	if err := fn.New().Create(fn.Function{Runtime: "go", Root: root}); err != nil {
+		t.Fatal(err)
+	}
+
+	cmd := cmdFn(NewClientFactory(func() *fn.Client {
+		return fn.New()
+	}))
+
+	cmd.SetArgs([]string{"--builder=invalid"})
+	if err := cmd.Execute(); err == nil {
+		t.Fatalf("expected error string '%v', got '%v'", "expected", err.Error())
+	}
+
+}
+
+// TestDeploy_ValidateBuilder tests that the ValidateBuilder validator
+// accepts === the set of known builders.
+func Test_ValidateBuilder(t *testing.T) {
+	for _, name := range builders.All() {
+		if err := ValidateBuilder(name); err != nil {
+			t.Fatalf("expected builder '%v' to be valid, but got error: %v", name, err)
+		}
+	}
+
+	// This CLI creates no builders beyond those in the core reposiory.  Other
+	// users of the client library may provide their own named implementation of
+	// the fn.Builder interface. Those would have a different set of valid
+	// builders.
+
+	if err := ValidateBuilder("invalid"); err == nil {
+		t.Fatalf("did not get expected error validating an invalid builder name")
+	}
+}
+
 func Test_runDeploy(t *testing.T) {
 	tests := []struct {
 		name                 string
@@ -275,10 +608,76 @@ runtime: go`,
 	}
 }
 
-// TestDeploy_BuilderPersistence ensures that the builder chosen is read from
-// the function by default, and is able to be overridden by flags/env vars.
-func TestDeploy_BuilderPersistence(t *testing.T) {
-	testBuilderPersistence(t, "docker.io/tigerteam", NewDeployCmd)
+func Test_checkNamespaceDeploy(t *testing.T) {
+	tests := []struct {
+		name          string
+		confNamespace string
+		funcNamespace string
+		context       bool
+		expected      string
+		expectedError string
+	}{
+		{
+			name: "defaults and no context returns empty with no error",
+		},
+		{
+			name:     "defaults with a context returns the context",
+			context:  true,
+			expected: "test-ns-deploy", // from ./testdata
+		},
+		{
+			name:          "extant value takes precidence when none provided",
+			confNamespace: "",
+			funcNamespace: "last-deploy-value",
+			context:       true,
+			expected:      "last-deploy-value",
+		},
+		{
+			name:          "config values take precidence",
+			confNamespace: "flag-value",
+			funcNamespace: "last-deploy-value",
+			context:       true,
+			expected:      "flag-value",
+		},
+	}
+
+	// contains a kube config with active namespace "test-ns-deploy"
+	contextPath := fmt.Sprintf("%s/testdata/kubeconfig_deploy_namespace", cwd())
+
+	defer WithEnvVar(t, "KUBECONFIG", contextPath)()
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+
+			root, rm := Mktemp(t)
+			defer rm()
+
+			if test.context {
+				defer WithEnvVar(t, "KUBECONFIG", contextPath)()
+			} else {
+				defer WithEnvVar(t, "KUBECONFIG", cwd())()
+			}
+
+			f := fn.Function{
+				Runtime:   "go",
+				Root:      root,
+				Namespace: test.funcNamespace,
+			}
+
+			if err := fn.New().Create(f); err != nil {
+				t.Fatal(err)
+			}
+
+			ns, err := checkNamespaceDeploy(test.funcNamespace, test.confNamespace)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if ns != test.expected {
+				t.Errorf("expected namespace '%v' got '%v'", test.expected, ns)
+			}
+
+		})
+	}
 }
 
 func Test_namespaceCheck(t *testing.T) {
diff --git a/function_unit_test.go b/function_unit_test.go
index b18ed56c..b433ac1c 100644
--- a/function_unit_test.go
+++ b/function_unit_test.go
@@ -5,8 +5,6 @@ package function
 
 import (
 	"testing"
-
-	. "knative.dev/kn-plugin-func/testing"
 )
 
 func TestFunction_ImageWithDigest(t *testing.T) {
@@ -48,91 +46,39 @@ func TestFunction_ImageWithDigest(t *testing.T) {
 	}
 }
 
-func Test_DerivedImage(t *testing.T) {
+// TestFunction_ImageName ensures that the full image name is
+// returned for a Function, based on the Function's Registry and Name,
+// including utilizing the DefaultRegistry if the Function's defined
+// registry is a single token (just the namespace).
+func TestFunction_ImageName(t *testing.T) {
+	var (
+		f   Function
+		got string
+		err error
+	)
 	tests := []struct {
-		name     string
-		fnName   string
-		image    string
-		registry string
-		want     string
+		registry      string
+		name          string
+		expectedImage string
+		expectError   bool
 	}{
-		{
-			name:     "No change",
-			fnName:   "testDerivedImage",
-			image:    "docker.io/foo/testDerivedImage:latest",
-			registry: "docker.io/foo",
-			want:     "docker.io/foo/testDerivedImage:latest",
-		},
-		{
-			name:     "Same registry without docker.io/, original with docker.io/",
-			fnName:   "testDerivedImage0",
-			image:    "docker.io/foo/testDerivedImage0:latest",
-			registry: "foo",
-			want:     "docker.io/foo/testDerivedImage0:latest",
-		},
-		{
-			name:     "Same registry, original without docker.io/",
-			fnName:   "testDerivedImage1",
-			image:    "foo/testDerivedImage1:latest",
-			registry: "foo",
-			want:     "docker.io/foo/testDerivedImage1:latest",
-		},
-		{
-			name:     "Different registry without docker.io/, original without docker.io/",
-			fnName:   "testDerivedImage2",
-			image:    "foo/testDerivedImage2:latest",
-			registry: "bar",
-			want:     "docker.io/bar/testDerivedImage2:latest",
-		},
-		{
-			name:     "Different registry with docker.io/, original without docker.io/",
-			fnName:   "testDerivedImage3",
-			image:    "foo/testDerivedImage3:latest",
-			registry: "docker.io/bar",
-			want:     "docker.io/bar/testDerivedImage3:latest",
-		},
-		{
-			name:     "Different registry with docker.io/, original with docker.io/",
-			fnName:   "testDerivedImage4",
-			image:    "docker.io/foo/testDerivedImage4:latest",
-			registry: "docker.io/bar",
-			want:     "docker.io/bar/testDerivedImage4:latest",
-		},
-		{
-			name:     "Different registry with quay.io/, original without docker.io/",
-			fnName:   "testDerivedImage5",
-			image:    "foo/testDerivedImage5:latest",
-			registry: "quay.io/foo",
-			want:     "quay.io/foo/testDerivedImage5:latest",
-		},
-		{
-			name:     "Different registry with quay.io/, original with docker.io/",
-			fnName:   "testDerivedImage6",
-			image:    "docker.io/foo/testDerivedImage6:latest",
-			registry: "quay.io/foo",
-			want:     "quay.io/foo/testDerivedImage6:latest",
-		},
+		{"alice", "myfunc", DefaultRegistry + "/alice/myfunc:latest", false},
+		{"quay.io/alice", "myfunc", "quay.io/alice/myfunc:latest", false},
+		{"docker.io/alice", "myfunc", "docker.io/alice/myfunc:latest", false},
+		{"docker.io/alice/sub", "myfunc", "docker.io/alice/sub/myfunc:latest", false},
+		{"alice", "", "", true},
+		{"", "myfunc", "", true},
 	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-
-			root := "testdata/" + tt.fnName
-			defer Using(t, root)()
-
-			// write out the function
-			client := New()
-			err := client.Create(Function{Runtime: "go", Name: tt.fnName, Root: root})
-			if err != nil {
-				t.Fatal(err)
-			}
-
-			got, err := DerivedImage(root, tt.registry)
-			if err != nil {
-				t.Errorf("DerivedImage() for image %v and registry %v; got error %v", tt.image, tt.registry, err)
-			}
-			if got != tt.want {
-				t.Errorf("DerivedImage() for image %v and registry %v; got %v, want %v", tt.image, tt.registry, got, tt.want)
-			}
-		})
+	for _, test := range tests {
+		f = Function{Registry: test.registry, Name: test.name}
+		got, err = f.ImageName()
+		if test.expectError && err == nil {
+			t.Errorf("registry '%v' and name '%v' did not yield the expected error",
+				test.registry, test.name)
+		}
+		if got != test.expectedImage {
+			t.Errorf("expected registry '%v' name '%v' to yield image '%v', got '%v'",
+				test.registry, test.name, test.expectedImage, got)
+		}
 	}
 }
diff --git a/hack/test-integration-podman.sh b/hack/test-integration-podman.sh
index 3aefb78c..151f60b8 100755
--- a/hack/test-integration-podman.sh
+++ b/hack/test-integration-podman.sh
@@ -17,7 +17,7 @@ podman_pid=$!
 
 DOCKER_HOST="unix://$(podman info -f '{{.Host.RemoteSocket.Path}}' 2> /dev/null)"
 export DOCKER_HOST
-go test -tags integration ./... -v
+go test -test.timeout=15m -tags integration ./... -v
 e=$?
 
 kill -TERM "$podman_pid" > /dev/null 2>&1
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 00000000..a18edc1f
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,68 @@
+# Functions E2E Test
+
+## Lifecycle tests
+
+Lifecycle tests exercises the most important phases of a function lifecycle starting from
+creation, going thru to build, deployment, execution and then deletion (CRUD operations).
+It runs func commands such as `create`, `deploy`, `list` and `delete` for a language
+runtime using both default `http` and `cloudevents` templates.
+
+## Extended tests
+
+Extended tests performs additional tests on `func` such as templates, config envs, volumes, labels and
+other scenarios.
+
+## On Cluster Builds tests
+
+On cluster builds e2e tests exercises functions built directly on cluster.
+The tests are organized per scenarios under `./_oncluster` folder.
+
+### Pre-requisites
+
+Prior to run On Cluster builds e2e tests ensure you are connected to
+a Kubernetes Cluster with the following deployed:
+
+- Knative Serving
+- Tekton
+- Tekton Tasks listed [here](../docs/reference/on_cluster_build.md)
+- Embedded Git Server (`func-git`) used by tests
+
+For your convenience you can run the following script to setup Tekton and required Tasks:
+```
+$ ../hack/tekton.sh
+```
+
+To install the Git Server required by tests, run:
+```
+$ ./gitserver.sh
+```
+
+#### Running all the Tests on KinD
+
+The below instructions will run all the tests on KinD using an **ephemeral** container registry.
+```
+# Pre-Reqs
+./hack/allocate.sh
+./hack/tekton.sh
+./test/gitserver.sh
+make build
+
+# Run tests
+./test/e2e_oncluter_tests.sh
+```
+
+#### Running "runtime" only scenario
+
+You can run only e2e tests to exercise a given language/runtime, for example *python*
+
+```
+env E2E_RUNTIMES=python TEST_TAGS=runtime ./test/e2e_oncluster_test.sh
+```
+
+#### Running tests except "runtime" ones
+
+You can run most of on cluster builds e2e scenarios, except the language/runtime specific
+ones, by running:
+```
+env E2E_RUNTIMES="" ./test/e2e_oncluster_test.sh
+```
diff --git a/test/_common/cmd.go b/test/_common/cmd.go
new file mode 100644
index 00000000..00a82343
--- /dev/null
+++ b/test/_common/cmd.go
@@ -0,0 +1,115 @@
+package common
+
+import (
+	"bytes"
+	"os"
+	"os/exec"
+	"strings"
+	"testing"
+)
+
+type TestExecCmd struct {
+
+	// binary to invoke
+	// Example: "func", "kn", "kubectl", "/usr/bin/sh"
+	Binary string
+
+	// Binary args to append before actual args. Examples:
+	//  when 'kn' binary binaryArgs should be ["func"]
+	BinaryArgs []string
+
+	// Run commands from Dir
+	SourceDir string
+
+	// Indicates shell should dump command line args during execution
+	ShouldDumpCmdLine bool
+
+	// Indicates shell should dump
+	ShouldDumpOnSuccess bool
+
+	// Fail Test on Error
+	ShouldFailOnError bool
+
+	// Environment variable to be used with the command
+	Env []string
+
+	// Optional function to be used to dump stdout command results
+	DumpLogger func(out string)
+
+	// Boolean
+	T *testing.T
+}
+
+// TestExecCmdResult stored command result
+type TestExecCmdResult struct {
+	Stdout string
+	Stderr string
+	Error  error
+}
+
+func (f *TestExecCmd) WithEnv(envKey string, envValue string) *TestExecCmd {
+	env := envKey + "=" + envValue
+	f.Env = append(f.Env, env)
+	return f
+}
+
+func (f *TestExecCmd) FromDir(dir string) *TestExecCmd {
+	f.SourceDir = dir
+	return f
+}
+
+func (f *TestExecCmd) Run(oneArgs string) TestExecCmdResult {
+	args := strings.Split(oneArgs, " ")
+	return f.Exec(args...)
+}
+
+// Exec invokes go exec library and runs a shell command combining the binary args with args from method signature
+func (f *TestExecCmd) Exec(args ...string) TestExecCmdResult {
+	finalArgs := f.BinaryArgs
+	if finalArgs == nil {
+		finalArgs = args
+	} else if args != nil {
+		finalArgs = append(finalArgs, args...)
+	}
+
+	if f.ShouldDumpCmdLine {
+		f.T.Log(f.Binary, strings.Join(finalArgs, " "))
+	}
+
+	var stderr bytes.Buffer
+	var stdout bytes.Buffer
+
+	cmd := exec.Command(f.Binary, finalArgs...)
+	cmd.Stderr = &stderr
+	cmd.Stdout = &stdout
+	if f.SourceDir != "" {
+		cmd.Dir = f.SourceDir
+	}
+	cmd.Env = append(os.Environ(), f.Env...)
+	err := cmd.Run()
+
+	result := TestExecCmdResult{
+		Stdout: stdout.String(),
+		Stderr: stderr.String(),
+		Error:  err,
+	}
+
+	if err == nil && f.ShouldDumpOnSuccess {
+		if result.Stdout != "" {
+			if f.DumpLogger != nil {
+				f.DumpLogger(result.Stdout)
+			} else {
+				f.T.Logf("%v", result.Stdout)
+			}
+		}
+	}
+	if err != nil {
+		f.T.Log(err.Error())
+		f.T.Log(result.Stderr)
+		if f.ShouldFailOnError {
+			f.T.Fail()
+		}
+	}
+
+	return result
+}
diff --git a/test/_common/gitserver.go b/test/_common/gitserver.go
new file mode 100644
index 00000000..b52737b9
--- /dev/null
+++ b/test/_common/gitserver.go
@@ -0,0 +1,106 @@
+package common
+
+import (
+	"context"
+
+	"knative.dev/kn-plugin-func/k8s"
+	e2e "knative.dev/kn-plugin-func/test/_e2e"
+
+	"strings"
+	"testing"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+func GetGitServer(T *testing.T) GitProvider {
+	gitTestServer := GitTestServerProvider{}
+	gitTestServer.Init(T)
+	return &gitTestServer
+}
+
+type GitRemoteRepo struct {
+	RepoName         string
+	ExternalCloneURL string
+	ClusterCloneURL  string
+}
+
+type GitProvider interface {
+	Init(T *testing.T)
+	CreateRepository(repoName string) *GitRemoteRepo
+	DeleteRepository(repoName string)
+}
+
+// ------------------------------------------------------
+// Git Server on Kubernetes as Knative Service (func-git)
+// ------------------------------------------------------
+
+type GitTestServerProvider struct {
+	PodName    string
+	ServiceUrl string
+	Kubectl    *TestExecCmd
+	t          *testing.T
+}
+
+func (g *GitTestServerProvider) Init(T *testing.T) {
+
+	g.t = T
+	if g.PodName == "" {
+		config, err := k8s.GetClientConfig().ClientConfig()
+		if err != nil {
+			T.Fatal(err.Error())
+		}
+		clientSet, err := kubernetes.NewForConfig(config)
+		if err != nil {
+			T.Fatal(err.Error())
+		}
+		ctx := context.Background()
+
+		namespace, _, _ := k8s.GetClientConfig().Namespace()
+		podList, err := clientSet.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
+			LabelSelector: "serving.knative.dev/service=func-git",
+		})
+		if err != nil {
+			T.Fatal(err.Error())
+		}
+		for _, pod := range podList.Items {
+			g.PodName = pod.Name
+		}
+	}
+
+	if g.ServiceUrl == "" {
+		// Get Route Name
+		g.ServiceUrl = e2e.GetKnativeServiceUrl(T, "func-git")
+	}
+
+	if g.Kubectl == nil {
+		g.Kubectl = &TestExecCmd{
+			Binary:              "kubectl",
+			ShouldDumpCmdLine:   true,
+			ShouldDumpOnSuccess: true,
+			T:                   T,
+		}
+	}
+	T.Logf("Initialized HTTP Func Git Server: Server URL = %v Pod Name = %v\n", g.ServiceUrl, g.PodName)
+}
+
+func (g *GitTestServerProvider) CreateRepository(repoName string) *GitRemoteRepo {
+	// kubectl exec $podname -c user-container -- git-repo create $reponame
+	cmdResult := g.Kubectl.Exec("exec", g.PodName, "-c", "user-container", "--", "git-repo", "create", repoName)
+	if !strings.Contains(cmdResult.Stdout, "created") {
+		g.t.Fatal("unable to create git bare repository " + repoName)
+	}
+	gitRepo := &GitRemoteRepo{
+		RepoName:         repoName,
+		ExternalCloneURL: g.ServiceUrl + "/" + repoName + ".git",
+		ClusterCloneURL:  "http://func-git.default.svc.cluster.local/" + repoName + ".git",
+	}
+	return gitRepo
+}
+
+func (g *GitTestServerProvider) DeleteRepository(repoName string) {
+	cmdResult := g.Kubectl.Exec("exec", g.PodName, "-c", "user-container", "--", "git-repo", "delete", repoName)
+	if !strings.Contains(cmdResult.Stdout, "deleted") {
+		g.t.Fatal("unable to delete git bare repository " + repoName)
+	}
+}
diff --git a/test/_common/knfunc.go b/test/_common/knfunc.go
new file mode 100644
index 00000000..ca1d2ae8
--- /dev/null
+++ b/test/_common/knfunc.go
@@ -0,0 +1,30 @@
+package common
+
+import (
+	"testing"
+
+	e2e "knative.dev/kn-plugin-func/test/_e2e"
+)
+
+func NewKnFuncShellCli(t *testing.T) *TestExecCmd {
+	knFunc := TestExecCmd{}
+	knFunc.T = t
+
+	if e2e.IsUseKnFunc() {
+		knFunc.Binary = "kn"
+		knFunc.BinaryArgs = []string{"func"}
+	} else {
+		knFunc.Binary = e2e.GetFuncBinaryPath()
+		if knFunc.Binary == "" {
+			t.Log("'func' binary not defined. Please set E2E_FUNC_BIN_PATH environment variable prior to running tests")
+			t.FailNow()
+		}
+	}
+	cmd := knFunc.Exec()
+	if cmd.Error != nil {
+		t.FailNow()
+	}
+	knFunc.ShouldDumpCmdLine = true
+	knFunc.ShouldFailOnError = true
+	return &knFunc
+}
diff --git a/test/_common/shell.go b/test/_common/shell.go
new file mode 100644
index 00000000..2d6dc22f
--- /dev/null
+++ b/test/_common/shell.go
@@ -0,0 +1,18 @@
+package common
+
+import (
+	"testing"
+)
+
+func NewShellCmd(t *testing.T, fromDirectory string) *TestExecCmd {
+
+	shellCmd := TestExecCmd{
+		Binary:              "sh",
+		BinaryArgs:          []string{"-c"},
+		SourceDir:           fromDirectory,
+		ShouldDumpCmdLine:   true,
+		ShouldDumpOnSuccess: true,
+		T:                   t,
+	}
+	return &shellCmd
+}
diff --git a/test/_e2e/cleaner.go b/test/_e2e/cleaner.go
new file mode 100644
index 00000000..5aa9353a
--- /dev/null
+++ b/test/_e2e/cleaner.go
@@ -0,0 +1,29 @@
+package e2e
+
+import "strings"
+
+// CleanOutput Some commands, such as deploy command, spans spinner chars and cursor shifts at output which are captured and merged
+// regular output messages. This functions is meant to remove these chars in order to facilitate tests assertions and data extraction from output
+func CleanOutput(stdOutput string) string {
+	toRemove := []string{
+		"🕛 ",
+		"🕐 ",
+		"🕑 ",
+		"🕒 ",
+		"🕓 ",
+		"🕔 ",
+		"🕕 ",
+		"🕖 ",
+		"🕗 ",
+		"🕘 ",
+		"🕙 ",
+		"🕚 ",
+		"\033[1A",
+		"\033[1B",
+		"\033[K",
+	}
+	for _, c := range toRemove {
+		stdOutput = strings.ReplaceAll(stdOutput, c, "")
+	}
+	return stdOutput
+}
diff --git a/test/_e2e/cmd_deploy_test.go b/test/_e2e/cmd_deploy_test.go
index da5473f4..0773a318 100644
--- a/test/_e2e/cmd_deploy_test.go
+++ b/test/_e2e/cmd_deploy_test.go
@@ -41,29 +41,3 @@ func Deploy(t *testing.T, knFunc *TestShellCmdRunner, project *FunctionTestProje
 	project.IsDeployed = true
 
 }
-
-// CleanOutput Some commands, such as deploy command, spans spinner chars and cursor shifts at output which are captured and merged
-// regular output messages. This functions is meant to remove these chars in order to facilitate tests assertions and data extraction from output
-func CleanOutput(deployOutput string) string {
-	toRemove := []string{
-		"🕛 ",
-		"🕐 ",
-		"🕑 ",
-		"🕒 ",
-		"🕓 ",
-		"🕔 ",
-		"🕕 ",
-		"🕖 ",
-		"🕗 ",
-		"🕘 ",
-		"🕙 ",
-		"🕚 ",
-		"\033[1A",
-		"\033[1B",
-		"\033[K",
-	}
-	for _, c := range toRemove {
-		deployOutput = strings.ReplaceAll(deployOutput, c, "")
-	}
-	return deployOutput
-}
diff --git a/test/_e2e/knative.go b/test/_e2e/knative.go
index 58c90470..cfb7ed4f 100644
--- a/test/_e2e/knative.go
+++ b/test/_e2e/knative.go
@@ -39,10 +39,18 @@ func RetrieveKnativeServiceResource(t *testing.T, serviceName string) *unstructu
 }
 
 // GetCurrentServiceRevision retrieves current revision name for the deployed function
-func GetCurrentServiceRevision(t *testing.T, project *FunctionTestProject) string {
-	resource := RetrieveKnativeServiceResource(t, project.FunctionName)
+func GetCurrentServiceRevision(t *testing.T, serviceName string) string {
+	resource := RetrieveKnativeServiceResource(t, serviceName)
 	rootMap := resource.UnstructuredContent()
 	statusMap := rootMap["status"].(map[string]interface{})
 	latestReadyRevision := statusMap["latestReadyRevisionName"].(string)
 	return latestReadyRevision
 }
+
+func GetKnativeServiceUrl(t *testing.T, functionName string) string {
+	resource := RetrieveKnativeServiceResource(t, functionName)
+	rootMap := resource.UnstructuredContent()
+	statusMap := rootMap["status"].(map[string]interface{})
+	url := statusMap["url"].(string)
+	return url
+}
diff --git a/test/_e2e/ready_check_test.go b/test/_e2e/ready_check.go
similarity index 85%
rename from test/_e2e/ready_check_test.go
rename to test/_e2e/ready_check.go
index dc864745..b289817c 100644
--- a/test/_e2e/ready_check_test.go
+++ b/test/_e2e/ready_check.go
@@ -22,9 +22,9 @@ func ReadyCheck(t *testing.T, knFunc *TestShellCmdRunner, project FunctionTestPr
 }
 
 // NewRevisionCheck waits for a new revision to report as ready
-func NewRevisionCheck(t *testing.T, previousRevision string, project *FunctionTestProject) (newRevision string) {
+func NewRevisionCheck(t *testing.T, previousRevision string, serviceName string) (newRevision string) {
 	err := wait.PollImmediate(5*time.Second, 1*time.Minute, func() (done bool, err error) {
-		newRevision = GetCurrentServiceRevision(t, project)
+		newRevision = GetCurrentServiceRevision(t, serviceName)
 		t.Logf("Waiting for new revision deployment (previous revision [%v], current revision [%v])", previousRevision, newRevision)
 		return newRevision != "" && newRevision != previousRevision, nil
 	})
diff --git a/test/_e2e/update_test.go b/test/_e2e/update_test.go
index fcbc36a3..c952ecbd 100644
--- a/test/_e2e/update_test.go
+++ b/test/_e2e/update_test.go
@@ -33,13 +33,13 @@ func Update(t *testing.T, knFunc *TestShellCmdRunner, project *FunctionTestProje
 		t.Fatal("an error has occurred while updating project folder with new sources.", err.Error())
 	}
 
-	previousRevision := GetCurrentServiceRevision(t, project)
+	previousRevision := GetCurrentServiceRevision(t, project.FunctionName)
 
 	// Redeploy function
 	Deploy(t, knFunc, project)
 
 	// Waits New Revision to become ready
-	NewRevisionCheck(t, previousRevision, project)
+	NewRevisionCheck(t, previousRevision, project.FunctionName)
 
 	// Indicates new project (from update templates) is in use
 	project.IsNewRevision = true
diff --git a/test/_oncluster/asserts.go b/test/_oncluster/asserts.go
new file mode 100644
index 00000000..8c1930f8
--- /dev/null
+++ b/test/_oncluster/asserts.go
@@ -0,0 +1,35 @@
+package oncluster
+
+import (
+	"testing"
+
+	"gotest.tools/v3/assert"
+)
+
+// AssertNoError ensure err is nil otherwise fails testing
+func AssertNoError(t *testing.T, err error) {
+	if err != nil {
+		t.Error(err.Error())
+		t.FailNow()
+	}
+}
+
+// AssertThatTektonPipelineRunSucceed verifies the pipeline and pipelinerun were actually created
+// on the cluster and ensure all the Tasks of the pipelinerun executed successfully
+// Also it logs a brief summary of execution of the pipeline for potential debug purposes
+func AssertThatTektonPipelineRunSucceed(t *testing.T, functionName string) {
+	assert.Assert(t, TektonPipelineExists(t, functionName), "tekton pipeline not found on cluster")
+	RunSummary := TektonPipelineLastRunSummary(t, functionName)
+	t.Logf("Tekton Run Summary:\n %v", RunSummary.ToString())
+	assert.Assert(t, RunSummary.IsSucceed(), "expected pipeline run was not succeeded")
+}
+
+// AssertThatTektonPipelineResourcesNotExists is intended to check the pipeline and pipelinerun resources
+// do not exists. This is meant to be called after a `func delete` to ensure everything is cleaned
+func AssertThatTektonPipelineResourcesNotExists(t *testing.T, functionName string) {
+	if !t.Failed() {
+		t.Log("Checking resources got cleaned")
+		assert.Assert(t, !TektonPipelineExists(t, functionName), "tekton pipeline was found but it should not exist")
+		assert.Assert(t, !TektonPipelineRunExists(t, functionName), "tekton pipelinerun was found but it should not exist")
+	}
+}
diff --git a/test/_oncluster/git_helper.go b/test/_oncluster/git_helper.go
new file mode 100644
index 00000000..ee899fa6
--- /dev/null
+++ b/test/_oncluster/git_helper.go
@@ -0,0 +1,67 @@
+package oncluster
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	yaml "gopkg.in/yaml.v2"
+	common "knative.dev/kn-plugin-func/test/_common"
+)
+
+type Git struct {
+	URL        string
+	Revision   string
+	ContextDir string
+}
+
+// UpdateFuncYamlGit update func.yaml file by setting build to git as well as git fields.
+func UpdateFuncYamlGit(t *testing.T, projectDir string, git Git) {
+
+	funcYamlPath := projectDir + "/func.yaml"
+	data, err := os.ReadFile(funcYamlPath)
+	AssertNoError(t, err)
+
+	m := make(map[interface{}]interface{})
+	err = yaml.Unmarshal([]byte(data), &m)
+	AssertNoError(t, err)
+
+	gitMap := make(map[interface{}]interface{})
+	m["build"] = "git"
+	m["git"] = gitMap
+
+	changeLog := fmt.Sprintln("build:", "git")
+	updateGitField := func(targetField string, targetValue string) {
+		if targetValue != "" {
+			gitMap[targetField] = targetValue
+			changeLog += fmt.Sprintln("git.", targetField, ":", targetValue)
+		}
+	}
+	updateGitField("url", git.URL)
+	updateGitField("revision", git.Revision)
+	updateGitField("contextDir", git.ContextDir)
+
+	outData, _ := yaml.Marshal(m)
+	err = os.WriteFile(funcYamlPath, outData, 0644)
+	AssertNoError(t, err)
+	t.Logf("func.yaml changed:\n%v", changeLog)
+}
+
+// GitInitialCommitAndPush Runs repeatable git commands used on every initial repository setup
+// such as `git init`, `git config user`, `git add .`, `git remote add ...` and `git push`
+func GitInitialCommitAndPush(t *testing.T, gitProjectPath string, originCloneURL string) (sh *common.TestExecCmd) {
+
+	sh = common.NewShellCmd(t, gitProjectPath)
+	sh.ShouldFailOnError = true
+	sh.ShouldDumpOnSuccess = true
+	sh.Exec(`git init`)
+	sh.Exec(`git branch -M main`)
+	sh.Exec(`git add .`)
+	sh.Exec(`git config user.name "John Smith"`)
+	sh.Exec(`git config user.email "john.smith@example.com"`)
+	sh.Exec(`git commit -m "initial commit"`)
+	sh.Exec(`git remote add origin ` + originCloneURL)
+	sh.Exec(`git push -u origin main`)
+	return sh
+
+}
diff --git a/test/_oncluster/nodejs_helper.go b/test/_oncluster/nodejs_helper.go
new file mode 100644
index 00000000..85d53b06
--- /dev/null
+++ b/test/_oncluster/nodejs_helper.go
@@ -0,0 +1,23 @@
+package oncluster
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+// WriteNewSimpleIndexJS is used to replace the content of "index.js" of a Node JS function created by a test case.
+// File content will cause the deployed function to, when invoked, return the value specified on `withBodyReturning`
+// params, which is handy for test assertions.
+func WriteNewSimpleIndexJS(t *testing.T, nodeJsFuncProjectDir string, withBodyReturning string) {
+	indexJsContent := fmt.Sprintf(`
+function invoke(context) {
+  return { body: '%v' }
+}
+module.exports = invoke;
+`, withBodyReturning)
+
+	err := os.WriteFile(filepath.Join(nodeJsFuncProjectDir, "index.js"), []byte(indexJsContent), 0644)
+	AssertNoError(t, err)
+}
diff --git a/test/_oncluster/scenario_basic_test.go b/test/_oncluster/scenario_basic_test.go
new file mode 100644
index 00000000..458f8384
--- /dev/null
+++ b/test/_oncluster/scenario_basic_test.go
@@ -0,0 +1,71 @@
+//go:build oncluster
+// +build oncluster
+
+package oncluster
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"gotest.tools/v3/assert"
+	common "knative.dev/kn-plugin-func/test/_common"
+	e2e "knative.dev/kn-plugin-func/test/_e2e"
+)
+
+// TestDefault covers basic test scenario that ensure on cluster build from a "default branch" and
+// code changes (new commits) will be properly built and deployed on new revision
+func TestBasicDefault(t *testing.T) {
+
+	var funcName = "test-func-basic"
+	var funcPath = filepath.Join(os.TempDir(), funcName)
+
+	func() {
+		gitServer := common.GitTestServerProvider{}
+		gitServer.Init(t)
+		remoteRepo := gitServer.CreateRepository(funcName)
+		defer gitServer.DeleteRepository(funcName)
+
+		knFunc := common.NewKnFuncShellCli(t)
+		knFunc.Exec("create", "-l", "node", funcPath)
+		defer os.RemoveAll(funcPath)
+
+		// Write an `index.js` that make node func to return 'first revision'
+		WriteNewSimpleIndexJS(t, funcPath, "first revision")
+
+		sh := GitInitialCommitAndPush(t, funcPath, remoteRepo.ExternalCloneURL)
+
+		// Update func.yaml build as git + url + context-dir
+		UpdateFuncYamlGit(t, funcPath, Git{URL: remoteRepo.ClusterCloneURL})
+
+		// Deploy it
+		knFunc.Exec("deploy", "-r", e2e.GetRegistry(), "-p", funcPath)
+		defer knFunc.Exec("delete", "-p", funcPath)
+
+		// Assert "first revision" is returned
+		result := knFunc.Exec("invoke", "-p", funcPath)
+		assert.Assert(t, strings.Contains(result.Stdout, "first revision"), "Func body does not contain 'first revision'")
+
+		previousServiceRevision := e2e.GetCurrentServiceRevision(t, funcName)
+
+		// Update index.js to force node func to return 'new revision'
+		WriteNewSimpleIndexJS(t, funcPath, "new revision")
+		sh.Exec(`git add index.js`)
+		sh.Exec(`git commit -m "revision 2"`)
+		sh.Exec(`git push`)
+
+		// Re-Deploy Func
+		knFunc.Exec("deploy", "-r", e2e.GetRegistry(), "-p", funcPath)
+		e2e.NewRevisionCheck(t, previousServiceRevision, funcName) // Wait New Service Revision
+
+		// -- Assertions --
+		result = knFunc.Exec("invoke", "-p", funcPath)
+		assert.Assert(t, strings.Contains(result.Stdout, "new revision"), "Func body does not contain 'new revision'")
+		AssertThatTektonPipelineRunSucceed(t, funcName)
+
+	}()
+
+	AssertThatTektonPipelineResourcesNotExists(t, funcName)
+
+}
diff --git a/test/_oncluster/scenario_context-dir_test.go b/test/_oncluster/scenario_context-dir_test.go
new file mode 100644
index 00000000..9c3a2981
--- /dev/null
+++ b/test/_oncluster/scenario_context-dir_test.go
@@ -0,0 +1,59 @@
+//go:build oncluster
+// +build oncluster
+
+package oncluster
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"gotest.tools/v3/assert"
+	common "knative.dev/kn-plugin-func/test/_common"
+	e2e "knative.dev/kn-plugin-func/test/_e2e"
+)
+
+// TestContextDirFunc tests the following use case:
+//  - As a Developer I want my function located in a specific directory on my project, hosted on my
+//    public git repository from the main branch, to get deployed on my cluster
+func TestContextDirFunc(t *testing.T) {
+
+	var gitProjectName = "test-project"
+	var gitProjectPath = filepath.Join(os.TempDir(), gitProjectName)
+	var funcName = "test-func-context-dir"
+	var funcContextDir = filepath.Join("functions", funcName)
+	var funcPath = filepath.Join(gitProjectPath, funcContextDir)
+
+	func() {
+
+		gitServer := common.GitTestServerProvider{}
+		gitServer.Init(t)
+		remoteRepo := gitServer.CreateRepository(gitProjectName)
+		defer gitServer.DeleteRepository(gitProjectName)
+
+		knFunc := common.NewKnFuncShellCli(t)
+		knFunc.Exec("create", "-l", "node", funcPath)
+
+		WriteNewSimpleIndexJS(t, funcPath, "hello dir")
+
+		defer os.RemoveAll(gitProjectPath)
+
+		// Initial commit to repository: git init + commit + push
+		GitInitialCommitAndPush(t, gitProjectPath, remoteRepo.ExternalCloneURL)
+
+		// Update func.yaml build as git + url + context-dir
+		UpdateFuncYamlGit(t, funcPath, Git{URL: remoteRepo.ClusterCloneURL, ContextDir: funcContextDir})
+
+		knFunc.Exec("deploy", "-r", e2e.GetRegistry(), "-p", funcPath)
+		defer knFunc.Exec("delete", "-p", funcPath)
+
+		// -- Assertions --
+		result := knFunc.Exec("invoke", "-p", funcPath)
+		assert.Assert(t, strings.Contains(result.Stdout, "hello dir"), "Func body does not contain 'hello dir'")
+		AssertThatTektonPipelineRunSucceed(t, funcName)
+
+	}()
+
+	AssertThatTektonPipelineResourcesNotExists(t, funcName)
+}
diff --git a/test/_oncluster/scenario_from-cli-local_test.go b/test/_oncluster/scenario_from-cli-local_test.go
new file mode 100644
index 00000000..42784610
--- /dev/null
+++ b/test/_oncluster/scenario_from-cli-local_test.go
@@ -0,0 +1,36 @@
+//go:build oncluster
+// +build oncluster
+
+package oncluster
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	common "knative.dev/kn-plugin-func/test/_common"
+	e2e "knative.dev/kn-plugin-func/test/_e2e"
+)
+
+// TestFromCliBuildLocal tests the scenario which func.yaml indicates that builds should be on cluster
+// but users wants to run a local build on its machine
+func TestFromCliBuildLocal(t *testing.T) {
+
+	var funcName = "test-func-cli-local"
+	var funcPath = filepath.Join(os.TempDir(), funcName)
+
+	knFunc := common.NewKnFuncShellCli(t)
+	knFunc.Exec("create", "-l", "node", funcPath)
+	defer os.RemoveAll(funcPath)
+
+	// Update func.yaml build as local + some fake url (it should not call it anyway)
+	UpdateFuncYamlGit(t, funcPath, Git{URL: "http://fake-repo/repo.git"})
+
+	knFunc.Exec("deploy", "-r", e2e.GetRegistry(), "-p", funcPath, "--build", "local")
+	defer knFunc.Exec("delete", "-p", funcPath)
+
+	// -- Assertions --
+	knFunc.Exec("invoke", "-p", funcPath)
+	AssertThatTektonPipelineResourcesNotExists(t, funcName)
+
+}
diff --git a/test/_oncluster/scenario_from-cli_test.go b/test/_oncluster/scenario_from-cli_test.go
new file mode 100644
index 00000000..326b01c8
--- /dev/null
+++ b/test/_oncluster/scenario_from-cli_test.go
@@ -0,0 +1,143 @@
+//go:build oncluster
+// +build oncluster
+
+package oncluster
+
+/*
+Tests on this file covers the scenarios when func.yaml is not modified (build: local)
+and git build strategy is specified thru CLI.
+
+A) Default Branch Test
+func deploy --build=git --git-url=http://gitserver/myfunc.git
+
+b) Feature Branch Test
+func deploy --build=git --git-url=http://gitserver/myfunc.git --git-branch=feature/my-branch
+
+c) Context Dir test
+func deploy --build=git --git-url=http://gitserver/myfunc.git --git-dir=functions/myfunc
+*/
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"gotest.tools/v3/assert"
+	common "knative.dev/kn-plugin-func/test/_common"
+	e2e "knative.dev/kn-plugin-func/test/_e2e"
+)
+
+// TestFromCliDefaultBranch triggers a default branch test by using CLI flags
+func TestFromCliDefaultBranch(t *testing.T) {
+
+	var gitProjectName = "test-func-yaml-build-local"
+	var gitProjectPath = filepath.Join(os.TempDir(), gitProjectName)
+	var funcName = gitProjectName
+	var funcPath = gitProjectPath
+
+	gitServer := common.GitTestServerProvider{}
+	gitServer.Init(t)
+	remoteRepo := gitServer.CreateRepository(gitProjectName)
+	defer gitServer.DeleteRepository(gitProjectName)
+
+	knFunc := common.NewKnFuncShellCli(t)
+	knFunc.Exec("create", "-l", "node", funcPath)
+	defer os.RemoveAll(gitProjectPath)
+
+	GitInitialCommitAndPush(t, gitProjectPath, remoteRepo.ExternalCloneURL)
+
+	knFunc.Exec("deploy",
+		"-r", e2e.GetRegistry(),
+		"-p", funcPath,
+		"--build", "git",
+		"--git-url", remoteRepo.ClusterCloneURL)
+
+	defer knFunc.Exec("delete", "-p", funcPath)
+
+	// ## ASSERTIONS
+	result := knFunc.Exec("invoke", "-p", funcPath)
+	assert.Assert(t, strings.Contains(result.Stdout, "Hello"), "Func body does not contain 'Hello'")
+	AssertThatTektonPipelineRunSucceed(t, funcName)
+
+}
+
+// TestFromCliFeatureBranch trigger a feature branch test by using CLI flags
+func TestFromCliFeatureBranch(t *testing.T) {
+
+	var funcName = "test-func-cli-feature-branch"
+	var funcPath = filepath.Join(os.TempDir(), funcName)
+
+	gitServer := common.GitTestServerProvider{}
+	gitServer.Init(t)
+	remoteRepo := gitServer.CreateRepository(funcName)
+	defer gitServer.DeleteRepository(funcName)
+
+	knFunc := common.NewKnFuncShellCli(t)
+	knFunc.Exec("create", "-l", "node", funcPath)
+	defer os.RemoveAll(funcPath)
+
+	GitInitialCommitAndPush(t, funcPath, remoteRepo.ExternalCloneURL)
+
+	WriteNewSimpleIndexJS(t, funcPath, "hello branch")
+
+	sh := common.NewShellCmd(t, funcPath)
+	sh.ShouldFailOnError = true
+	sh.Exec("git checkout -b feature/branch")
+	sh.Exec("git add index.js")
+	sh.Exec(`git commit -m "feature branch change"`)
+	sh.Exec("git push -u origin feature/branch")
+
+	knFunc.Exec("deploy",
+		"-r", e2e.GetRegistry(),
+		"-p", funcPath,
+		"--build", "git",
+		"--git-url", remoteRepo.ClusterCloneURL,
+		"--git-branch", "feature/branch")
+
+	defer knFunc.Exec("delete", "-p", funcPath)
+
+	// ## ASSERTIONS
+	result := knFunc.Exec("invoke", "-p", funcPath)
+	assert.Assert(t, strings.Contains(result.Stdout, "hello branch"), "Func body does not contain 'hello branch'")
+	AssertThatTektonPipelineRunSucceed(t, funcName)
+
+}
+
+// TestFromCliContextDirFunc triggers a context dir test by using CLI flags
+func TestFromCliContextDirFunc(t *testing.T) {
+
+	var gitProjectName = "test-project"
+	var gitProjectPath = filepath.Join(os.TempDir(), gitProjectName)
+	var funcName = "test-func-context-dir"
+	var funcContextDir = filepath.Join("functions", funcName)
+	var funcPath = filepath.Join(gitProjectPath, funcContextDir)
+
+	gitServer := common.GitTestServerProvider{}
+	gitServer.Init(t)
+	remoteRepo := gitServer.CreateRepository(gitProjectName)
+	defer gitServer.DeleteRepository(gitProjectName)
+
+	knFunc := common.NewKnFuncShellCli(t)
+	knFunc.Exec("create", "-l", "node", funcPath)
+	defer os.RemoveAll(gitProjectPath)
+
+	WriteNewSimpleIndexJS(t, funcPath, "hello dir")
+
+	GitInitialCommitAndPush(t, gitProjectPath, remoteRepo.ExternalCloneURL)
+
+	knFunc.Exec("deploy",
+		"-r", e2e.GetRegistry(),
+		"-p", funcPath,
+		"--build", "git",
+		"--git-url", remoteRepo.ClusterCloneURL,
+		"--git-dir", funcContextDir)
+
+	defer knFunc.Exec("delete", "-p", funcPath)
+
+	// -- Assertions --
+	result := knFunc.Exec("invoke", "-p", funcPath)
+	assert.Assert(t, strings.Contains(result.Stdout, "hello dir"), "Func body does not contain 'hello dir'")
+	AssertThatTektonPipelineRunSucceed(t, funcName)
+
+}
diff --git a/test/_oncluster/scenario_revision_test.go b/test/_oncluster/scenario_revision_test.go
new file mode 100644
index 00000000..a24934f3
--- /dev/null
+++ b/test/_oncluster/scenario_revision_test.go
@@ -0,0 +1,121 @@
+//go:build oncluster
+// +build oncluster
+
+package oncluster
+
+/*
+Tests on this file covers "on cluster build" use cases:
+
+A) I want my function hosted on my public git repository from a FEATURE BRANCH to get built deployed
+b) I want my function hosted on my public git repository from a specific GIT TAG to get built and deployed
+c) I want my function hosted on my public git repository from a specific COMMIT HASH to get built and deployed
+
+*/
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	common "knative.dev/kn-plugin-func/test/_common"
+	e2e "knative.dev/kn-plugin-func/test/_e2e"
+)
+
+func TestFromFeatureBranch(t *testing.T) {
+
+	setupCodeFn := func(sh *common.TestExecCmd, funcProjectPath string, clusterCloneUrl string) {
+
+		WriteNewSimpleIndexJS(t, funcProjectPath, "hello branch")
+		sh.Exec("git checkout -b feature/branch")
+		sh.Exec("git add index.js")
+		sh.Exec(`git commit -m "feature branch change"`)
+		sh.Exec("git push -u origin feature/branch")
+		UpdateFuncYamlGit(t, funcProjectPath, Git{URL: clusterCloneUrl, Revision: "feature/branch"})
+
+	}
+	assertBodyFn := func(response string) bool {
+		return strings.Contains(response, "hello branch")
+	}
+	GitRevisionCheck(t, "test-func-feature-branch", setupCodeFn, assertBodyFn)
+}
+
+func TestFromRevisionTag(t *testing.T) {
+
+	setupCodeFn := func(sh *common.TestExecCmd, funcProjectPath string, clusterCloneUrl string) {
+
+		WriteNewSimpleIndexJS(t, funcProjectPath, "hello v1")
+		sh.Exec("git add index.js")
+		sh.Exec(`git commit -m "version 1"`)
+		sh.Exec("git push origin main")
+		sh.Exec("git tag tag-v1")
+		sh.Exec("git push origin tag-v1")
+		WriteNewSimpleIndexJS(t, funcProjectPath, "hello v2")
+		sh.Exec("git add index.js")
+		sh.Exec(`git commit -m "version 2"`)
+		sh.Exec("git push origin main")
+		UpdateFuncYamlGit(t, funcProjectPath, Git{URL: clusterCloneUrl, Revision: "tag-v1"})
+
+	}
+	assertBodyFn := func(response string) bool {
+		return strings.Contains(response, "hello v1")
+	}
+	GitRevisionCheck(t, "test-func-tag", setupCodeFn, assertBodyFn)
+}
+
+func TestFromCommitHash(t *testing.T) {
+
+	setupCodeFn := func(sh *common.TestExecCmd, funcProjectPath string, clusterCloneUrl string) {
+
+		WriteNewSimpleIndexJS(t, funcProjectPath, "hello v1")
+		sh.Exec("git add index.js")
+		sh.Exec(`git commit -m "version 1"`)
+		sh.Exec("git push origin main")
+		gitRevParse := sh.Exec("git rev-parse HEAD")
+		WriteNewSimpleIndexJS(t, funcProjectPath, "hello v2")
+		sh.Exec("git add index.js")
+		sh.Exec(`git commit -m "version 2"`)
+		sh.Exec("git push origin main")
+		commitHash := strings.TrimSpace(gitRevParse.Stdout)
+		UpdateFuncYamlGit(t, funcProjectPath, Git{URL: clusterCloneUrl, Revision: commitHash})
+
+		t.Logf("Revision Check: commit hash resolved to [%v]", commitHash)
+	}
+	assertBodyFn := func(response string) bool {
+		return strings.Contains(response, "hello v1")
+	}
+	GitRevisionCheck(t, "test-func-commit", setupCodeFn, assertBodyFn)
+}
+
+func GitRevisionCheck(
+	t *testing.T,
+	funcName string,
+	setupCodeFn func(shell *common.TestExecCmd, funcProjectPath string, clusterCloneUrl string),
+	assertBodyFn func(response string) bool) {
+
+	var funcPath = filepath.Join(os.TempDir(), funcName)
+
+	gitServer := common.GitTestServerProvider{}
+	gitServer.Init(t)
+	remoteRepo := gitServer.CreateRepository(funcName)
+	defer gitServer.DeleteRepository(funcName)
+
+	knFunc := common.NewKnFuncShellCli(t)
+	knFunc.Exec("create", "-l", "node", funcPath)
+	defer os.RemoveAll(funcPath)
+
+	sh := GitInitialCommitAndPush(t, funcPath, remoteRepo.ExternalCloneURL)
+
+	// Setup specific code
+	setupCodeFn(sh, funcPath, remoteRepo.ClusterCloneURL)
+
+	knFunc.Exec("deploy", "-r", e2e.GetRegistry(), "-p", funcPath)
+	defer knFunc.Exec("delete", "-p", funcPath)
+
+	// -- Assertions --
+	result := knFunc.Exec("invoke", "-p", funcPath)
+	if !assertBodyFn(result.Stdout) {
+		t.Error("Func Body does not contains expected expression")
+	}
+	AssertThatTektonPipelineRunSucceed(t, funcName)
+}
diff --git a/test/_oncluster/scenario_runtime_test.go b/test/_oncluster/scenario_runtime_test.go
new file mode 100644
index 00000000..50840484
--- /dev/null
+++ b/test/_oncluster/scenario_runtime_test.go
@@ -0,0 +1,69 @@
+//go:build oncluster || runtime
+// +build oncluster runtime
+
+package oncluster
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	common "knative.dev/kn-plugin-func/test/_common"
+	e2e "knative.dev/kn-plugin-func/test/_e2e"
+)
+
+// TestRuntime will invoke a language runtime test against (by default) to all runtimes.
+// The Environment Variable E2E_RUNTIMES can be used to select the languages/runtimes to be tested
+func TestRuntime(t *testing.T) {
+
+	var runtimeList = []string{}
+	runtimes, present := os.LookupEnv("E2E_RUNTIMES")
+
+	if present {
+		if runtimes != "" {
+			runtimeList = strings.Split(runtimes, " ")
+		}
+	} else {
+		runtimeList = []string{"node", "python", "quarkus", "springboot", "typescript"} // "go" and "rust" pending support
+	}
+	for _, lang := range runtimeList {
+		t.Run(lang+"_test", func(t *testing.T) {
+			runtimeImpl(t, lang)
+		})
+	}
+
+}
+
+func runtimeImpl(t *testing.T, lang string) {
+
+	var gitProjectName = "test-func-lang-" + lang
+	var gitProjectPath = filepath.Join(os.TempDir(), gitProjectName)
+	var funcName = gitProjectName
+	var funcPath = gitProjectPath
+
+	gitServer := common.GitTestServerProvider{}
+	gitServer.Init(t)
+	remoteRepo := gitServer.CreateRepository(gitProjectName)
+	defer gitServer.DeleteRepository(gitProjectName)
+
+	knFunc := common.NewKnFuncShellCli(t)
+	knFunc.Exec("create", "-l", lang, funcPath)
+	defer os.RemoveAll(gitProjectPath)
+
+	GitInitialCommitAndPush(t, gitProjectPath, remoteRepo.ExternalCloneURL)
+
+	knFunc.Exec("deploy",
+		"-r", e2e.GetRegistry(),
+		"-p", funcPath,
+		"--build", "git",
+		"--git-url", remoteRepo.ClusterCloneURL)
+
+	defer knFunc.Exec("delete", "-p", funcPath)
+
+	// -- Assertions --
+	result := knFunc.Exec("invoke", "-p", funcPath)
+	t.Log(result)
+	AssertThatTektonPipelineRunSucceed(t, funcName)
+
+}
diff --git a/test/_oncluster/tekton.go b/test/_oncluster/tekton.go
new file mode 100644
index 00000000..9b1ae1d9
--- /dev/null
+++ b/test/_oncluster/tekton.go
@@ -0,0 +1,96 @@
+package oncluster
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"knative.dev/kn-plugin-func/k8s"
+	"knative.dev/kn-plugin-func/pipelines/tekton"
+)
+
+// TektonPipelineExists verifies pipeline with a given prefix exists on cluster
+func TektonPipelineExists(t *testing.T, pipelinePrefix string) bool {
+	namespace, _, _ := k8s.GetClientConfig().Namespace()
+	client, ns, _ := tekton.NewTektonClientAndResolvedNamespace(namespace)
+	pipelines, err := client.Pipelines(ns).List(context.Background(), v1.ListOptions{})
+	if err != nil {
+		t.Error(err.Error())
+	}
+	for _, pipeline := range pipelines.Items {
+		if strings.HasPrefix(pipeline.Name, pipelinePrefix) && strings.HasSuffix(pipeline.Name, "-pipeline") {
+			return true
+		}
+	}
+	return false
+}
+
+// TektonPipelineRunExists verifies pipelinerun with a given prefix exists on cluster
+func TektonPipelineRunExists(t *testing.T, pipelineRunPrefix string) bool {
+	namespace, _, _ := k8s.GetClientConfig().Namespace()
+	client, ns, _ := tekton.NewTektonClientAndResolvedNamespace(namespace)
+	pipelineRuns, err := client.PipelineRuns(ns).List(context.Background(), v1.ListOptions{})
+	if err != nil {
+		t.Error(err.Error())
+	}
+	for _, run := range pipelineRuns.Items {
+		if strings.HasPrefix(run.Name, pipelineRunPrefix) {
+			return true
+		}
+	}
+	return false
+}
+
+type PipelineRunSummary struct {
+	PipelineRunName   string
+	PipelineRunStatus string
+	TasksRunSummary   []PipelineTaskRunSummary
+}
+type PipelineTaskRunSummary struct {
+	TaskName   string
+	TaskStatus string
+}
+
+func (p *PipelineRunSummary) ToString() string {
+	r := fmt.Sprintf("run: %-42v, status: %v\n", p.PipelineRunName, p.PipelineRunStatus)
+	for _, t := range p.TasksRunSummary {
+		r = r + fmt.Sprintf(" task: %-15v, status: %v\n", t.TaskName, t.TaskStatus)
+	}
+	return r
+}
+
+func (p *PipelineRunSummary) IsSucceed() bool {
+	return p.PipelineRunStatus == "Succeeded"
+}
+
+// TektonPipelTektonPipelineLastRunSummary gather information about a pipeline run such as
+// list of tasks executed and status of each task execution. It is meant to be used on assertions
+func TektonPipelineLastRunSummary(t *testing.T, pipelinePrefix string) *PipelineRunSummary {
+	namespace, _, _ := k8s.GetClientConfig().Namespace()
+	client, ns, _ := tekton.NewTektonClientAndResolvedNamespace(namespace)
+	pipelineRuns, err := client.PipelineRuns(ns).List(context.Background(), v1.ListOptions{})
+	if err != nil {
+		t.Error(err.Error())
+	}
+	lr := PipelineRunSummary{}
+	for _, run := range pipelineRuns.Items {
+		if strings.HasPrefix(run.Name, pipelinePrefix) {
+			lr.PipelineRunName = run.Name
+			if len(run.Status.Conditions) > 0 {
+				lr.PipelineRunStatus = run.Status.Conditions[0].Reason
+			}
+			lr.TasksRunSummary = []PipelineTaskRunSummary{}
+			for _, taskRun := range run.Status.TaskRuns {
+				trun := PipelineTaskRunSummary{}
+				trun.TaskName = taskRun.PipelineTaskName
+				if len(taskRun.Status.Conditions) > 0 {
+					trun.TaskStatus = taskRun.Status.Conditions[0].Reason
+				}
+				lr.TasksRunSummary = append(lr.TasksRunSummary, trun)
+			}
+		}
+	}
+	return &lr
+}
diff --git a/test/e2e_oncluster_tests.sh b/test/e2e_oncluster_tests.sh
new file mode 100755
index 00000000..cdaf1a69
--- /dev/null
+++ b/test/e2e_oncluster_tests.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+# 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.
+
+#
+# Runs basic lifecycle E2E tests against kn func cli for a given language/runtime.
+# By default it will run e2e tests against 'func' binary, but you can change it to use 'kn func' instead
+#
+# The following environment variable can be set in order to customize e2e execution:
+#
+# E2E_USE_KN_FUNC    When set to "true" indicates e2e to issue func command using kn cli.
+#
+# E2E_REGISTRY_URL   Indicates a specific registry (i.e: "quay.io/user") should be used. Make sure
+#                    to authenticate to the registry (i.e: docker login ...) prior to execute the script
+#                    By default it uses "ttl.sh" registry
+#
+# E2E_FUNC_BIN_PATH  Path to func binary. Derived by this script when not set
+#
+# E2E_RUNTIMES       List of runtimes (space separated) to execute TestRuntime.
+#
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+runtime=${1:-}
+use_kn_func=${E2E_USE_KN_FUNC:-}
+
+curdir=$(pwd)
+cd $(dirname $0)
+cd ../
+
+REGISTRY_PROJ=knfunc$(head -c 128 </dev/urandom | LC_CTYPE=C tr -dc 'a-z0-9' | fold -w 8 | head -n 1)
+export E2E_REGISTRY_URL=${E2E_REGISTRY_URL:-ttl.sh/$REGISTRY_PROJ}
+export E2E_FUNC_BIN_PATH=${E2E_FUNC_BIN_PATH:-$(pwd)/func}
+
+# Make sure 'func' binary is built in case KN FUNC was not required for testing
+if [[ ! -f "$E2E_FUNC_BIN_PATH" && "$use_kn_func" != "true" ]]; then
+  echo "func binary not found. Please run 'make build' prior to run e2e."
+  exit 1
+fi
+
+
+go clean -testcache
+go test -v -test.v -test.timeout=90m -tags="${TEST_TAGS:-oncluster}" ./test/_oncluster/
+ret=$?
+
+cd $curdir
+exit $ret
diff --git a/test/gitserver.sh b/test/gitserver.sh
new file mode 100755
index 00000000..f3381f7a
--- /dev/null
+++ b/test/gitserver.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+# 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.
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+git_server() {
+  echo "Creating Git Server Knative service..."
+  cat << EOF | kubectl apply -f -
+apiVersion: serving.knative.dev/v1
+kind: Service
+metadata:
+  name: func-git
+  labels:
+    app: git
+spec:
+  template:
+    metadata:
+      annotations:
+        autoscaling.knative.dev/max-scale: "1"
+        autoscaling.knative.dev/min-scale: "1"
+        client.knative.dev/user-image: ghcr.io/jrangelramos/gitserver
+    spec:
+      containers:
+      - image: ghcr.io/jrangelramos/gitserver
+        ports:
+        - containerPort: 80
+        resources: {}
+status: {}
+EOF
+
+  kubectl wait ksvc --for=condition=RoutesReady --timeout=30s -l "app=git"
+}
+
+git_server
+
+echo Done