Repository: knative-sandbox/kn-plugin-func
Pull request #656 (2021-11-11 16:03:46)

chore: extract config validate
- :broom: extract config validation error text formatter

Minor cleanup to address config validation which is quite user friendly, but whose code was a little bit unwieldy.  
In particular the code now fails fast on errors reading func.yaml, extracts the unmarshaling error formatter, extracts logical validator, collapses validation string formatting logic a bit and switches to a string builder for clarity.

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

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

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

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

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

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

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":[]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/656?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) Report
> Merging [#656](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/656?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (be59e55) into [main](https://codecov.io/gh/knative-sandbox/kn-plugin-func/commit/fc1e874a5d2e61025cd6e032318a0a22a638343c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (fc1e874) will **increase** coverage by `0.16%`.
> The diff coverage is `38.70%`.

[![Impacted file tree graph](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/656/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/656?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     #656      +/-   ##
==========================================
+ Coverage   36.26%   36.42%   +0.16%     
==========================================
  Files          38       38              
  Lines        3847     3835      -12     
==========================================
+ Hits         1395     1397       +2     
+ Misses       2268     2255      -13     
+ Partials      184      183       -1     
```


| [Impacted Files](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/656?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) | Coverage Δ | |
|---|---|---|
| [config.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/656/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y29uZmlnLmdv) | `65.93% <38.70%> (+4.11%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/656?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/656?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox). Last update [fc1e874...be59e55](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/656?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/.codecov.yaml b/.codecov.yaml
index 16733ebf..f6fd0b2e 100644
--- a/.codecov.yaml
+++ b/.codecov.yaml
@@ -2,3 +2,4 @@ ignore:
   - third_party
   - vendor
   - testdata
+  - docker/docker_client_generated.go
diff --git a/Makefile b/Makefile
index 86b8fa0c..1099467e 100644
--- a/Makefile
+++ b/Makefile
@@ -60,7 +60,7 @@ check: bin/golangci-lint ## Check code quality (lint)
 	cd test/_e2e && ../../bin/golangci-lint run --timeout 300s
 
 bin/golangci-lint:
-	curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./bin v1.40.1
+	curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./bin v1.43.0
 
 pkged.go: $(TEMPLATES)
 	@rm -rf templates/node/cloudevents/node_modules
diff --git a/buildpacks/builder.go b/buildpacks/builder.go
index ea8a74fc..cc5612b4 100644
--- a/buildpacks/builder.go
+++ b/buildpacks/builder.go
@@ -11,6 +11,8 @@ import (
 	"runtime"
 	"strings"
 
+	"github.com/docker/docker/client"
+
 	fn "knative.dev/kn-plugin-func"
 	"knative.dev/kn-plugin-func/docker"
 
@@ -79,10 +81,11 @@ func (builder *Builder) Build(ctx context.Context, f fn.Function) (err error) {
 		logWriter = &bytes.Buffer{}
 	}
 
-	cli, dockerHost, err := docker.NewDockerClient()
+	cli, dockerHost, err := docker.NewClient(client.DefaultDockerHost)
 	if err != nil {
 		return err
 	}
+	defer cli.Close()
 
 	version, err := cli.ServerVersion(ctx)
 	if err != nil {
diff --git a/client.go b/client.go
index d1c92d47..34306f16 100644
--- a/client.go
+++ b/client.go
@@ -481,7 +481,8 @@ func (c *Client) Create(f Function) (err error) {
 		return
 	}
 
-	// Write the Function metadata (func.yaml)
+	// Mark it as having been created via this client library and Write (save)
+	f.Created = time.Now()
 	if err = writeConfig(f); err != nil {
 		return
 	}
diff --git a/config.go b/config.go
index 11624cfa..9a65c4b9 100644
--- a/config.go
+++ b/config.go
@@ -8,6 +8,7 @@ import (
 	"path/filepath"
 	"regexp"
 	"strings"
+	"time"
 
 	"gopkg.in/yaml.v2"
 	"k8s.io/apimachinery/pkg/api/resource"
@@ -146,6 +147,7 @@ type Config struct {
 	Annotations     map[string]string `yaml:"annotations"`
 	Options         Options           `yaml:"options"`
 	Labels          Labels            `yaml:"labels"`
+	Created         time.Time         `yaml:"created"`
 	// Add new values to the toConfig/fromConfig functions.
 }
 
@@ -167,86 +169,53 @@ func newConfig(root string) (c Config, err error) {
 	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)
+}
 
-	errMsg := ""
-	errMsgHeader := "'func.yaml' config file is not valid:\n"
-	errMsgReg := regexp.MustCompile("not found in type .*")
-
-	// Let's try to unmarshal the config file, any fields that are found
-	// in the data that do not have corresponding struct members, or mapping
-	// keys that are duplicates, will result in an error.
-	err = yaml.UnmarshalStrict(bb, &c)
-	if err != nil {
-		errMsg = err.Error()
-
-		if strings.HasPrefix(errMsg, "yaml: unmarshal errors:") {
-			errMsg = errMsgReg.ReplaceAllString(errMsg, "is not valid")
-			errMsg = strings.Replace(errMsg, "yaml: unmarshal errors:\n", errMsgHeader, 1)
-		} else if strings.HasPrefix(errMsg, "yaml:") {
-			errMsg = errMsgReg.ReplaceAllString(errMsg, "is not valid")
-			errMsg = strings.Replace(errMsg, "yaml: ", errMsgHeader+"  ", 1)
-		}
+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)
+}
 
-	// Let's check that all entries in `volumes`, `buildEnvs`, `envs` and `options` contain all required fields
-	volumesErrors := validateVolumes(c.Volumes)
-	buildEnvsErrors := ValidateBuildEnvs((c.BuildEnvs))
-	envsErrors := ValidateEnvs(c.Envs)
-	optionsErrors := validateOptions(c.Options)
-	labelsErrors := ValidateLabels(c.Labels)
-	if len(volumesErrors) > 0 || len(buildEnvsErrors) > 0 || len(envsErrors) > 0 || len(optionsErrors) > 0 || len(labelsErrors) > 0 {
-		// if there aren't any previously reported errors, we need to set the error message header first
-		if errMsg == "" {
-			errMsg = errMsgHeader
-		} else {
-			// if there are some previously reporeted errors, we need to indent them
-			errMsg = errMsg + "\n"
-		}
+func validateConfig(c Config) error {
+	errs := [][]string{
+		validateVolumes(c.Volumes),
+		ValidateBuildEnvs(c.BuildEnvs),
+		ValidateEnvs(c.Envs),
+		validateOptions(c.Options),
+		ValidateLabels(c.Labels),
+	}
 
-		// lets make the error message a little bit nice -> indent each error message
-		for i := range volumesErrors {
-			volumesErrors[i] = "  " + volumesErrors[i]
-		}
-		for i := range buildEnvsErrors {
-			buildEnvsErrors[i] = "  " + buildEnvsErrors[i]
+	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 i := range envsErrors {
-			envsErrors[i] = "  " + envsErrors[i]
+		for _, e := range ee {
+			ctr++
+			b.WriteString("\t" + e)
 		}
-		for i := range optionsErrors {
-			optionsErrors[i] = "  " + optionsErrors[i]
-		}
-		for i := range labelsErrors {
-			labelsErrors[i] = "  " + labelsErrors[i]
-		}
-		errMsg = errMsg + strings.Join(volumesErrors, "\n")
-		// we have errors from both volumes and buildEnvs sections -> let's make sure they are both indented
-		if len(buildEnvsErrors) > 0 && len(volumesErrors) > 0 {
-			errMsg = errMsg + "\n"
-		}
-		errMsg = errMsg + strings.Join(buildEnvsErrors, "\n")
-		// we have errors from volumes, buildEnvs and envs sections -> let's make sure they are indented
-		if len(envsErrors) > 0 && (len(volumesErrors) > 0 || len(buildEnvsErrors) > 0) {
-			errMsg = errMsg + "\n"
-		}
-		errMsg = errMsg + strings.Join(envsErrors, "\n")
-		// lets indent options related errors if there are already some set
-		if len(optionsErrors) > 0 && (len(volumesErrors) > 0 || len(buildEnvsErrors) > 0 || len(envsErrors) > 0) {
-			errMsg = errMsg + "\n"
-		}
-		errMsg = errMsg + strings.Join(optionsErrors, "\n")
-		// now also handle labels related errors
-		if len(labelsErrors) > 0 && (len(optionsErrors) > 0 || len(volumesErrors) > 0 || len(buildEnvsErrors) > 0 || len(envsErrors) > 0) {
-			errMsg = errMsg + "\n"
-		}
-		errMsg = errMsg + strings.Join(labelsErrors, "\n")
 	}
-
-	if errMsg != "" {
-		err = errors.New(errMsg)
+	if ctr == 0 {
+		return nil // No errors were encountered
 	}
-
-	return
+	return errors.New(b.String())
 }
 
 // fromConfig returns a Function populated from config.
@@ -268,6 +237,7 @@ func fromConfig(c Config) (f Function) {
 		Annotations:     c.Annotations,
 		Options:         c.Options,
 		Labels:          c.Labels,
+		Created:         c.Created,
 	}
 }
 
@@ -289,6 +259,7 @@ func toConfig(f Function) Config {
 		Annotations:     f.Annotations,
 		Options:         f.Options,
 		Labels:          f.Labels,
+		Created:         f.Created,
 	}
 }
 
diff --git a/docker/docker_client.go b/docker/docker_client.go
index 4f3e88d5..cfc74786 100644
--- a/docker/docker_client.go
+++ b/docker/docker_client.go
@@ -1,18 +1,44 @@
 package docker
 
 import (
+	"context"
+	"errors"
+	"fmt"
 	"net/http"
 	"net/url"
 	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"syscall"
+	"time"
 
 	"knative.dev/kn-plugin-func/ssh"
 
 	"github.com/docker/docker/client"
 )
 
-func NewDockerClient() (dockerClient client.CommonAPIClient, dockerHost string, err error) {
+func NewClient(defaultHost string) (dockerClient client.CommonAPIClient, dockerHost string, err error) {
+	var _url *url.URL
+
 	dockerHost = os.Getenv("DOCKER_HOST")
-	_url, err := url.Parse(dockerHost)
+
+	if dockerHost == "" && runtime.GOOS == "linux" && podmanPresent() {
+		_url, err = url.Parse(defaultHost)
+		if err != nil {
+			return
+		}
+		_, err = os.Stat(_url.Path)
+		switch {
+		case err != nil && !os.IsNotExist(err):
+			return
+		case os.IsNotExist(err):
+			dockerClient, dockerHost, err = newClientWithPodmanService()
+			return
+		}
+	}
+
+	_url, err = url.Parse(dockerHost)
 	isSSH := err == nil && _url.Scheme == "ssh"
 
 	if !isSSH {
@@ -47,3 +73,64 @@ func NewDockerClient() (dockerClient client.CommonAPIClient, dockerHost string,
 
 	return dockerClient, dockerHost, err
 }
+
+func podmanPresent() bool {
+	_, err := exec.LookPath("podman")
+	return err == nil
+}
+
+// creates a docker client that has its own podman service associated with it
+// the service is shutdown when Close() is called on the client
+func newClientWithPodmanService() (dockerClient client.CommonAPIClient, dockerHost string, err error) {
+	tmpDir, err := os.MkdirTemp("", "func-podman-")
+	if err != nil {
+		return
+	}
+
+	podmanSocket := filepath.Join(tmpDir, "podman.sock")
+	dockerHost = fmt.Sprintf("unix://%s", podmanSocket)
+
+	cmd := exec.Command("podman", "system", "service", dockerHost, "--time=0")
+	err = cmd.Start()
+	if err != nil {
+		return
+	}
+
+	dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithHost(dockerHost), client.WithAPIVersionNegotiation())
+	stopPodmanService := func() {
+		_ = cmd.Process.Signal(syscall.SIGTERM)
+		_ = os.RemoveAll(tmpDir)
+	}
+	dockerClient = withPodman{
+		pimpl:      dockerClient,
+		stopPodman: stopPodmanService,
+	}
+
+	podmanServiceRunning := false
+	// give a time to podman to start
+	for i := 0; i < 40; i++ {
+		if _, e := dockerClient.Ping(context.Background()); e == nil {
+			podmanServiceRunning = true
+			break
+		}
+		time.Sleep(time.Millisecond * 250)
+	}
+
+	if !podmanServiceRunning {
+		stopPodmanService()
+		err = errors.New("failed to start podman service")
+	}
+
+	return
+}
+
+type withPodman struct {
+	stopPodman func()
+	pimpl      client.CommonAPIClient
+}
+
+// Close function need to stop associated podman service
+func (w withPodman) Close() error {
+	defer w.stopPodman()
+	return w.pimpl.Close()
+}
diff --git a/docker/docker_client_generated.go b/docker/docker_client_generated.go
new file mode 100644
index 00000000..f13daea2
--- /dev/null
+++ b/docker/docker_client_generated.go
@@ -0,0 +1,488 @@
+package docker
+
+import (
+	"context"
+	"io"
+	"net"
+	"net/http"
+	"time"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/events"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/image"
+	"github.com/docker/docker/api/types/network"
+	"github.com/docker/docker/api/types/registry"
+	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/api/types/volume"
+	v1 "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+//<editor-fold desc="plain forwards to pimpl">
+
+func (w withPodman) BuildCachePrune(arg0 context.Context, arg1 types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
+	return w.pimpl.BuildCachePrune(arg0, arg1)
+}
+
+func (w withPodman) BuildCancel(arg0 context.Context, arg1 string) error {
+	return w.pimpl.BuildCancel(arg0, arg1)
+}
+
+func (w withPodman) ClientVersion() string {
+	return w.pimpl.ClientVersion()
+}
+
+func (w withPodman) ConfigCreate(arg0 context.Context, arg1 swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
+	return w.pimpl.ConfigCreate(arg0, arg1)
+}
+
+func (w withPodman) ConfigInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Config, []uint8, error) {
+	return w.pimpl.ConfigInspectWithRaw(arg0, arg1)
+}
+
+func (w withPodman) ConfigList(arg0 context.Context, arg1 types.ConfigListOptions) ([]swarm.Config, error) {
+	return w.pimpl.ConfigList(arg0, arg1)
+}
+
+func (w withPodman) ConfigRemove(arg0 context.Context, arg1 string) error {
+	return w.pimpl.ConfigRemove(arg0, arg1)
+}
+
+func (w withPodman) ConfigUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.ConfigSpec) error {
+	return w.pimpl.ConfigUpdate(arg0, arg1, arg2, arg3)
+}
+
+func (w withPodman) ContainerAttach(arg0 context.Context, arg1 string, arg2 types.ContainerAttachOptions) (types.HijackedResponse, error) {
+	return w.pimpl.ContainerAttach(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerCommit(arg0 context.Context, arg1 string, arg2 types.ContainerCommitOptions) (types.IDResponse, error) {
+	return w.pimpl.ContainerCommit(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerCreate(arg0 context.Context, arg1 *container.Config, arg2 *container.HostConfig, arg3 *network.NetworkingConfig, arg4 *v1.Platform, arg5 string) (container.ContainerCreateCreatedBody, error) {
+	return w.pimpl.ContainerCreate(arg0, arg1, arg2, arg3, arg4, arg5)
+}
+
+func (w withPodman) ContainerDiff(arg0 context.Context, arg1 string) ([]container.ContainerChangeResponseItem, error) {
+	return w.pimpl.ContainerDiff(arg0, arg1)
+}
+
+func (w withPodman) ContainerExecAttach(arg0 context.Context, arg1 string, arg2 types.ExecStartCheck) (types.HijackedResponse, error) {
+	return w.pimpl.ContainerExecAttach(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerExecCreate(arg0 context.Context, arg1 string, arg2 types.ExecConfig) (types.IDResponse, error) {
+	return w.pimpl.ContainerExecCreate(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerExecInspect(arg0 context.Context, arg1 string) (types.ContainerExecInspect, error) {
+	return w.pimpl.ContainerExecInspect(arg0, arg1)
+}
+
+func (w withPodman) ContainerExecResize(arg0 context.Context, arg1 string, arg2 types.ResizeOptions) error {
+	return w.pimpl.ContainerExecResize(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerExecStart(arg0 context.Context, arg1 string, arg2 types.ExecStartCheck) error {
+	return w.pimpl.ContainerExecStart(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerExport(arg0 context.Context, arg1 string) (io.ReadCloser, error) {
+	return w.pimpl.ContainerExport(arg0, arg1)
+}
+
+func (w withPodman) ContainerInspect(arg0 context.Context, arg1 string) (types.ContainerJSON, error) {
+	return w.pimpl.ContainerInspect(arg0, arg1)
+}
+
+func (w withPodman) ContainerInspectWithRaw(arg0 context.Context, arg1 string, arg2 bool) (types.ContainerJSON, []uint8, error) {
+	return w.pimpl.ContainerInspectWithRaw(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerKill(arg0 context.Context, arg1 string, arg2 string) error {
+	return w.pimpl.ContainerKill(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerList(arg0 context.Context, arg1 types.ContainerListOptions) ([]types.Container, error) {
+	return w.pimpl.ContainerList(arg0, arg1)
+}
+
+func (w withPodman) ContainerLogs(arg0 context.Context, arg1 string, arg2 types.ContainerLogsOptions) (io.ReadCloser, error) {
+	return w.pimpl.ContainerLogs(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerPause(arg0 context.Context, arg1 string) error {
+	return w.pimpl.ContainerPause(arg0, arg1)
+}
+
+func (w withPodman) ContainerRemove(arg0 context.Context, arg1 string, arg2 types.ContainerRemoveOptions) error {
+	return w.pimpl.ContainerRemove(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerRename(arg0 context.Context, arg1 string, arg2 string) error {
+	return w.pimpl.ContainerRename(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerResize(arg0 context.Context, arg1 string, arg2 types.ResizeOptions) error {
+	return w.pimpl.ContainerResize(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerRestart(arg0 context.Context, arg1 string, arg2 *time.Duration) error {
+	return w.pimpl.ContainerRestart(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerStart(arg0 context.Context, arg1 string, arg2 types.ContainerStartOptions) error {
+	return w.pimpl.ContainerStart(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerStatPath(arg0 context.Context, arg1 string, arg2 string) (types.ContainerPathStat, error) {
+	return w.pimpl.ContainerStatPath(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerStats(arg0 context.Context, arg1 string, arg2 bool) (types.ContainerStats, error) {
+	return w.pimpl.ContainerStats(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerStatsOneShot(arg0 context.Context, arg1 string) (types.ContainerStats, error) {
+	return w.pimpl.ContainerStatsOneShot(arg0, arg1)
+}
+
+func (w withPodman) ContainerStop(arg0 context.Context, arg1 string, arg2 *time.Duration) error {
+	return w.pimpl.ContainerStop(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerTop(arg0 context.Context, arg1 string, arg2 []string) (container.ContainerTopOKBody, error) {
+	return w.pimpl.ContainerTop(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerUnpause(arg0 context.Context, arg1 string) error {
+	return w.pimpl.ContainerUnpause(arg0, arg1)
+}
+
+func (w withPodman) ContainerUpdate(arg0 context.Context, arg1 string, arg2 container.UpdateConfig) (container.ContainerUpdateOKBody, error) {
+	return w.pimpl.ContainerUpdate(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainerWait(arg0 context.Context, arg1 string, arg2 container.WaitCondition) (<-chan container.ContainerWaitOKBody, <-chan error) {
+	return w.pimpl.ContainerWait(arg0, arg1, arg2)
+}
+
+func (w withPodman) ContainersPrune(arg0 context.Context, arg1 filters.Args) (types.ContainersPruneReport, error) {
+	return w.pimpl.ContainersPrune(arg0, arg1)
+}
+
+func (w withPodman) CopyFromContainer(arg0 context.Context, arg1 string, arg2 string) (io.ReadCloser, types.ContainerPathStat, error) {
+	return w.pimpl.CopyFromContainer(arg0, arg1, arg2)
+}
+
+func (w withPodman) CopyToContainer(arg0 context.Context, arg1 string, arg2 string, arg3 io.Reader, arg4 types.CopyToContainerOptions) error {
+	return w.pimpl.CopyToContainer(arg0, arg1, arg2, arg3, arg4)
+}
+
+func (w withPodman) DaemonHost() string {
+	return w.pimpl.DaemonHost()
+}
+
+func (w withPodman) DialHijack(arg0 context.Context, arg1 string, arg2 string, arg3 map[string][]string) (net.Conn, error) {
+	return w.pimpl.DialHijack(arg0, arg1, arg2, arg3)
+}
+
+func (w withPodman) Dialer() func(context.Context) (net.Conn, error) {
+	return w.pimpl.Dialer()
+}
+
+func (w withPodman) DiskUsage(arg0 context.Context) (types.DiskUsage, error) {
+	return w.pimpl.DiskUsage(arg0)
+}
+
+func (w withPodman) DistributionInspect(arg0 context.Context, arg1 string, arg2 string) (registry.DistributionInspect, error) {
+	return w.pimpl.DistributionInspect(arg0, arg1, arg2)
+}
+
+func (w withPodman) Events(arg0 context.Context, arg1 types.EventsOptions) (<-chan events.Message, <-chan error) {
+	return w.pimpl.Events(arg0, arg1)
+}
+
+func (w withPodman) HTTPClient() *http.Client {
+	return w.pimpl.HTTPClient()
+}
+
+func (w withPodman) ImageBuild(arg0 context.Context, arg1 io.Reader, arg2 types.ImageBuildOptions) (types.ImageBuildResponse, error) {
+	return w.pimpl.ImageBuild(arg0, arg1, arg2)
+}
+
+func (w withPodman) ImageCreate(arg0 context.Context, arg1 string, arg2 types.ImageCreateOptions) (io.ReadCloser, error) {
+	return w.pimpl.ImageCreate(arg0, arg1, arg2)
+}
+
+func (w withPodman) ImageHistory(arg0 context.Context, arg1 string) ([]image.HistoryResponseItem, error) {
+	return w.pimpl.ImageHistory(arg0, arg1)
+}
+
+func (w withPodman) ImageImport(arg0 context.Context, arg1 types.ImageImportSource, arg2 string, arg3 types.ImageImportOptions) (io.ReadCloser, error) {
+	return w.pimpl.ImageImport(arg0, arg1, arg2, arg3)
+}
+
+func (w withPodman) ImageInspectWithRaw(arg0 context.Context, arg1 string) (types.ImageInspect, []uint8, error) {
+	return w.pimpl.ImageInspectWithRaw(arg0, arg1)
+}
+
+func (w withPodman) ImageList(arg0 context.Context, arg1 types.ImageListOptions) ([]types.ImageSummary, error) {
+	return w.pimpl.ImageList(arg0, arg1)
+}
+
+func (w withPodman) ImageLoad(arg0 context.Context, arg1 io.Reader, arg2 bool) (types.ImageLoadResponse, error) {
+	return w.pimpl.ImageLoad(arg0, arg1, arg2)
+}
+
+func (w withPodman) ImagePull(arg0 context.Context, arg1 string, arg2 types.ImagePullOptions) (io.ReadCloser, error) {
+	return w.pimpl.ImagePull(arg0, arg1, arg2)
+}
+
+func (w withPodman) ImagePush(arg0 context.Context, arg1 string, arg2 types.ImagePushOptions) (io.ReadCloser, error) {
+	return w.pimpl.ImagePush(arg0, arg1, arg2)
+}
+
+func (w withPodman) ImageRemove(arg0 context.Context, arg1 string, arg2 types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
+	return w.pimpl.ImageRemove(arg0, arg1, arg2)
+}
+
+func (w withPodman) ImageSave(arg0 context.Context, arg1 []string) (io.ReadCloser, error) {
+	return w.pimpl.ImageSave(arg0, arg1)
+}
+
+func (w withPodman) ImageSearch(arg0 context.Context, arg1 string, arg2 types.ImageSearchOptions) ([]registry.SearchResult, error) {
+	return w.pimpl.ImageSearch(arg0, arg1, arg2)
+}
+
+func (w withPodman) ImageTag(arg0 context.Context, arg1 string, arg2 string) error {
+	return w.pimpl.ImageTag(arg0, arg1, arg2)
+}
+
+func (w withPodman) ImagesPrune(arg0 context.Context, arg1 filters.Args) (types.ImagesPruneReport, error) {
+	return w.pimpl.ImagesPrune(arg0, arg1)
+}
+
+func (w withPodman) Info(arg0 context.Context) (types.Info, error) {
+	return w.pimpl.Info(arg0)
+}
+
+func (w withPodman) NegotiateAPIVersion(arg0 context.Context) {
+	w.pimpl.NegotiateAPIVersion(arg0)
+}
+
+func (w withPodman) NegotiateAPIVersionPing(arg0 types.Ping) {
+	w.pimpl.NegotiateAPIVersionPing(arg0)
+}
+
+func (w withPodman) NetworkConnect(arg0 context.Context, arg1 string, arg2 string, arg3 *network.EndpointSettings) error {
+	return w.pimpl.NetworkConnect(arg0, arg1, arg2, arg3)
+}
+
+func (w withPodman) NetworkCreate(arg0 context.Context, arg1 string, arg2 types.NetworkCreate) (types.NetworkCreateResponse, error) {
+	return w.pimpl.NetworkCreate(arg0, arg1, arg2)
+}
+
+func (w withPodman) NetworkDisconnect(arg0 context.Context, arg1 string, arg2 string, arg3 bool) error {
+	return w.pimpl.NetworkDisconnect(arg0, arg1, arg2, arg3)
+}
+
+func (w withPodman) NetworkInspect(arg0 context.Context, arg1 string, arg2 types.NetworkInspectOptions) (types.NetworkResource, error) {
+	return w.pimpl.NetworkInspect(arg0, arg1, arg2)
+}
+
+func (w withPodman) NetworkInspectWithRaw(arg0 context.Context, arg1 string, arg2 types.NetworkInspectOptions) (types.NetworkResource, []uint8, error) {
+	return w.pimpl.NetworkInspectWithRaw(arg0, arg1, arg2)
+}
+
+func (w withPodman) NetworkList(arg0 context.Context, arg1 types.NetworkListOptions) ([]types.NetworkResource, error) {
+	return w.pimpl.NetworkList(arg0, arg1)
+}
+
+func (w withPodman) NetworkRemove(arg0 context.Context, arg1 string) error {
+	return w.pimpl.NetworkRemove(arg0, arg1)
+}
+
+func (w withPodman) NetworksPrune(arg0 context.Context, arg1 filters.Args) (types.NetworksPruneReport, error) {
+	return w.pimpl.NetworksPrune(arg0, arg1)
+}
+
+func (w withPodman) NodeInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Node, []uint8, error) {
+	return w.pimpl.NodeInspectWithRaw(arg0, arg1)
+}
+
+func (w withPodman) NodeList(arg0 context.Context, arg1 types.NodeListOptions) ([]swarm.Node, error) {
+	return w.pimpl.NodeList(arg0, arg1)
+}
+
+func (w withPodman) NodeRemove(arg0 context.Context, arg1 string, arg2 types.NodeRemoveOptions) error {
+	return w.pimpl.NodeRemove(arg0, arg1, arg2)
+}
+
+func (w withPodman) NodeUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.NodeSpec) error {
+	return w.pimpl.NodeUpdate(arg0, arg1, arg2, arg3)
+}
+
+func (w withPodman) Ping(arg0 context.Context) (types.Ping, error) {
+	return w.pimpl.Ping(arg0)
+}
+
+func (w withPodman) PluginCreate(arg0 context.Context, arg1 io.Reader, arg2 types.PluginCreateOptions) error {
+	return w.pimpl.PluginCreate(arg0, arg1, arg2)
+}
+
+func (w withPodman) PluginDisable(arg0 context.Context, arg1 string, arg2 types.PluginDisableOptions) error {
+	return w.pimpl.PluginDisable(arg0, arg1, arg2)
+}
+
+func (w withPodman) PluginEnable(arg0 context.Context, arg1 string, arg2 types.PluginEnableOptions) error {
+	return w.pimpl.PluginEnable(arg0, arg1, arg2)
+}
+
+func (w withPodman) PluginInspectWithRaw(arg0 context.Context, arg1 string) (*types.Plugin, []uint8, error) {
+	return w.pimpl.PluginInspectWithRaw(arg0, arg1)
+}
+
+func (w withPodman) PluginInstall(arg0 context.Context, arg1 string, arg2 types.PluginInstallOptions) (io.ReadCloser, error) {
+	return w.pimpl.PluginInstall(arg0, arg1, arg2)
+}
+
+func (w withPodman) PluginList(arg0 context.Context, arg1 filters.Args) (types.PluginsListResponse, error) {
+	return w.pimpl.PluginList(arg0, arg1)
+}
+
+func (w withPodman) PluginPush(arg0 context.Context, arg1 string, arg2 string) (io.ReadCloser, error) {
+	return w.pimpl.PluginPush(arg0, arg1, arg2)
+}
+
+func (w withPodman) PluginRemove(arg0 context.Context, arg1 string, arg2 types.PluginRemoveOptions) error {
+	return w.pimpl.PluginRemove(arg0, arg1, arg2)
+}
+
+func (w withPodman) PluginSet(arg0 context.Context, arg1 string, arg2 []string) error {
+	return w.pimpl.PluginSet(arg0, arg1, arg2)
+}
+
+func (w withPodman) PluginUpgrade(arg0 context.Context, arg1 string, arg2 types.PluginInstallOptions) (io.ReadCloser, error) {
+	return w.pimpl.PluginUpgrade(arg0, arg1, arg2)
+}
+
+func (w withPodman) RegistryLogin(arg0 context.Context, arg1 types.AuthConfig) (registry.AuthenticateOKBody, error) {
+	return w.pimpl.RegistryLogin(arg0, arg1)
+}
+
+func (w withPodman) SecretCreate(arg0 context.Context, arg1 swarm.SecretSpec) (types.SecretCreateResponse, error) {
+	return w.pimpl.SecretCreate(arg0, arg1)
+}
+
+func (w withPodman) SecretInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Secret, []uint8, error) {
+	return w.pimpl.SecretInspectWithRaw(arg0, arg1)
+}
+
+func (w withPodman) SecretList(arg0 context.Context, arg1 types.SecretListOptions) ([]swarm.Secret, error) {
+	return w.pimpl.SecretList(arg0, arg1)
+}
+
+func (w withPodman) SecretRemove(arg0 context.Context, arg1 string) error {
+	return w.pimpl.SecretRemove(arg0, arg1)
+}
+
+func (w withPodman) SecretUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.SecretSpec) error {
+	return w.pimpl.SecretUpdate(arg0, arg1, arg2, arg3)
+}
+
+func (w withPodman) ServerVersion(arg0 context.Context) (types.Version, error) {
+	return w.pimpl.ServerVersion(arg0)
+}
+
+func (w withPodman) ServiceCreate(arg0 context.Context, arg1 swarm.ServiceSpec, arg2 types.ServiceCreateOptions) (types.ServiceCreateResponse, error) {
+	return w.pimpl.ServiceCreate(arg0, arg1, arg2)
+}
+
+func (w withPodman) ServiceInspectWithRaw(arg0 context.Context, arg1 string, arg2 types.ServiceInspectOptions) (swarm.Service, []uint8, error) {
+	return w.pimpl.ServiceInspectWithRaw(arg0, arg1, arg2)
+}
+
+func (w withPodman) ServiceList(arg0 context.Context, arg1 types.ServiceListOptions) ([]swarm.Service, error) {
+	return w.pimpl.ServiceList(arg0, arg1)
+}
+
+func (w withPodman) ServiceLogs(arg0 context.Context, arg1 string, arg2 types.ContainerLogsOptions) (io.ReadCloser, error) {
+	return w.pimpl.ServiceLogs(arg0, arg1, arg2)
+}
+
+func (w withPodman) ServiceRemove(arg0 context.Context, arg1 string) error {
+	return w.pimpl.ServiceRemove(arg0, arg1)
+}
+
+func (w withPodman) ServiceUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.ServiceSpec, arg4 types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) {
+	return w.pimpl.ServiceUpdate(arg0, arg1, arg2, arg3, arg4)
+}
+
+func (w withPodman) SwarmGetUnlockKey(arg0 context.Context) (types.SwarmUnlockKeyResponse, error) {
+	return w.pimpl.SwarmGetUnlockKey(arg0)
+}
+
+func (w withPodman) SwarmInit(arg0 context.Context, arg1 swarm.InitRequest) (string, error) {
+	return w.pimpl.SwarmInit(arg0, arg1)
+}
+
+func (w withPodman) SwarmInspect(arg0 context.Context) (swarm.Swarm, error) {
+	return w.pimpl.SwarmInspect(arg0)
+}
+
+func (w withPodman) SwarmJoin(arg0 context.Context, arg1 swarm.JoinRequest) error {
+	return w.pimpl.SwarmJoin(arg0, arg1)
+}
+
+func (w withPodman) SwarmLeave(arg0 context.Context, arg1 bool) error {
+	return w.pimpl.SwarmLeave(arg0, arg1)
+}
+
+func (w withPodman) SwarmUnlock(arg0 context.Context, arg1 swarm.UnlockRequest) error {
+	return w.pimpl.SwarmUnlock(arg0, arg1)
+}
+
+func (w withPodman) SwarmUpdate(arg0 context.Context, arg1 swarm.Version, arg2 swarm.Spec, arg3 swarm.UpdateFlags) error {
+	return w.pimpl.SwarmUpdate(arg0, arg1, arg2, arg3)
+}
+
+func (w withPodman) TaskInspectWithRaw(arg0 context.Context, arg1 string) (swarm.Task, []uint8, error) {
+	return w.pimpl.TaskInspectWithRaw(arg0, arg1)
+}
+
+func (w withPodman) TaskList(arg0 context.Context, arg1 types.TaskListOptions) ([]swarm.Task, error) {
+	return w.pimpl.TaskList(arg0, arg1)
+}
+
+func (w withPodman) TaskLogs(arg0 context.Context, arg1 string, arg2 types.ContainerLogsOptions) (io.ReadCloser, error) {
+	return w.pimpl.TaskLogs(arg0, arg1, arg2)
+}
+
+func (w withPodman) VolumeCreate(arg0 context.Context, arg1 volume.VolumeCreateBody) (types.Volume, error) {
+	return w.pimpl.VolumeCreate(arg0, arg1)
+}
+
+func (w withPodman) VolumeInspect(arg0 context.Context, arg1 string) (types.Volume, error) {
+	return w.pimpl.VolumeInspect(arg0, arg1)
+}
+
+func (w withPodman) VolumeInspectWithRaw(arg0 context.Context, arg1 string) (types.Volume, []uint8, error) {
+	return w.pimpl.VolumeInspectWithRaw(arg0, arg1)
+}
+
+func (w withPodman) VolumeList(arg0 context.Context, arg1 filters.Args) (volume.VolumeListOKBody, error) {
+	return w.pimpl.VolumeList(arg0, arg1)
+}
+
+func (w withPodman) VolumeRemove(arg0 context.Context, arg1 string, arg2 bool) error {
+	return w.pimpl.VolumeRemove(arg0, arg1, arg2)
+}
+
+func (w withPodman) VolumesPrune(arg0 context.Context, arg1 filters.Args) (types.VolumesPruneReport, error) {
+	return w.pimpl.VolumesPrune(arg0, arg1)
+}
+
+//</editor-fold>
diff --git a/docker/pusher.go b/docker/pusher.go
index df1aaf7a..73e0fff3 100644
--- a/docker/pusher.go
+++ b/docker/pusher.go
@@ -13,6 +13,8 @@ import (
 	"regexp"
 	"strings"
 
+	"github.com/docker/docker/client"
+
 	fn "knative.dev/kn-plugin-func"
 
 	"github.com/containers/image/v5/pkg/docker/config"
@@ -39,10 +41,11 @@ var ErrUnauthorized = errors.New("bad credentials")
 type VerifyCredentialsCallback func(ctx context.Context, username, password, registry string) error
 
 func CheckAuth(ctx context.Context, username, password, registry string) error {
-	cli, _, err := NewDockerClient()
+	cli, _, err := NewClient(client.DefaultDockerHost)
 	if err != nil {
 		return err
 	}
+	defer cli.Close()
 
 	_, err = cli.RegistryLogin(ctx, types.AuthConfig{Username: username, Password: password, ServerAddress: registry})
 	if err != nil && strings.Contains(err.Error(), "401 Unauthorized") {
@@ -242,10 +245,11 @@ func (n *Pusher) Push(ctx context.Context, f fn.Function) (digest string, err er
 		return "", err
 	}
 
-	cli, _, err := NewDockerClient()
+	cli, _, err := NewClient(client.DefaultDockerHost)
 	if err != nil {
 		return "", fmt.Errorf("failed to create docker api client: %w", err)
 	}
+	defer cli.Close()
 
 	n.progressListener.Stopping()
 	credentials, err := n.credentialsProvider(ctx, registry)
diff --git a/docker/runner.go b/docker/runner.go
index ff67103f..a96fa47c 100644
--- a/docker/runner.go
+++ b/docker/runner.go
@@ -8,6 +8,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/docker/docker/client"
+
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/pkg/stdcopy"
@@ -33,10 +35,11 @@ func (n *Runner) Run(ctx context.Context, f fn.Function) error {
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
-	cli, _, err := NewDockerClient()
+	cli, _, err := NewClient(client.DefaultDockerHost)
 	if err != nil {
 		return errors.Wrap(err, "failed to create docker api client")
 	}
+	defer cli.Close()
 
 	if f.Image == "" {
 		return errors.New("Function has no associated Image. Has it been built?")
diff --git a/function.go b/function.go
index a0114c77..0697f98d 100644
--- a/function.go
+++ b/function.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
+	"time"
 )
 
 type Function struct {
@@ -75,6 +76,11 @@ type Function struct {
 
 	// Health endpoints specified by the language pack
 	HealthEndpoints HealthEndpoints
+
+	// Created time is the moment that creation was successfully completed
+	// according to the client which is in charge of what constitutes being
+	// fully "Created" (aka initialized)
+	Created time.Time
 }
 
 // NewFunction loads a Function from a path on disk. use .Initialized() to determine if
diff --git a/go.mod b/go.mod
index d498bcb1..a999454e 100644
--- a/go.mod
+++ b/go.mod
@@ -21,6 +21,7 @@ require (
 	github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c
 	github.com/markbates/pkger v0.17.1
 	github.com/mitchellh/go-homedir v1.1.0
+	github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6
 	github.com/ory/viper v1.7.5
 	github.com/pkg/errors v0.9.1
 	github.com/spf13/cobra v1.2.1
diff --git a/schema/func_yaml-schema.json b/schema/func_yaml-schema.json
index f982af45..5f1983a7 100644
--- a/schema/func_yaml-schema.json
+++ b/schema/func_yaml-schema.json
@@ -18,7 +18,8 @@
 				"envs",
 				"annotations",
 				"options",
-				"labels"
+				"labels",
+				"created"
 			],
 			"properties": {
 				"name": {
@@ -96,6 +97,10 @@
 						"$ref": "#/definitions/Label"
 					},
 					"type": "array"
+				},
+				"created": {
+					"type": "string",
+					"format": "date-time"
 				}
 			},
 			"additionalProperties": false,
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 49e37c0d..af64fa05 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -536,6 +536,7 @@ github.com/morikuni/aec
 # github.com/opencontainers/go-digest v1.0.0
 github.com/opencontainers/go-digest
 # github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6
+## explicit
 github.com/opencontainers/image-spec/specs-go
 github.com/opencontainers/image-spec/specs-go/v1
 # github.com/opencontainers/runc v1.0.0-rc93


Test patch:
diff --git a/client_test.go b/client_test.go
index 8ba35035..12d6ac97 100644
--- a/client_test.go
+++ b/client_test.go
@@ -12,6 +12,7 @@ import (
 	"path/filepath"
 	"reflect"
 	"testing"
+	"time"
 
 	fn "knative.dev/kn-plugin-func"
 	"knative.dev/kn-plugin-func/mock"
@@ -877,6 +878,29 @@ func TestRuntimes(t *testing.T) {
 	}
 }
 
+// TestCreateStamp ensures that the creation timestamp is set on functions
+// which are successfully initialized using the client library.
+func TestCreateStamp(t *testing.T) {
+	root := "testdata/example.com/testCreateStamp"
+	defer using(t, root)()
+
+	start := time.Now()
+
+	client := fn.New(fn.WithRegistry(TestRegistry))
+
+	if err := client.New(context.Background(), fn.Function{Root: root}); err != nil {
+		t.Fatal(err)
+	}
+
+	f, err := fn.NewFunction(root)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !f.Created.After(start) {
+		t.Fatalf("expected function timestamp to be after '%v', got '%v'", start, f.Created)
+	}
+}
+
 // Helpers ----
 
 // USING:  Make specified dir.  Return deferrable cleanup fn.
diff --git a/cloudevents/emitter_test.go b/cloudevents/emitter_test.go
index 31fbe631..a16ddbe6 100644
--- a/cloudevents/emitter_test.go
+++ b/cloudevents/emitter_test.go
@@ -1,3 +1,6 @@
+//go:build !integration
+// +build !integration
+
 package cloudevents
 
 import (
diff --git a/cmd/delete_test.go b/cmd/delete_test.go
index 7eba6437..895a4b48 100644
--- a/cmd/delete_test.go
+++ b/cmd/delete_test.go
@@ -63,6 +63,7 @@ builders:
 envs: []
 annotations: {}
 labels: []
+created: 2021-01-01T00:00:00+00:00
 `
 	if err := ioutil.WriteFile("func.yaml", []byte(funcYaml), 0600); err != nil {
 		t.Fatal(err)
diff --git a/docker/credentials_helper_test.go b/docker/credentials_helper_test.go
index 5e60f074..0c536f39 100644
--- a/docker/credentials_helper_test.go
+++ b/docker/credentials_helper_test.go
@@ -1,3 +1,6 @@
+//go:build !integration
+// +build !integration
+
 package docker
 
 import "testing"
diff --git a/docker/docker_client_linux_test.go b/docker/docker_client_linux_test.go
new file mode 100644
index 00000000..361fd08e
--- /dev/null
+++ b/docker/docker_client_linux_test.go
@@ -0,0 +1,114 @@
+package docker_test
+
+import (
+	"context"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"testing"
+	"time"
+
+	"knative.dev/kn-plugin-func/docker"
+)
+
+// Test that we are starting podman service on behalf of user
+// if docker daemon is not present.
+func TestNewDockerClientWithAutomaticPodman(t *testing.T) {
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Minute*1)
+	defer cancel()
+
+	defer withMockedPodmanBinary(t)()
+	defer withEnvVar(t, "DOCKER_HOST", "")()
+
+	dockerClient, _, err := docker.NewClient("unix:///var/run/nonexistent.sock")
+	if err != nil {
+		t.Error(err)
+	}
+	defer dockerClient.Close()
+
+	_, err = dockerClient.Ping(ctx)
+	if err != nil {
+		t.Error(err)
+	}
+
+}
+
+const helperGoScriptContent = `package main
+
+import (
+	"fmt"
+	"net"
+	"net/http"
+	"net/url"
+	"os"
+)
+
+func main() {
+	dockerHost := os.Args[3]
+	u, err := url.Parse(dockerHost)
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+	sock := u.Path
+
+	server := http.Server{}
+	listener, err := net.Listen("unix", sock)
+	if err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(1)
+	}
+	// mimics /_ping endpoint
+	server.Handler = http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+		writer.Header().Add("Content-Type", "text/plain")
+		writer.WriteHeader(200)
+		writer.Write([]byte("OK"))
+	})
+	server.Serve(listener)
+}
+`
+
+func withMockedPodmanBinary(t *testing.T) func() {
+	var err error
+	binDir := t.TempDir()
+
+	newPath := binDir + string(os.PathListSeparator) + os.Getenv("PATH")
+	cleanUpPath := withEnvVar(t, "PATH", newPath)
+
+	helperGoScriptPath := filepath.Join(binDir, "main.go")
+
+	err = ioutil.WriteFile(helperGoScriptPath,
+		[]byte(helperGoScriptContent),
+		0400)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	runnerScriptName := "podman"
+	if runtime.GOOS == "windows" {
+		runnerScriptName = runnerScriptName + ".bat"
+	}
+
+	runnerScriptContent := `#!/bin/sh
+exec go run GO_SCRIPT_PATH $@;
+`
+	if runtime.GOOS == "windows" {
+		runnerScriptContent = `@echo off
+go.exe run GO_SCRIPT_PATH %*
+`
+	}
+
+	runnerScriptPath := filepath.Join(binDir, runnerScriptName)
+	runnerScriptContent = strings.ReplaceAll(runnerScriptContent, "GO_SCRIPT_PATH", helperGoScriptPath)
+	err = ioutil.WriteFile(runnerScriptPath, []byte(runnerScriptContent), 0700)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return func() {
+		cleanUpPath()
+	}
+}
diff --git a/docker/docker_client_test.go b/docker/docker_client_test.go
new file mode 100644
index 00000000..4f227976
--- /dev/null
+++ b/docker/docker_client_test.go
@@ -0,0 +1,86 @@
+package docker_test
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/docker/docker/client"
+	"knative.dev/kn-plugin-func/docker"
+)
+
+// Test that we are creating client in accordance
+// with the DOCKER_HOST environment variable
+func TestNewDockerClient(t *testing.T) {
+	ctx, cancel := context.WithTimeout(context.Background(), time.Minute*1)
+	defer cancel()
+
+	tmpDir := t.TempDir()
+	sock := filepath.Join(tmpDir, "docker.sock")
+
+	defer startMockDaemon(t, sock)()
+
+	defer withEnvVar(t, "DOCKER_HOST", fmt.Sprintf("unix://%s", sock))()
+
+	dockerClient, _, err := docker.NewClient(client.DefaultDockerHost)
+	if err != nil {
+		t.Error(err)
+	}
+	defer dockerClient.Close()
+
+	_, err = dockerClient.Ping(ctx)
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func withEnvVar(t *testing.T, name, value string) func() {
+	oldDh, hadDh := os.LookupEnv(name)
+	err := os.Setenv(name, value)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return func() {
+		if hadDh {
+			os.Setenv(name, oldDh)
+		} else {
+			os.Unsetenv(name)
+		}
+	}
+}
+
+func startMockDaemon(t *testing.T, sock string) func() {
+
+	server := http.Server{}
+	listener, err := net.Listen("unix", sock)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// mimics /_ping endpoint
+	server.Handler = http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+		writer.Header().Add("Content-Type", "text/plain")
+		writer.WriteHeader(200)
+		_, _ = writer.Write([]byte("OK"))
+	})
+
+	serErrChan := make(chan error)
+	go func() {
+		serErrChan <- server.Serve(listener)
+	}()
+	return func() {
+		err := server.Shutdown(context.Background())
+		if err != nil {
+			t.Fatal(err)
+		}
+		err = <-serErrChan
+		if err != nil && !strings.Contains(err.Error(), "Server closed") {
+			t.Fatal(err)
+		}
+	}
+}
diff --git a/docker/pusher_test.go b/docker/pusher_test.go
index d1f90fbc..d3c7caa4 100644
--- a/docker/pusher_test.go
+++ b/docker/pusher_test.go
@@ -1,3 +1,6 @@
+//go:build !integration
+// +build !integration
+
 package docker
 
 import (
diff --git a/knative/deployer_test.go b/knative/deployer_test.go
index 68f184f0..43065e42 100644
--- a/knative/deployer_test.go
+++ b/knative/deployer_test.go
@@ -1,3 +1,6 @@
+//go:build !integration
+// +build !integration
+
 package knative
 
 import (
diff --git a/repository_test.go b/repository_test.go
index a55aa405..e5a55d37 100644
--- a/repository_test.go
+++ b/repository_test.go
@@ -1,3 +1,6 @@
+//go:build !integration
+// +build !integration
+
 package function_test
 
 import (