Repository: knative-sandbox/kn-plugin-func
Pull request #1224 (2022-09-08 14:42:20)

feat: UX improvements for docker/podman usage
# Changes

- :gift: Better error message if docker/podman not present.
- :gift: Auto detect podman machine's socket on mac/win, so user doesn't have to set `DOCKER_HOST`.


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

This pull-request has been approved by: *<a href="https://github.com/knative-sandbox/kn-plugin-func/pull/1224#" title="Author self-approved">matejvasek</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)~~ [matejvasek]

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/1224?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) Report
> Merging [#1224](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1224?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (1148bc3) into [main](https://codecov.io/gh/knative-sandbox/kn-plugin-func/commit/4e5e9927b24576ba7ca5fbe6d08c0eefa65bc266?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (4e5e992) will **decrease** coverage by `0.04%`.
> The diff coverage is `25.00%`.

```diff
@@            Coverage Diff             @@
##             main    #1224      +/-   ##
==========================================
- Coverage   45.38%   45.34%   -0.05%     
==========================================
  Files          68       68              
  Lines        9727     9747      +20     
==========================================
+ Hits         4415     4420       +5     
- Misses       4919     4934      +15     
  Partials      393      393              
```


| [Impacted Files](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1224?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) | Coverage Δ | |
|---|---|---|
| [docker/docker\_client.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1224/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-ZG9ja2VyL2RvY2tlcl9jbGllbnQuZ28=) | `72.66% <25.00%> (-9.65%)` | :arrow_down: |
| [s2i/builder.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/1224/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-czJpL2J1aWxkZXIuZ28=) | `62.24% <0.00%> (+1.20%)` | :arrow_up: |

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

knative-prow[bot]: New changes are detected. LGTM label has been removed.
matejvasek: PTAL @lance @lkingland 

Patch:
diff --git a/cmd/func/main.go b/cmd/func/main.go
index e40812b3..8cc06254 100644
--- a/cmd/func/main.go
+++ b/cmd/func/main.go
@@ -2,12 +2,15 @@ package main
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
+	"os/exec"
 	"os/signal"
 	"syscall"
 
 	"knative.dev/kn-plugin-func/cmd"
+	"knative.dev/kn-plugin-func/docker"
 )
 
 // Statically-populated build metadata set by `make build`.
@@ -39,6 +42,29 @@ func main() {
 		if ctx.Err() != nil {
 			os.Exit(130)
 		}
+
+		if errors.Is(err, docker.ErrNoDocker) {
+			if !dockerOrPodmanInstalled() {
+				fmt.Fprintln(os.Stderr, `Docker/Podman not installed.
+Please consider installing one of these:
+  https://podman-desktop.io/
+  https://www.docker.com/products/docker-desktop/`)
+			} else {
+				fmt.Fprintln(os.Stderr, `Possible causes:
+  The docker/podman daemon is not running.
+  The DOCKER_HOST environment variable is not set.`)
+			}
+		}
+
 		os.Exit(1)
 	}
 }
+
+func dockerOrPodmanInstalled() bool {
+	_, err := exec.LookPath("podman")
+	if err == nil {
+		return true
+	}
+	_, err = exec.LookPath("docker")
+	return err == nil
+}
diff --git a/docker/docker_client.go b/docker/docker_client.go
index 3ee9d36d..623a6cca 100644
--- a/docker/docker_client.go
+++ b/docker/docker_client.go
@@ -3,6 +3,7 @@ package docker
 import (
 	"bytes"
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
@@ -15,11 +16,13 @@ import (
 	"syscall"
 	"time"
 
-	"knative.dev/kn-plugin-func/ssh"
-
 	"github.com/docker/docker/client"
+
+	"knative.dev/kn-plugin-func/ssh"
 )
 
+var ErrNoDocker = errors.New("docker/podman API not available")
+
 // NewClient creates a new docker client.
 // reads the DOCKER_HOST envvar but it may or may not return it as dockerHost.
 //  - For local connection (unix socket and windows named pipe) it returns the
@@ -28,27 +31,45 @@ import (
 //  - For TCP connections it returns "" so it defaults in the remote (note that
 //    one should not be use client.DefaultDockerHost in this situation). This is
 //    needed beaus of TCP+tls connections.
-func NewClient(defaultHost string) (dockerClient client.CommonAPIClient, dockerHost string, err error) {
+func NewClient(defaultHost string) (dockerClient client.CommonAPIClient, dockerHostInRemote string, err error) {
 	var _url *url.URL
 
-	dockerHost = os.Getenv("DOCKER_HOST")
+	dockerHost := os.Getenv("DOCKER_HOST")
+	dockerHostSSHIdentity := os.Getenv("DOCKER_HOST_SSH_IDENTITY")
 
-	if dockerHost == "" && runtime.GOOS == "linux" && podmanPresent() {
+	if dockerHost == "" {
 		_url, err = url.Parse(defaultHost)
 		if err != nil {
 			return
 		}
 		_, err = os.Stat(_url.Path)
 		switch {
+		case err == nil:
+			dockerHost = defaultHost
 		case err != nil && !os.IsNotExist(err):
 			return
-		case os.IsNotExist(err):
-			dockerClient, dockerHost, err = newClientWithPodmanService()
-			dockerClient = &closeGuardingClient{pimpl: dockerClient}
-			return
+		case os.IsNotExist(err) && podmanPresent():
+			if runtime.GOOS == "linux" {
+				// on Linux: spawn temporary podman service
+				dockerClient, dockerHostInRemote, err = newClientWithPodmanService()
+				dockerClient = &closeGuardingClient{pimpl: dockerClient}
+				return
+			} else {
+				// on non-Linux: try to use connection to podman machine
+				dh, dhid := tryGetPodmanRemoteConn()
+				if dh != "" {
+					dockerHost, dockerHostSSHIdentity = dh, dhid
+				}
+			}
 		}
 	}
 
+	if dockerHost == "" {
+		return nil, "", ErrNoDocker
+	}
+
+	dockerHostInRemote = dockerHost
+
 	_url, err = url.Parse(dockerHost)
 	isSSH := err == nil && _url.Scheme == "ssh"
 	isTCP := err == nil && _url.Scheme == "tcp"
@@ -57,23 +78,23 @@ func NewClient(defaultHost string) (dockerClient client.CommonAPIClient, dockerH
 		// With TCP, it's difficult to determine how to expose the daemon socket to lifecycle containers,
 		// so we are defaulting to standard docker location by returning empty string.
 		// This should work well most of the time.
-		dockerHost = ""
+		dockerHostInRemote = ""
 	}
 
 	if !isSSH {
-		dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
+		dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation(), client.WithHost(dockerHost))
 		dockerClient = &closeGuardingClient{pimpl: dockerClient}
 		return
 	}
 
 	credentialsConfig := ssh.Config{
-		Identity:           os.Getenv("DOCKER_HOST_SSH_IDENTITY"),
+		Identity:           dockerHostSSHIdentity,
 		PassPhrase:         os.Getenv("DOCKER_HOST_SSH_IDENTITY_PASSPHRASE"),
 		PasswordCallback:   ssh.NewPasswordCbk(),
 		PassPhraseCallback: ssh.NewPassPhraseCbk(),
 		HostKeyCallback:    ssh.NewHostKeyCbk(),
 	}
-	contextDialer, dockerHost, err := ssh.NewDialContext(_url, credentialsConfig)
+	contextDialer, dockerHostInRemote, err := ssh.NewDialContext(_url, credentialsConfig)
 	if err != nil {
 		return
 	}
@@ -101,7 +122,36 @@ func NewClient(defaultHost string) (dockerClient client.CommonAPIClient, dockerH
 	}
 
 	dockerClient = &closeGuardingClient{pimpl: dockerClient}
-	return dockerClient, dockerHost, err
+	return dockerClient, dockerHostInRemote, err
+}
+
+// tries to get connection to default podman machine
+func tryGetPodmanRemoteConn() (uri string, identity string) {
+	cmd := exec.Command("podman", "system", "connection", "list", "--format=json")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return "", ""
+	}
+	var connections []struct {
+		Name     string
+		URI      string
+		Identity string
+		Default  bool
+	}
+	err = json.Unmarshal(out, &connections)
+	if err != nil {
+		return "", ""
+	}
+
+	for _, c := range connections {
+		if c.Default {
+			uri = c.URI
+			identity = c.Identity
+			break
+		}
+	}
+
+	return uri, identity
 }
 
 func podmanPresent() bool {
diff --git a/docs/reference/podman.md b/docs/reference/podman.md
index c4892c29..a875f22d 100644
--- a/docs/reference/podman.md
+++ b/docs/reference/podman.md
@@ -26,6 +26,8 @@ Now you may use `func` as usual.
 
 For macOS and Windows we recommend [Podman Desktop](https://podman-desktop.io/).
 
+It's recommended to run podman machine in rootful mode (`podman machine set --rootful`).
+
 The docs will guide you trough `podman machine` setup.
 The `podman machine start` command it will output path to API socket.
 
diff --git a/function.go b/function.go
index c1558e96..9fd7a8b6 100644
--- a/function.go
+++ b/function.go
@@ -11,6 +11,8 @@ import (
 	"time"
 
 	"gopkg.in/yaml.v2"
+	fnlabels "knative.dev/kn-plugin-func/k8s/labels"
+	"knative.dev/pkg/ptr"
 )
 
 // FunctionFile is the file used for the serialized form of a function.
@@ -344,6 +346,61 @@ func (f Function) ImageWithDigest() string {
 	return part1 + strings.Split(part2, ":")[0] + "@" + f.ImageDigest
 }
 
+// LabelsMap combines default labels with the labels slice provided.
+// It will the resulting slice with ValidateLabels and return a key/value map.
+//   - key: EXAMPLE1                            # Label directly from a value
+//     value: value1
+//   - key: EXAMPLE2                            # Label from the local ENV var
+//     value: {{ env:MY_ENV }}
+func (f Function) LabelsMap() (map[string]string, error) {
+	defaultLabels := []Label{
+		{
+			Key:   ptr.String(fnlabels.FunctionKey),
+			Value: ptr.String(fnlabels.FunctionValue),
+		},
+		{
+			Key:   ptr.String(fnlabels.FunctionNameKey),
+			Value: ptr.String(f.Name),
+		},
+		{
+			Key:   ptr.String(fnlabels.FunctionRuntimeKey),
+			Value: ptr.String(f.Runtime),
+		},
+		// --- handle usage of deprecated labels (`boson.dev/function`, `boson.dev/runtime`)
+		{
+			Key:   ptr.String(fnlabels.DeprecatedFunctionKey),
+			Value: ptr.String(fnlabels.FunctionValue),
+		},
+		{
+			Key:   ptr.String(fnlabels.DeprecatedFunctionRuntimeKey),
+			Value: ptr.String(f.Runtime),
+		},
+		// --- end of handling usage of deprecated runtime labels
+	}
+
+	labels := append(defaultLabels, f.Labels...)
+	if err := ValidateLabels(labels); len(err) != 0 {
+		return nil, errors.New(strings.Join(err, " "))
+	}
+
+	l := map[string]string{}
+	for _, label := range labels {
+		if label.Value == nil {
+			l[*label.Key] = ""
+		} else {
+			if strings.HasPrefix(*label.Value, "{{") {
+				// env variable format is validated above in ValidateLabels
+				match := regLocalEnv.FindStringSubmatch(*label.Value)
+				l[*label.Key] = os.Getenv(match[1])
+			} else {
+				l[*label.Key] = *label.Value
+			}
+		}
+	}
+
+	return l, nil
+}
+
 // assertEmptyRoot ensures that the directory is empty enough to be used for
 // initializing a new function.
 func assertEmptyRoot(path string) (err error) {
diff --git a/k8s/persistent_volumes.go b/k8s/persistent_volumes.go
index c0a3a61c..cf685305 100644
--- a/k8s/persistent_volumes.go
+++ b/k8s/persistent_volumes.go
@@ -17,7 +17,7 @@ func GetPersistentVolumeClaim(ctx context.Context, name, namespaceOverride strin
 	return client.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, name, metav1.GetOptions{})
 }
 
-func CreatePersistentVolumeClaim(ctx context.Context, name, namespaceOverride string, labels map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity) (err error) {
+func CreatePersistentVolumeClaim(ctx context.Context, name, namespaceOverride string, labels map[string]string, annotations map[string]string, accessMode corev1.PersistentVolumeAccessMode, resourceRequest resource.Quantity) (err error) {
 	client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride)
 	if err != nil {
 		return
@@ -25,9 +25,10 @@ func CreatePersistentVolumeClaim(ctx context.Context, name, namespaceOverride st
 
 	pvc := &corev1.PersistentVolumeClaim{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      name,
-			Namespace: namespace,
-			Labels:    labels,
+			Name:        name,
+			Namespace:   namespace,
+			Labels:      labels,
+			Annotations: annotations,
 		},
 		Spec: corev1.PersistentVolumeClaimSpec{
 			AccessModes: []corev1.PersistentVolumeAccessMode{accessMode},
diff --git a/k8s/secrets.go b/k8s/secrets.go
index 0fd8780b..7884234f 100644
--- a/k8s/secrets.go
+++ b/k8s/secrets.go
@@ -83,7 +83,7 @@ func DeleteSecrets(ctx context.Context, namespaceOverride string, listOptions me
 	return client.CoreV1().Secrets(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, listOptions)
 }
 
-func EnsureDockerRegistrySecretExist(ctx context.Context, name, namespaceOverride string, labels map[string]string, username, password, server string) (err error) {
+func EnsureDockerRegistrySecretExist(ctx context.Context, name, namespaceOverride string, labels map[string]string, annotations map[string]string, username, password, server string) (err error) {
 	client, namespace, err := NewClientAndResolvedNamespace(namespaceOverride)
 	if err != nil {
 		return
@@ -110,9 +110,10 @@ func EnsureDockerRegistrySecretExist(ctx context.Context, name, namespaceOverrid
 	if createSecret || !bytes.Equal(currentSecret.Data[secretKey], dockerConfigJSONContent) {
 		secret := &corev1.Secret{
 			ObjectMeta: metav1.ObjectMeta{
-				Name:      name,
-				Namespace: namespace,
-				Labels:    labels,
+				Name:        name,
+				Namespace:   namespace,
+				Labels:      labels,
+				Annotations: annotations,
 			},
 			Type: corev1.SecretTypeOpaque,
 			Data: map[string][]byte{},
diff --git a/knative/deployer.go b/knative/deployer.go
index 75996cc6..7bacf2d2 100644
--- a/knative/deployer.go
+++ b/knative/deployer.go
@@ -22,7 +22,6 @@ import (
 
 	fn "knative.dev/kn-plugin-func"
 	"knative.dev/kn-plugin-func/k8s"
-	"knative.dev/kn-plugin-func/k8s/labels"
 )
 
 const LIVENESS_ENDPOINT = "/health/liveness"
@@ -294,10 +293,13 @@ func generateNewService(f fn.Function, decorator DeployDecorator) (*v1.Service,
 	}
 	container.VolumeMounts = newVolumeMounts
 
-	labels, err := processLabels(f, decorator)
+	labels, err := f.LabelsMap()
 	if err != nil {
 		return nil, err
 	}
+	if decorator != nil {
+		labels = decorator.UpdateLabels(f, labels)
+	}
 
 	annotations := f.Annotations
 	if decorator != nil {
@@ -374,10 +376,14 @@ func updateService(f fn.Function, newEnv []corev1.EnvVar, newEnvFrom []corev1.En
 			return service, err
 		}
 
-		labels, err := processLabels(f, decorator)
+		labels, err := f.LabelsMap()
 		if err != nil {
-			return service, err
+			return nil, err
 		}
+		if decorator != nil {
+			labels = decorator.UpdateLabels(f, labels)
+		}
+
 		service.ObjectMeta.Labels = labels
 		service.Spec.Template.ObjectMeta.Labels = labels
 
@@ -395,54 +401,6 @@ func updateService(f fn.Function, newEnv []corev1.EnvVar, newEnvFrom []corev1.En
 	}
 }
 
-// processLabels generates a map of labels as key/value pairs from a function config
-// labels:
-//   - key: EXAMPLE1                            # Label directly from a value
-//     value: value1
-//   - key: EXAMPLE2                            # Label from the local ENV var
-//     value: {{ env:MY_ENV }}
-func processLabels(f fn.Function, decorator DeployDecorator) (map[string]string, error) {
-	labels := map[string]string{
-		labels.FunctionKey:        labels.FunctionValue,
-		labels.FunctionNameKey:    f.Name,
-		labels.FunctionRuntimeKey: f.Runtime,
-
-		// --- handle usage of deprecated labels (`boson.dev/function`, `boson.dev/runtime`)
-		labels.DeprecatedFunctionKey:        labels.FunctionValue,
-		labels.DeprecatedFunctionRuntimeKey: f.Runtime,
-		// --- end of handling usage of deprecated runtime labels
-	}
-
-	if decorator != nil {
-		labels = decorator.UpdateLabels(f, labels)
-	}
-
-	for _, label := range f.Labels {
-		if label.Key != nil && label.Value != nil {
-			if strings.HasPrefix(*label.Value, "{{") {
-				slices := strings.Split(strings.Trim(*label.Value, "{} "), ":")
-				if len(slices) == 2 {
-					// label from the local ENV var, eg. author={{ env:$USER }}
-					localValue, err := processLocalEnvValue(*label.Value)
-					if err != nil {
-						return nil, err
-					}
-					labels[*label.Key] = localValue
-					continue
-				}
-			} else {
-				// a standard label with key and value, eg. author=alice@example.com
-				labels[*label.Key] = *label.Value
-				continue
-			}
-		} else if label.Key != nil && label.Value == nil {
-			labels[*label.Key] = ""
-		}
-	}
-
-	return labels, nil
-}
-
 // processEnvs generates array of EnvVars and EnvFromSources from a function config
 // envs:
 //   - name: EXAMPLE1                            # ENV directly from a value
diff --git a/pipelines/tekton/pipeplines_provider.go b/pipelines/tekton/pipeplines_provider.go
index 03293ebd..ed12f6ec 100644
--- a/pipelines/tekton/pipeplines_provider.go
+++ b/pipelines/tekton/pipeplines_provider.go
@@ -19,7 +19,7 @@ import (
 	fn "knative.dev/kn-plugin-func"
 	"knative.dev/kn-plugin-func/docker"
 	"knative.dev/kn-plugin-func/k8s"
-	"knative.dev/kn-plugin-func/k8s/labels"
+	fnlabels "knative.dev/kn-plugin-func/k8s/labels"
 	"knative.dev/kn-plugin-func/knative"
 	"knative.dev/pkg/apis"
 )
@@ -97,12 +97,15 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) error {
 	pp.namespace = namespace
 
 	// let's specify labels that will be applied to every resouce that is created for a Pipeline
-	labels := map[string]string{labels.FunctionNameKey: f.Name}
+	labels, err := f.LabelsMap()
+	if err != nil {
+		return err
+	}
 	if pp.decorator != nil {
 		labels = pp.decorator.UpdateLabels(f, labels)
 	}
 
-	err = k8s.CreatePersistentVolumeClaim(ctx, getPipelinePvcName(f), pp.namespace, labels, corev1.ReadWriteOnce, *resource.NewQuantity(DefaultPersistentVolumeClaimSize, resource.DecimalSI))
+	err = k8s.CreatePersistentVolumeClaim(ctx, getPipelinePvcName(f), pp.namespace, labels, f.Annotations, corev1.ReadWriteOnce, *resource.NewQuantity(DefaultPersistentVolumeClaimSize, resource.DecimalSI))
 	if err != nil {
 		if !errors.IsAlreadyExists(err) {
 			return fmt.Errorf("problem creating persistent volume claim: %v", err)
@@ -135,7 +138,7 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) error {
 		registry = authn.DefaultAuthKey
 	}
 
-	err = k8s.EnsureDockerRegistrySecretExist(ctx, getPipelineSecretName(f), pp.namespace, labels, creds.Username, creds.Password, registry)
+	err = k8s.EnsureDockerRegistrySecretExist(ctx, getPipelineSecretName(f), pp.namespace, labels, f.Annotations, creds.Username, creds.Password, registry)
 	if err != nil {
 		return fmt.Errorf("problem in creating secret: %v", err)
 	}
@@ -182,7 +185,7 @@ func (pp *PipelinesProvider) Run(ctx context.Context, f fn.Function) error {
 
 func (pp *PipelinesProvider) Remove(ctx context.Context, f fn.Function) error {
 
-	l := k8slabels.SelectorFromSet(k8slabels.Set(map[string]string{labels.FunctionNameKey: f.Name}))
+	l := k8slabels.SelectorFromSet(k8slabels.Set(map[string]string{fnlabels.FunctionNameKey: f.Name}))
 	listOptions := metav1.ListOptions{
 		LabelSelector: l.String(),
 	}
diff --git a/pipelines/tekton/resources.go b/pipelines/tekton/resources.go
index ac6bb3b4..394e5423 100644
--- a/pipelines/tekton/resources.go
+++ b/pipelines/tekton/resources.go
@@ -98,8 +98,9 @@ func generatePipeline(f fn.Function, labels map[string]string) *pplnv1beta1.Pipe
 
 	return &pplnv1beta1.Pipeline{
 		ObjectMeta: v1.ObjectMeta{
-			Name:   pipelineName,
-			Labels: labels,
+			Name:        pipelineName,
+			Labels:      labels,
+			Annotations: f.Annotations,
 		},
 		Spec: pplnv1beta1.PipelineSpec{
 			Params:     params,
@@ -200,6 +201,7 @@ func generatePipelineRun(f fn.Function, labels map[string]string) *pplnv1beta1.P
 		ObjectMeta: v1.ObjectMeta{
 			GenerateName: fmt.Sprintf("%s-run-", getPipelineName(f)),
 			Labels:       labels,
+			Annotations:  f.Annotations,
 		},
 		Spec: pplnv1beta1.PipelineRunSpec{
 			PipelineRef: &pplnv1beta1.PipelineRef{
diff --git a/s2i/builder.go b/s2i/builder.go
index b5ad461b..1e7d6d8e 100644
--- a/s2i/builder.go
+++ b/s2i/builder.go
@@ -238,7 +238,7 @@ func (b *Builder) Build(ctx context.Context, f fn.Function) (err error) {
 			if err != nil {
 				return fmt.Errorf("cannot create tar header: %w", err)
 			}
-			hdr.Name = p
+			hdr.Name = filepath.ToSlash(p)
 
 			err = tw.WriteHeader(hdr)
 			if err != nil {


Test patch:
diff --git a/docker/docker_client_ssh_test.go b/docker/docker_client_ssh_test.go
index 7215dd91..0f214f36 100644
--- a/docker/docker_client_ssh_test.go
+++ b/docker/docker_client_ssh_test.go
@@ -39,12 +39,16 @@ func TestNewDockerClientWithSSH(t *testing.T) {
 
 	defer WithEnvVar(t, "DOCKER_HOST", fmt.Sprintf("ssh://user:pwd@%s", sshConf.address))()
 
-	dockerClient, _, err := docker.NewClient(client.DefaultDockerHost)
+	dockerClient, dockerHostInRemote, err := docker.NewClient(client.DefaultDockerHost)
 	if err != nil {
 		t.Fatal(err)
 	}
 	defer dockerClient.Close()
 
+	if dockerHostInRemote != `unix://`+sshDockerSocket {
+		t.Errorf("bad remote DOCKER_HOST: expected %q but got %q", `unix://`+sshDockerSocket, dockerHostInRemote)
+	}
+
 	_, err = dockerClient.Ping(ctx)
 	if err != nil {
 		t.Error(err)
diff --git a/function_unit_test.go b/function_unit_test.go
index b433ac1c..4729bfe3 100644
--- a/function_unit_test.go
+++ b/function_unit_test.go
@@ -4,7 +4,11 @@
 package function
 
 import (
+	"reflect"
 	"testing"
+
+	fnlabels "knative.dev/kn-plugin-func/k8s/labels"
+	. "knative.dev/kn-plugin-func/testing"
 )
 
 func TestFunction_ImageWithDigest(t *testing.T) {
@@ -82,3 +86,126 @@ func TestFunction_ImageName(t *testing.T) {
 		}
 	}
 }
+
+func Test_LabelsMap(t *testing.T) {
+	key1 := "key1"
+	key2 := "key2"
+	value1 := "value1"
+	value2 := "value2"
+
+	defer WithEnvVar(t, "BAD_EXAMPLE", ":invalid")()
+	valueLocalEnvIncorrect4 := "{{env:BAD_EXAMPLE}}"
+
+	defer WithEnvVar(t, "GOOD_EXAMPLE", "valid")()
+	valueLocalEnv4 := "{{env:GOOD_EXAMPLE}}"
+
+	tests := []struct {
+		name        string
+		labels      []Label
+		expectErr   bool
+		expectedMap map[string]string
+	}{
+		{
+			name: "invalid Labels should return err",
+			labels: []Label{
+				{
+					Value: &value1,
+				},
+			},
+			expectErr: true,
+		},
+		{
+			name: "with valid env var",
+			labels: []Label{
+				{
+					Key:   &key1,
+					Value: &valueLocalEnv4,
+				},
+			},
+			expectErr: false,
+			expectedMap: map[string]string{
+				key1: "valid",
+			},
+		},
+		{
+			name: "with invalid env var",
+			labels: []Label{
+				{
+					Key:   &key1,
+					Value: &valueLocalEnvIncorrect4,
+				},
+			},
+			expectErr: true,
+		},
+		{
+			name: "empty labels allowed. returns default labels",
+			labels: []Label{
+				{
+					Key: &key1,
+				},
+			},
+			expectErr: false,
+			expectedMap: map[string]string{
+				key1: "",
+			},
+		},
+		{
+			name: "full set of labels",
+			labels: []Label{
+				{
+					Key:   &key1,
+					Value: &value1,
+				},
+				{
+					Key:   &key2,
+					Value: &value2,
+				},
+			},
+			expectErr: false,
+			expectedMap: map[string]string{
+				key1: value1,
+				key2: value2,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			f := Function{
+				Name:    "some-function",
+				Runtime: "golang",
+				Labels:  tt.labels,
+			}
+			got, err := f.LabelsMap()
+
+			if tt.expectErr {
+				if err == nil {
+					t.Error("expected error but didn't get an error from LabelsMap")
+				}
+			} else {
+				if err != nil {
+					t.Errorf("got unexpected err: %s", err)
+				}
+			}
+			if err == nil {
+				defaultLabels := expectedDefaultLabels(f)
+				for k, v := range defaultLabels {
+					tt.expectedMap[k] = v
+				}
+				if res := reflect.DeepEqual(got, tt.expectedMap); !res {
+					t.Errorf("mismatch in actual and expected labels return. actual: %#v, expected: %#v", got, tt.expectedMap)
+				}
+			}
+		})
+	}
+}
+
+func expectedDefaultLabels(f Function) map[string]string {
+	return map[string]string{
+		fnlabels.FunctionKey:                  fnlabels.FunctionValue,
+		fnlabels.FunctionNameKey:              f.Name,
+		fnlabels.FunctionRuntimeKey:           f.Runtime,
+		fnlabels.DeprecatedFunctionKey:        fnlabels.FunctionValue,
+		fnlabels.DeprecatedFunctionRuntimeKey: f.Runtime,
+	}
+}