Repository: knative-sandbox/kn-plugin-func
Pull request #665 (2021-11-17 14:05:01)

[main] Update community files
Cron -knative-prow-robot

/cc knative-sandbox/kn-plugin-func-approvers
/assign knative-sandbox/kn-plugin-func-approvers

Produced by: knative-sandbox/knobots/actions/update-community

Details:
```

```

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

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

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=knative-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":["lkingland"]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/665?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) Report
> Merging [#665](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/665?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (7b4df86) into [main](https://codecov.io/gh/knative-sandbox/kn-plugin-func/commit/3935747b91e31804070f0939f346e87f635fe09d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (3935747) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/665/graphs/tree.svg?width=650&height=150&src=pr&token=da1B9bMsQ9&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox)](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/665?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox)

```diff
@@           Coverage Diff           @@
##             main     #665   +/-   ##
=======================================
  Coverage   37.52%   37.52%           
=======================================
  Files          38       38           
  Lines        3891     3891           
=======================================
  Hits         1460     1460           
  Misses       2238     2238           
  Partials      193      193           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/665?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/665?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox). Last update [3935747...7b4df86](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/665?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox).


Patch:
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index 0bf1af7f..d5291f51 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -68,6 +68,7 @@ aliases:
   - lberk
   - matzew
   - phamilton
+  - pierDipi
   - steven0711dong
   - travis-minke-sap
   eventing-kafka-broker-approvers:
@@ -128,6 +129,8 @@ aliases:
   - lance
   - lkingland
   - matejvasek
+  - nainaz
+  - omerbensaadon
   - zroubalik
   kn-plugin-migration-approvers:
   - maximilien
diff --git a/client.go b/client.go
index 34306f16..9d214c66 100644
--- a/client.go
+++ b/client.go
@@ -430,9 +430,9 @@ func (c *Client) New(ctx context.Context, cfg Function) (err error) {
 
 // Create a new Function project locally using the settings provided on a
 // Function object.
-func (c *Client) Create(f Function) (err error) {
+func (c *Client) Create(cfg Function) (err error) {
 	// Create project root directory, if it doesn't already exist
-	if err = os.MkdirAll(f.Root, 0755); err != nil {
+	if err = os.MkdirAll(cfg.Root, 0755); err != nil {
 		return
 	}
 
@@ -442,11 +442,11 @@ func (c *Client) Create(f Function) (err error) {
 	// immediately exit with error (prior to actual creation) if this is
 	// a Function already initialized at that path (Create should never
 	// clobber a pre-existing Function)
-	defaults, err := NewFunction(f.Root)
+	f, err := NewFunctionFromDefaults(cfg)
 	if err != nil {
 		return
 	}
-	if defaults.Initialized() {
+	if f.Initialized() {
 		err = fmt.Errorf("Function at '%v' already initialized", f.Root)
 		return
 	}
@@ -463,16 +463,6 @@ func (c *Client) Create(f Function) (err error) {
 		return
 	}
 
-	// Assert runtime was provided, or default.
-	if f.Runtime == "" {
-		f.Runtime = DefaultRuntime
-	}
-
-	// Assert template name was provided, or default.
-	if f.Template == "" {
-		f.Template = DefaultTemplate
-	}
-
 	// Write out the template for a Function
 	// returns a Function which may be mutated based on the content of
 	// the template (default Function, builders, buildpacks, etc).
@@ -483,7 +473,7 @@ func (c *Client) Create(f Function) (err error) {
 
 	// Mark it as having been created via this client library and Write (save)
 	f.Created = time.Now()
-	if err = writeConfig(f); err != nil {
+	if err = f.Write(); err != nil {
 		return
 	}
 
@@ -544,9 +534,9 @@ func (c *Client) Build(ctx context.Context, path string) (err error) {
 		return
 	}
 
-	// Write out config, which will now contain a populated image tag
-	// if it had not already
-	if err = writeConfig(f); err != nil {
+	// Write (save) - Serialize the Function to disk
+	// Will now contain populated image tag.
+	if err = f.Write(); err != nil {
 		return
 	}
 
@@ -585,9 +575,9 @@ func (c *Client) Deploy(ctx context.Context, path string) (err error) {
 		return
 	}
 
-	// Store the produced image Digest in the config
+	// Record the Image Digest pushed.
 	f.ImageDigest = imageDigest
-	if err = writeConfig(f); err != nil {
+	if err = f.Write(); err != nil {
 		return
 	}
 
diff --git a/cmd/build.go b/cmd/build.go
index 58542bce..8e1986b5 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -145,7 +145,7 @@ func runBuild(cmd *cobra.Command, _ []string, clientFn buildClientFn) (err error
 	}
 
 	// All set, let's write changes in the config to the disk
-	err = function.WriteConfig()
+	err = function.Write()
 	if err != nil {
 		return
 	}
diff --git a/cmd/config.go b/cmd/config.go
index 40a78d27..a604789a 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -38,7 +38,7 @@ func (s standardLoaderSaver) Load(path string) (fn.Function, error) {
 }
 
 func (s standardLoaderSaver) Save(f fn.Function) error {
-	return f.WriteConfig()
+	return f.Write()
 }
 
 var defaultLoaderSaver standardLoaderSaver
diff --git a/cmd/config_envs.go b/cmd/config_envs.go
index 62d2dc67..1c5523ae 100644
--- a/cmd/config_envs.go
+++ b/cmd/config_envs.go
@@ -375,7 +375,7 @@ func runAddEnvsPrompt(ctx context.Context, f fn.Function) (err error) {
 		f.Envs[insertToIndex] = newEnv
 	}
 
-	err = f.WriteConfig()
+	err = f.Write()
 	if err == nil {
 		fmt.Println("Environment variable entry was added to the function configuration")
 	}
@@ -419,7 +419,7 @@ func runRemoveEnvsPrompt(f fn.Function) (err error) {
 
 	if removed {
 		f.Envs = newEnvs
-		err = f.WriteConfig()
+		err = f.Write()
 		if err == nil {
 			fmt.Println("Environment variable entry was removed from the function configuration")
 		}
diff --git a/cmd/config_volumes.go b/cmd/config_volumes.go
index e82af6af..9b26cb7b 100644
--- a/cmd/config_volumes.go
+++ b/cmd/config_volumes.go
@@ -192,7 +192,7 @@ func runAddVolumesPrompt(ctx context.Context, f fn.Function) (err error) {
 
 	f.Volumes = append(f.Volumes, newVolume)
 
-	err = f.WriteConfig()
+	err = f.Write()
 	if err == nil {
 		fmt.Println("Volume entry was added to the function configuration")
 	}
@@ -236,7 +236,7 @@ func runRemoveVolumesPrompt(f fn.Function) (err error) {
 
 	if removed {
 		f.Volumes = newVolumes
-		err = f.WriteConfig()
+		err = f.Write()
 		if err == nil {
 			fmt.Println("Volume entry was removed from the function configuration")
 		}
diff --git a/cmd/deploy.go b/cmd/deploy.go
index f272d4f5..eb95a8e5 100644
--- a/cmd/deploy.go
+++ b/cmd/deploy.go
@@ -158,7 +158,7 @@ func runDeploy(cmd *cobra.Command, _ []string, clientFn deployClientFn) (err err
 	}
 
 	// All set, let's write changes in the config to the disk
-	err = function.WriteConfig()
+	err = function.Write()
 	if err != nil {
 		return
 	}
diff --git a/cmd/run.go b/cmd/run.go
index 44839a42..508774d8 100644
--- a/cmd/run.go
+++ b/cmd/run.go
@@ -75,7 +75,7 @@ func runRun(cmd *cobra.Command, args []string, clientFn runClientFn) (err error)
 		return
 	}
 
-	err = function.WriteConfig()
+	err = function.Write()
 	if err != nil {
 		return
 	}
diff --git a/config.go b/config.go
index 3bfae61c..ef56798b 100644
--- a/config.go
+++ b/config.go
@@ -1,534 +1,5 @@
 package function
 
-import (
-	"errors"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"regexp"
-	"strings"
-	"time"
-
-	"gopkg.in/yaml.v2"
-	"k8s.io/apimachinery/pkg/api/resource"
-	"knative.dev/kn-plugin-func/utils"
-)
-
-// ConfigFile is the name of the config's serialized form.
-const ConfigFile = "func.yaml"
-
-var (
-	regWholeSecret      = regexp.MustCompile(`^{{\s*secret:((?:\w|['-]\w)+)\s*}}$`)
-	regKeyFromSecret    = regexp.MustCompile(`^{{\s*secret:((?:\w|['-]\w)+):([-._a-zA-Z0-9]+)\s*}}$`)
-	regWholeConfigMap   = regexp.MustCompile(`^{{\s*configMap:((?:\w|['-]\w)+)\s*}}$`)
-	regKeyFromConfigMap = regexp.MustCompile(`^{{\s*configMap:((?:\w|['-]\w)+):([-._a-zA-Z0-9]+)\s*}}$`)
-	regLocalEnv         = regexp.MustCompile(`^{{\s*env:(\w+)\s*}}$`)
-)
-
-type Volume struct {
-	Secret    *string `yaml:"secret,omitempty" jsonschema:"oneof_required=secret"`
-	ConfigMap *string `yaml:"configMap,omitempty" jsonschema:"oneof_required=configmap"`
-	Path      *string `yaml:"path"`
-}
-
-func (v Volume) String() string {
-	if v.ConfigMap != nil {
-		return fmt.Sprintf("ConfigMap \"%s\" mounted at path: \"%s\"", *v.ConfigMap, *v.Path)
-	} else if v.Secret != nil {
-		return fmt.Sprintf("Secret \"%s\" mounted at path: \"%s\"", *v.Secret, *v.Path)
-	}
-
-	return ""
-}
-
-type Env struct {
-	Name  *string `yaml:"name,omitempty" jsonschema:"pattern=^[-._a-zA-Z][-._a-zA-Z0-9]*$"`
-	Value *string `yaml:"value"`
-}
-
-func (e Env) String() string {
-	if e.Name == nil && e.Value != nil {
-		match := regWholeSecret.FindStringSubmatch(*e.Value)
-		if len(match) == 2 {
-			return fmt.Sprintf("All key=value pairs from Secret \"%s\"", match[1])
-		}
-		match = regWholeConfigMap.FindStringSubmatch(*e.Value)
-		if len(match) == 2 {
-			return fmt.Sprintf("All key=value pairs from ConfigMap \"%s\"", match[1])
-		}
-	} else if e.Name != nil && e.Value != nil {
-		match := regKeyFromSecret.FindStringSubmatch(*e.Value)
-		if len(match) == 3 {
-			return fmt.Sprintf("Env \"%s\" with value set from key \"%s\" from Secret \"%s\"", *e.Name, match[2], match[1])
-		}
-		match = regKeyFromConfigMap.FindStringSubmatch(*e.Value)
-		if len(match) == 3 {
-			return fmt.Sprintf("Env \"%s\" with value set from key \"%s\" from ConfigMap \"%s\"", *e.Name, match[2], match[1])
-		}
-		match = regLocalEnv.FindStringSubmatch(*e.Value)
-		if len(match) == 2 {
-			return fmt.Sprintf("Env \"%s\" with value set from local env variable \"%s\"", *e.Name, match[1])
-		}
-
-		return fmt.Sprintf("Env \"%s\" with value \"%s\"", *e.Name, *e.Value)
-	}
-	return ""
-}
-
-type Label struct {
-	// Key consist of optional prefix part (ended by '/') and name part
-	// Prefix part validation pattern: [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
-	// Name part validation pattern: ([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]
-	Key   *string `yaml:"key" jsonschema:"pattern=^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\\/)?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$"`
-	Value *string `yaml:"value,omitempty" jsonschema:"pattern=^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$"`
-}
-
-func (l Label) String() string {
-	if l.Key != nil && l.Value == nil {
-		return fmt.Sprintf("Label with key \"%s\"", *l.Key)
-	} else if l.Key != nil && l.Value != nil {
-		match := regLocalEnv.FindStringSubmatch(*l.Value)
-		if len(match) == 2 {
-			return fmt.Sprintf("Label with key \"%s\" and value set from local env variable \"%s\"", *l.Key, match[1])
-		}
-		return fmt.Sprintf("Label with key \"%s\" and value \"%s\"", *l.Key, *l.Value)
-	}
-	return ""
-}
-
-type Options struct {
-	Scale     *ScaleOptions     `yaml:"scale,omitempty"`
-	Resources *ResourcesOptions `yaml:"resources,omitempty"`
-}
-
-type ScaleOptions struct {
-	Min         *int64   `yaml:"min,omitempty" jsonschema_extras:"minimum=0"`
-	Max         *int64   `yaml:"max,omitempty" jsonschema_extras:"minimum=0"`
-	Metric      *string  `yaml:"metric,omitempty" jsonschema:"enum=concurrency,enum=rps"`
-	Target      *float64 `yaml:"target,omitempty" jsonschema_extras:"minimum=0.01"`
-	Utilization *float64 `yaml:"utilization,omitempty" jsonschema:"minimum=1,maximum=100"`
-}
-
-type ResourcesOptions struct {
-	Requests *ResourcesRequestsOptions `yaml:"requests,omitempty"`
-	Limits   *ResourcesLimitsOptions   `yaml:"limits,omitempty"`
-}
-
-type ResourcesLimitsOptions struct {
-	CPU         *string `yaml:"cpu,omitempty" jsonschema:"pattern=^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$"`
-	Memory      *string `yaml:"memory,omitempty" jsonschema:"pattern=^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$"`
-	Concurrency *int64  `yaml:"concurrency,omitempty" jsonschema_extras:"minimum=0"`
-}
-
-type ResourcesRequestsOptions struct {
-	CPU    *string `yaml:"cpu,omitempty" jsonschema:"pattern=^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$"`
-	Memory *string `yaml:"memory,omitempty" jsonschema:"pattern=^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$"`
-}
-
-// Config represents the serialized state of a Function's metadata.
-// See the Function struct for attribute documentation.
-type Config struct {
-	Name            string            `yaml:"name" jsonschema:"pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"`
-	Namespace       string            `yaml:"namespace"`
-	Runtime         string            `yaml:"runtime"`
-	Image           string            `yaml:"image"`
-	ImageDigest     string            `yaml:"imageDigest"`
-	Builder         string            `yaml:"builder"`
-	Builders        map[string]string `yaml:"builders"`
-	Buildpacks      []string          `yaml:"buildpacks"`
-	HealthEndpoints HealthEndpoints   `yaml:"healthEndpoints"`
-	Volumes         []Volume          `yaml:"volumes"`
-	BuildEnvs       []Env             `yaml:"buildEnvs"`
-	Envs            []Env             `yaml:"envs"`
-	Annotations     map[string]string `yaml:"annotations"`
-	Options         Options           `yaml:"options"`
-	Labels          []Label           `yaml:"labels"`
-	Created         time.Time         `yaml:"created"`
-	// Add new values to the toConfig/fromConfig functions.
-}
-
-// newConfig returns a Config populated from data serialized to disk if it is
-// available.  Errors are returned if the path is not valid, if there are
-// errors accessing an extant config file, or the contents of the file do not
-// unmarshall.  A missing file at a valid path does not error but returns the
-// empty value of Config.
-func newConfig(root string) (c Config, err error) {
-	filename := filepath.Join(root, ConfigFile)
-	if _, err = os.Stat(filename); err != nil {
-		// do not consider a missing config file an error.  Just return.
-		if os.IsNotExist(err) {
-			err = nil
-		}
-		return
-	}
-	bb, err := ioutil.ReadFile(filename)
-	if err != nil {
-		return
-	}
-	if err = yaml.UnmarshalStrict(bb, &c); err != nil {
-		return c, formatUnmarshalError(err) // fail fast on sytax errors
-	}
-	return c, validateConfig(c)
-}
-
-func formatUnmarshalError(err error) error {
-	var (
-		e      = err.Error()
-		rxp    = regexp.MustCompile("not found in type .*")
-		header = fmt.Sprintf("'%v' is not valid:\n", ConfigFile)
-	)
-	if strings.HasPrefix(e, "yaml: unmarshal errors:") {
-		e = rxp.ReplaceAllString(e, "is not valid")
-		e = strings.Replace(e, "yaml: unmarshal errors:\n", header, 1)
-	} else if strings.HasPrefix(e, "yaml:") {
-		e = rxp.ReplaceAllString(e, "is not valid")
-		e = strings.Replace(e, "yaml: ", header+"  ", 1)
-	}
-	return errors.New(e)
-}
-
-func validateConfig(c Config) error {
-	errs := [][]string{
-		validateVolumes(c.Volumes),
-		ValidateBuildEnvs(c.BuildEnvs),
-		ValidateEnvs(c.Envs),
-		validateOptions(c.Options),
-		ValidateLabels(c.Labels),
-	}
-
-	var ctr int
-	var b strings.Builder
-	b.WriteString(fmt.Sprintf("'%v' contains errors:", ConfigFile))
-	for _, ee := range errs {
-		if len(ee) > 0 {
-			b.WriteString("\n") // Precede each group of errors with a linebreak
-		}
-		for _, e := range ee {
-			ctr++
-			b.WriteString("\t" + e)
-		}
-	}
-	if ctr == 0 {
-		return nil // No errors were encountered
-	}
-	return errors.New(b.String())
-}
-
-// fromConfig returns a Function populated from config.
-// Note that config does not include ancillary fields not serialized, such as Root.
-func fromConfig(c Config) (f Function) {
-	return Function{
-		Name:            c.Name,
-		Namespace:       c.Namespace,
-		Runtime:         c.Runtime,
-		Image:           c.Image,
-		ImageDigest:     c.ImageDigest,
-		Builder:         c.Builder,
-		Builders:        c.Builders,
-		Buildpacks:      c.Buildpacks,
-		HealthEndpoints: c.HealthEndpoints,
-		Volumes:         c.Volumes,
-		BuildEnvs:       c.BuildEnvs,
-		Envs:            c.Envs,
-		Annotations:     c.Annotations,
-		Options:         c.Options,
-		Labels:          c.Labels,
-		Created:         c.Created,
-	}
-}
-
-// toConfig serializes a Function to a config object.
-func toConfig(f Function) Config {
-	return Config{
-		Name:            f.Name,
-		Namespace:       f.Namespace,
-		Runtime:         f.Runtime,
-		Image:           f.Image,
-		ImageDigest:     f.ImageDigest,
-		Builder:         f.Builder,
-		Builders:        f.Builders,
-		Buildpacks:      f.Buildpacks,
-		HealthEndpoints: f.HealthEndpoints,
-		Volumes:         f.Volumes,
-		BuildEnvs:       f.BuildEnvs,
-		Envs:            f.Envs,
-		Annotations:     f.Annotations,
-		Options:         f.Options,
-		Labels:          f.Labels,
-		Created:         f.Created,
-	}
-}
-
-// writeConfig for the given Function out to disk at root.
-func writeConfig(f Function) (err error) {
-	path := filepath.Join(f.Root, ConfigFile)
-	c := toConfig(f)
-	var bb []byte
-	if bb, err = yaml.Marshal(&c); err != nil {
-		return
-	}
-	return ioutil.WriteFile(path, bb, 0644)
-}
-
-// validateVolumes checks that input Volumes are correct and contain all necessary fields.
-// Returns array of error messages, empty if no errors are found
-//
-// Allowed settings:
-// - secret: example-secret              		# mount Secret as Volume
-// 	 path: /etc/secret-volume
-// - configMap: example-configMap              	# mount ConfigMap as Volume
-// 	 path: /etc/configMap-volume
-func validateVolumes(volumes []Volume) (errors []string) {
-
-	for i, vol := range volumes {
-		if vol.Secret != nil && vol.ConfigMap != nil {
-			errors = append(errors, fmt.Sprintf("volume entry #%d is not properly set, both secret '%s' and configMap '%s' can not be set at the same time",
-				i, *vol.Secret, *vol.ConfigMap))
-		} else if vol.Path == nil && vol.Secret == nil && vol.ConfigMap == nil {
-			errors = append(errors, fmt.Sprintf("volume entry #%d is not properly set", i))
-		} else if vol.Path == nil {
-			if vol.Secret != nil {
-				errors = append(errors, fmt.Sprintf("volume entry #%d is missing path field, only secret '%s' is set", i, *vol.Secret))
-			} else if vol.ConfigMap != nil {
-				errors = append(errors, fmt.Sprintf("volume entry #%d is missing path field, only configMap '%s' is set", i, *vol.ConfigMap))
-			}
-		} else if vol.Path != nil && vol.Secret == nil && vol.ConfigMap == nil {
-			errors = append(errors, fmt.Sprintf("volume entry #%d is missing secret or configMap field, only path '%s' is set", i, *vol.Path))
-		}
-	}
-
-	return
-}
-
-// ValidateBuildEnvs checks that input BuildEnvs are correct and contain all necessary fields.
-// Returns array of error messages, empty if no errors are found
-//
-// Allowed settings:
-// - name: EXAMPLE1                					# ENV directly from a value
-//   value: value1
-// - name: EXAMPLE2                 				# ENV from the local ENV var
-//   value: {{ env:MY_ENV }}
-func ValidateBuildEnvs(envs []Env) (errors []string) {
-	for i, env := range envs {
-		if env.Name == nil && env.Value == nil {
-			errors = append(errors, fmt.Sprintf("env entry #%d is not properly set", i))
-		} else if env.Value == nil {
-			errors = append(errors, fmt.Sprintf("env entry #%d is missing value field, only name '%s' is set", i, *env.Name))
-		} else {
-
-			if err := utils.ValidateEnvVarName(*env.Name); err != nil {
-				errors = append(errors, fmt.Sprintf("env entry #%d has invalid name set: %q; %s", i, *env.Name, err.Error()))
-			}
-
-			if strings.HasPrefix(*env.Value, "{{") {
-				// ENV from the local ENV var; {{ env:MY_ENV }}
-				if !regLocalEnv.MatchString(*env.Value) {
-					errors = append(errors,
-						fmt.Sprintf(
-							"env entry #%d with name '%s' has invalid value field set, it has '%s', but allowed is only '{{ env:MY_ENV }}'",
-							i, *env.Name, *env.Value))
-				}
-			}
-		}
-	}
-
-	return
-}
-
-// ValidateEnvs checks that input Envs are correct and contain all necessary fields.
-// Returns array of error messages, empty if no errors are found
-//
-// Allowed settings:
-// - name: EXAMPLE1                					# ENV directly from a value
-//   value: value1
-// - name: EXAMPLE2                 				# ENV from the local ENV var
-//   value: {{ env:MY_ENV }}
-// - name: EXAMPLE3
-//   value: {{ secret:secretName:key }}   			# ENV from a key in secret
-// - value: {{ secret:secretName }}          		# all key-pair values from secret are set as ENV
-// - name: EXAMPLE4
-//   value: {{ configMap:configMapName:key }}   	# ENV from a key in configMap
-// - value: {{ configMap:configMapName }}          	# all key-pair values from configMap are set as ENV
-func ValidateEnvs(envs []Env) (errors []string) {
-	for i, env := range envs {
-		if env.Name == nil && env.Value == nil {
-			errors = append(errors, fmt.Sprintf("env entry #%d is not properly set", i))
-		} else if env.Value == nil {
-			errors = append(errors, fmt.Sprintf("env entry #%d is missing value field, only name '%s' is set", i, *env.Name))
-		} else if env.Name == nil {
-			// all key-pair values from secret are set as ENV; {{ secret:secretName }} or {{ configMap:configMapName }}
-			if !regWholeSecret.MatchString(*env.Value) && !regWholeConfigMap.MatchString(*env.Value) {
-				errors = append(errors, fmt.Sprintf("env entry #%d has invalid value field set, it has '%s', but allowed is only '{{ secret:secretName }}' or '{{ configMap:configMapName }}'",
-					i, *env.Value))
-			}
-		} else {
-
-			if err := utils.ValidateEnvVarName(*env.Name); err != nil {
-				errors = append(errors, fmt.Sprintf("env entry #%d has invalid name set: %q; %s", i, *env.Name, err.Error()))
-			}
-
-			if strings.HasPrefix(*env.Value, "{{") {
-				// ENV from the local ENV var; {{ env:MY_ENV }}
-				// or
-				// ENV from a key in secret/configMap;  {{ secret:secretName:key }} or {{ configMap:configMapName:key }}
-				if !regLocalEnv.MatchString(*env.Value) && !regKeyFromSecret.MatchString(*env.Value) && !regKeyFromConfigMap.MatchString(*env.Value) {
-					errors = append(errors,
-						fmt.Sprintf(
-							"env entry #%d with name '%s' has invalid value field set, it has '%s', but allowed is only '{{ env:MY_ENV }}', '{{ secret:secretName:key }}' or '{{ configMap:configMapName:key }}'",
-							i, *env.Name, *env.Value))
-				}
-			}
-		}
-	}
-
-	return
-}
-
-// ValidateLabels checks that input labels are correct and contain all necessary fields.
-// Returns array of error messages, empty if no errors are found
-//
-// Allowed settings:
-// - key: EXAMPLE1                				# label directly from a value
-//   value: value1
-// - key: EXAMPLE2                 				# label from the local ENV var
-//   value: {{ env:MY_ENV }}
-func ValidateLabels(labels []Label) (errors []string) {
-	for i, label := range labels {
-		if label.Key == nil && label.Value == nil {
-			errors = append(errors, fmt.Sprintf("label entry #%d is not properly set", i))
-		} else if label.Key == nil && label.Value != nil {
-			errors = append(errors, fmt.Sprintf("label entry #%d is missing key field, only value '%s' is set", i, *label.Value))
-		} else {
-			if err := utils.ValidateLabelKey(*label.Key); err != nil {
-				errors = append(errors, fmt.Sprintf("label entry #%d has invalid key set: %q; %s", i, *label.Key, err.Error()))
-			}
-			if label.Value != nil {
-				if err := utils.ValidateLabelValue(*label.Value); err != nil {
-					errors = append(errors, fmt.Sprintf("label entry #%d has invalid value set: %q; %s", i, *label.Value, err.Error()))
-				}
-
-				if strings.HasPrefix(*label.Value, "{{") {
-					// ENV from the local ENV var; {{ env:MY_ENV }}
-					if !regLocalEnv.MatchString(*label.Value) {
-						errors = append(errors,
-							fmt.Sprintf(
-								"label entry #%d with key '%s' has invalid value field set, it has '%s', but allowed is only '{{ env:MY_ENV }}'",
-								i, *label.Key, *label.Value))
-					} else {
-						match := regLocalEnv.FindStringSubmatch(*label.Value)
-						value := os.Getenv(match[1])
-						if err := utils.ValidateLabelValue(value); err != nil {
-							errors = append(errors, fmt.Sprintf("label entry #%d with key '%s' has invalid value when the environment is evaluated: '%s': %s", i, *label.Key, value, err.Error()))
-						}
-					}
-				}
-			}
-		}
-	}
-
-	return
-}
-
-// validateOptions checks that input Options are correctly set.
-// Returns array of error messages, empty if no errors are found
-func validateOptions(options Options) (errors []string) {
-
-	// options.scale
-	if options.Scale != nil {
-		if options.Scale.Min != nil {
-			if *options.Scale.Min < 0 {
-				errors = append(errors, fmt.Sprintf("options field \"scale.min\" has invalid value set: %d, the value must be greater than \"0\"",
-					*options.Scale.Min))
-			}
-		}
-
-		if options.Scale.Max != nil {
-			if *options.Scale.Max < 0 {
-				errors = append(errors, fmt.Sprintf("options field \"scale.max\" has invalid value set: %d, the value must be greater than \"0\"",
-					*options.Scale.Max))
-			}
-		}
-
-		if options.Scale.Min != nil && options.Scale.Max != nil {
-			if *options.Scale.Max < *options.Scale.Min {
-				errors = append(errors, "options field \"scale.max\" value must be greater or equal to \"scale.min\"")
-			}
-		}
-
-		if options.Scale.Metric != nil {
-			if *options.Scale.Metric != "concurrency" && *options.Scale.Metric != "rps" {
-				errors = append(errors, fmt.Sprintf("options field \"scale.metric\" has invalid value set: %s, allowed is only \"concurrency\" or \"rps\"",
-					*options.Scale.Metric))
-			}
-		}
-
-		if options.Scale.Target != nil {
-			if *options.Scale.Target < 0.01 {
-				errors = append(errors, fmt.Sprintf("options field \"scale.target\" has value set to \"%f\", but it must not be less than 0.01",
-					*options.Scale.Target))
-			}
-		}
-
-		if options.Scale.Utilization != nil {
-			if *options.Scale.Utilization < 1 || *options.Scale.Utilization > 100 {
-				errors = append(errors,
-					fmt.Sprintf("options field \"scale.utilization\" has value set to \"%f\", but it must not be less than 1 or greater than 100",
-						*options.Scale.Utilization))
-			}
-		}
-	}
-
-	// options.resource
-	if options.Resources != nil {
-
-		// options.resource.requests
-		if options.Resources.Requests != nil {
-
-			if options.Resources.Requests.CPU != nil {
-				_, err := resource.ParseQuantity(*options.Resources.Requests.CPU)
-				if err != nil {
-					errors = append(errors, fmt.Sprintf("options field \"resources.requests.cpu\" has invalid value set: \"%s\"; \"%s\"",
-						*options.Resources.Requests.CPU, err.Error()))
-				}
-			}
-
-			if options.Resources.Requests.Memory != nil {
-				_, err := resource.ParseQuantity(*options.Resources.Requests.Memory)
-				if err != nil {
-					errors = append(errors, fmt.Sprintf("options field \"resources.requests.memory\" has invalid value set: \"%s\"; \"%s\"",
-						*options.Resources.Requests.Memory, err.Error()))
-				}
-			}
-		}
-
-		// options.resource.limits
-		if options.Resources.Limits != nil {
-
-			if options.Resources.Limits.CPU != nil {
-				_, err := resource.ParseQuantity(*options.Resources.Limits.CPU)
-				if err != nil {
-					errors = append(errors, fmt.Sprintf("options field \"resources.limits.cpu\" has invalid value set: \"%s\"; \"%s\"",
-						*options.Resources.Limits.CPU, err.Error()))
-				}
-			}
-
-			if options.Resources.Limits.Memory != nil {
-				_, err := resource.ParseQuantity(*options.Resources.Limits.Memory)
-				if err != nil {
-					errors = append(errors, fmt.Sprintf("options field \"resources.limits.memory\" has invalid value set: \"%s\"; \"%s\"",
-						*options.Resources.Limits.Memory, err.Error()))
-				}
-			}
-
-			if options.Resources.Limits.Concurrency != nil {
-				if *options.Resources.Limits.Concurrency < 0 {
-					errors = append(errors, fmt.Sprintf("options field \"resources.limits.concurrency\" has value set to \"%d\", but it must not be less than 0",
-						*options.Resources.Limits.Concurrency))
-				}
-			}
-		}
-	}
-
-	return
-}
+// Config is local and global configuration which is not "part of the Function"
+// and is thus not likely to be tracked in source control.
+type Config struct{}
diff --git a/docs/guides/templates.md b/docs/guides/templates.md
index 9e116e46..16c44b51 100644
--- a/docs/guides/templates.md
+++ b/docs/guides/templates.md
@@ -2,7 +2,7 @@
 
 When a Function is created, an example implementation and a Function metadata file are written into the new Function's working directory.  Together, these files are referred to as the Function's Template.  Included are the templates 'http' and 'events' for each supported language runtime.
 
-These embedded templates are minimal by design.  The Function contains a minimum of external dependencies, and the 'func.yaml' defines a final environment within which the Funciton will execute that is devoid of any extraneous packages or services.
+These embedded templates are minimal by design.  The Function contains a minimum of external dependencies, and the 'func.yaml' defines a final environment within which the Function will execute that is devoid of any extraneous packages or services.
 
 To make use of more complex initial Function implementions, or to define runtime environments with arbitrarily complex requirements, the templates system is fully pluggable.
 
diff --git a/function.go b/function.go
index fed90928..ddccc217 100644
--- a/function.go
+++ b/function.go
@@ -6,26 +6,32 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"time"
+
+	"gopkg.in/yaml.v2"
 )
 
+// FunctionFile is the file used for the serialized form of a Function.
+const FunctionFile = "func.yaml"
+
 type Function struct {
 	// Root on disk at which to find/create source and config files.
-	Root string
+	Root string `yaml:"-"`
 
 	// Name of the Function.  If not provided, path derivation is attempted when
 	// requried (such as for initialization).
-	Name string
+	Name string `yaml:"name" jsonschema:"pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"`
 
 	// Namespace into which the Function is deployed on supported platforms.
-	Namespace string
+	Namespace string `yaml:"namespace"`
 
 	// Runtime is the language plus context.  nodejs|go|quarkus|rust etc.
-	Runtime string
+	Runtime string `yaml:"runtime"`
 
 	// Template for the Function.
-	Template string
+	Template string `yaml:"-"`
 
 	// Registry at which to store interstitial containers, in the form
 	// [registry]/[user].
@@ -40,42 +46,42 @@ type Function struct {
 	//   alice/my.function.name
 	// If Image is provided, it overrides the default of concatenating
 	// "Registry+Name:latest" to derive the Image.
-	Image string
+	Image string `yaml:"image"`
 
 	// SHA256 hash of the latest image that has been built
-	ImageDigest string
+	ImageDigest string `yaml:"imageDigest"`
 
 	// Builder represents the CNCF Buildpack builder image for a function
-	Builder string
+	Builder string `yaml:"builder"`
 
 	// Map containing known builders.
 	// e.g. { "jvm": "docker.io/example/quarkus-jvm-builder" }
-	Builders map[string]string
+	Builders map[string]string `yaml:"builders"`
 
 	// Optional list of buildpacks to use when building the function
-	Buildpacks []string
+	Buildpacks []string `yaml:"buildpacks"`
 
 	// List of volumes to be mounted to the function
-	Volumes []Volume
+	Volumes []Volume `yaml:"volumes"`
 
 	// Build Env variables to be set
-	BuildEnvs []Env
+	BuildEnvs []Env `yaml:"buildEnvs"`
 
 	// Env variables to be set
-	Envs []Env
+	Envs []Env `yaml:"envs"`
 
 	// Map containing user-supplied annotations
 	// Example: { "division": "finance" }
-	Annotations map[string]string
+	Annotations map[string]string `yaml:"annotations"`
 
 	// Options to be set on deployed function (scaling, etc.)
-	Options Options
+	Options Options `yaml:"options"`
 
 	// Map of user-supplied labels
-	Labels []Label
+	Labels []Label `yaml:"labels"`
 
 	// Health endpoints specified by the language pack
-	HealthEndpoints HealthEndpoints
+	HealthEndpoints HealthEndpoints `yaml:"healthEndpoints"`
 
 	// Created time is the moment that creation was successfully completed
 	// according to the client which is in charge of what constitutes being
@@ -83,66 +89,88 @@ type Function struct {
 	Created time.Time
 }
 
-// NewFunction loads a Function from a path on disk. use .Initialized() to determine if
-// the path contained an initialized Function.
-// NewFunction creates a Function struct whose attributes are loaded from the
-// configuraiton located at path.
+// NewFunction loads a Function from a path on disk.
+// Errors are returned if the path is not valid, the serialized field could not
+// be accessed, or if the contents of the file could not be unmarshaled into a
+// Function.  A valid path with no associated FunctionFile is not an error but
+// rather returns a Function with static defaults set, and will return false
+// from .Initialized().
 func NewFunction(root string) (f Function, err error) {
+	// NewFunction is essentially a convenience/decorator over the more fully-
+	// featured constructor which takes a full function object as defaults.
+	return NewFunctionFromDefaults(Function{Root: root})
+}
 
-	// Expand the passed root to its absolute path (default current dir)
-	if root, err = filepath.Abs(root); err != nil {
-		return
+// NewFunctionFromDefaults is equivalent to calling NewFunction, but will use
+// the provided function as defaults.
+func NewFunctionFromDefaults(f Function) (Function, error) {
+	var err error
+	if f.Runtime == "" {
+		f.Runtime = DefaultRuntime
 	}
-
-	// Load a Config from the given absolute path
-	c, err := newConfig(root)
-	if err != nil {
-		return
+	if f.Template == "" {
+		f.Template = DefaultTemplate
 	}
-
-	// Let's set Function name, if it is not already set
-	if c.Name == "" {
-		pathParts := strings.Split(strings.TrimRight(root, string(os.PathSeparator)), string(os.PathSeparator))
-		c.Name = pathParts[len(pathParts)-1]
+	if f.Root, err = filepath.Abs(f.Root); err != nil {
+		return f, err
 	}
+	if f.Name == "" {
+		f.Name = nameFromPath(f.Root)
+	}
+	return unmarshalFunction(f)
+}
 
-	// set Function to the value of the config loaded from disk.
-	f = fromConfig(c)
-
-	// The only value not included in the config is the effective path on disk
-	f.Root = root
-	return
+// nameFromPath returns the default name for a Function derived from a path.
+// This consists of the last directory in the given path, if derivable (empty
+// paths, paths consisting of all slashes, etc. return the zero value "")
+func nameFromPath(path string) string {
+	pathParts := strings.Split(strings.TrimRight(path, string(os.PathSeparator)), string(os.PathSeparator))
+	return pathParts[len(pathParts)-1]
+	/* the above may have edge conditions as it assumes the trailing value
+	 * is a directory name.  If errors are encountered, we _may_ need to use the
+	 * inbuilt logic in the std lib and either check if the path indicated is a
+	 * directory (appending slash) and then run:
+					 base := filepath.Base(filepath.Dir(path))
+					 if base == string(os.PathSeparator) || base == "." {
+									 return "" // Consider it underivable: string zero value
+					 }
+					 return base
+	*/
 }
 
-// WriteConfig writes this Function's configuration to disk.
-func (f Function) WriteConfig() (err error) {
-	return writeConfig(f)
+// Write aka (save, serialize, marshal) the Function to disk at its path.
+func (f Function) Write() (err error) {
+	path := filepath.Join(f.Root, FunctionFile)
+	var bb []byte
+	if bb, err = yaml.Marshal(&f); err != nil {
+		return
+	}
+	// TODO: open existing file for writing, such that existing permissions
+	// are preserved.
+	return ioutil.WriteFile(path, bb, 0644)
 }
 
 // Initialized returns if the Function has been initialized.
 // Any errors are considered failure (invalid or inaccessible root, config file, etc).
 func (f Function) Initialized() bool {
-	// Load the Function's configuration from disk and check if the (required) value Runtime is populated.
-	c, err := newConfig(f.Root)
-	if err != nil {
-		return false
-	}
-
-	return c.Runtime != "" && c.Name != ""
+	return !f.Created.IsZero()
 }
 
 // Built indicates the Function has been built.  Does not guarantee the
 // image indicated actually exists, just that it _should_ exist based off
-// the current state of the Funciton object, in particular the value of
+// the current state of the Function object, in particular the value of
 // the Image and ImageDiget fields.
 func (f Function) Built() bool {
 	// If Image (the override) and ImageDigest (the most recent build stamp) are
 	// both empty, the Function is considered unbuilt.
+	// TODO: upgrade to a "build complete" timestamp.
 	return f.Image != "" || f.ImageDigest != ""
 }
 
 // ImageWithDigest returns the full reference to the image including SHA256 Digest.
 // If Digest is empty, image:tag is returned.
+// TODO: Populate this only on a successful deploy, as this results on a dirty
+// git tree on every build.
 func (f Function) ImageWithDigest() string {
 	// Return image, if Digest is empty
 	if f.ImageDigest == "" {
@@ -170,6 +198,8 @@ func (f Function) ImageWithDigest() string {
 // 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 {
@@ -249,7 +279,7 @@ func assertEmptyRoot(path string) (err error) {
 // contentiousFiles are files which, if extant, preclude the creation of a
 // Function rooted in the given directory.
 var contentiousFiles = []string{
-	ConfigFile,
+	FunctionFile,
 }
 
 // contentiousFilesIn the given directoy
@@ -279,3 +309,94 @@ func isEffectivelyEmpty(dir string) (bool, error) {
 	}
 	return true, nil
 }
+
+// unmarshalFunction from disk (FunctionFile) using the passed Function as
+// its defaults.  If no serialized function exists at path, the Function
+// returned is equivalent to the default passed.
+func unmarshalFunction(f Function) (Function, error) {
+	var err error
+	var filename = filepath.Join(f.Root, FunctionFile)
+
+	// Return if there is no file to load, or if there is an error reading.
+	if _, err = os.Stat(filename); err != nil {
+		if os.IsNotExist(err) {
+			err = nil // missing file is not an error.
+		}
+		return f, err
+	}
+
+	// Load the file
+	bb, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return f, err
+	}
+
+	// Unmarshal as yaml
+	if err = yaml.UnmarshalStrict(bb, &f); err != nil {
+		// Return immediately if there are syntactic errors.
+		return f, formatUnmarshalError(err)
+	}
+
+	return f, validateFunction(f)
+}
+
+// Validate Function is logically correct, returning a bundled, and quite
+// verbose, formatted error detailing any issues.
+func validateFunction(f Function) error {
+	var ctr int
+	errs := [][]string{
+		validateVolumes(f.Volumes),
+		ValidateBuildEnvs(f.BuildEnvs),
+		ValidateEnvs(f.Envs),
+		validateOptions(f.Options),
+		ValidateLabels(f.Labels),
+	}
+
+	var b strings.Builder
+	b.WriteString(fmt.Sprintf("'%v' contains errors:", FunctionFile))
+
+	for _, ee := range errs {
+		if len(ee) > 0 {
+			b.WriteString("\n") // Precede each group of errors with a linebreak
+		}
+		for _, e := range ee {
+			ctr++
+			b.WriteString("\t" + e)
+		}
+	}
+
+	if ctr == 0 {
+		return nil // Return nil if there were no validation errors.
+	}
+
+	return errors.New(b.String())
+}
+
+// Format yaml unmarshall error to be more human friendly.
+func formatUnmarshalError(err error) error {
+	var (
+		e      = err.Error()
+		rxp    = regexp.MustCompile("not found in type .*")
+		header = fmt.Sprintf("'%v' is not valid:\n", FunctionFile)
+	)
+
+	if strings.HasPrefix(e, "yaml: unmarshal errors:") {
+		e = rxp.ReplaceAllString(e, "is not valid")
+		e = strings.Replace(e, "yaml: unmarshal errors:\n", header, 1)
+	} else if strings.HasPrefix(e, "yaml:") {
+		e = rxp.ReplaceAllString(e, "is not valid")
+		e = strings.Replace(e, "yaml: ", header+"  ", 1)
+	}
+
+	return errors.New(e)
+}
+
+// Regex used during instantiation and validation of various Function fields
+// by labels, envs, options, etc.
+var (
+	regWholeSecret      = regexp.MustCompile(`^{{\s*secret:((?:\w|['-]\w)+)\s*}}$`)
+	regKeyFromSecret    = regexp.MustCompile(`^{{\s*secret:((?:\w|['-]\w)+):([-._a-zA-Z0-9]+)\s*}}$`)
+	regWholeConfigMap   = regexp.MustCompile(`^{{\s*configMap:((?:\w|['-]\w)+)\s*}}$`)
+	regKeyFromConfigMap = regexp.MustCompile(`^{{\s*configMap:((?:\w|['-]\w)+):([-._a-zA-Z0-9]+)\s*}}$`)
+	regLocalEnv         = regexp.MustCompile(`^{{\s*env:(\w+)\s*}}$`)
+)
diff --git a/function_envs.go b/function_envs.go
new file mode 100644
index 00000000..ea244bc0
--- /dev/null
+++ b/function_envs.go
@@ -0,0 +1,126 @@
+package function
+
+import (
+	"fmt"
+	"strings"
+
+	"knative.dev/kn-plugin-func/utils"
+)
+
+type Env struct {
+	Name  *string `yaml:"name,omitempty" jsonschema:"pattern=^[-._a-zA-Z][-._a-zA-Z0-9]*$"`
+	Value *string `yaml:"value"`
+}
+
+func (e Env) String() string {
+	if e.Name == nil && e.Value != nil {
+		match := regWholeSecret.FindStringSubmatch(*e.Value)
+		if len(match) == 2 {
+			return fmt.Sprintf("All key=value pairs from Secret \"%s\"", match[1])
+		}
+		match = regWholeConfigMap.FindStringSubmatch(*e.Value)
+		if len(match) == 2 {
+			return fmt.Sprintf("All key=value pairs from ConfigMap \"%s\"", match[1])
+		}
+	} else if e.Name != nil && e.Value != nil {
+		match := regKeyFromSecret.FindStringSubmatch(*e.Value)
+		if len(match) == 3 {
+			return fmt.Sprintf("Env \"%s\" with value set from key \"%s\" from Secret \"%s\"", *e.Name, match[2], match[1])
+		}
+		match = regKeyFromConfigMap.FindStringSubmatch(*e.Value)
+		if len(match) == 3 {
+			return fmt.Sprintf("Env \"%s\" with value set from key \"%s\" from ConfigMap \"%s\"", *e.Name, match[2], match[1])
+		}
+		match = regLocalEnv.FindStringSubmatch(*e.Value)
+		if len(match) == 2 {
+			return fmt.Sprintf("Env \"%s\" with value set from local env variable \"%s\"", *e.Name, match[1])
+		}
+
+		return fmt.Sprintf("Env \"%s\" with value \"%s\"", *e.Name, *e.Value)
+	}
+	return ""
+}
+
+// ValidateEnvs checks that input Envs are correct and contain all necessary fields.
+// Returns array of error messages, empty if no errors are found
+//
+// Allowed settings:
+// - name: EXAMPLE1                					# ENV directly from a value
+//   value: value1
+// - name: EXAMPLE2                 				# ENV from the local ENV var
+//   value: {{ env:MY_ENV }}
+// - name: EXAMPLE3
+//   value: {{ secret:secretName:key }}   			# ENV from a key in secret
+// - value: {{ secret:secretName }}          		# all key-pair values from secret are set as ENV
+// - name: EXAMPLE4
+//   value: {{ configMap:configMapName:key }}   	# ENV from a key in configMap
+// - value: {{ configMap:configMapName }}          	# all key-pair values from configMap are set as ENV
+func ValidateEnvs(envs []Env) (errors []string) {
+	for i, env := range envs {
+		if env.Name == nil && env.Value == nil {
+			errors = append(errors, fmt.Sprintf("env entry #%d is not properly set", i))
+		} else if env.Value == nil {
+			errors = append(errors, fmt.Sprintf("env entry #%d is missing value field, only name '%s' is set", i, *env.Name))
+		} else if env.Name == nil {
+			// all key-pair values from secret are set as ENV; {{ secret:secretName }} or {{ configMap:configMapName }}
+			if !regWholeSecret.MatchString(*env.Value) && !regWholeConfigMap.MatchString(*env.Value) {
+				errors = append(errors, fmt.Sprintf("env entry #%d has invalid value field set, it has '%s', but allowed is only '{{ secret:secretName }}' or '{{ configMap:configMapName }}'",
+					i, *env.Value))
+			}
+		} else {
+
+			if err := utils.ValidateEnvVarName(*env.Name); err != nil {
+				errors = append(errors, fmt.Sprintf("env entry #%d has invalid name set: %q; %s", i, *env.Name, err.Error()))
+			}
+
+			if strings.HasPrefix(*env.Value, "{{") {
+				// ENV from the local ENV var; {{ env:MY_ENV }}
+				// or
+				// ENV from a key in secret/configMap;  {{ secret:secretName:key }} or {{ configMap:configMapName:key }}
+				if !regLocalEnv.MatchString(*env.Value) && !regKeyFromSecret.MatchString(*env.Value) && !regKeyFromConfigMap.MatchString(*env.Value) {
+					errors = append(errors,
+						fmt.Sprintf(
+							"env entry #%d with name '%s' has invalid value field set, it has '%s', but allowed is only '{{ env:MY_ENV }}', '{{ secret:secretName:key }}' or '{{ configMap:configMapName:key }}'",
+							i, *env.Name, *env.Value))
+				}
+			}
+		}
+	}
+
+	return
+}
+
+// ValidateBuildEnvs checks that input BuildEnvs are correct and contain all necessary fields.
+// Returns array of error messages, empty if no errors are found
+//
+// Allowed settings:
+// - name: EXAMPLE1                					# ENV directly from a value
+//   value: value1
+// - name: EXAMPLE2                 				# ENV from the local ENV var
+//   value: {{ env:MY_ENV }}
+func ValidateBuildEnvs(envs []Env) (errors []string) {
+	for i, env := range envs {
+		if env.Name == nil && env.Value == nil {
+			errors = append(errors, fmt.Sprintf("env entry #%d is not properly set", i))
+		} else if env.Value == nil {
+			errors = append(errors, fmt.Sprintf("env entry #%d is missing value field, only name '%s' is set", i, *env.Name))
+		} else {
+
+			if err := utils.ValidateEnvVarName(*env.Name); err != nil {
+				errors = append(errors, fmt.Sprintf("env entry #%d has invalid name set: %q; %s", i, *env.Name, err.Error()))
+			}
+
+			if strings.HasPrefix(*env.Value, "{{") {
+				// ENV from the local ENV var; {{ env:MY_ENV }}
+				if !regLocalEnv.MatchString(*env.Value) {
+					errors = append(errors,
+						fmt.Sprintf(
+							"env entry #%d with name '%s' has invalid value field set, it has '%s', but allowed is only '{{ env:MY_ENV }}'",
+							i, *env.Name, *env.Value))
+				}
+			}
+		}
+	}
+
+	return
+}
diff --git a/function_labels.go b/function_labels.go
new file mode 100644
index 00000000..87d55c39
--- /dev/null
+++ b/function_labels.go
@@ -0,0 +1,75 @@
+package function
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"knative.dev/kn-plugin-func/utils"
+)
+
+type Label struct {
+	// Key consist of optional prefix part (ended by '/') and name part
+	// Prefix part validation pattern: [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
+	// Name part validation pattern: ([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]
+	Key   *string `yaml:"key" jsonschema:"pattern=^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\\/)?([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$"`
+	Value *string `yaml:"value,omitempty" jsonschema:"pattern=^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$"`
+}
+
+func (l Label) String() string {
+	if l.Key != nil && l.Value == nil {
+		return fmt.Sprintf("Label with key \"%s\"", *l.Key)
+	} else if l.Key != nil && l.Value != nil {
+		match := regLocalEnv.FindStringSubmatch(*l.Value)
+		if len(match) == 2 {
+			return fmt.Sprintf("Label with key \"%s\" and value set from local env variable \"%s\"", *l.Key, match[1])
+		}
+		return fmt.Sprintf("Label with key \"%s\" and value \"%s\"", *l.Key, *l.Value)
+	}
+	return ""
+}
+
+// ValidateLabels checks that input labels are correct and contain all necessary fields.
+// Returns array of error messages, empty if no errors are found
+//
+// Allowed settings:
+// - key: EXAMPLE1                				# label directly from a value
+//   value: value1
+// - key: EXAMPLE2                 				# label from the local ENV var
+//   value: {{ env:MY_ENV }}
+func ValidateLabels(labels []Label) (errors []string) {
+	for i, label := range labels {
+		if label.Key == nil && label.Value == nil {
+			errors = append(errors, fmt.Sprintf("label entry #%d is not properly set", i))
+		} else if label.Key == nil && label.Value != nil {
+			errors = append(errors, fmt.Sprintf("label entry #%d is missing key field, only value '%s' is set", i, *label.Value))
+		} else {
+			if err := utils.ValidateLabelKey(*label.Key); err != nil {
+				errors = append(errors, fmt.Sprintf("label entry #%d has invalid key set: %q; %s", i, *label.Key, err.Error()))
+			}
+			if label.Value != nil {
+				if err := utils.ValidateLabelValue(*label.Value); err != nil {
+					errors = append(errors, fmt.Sprintf("label entry #%d has invalid value set: %q; %s", i, *label.Value, err.Error()))
+				}
+
+				if strings.HasPrefix(*label.Value, "{{") {
+					// ENV from the local ENV var; {{ env:MY_ENV }}
+					if !regLocalEnv.MatchString(*label.Value) {
+						errors = append(errors,
+							fmt.Sprintf(
+								"label entry #%d with key '%s' has invalid value field set, it has '%s', but allowed is only '{{ env:MY_ENV }}'",
+								i, *label.Key, *label.Value))
+					} else {
+						match := regLocalEnv.FindStringSubmatch(*label.Value)
+						value := os.Getenv(match[1])
+						if err := utils.ValidateLabelValue(value); err != nil {
+							errors = append(errors, fmt.Sprintf("label entry #%d with key '%s' has invalid value when the environment is evaluated: '%s': %s", i, *label.Key, value, err.Error()))
+						}
+					}
+				}
+			}
+		}
+	}
+
+	return
+}
diff --git a/function_options.go b/function_options.go
new file mode 100644
index 00000000..19d750a3
--- /dev/null
+++ b/function_options.go
@@ -0,0 +1,139 @@
+package function
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+type Options struct {
+	Scale     *ScaleOptions     `yaml:"scale,omitempty"`
+	Resources *ResourcesOptions `yaml:"resources,omitempty"`
+}
+
+type ScaleOptions struct {
+	Min         *int64   `yaml:"min,omitempty" jsonschema_extras:"minimum=0"`
+	Max         *int64   `yaml:"max,omitempty" jsonschema_extras:"minimum=0"`
+	Metric      *string  `yaml:"metric,omitempty" jsonschema:"enum=concurrency,enum=rps"`
+	Target      *float64 `yaml:"target,omitempty" jsonschema_extras:"minimum=0.01"`
+	Utilization *float64 `yaml:"utilization,omitempty" jsonschema:"minimum=1,maximum=100"`
+}
+
+type ResourcesOptions struct {
+	Requests *ResourcesRequestsOptions `yaml:"requests,omitempty"`
+	Limits   *ResourcesLimitsOptions   `yaml:"limits,omitempty"`
+}
+
+type ResourcesLimitsOptions struct {
+	CPU         *string `yaml:"cpu,omitempty" jsonschema:"pattern=^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$"`
+	Memory      *string `yaml:"memory,omitempty" jsonschema:"pattern=^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$"`
+	Concurrency *int64  `yaml:"concurrency,omitempty" jsonschema_extras:"minimum=0"`
+}
+
+type ResourcesRequestsOptions struct {
+	CPU    *string `yaml:"cpu,omitempty" jsonschema:"pattern=^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$"`
+	Memory *string `yaml:"memory,omitempty" jsonschema:"pattern=^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$"`
+}
+
+// validateOptions checks that input Options are correctly set.
+// Returns array of error messages, empty if no errors are found
+func validateOptions(options Options) (errors []string) {
+
+	// options.scale
+	if options.Scale != nil {
+		if options.Scale.Min != nil {
+			if *options.Scale.Min < 0 {
+				errors = append(errors, fmt.Sprintf("options field \"scale.min\" has invalid value set: %d, the value must be greater than \"0\"",
+					*options.Scale.Min))
+			}
+		}
+
+		if options.Scale.Max != nil {
+			if *options.Scale.Max < 0 {
+				errors = append(errors, fmt.Sprintf("options field \"scale.max\" has invalid value set: %d, the value must be greater than \"0\"",
+					*options.Scale.Max))
+			}
+		}
+
+		if options.Scale.Min != nil && options.Scale.Max != nil {
+			if *options.Scale.Max < *options.Scale.Min {
+				errors = append(errors, "options field \"scale.max\" value must be greater or equal to \"scale.min\"")
+			}
+		}
+
+		if options.Scale.Metric != nil {
+			if *options.Scale.Metric != "concurrency" && *options.Scale.Metric != "rps" {
+				errors = append(errors, fmt.Sprintf("options field \"scale.metric\" has invalid value set: %s, allowed is only \"concurrency\" or \"rps\"",
+					*options.Scale.Metric))
+			}
+		}
+
+		if options.Scale.Target != nil {
+			if *options.Scale.Target < 0.01 {
+				errors = append(errors, fmt.Sprintf("options field \"scale.target\" has value set to \"%f\", but it must not be less than 0.01",
+					*options.Scale.Target))
+			}
+		}
+
+		if options.Scale.Utilization != nil {
+			if *options.Scale.Utilization < 1 || *options.Scale.Utilization > 100 {
+				errors = append(errors,
+					fmt.Sprintf("options field \"scale.utilization\" has value set to \"%f\", but it must not be less than 1 or greater than 100",
+						*options.Scale.Utilization))
+			}
+		}
+	}
+
+	// options.resource
+	if options.Resources != nil {
+
+		// options.resource.requests
+		if options.Resources.Requests != nil {
+
+			if options.Resources.Requests.CPU != nil {
+				_, err := resource.ParseQuantity(*options.Resources.Requests.CPU)
+				if err != nil {
+					errors = append(errors, fmt.Sprintf("options field \"resources.requests.cpu\" has invalid value set: \"%s\"; \"%s\"",
+						*options.Resources.Requests.CPU, err.Error()))
+				}
+			}
+
+			if options.Resources.Requests.Memory != nil {
+				_, err := resource.ParseQuantity(*options.Resources.Requests.Memory)
+				if err != nil {
+					errors = append(errors, fmt.Sprintf("options field \"resources.requests.memory\" has invalid value set: \"%s\"; \"%s\"",
+						*options.Resources.Requests.Memory, err.Error()))
+				}
+			}
+		}
+
+		// options.resource.limits
+		if options.Resources.Limits != nil {
+
+			if options.Resources.Limits.CPU != nil {
+				_, err := resource.ParseQuantity(*options.Resources.Limits.CPU)
+				if err != nil {
+					errors = append(errors, fmt.Sprintf("options field \"resources.limits.cpu\" has invalid value set: \"%s\"; \"%s\"",
+						*options.Resources.Limits.CPU, err.Error()))
+				}
+			}
+
+			if options.Resources.Limits.Memory != nil {
+				_, err := resource.ParseQuantity(*options.Resources.Limits.Memory)
+				if err != nil {
+					errors = append(errors, fmt.Sprintf("options field \"resources.limits.memory\" has invalid value set: \"%s\"; \"%s\"",
+						*options.Resources.Limits.Memory, err.Error()))
+				}
+			}
+
+			if options.Resources.Limits.Concurrency != nil {
+				if *options.Resources.Limits.Concurrency < 0 {
+					errors = append(errors, fmt.Sprintf("options field \"resources.limits.concurrency\" has value set to \"%d\", but it must not be less than 0",
+						*options.Resources.Limits.Concurrency))
+				}
+			}
+		}
+	}
+
+	return
+}
diff --git a/function_volumes.go b/function_volumes.go
new file mode 100644
index 00000000..fd85dd07
--- /dev/null
+++ b/function_volumes.go
@@ -0,0 +1,49 @@
+package function
+
+import "fmt"
+
+type Volume struct {
+	Secret    *string `yaml:"secret,omitempty" jsonschema:"oneof_required=secret"`
+	ConfigMap *string `yaml:"configMap,omitempty" jsonschema:"oneof_required=configmap"`
+	Path      *string `yaml:"path"`
+}
+
+func (v Volume) String() string {
+	if v.ConfigMap != nil {
+		return fmt.Sprintf("ConfigMap \"%s\" mounted at path: \"%s\"", *v.ConfigMap, *v.Path)
+	} else if v.Secret != nil {
+		return fmt.Sprintf("Secret \"%s\" mounted at path: \"%s\"", *v.Secret, *v.Path)
+	}
+
+	return ""
+}
+
+// validateVolumes checks that input Volumes are correct and contain all necessary fields.
+// Returns array of error messages, empty if no errors are found
+//
+// Allowed settings:
+// - secret: example-secret              		# mount Secret as Volume
+// 	 path: /etc/secret-volume
+// - configMap: example-configMap              	# mount ConfigMap as Volume
+// 	 path: /etc/configMap-volume
+func validateVolumes(volumes []Volume) (errors []string) {
+
+	for i, vol := range volumes {
+		if vol.Secret != nil && vol.ConfigMap != nil {
+			errors = append(errors, fmt.Sprintf("volume entry #%d is not properly set, both secret '%s' and configMap '%s' can not be set at the same time",
+				i, *vol.Secret, *vol.ConfigMap))
+		} else if vol.Path == nil && vol.Secret == nil && vol.ConfigMap == nil {
+			errors = append(errors, fmt.Sprintf("volume entry #%d is not properly set", i))
+		} else if vol.Path == nil {
+			if vol.Secret != nil {
+				errors = append(errors, fmt.Sprintf("volume entry #%d is missing path field, only secret '%s' is set", i, *vol.Secret))
+			} else if vol.ConfigMap != nil {
+				errors = append(errors, fmt.Sprintf("volume entry #%d is missing path field, only configMap '%s' is set", i, *vol.ConfigMap))
+			}
+		} else if vol.Path != nil && vol.Secret == nil && vol.ConfigMap == nil {
+			errors = append(errors, fmt.Sprintf("volume entry #%d is missing secret or configMap field, only path '%s' is set", i, *vol.Path))
+		}
+	}
+
+	return
+}
diff --git a/schema/func_yaml-schema.json b/schema/func_yaml-schema.json
index 5f1983a7..31426746 100644
--- a/schema/func_yaml-schema.json
+++ b/schema/func_yaml-schema.json
@@ -1,25 +1,42 @@
 {
 	"$schema": "http://json-schema.org/draft-04/schema#",
-	"$ref": "#/definitions/Config",
+	"$ref": "#/definitions/Function",
 	"definitions": {
-		"Config": {
+		"Env": {
+			"required": [
+				"value"
+			],
+			"properties": {
+				"name": {
+					"pattern": "^[-._a-zA-Z][-._a-zA-Z0-9]*$",
+					"type": "string"
+				},
+				"value": {
+					"type": "string"
+				}
+			},
+			"additionalProperties": false,
+			"type": "object"
+		},
+		"Function": {
 			"required": [
 				"name",
 				"namespace",
 				"runtime",
+				"Registry",
 				"image",
 				"imageDigest",
 				"builder",
 				"builders",
 				"buildpacks",
-				"healthEndpoints",
 				"volumes",
 				"buildEnvs",
 				"envs",
 				"annotations",
 				"options",
 				"labels",
-				"created"
+				"healthEndpoints",
+				"Created"
 			],
 			"properties": {
 				"name": {
@@ -32,6 +49,9 @@
 				"runtime": {
 					"type": "string"
 				},
+				"Registry": {
+					"type": "string"
+				},
 				"image": {
 					"type": "string"
 				},
@@ -55,10 +75,6 @@
 					},
 					"type": "array"
 				},
-				"healthEndpoints": {
-					"$schema": "http://json-schema.org/draft-04/schema#",
-					"$ref": "#/definitions/HealthEndpoints"
-				},
 				"volumes": {
 					"items": {
 						"$schema": "http://json-schema.org/draft-04/schema#",
@@ -98,7 +114,11 @@
 					},
 					"type": "array"
 				},
-				"created": {
+				"healthEndpoints": {
+					"$schema": "http://json-schema.org/draft-04/schema#",
+					"$ref": "#/definitions/HealthEndpoints"
+				},
+				"Created": {
 					"type": "string",
 					"format": "date-time"
 				}
@@ -106,22 +126,6 @@
 			"additionalProperties": false,
 			"type": "object"
 		},
-		"Env": {
-			"required": [
-				"value"
-			],
-			"properties": {
-				"name": {
-					"pattern": "^[-._a-zA-Z][-._a-zA-Z0-9]*$",
-					"type": "string"
-				},
-				"value": {
-					"type": "string"
-				}
-			},
-			"additionalProperties": false,
-			"type": "object"
-		},
 		"HealthEndpoints": {
 			"properties": {
 				"liveness": {
diff --git a/schema/generator/main.go b/schema/generator/main.go
index c1f48c7a..516899aa 100644
--- a/schema/generator/main.go
+++ b/schema/generator/main.go
@@ -23,7 +23,7 @@ func main() {
 // Genereated schema is written into schema/func_yaml-schema.json file
 func generateFuncYamlSchema() error {
 	// generate json schema for Function struct
-	js := jsonschema.Reflect(&fn.Config{})
+	js := jsonschema.Reflect(&fn.Function{})
 	schema, err := js.MarshalJSON()
 	if err != nil {
 		return err


Test patch:
diff --git a/client_test.go b/client_test.go
index bbb7b5e0..310fd958 100644
--- a/client_test.go
+++ b/client_test.go
@@ -38,7 +38,7 @@ func TestNew(t *testing.T) {
 	root := "testdata/example.com/testNew"
 	defer Using(t, root)()
 
-	client := fn.New(fn.WithRegistry(TestRegistry))
+	client := fn.New(fn.WithRegistry(TestRegistry), fn.WithVerbose(true))
 
 	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
 		t.Fatal(err)
@@ -58,13 +58,13 @@ func TestWritesTemplate(t *testing.T) {
 	}
 
 	// Assert the standard config file was written
-	if _, err := os.Stat(filepath.Join(root, fn.ConfigFile)); os.IsNotExist(err) {
-		t.Fatalf("Initialize did not result in '%v' being written to '%v'", fn.ConfigFile, root)
+	if _, err := os.Stat(filepath.Join(root, fn.FunctionFile)); os.IsNotExist(err) {
+		t.Fatalf("Initialize did not result in '%v' being written to '%v'", fn.FunctionFile, root)
 	}
 
 	// Assert a file from the template was written
 	if _, err := os.Stat(filepath.Join(root, "README.md")); os.IsNotExist(err) {
-		t.Fatalf("Initialize did not result in '%v' being written to '%v'", fn.ConfigFile, root)
+		t.Fatalf("Initialize did not result in '%v' being written to '%v'", fn.FunctionFile, root)
 	}
 }
 
diff --git a/config_test.go b/config_test.go
index cdd790fb..d67574d4 100644
--- a/config_test.go
+++ b/config_test.go
@@ -1,1305 +1,36 @@
 //go:build !integration
 // +build !integration
 
-package function
+package function_test
+
+// Config tests do not have private access, as they are testing manifest
+// behavior of the public package interface.  For implementation tests, see
+// files suffixed '_unit_test.go'.
 
 import (
-	"os"
 	"testing"
-
-	"knative.dev/pkg/ptr"
 )
 
-func Test_validateVolumes(t *testing.T) {
-
-	secret := "secret"
-	path := "path"
-	secret2 := "secret2"
-	path2 := "path2"
-	cm := "configMap"
-
-	tests := []struct {
-		name    string
-		volumes []Volume
-		errs    int
-	}{
-		{
-			"correct entry - single volume with secret",
-			[]Volume{
-				{
-					Secret: &secret,
-					Path:   &path,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - single volume with configmap",
-			[]Volume{
-				{
-					ConfigMap: &cm,
-					Path:      &path,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - multiple volumes with secrets",
-			[]Volume{
-				{
-					Secret: &secret,
-					Path:   &path,
-				},
-				{
-					Secret: &secret2,
-					Path:   &path2,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - multiple volumes with both secret and configMap",
-			[]Volume{
-				{
-					Secret: &secret,
-					Path:   &path,
-				},
-				{
-					ConfigMap: &cm,
-					Path:      &path2,
-				},
-			},
-			0,
-		},
-		{
-			"missing secret/configMap - single volume",
-			[]Volume{
-				{
-					Path: &path,
-				},
-			},
-			1,
-		},
-		{
-			"missing path - single volume with secret",
-			[]Volume{
-				{
-					Secret: &secret,
-				},
-			},
-			1,
-		},
-		{
-			"missing path - single volume with configMap",
-			[]Volume{
-				{
-					ConfigMap: &cm,
-				},
-			},
-			1,
-		},
-		{
-			"missing secret/configMap and path - single volume",
-			[]Volume{
-				{},
-			},
-			1,
-		},
-		{
-			"missing secret/configMap in one volume - multiple volumes",
-			[]Volume{
-				{
-					Secret: &secret,
-					Path:   &path,
-				},
-				{
-					Path: &path2,
-				},
-			},
-			1,
-		},
-		{
-			"missing secret/configMap and path in two different volumes - multiple volumes",
-			[]Volume{
-				{
-					Secret: &secret,
-					Path:   &path,
-				},
-				{
-					Secret: &secret,
-				},
-				{
-					Path: &path2,
-				},
-			},
-			2,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := validateVolumes(tt.volumes); len(got) != tt.errs {
-				t.Errorf("validateVolumes() = %v\n got %d errors but want %d", got, len(got), tt.errs)
-			}
-		})
-	}
-
-}
-
-func Test_validateBuildEnvs(t *testing.T) {
-
-	name := "name"
-	name2 := "name2"
-	value := "value"
-	value2 := "value2"
-
-	incorrectName := ",foo"
-	incorrectName2 := ":foo"
-
-	valueLocalEnv := "{{ env:MY_ENV }}"
-	valueLocalEnv2 := "{{ env:MY_ENV2 }}"
-	valueLocalEnv3 := "{{env:MY_ENV3}}"
-	valueLocalEnvIncorrect := "{{ envs:MY_ENV }}"
-	valueLocalEnvIncorrect2 := "{{ MY_ENV }}"
-	valueLocalEnvIncorrect3 := "{{env:MY_ENV}}foo"
-
-	tests := []struct {
-		name string
-		envs []Env
-		errs int
-	}{
-		{
-			"correct entry - single env with value",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &value,
-				},
-			},
-			0,
-		},
-		{
-			"incorrect entry - missing value",
-			[]Env{
-				{
-					Name: &name,
-				},
-			},
-			1,
-		},
-		{
-			"incorrect entry - invalid name",
-			[]Env{
-				{
-					Name:  &incorrectName,
-					Value: &value,
-				},
-			},
-			1,
-		},
-		{
-			"incorrect entry - invalid name2",
-			[]Env{
-				{
-					Name:  &incorrectName2,
-					Value: &value,
-				},
-			},
-			1,
-		},
-		{
-			"correct entry - multiple envs with value",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &value,
-				},
-				{
-					Name:  &name2,
-					Value: &value2,
-				},
-			},
-			0,
-		},
-		{
-			"incorrect entry - mmissing value - multiple envs",
-			[]Env{
-				{
-					Name: &name,
-				},
-				{
-					Name: &name2,
-				},
-			},
-			2,
-		},
-		{
-			"correct entry - single env with value Local env",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueLocalEnv,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - multiple envs with value Local env",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueLocalEnv,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnv2,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnv3,
-				},
-			},
-			0,
-		},
-		{
-			"incorrect entry - multiple envs with value Local env",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueLocalEnv,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnvIncorrect,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnvIncorrect2,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnvIncorrect3,
-				},
-			},
-			3,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := ValidateEnvs(tt.envs); len(got) != tt.errs {
-				t.Errorf("validateEnvs() = %v\n got %d errors but want %d", got, len(got), tt.errs)
-			}
-		})
-	}
-}
-
-func Test_validateEnvs(t *testing.T) {
-
-	name := "name"
-	name2 := "name2"
-	value := "value"
-	value2 := "value2"
-
-	incorrectName := ",foo"
-	incorrectName2 := ":foo"
-
-	valueLocalEnv := "{{ env:MY_ENV }}"
-	valueLocalEnv2 := "{{ env:MY_ENV2 }}"
-	valueLocalEnv3 := "{{env:MY_ENV3}}"
-	valueLocalEnvIncorrect := "{{ envs:MY_ENV }}"
-	valueLocalEnvIncorrect2 := "{{ MY_ENV }}"
-	valueLocalEnvIncorrect3 := "{{env:MY_ENV}}foo"
-
-	valueSecretKey := "{{ secret:mysecret:key }}"
-	valueSecretKey2 := "{{secret:my-secret:key }}"
-	valueSecretKey3 := "{{secret:my-secret:key2}}"
-	valueSecretKey4 := "{{secret:my-secret:key-2}}"
-	valueSecretKey5 := "{{secret:my-secret:key.2}}"
-	valueSecretKey6 := "{{secret:my-secret:key_2}}"
-	valueSecretKey7 := "{{secret:my-secret:key_2-1}}"
-	valueSecretKey8 := "{{secret:my-secret:key_2-1.3}}"
-	valueSecretKeyIncorrect := "{{ secret:my-secret:key,key }}"
-	valueSecretKeyIncorrect2 := "{{ my-secret:key }}"
-	valueSecretKeyIncorrect3 := "{{ secret:my-secret:key }}foo"
-	valueConfigMapKey := "{{ configMap:myconfigmap:key }}"
-	valueConfigMapKey2 := "{{ configMap:myconfigmap:key }}"
-	valueConfigMapKey3 := "{{ configMap:myconfigmap:key2 }}"
-	valueConfigMapKey4 := "{{ configMap:myconfigmap:key-2 }}"
-	valueConfigMapKey5 := "{{ configMap:myconfigmap:key.2 }}"
-	valueConfigMapKey6 := "{{ configMap:myconfigmap:key_2 }}"
-	valueConfigMapKey7 := "{{ configMap:myconfigmap:key_2-1 }}"
-	valueConfigMapKey8 := "{{ configMap:myconfigmap:key_2.1 }}"
-
-	valueSecret := "{{ secret:my-secret }}"
-	valueSecret2 := "{{ secret:mysecret }}"
-	valueSecret3 := "{{ secret:mysecret}}"
-	valueSecretIncorrect := "{{ my-secret }}"
-	valueSecretIncorrect2 := "my-secret"
-	valueSecretIncorrect3 := "{{ secret:my-secret }}foo"
-	valueConfigMap := "{{ configMap:myconfigmap }}"
-
-	tests := []struct {
-		name string
-		envs []Env
-		errs int
-	}{
-		{
-			"correct entry - single env with value",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &value,
-				},
-			},
-			0,
-		},
-		{
-			"incorrect entry - missing value",
-			[]Env{
-				{
-					Name: &name,
-				},
-			},
-			1,
-		},
-		{
-			"incorrect entry - invalid name",
-			[]Env{
-				{
-					Name:  &incorrectName,
-					Value: &value,
-				},
-			},
-			1,
-		},
-		{
-			"incorrect entry - invalid name2",
-			[]Env{
-				{
-					Name:  &incorrectName2,
-					Value: &value,
-				},
-			},
-			1,
-		},
-		{
-			"correct entry - multiple envs with value",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &value,
-				},
-				{
-					Name:  &name2,
-					Value: &value2,
-				},
-			},
-			0,
-		},
-		{
-			"incorrect entry - mmissing value - multiple envs",
-			[]Env{
-				{
-					Name: &name,
-				},
-				{
-					Name: &name2,
-				},
-			},
-			2,
-		},
-		{
-			"correct entry - single env with value Local env",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueLocalEnv,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - multiple envs with value Local env",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueLocalEnv,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnv2,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnv3,
-				},
-			},
-			0,
-		},
-		{
-			"incorrect entry - multiple envs with value Local env",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueLocalEnv,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnvIncorrect,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnvIncorrect2,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnvIncorrect3,
-				},
-			},
-			3,
-		},
-		{
-			"correct entry - single secret with key",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueSecretKey,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - single configMap with key",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueConfigMapKey,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - multiple configMaps with key",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueConfigMapKey,
-				},
-				{
-					Name:  &name,
-					Value: &valueConfigMapKey2,
-				},
-				{
-					Name:  &name,
-					Value: &valueConfigMapKey3,
-				},
-				{
-					Name:  &name,
-					Value: &valueConfigMapKey4,
-				},
-				{
-					Name:  &name,
-					Value: &valueConfigMapKey5,
-				},
-				{
-					Name:  &name,
-					Value: &valueConfigMapKey6,
-				},
-				{
-					Name:  &name,
-					Value: &valueConfigMapKey7,
-				},
-				{
-					Name:  &name,
-					Value: &valueConfigMapKey8,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - multiple secrets with key",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueSecretKey,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKey2,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKey3,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKey4,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKey5,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKey6,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKey7,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKey8,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - both secret and configmap with key",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueSecretKey,
-				},
-				{
-					Name:  &name,
-					Value: &valueConfigMapKey,
-				},
-			},
-			0,
-		},
-		{
-			"incorrect entry - single secret with key",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueSecretKeyIncorrect,
-				},
-			},
-			1,
-		},
-		{
-			"incorrect entry - mutliple secrets with key",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &valueSecretKey,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKeyIncorrect,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKeyIncorrect2,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKeyIncorrect3,
-				},
-			},
-			3,
-		},
-		{
-			"correct entry - single whole secret",
-			[]Env{
-				{
-					Value: &valueSecret,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - single whole configMap",
-			[]Env{
-				{
-					Value: &valueConfigMap,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - multiple whole secret",
-			[]Env{
-				{
-					Value: &valueSecret,
-				},
-				{
-					Value: &valueSecret2,
-				},
-				{
-					Value: &valueSecret3,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - both whole secret and configMap",
-			[]Env{
-				{
-					Value: &valueSecret,
-				},
-				{
-					Value: &valueConfigMap,
-				},
-			},
-			0,
-		},
-		{
-			"incorrect entry - single whole secret",
-			[]Env{
-				{
-					Value: &value,
-				},
-			},
-			1,
-		},
-		{
-			"incorrect entry - multiple whole secret",
-			[]Env{
-				{
-					Value: &valueSecretIncorrect,
-				},
-				{
-					Value: &valueSecretIncorrect2,
-				},
-				{
-					Value: &valueSecretIncorrect3,
-				},
-				{
-					Value: &value,
-				},
-				{
-					Value: &valueLocalEnv,
-				},
-				{
-					Value: &valueLocalEnv2,
-				},
-				{
-					Value: &valueLocalEnv3,
-				},
-				{
-					Value: &valueSecret,
-				},
-			},
-			7,
-		},
-		{
-			"correct entry - all combinations",
-			[]Env{
-				{
-					Name:  &name,
-					Value: &value,
-				},
-				{
-					Name:  &name2,
-					Value: &value2,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnv,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnv2,
-				},
-				{
-					Name:  &name,
-					Value: &valueLocalEnv3,
-				},
-				{
-					Value: &valueSecret,
-				},
-				{
-					Value: &valueSecret2,
-				},
-				{
-					Value: &valueSecret3,
-				},
-				{
-					Value: &valueConfigMap,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKey,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKey2,
-				},
-				{
-					Name:  &name,
-					Value: &valueSecretKey3,
-				},
-				{
-					Name:  &name,
-					Value: &valueConfigMapKey,
-				},
-			},
-			0,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := ValidateEnvs(tt.envs); len(got) != tt.errs {
-				t.Errorf("validateEnvs() = %v\n got %d errors but want %d", got, len(got), tt.errs)
-			}
-		})
-	}
-
+// TestConfigPathDefault ensures that config defaults to XDG_CONFIG_HOME/func
+func TestConfigPathDefault(t *testing.T) {
+	// TODO
+	// Set XDG_CONFIG_PATH to ./testdata/config
+	// Confirm the config is populated from the test files.
 }
 
-func Test_validateLabels(t *testing.T) {
-
-	key := "name"
-	key2 := "name-two"
-	key3 := "prefix.io/name3"
-	value := "value"
-	value2 := "value2"
-	value3 := "value3"
-
-	incorrectKey := ",foo"
-	incorrectKey2 := ":foo"
-	incorrectValue := ":foo"
-
-	valueLocalEnv := "{{ env:MY_ENV }}"
-	valueLocalEnv2 := "{{ env:MY_ENV2 }}"
-	valueLocalEnv3 := "{{env:MY_ENV3}}"
-	valueLocalEnvIncorrect := "{{ envs:MY_ENV }}"
-	valueLocalEnvIncorrect2 := "{{ MY_ENV }}"
-	valueLocalEnvIncorrect3 := "{{env:MY_ENV}}foo"
-
-	os.Setenv("BAD_EXAMPLE", ":invalid")
-	valueLocalEnvIncorrect4 := "{{env:BAD_EXAMPLE}}"
-
-	os.Setenv("GOOD_EXAMPLE", "valid")
-	valueLocalEnv4 := "{{env:GOOD_EXAMPLE}}"
-
-	tests := []struct {
-		key    string
-		labels []Label
-		errs   int
-	}{
-		{
-			"correct entry - single label with value",
-			[]Label{
-				{
-					Key:   &key,
-					Value: &value,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - prefixed label with value",
-			[]Label{
-				{
-					Key:   &key3,
-					Value: &value3,
-				},
-			},
-			0,
-		}, {
-			"incorrect entry - missing key",
-			[]Label{
-				{
-					Value: &value,
-				},
-			},
-			1,
-		}, {
-			"incorrect entry - missing multiple keys",
-			[]Label{
-				{
-					Value: &value,
-				},
-				{
-					Value: &value2,
-				},
-			},
-			2,
-		},
-		{
-			"incorrect entry - invalid key",
-			[]Label{
-				{
-					Key:   &incorrectKey,
-					Value: &value,
-				},
-			},
-			1,
-		},
-		{
-			"incorrect entry - invalid key2",
-			[]Label{
-				{
-					Key:   &incorrectKey2,
-					Value: &value,
-				},
-			},
-			1,
-		},
-		{
-			"incorrect entry - invalid value",
-			[]Label{
-				{
-					Key:   &key,
-					Value: &incorrectValue,
-				},
-			},
-			1,
-		},
-		{
-			"correct entry - multiple labels with value",
-			[]Label{
-				{
-					Key:   &key,
-					Value: &value,
-				},
-				{
-					Key:   &key2,
-					Value: &value2,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - missing value - multiple labels",
-			[]Label{
-				{
-					Key: &key,
-				},
-				{
-					Key: &key2,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - single label with value from local env",
-			[]Label{
-				{
-					Key:   &key,
-					Value: &valueLocalEnv,
-				},
-			},
-			0,
-		},
-		{
-			"correct entry - multiple labels with values from Local env",
-			[]Label{
-				{
-					Key:   &key,
-					Value: &valueLocalEnv,
-				},
-				{
-					Key:   &key,
-					Value: &valueLocalEnv2,
-				},
-				{
-					Key:   &key,
-					Value: &valueLocalEnv3,
-				},
-			},
-			0,
-		},
-		{
-			"incorrect entry - multiple labels with values from Local env",
-			[]Label{
-				{
-					Key:   &key,
-					Value: &valueLocalEnv,
-				},
-				{
-					Key:   &key,
-					Value: &valueLocalEnvIncorrect,
-				},
-				{
-					Key:   &key,
-					Value: &valueLocalEnvIncorrect2,
-				},
-				{
-					Key:   &key,
-					Value: &valueLocalEnvIncorrect3,
-				},
-			},
-			3,
-		},
-		{
-			"correct entry - good environment variable value",
-			[]Label{
-				{
-					Key:   &key,
-					Value: &valueLocalEnv4,
-				},
-			},
-			0,
-		}, {
-			"incorrect entry - bad environment variable value",
-			[]Label{
-				{
-					Key:   &key,
-					Value: &valueLocalEnvIncorrect4,
-				},
-			},
-			1,
-		},
-		{
-			"correct entry - all combinations",
-			[]Label{
-				{
-					Key:   &key,
-					Value: &value,
-				},
-				{
-					Key:   &key2,
-					Value: &value2,
-				},
-				{
-					Key:   &key3,
-					Value: &value3,
-				},
-				{
-					Key:   &key,
-					Value: &valueLocalEnv,
-				},
-				{
-					Key:   &key,
-					Value: &valueLocalEnv2,
-				},
-				{
-					Key:   &key,
-					Value: &valueLocalEnv3,
-				},
-			},
-			0,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.key, func(t *testing.T) {
-			if got := ValidateLabels(tt.labels); len(got) != tt.errs {
-				t.Errorf("validateLabels() = %v\n got %d errors but want %d", got, len(got), tt.errs)
-			}
-		})
-	}
-
+// TestConfigPath ensure that the config path provided via the WithConfig
+// option is respected.
+func TestConfigPath(t *testing.T) {
+	// TODO
+	// Create a client specifying ./testdata/config
+	// Confirm the config is populated from the test files.
 }
 
-func Test_validateOptions(t *testing.T) {
-
-	tests := []struct {
-		name    string
-		options Options
-		errs    int
-	}{
-		{
-			"correct 'scale.metric' - concurrency",
-			Options{
-				Scale: &ScaleOptions{
-					Metric: ptr.String("concurrency"),
-				},
-			},
-			0,
-		},
-		{
-			"correct 'scale.metric' - rps",
-			Options{
-				Scale: &ScaleOptions{
-					Metric: ptr.String("rps"),
-				},
-			},
-			0,
-		},
-		{
-			"incorrect 'scale.metric'",
-			Options{
-				Scale: &ScaleOptions{
-					Metric: ptr.String("foo"),
-				},
-			},
-			1,
-		},
-		{
-			"correct 'scale.min'",
-			Options{
-				Scale: &ScaleOptions{
-					Min: ptr.Int64(1),
-				},
-			},
-			0,
-		},
-		{
-			"correct 'scale.max'",
-			Options{
-				Scale: &ScaleOptions{
-					Max: ptr.Int64(10),
-				},
-			},
-			0,
-		},
-		{
-			"correct  'scale.min' & 'scale.max'",
-			Options{
-				Scale: &ScaleOptions{
-					Min: ptr.Int64(0),
-					Max: ptr.Int64(10),
-				},
-			},
-			0,
-		},
-		{
-			"incorrect  'scale.min' & 'scale.max'",
-			Options{
-				Scale: &ScaleOptions{
-					Min: ptr.Int64(100),
-					Max: ptr.Int64(10),
-				},
-			},
-			1,
-		},
-		{
-			"incorrect 'scale.min' - negative value",
-			Options{
-				Scale: &ScaleOptions{
-					Min: ptr.Int64(-10),
-				},
-			},
-			1,
-		},
-		{
-			"incorrect 'scale.max' - negative value",
-			Options{
-				Scale: &ScaleOptions{
-					Max: ptr.Int64(-10),
-				},
-			},
-			1,
-		},
-		{
-			"correct 'scale.target'",
-			Options{
-				Scale: &ScaleOptions{
-					Target: ptr.Float64(50),
-				},
-			},
-			0,
-		},
-		{
-			"incorrect 'scale.target'",
-			Options{
-				Scale: &ScaleOptions{
-					Target: ptr.Float64(0),
-				},
-			},
-			1,
-		},
-		{
-			"correct 'scale.utilization'",
-			Options{
-				Scale: &ScaleOptions{
-					Utilization: ptr.Float64(50),
-				},
-			},
-			0,
-		},
-		{
-			"incorrect 'scale.utilization' - < 1",
-			Options{
-				Scale: &ScaleOptions{
-					Utilization: ptr.Float64(0),
-				},
-			},
-			1,
-		},
-		{
-			"incorrect 'scale.utilization' - > 100",
-			Options{
-				Scale: &ScaleOptions{
-					Utilization: ptr.Float64(110),
-				},
-			},
-			1,
-		},
-		{
-			"correct 'resources.requests.cpu'",
-			Options{
-				Resources: &ResourcesOptions{
-					Requests: &ResourcesRequestsOptions{
-						CPU: ptr.String("1000m"),
-					},
-				},
-			},
-			0,
-		},
-		{
-			"incorrect 'resources.requests.cpu'",
-			Options{
-				Resources: &ResourcesOptions{
-					Requests: &ResourcesRequestsOptions{
-						CPU: ptr.String("foo"),
-					},
-				},
-			},
-			1,
-		},
-		{
-			"correct 'resources.requests.memory'",
-			Options{
-				Resources: &ResourcesOptions{
-					Requests: &ResourcesRequestsOptions{
-						Memory: ptr.String("100Mi"),
-					},
-				},
-			},
-			0,
-		},
-		{
-			"incorrect 'resources.requests.memory'",
-			Options{
-				Resources: &ResourcesOptions{
-					Requests: &ResourcesRequestsOptions{
-						Memory: ptr.String("foo"),
-					},
-				},
-			},
-			1,
-		},
-		{
-			"correct 'resources.limits.cpu'",
-			Options{
-				Resources: &ResourcesOptions{
-					Limits: &ResourcesLimitsOptions{
-						CPU: ptr.String("1000m"),
-					},
-				},
-			},
-			0,
-		},
-		{
-			"incorrect 'resources.limits.cpu'",
-			Options{
-				Resources: &ResourcesOptions{
-					Limits: &ResourcesLimitsOptions{
-						CPU: ptr.String("foo"),
-					},
-				},
-			},
-			1,
-		},
-		{
-			"correct 'resources.limits.memory'",
-			Options{
-				Resources: &ResourcesOptions{
-					Limits: &ResourcesLimitsOptions{
-						Memory: ptr.String("100Mi"),
-					},
-				},
-			},
-			0,
-		},
-		{
-			"incorrect 'resources.limits.memory'",
-			Options{
-				Resources: &ResourcesOptions{
-					Limits: &ResourcesLimitsOptions{
-						Memory: ptr.String("foo"),
-					},
-				},
-			},
-			1,
-		},
-		{
-			"correct 'resources.limits.concurrency'",
-			Options{
-				Resources: &ResourcesOptions{
-					Limits: &ResourcesLimitsOptions{
-						Concurrency: ptr.Int64(50),
-					},
-				},
-			},
-			0,
-		},
-		{
-			"correct 'resources.limits.concurrency' - 0",
-			Options{
-				Resources: &ResourcesOptions{
-					Limits: &ResourcesLimitsOptions{
-						Concurrency: ptr.Int64(0),
-					},
-				},
-			},
-			0,
-		},
-		{
-			"incorrect 'resources.limits.concurrency' - negative value",
-			Options{
-				Resources: &ResourcesOptions{
-					Limits: &ResourcesLimitsOptions{
-						Concurrency: ptr.Int64(-10),
-					},
-				},
-			},
-			1,
-		},
-		{
-			"correct all options",
-			Options{
-				Resources: &ResourcesOptions{
-					Requests: &ResourcesRequestsOptions{
-						CPU:    ptr.String("1000m"),
-						Memory: ptr.String("100Mi"),
-					},
-					Limits: &ResourcesLimitsOptions{
-						CPU:         ptr.String("1000m"),
-						Memory:      ptr.String("100Mi"),
-						Concurrency: ptr.Int64(10),
-					},
-				},
-				Scale: &ScaleOptions{
-					Min:         ptr.Int64(0),
-					Max:         ptr.Int64(10),
-					Metric:      ptr.String("concurrency"),
-					Target:      ptr.Float64(40.5),
-					Utilization: ptr.Float64(35.5),
-				},
-			},
-			0,
-		},
-		{
-			"incorrect all options",
-			Options{
-				Resources: &ResourcesOptions{
-					Requests: &ResourcesRequestsOptions{
-						CPU:    ptr.String("foo"),
-						Memory: ptr.String("foo"),
-					},
-					Limits: &ResourcesLimitsOptions{
-						CPU:         ptr.String("foo"),
-						Memory:      ptr.String("foo"),
-						Concurrency: ptr.Int64(-1),
-					},
-				},
-				Scale: &ScaleOptions{
-					Min:         ptr.Int64(-1),
-					Max:         ptr.Int64(-1),
-					Metric:      ptr.String("foo"),
-					Target:      ptr.Float64(-1),
-					Utilization: ptr.Float64(110),
-				},
-			},
-			10,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := validateOptions(tt.options); len(got) != tt.errs {
-				t.Errorf("validateOptions() = %v\n got %d errors but want %d", got, len(got), tt.errs)
-			}
-		})
-	}
-
+// TestConfigRepositoriesPath ensures that the repositories directory within
+// the effective config path is created if it does not already exist.
+func TestConfigRepositoriesPath(t *testing.T) {
+	// TODO
+	// Create a temporary directory
+	// Specify this directory as the config path when instantiating a client.
+	// Confirm that the repositories directory is created.
 }
diff --git a/function_envs_unit_test.go b/function_envs_unit_test.go
new file mode 100644
index 00000000..ace42b70
--- /dev/null
+++ b/function_envs_unit_test.go
@@ -0,0 +1,611 @@
+//go:build !integration
+// +build !integration
+
+package function
+
+import "testing"
+
+func Test_validateBuildEnvs(t *testing.T) {
+
+	name := "name"
+	name2 := "name2"
+	value := "value"
+	value2 := "value2"
+
+	incorrectName := ",foo"
+	incorrectName2 := ":foo"
+
+	valueLocalEnv := "{{ env:MY_ENV }}"
+	valueLocalEnv2 := "{{ env:MY_ENV2 }}"
+	valueLocalEnv3 := "{{env:MY_ENV3}}"
+	valueLocalEnvIncorrect := "{{ envs:MY_ENV }}"
+	valueLocalEnvIncorrect2 := "{{ MY_ENV }}"
+	valueLocalEnvIncorrect3 := "{{env:MY_ENV}}foo"
+
+	tests := []struct {
+		name string
+		envs []Env
+		errs int
+	}{
+		{
+			"correct entry - single env with value",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &value,
+				},
+			},
+			0,
+		},
+		{
+			"incorrect entry - missing value",
+			[]Env{
+				{
+					Name: &name,
+				},
+			},
+			1,
+		},
+		{
+			"incorrect entry - invalid name",
+			[]Env{
+				{
+					Name:  &incorrectName,
+					Value: &value,
+				},
+			},
+			1,
+		},
+		{
+			"incorrect entry - invalid name2",
+			[]Env{
+				{
+					Name:  &incorrectName2,
+					Value: &value,
+				},
+			},
+			1,
+		},
+		{
+			"correct entry - multiple envs with value",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &value,
+				},
+				{
+					Name:  &name2,
+					Value: &value2,
+				},
+			},
+			0,
+		},
+		{
+			"incorrect entry - mmissing value - multiple envs",
+			[]Env{
+				{
+					Name: &name,
+				},
+				{
+					Name: &name2,
+				},
+			},
+			2,
+		},
+		{
+			"correct entry - single env with value Local env",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueLocalEnv,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - multiple envs with value Local env",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueLocalEnv,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnv2,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnv3,
+				},
+			},
+			0,
+		},
+		{
+			"incorrect entry - multiple envs with value Local env",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueLocalEnv,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnvIncorrect,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnvIncorrect2,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnvIncorrect3,
+				},
+			},
+			3,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := ValidateEnvs(tt.envs); len(got) != tt.errs {
+				t.Errorf("validateEnvs() = %v\n got %d errors but want %d", got, len(got), tt.errs)
+			}
+		})
+	}
+}
+
+func Test_validateEnvs(t *testing.T) {
+
+	name := "name"
+	name2 := "name2"
+	value := "value"
+	value2 := "value2"
+
+	incorrectName := ",foo"
+	incorrectName2 := ":foo"
+
+	valueLocalEnv := "{{ env:MY_ENV }}"
+	valueLocalEnv2 := "{{ env:MY_ENV2 }}"
+	valueLocalEnv3 := "{{env:MY_ENV3}}"
+	valueLocalEnvIncorrect := "{{ envs:MY_ENV }}"
+	valueLocalEnvIncorrect2 := "{{ MY_ENV }}"
+	valueLocalEnvIncorrect3 := "{{env:MY_ENV}}foo"
+
+	valueSecretKey := "{{ secret:mysecret:key }}"
+	valueSecretKey2 := "{{secret:my-secret:key }}"
+	valueSecretKey3 := "{{secret:my-secret:key2}}"
+	valueSecretKey4 := "{{secret:my-secret:key-2}}"
+	valueSecretKey5 := "{{secret:my-secret:key.2}}"
+	valueSecretKey6 := "{{secret:my-secret:key_2}}"
+	valueSecretKey7 := "{{secret:my-secret:key_2-1}}"
+	valueSecretKey8 := "{{secret:my-secret:key_2-1.3}}"
+	valueSecretKeyIncorrect := "{{ secret:my-secret:key,key }}"
+	valueSecretKeyIncorrect2 := "{{ my-secret:key }}"
+	valueSecretKeyIncorrect3 := "{{ secret:my-secret:key }}foo"
+	valueConfigMapKey := "{{ configMap:myconfigmap:key }}"
+	valueConfigMapKey2 := "{{ configMap:myconfigmap:key }}"
+	valueConfigMapKey3 := "{{ configMap:myconfigmap:key2 }}"
+	valueConfigMapKey4 := "{{ configMap:myconfigmap:key-2 }}"
+	valueConfigMapKey5 := "{{ configMap:myconfigmap:key.2 }}"
+	valueConfigMapKey6 := "{{ configMap:myconfigmap:key_2 }}"
+	valueConfigMapKey7 := "{{ configMap:myconfigmap:key_2-1 }}"
+	valueConfigMapKey8 := "{{ configMap:myconfigmap:key_2.1 }}"
+
+	valueSecret := "{{ secret:my-secret }}"
+	valueSecret2 := "{{ secret:mysecret }}"
+	valueSecret3 := "{{ secret:mysecret}}"
+	valueSecretIncorrect := "{{ my-secret }}"
+	valueSecretIncorrect2 := "my-secret"
+	valueSecretIncorrect3 := "{{ secret:my-secret }}foo"
+	valueConfigMap := "{{ configMap:myconfigmap }}"
+
+	tests := []struct {
+		name string
+		envs []Env
+		errs int
+	}{
+		{
+			"correct entry - single env with value",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &value,
+				},
+			},
+			0,
+		},
+		{
+			"incorrect entry - missing value",
+			[]Env{
+				{
+					Name: &name,
+				},
+			},
+			1,
+		},
+		{
+			"incorrect entry - invalid name",
+			[]Env{
+				{
+					Name:  &incorrectName,
+					Value: &value,
+				},
+			},
+			1,
+		},
+		{
+			"incorrect entry - invalid name2",
+			[]Env{
+				{
+					Name:  &incorrectName2,
+					Value: &value,
+				},
+			},
+			1,
+		},
+		{
+			"correct entry - multiple envs with value",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &value,
+				},
+				{
+					Name:  &name2,
+					Value: &value2,
+				},
+			},
+			0,
+		},
+		{
+			"incorrect entry - mmissing value - multiple envs",
+			[]Env{
+				{
+					Name: &name,
+				},
+				{
+					Name: &name2,
+				},
+			},
+			2,
+		},
+		{
+			"correct entry - single env with value Local env",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueLocalEnv,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - multiple envs with value Local env",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueLocalEnv,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnv2,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnv3,
+				},
+			},
+			0,
+		},
+		{
+			"incorrect entry - multiple envs with value Local env",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueLocalEnv,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnvIncorrect,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnvIncorrect2,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnvIncorrect3,
+				},
+			},
+			3,
+		},
+		{
+			"correct entry - single secret with key",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueSecretKey,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - single configMap with key",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueConfigMapKey,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - multiple configMaps with key",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueConfigMapKey,
+				},
+				{
+					Name:  &name,
+					Value: &valueConfigMapKey2,
+				},
+				{
+					Name:  &name,
+					Value: &valueConfigMapKey3,
+				},
+				{
+					Name:  &name,
+					Value: &valueConfigMapKey4,
+				},
+				{
+					Name:  &name,
+					Value: &valueConfigMapKey5,
+				},
+				{
+					Name:  &name,
+					Value: &valueConfigMapKey6,
+				},
+				{
+					Name:  &name,
+					Value: &valueConfigMapKey7,
+				},
+				{
+					Name:  &name,
+					Value: &valueConfigMapKey8,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - multiple secrets with key",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueSecretKey,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKey2,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKey3,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKey4,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKey5,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKey6,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKey7,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKey8,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - both secret and configmap with key",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueSecretKey,
+				},
+				{
+					Name:  &name,
+					Value: &valueConfigMapKey,
+				},
+			},
+			0,
+		},
+		{
+			"incorrect entry - single secret with key",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueSecretKeyIncorrect,
+				},
+			},
+			1,
+		},
+		{
+			"incorrect entry - multiple secrets with key",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &valueSecretKey,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKeyIncorrect,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKeyIncorrect2,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKeyIncorrect3,
+				},
+			},
+			3,
+		},
+		{
+			"correct entry - single whole secret",
+			[]Env{
+				{
+					Value: &valueSecret,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - single whole configMap",
+			[]Env{
+				{
+					Value: &valueConfigMap,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - multiple whole secret",
+			[]Env{
+				{
+					Value: &valueSecret,
+				},
+				{
+					Value: &valueSecret2,
+				},
+				{
+					Value: &valueSecret3,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - both whole secret and configMap",
+			[]Env{
+				{
+					Value: &valueSecret,
+				},
+				{
+					Value: &valueConfigMap,
+				},
+			},
+			0,
+		},
+		{
+			"incorrect entry - single whole secret",
+			[]Env{
+				{
+					Value: &value,
+				},
+			},
+			1,
+		},
+		{
+			"incorrect entry - multiple whole secret",
+			[]Env{
+				{
+					Value: &valueSecretIncorrect,
+				},
+				{
+					Value: &valueSecretIncorrect2,
+				},
+				{
+					Value: &valueSecretIncorrect3,
+				},
+				{
+					Value: &value,
+				},
+				{
+					Value: &valueLocalEnv,
+				},
+				{
+					Value: &valueLocalEnv2,
+				},
+				{
+					Value: &valueLocalEnv3,
+				},
+				{
+					Value: &valueSecret,
+				},
+			},
+			7,
+		},
+		{
+			"correct entry - all combinations",
+			[]Env{
+				{
+					Name:  &name,
+					Value: &value,
+				},
+				{
+					Name:  &name2,
+					Value: &value2,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnv,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnv2,
+				},
+				{
+					Name:  &name,
+					Value: &valueLocalEnv3,
+				},
+				{
+					Value: &valueSecret,
+				},
+				{
+					Value: &valueSecret2,
+				},
+				{
+					Value: &valueSecret3,
+				},
+				{
+					Value: &valueConfigMap,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKey,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKey2,
+				},
+				{
+					Name:  &name,
+					Value: &valueSecretKey3,
+				},
+				{
+					Name:  &name,
+					Value: &valueConfigMapKey,
+				},
+			},
+			0,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := ValidateEnvs(tt.envs); len(got) != tt.errs {
+				t.Errorf("validateEnvs() = %v\n got %d errors but want %d", got, len(got), tt.errs)
+			}
+		})
+	}
+
+}
diff --git a/function_labels_unit_test.go b/function_labels_unit_test.go
new file mode 100644
index 00000000..c89e4f21
--- /dev/null
+++ b/function_labels_unit_test.go
@@ -0,0 +1,245 @@
+//go:build !integration
+// +build !integration
+
+package function
+
+import (
+	"os"
+	"testing"
+)
+
+func Test_validateLabels(t *testing.T) {
+
+	key := "name"
+	key2 := "name-two"
+	key3 := "prefix.io/name3"
+	value := "value"
+	value2 := "value2"
+	value3 := "value3"
+
+	incorrectKey := ",foo"
+	incorrectKey2 := ":foo"
+	incorrectValue := ":foo"
+
+	valueLocalEnv := "{{ env:MY_ENV }}"
+	valueLocalEnv2 := "{{ env:MY_ENV2 }}"
+	valueLocalEnv3 := "{{env:MY_ENV3}}"
+	valueLocalEnvIncorrect := "{{ envs:MY_ENV }}"
+	valueLocalEnvIncorrect2 := "{{ MY_ENV }}"
+	valueLocalEnvIncorrect3 := "{{env:MY_ENV}}foo"
+
+	os.Setenv("BAD_EXAMPLE", ":invalid")
+	valueLocalEnvIncorrect4 := "{{env:BAD_EXAMPLE}}"
+
+	os.Setenv("GOOD_EXAMPLE", "valid")
+	valueLocalEnv4 := "{{env:GOOD_EXAMPLE}}"
+
+	tests := []struct {
+		key    string
+		labels []Label
+		errs   int
+	}{
+		{
+			"correct entry - single label with value",
+			[]Label{
+				{
+					Key:   &key,
+					Value: &value,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - prefixed label with value",
+			[]Label{
+				{
+					Key:   &key3,
+					Value: &value3,
+				},
+			},
+			0,
+		}, {
+			"incorrect entry - missing key",
+			[]Label{
+				{
+					Value: &value,
+				},
+			},
+			1,
+		}, {
+			"incorrect entry - missing multiple keys",
+			[]Label{
+				{
+					Value: &value,
+				},
+				{
+					Value: &value2,
+				},
+			},
+			2,
+		},
+		{
+			"incorrect entry - invalid key",
+			[]Label{
+				{
+					Key:   &incorrectKey,
+					Value: &value,
+				},
+			},
+			1,
+		},
+		{
+			"incorrect entry - invalid key2",
+			[]Label{
+				{
+					Key:   &incorrectKey2,
+					Value: &value,
+				},
+			},
+			1,
+		},
+		{
+			"incorrect entry - invalid value",
+			[]Label{
+				{
+					Key:   &key,
+					Value: &incorrectValue,
+				},
+			},
+			1,
+		},
+		{
+			"correct entry - multiple labels with value",
+			[]Label{
+				{
+					Key:   &key,
+					Value: &value,
+				},
+				{
+					Key:   &key2,
+					Value: &value2,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - missing value - multiple labels",
+			[]Label{
+				{
+					Key: &key,
+				},
+				{
+					Key: &key2,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - single label with value from local env",
+			[]Label{
+				{
+					Key:   &key,
+					Value: &valueLocalEnv,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - multiple labels with values from Local env",
+			[]Label{
+				{
+					Key:   &key,
+					Value: &valueLocalEnv,
+				},
+				{
+					Key:   &key,
+					Value: &valueLocalEnv2,
+				},
+				{
+					Key:   &key,
+					Value: &valueLocalEnv3,
+				},
+			},
+			0,
+		},
+		{
+			"incorrect entry - multiple labels with values from Local env",
+			[]Label{
+				{
+					Key:   &key,
+					Value: &valueLocalEnv,
+				},
+				{
+					Key:   &key,
+					Value: &valueLocalEnvIncorrect,
+				},
+				{
+					Key:   &key,
+					Value: &valueLocalEnvIncorrect2,
+				},
+				{
+					Key:   &key,
+					Value: &valueLocalEnvIncorrect3,
+				},
+			},
+			3,
+		},
+		{
+			"correct entry - good environment variable value",
+			[]Label{
+				{
+					Key:   &key,
+					Value: &valueLocalEnv4,
+				},
+			},
+			0,
+		}, {
+			"incorrect entry - bad environment variable value",
+			[]Label{
+				{
+					Key:   &key,
+					Value: &valueLocalEnvIncorrect4,
+				},
+			},
+			1,
+		},
+		{
+			"correct entry - all combinations",
+			[]Label{
+				{
+					Key:   &key,
+					Value: &value,
+				},
+				{
+					Key:   &key2,
+					Value: &value2,
+				},
+				{
+					Key:   &key3,
+					Value: &value3,
+				},
+				{
+					Key:   &key,
+					Value: &valueLocalEnv,
+				},
+				{
+					Key:   &key,
+					Value: &valueLocalEnv2,
+				},
+				{
+					Key:   &key,
+					Value: &valueLocalEnv3,
+				},
+			},
+			0,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.key, func(t *testing.T) {
+			if got := ValidateLabels(tt.labels); len(got) != tt.errs {
+				t.Errorf("validateLabels() = %v\n got %d errors but want %d", got, len(got), tt.errs)
+			}
+		})
+	}
+}
diff --git a/function_options_unit_test.go b/function_options_unit_test.go
new file mode 100644
index 00000000..2364af2e
--- /dev/null
+++ b/function_options_unit_test.go
@@ -0,0 +1,326 @@
+//go:build !integration
+// +build !integration
+
+package function
+
+import (
+	"testing"
+
+	"knative.dev/pkg/ptr"
+)
+
+func Test_validateOptions(t *testing.T) {
+
+	tests := []struct {
+		name    string
+		options Options
+		errs    int
+	}{
+		{
+			"correct 'scale.metric' - concurrency",
+			Options{
+				Scale: &ScaleOptions{
+					Metric: ptr.String("concurrency"),
+				},
+			},
+			0,
+		},
+		{
+			"correct 'scale.metric' - rps",
+			Options{
+				Scale: &ScaleOptions{
+					Metric: ptr.String("rps"),
+				},
+			},
+			0,
+		},
+		{
+			"incorrect 'scale.metric'",
+			Options{
+				Scale: &ScaleOptions{
+					Metric: ptr.String("foo"),
+				},
+			},
+			1,
+		},
+		{
+			"correct 'scale.min'",
+			Options{
+				Scale: &ScaleOptions{
+					Min: ptr.Int64(1),
+				},
+			},
+			0,
+		},
+		{
+			"correct 'scale.max'",
+			Options{
+				Scale: &ScaleOptions{
+					Max: ptr.Int64(10),
+				},
+			},
+			0,
+		},
+		{
+			"correct  'scale.min' & 'scale.max'",
+			Options{
+				Scale: &ScaleOptions{
+					Min: ptr.Int64(0),
+					Max: ptr.Int64(10),
+				},
+			},
+			0,
+		},
+		{
+			"incorrect  'scale.min' & 'scale.max'",
+			Options{
+				Scale: &ScaleOptions{
+					Min: ptr.Int64(100),
+					Max: ptr.Int64(10),
+				},
+			},
+			1,
+		},
+		{
+			"incorrect 'scale.min' - negative value",
+			Options{
+				Scale: &ScaleOptions{
+					Min: ptr.Int64(-10),
+				},
+			},
+			1,
+		},
+		{
+			"incorrect 'scale.max' - negative value",
+			Options{
+				Scale: &ScaleOptions{
+					Max: ptr.Int64(-10),
+				},
+			},
+			1,
+		},
+		{
+			"correct 'scale.target'",
+			Options{
+				Scale: &ScaleOptions{
+					Target: ptr.Float64(50),
+				},
+			},
+			0,
+		},
+		{
+			"incorrect 'scale.target'",
+			Options{
+				Scale: &ScaleOptions{
+					Target: ptr.Float64(0),
+				},
+			},
+			1,
+		},
+		{
+			"correct 'scale.utilization'",
+			Options{
+				Scale: &ScaleOptions{
+					Utilization: ptr.Float64(50),
+				},
+			},
+			0,
+		},
+		{
+			"incorrect 'scale.utilization' - < 1",
+			Options{
+				Scale: &ScaleOptions{
+					Utilization: ptr.Float64(0),
+				},
+			},
+			1,
+		},
+		{
+			"incorrect 'scale.utilization' - > 100",
+			Options{
+				Scale: &ScaleOptions{
+					Utilization: ptr.Float64(110),
+				},
+			},
+			1,
+		},
+		{
+			"correct 'resources.requests.cpu'",
+			Options{
+				Resources: &ResourcesOptions{
+					Requests: &ResourcesRequestsOptions{
+						CPU: ptr.String("1000m"),
+					},
+				},
+			},
+			0,
+		},
+		{
+			"incorrect 'resources.requests.cpu'",
+			Options{
+				Resources: &ResourcesOptions{
+					Requests: &ResourcesRequestsOptions{
+						CPU: ptr.String("foo"),
+					},
+				},
+			},
+			1,
+		},
+		{
+			"correct 'resources.requests.memory'",
+			Options{
+				Resources: &ResourcesOptions{
+					Requests: &ResourcesRequestsOptions{
+						Memory: ptr.String("100Mi"),
+					},
+				},
+			},
+			0,
+		},
+		{
+			"incorrect 'resources.requests.memory'",
+			Options{
+				Resources: &ResourcesOptions{
+					Requests: &ResourcesRequestsOptions{
+						Memory: ptr.String("foo"),
+					},
+				},
+			},
+			1,
+		},
+		{
+			"correct 'resources.limits.cpu'",
+			Options{
+				Resources: &ResourcesOptions{
+					Limits: &ResourcesLimitsOptions{
+						CPU: ptr.String("1000m"),
+					},
+				},
+			},
+			0,
+		},
+		{
+			"incorrect 'resources.limits.cpu'",
+			Options{
+				Resources: &ResourcesOptions{
+					Limits: &ResourcesLimitsOptions{
+						CPU: ptr.String("foo"),
+					},
+				},
+			},
+			1,
+		},
+		{
+			"correct 'resources.limits.memory'",
+			Options{
+				Resources: &ResourcesOptions{
+					Limits: &ResourcesLimitsOptions{
+						Memory: ptr.String("100Mi"),
+					},
+				},
+			},
+			0,
+		},
+		{
+			"incorrect 'resources.limits.memory'",
+			Options{
+				Resources: &ResourcesOptions{
+					Limits: &ResourcesLimitsOptions{
+						Memory: ptr.String("foo"),
+					},
+				},
+			},
+			1,
+		},
+		{
+			"correct 'resources.limits.concurrency'",
+			Options{
+				Resources: &ResourcesOptions{
+					Limits: &ResourcesLimitsOptions{
+						Concurrency: ptr.Int64(50),
+					},
+				},
+			},
+			0,
+		},
+		{
+			"correct 'resources.limits.concurrency' - 0",
+			Options{
+				Resources: &ResourcesOptions{
+					Limits: &ResourcesLimitsOptions{
+						Concurrency: ptr.Int64(0),
+					},
+				},
+			},
+			0,
+		},
+		{
+			"incorrect 'resources.limits.concurrency' - negative value",
+			Options{
+				Resources: &ResourcesOptions{
+					Limits: &ResourcesLimitsOptions{
+						Concurrency: ptr.Int64(-10),
+					},
+				},
+			},
+			1,
+		},
+		{
+			"correct all options",
+			Options{
+				Resources: &ResourcesOptions{
+					Requests: &ResourcesRequestsOptions{
+						CPU:    ptr.String("1000m"),
+						Memory: ptr.String("100Mi"),
+					},
+					Limits: &ResourcesLimitsOptions{
+						CPU:         ptr.String("1000m"),
+						Memory:      ptr.String("100Mi"),
+						Concurrency: ptr.Int64(10),
+					},
+				},
+				Scale: &ScaleOptions{
+					Min:         ptr.Int64(0),
+					Max:         ptr.Int64(10),
+					Metric:      ptr.String("concurrency"),
+					Target:      ptr.Float64(40.5),
+					Utilization: ptr.Float64(35.5),
+				},
+			},
+			0,
+		},
+		{
+			"incorrect all options",
+			Options{
+				Resources: &ResourcesOptions{
+					Requests: &ResourcesRequestsOptions{
+						CPU:    ptr.String("foo"),
+						Memory: ptr.String("foo"),
+					},
+					Limits: &ResourcesLimitsOptions{
+						CPU:         ptr.String("foo"),
+						Memory:      ptr.String("foo"),
+						Concurrency: ptr.Int64(-1),
+					},
+				},
+				Scale: &ScaleOptions{
+					Min:         ptr.Int64(-1),
+					Max:         ptr.Int64(-1),
+					Metric:      ptr.String("foo"),
+					Target:      ptr.Float64(-1),
+					Utilization: ptr.Float64(110),
+				},
+			},
+			10,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := validateOptions(tt.options); len(got) != tt.errs {
+				t.Errorf("validateOptions() = %v\n got %d errors but want %d", got, len(got), tt.errs)
+			}
+		})
+	}
+
+}
diff --git a/function_test.go b/function_test.go
index 4c7f144a..dc7bfa03 100644
--- a/function_test.go
+++ b/function_test.go
@@ -10,137 +10,15 @@ import (
 	. "knative.dev/kn-plugin-func/testing"
 )
 
-func TestFunction_ImageWithDigest(t *testing.T) {
-	type fields struct {
-		Image       string
-		ImageDigest string
-	}
-	tests := []struct {
-		name   string
-		fields fields
-		want   string
-	}{
-		{
-			name:   "Full path with port",
-			fields: fields{Image: "image-registry.openshift-image-registry.svc.cluster.local:5000/default/bar", ImageDigest: "42"},
-			want:   "image-registry.openshift-image-registry.svc.cluster.local:5000/default/bar@42",
-		},
-		{
-			name:   "Path with namespace",
-			fields: fields{Image: "johndoe/bar", ImageDigest: "42"},
-			want:   "johndoe/bar@42",
-		},
-		{
-			name:   "Just image name",
-			fields: fields{Image: "bar:latest", ImageDigest: "42"},
-			want:   "bar@42",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			f := fn.Function{
-				Image:       tt.fields.Image,
-				ImageDigest: tt.fields.ImageDigest,
-			}
-			if got := f.ImageWithDigest(); got != tt.want {
-				t.Errorf("ImageWithDigest() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
-
-func Test_DerivedImage(t *testing.T) {
-
-	tests := []struct {
-		name     string
-		fnName   string
-		image    string
-		registry string
-		want     string
-	}{
-		{
-			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",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-
-			root := "testdata/" + tt.fnName
-			defer Using(t, root)()
-
-			f := fn.Function{
-				Name:  tt.fnName,
-				Root:  root,
-				Image: tt.image,
-			}
-
-			// write out the function
-			client := fn.New()
-			err := client.Create(f)
-			if err != nil {
-				t.Fatal(err)
-			}
-
-			got, err := fn.DerivedImage(f.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)
-			}
-		})
+// TestFunctionNameDefault ensures that a Function's name is defaulted to that
+// which can be derived from the last part of its path.
+func TestFunctionNameDefault(t *testing.T) {
+	root := "testdata/testFunctionNameDefault"
+	defer Using(t, root)()
+	_, err := fn.NewFunction(root)
+	if err != nil {
+		t.Fatal(err)
 	}
+	// TODO
+	// Test that the name was defaulted
 }
diff --git a/function_unit_test.go b/function_unit_test.go
new file mode 100644
index 00000000..41d99c98
--- /dev/null
+++ b/function_unit_test.go
@@ -0,0 +1,144 @@
+//go:build !integration
+// +build !integration
+
+package function
+
+import (
+	"testing"
+
+	. "knative.dev/kn-plugin-func/testing"
+)
+
+func TestFunction_ImageWithDigest(t *testing.T) {
+	type fields struct {
+		Image       string
+		ImageDigest string
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		want   string
+	}{
+		{
+			name:   "Full path with port",
+			fields: fields{Image: "image-registry.openshift-image-registry.svc.cluster.local:5000/default/bar", ImageDigest: "42"},
+			want:   "image-registry.openshift-image-registry.svc.cluster.local:5000/default/bar@42",
+		},
+		{
+			name:   "Path with namespace",
+			fields: fields{Image: "johndoe/bar", ImageDigest: "42"},
+			want:   "johndoe/bar@42",
+		},
+		{
+			name:   "Just image name",
+			fields: fields{Image: "bar:latest", ImageDigest: "42"},
+			want:   "bar@42",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			f := Function{
+				Image:       tt.fields.Image,
+				ImageDigest: tt.fields.ImageDigest,
+			}
+			if got := f.ImageWithDigest(); got != tt.want {
+				t.Errorf("ImageWithDigest() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_DerivedImage(t *testing.T) {
+	tests := []struct {
+		name     string
+		fnName   string
+		image    string
+		registry string
+		want     string
+	}{
+		{
+			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",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			root := "testdata/" + tt.fnName
+			defer Using(t, root)()
+
+			f := Function{
+				Name:  tt.fnName,
+				Root:  root,
+				Image: tt.image,
+			}
+
+			// write out the function
+			client := New()
+			err := client.Create(f)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			got, err := DerivedImage(f.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)
+			}
+		})
+	}
+}
diff --git a/function_volumes_unit_test.go b/function_volumes_unit_test.go
new file mode 100644
index 00000000..469363a8
--- /dev/null
+++ b/function_volumes_unit_test.go
@@ -0,0 +1,141 @@
+//go:build !integration
+// +build !integration
+
+package function
+
+import "testing"
+
+func Test_validateVolumes(t *testing.T) {
+
+	secret := "secret"
+	path := "path"
+	secret2 := "secret2"
+	path2 := "path2"
+	cm := "configMap"
+
+	tests := []struct {
+		name    string
+		volumes []Volume
+		errs    int
+	}{
+		{
+			"correct entry - single volume with secret",
+			[]Volume{
+				{
+					Secret: &secret,
+					Path:   &path,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - single volume with configmap",
+			[]Volume{
+				{
+					ConfigMap: &cm,
+					Path:      &path,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - multiple volumes with secrets",
+			[]Volume{
+				{
+					Secret: &secret,
+					Path:   &path,
+				},
+				{
+					Secret: &secret2,
+					Path:   &path2,
+				},
+			},
+			0,
+		},
+		{
+			"correct entry - multiple volumes with both secret and configMap",
+			[]Volume{
+				{
+					Secret: &secret,
+					Path:   &path,
+				},
+				{
+					ConfigMap: &cm,
+					Path:      &path2,
+				},
+			},
+			0,
+		},
+		{
+			"missing secret/configMap - single volume",
+			[]Volume{
+				{
+					Path: &path,
+				},
+			},
+			1,
+		},
+		{
+			"missing path - single volume with secret",
+			[]Volume{
+				{
+					Secret: &secret,
+				},
+			},
+			1,
+		},
+		{
+			"missing path - single volume with configMap",
+			[]Volume{
+				{
+					ConfigMap: &cm,
+				},
+			},
+			1,
+		},
+		{
+			"missing secret/configMap and path - single volume",
+			[]Volume{
+				{},
+			},
+			1,
+		},
+		{
+			"missing secret/configMap in one volume - multiple volumes",
+			[]Volume{
+				{
+					Secret: &secret,
+					Path:   &path,
+				},
+				{
+					Path: &path2,
+				},
+			},
+			1,
+		},
+		{
+			"missing secret/configMap and path in two different volumes - multiple volumes",
+			[]Volume{
+				{
+					Secret: &secret,
+					Path:   &path,
+				},
+				{
+					Secret: &secret,
+				},
+				{
+					Path: &path2,
+				},
+			},
+			2,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := validateVolumes(tt.volumes); len(got) != tt.errs {
+				t.Errorf("validateVolumes() = %v\n got %d errors but want %d", got, len(got), tt.errs)
+			}
+		})
+	}
+}