Repository: knative-sandbox/kn-plugin-func
Pull request #686 (2021-11-29 13:15:35)

refactor: config path accessors with instantiation cleanup
Cleans up instantiation logic and config/repo path accessors/creators, fixing a bug or two along the way.

- :broom:  Enables separate paths: when repositories path is outside config path
- :broom:  Removes need to use a client to trigger creation of paths by re-enabling static path accessors
- :broom: Cleans up instantiation logic
- :broom: Removes some unused accessors

/kind techdebt


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/686#" 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/686?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) Report
> Merging [#686](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/686?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (650d825) into [main](https://codecov.io/gh/knative-sandbox/kn-plugin-func/commit/ccf00152be0ceba1794267f8e03a09cb32fee514?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) (ccf0015) will **increase** coverage by `0.17%`.
> The diff coverage is `86.20%`.

[![Impacted file tree graph](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/686/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/686?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     #686      +/-   ##
==========================================
+ Coverage   38.03%   38.21%   +0.17%     
==========================================
  Files          42       42              
  Lines        3917     3920       +3     
==========================================
+ Hits         1490     1498       +8     
+ Misses       2225     2218       -7     
- Partials      202      204       +2     
```


| [Impacted Files](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/686?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox) | Coverage Δ | |
|---|---|---|
| [cmd/root.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/686/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y21kL3Jvb3QuZ28=) | `47.16% <ø> (-0.50%)` | :arrow_down: |
| [client.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/686/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y2xpZW50Lmdv) | `64.93% <80.95%> (+3.43%)` | :arrow_up: |
| [cmd/create.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/686/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y21kL2NyZWF0ZS5nbw==) | `46.44% <100.00%> (ø)` | |
| [cmd/repository.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/686/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-Y21kL3JlcG9zaXRvcnkuZ28=) | `53.17% <100.00%> (ø)` | |
| [repositories.go](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/686/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox#diff-cmVwb3NpdG9yaWVzLmdv) | `52.00% <100.00%> (-1.95%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/686?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/686?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=knative-sandbox). Last update [ccf0015...650d825](https://codecov.io/gh/knative-sandbox/kn-plugin-func/pull/686?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).

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

<details>

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

Patch:
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index d5291f51..decb5e81 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -9,8 +9,7 @@ aliases:
   - julz
   - maximilien
   autoscaling-wg-leads:
-  - markusthoemmes
-  - vagababov
+  - julz
   client-wg-leads:
   - dsimansk
   - navidshaikh
@@ -21,7 +20,6 @@ aliases:
   - rhuss
   container-freezer-approvers:
   - julz
-  - markusthoemmes
   - psschwei
   control-protocol-approvers:
   - devguyio
@@ -236,9 +234,8 @@ aliases:
   serving-writers:
   - ZhiminXiang
   - dprotaso
-  - markusthoemmes
+  - julz
   - nak3
-  - vagababov
   source-wg-leads:
   - lionelvillard
   steering-committee:
diff --git a/client.go b/client.go
index a02754f6..10188eec 100644
--- a/client.go
+++ b/client.go
@@ -8,6 +8,7 @@ import (
 	"os"
 	"path/filepath"
 	"runtime"
+	"runtime/debug"
 	"time"
 
 	"github.com/mitchellh/go-homedir"
@@ -26,12 +27,17 @@ const (
 	// DefaultVersion is the initial value for string members whose implicit type
 	// is a semver.
 	DefaultVersion = "0.0.0"
+
+	// DefaultConfigPath is used in the unlikely event that
+	// the user has no home directory (no ~), there is no
+	// XDG_CONFIG_HOME set, and no WithConfigPath was used.
+	DefaultConfigPath = ".config/func"
 )
 
 // Client for managing Function instances.
 type Client struct {
-	repositories     *Repositories    // Repositories management
-	templates        *Templates       // Templates management
+	repositoriesPath string           // path to repositories
+	repositoriesURI  string           // repo URI (overrides repositories path)
 	verbose          bool             // print verbose logs
 	builder          Builder          // Builds a runnable image source
 	pusher           Pusher           // Pushes Funcation image to a remote
@@ -44,6 +50,8 @@ type Client struct {
 	registry         string           // default registry for OCI image tags
 	progressListener ProgressListener // progress listener
 	emitter          Emitter          // Emits CloudEvents to functions
+	repositories     *Repositories    // Repositories management
+	templates        *Templates       // Templates management
 }
 
 // ErrNotBuilt indicates the Function has not yet been built.
@@ -161,14 +169,8 @@ type Emitter interface {
 
 // New client for Function management.
 func New(options ...Option) *Client {
-	// Assert the global config directory exists (including child directories
-	// such as repositories)
-	assertConfigDir()
-
 	// Instantiate client with static defaults.
 	c := &Client{
-		repositories:     &Repositories{},
-		templates:        &Templates{},
 		builder:          &noopBuilder{output: os.Stdout},
 		pusher:           &noopPusher{output: os.Stdout},
 		deployer:         &noopDeployer{output: os.Stdout},
@@ -178,54 +180,61 @@ func New(options ...Option) *Client {
 		dnsProvider:      &noopDNSProvider{output: os.Stdout},
 		progressListener: &NoopProgressListener{},
 		emitter:          &noopEmitter{},
+		repositoriesPath: filepath.Join(ConfigPath(), "repositories"),
 	}
-	c.repositories = newRepositories(c)
-	c.templates = newTemplates(c)
 	for _, o := range options {
 		o(c)
 	}
+	// Initialize sub-managers using now-fully-initialized client.
+	c.repositories = newRepositories(c)
+	c.templates = newTemplates(c)
+
+	// Trigger the creation of the config and repository paths
+	_ = ConfigPath()         // Config is package-global scoped
+	_ = c.RepositoriesPath() // Repositories is Client-specific
+
 	return c
 }
 
-// assertConfigDir makes a best-effort attempt to create the config directory
-// (including required subdirectories).
-func assertConfigDir() {
-	// NOTE: regarding running as a user with no home directory:
-	// The default is .config/func in current working directory when there is no
-	// available HOME in which to find .`~/.config/func`.
-	// Since it is expected that the code elsewhere never assume the config
-	// directory exists (doing so is a racing condition), it is valid to simply
-	// handle errors at this level.
-	if err := os.MkdirAll(RepositoriesPath(), 0700); err != nil {
-		fmt.Fprintf(os.Stderr, "Error creating '%v': %v", RepositoriesPath(), err)
-	}
-}
+// The default config path is evaluated in the following order, from lowest
+// to highest precedence.
+// 1.  The static default is DefaultConfigPath (./.config/func)
+// 2.  ~/.config/func if it exists (can be expanded: user has a home dir)
+// 3.  The value of $XDG_CONFIG_PATH/func if the environment variable exists.
+// The path will be created if it does not already exist.
+func ConfigPath() (path string) {
+	path = DefaultConfigPath
 
-// RepositoriesPath is the static default path to repositories used by a Client.
-// This path can be overridden on intantiation of a client using the
-// WithRepositories option.
-func RepositoriesPath() string {
-	return filepath.Join(ConfigPath(), "repositories")
-}
+	// ~/.config/func is the default if ~ can be expanded
+	if home, err := homedir.Expand("~"); err == nil {
+		path = filepath.Join(home, ".config", "func")
+	}
 
-// ConfigPath returns the default config directory path used by the Client.
-// The default is ~/.config/func.  If defined, XDG_CONFIG_HOME is used.
-// In the event of an error calculating ~ (no home directory for the current
-// user), the relative path '.config/func' is used.
-func ConfigPath() string {
 	// 'XDG_CONFIG_HOME/func' takes precidence if defined
 	if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
-		return filepath.Join(xdg, "func")
+		path = filepath.Join(xdg, "func")
 	}
 
-	// ~/.config/func is the default if ~ can be expanded
-	if home, err := homedir.Expand("~"); err == nil {
-		return filepath.Join(home, ".config", "func")
-	}
+	mkdir(path) // make sure it exists
+	return
+}
+
+// RepositoriesPath accesses the currently effective repositories path,
+// which defaults to [ConfigPath]/repositories but can be set explicitly using
+// the WithRepositories option when creating the client..
+// The path will be created if it does not already exist.
+func (c *Client) RepositoriesPath() (path string) {
+	path = c.repositories.Path()
+	mkdir(path) // make sure it exists
+	return
+}
 
-	// The default (edge case) is to return a relative path of .config inidicating
-	// the current working directory when neither aforementioned exist.
-	return ".config/func"
+// RepositoriesPath is a convenience method for accessing the default path to
+// repositories that will be used by new instances of a Client unless options
+// such as WithRepositories are used to override.
+// The path will be created if it does not already exist.
+func RepositoriesPath() string {
+	return New().RepositoriesPath()
 }
 
 // OPTIONS
@@ -308,21 +317,21 @@ func WithDNSProvider(provider DNSProvider) Option {
 	}
 }
 
-// WithRepositories sets the location to use for extensible template repositories.
-// Extensible template repositories are additional templates that exist on disk and are
-// not built into the binary.
+// WithRepositories sets the location to use for extensible template
+// repositories.  Extensible template repositories are additional templates
+// that exist on disk and are not built into the binary.
 func WithRepositories(path string) Option {
 	return func(c *Client) {
-		c.Repositories().SetPath(path)
+		c.repositoriesPath = path
 	}
 }
 
-// WithRepository sets a specific URL to a Git repository from which to pull templates.
-// This setting's existence precldes the use of either the inbuilt templates or any
-// repositories from the extensible repositories path.
+// WithRepository sets a specific URL to a Git repository from which to pull
+// templates.  This setting's existence precldes the use of either the inbuilt
+// templates or any repositories from the extensible repositories path.
 func WithRepository(uri string) Option {
 	return func(c *Client) {
-		c.Repositories().SetRemote(uri)
+		c.repositoriesURI = uri
 	}
 }
 
@@ -593,15 +602,7 @@ func (c *Client) Deploy(ctx context.Context, path string) (err error) {
 		return ErrNotBuilt
 	}
 
-	// Push the image for the named service to the configured registry
-	imageDigest, err := c.pusher.Push(ctx, f)
-	if err != nil {
-		return
-	}
-
-	// Record the Image Digest pushed.
-	f.ImageDigest = imageDigest
-	if err = f.Write(); err != nil {
+	if err = c.Push(ctx, &f); err != nil {
 		return
 	}
 
@@ -715,6 +716,18 @@ func (c *Client) Emit(ctx context.Context, endpoint string) error {
 	return c.emitter.Emit(ctx, endpoint)
 }
 
+// Push the image for the named service to the configured registry
+func (c *Client) Push(ctx context.Context, f *Function) (err error) {
+	imageDigest, err := c.pusher.Push(ctx, *f)
+	if err != nil {
+		return
+	}
+
+	// Record the Image Digest pushed.
+	f.ImageDigest = imageDigest
+	return f.Write()
+}
+
 // DEFAULTS
 // ---------
 
@@ -780,3 +793,14 @@ func (p *NoopProgressListener) Increment(m string) {}
 func (p *NoopProgressListener) Complete(m string)  {}
 func (p *NoopProgressListener) Stopping()          {}
 func (p *NoopProgressListener) Done()              {}
+
+// mkdir attempts to mkdir, writing any errors to stderr.
+func mkdir(path string) {
+	// Since it is expected that the code elsewhere never assume directories
+	// exist (doing so is a racing condition), it is valid to simply
+	// handle errors at this level.
+	if err := os.MkdirAll(path, 0700); err != nil {
+		fmt.Fprintf(os.Stderr, "Error creating '%v': %v", path, err)
+		debug.PrintStack()
+	}
+}
diff --git a/cmd/build.go b/cmd/build.go
index a60231d7..c20d206d 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -11,6 +11,7 @@ import (
 
 	fn "knative.dev/kn-plugin-func"
 	"knative.dev/kn-plugin-func/buildpacks"
+	"knative.dev/kn-plugin-func/docker"
 	"knative.dev/kn-plugin-func/progress"
 )
 
@@ -24,11 +25,31 @@ func newBuildClient(cfg buildConfig) (*fn.Client, error) {
 	builder := buildpacks.NewBuilder()
 	listener := progress.New()
 
+	pusherOption := fn.WithPusher(nil)
+	if cfg.Push {
+		credentialsProvider := docker.NewCredentialsProvider(
+			newCredentialsCallback(),
+			docker.CheckAuth,
+			newChooseHelperCallback(),
+		)
+		pusher, err := docker.NewPusher(
+			docker.WithCredentialsProvider(credentialsProvider),
+			docker.WithProgressListener(listener),
+		)
+
+		if err != nil {
+			return nil, err
+		}
+		pusher.Verbose = cfg.Verbose
+		pusherOption = fn.WithPusher(pusher)
+	}
 	builder.Verbose = cfg.Verbose
 	listener.Verbose = cfg.Verbose
 
 	return fn.New(
 		fn.WithBuilder(builder),
+		// fn.WithPusher(pusher),
+		pusherOption,
 		fn.WithProgressListener(listener),
 		fn.WithRegistry(cfg.Registry), // for image name when --image not provided
 		fn.WithVerbose(cfg.Verbose)), nil
@@ -64,7 +85,7 @@ kn func build
 kn func build --builder cnbs/sample-builder:bionic
 `,
 		SuggestFor: []string{"biuld", "buidl", "built"},
-		PreRunE:    bindEnv("image", "path", "builder", "registry", "confirm"),
+		PreRunE:    bindEnv("image", "path", "builder", "registry", "confirm", "push"),
 	}
 
 	cmd.Flags().StringP("builder", "b", "", "Buildpack builder, either an as a an image name or a mapping name.\nSpecified value is stored in func.yaml for subsequent builds.")
@@ -72,6 +93,7 @@ kn func build --builder cnbs/sample-builder:bionic
 	cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE)")
 	cmd.Flags().StringP("path", "p", cwd(), "Path to the project directory (Env: $FUNC_PATH)")
 	cmd.Flags().StringP("registry", "r", "", "Registry + namespace part of the image to build, ex 'quay.io/myuser'.  The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
+	cmd.Flags().BoolP("push", "u", false, "Attempt to push the function image after being successfully built")
 
 	if err := cmd.RegisterFlagCompletionFunc("builder", CompleteBuilderList); err != nil {
 		fmt.Println("internal: error while calling RegisterFlagCompletionFunc: ", err)
@@ -170,7 +192,11 @@ func runBuild(cmd *cobra.Command, _ []string, clientFn buildClientFn) (err error
 		return err
 	}
 
-	return client.Build(cmd.Context(), config.Path)
+	err = client.Build(cmd.Context(), config.Path)
+	if err == nil && config.Push {
+		err = client.Push(cmd.Context(), &function)
+	}
+	return
 }
 
 type buildConfig struct {
@@ -206,6 +232,7 @@ func newBuildConfig() buildConfig {
 		Verbose:  viper.GetBool("verbose"), // defined on root
 		Confirm:  viper.GetBool("confirm"),
 		Builder:  viper.GetString("builder"),
+		Push:     viper.GetBool("push"),
 	}
 }
 
diff --git a/cmd/create.go b/cmd/create.go
index 5c80f8d9..2920fd9e 100644
--- a/cmd/create.go
+++ b/cmd/create.go
@@ -246,7 +246,7 @@ func newCreateConfig(args []string, clientFn createClientFn) (cfg createConfig,
 	// it is still available as an environment variable.
 	repositories = os.Getenv("FUNC_REPOSITORIES")
 	if repositories == "" { // if no env var provided
-		repositories = fn.RepositoriesPath() // use ~/.config/func/repositories
+		repositories = fn.New().RepositoriesPath() // use ~/.config/func/repositories
 	}
 
 	// Config is the final default values based off the execution context.
diff --git a/cmd/root.go b/cmd/root.go
index 8257d971..74ddec34 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -156,7 +156,7 @@ func cwd() (cwd string) {
 // bindFunc which conforms to the cobra PreRunE method signature
 type bindFunc func(*cobra.Command, []string) error
 
-// bindEnv returns a bindFunc that binds env vars to the namd flags.
+// bindEnv returns a bindFunc that binds env vars to the named flags.
 func bindEnv(flags ...string) bindFunc {
 	return func(cmd *cobra.Command, args []string) (err error) {
 		for _, flag := range flags {
diff --git a/docker/pusher.go b/docker/pusher.go
index ace57860..73e0fff3 100644
--- a/docker/pusher.go
+++ b/docker/pusher.go
@@ -90,10 +90,6 @@ func NewCredentialsProvider(
 		}
 	}
 
-	// Creating an instance of the fn.Client ensures that the config path
-	// exists:
-	_ = fn.New()
-
 	authFilePath := filepath.Join(fn.ConfigPath(), "auth.json")
 	sys := &containersTypes.SystemContext{
 		AuthFilePath: authFilePath,
diff --git a/docs/guides/podman.md b/docs/guides/podman.md
index 825580be..d197dcd6 100644
--- a/docs/guides/podman.md
+++ b/docs/guides/podman.md
@@ -14,3 +14,35 @@ Then set the environment variable `DOCKER_HOST` to the socket so `func` knows wh
 ❯ func build -v
 ```
 Now you may use `func` as usual.
+
+## macOS
+
+Under macOS you need to use `podman machine` as `podman` is native to Linux.
+
+```
+❯ brew install podman
+❯ podman machine init --cpus=2 --disk-size=30 --memory=8192
+❯ podman machine start
+❯ ssh-add -k $HOME/.ssh/podman-machine-default
+❯ export DOCKER_HOST=(podman system connection ls --format="{{.URI}}" | grep root)
+```
+
+## Known issues
+
+* In func version 0.19, some runtimes might be unable to build a function by using podman. You might see an error message similar to the following:
+
+    ```
+    ERROR: failed to image: error during connect: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.40/info": EOF
+    ```
+  * The following workaround exists for this issue:
+    * Update the podman service by adding --time=0 to the service ExecStart definition in the podman configuration file at `/etc/systemd/user/podman.service`.You might need to copy the default configuration file from `/usr/lib/systemd/user/podman.service` first.
+      </br></br>
+      Example service configuration:
+
+      `ExecStart=/usr/bin/podman $LOGGING system service --time=0`
+      </br></br>
+      After editing the configuration files you need to reload/restart service:
+      ```
+      ❯ systemctl --user daemon-reload
+      ❯ systemctl restart --user podman.socket
+      ```
diff --git a/repositories.go b/repositories.go
index 4f9dee18..a797fb12 100644
--- a/repositories.go
+++ b/repositories.go
@@ -31,9 +31,16 @@ type Repositories struct {
 	path string
 
 	// Optional uri of a single repo to use in leau of embedded and extensible.
+	// Enables single-repository mode.  This replaces the default embedded repo
+	// and extended repositories.  This is an important mode for both diskless
+	// (config-less) operation, such as security-restrited environments, and for
+	// running as a library in which case environmental settings should be
+	// ignored in favor of a more functional approach in which only inputs affect
+	// outputs.
 	remote string
 
-	// backreference to the client enabling full api access for the repo manager
+	// backreference to the client enabling this repositorires manager to
+	// have full API access.
 	client *Client
 }
 
@@ -42,32 +49,17 @@ type Repositories struct {
 // full client API during implementations.
 func newRepositories(client *Client) *Repositories {
 	return &Repositories{
-		path:   DefaultRepositoriesPath,
 		client: client,
+		path:   client.repositoriesPath,
+		remote: client.repositoriesURI,
 	}
 }
 
-// SetPath to repositories under management.
-func (r *Repositories) SetPath(path string) {
-	r.path = path
-}
-
 // Path returns the currently active repositories path under management.
 func (r *Repositories) Path() string {
 	return r.path
 }
 
-// SetRemote enables single-repository mode.
-// Enables single-repository mode.  This replaces the default embedded repo
-// and extended repositories.  This is an important mode for both diskless
-// (config-less) operation, such as security-restrited environments, and for
-// running as a library in which case environmental settings should be
-// ignored in favor of a more functional approach in which only inputs affect
-// outputs.
-func (r *Repositories) SetRemote(uri string) {
-	r.remote = uri
-}
-
 // List all repositories the current configuration of the repo manager has
 // defined.
 func (r *Repositories) List() ([]string, error) {


Test patch:
diff --git a/client_int_test.go b/client_int_test.go
index c6369db0..2497c329 100644
--- a/client_int_test.go
+++ b/client_int_test.go
@@ -161,7 +161,6 @@ func TestRemoteRepositories(t *testing.T) {
 	client := fn.New(
 		fn.WithRegistry(DefaultRegistry),
 		fn.WithRepository("https://github.com/boson-project/test-templates"),
-		fn.WithRepositories("testdata/repositories"),
 	)
 	err := client.Create(fn.Function{
 		Root:     ".",
diff --git a/cmd/build_test.go b/cmd/build_test.go
index 199a46a2..56b733dc 100644
--- a/cmd/build_test.go
+++ b/cmd/build_test.go
@@ -1,9 +1,13 @@
 package cmd
 
 import (
+	"context"
+	"fmt"
 	"io/ioutil"
+	"os"
 	"testing"
 
+	"github.com/ory/viper"
 	fn "knative.dev/kn-plugin-func"
 	"knative.dev/kn-plugin-func/mock"
 )
@@ -49,3 +53,125 @@ created: 2021-01-01T00:00:00+00:00
 		t.Fatal("Expected error")
 	}
 }
+
+func Test_runBuild(t *testing.T) {
+	tests := []struct {
+		name         string
+		pushFlag     bool
+		fileContents string
+		shouldBuild  bool
+		shouldPush   bool
+		wantErr      bool
+	}{
+		{
+			name:     "push flag triggers push after build",
+			pushFlag: true,
+			fileContents: `name: test-func
+runtime: go
+created: 2009-11-10 23:00:00`,
+			shouldBuild: true,
+			shouldPush:  true,
+		},
+		{
+			name:     "push flag with failing push",
+			pushFlag: true,
+			fileContents: `name: test-func
+runtime: go
+created: 2009-11-10 23:00:00`,
+			shouldBuild: true,
+			shouldPush:  true,
+			wantErr:     true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockPusher := mock.NewPusher()
+			failPusher := &mock.Pusher{
+				PushFn: func(f fn.Function) (string, error) {
+					return "", fmt.Errorf("push failed")
+				},
+			}
+			mockBuilder := mock.NewBuilder()
+			cmd := NewBuildCmd(func(bc buildConfig) (*fn.Client, error) {
+				pusher := mockPusher
+				if tt.wantErr {
+					pusher = failPusher
+				}
+				return fn.New(
+					fn.WithBuilder(mockBuilder),
+					fn.WithPusher(pusher),
+				), nil
+			})
+
+			tempDir, err := os.MkdirTemp("", "func-tests")
+			if err != nil {
+				t.Fatalf("temp dir couldn't be created %v", err)
+			}
+			t.Log("tempDir created:", tempDir)
+			t.Cleanup(func() {
+				os.RemoveAll(tempDir)
+			})
+
+			fullPath := tempDir + "/func.yaml"
+			tempFile, err := os.Create(fullPath)
+			if err != nil {
+				t.Fatalf("temp file couldn't be created %v", err)
+			}
+			_, err = tempFile.WriteString(tt.fileContents)
+			if err != nil {
+				t.Fatalf("file content was not written %v", err)
+			}
+
+			cmd.SetArgs([]string{"--path=" + tempDir})
+			viper.SetDefault("push", tt.pushFlag)
+			viper.SetDefault("registry", "docker.io/tigerteam")
+
+			err = cmd.Execute()
+			if tt.wantErr != (err != nil) {
+				t.Errorf("Wanted error %v but actually got %v", tt.wantErr, err)
+			}
+
+			if mockBuilder.BuildInvoked != tt.shouldBuild {
+				t.Errorf("Build execution expected: %v but was actually %v", tt.shouldBuild, mockBuilder.BuildInvoked)
+			}
+
+			if tt.shouldPush != (mockPusher.PushInvoked || failPusher.PushInvoked) {
+				t.Errorf("Push execution expected: %v but was actually mockPusher invoked: %v failPusher invoked %v", tt.shouldPush, mockPusher.PushInvoked, failPusher.PushInvoked)
+			}
+		})
+	}
+}
+
+func Test_newBuildClient(t *testing.T) {
+	tests := []struct {
+		name    string
+		cfg     buildConfig
+		succeed bool
+	}{
+		{
+			name: "push flag set to false avoids pusher instanciation",
+			cfg: buildConfig{
+				Push: false,
+			},
+			succeed: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			client, err := newBuildClient(tt.cfg)
+			if err != nil {
+				t.Error(err)
+			}
+			defer func() {
+				if r := recover(); r != nil && tt.succeed {
+					t.Errorf("expected function call to succeed %v, got actually %v", tt.succeed, r)
+				}
+			}()
+			err = client.Push(context.TODO(), &fn.Function{})
+			if err != nil {
+				t.Error(err)
+			}
+		})
+	}
+}