Repository: GoogleContainerTools/skaffold
Pull request #7444 (2022-05-25 22:56:07)

feat: add helm renderer and update helm render integration tests
Fixes: https://github.com/GoogleContainerTools/skaffold/issues/7285

**Description**
This PR adds the helm renderer, which uses `helm template` to render manifests. It also updates and reenables the `TestHelmRender` integration test.

There is also a huge amount of refactoring in this PR to move a lot of the code shared between the helm renderer (`pkg/skaffold/render/renderer/helm`) and helm legacy deployer (`pkg/skaffold/deploy/helm`) into a common package `pkg/skaffold/helm`


codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) Report
> Merging [#7444](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (ef9f20c) into [main](https://codecov.io/gh/GoogleContainerTools/skaffold/commit/290280e8c6c19231e9ed5b5c027f2c01d22f75be?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (290280e) will **decrease** coverage by `4.20%`.
> The diff coverage is `55.29%`.

```diff
@@            Coverage Diff             @@
##             main    #7444      +/-   ##
==========================================
- Coverage   70.48%   66.27%   -4.21%     
==========================================
  Files         515      576      +61     
  Lines       23150    27690    +4540     
==========================================
+ Hits        16317    18352    +2035     
- Misses       5776     7997    +2221     
- Partials     1057     1341     +284     
```


| [Impacted Files](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | Coverage Δ | |
|---|---|---|
| [cmd/skaffold/app/cmd/credits/export.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY3JlZGl0cy9leHBvcnQuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/deploy.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZGVwbG95Lmdv) | `50.00% <0.00%> (-3.85%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/test.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvdGVzdC5nbw==) | `44.44% <0.00%> (ø)` | |
| [cmd/skaffold/skaffold.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL3NrYWZmb2xkLmdv) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/render.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvcmVuZGVyLmdv) | `35.48% <11.11%> (-5.90%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/inspect\_tests.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvaW5zcGVjdF90ZXN0cy5nbw==) | `62.50% <14.28%> (-1.14%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/lsp.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvbHNwLmdv) | `28.12% <28.12%> (ø)` | |
| [cmd/skaffold/app/cmd/fix.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZml4Lmdv) | `55.93% <38.88%> (-20.54%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/verify.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvdmVyaWZ5Lmdv) | `41.17% <41.17%> (ø)` | |
| [cmd/skaffold/app/cmd/lint.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvbGludC5nbw==) | `42.85% <42.85%> (ø)` | |
| ... and [306 more](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7444/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | |

:mega: Codecov can now indicate which changes are the most critical in Pull Requests. [Learn more](https://about.codecov.io/product/feature/runtime-insights/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools)

aaron-prindle: Seems `TestHelmRender` is failing atm:
https://github.com/GoogleContainerTools/skaffold/runs/6614566005?check_suite_focus=true
MarlonGamez: @aaron-prindle yeah the data there is all correct I believe but the test expects it to be ordered differently. Assuming something changed with helm that's causing this. I'm gonna update the expected data to match, wdyt? Unless there's a flag I'm missing that will allow it to be ordered as we expect
aaron-prindle: Not sure how/if this is related to this PR but the failing test seems like a legit failure IIUC:
```
--- FAIL: TestDockerCLICheckCacheFromArgs/multiple_cache-from_images (0.01s)
        cmd_helper.go:208: 
            wanted: docker build . --file C:\Users\RUNNER~1\AppData\Local\Temp\skaffold4[15](https://github.com/GoogleContainerTools/skaffold/runs/6820459087?check_suite_focus=true#step:6:16)5648276\Dockerfile -t tag --cache-from from/image1 --cache-from from/image2
               got: docker build . --file C:\Users\RUNNER~1\AppData\Local\Temp\skaffold4155648276\C:\Users\RUNNER~1\AppData\Local\Temp\skaffold4155648276\Dockerfile -t tag --cache-from from/image1 --cache-from from/image2
    --- FAIL: TestDockerCLICheckCacheFromArgs/cache-from_self_uses_tagged_image (0.02s)
        cmd_helper.go:208: 
            wanted: docker build . --file C:\Users\RUNNER~1\AppData\Local\Temp\skaffold365434937\Dockerfile -t gcr.io/k8s-skaffold/test:tagged --cache-from gcr.io/k8s-skaffold/test:tagged
               got: docker build . --file C:\Users\RUNNER~1\AppData\Local\Temp\skaffold365434937\C:\Users\RUNNER~1\AppData\Local\Temp\skaffold365434937\Dockerfile -t gcr.io/k8s-skaffold/test:tagged --cache-from gcr.io/k8s-skaffold/test:tagged
```
Looks like a path is getting double appended

Patch:
diff --git a/pkg/skaffold/deploy/helm/args.go b/pkg/skaffold/deploy/helm/args.go
index 545386f44..d02c5b85a 100644
--- a/pkg/skaffold/deploy/helm/args.go
+++ b/pkg/skaffold/deploy/helm/args.go
@@ -17,19 +17,12 @@ limitations under the License.
 package helm
 
 import (
-	"context"
-	"fmt"
-	"runtime"
-	"strconv"
-
 	"github.com/blang/semver"
-	"github.com/mitchellh/go-homedir"
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/helm"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
 )
 
 // installOpts are options to be passed to "helm install"
@@ -46,73 +39,6 @@ type installOpts struct {
 	version      string
 }
 
-// constructOverrideArgs creates the command line arguments for overrides
-func constructOverrideArgs(r *latest.HelmRelease, builds []graph.Artifact, args []string) ([]string, error) {
-	for _, k := range sortKeys(r.SetValues) {
-		args = append(args, "--set", fmt.Sprintf("%s=%s", k, r.SetValues[k]))
-	}
-
-	for _, k := range sortKeys(r.SetFiles) {
-		exp, err := homedir.Expand(r.SetFiles[k])
-		if err != nil {
-			return nil, fmt.Errorf("unable to expand %q: %w", r.SetFiles[k], err)
-		}
-		exp = sanitizeFilePath(exp, runtime.GOOS == "windows")
-
-		args = append(args, "--set-file", fmt.Sprintf("%s=%s", k, exp))
-	}
-
-	envMap := map[string]string{}
-	for idx, b := range builds {
-		suffix := ""
-		if idx > 0 {
-			suffix = strconv.Itoa(idx + 1)
-		}
-
-		for k, v := range envVarForImage(b.ImageName, b.Tag) {
-			envMap[k+suffix] = v
-		}
-	}
-	log.Entry(context.TODO()).Debugf("EnvVarMap: %+v\n", envMap)
-
-	for _, k := range sortKeys(r.SetValueTemplates) {
-		v, err := util.ExpandEnvTemplate(r.SetValueTemplates[k], envMap)
-		if err != nil {
-			return nil, err
-		}
-		expandedKey, err := util.ExpandEnvTemplate(k, envMap)
-		if err != nil {
-			return nil, err
-		}
-
-		args = append(args, "--set", fmt.Sprintf("%s=%s", expandedKey, v))
-	}
-
-	for _, v := range r.ValuesFiles {
-		exp, err := homedir.Expand(v)
-		if err != nil {
-			return nil, fmt.Errorf("unable to expand %q: %w", v, err)
-		}
-
-		exp, err = util.ExpandEnvTemplate(exp, envMap)
-		if err != nil {
-			return nil, err
-		}
-
-		args = append(args, "-f", exp)
-	}
-	return args, nil
-}
-
-// getArgs calculates the correct arguments to "helm get"
-func getArgs(releaseName string, namespace string) []string {
-	args := []string{"get", "all"}
-	if namespace != "" {
-		args = append(args, "--namespace", namespace)
-	}
-	return append(args, releaseName)
-}
-
 // installArgs calculates the correct arguments to "helm install"
 func (h *Deployer) installArgs(r latest.HelmRelease, builds []graph.Artifact, o installOpts) ([]string, error) {
 	var args []string
@@ -162,12 +88,12 @@ func (h *Deployer) installArgs(r latest.HelmRelease, builds []graph.Artifact, o
 
 	if r.CreateNamespace != nil && *r.CreateNamespace && !o.upgrade {
 		if o.helmVersion.LT(helm32Version) {
-			return nil, createNamespaceErr(h.bV.String())
+			return nil, helm.CreateNamespaceErr(h.bV.String())
 		}
 		args = append(args, "--create-namespace")
 	}
 
-	args, err := constructOverrideArgs(&r, builds, args)
+	args, err := helm.ConstructOverrideArgs(&r, builds, args)
 	if err != nil {
 		return nil, err
 	}
@@ -182,53 +108,3 @@ func (h *Deployer) installArgs(r latest.HelmRelease, builds []graph.Artifact, o
 
 	return args, nil
 }
-
-// sanitizeFilePath is used to sanitize filepaths that are provided to the `setFiles` flag
-// helm `setFiles` doesn't work with the unescaped filepath separator (\) for Windows or if there are unescaped tabs and spaces in the directory names.
-// So we escape all odd count occurrences of `\` for Windows, and wrap the entire string in quotes if it has spaces.
-// This is very specific to the way helm handles its flags.
-// See https://github.com/helm/helm/blob/d55c53df4e394fb62b0514a09c57bce235dd7877/pkg/cli/values/options.
-// Otherwise the windows `syscall` package implements its own sanitizing for command args that's used by `exec.Cmd`.
-// See https://github.com/golang/go/blob/6951da56b0ae2cd4250fc1b0350d090aed633ac1/src/syscall/exec_windows.go#L27
-func sanitizeFilePath(s string, isWindowsOS bool) string {
-	if len(s) == 0 {
-		return `""`
-	}
-	needsQuotes := false
-	for i := 0; i < len(s); i++ {
-		if s[i] == ' ' || s[i] == '\t' {
-			needsQuotes = true
-			break
-		}
-	}
-
-	if !isWindowsOS {
-		if needsQuotes {
-			return fmt.Sprintf(`"%s"`, s)
-		}
-		return s
-	}
-
-	var b []byte
-	slashes := 0
-	for i := 0; i < len(s); i++ {
-		switch s[i] {
-		case '\\':
-			slashes++
-		default:
-			// ensure a single slash is escaped
-			if slashes == 1 {
-				b = append(b, '\\')
-			}
-			slashes = 0
-		}
-		b = append(b, s[i])
-	}
-	if slashes == 1 {
-		b = append(b, '\\')
-	}
-	if needsQuotes {
-		return fmt.Sprintf(`"%s"`, string(b))
-	}
-	return string(b)
-}
diff --git a/pkg/skaffold/deploy/helm/helm.go b/pkg/skaffold/deploy/helm/helm.go
index 2294ebea7..2361c9788 100644
--- a/pkg/skaffold/deploy/helm/helm.go
+++ b/pkg/skaffold/deploy/helm/helm.go
@@ -25,7 +25,6 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"regexp"
 	"sort"
 	"strings"
 	"time"
@@ -45,6 +44,7 @@ import (
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/types"
 	deployutil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/util"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/helm"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/hooks"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
 	pkgkubectl "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubectl"
@@ -68,25 +68,13 @@ import (
 )
 
 var (
-	// versionRegex extracts version from "helm version --client", for instance: "2.14.0-rc.2"
-	versionRegex = regexp.MustCompile(`v?(\d[\w.\-]+)`)
-
 	// helm32Version represents the version cut-off for helm3 behavior
 	helm32Version = semver.MustParse("3.2.0")
 
 	// helm31Version represents the version cut-off for helm3.1 post-renderer behavior
 	helm31Version = semver.MustParse("3.1.0")
-
-	// error to throw when helm version can't be determined
-	versionErrorString = "failed to determine binary version: %w"
-
-	// osExecutable allows for replacing the skaffold binary for testing purposes
-	osExecutable = os.Executable
 )
 
-// for testing
-var writeBuildArtifactsFunc = writeBuildArtifacts
-
 // Deployer deploys workflows using the helm CLI
 type Deployer struct {
 	*latest.LegacyHelmDeploy
@@ -125,6 +113,13 @@ type Deployer struct {
 	transformableDenylist  map[apimachinery.GroupKind]latest.ResourceFilter
 }
 
+func (h Deployer) EnableDebug() bool         { return h.enableDebug }
+func (h Deployer) ConfigFile() string        { return h.configFile }
+func (h Deployer) KubeContext() string       { return h.kubeContext }
+func (h Deployer) KubeConfig() string        { return h.kubeConfig }
+func (h Deployer) Labels() map[string]string { return h.labels }
+func (h Deployer) GlobalFlags() []string     { return h.LegacyHelmDeploy.Flags.Global }
+
 type Config interface {
 	kubectl.Config
 	kstatus.Config
@@ -136,13 +131,13 @@ type Config interface {
 
 // NewDeployer returns a configured Deployer.  Returns an error if current version of helm is less than 3.1.0.
 func NewDeployer(ctx context.Context, cfg Config, labeller *label.DefaultLabeller, h *latest.LegacyHelmDeploy, artifacts []*latest.Artifact) (*Deployer, error) {
-	hv, err := binVer(ctx)
+	hv, err := helm.BinVer(ctx)
 	if err != nil {
-		return nil, versionGetErr(err)
+		return nil, helm.VersionGetErr(err)
 	}
 
 	if hv.LT(helm31Version) {
-		return nil, minVersionErr(helm31Version.String())
+		return nil, helm.MinVersionErr(helm31Version.String())
 	}
 
 	podSelector := kubernetes.NewImageList()
@@ -250,20 +245,20 @@ func (h *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Art
 	for _, r := range h.Releases {
 		releaseName, err := util.ExpandEnvTemplateOrFail(r.Name, nil)
 		if err != nil {
-			return userErr(fmt.Sprintf("cannot expand release name %q", r.Name), err)
+			return helm.UserErr(fmt.Sprintf("cannot expand release name %q", r.Name), err)
 		}
 		chartVersion, err := util.ExpandEnvTemplateOrFail(r.Version, nil)
 		if err != nil {
-			return userErr(fmt.Sprintf("cannot expand chart version %q", r.Version), err)
+			return helm.UserErr(fmt.Sprintf("cannot expand chart version %q", r.Version), err)
 		}
 
 		repo, err := util.ExpandEnvTemplateOrFail(r.Repo, nil)
 		if err != nil {
-			return userErr(fmt.Sprintf("cannot expand repo %q", r.Repo), err)
+			return helm.UserErr(fmt.Sprintf("cannot expand repo %q", r.Repo), err)
 		}
 		m, results, err := h.deployRelease(ctx, out, releaseName, r, builds, h.bV, chartVersion, repo)
 		if err != nil {
-			return userErr(fmt.Sprintf("deploying %q", releaseName), err)
+			return helm.UserErr(fmt.Sprintf("deploying %q", releaseName), err)
 		}
 
 		manifests.Append(m)
@@ -346,7 +341,7 @@ func (h *Deployer) Dependencies() ([]string, error) {
 		}
 
 		if err := walk.From(release.ChartPath).When(isDep).AppendPaths(&deps); err != nil {
-			return deps, userErr("issue walking releases", err)
+			return deps, helm.UserErr("issue walking releases", err)
 		}
 	}
 	sort.Strings(deps)
@@ -366,7 +361,7 @@ func (h *Deployer) Cleanup(ctx context.Context, out io.Writer, dryRun bool, list
 			return fmt.Errorf("cannot parse the release name template: %w", err)
 		}
 
-		namespace, err := h.releaseNamespace(r)
+		namespace, err := helm.ReleaseNamespace(h.namespace, r)
 		if err != nil {
 			return err
 		}
@@ -381,7 +376,7 @@ func (h *Deployer) Cleanup(ctx context.Context, out io.Writer, dryRun bool, list
 		if namespace != "" {
 			args = append(args, "--namespace", namespace)
 		}
-		if err := h.exec(ctx, out, false, nil, args...); err != nil {
+		if err := helm.Exec(ctx, h, out, false, nil, args...); err != nil {
 			errMsgs = append(errMsgs, err.Error())
 		}
 	}
@@ -402,7 +397,7 @@ func (h *Deployer) Render(ctx context.Context, out io.Writer, builds []graph.Art
 	var postRendererArgs []string
 
 	if len(builds) > 0 {
-		skaffoldBinary, filterEnv, cleanup, err := h.prepareSkaffoldFilter(builds)
+		skaffoldBinary, filterEnv, cleanup, err := helm.PrepareSkaffoldFilter(h, builds)
 		if err != nil {
 			return fmt.Errorf("could not prepare `skaffold filter`: %w", err)
 		}
@@ -415,21 +410,21 @@ func (h *Deployer) Render(ctx context.Context, out io.Writer, builds []graph.Art
 	for _, r := range h.Releases {
 		releaseName, err := util.ExpandEnvTemplateOrFail(r.Name, nil)
 		if err != nil {
-			return userErr(fmt.Sprintf("cannot expand release name %q", r.Name), err)
+			return helm.UserErr(fmt.Sprintf("cannot expand release name %q", r.Name), err)
 		}
 
-		args := []string{"template", releaseName, chartSource(r)}
+		args := []string{"template", releaseName, helm.ChartSource(r)}
 		args = append(args, postRendererArgs...)
 		if r.Packaged == nil && r.Version != "" {
 			args = append(args, "--version", r.Version)
 		}
 
-		args, err = constructOverrideArgs(&r, builds, args)
+		args, err = helm.ConstructOverrideArgs(&r, builds, args)
 		if err != nil {
-			return userErr("construct override args", err)
+			return helm.UserErr("construct override args", err)
 		}
 
-		namespace, err := h.releaseNamespace(r)
+		namespace, err := helm.ReleaseNamespace(h.namespace, r)
 		if err != nil {
 			return err
 		}
@@ -443,8 +438,8 @@ func (h *Deployer) Render(ctx context.Context, out io.Writer, builds []graph.Art
 		}
 
 		outBuffer := new(bytes.Buffer)
-		if err := h.exec(ctx, outBuffer, false, helmEnv, args...); err != nil {
-			return userErr("std out err", fmt.Errorf(outBuffer.String()))
+		if err := helm.Exec(ctx, h, outBuffer, false, helmEnv, args...); err != nil {
+			return helm.UserErr("std out err", fmt.Errorf(outBuffer.String()))
 		}
 		renderedManifests.Write(outBuffer.Bytes())
 	}
@@ -496,7 +491,7 @@ func (h *Deployer) deployRelease(ctx context.Context, out io.Writer, releaseName
 		upgrade:     true,
 		flags:       h.Flags.Upgrade,
 		force:       h.forceDeploy,
-		chartPath:   chartSource(r),
+		chartPath:   helm.ChartSource(r),
 		helmVersion: helmVersion,
 		repo:        repo,
 		version:     chartVersion,
@@ -504,7 +499,7 @@ func (h *Deployer) deployRelease(ctx context.Context, out io.Writer, releaseName
 
 	installEnv := util.OSEnviron()
 	if len(builds) > 0 {
-		skaffoldBinary, filterEnv, cleanup, err := h.prepareSkaffoldFilter(builds)
+		skaffoldBinary, filterEnv, cleanup, err := helm.PrepareSkaffoldFilter(h, builds)
 		if err != nil {
 			return nil, nil, fmt.Errorf("could not prepare `skaffold filter`: %w", err)
 		}
@@ -514,12 +509,12 @@ func (h *Deployer) deployRelease(ctx context.Context, out io.Writer, releaseName
 		opts.postRenderer = skaffoldBinary
 		defer cleanup()
 	}
-	opts.namespace, err = h.releaseNamespace(r)
+	opts.namespace, err = helm.ReleaseNamespace(h.namespace, r)
 	if err != nil {
 		return nil, nil, err
 	}
 
-	if err := h.exec(ctx, ioutil.Discard, false, nil, getArgs(releaseName, opts.namespace)...); err != nil {
+	if err := helm.Exec(ctx, h, ioutil.Discard, false, nil, helm.GetArgs(releaseName, opts.namespace)...); err != nil {
 		output.Yellow.Fprintf(out, "Helm release %s not installed. Installing...\n", releaseName)
 
 		opts.upgrade = false
@@ -538,8 +533,8 @@ func (h *Deployer) deployRelease(ctx context.Context, out io.Writer, releaseName
 	if !r.SkipBuildDependencies && r.ChartPath != "" {
 		olog.Entry(ctx).Info("Building helm dependencies...")
 
-		if err := h.exec(ctx, out, false, nil, "dep", "build", r.ChartPath); err != nil {
-			return nil, nil, userErr("building helm dependencies", err)
+		if err := helm.Exec(ctx, h, out, false, nil, "dep", "build", r.ChartPath); err != nil {
+			return nil, nil, helm.UserErr("building helm dependencies", err)
 		}
 	}
 
@@ -547,11 +542,11 @@ func (h *Deployer) deployRelease(ctx context.Context, out io.Writer, releaseName
 	if len(r.Overrides.Values) != 0 {
 		overrides, err := yaml.Marshal(r.Overrides)
 		if err != nil {
-			return nil, nil, userErr("cannot marshal overrides to create overrides values.yaml", err)
+			return nil, nil, helm.UserErr("cannot marshal overrides to create overrides values.yaml", err)
 		}
 
 		if err := ioutil.WriteFile(constants.HelmOverridesFilename, overrides, 0666); err != nil {
-			return nil, nil, userErr(fmt.Sprintf("cannot create file %q", constants.HelmOverridesFilename), err)
+			return nil, nil, helm.UserErr(fmt.Sprintf("cannot create file %q", constants.HelmOverridesFilename), err)
 		}
 
 		defer func() {
@@ -562,7 +557,7 @@ func (h *Deployer) deployRelease(ctx context.Context, out io.Writer, releaseName
 	if r.Packaged != nil {
 		chartPath, err := h.packageChart(ctx, r)
 		if err != nil {
-			return nil, nil, userErr("cannot package chart", err)
+			return nil, nil, helm.UserErr("cannot package chart", err)
 		}
 
 		opts.chartPath = chartPath
@@ -570,18 +565,18 @@ func (h *Deployer) deployRelease(ctx context.Context, out io.Writer, releaseName
 
 	args, err := h.installArgs(r, builds, opts)
 	if err != nil {
-		return nil, nil, userErr("release args", err)
+		return nil, nil, helm.UserErr("release args", err)
 	}
 
-	err = h.exec(ctx, out, r.UseHelmSecrets, installEnv, args...)
+	err = helm.Exec(ctx, h, out, r.UseHelmSecrets, installEnv, args...)
 	if err != nil {
-		return nil, nil, userErr("install", err)
+		return nil, nil, helm.UserErr("install", err)
 	}
 
 	// get the kubernetes manifests deployed to the cluster
 	b, err := h.getReleaseManifest(ctx, releaseName, opts.namespace)
 	if err != nil {
-		return nil, nil, userErr("get release", err)
+		return nil, nil, helm.UserErr("get release", err)
 	}
 	artifacts := parseReleaseManifests(opts.namespace, bufio.NewReader(bytes.NewReader(b)))
 	return b, artifacts, nil
@@ -597,9 +592,9 @@ func (h *Deployer) getReleaseManifest(ctx context.Context, releaseName string, n
 	err := backoff.Retry(
 		func() error {
 			// only intereted in the deployed YAML
-			args := getArgs(releaseName, namespace)
+			args := helm.GetArgs(releaseName, namespace)
 			args = append(args, "--template", "{{.Release.Manifest}}")
-			if err := h.exec(ctx, &b, false, nil, args...); err != nil {
+			if err := helm.Exec(ctx, h, &b, false, nil, args...); err != nil {
 				olog.Entry(ctx).Debugf("unable to get release: %v (may retry):\n%s", err, b.String())
 				return err
 			}
@@ -644,7 +639,7 @@ func (h *Deployer) packageChart(ctx context.Context, r latest.HelmRelease) (stri
 
 	buf := &bytes.Buffer{}
 
-	if err := h.exec(ctx, buf, false, nil, args...); err != nil {
+	if err := helm.Exec(ctx, h, buf, false, nil, args...); err != nil {
 		return "", fmt.Errorf("package chart into a .tgz archive: %v: %w", args, err)
 	}
 
@@ -658,13 +653,6 @@ func (h *Deployer) packageChart(ctx context.Context, r latest.HelmRelease) (stri
 	return output[idx:], nil
 }
 
-func chartSource(r latest.HelmRelease) string {
-	if r.RemoteChart != "" {
-		return r.RemoteChart
-	}
-	return r.ChartPath
-}
-
 func warnAboutUnusedImages(builds []graph.Artifact, manifests manifest.ManifestList) {
 	seen := map[string]bool{}
 	images, _ := manifests.GetImages(manifest.NewResourceSelectorImages(manifest.TransformAllowlist, manifest.TransformDenylist))
diff --git a/pkg/skaffold/deploy/helm/util.go b/pkg/skaffold/deploy/helm/util.go
index ff946c8b1..a54660cfa 100644
--- a/pkg/skaffold/deploy/helm/util.go
+++ b/pkg/skaffold/deploy/helm/util.go
@@ -17,76 +17,12 @@ limitations under the License.
 package helm
 
 import (
-	"context"
-	"encoding/json"
 	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-	"os/exec"
-	"sort"
-	"strings"
 
-	"github.com/blang/semver"
-	shell "github.com/kballard/go-shellquote"
-
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
 )
 
-// copy of cmd/skaffold/app/flags.BuildOutputs
-type buildOutputs struct {
-	Builds []graph.Artifact `json:"builds"`
-}
-
-func writeBuildArtifacts(builds []graph.Artifact) (string, func(), error) {
-	buildOutput, err := json.Marshal(buildOutputs{builds})
-	if err != nil {
-		return "", nil, fmt.Errorf("cannot marshal build artifacts: %w", err)
-	}
-
-	f, err := ioutil.TempFile("", "builds*.yaml")
-	if err != nil {
-		return "", nil, fmt.Errorf("cannot create temp file: %w", err)
-	}
-	if _, err := f.Write(buildOutput); err != nil {
-		return "", nil, fmt.Errorf("cannot write to temp file: %w", err)
-	}
-	if err := f.Close(); err != nil {
-		return "", nil, fmt.Errorf("cannot close temp file: %w", err)
-	}
-	return f.Name(), func() { os.Remove(f.Name()) }, nil
-}
-
-// sortKeys returns the map keys in sorted order
-func sortKeys(m map[string]string) []string {
-	s := make([]string, 0, len(m))
-	for k := range m {
-		s = append(s, k)
-	}
-	sort.Strings(s)
-	return s
-}
-
-// binVer returns the version of the helm binary found in PATH.
-func binVer(ctx context.Context) (semver.Version, error) {
-	cmd := exec.Command("helm", "version", "--client")
-	b, err := util.RunCmdOut(ctx, cmd)
-	if err != nil {
-		return semver.Version{}, fmt.Errorf("helm version command failed %q: %w", string(b), err)
-	}
-	raw := string(b)
-	matches := versionRegex.FindStringSubmatch(raw)
-	if len(matches) == 0 {
-		return semver.Version{}, fmt.Errorf("unable to parse output: %q", raw)
-	}
-	return semver.ParseTolerant(matches[1])
-}
-
 // imageSetFromConfig calculates the --set-string value from the helm config
 func imageSetFromConfig(cfg *latest.HelmConventionConfig, valueName string, tag string) (string, error) {
 	if cfg == nil {
@@ -114,111 +50,3 @@ func imageSetFromConfig(cfg *latest.HelmConventionConfig, valueName string, tag
 
 	return fmt.Sprintf("%[1]s.repository=%[2]s,%[1]s.tag=%[3]s", valueName, ref.BaseName, imageTag), nil
 }
-
-func (h *Deployer) prepareSkaffoldFilter(builds []graph.Artifact) (skaffoldBinary string, env []string, cleanup func(), err error) {
-	skaffoldBinary, err = osExecutable()
-	if err != nil {
-		return "", nil, nil, fmt.Errorf("cannot locate this Skaffold binary: %w", err)
-	}
-
-	var buildsFile string
-	if len(builds) > 0 {
-		buildsFile, cleanup, err = writeBuildArtifactsFunc(builds)
-		if err != nil {
-			return "", nil, nil, fmt.Errorf("could not write build-artifacts: %w", err)
-		}
-	}
-	cmdLine := h.generateSkaffoldFilter(buildsFile)
-	env = append(env, fmt.Sprintf("SKAFFOLD_CMDLINE=%s", shell.Join(cmdLine...)))
-	env = append(env, fmt.Sprintf("SKAFFOLD_FILENAME=%s", h.configFile))
-	return
-}
-
-// generateSkaffoldFilter creates a `skaffold filter`` command-line for applying the various
-// Skaffold manifest filters, such a debugging, image replacement, and applying labels.
-func (h *Deployer) generateSkaffoldFilter(buildsFile string) []string {
-	args := []string{"filter", "--kube-context", h.kubeContext}
-	if h.enableDebug {
-		args = append(args, "--debugging")
-	}
-	for k, v := range h.labels {
-		args = append(args, fmt.Sprintf("--label=%s=%s", k, v))
-	}
-	if len(buildsFile) > 0 {
-		args = append(args, "--build-artifacts", buildsFile)
-	}
-	args = append(args, h.Flags.Global...)
-
-	if h.kubeConfig != "" {
-		args = append(args, "--kubeconfig", h.kubeConfig)
-	}
-	return args
-}
-
-func (h *Deployer) releaseNamespace(r latest.HelmRelease) (string, error) {
-	if h.namespace != "" {
-		return h.namespace, nil
-	} else if r.Namespace != "" {
-		namespace, err := util.ExpandEnvTemplateOrFail(r.Namespace, nil)
-		if err != nil {
-			return "", fmt.Errorf("cannot parse the release namespace template: %w", err)
-		}
-		return namespace, nil
-	}
-	return "", nil
-}
-
-// envVarForImage creates an environment map for an image and digest tag (fqn)
-func envVarForImage(imageName string, digest string) map[string]string {
-	customMap := map[string]string{
-		"IMAGE_NAME": imageName,
-		"DIGEST":     digest, // The `DIGEST` name is kept for compatibility reasons
-	}
-
-	// Standardize access to Image reference fields in templates
-	ref, err := docker.ParseReference(digest)
-	if err == nil {
-		customMap[constants.ImageRef.Repo] = ref.BaseName
-		customMap[constants.ImageRef.Tag] = ref.Tag
-		customMap[constants.ImageRef.Digest] = ref.Digest
-	} else {
-		log.Entry(context.TODO()).Warnf("unable to extract values for %v, %v and %v from image %v due to error:\n%v", constants.ImageRef.Repo, constants.ImageRef.Tag, constants.ImageRef.Digest, digest, err)
-	}
-
-	if digest == "" {
-		return customMap
-	}
-
-	// DIGEST_ALGO and DIGEST_HEX are deprecated and will contain nonsense values
-	names := strings.SplitN(digest, ":", 2)
-	if len(names) >= 2 {
-		customMap["DIGEST_ALGO"] = names[0]
-		customMap["DIGEST_HEX"] = names[1]
-	} else {
-		customMap["DIGEST_HEX"] = digest
-	}
-	return customMap
-}
-
-// exec executes the helm command, writing combined stdout/stderr to the provided writer
-func (h *Deployer) exec(ctx context.Context, out io.Writer, useSecrets bool, env []string, args ...string) error {
-	args = append([]string{"--kube-context", h.kubeContext}, args...)
-	args = append(args, h.Flags.Global...)
-
-	if h.kubeConfig != "" {
-		args = append(args, "--kubeconfig", h.kubeConfig)
-	}
-
-	if useSecrets {
-		args = append([]string{"secrets"}, args...)
-	}
-
-	cmd := exec.CommandContext(ctx, "helm", args...)
-	if len(env) > 0 {
-		cmd.Env = env
-	}
-	cmd.Stdout = out
-	cmd.Stderr = out
-
-	return util.RunCmd(ctx, cmd)
-}
diff --git a/pkg/skaffold/docker/dependencies.go b/pkg/skaffold/docker/dependencies.go
index 08ab506af..5cef07446 100644
--- a/pkg/skaffold/docker/dependencies.go
+++ b/pkg/skaffold/docker/dependencies.go
@@ -21,10 +21,12 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"runtime"
 	"sort"
 
 	"github.com/docker/docker/builder/dockerignore"
 
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/walk"
 )
@@ -62,6 +64,9 @@ func NormalizeDockerfilePath(context, dockerfile string) (string, error) {
 			return filepath.Abs(dockerfile)
 		}
 	}
+	if runtime.GOOS == constants.Windows && (filepath.VolumeName(dockerfile) != "" || filepath.IsAbs(dockerfile)) {
+		return dockerfile, nil
+	}
 	return filepath.Abs(rel)
 }
 
diff --git a/pkg/skaffold/helm/args.go b/pkg/skaffold/helm/args.go
new file mode 100644
index 000000000..311b2465c
--- /dev/null
+++ b/pkg/skaffold/helm/args.go
@@ -0,0 +1,134 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package helm
+
+import (
+	"context"
+	"fmt"
+	"runtime"
+	"strconv"
+	"strings"
+
+	"github.com/mitchellh/go-homedir"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
+	maps "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util/map"
+)
+
+// ConstructOverrideArgs creates the command line arguments for overrides
+func ConstructOverrideArgs(r *latest.HelmRelease, builds []graph.Artifact, args []string) ([]string, error) {
+	for _, k := range maps.SortKeys(r.SetValues) {
+		args = append(args, "--set", fmt.Sprintf("%s=%s", k, r.SetValues[k]))
+	}
+
+	for _, k := range maps.SortKeys(r.SetFiles) {
+		exp, err := homedir.Expand(r.SetFiles[k])
+		if err != nil {
+			return nil, fmt.Errorf("unable to expand %q: %w", r.SetFiles[k], err)
+		}
+		exp = SanitizeFilePath(exp, runtime.GOOS == "windows")
+
+		args = append(args, "--set-file", fmt.Sprintf("%s=%s", k, exp))
+	}
+
+	envMap := map[string]string{}
+	for idx, b := range builds {
+		suffix := ""
+		if idx > 0 {
+			suffix = strconv.Itoa(idx + 1)
+		}
+
+		for k, v := range envVarForImage(b.ImageName, b.Tag) {
+			envMap[k+suffix] = v
+		}
+	}
+	log.Entry(context.TODO()).Debugf("EnvVarMap: %+v\n", envMap)
+
+	for _, k := range maps.SortKeys(r.SetValueTemplates) {
+		v, err := util.ExpandEnvTemplate(r.SetValueTemplates[k], envMap)
+		if err != nil {
+			return nil, err
+		}
+		expandedKey, err := util.ExpandEnvTemplate(k, envMap)
+		if err != nil {
+			return nil, err
+		}
+
+		args = append(args, "--set", fmt.Sprintf("%s=%s", expandedKey, v))
+	}
+
+	for _, v := range r.ValuesFiles {
+		exp, err := homedir.Expand(v)
+		if err != nil {
+			return nil, fmt.Errorf("unable to expand %q: %w", v, err)
+		}
+
+		exp, err = util.ExpandEnvTemplate(exp, envMap)
+		if err != nil {
+			return nil, err
+		}
+
+		args = append(args, "-f", exp)
+	}
+	return args, nil
+}
+
+// GetArgs calculates the correct arguments to "helm get"
+func GetArgs(releaseName string, namespace string) []string {
+	args := []string{"get", "all"}
+	if namespace != "" {
+		args = append(args, "--namespace", namespace)
+	}
+	return append(args, releaseName)
+}
+
+// envVarForImage creates an environment map for an image and digest tag (fqn)
+func envVarForImage(imageName string, digest string) map[string]string {
+	customMap := map[string]string{
+		"IMAGE_NAME": imageName,
+		"DIGEST":     digest, // The `DIGEST` name is kept for compatibility reasons
+	}
+
+	// Standardize access to Image reference fields in templates
+	ref, err := docker.ParseReference(digest)
+	if err == nil {
+		customMap[constants.ImageRef.Repo] = ref.BaseName
+		customMap[constants.ImageRef.Tag] = ref.Tag
+		customMap[constants.ImageRef.Digest] = ref.Digest
+	} else {
+		log.Entry(context.TODO()).Warnf("unable to extract values for %v, %v and %v from image %v due to error:\n%v", constants.ImageRef.Repo, constants.ImageRef.Tag, constants.ImageRef.Digest, digest, err)
+	}
+
+	if digest == "" {
+		return customMap
+	}
+
+	// DIGEST_ALGO and DIGEST_HEX are deprecated and will contain nonsense values
+	names := strings.SplitN(digest, ":", 2)
+	if len(names) >= 2 {
+		customMap["DIGEST_ALGO"] = names[0]
+		customMap["DIGEST_HEX"] = names[1]
+	} else {
+		customMap["DIGEST_HEX"] = digest
+	}
+	return customMap
+}
diff --git a/pkg/skaffold/helm/bin.go b/pkg/skaffold/helm/bin.go
new file mode 100644
index 000000000..6ead97ac1
--- /dev/null
+++ b/pkg/skaffold/helm/bin.go
@@ -0,0 +1,130 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package helm
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"regexp"
+
+	"github.com/blang/semver"
+	shell "github.com/kballard/go-shellquote"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
+)
+
+var (
+	// VersionRegex extracts version from "helm version --client", for instance: "2.14.0-rc.2"
+	VersionRegex = regexp.MustCompile(`v?(\d[\w.\-]+)`)
+
+	// OSExecutable allows for replacing the skaffold binary for testing purposes
+	OSExecutable = os.Executable
+
+	// WriteBuildArtifacts is meant to be reassigned for testing
+	WriteBuildArtifacts = writeBuildArtifacts
+)
+
+type Client interface {
+	EnableDebug() bool
+	ConfigFile() string
+	KubeConfig() string
+	KubeContext() string
+	Labels() map[string]string
+	GlobalFlags() []string
+}
+
+// BinVer returns the version of the helm binary found in PATH.
+func BinVer(ctx context.Context) (semver.Version, error) {
+	cmd := exec.Command("helm", "version", "--client")
+	b, err := util.RunCmdOut(ctx, cmd)
+	if err != nil {
+		return semver.Version{}, fmt.Errorf("helm version command failed %q: %w", string(b), err)
+	}
+	raw := string(b)
+	matches := VersionRegex.FindStringSubmatch(raw)
+	if len(matches) == 0 {
+		return semver.Version{}, fmt.Errorf("unable to parse output: %q", raw)
+	}
+	return semver.ParseTolerant(matches[1])
+}
+
+func PrepareSkaffoldFilter(h Client, builds []graph.Artifact) (skaffoldBinary string, env []string, cleanup func(), err error) {
+	skaffoldBinary, err = OSExecutable()
+	if err != nil {
+		return "", nil, nil, fmt.Errorf("cannot locate this Skaffold binary: %w", err)
+	}
+
+	var buildsFile string
+	if len(builds) > 0 {
+		buildsFile, cleanup, err = WriteBuildArtifacts(builds)
+		if err != nil {
+			return "", nil, nil, fmt.Errorf("could not write build-artifacts: %w", err)
+		}
+	}
+	cmdLine := generateSkaffoldFilter(h, buildsFile)
+	env = append(env, fmt.Sprintf("SKAFFOLD_CMDLINE=%s", shell.Join(cmdLine...)))
+	env = append(env, fmt.Sprintf("SKAFFOLD_FILENAME=%s", h.ConfigFile()))
+	return
+}
+
+// generateSkaffoldFilter creates a `skaffold filter`` command-line for applying the various
+// Skaffold manifest filters, such a debugging, image replacement, and applying labels.
+func generateSkaffoldFilter(h Client, buildsFile string) []string {
+	args := []string{"filter", "--kube-context", h.KubeContext()}
+	if h.EnableDebug() {
+		args = append(args, "--debugging")
+	}
+	for k, v := range h.Labels() {
+		args = append(args, fmt.Sprintf("--label=%s=%s", k, v))
+	}
+	if len(buildsFile) > 0 {
+		args = append(args, "--build-artifacts", buildsFile)
+	}
+	args = append(args, h.GlobalFlags()...)
+
+	if h.KubeConfig() != "" {
+		args = append(args, "--kubeconfig", h.KubeConfig())
+	}
+	return args
+}
+
+// Exec executes the helm command, writing combined stdout/stderr to the provided writer
+func Exec(ctx context.Context, h Client, out io.Writer, useSecrets bool, env []string, args ...string) error {
+	args = append([]string{"--kube-context", h.KubeContext()}, args...)
+	args = append(args, h.GlobalFlags()...)
+
+	if h.KubeConfig() != "" {
+		args = append(args, "--kubeconfig", h.KubeConfig())
+	}
+
+	if useSecrets {
+		args = append([]string{"secrets"}, args...)
+	}
+
+	cmd := exec.CommandContext(ctx, "helm", args...)
+	if len(env) > 0 {
+		cmd.Env = env
+	}
+	cmd.Stdout = out
+	cmd.Stderr = out
+
+	return util.RunCmd(ctx, cmd)
+}
diff --git a/pkg/skaffold/deploy/helm/errors.go b/pkg/skaffold/helm/errors.go
similarity index 87%
rename from pkg/skaffold/deploy/helm/errors.go
rename to pkg/skaffold/helm/errors.go
index 59e5925b8..f54f09a8d 100644
--- a/pkg/skaffold/deploy/helm/errors.go
+++ b/pkg/skaffold/helm/errors.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2020 The Skaffold Authors
+Copyright 2022 The Skaffold Authors
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -26,12 +26,17 @@ import (
 	"github.com/GoogleContainerTools/skaffold/proto/v1"
 )
 
+var (
+	// error to throw when helm version can't be determined
+	versionErrorString = "failed to determine binary version: %w"
+)
+
 const (
 	installLink = "https://helm.sh/docs/intro/install"
 	toolName    = "Helm"
 )
 
-func versionGetErr(err error) error {
+func VersionGetErr(err error) error {
 	return sErrors.NewError(err,
 		&proto.ActionableErr{
 			Message: deployerr.MissingToolErr(toolName, fmt.Errorf(versionErrorString, err)),
@@ -45,7 +50,7 @@ func versionGetErr(err error) error {
 		})
 }
 
-func minVersionErr(minVer string) error {
+func MinVersionErr(minVer string) error {
 	return sErrors.NewErrorWithStatusCode(
 		&proto.ActionableErr{
 			Message: fmt.Sprintf("skaffold requires Helm version %s or greater", minVer),
@@ -59,11 +64,11 @@ func minVersionErr(minVer string) error {
 		})
 }
 
-func userErr(prefix string, err error) error {
+func UserErr(prefix string, err error) error {
 	return deployerr.UserError(errors.Wrap(err, prefix), proto.StatusCode_DEPLOY_HELM_USER_ERR)
 }
 
-func createNamespaceErr(version string) error {
+func CreateNamespaceErr(version string) error {
 	return sErrors.NewErrorWithStatusCode(
 		&proto.ActionableErr{
 			Message: fmt.Sprintf("Skaffold config options `createNamespace` is not available in the current Helm version %s", version),
diff --git a/pkg/skaffold/helm/util.go b/pkg/skaffold/helm/util.go
new file mode 100644
index 000000000..47b23ee30
--- /dev/null
+++ b/pkg/skaffold/helm/util.go
@@ -0,0 +1,122 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package helm
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
+)
+
+// copy of cmd/skaffold/app/flags.BuildOutputs
+type buildOutputs struct {
+	Builds []graph.Artifact `json:"builds"`
+}
+
+func writeBuildArtifacts(builds []graph.Artifact) (string, func(), error) {
+	buildOutput, err := json.Marshal(buildOutputs{builds})
+	if err != nil {
+		return "", nil, fmt.Errorf("cannot marshal build artifacts: %w", err)
+	}
+
+	f, err := ioutil.TempFile("", "builds*.yaml")
+	if err != nil {
+		return "", nil, fmt.Errorf("cannot create temp file: %w", err)
+	}
+	if _, err := f.Write(buildOutput); err != nil {
+		return "", nil, fmt.Errorf("cannot write to temp file: %w", err)
+	}
+	if err := f.Close(); err != nil {
+		return "", nil, fmt.Errorf("cannot close temp file: %w", err)
+	}
+	return f.Name(), func() { os.Remove(f.Name()) }, nil
+}
+
+// SanitizeFilePath is used to sanitize filepaths that are provided to the `setFiles` flag
+// helm `setFiles` doesn't work with the unescaped filepath separator (\) for Windows or if there are unescaped tabs and spaces in the directory names.
+// So we escape all odd count occurrences of `\` for Windows, and wrap the entire string in quotes if it has spaces.
+// This is very specific to the way helm handles its flags.
+// See https://github.com/helm/helm/blob/d55c53df4e394fb62b0514a09c57bce235dd7877/pkg/cli/values/options.
+// Otherwise the windows `syscall` package implements its own sanitizing for command args that's used by `exec.Cmd`.
+// See https://github.com/golang/go/blob/6951da56b0ae2cd4250fc1b0350d090aed633ac1/src/syscall/exec_windows.go#L27
+func SanitizeFilePath(s string, isWindowsOS bool) string {
+	if len(s) == 0 {
+		return `""`
+	}
+	needsQuotes := false
+	for i := 0; i < len(s); i++ {
+		if s[i] == ' ' || s[i] == '\t' {
+			needsQuotes = true
+			break
+		}
+	}
+
+	if !isWindowsOS {
+		if needsQuotes {
+			return fmt.Sprintf(`"%s"`, s)
+		}
+		return s
+	}
+
+	var b []byte
+	slashes := 0
+	for i := 0; i < len(s); i++ {
+		switch s[i] {
+		case '\\':
+			slashes++
+		default:
+			// ensure a single slash is escaped
+			if slashes == 1 {
+				b = append(b, '\\')
+			}
+			slashes = 0
+		}
+		b = append(b, s[i])
+	}
+	if slashes == 1 {
+		b = append(b, '\\')
+	}
+	if needsQuotes {
+		return fmt.Sprintf(`"%s"`, string(b))
+	}
+	return string(b)
+}
+
+func ChartSource(r latest.HelmRelease) string {
+	if r.RemoteChart != "" {
+		return r.RemoteChart
+	}
+	return r.ChartPath
+}
+
+func ReleaseNamespace(namespace string, release latest.HelmRelease) (string, error) {
+	if namespace != "" {
+		return namespace, nil
+	} else if release.Namespace != "" {
+		namespace, err := util.ExpandEnvTemplateOrFail(release.Namespace, nil)
+		if err != nil {
+			return "", fmt.Errorf("cannot parse the release namespace template: %w", err)
+		}
+		return namespace, nil
+	}
+	return "", nil
+}
diff --git a/pkg/skaffold/render/config.go b/pkg/skaffold/render/config.go
index 53ee05e63..1f22d944c 100644
--- a/pkg/skaffold/render/config.go
+++ b/pkg/skaffold/render/config.go
@@ -17,12 +17,30 @@ limitations under the License.
 package render
 
 import (
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
 )
 
 type Config interface {
 	GetWorkingDir() string
+	TransformRulesFile() string
+	ConfigurationFile() string
+	GetKubeContext() string
+	GetKubeConfig() string
 	TransformAllowList() []latest.ResourceFilter
 	TransformDenyList() []latest.ResourceFilter
-	TransformRulesFile() string
+	Mode() config.RunMode
 }
+
+type MockConfig struct {
+	WorkingDir string
+}
+
+func (mc MockConfig) GetWorkingDir() string                       { return mc.WorkingDir }
+func (mc MockConfig) TransformAllowList() []latest.ResourceFilter { return nil }
+func (mc MockConfig) TransformDenyList() []latest.ResourceFilter  { return nil }
+func (mc MockConfig) TransformRulesFile() string                  { return "" }
+func (mc MockConfig) ConfigurationFile() string                   { return "" }
+func (mc MockConfig) GetKubeConfig() string                       { return "" }
+func (mc MockConfig) GetKubeContext() string                      { return "" }
+func (mc MockConfig) Mode() config.RunMode                        { return "" }
diff --git a/pkg/skaffold/render/generate/generate.go b/pkg/skaffold/render/generate/generate.go
index 1f1fec19c..838286431 100644
--- a/pkg/skaffold/render/generate/generate.go
+++ b/pkg/skaffold/render/generate/generate.go
@@ -154,7 +154,6 @@ func (g Generator) Generate(ctx context.Context, out io.Writer) (manifest.Manife
 		manifests.Append(manifestFileContent)
 	}
 
-	// TODO(yuwenma): helm resources. `render.generate.helmCharts`
 	return manifests, nil
 }
 
diff --git a/pkg/skaffold/render/renderer/helm/helm.go b/pkg/skaffold/render/renderer/helm/helm.go
new file mode 100644
index 000000000..06720c5ba
--- /dev/null
+++ b/pkg/skaffold/render/renderer/helm/helm.go
@@ -0,0 +1,158 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package helm
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+
+	apimachinery "k8s.io/apimachinery/pkg/runtime/schema"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/helm"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/generate"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/renderer/util"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
+	sUtil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
+)
+
+type Helm struct {
+	generate.Generator
+	config *latest.Helm
+
+	kubeContext string
+	kubeConfig  string
+	namespace   string
+	configFile  string
+	labels      map[string]string
+	enableDebug bool
+
+	transformAllowlist map[apimachinery.GroupKind]latest.ResourceFilter
+	transformDenylist  map[apimachinery.GroupKind]latest.ResourceFilter
+}
+
+func (h Helm) EnableDebug() bool         { return h.enableDebug }
+func (h Helm) ConfigFile() string        { return h.configFile }
+func (h Helm) KubeContext() string       { return h.kubeContext }
+func (h Helm) KubeConfig() string        { return h.kubeConfig }
+func (h Helm) Labels() map[string]string { return h.labels }
+func (h Helm) GlobalFlags() []string     { return h.config.Flags.Global }
+
+func New(cfg render.Config, rCfg latest.RenderConfig, labels map[string]string) (Helm, error) {
+	generator := generate.NewGenerator(cfg.GetWorkingDir(), rCfg.Generate)
+	transformAllowlist, transformDenylist, err := util.ConsolidateTransformConfiguration(cfg)
+	if err != nil {
+		return Helm{}, err
+	}
+	return Helm{
+		Generator: generator,
+		config:    rCfg.Helm,
+
+		enableDebug: cfg.Mode() == config.RunModes.Debug,
+		configFile:  cfg.ConfigurationFile(),
+		kubeContext: cfg.GetKubeContext(),
+		kubeConfig:  cfg.GetKubeConfig(),
+		labels:      labels,
+
+		transformAllowlist: transformAllowlist,
+		transformDenylist:  transformDenylist,
+	}, nil
+}
+
+func (h Helm) Render(ctx context.Context, out io.Writer, builds []graph.Artifact, _ bool) (manifest.ManifestList, error) {
+	_, endTrace := instrumentation.StartTrace(ctx, "Render_HelmManifests")
+	log.Entry(ctx).Infof("rendering using helm")
+	instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{
+		"RendererType": "helm",
+	})
+
+	manifests, err := h.generateHelmManifests(ctx, builds)
+	endTrace()
+	return manifests, err
+}
+
+func (h Helm) generateHelmManifests(ctx context.Context, builds []graph.Artifact) (manifest.ManifestList, error) {
+	renderedManifests := new(bytes.Buffer)
+	helmEnv := sUtil.OSEnviron()
+	var postRendererArgs []string
+
+	if len(builds) > 0 {
+		skaffoldBinary, filterEnv, cleanup, err := helm.PrepareSkaffoldFilter(h, builds)
+		if err != nil {
+			return nil, fmt.Errorf("could not prepare `skaffold filter`: %w", err)
+		}
+		// need to include current environment, specifically for HOME to lookup ~/.kube/config
+		helmEnv = append(helmEnv, filterEnv...)
+		postRendererArgs = []string{"--post-renderer", skaffoldBinary}
+		defer cleanup()
+	}
+
+	for _, release := range h.config.Releases {
+		releaseName, err := sUtil.ExpandEnvTemplateOrFail(release.Name, nil)
+		if err != nil {
+			return nil, helm.UserErr(fmt.Sprintf("cannot expand release name %q", release.Name), err)
+		}
+
+		args := []string{"template", releaseName, helm.ChartSource(release)}
+		args = append(args, postRendererArgs...)
+		if release.Packaged == nil && release.Version != "" {
+			args = append(args, "--version", release.Version)
+		}
+
+		args, err = helm.ConstructOverrideArgs(&release, builds, args)
+		if err != nil {
+			return nil, helm.UserErr("construct override args", err)
+		}
+
+		namespace, err := helm.ReleaseNamespace(h.namespace, release)
+		if err != nil {
+			return nil, err
+		}
+		if namespace != "" {
+			args = append(args, "--namespace", namespace)
+		}
+
+		if release.Repo != "" {
+			args = append(args, "--repo")
+			args = append(args, release.Repo)
+		}
+
+		outBuffer := new(bytes.Buffer)
+		if err := helm.Exec(ctx, h, outBuffer, false, helmEnv, args...); err != nil {
+			return nil, helm.UserErr("std out err", fmt.Errorf(outBuffer.String()))
+		}
+		renderedManifests.Write(outBuffer.Bytes())
+	}
+	manifests, err := manifest.Load(bytes.NewReader(renderedManifests.Bytes()))
+	if err != nil {
+		return nil, err
+	}
+
+	manifests, err = manifests.SetLabels(h.labels, manifest.NewResourceSelectorLabels(h.transformAllowlist, h.transformDenylist))
+	if err != nil {
+		return nil, err
+	}
+
+	return manifests, nil
+}
diff --git a/pkg/skaffold/render/renderer/renderer.go b/pkg/skaffold/render/renderer/renderer.go
index 2aab295d4..8255ff575 100644
--- a/pkg/skaffold/render/renderer/renderer.go
+++ b/pkg/skaffold/render/renderer/renderer.go
@@ -24,6 +24,7 @@ import (
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/renderer/helm"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/renderer/kpt"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/renderer/kubectl"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/renderer/noop"
@@ -42,6 +43,10 @@ func New(cfg render.Config, renderCfg latest.RenderConfig, hydrationDir string,
 	if usingLegacyHelmDeploy {
 		return noop.New(renderCfg, cfg.GetWorkingDir(), hydrationDir, labels)
 	}
+	if renderCfg.Validate == nil && renderCfg.Transform == nil && renderCfg.Helm != nil {
+		log.Entry(context.TODO()).Debug("setting up helm renderer")
+		return helm.New(cfg, renderCfg, labels)
+	}
 	if renderCfg.Validate == nil && renderCfg.Transform == nil && renderCfg.Kpt == nil {
 		log.Entry(context.TODO()).Debug("setting up kubectl renderer")
 		return kubectl.New(cfg, renderCfg, labels)
diff --git a/pkg/skaffold/util/map/map.go b/pkg/skaffold/util/map/map.go
new file mode 100644
index 000000000..09291524e
--- /dev/null
+++ b/pkg/skaffold/util/map/map.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package maps
+
+import "sort"
+
+// SortKeys returns the map keys in sorted order
+func SortKeys(m map[string]string) []string {
+	s := make([]string, 0, len(m))
+	for k := range m {
+		s = append(s, k)
+	}
+	sort.Strings(s)
+	return s
+}


Test patch:
diff --git a/integration/dev_dependencies_test.go b/integration/dev_dependencies_test.go
index da50bc500..a3f7c21f2 100644
--- a/integration/dev_dependencies_test.go
+++ b/integration/dev_dependencies_test.go
@@ -26,9 +26,6 @@ import (
 )
 
 func TestDev_WithDependencies(t *testing.T) {
-	// TODO: This test shall pass once render v2 is completed.
-	t.SkipNow()
-
 	MarkIntegrationTest(t, CanRunWithoutGcp)
 	t.Run("required artifact rebuild & redeploy also rebuilds & redeploys dependencies", func(t *testing.T) {
 		ns, client := SetupNamespace(t)
diff --git a/integration/render_test.go b/integration/render_test.go
index 3f27fb3ee..a75178c13 100644
--- a/integration/render_test.go
+++ b/integration/render_test.go
@@ -28,11 +28,9 @@ import (
 	yaml "gopkg.in/yaml.v2"
 
 	"github.com/GoogleContainerTools/skaffold/integration/skaffold"
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/helm"
-	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/label"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/renderer/kubectl"
-	runcontext "github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext/v2"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
 	"github.com/GoogleContainerTools/skaffold/testutil"
 )
@@ -75,7 +73,7 @@ spec:
 			Generate: latest.Generate{
 				RawK8s: []string{"deployment.yaml"}},
 		}
-		mockCfg := mockConfig{workingDir: tmpDir.Root()}
+		mockCfg := render.MockConfig{WorkingDir: tmpDir.Root()}
 		r, err := kubectl.New(mockCfg, rc, map[string]string{})
 		t.RequireNoError(err)
 		var b bytes.Buffer
@@ -214,7 +212,7 @@ spec:
 				Generate: latest.Generate{
 					RawK8s: []string{"deployment.yaml"}},
 			}
-			mockCfg := mockConfig{workingDir: tmpDir.Root()}
+			mockCfg := render.MockConfig{WorkingDir: tmpDir.Root()}
 			r, err := kubectl.New(mockCfg, rc, map[string]string{})
 			t.RequireNoError(err)
 			var b bytes.Buffer
@@ -228,52 +226,42 @@ spec:
 
 func TestHelmRender(t *testing.T) {
 	MarkIntegrationTest(t, CanRunWithoutGcp)
-	// TODO Fix test https://github.com/GoogleContainerTools/skaffold/issues/7285
-	t.Skipf("Fix todo https://github.com/GoogleContainerTools/skaffold/issues/7285")
 
 	tests := []struct {
 		description  string
+		dir          string
+		args         []string
 		builds       []graph.Artifact
 		helmReleases []latest.HelmRelease
 		expectedOut  string
 	}{
 		{
 			description: "Bare bones render",
-			builds: []graph.Artifact{
-				{
-					ImageName: "gke-loadbalancer",
-					Tag:       "gke-loadbalancer:test",
-				},
-			},
-			helmReleases: []latest.HelmRelease{{
-				Name:      "gke-loadbalancer",
-				ChartPath: "testdata/gke_loadbalancer/loadbalancer-helm",
-			}},
-			expectedOut: `---
-# Source: loadbalancer-helm/templates/k8s.yaml
-apiVersion: v1
+			dir:         "testdata/gke_loadbalancer-render",
+			expectedOut: `apiVersion: v1
 kind: Service
 metadata:
-  name: gke-loadbalancer
   labels:
     app: gke-loadbalancer
+    skaffold.dev/run-id: phony-run-id
+  name: gke-loadbalancer
 spec:
-  type: LoadBalancer
   ports:
-    - port: 80
-      targetPort: 3000
-      protocol: TCP
-      name: http
+  - name: http
+    port: 80
+    protocol: TCP
+    targetPort: 3000
   selector:
-    app: "gke-loadbalancer"
+    app: gke-loadbalancer
+  type: LoadBalancer
 ---
-# Source: loadbalancer-helm/templates/k8s.yaml
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: gke-loadbalancer
   labels:
     app: gke-loadbalancer
+    skaffold.dev/run-id: phony-run-id
+  name: gke-loadbalancer
 spec:
   replicas: 1
   selector:
@@ -283,62 +271,50 @@ spec:
     metadata:
       labels:
         app: gke-loadbalancer
+        skaffold.dev/run-id: phony-run-id
     spec:
       containers:
-        - name: gke-container
-          image: gke-loadbalancer:test
-          ports:
-            - containerPort: 3000
-
+      - image: gke-loadbalancer:test
+        name: gke-container
+        ports:
+        - containerPort: 3000
 `,
 		},
 		{
 			description: "A more complex template",
-			builds: []graph.Artifact{
-				{
-					ImageName: "gcr.io/k8s-skaffold/skaffold-helm",
-					Tag:       "gcr.io/k8s-skaffold/skaffold-helm:sha256-nonsenslettersandnumbers",
-				},
-			},
-			helmReleases: []latest.HelmRelease{{
-				Name:      "skaffold-helm",
-				ChartPath: "testdata/helm/skaffold-helm",
-				SetValues: map[string]string{
-					"pullPolicy": "Always",
-				},
-			}},
-			expectedOut: `---
-# Source: skaffold-helm/templates/service.yaml
-apiVersion: v1
+			dir:         "testdata/helm-render",
+			args:        []string{"--profile=helm-render"},
+			expectedOut: `apiVersion: v1
 kind: Service
 metadata:
-  name: skaffold-helm-skaffold-helm
   labels:
     app: skaffold-helm
     chart: skaffold-helm-0.1.0
-    release: skaffold-helm
     heritage: Helm
+    release: skaffold-helm
+    skaffold.dev/run-id: phony-run-id
+  name: skaffold-helm-skaffold-helm
 spec:
-  type: ClusterIP
   ports:
-    - port: 80
-      targetPort: 80
-      protocol: TCP
-      name: nginx
+  - name: nginx
+    port: 80
+    protocol: TCP
+    targetPort: 80
   selector:
     app: skaffold-helm
     release: skaffold-helm
+  type: ClusterIP
 ---
-# Source: skaffold-helm/templates/deployment.yaml
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: skaffold-helm
   labels:
     app: skaffold-helm
     chart: skaffold-helm-0.1.0
-    release: skaffold-helm
     heritage: Helm
+    release: skaffold-helm
+    skaffold.dev/run-id: phony-run-id
+  name: skaffold-helm
 spec:
   replicas: 1
   selector:
@@ -350,60 +326,42 @@ spec:
       labels:
         app: skaffold-helm
         release: skaffold-helm
+        skaffold.dev/run-id: phony-run-id
     spec:
       containers:
-        - name: skaffold-helm
-          image: gcr.io/k8s-skaffold/skaffold-helm:sha256-nonsenslettersandnumbers
-          imagePullPolicy: Always
-          ports:
-            - containerPort: 80
-          resources:
-            {}
+      - image: gcr.io/k8s-skaffold/skaffold-helm:sha256-nonsenselettersandnumbers
+        imagePullPolicy: always
+        name: skaffold-helm
+        ports:
+        - containerPort: 80
+        resources: {}
 ---
-# Source: skaffold-helm/templates/ingress.yaml
 apiVersion: extensions/v1beta1
 kind: Ingress
 metadata:
-  name: skaffold-helm-skaffold-helm
+  annotations: null
   labels:
     app: skaffold-helm
     chart: skaffold-helm-0.1.0
-    release: skaffold-helm
     heritage: Helm
-  annotations:
+    release: skaffold-helm
+  name: skaffold-helm-skaffold-helm
 spec:
   rules:
-    - http:
-        paths:
-          - path: /
-            backend:
-              serviceName: skaffold-helm-skaffold-helm
-              servicePort: 80
-
+  - http:
+      paths:
+      - backend:
+          serviceName: skaffold-helm-skaffold-helm
+          servicePort: 80
+        path: /
 `,
 		},
 	}
 	for _, test := range tests {
-		testutil.Run(t, test.description, func(t *testutil.T) {
-			deployer, err := helm.NewDeployer(context.Background(), &runcontext.RunContext{
-				Pipelines: runcontext.NewPipelines([]latest.Pipeline{{
-					Deploy: latest.DeployConfig{
-						DeployType: latest.DeployType{
-							LegacyHelmDeploy: &latest.LegacyHelmDeploy{
-								Releases: test.helmReleases,
-							},
-						},
-					},
-				}}),
-			}, &label.DefaultLabeller{}, &latest.LegacyHelmDeploy{
-				Releases: test.helmReleases,
-			}, nil)
-			t.RequireNoError(err)
-			var b bytes.Buffer
-			err = deployer.Render(context.Background(), &b, test.builds, true, "")
+		t.Run(test.description, func(t *testing.T) {
+			out := skaffold.Render(append([]string{"--build-artifacts=builds.out.json", "--default-repo=", "--label=skaffold.dev/run-id=phony-run-id"}, test.args...)...).InDir(test.dir).RunOrFailOutput(t)
 
-			t.CheckNoError(err)
-			t.CheckDeepEqual(test.expectedOut, b.String())
+			testutil.CheckDeepEqual(t, test.expectedOut, string(out))
 		})
 	}
 }
@@ -744,12 +702,3 @@ spec:
 		})
 	}
 }
-
-type mockConfig struct {
-	workingDir string
-}
-
-func (mc mockConfig) GetWorkingDir() string                       { return mc.workingDir }
-func (mc mockConfig) TransformAllowList() []latest.ResourceFilter { return nil }
-func (mc mockConfig) TransformDenyList() []latest.ResourceFilter  { return nil }
-func (mc mockConfig) TransformRulesFile() string                  { return "" }
diff --git a/integration/testdata/build-dependencies/skaffold.yaml b/integration/testdata/build-dependencies/skaffold.yaml
index 07bda2166..e3612bb87 100644
--- a/integration/testdata/build-dependencies/skaffold.yaml
+++ b/integration/testdata/build-dependencies/skaffold.yaml
@@ -42,10 +42,9 @@ build:
         SLEEP: "3"
         FAIL: "0"
 
-deploy:
-  kubectl:
-    manifests:
-    - 'kubernetes/*.yaml'
+manifests:
+   rawYaml:
+      - 'kubernetes/*.yaml'
 
 profiles:
 - name: concurrency-0
diff --git a/integration/testdata/gke_loadbalancer-render/Dockerfile b/integration/testdata/gke_loadbalancer-render/Dockerfile
new file mode 100644
index 000000000..e7d55a77a
--- /dev/null
+++ b/integration/testdata/gke_loadbalancer-render/Dockerfile
@@ -0,0 +1,7 @@
+FROM golang:1.15-alpine as builder
+COPY main.go .
+RUN go build -o /main .
+
+FROM alpine:3
+CMD ["./main"]
+COPY --from=builder /main .
diff --git a/integration/testdata/gke_loadbalancer-render/builds.out.json b/integration/testdata/gke_loadbalancer-render/builds.out.json
new file mode 100644
index 000000000..bdd33ec6d
--- /dev/null
+++ b/integration/testdata/gke_loadbalancer-render/builds.out.json
@@ -0,0 +1,1 @@
+{"builds":[{"imageName":"gke-loadbalancer","tag":"gke-loadbalancer:test"}]}
\ No newline at end of file
diff --git a/integration/testdata/gke_loadbalancer-render/loadbalancer-helm/Chart.yaml b/integration/testdata/gke_loadbalancer-render/loadbalancer-helm/Chart.yaml
new file mode 100644
index 000000000..aa0902ca1
--- /dev/null
+++ b/integration/testdata/gke_loadbalancer-render/loadbalancer-helm/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+description: A Helm chart for Kubernetes
+name: loadbalancer-helm
+version: 0.1.0
diff --git a/integration/testdata/gke_loadbalancer-render/loadbalancer-helm/templates/k8s.yaml b/integration/testdata/gke_loadbalancer-render/loadbalancer-helm/templates/k8s.yaml
new file mode 100644
index 000000000..0aa4ac1a2
--- /dev/null
+++ b/integration/testdata/gke_loadbalancer-render/loadbalancer-helm/templates/k8s.yaml
@@ -0,0 +1,37 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: gke-loadbalancer
+  labels:
+    app: gke-loadbalancer
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: gke-loadbalancer
+  template:
+    metadata:
+      labels:
+        app: gke-loadbalancer
+    spec:
+      containers:
+        - name: gke-container
+          image: {{ .Values.image }}
+          ports:
+            - containerPort: 3000
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: gke-loadbalancer
+  labels:
+    app: gke-loadbalancer
+spec:
+  type: LoadBalancer
+  ports:
+    - port: 80
+      targetPort: 3000
+      protocol: TCP
+      name: http
+  selector:
+    app: "gke-loadbalancer"
diff --git a/integration/testdata/gke_loadbalancer-render/loadbalancer-helm/values.yaml b/integration/testdata/gke_loadbalancer-render/loadbalancer-helm/values.yaml
new file mode 100644
index 000000000..1e3f5df32
--- /dev/null
+++ b/integration/testdata/gke_loadbalancer-render/loadbalancer-helm/values.yaml
@@ -0,0 +1,3 @@
+image: gke-loadbalancer
+
+
diff --git a/integration/testdata/gke_loadbalancer-render/main.go b/integration/testdata/gke_loadbalancer-render/main.go
new file mode 100644
index 000000000..b97ccb54d
--- /dev/null
+++ b/integration/testdata/gke_loadbalancer-render/main.go
@@ -0,0 +1,17 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+)
+
+func handler(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "hello!!\n")
+}
+
+func main() {
+	log.Print("gke loadbalancer server ready")
+	http.HandleFunc("/", handler)
+	http.ListenAndServe(":3000", nil)
+}
diff --git a/integration/testdata/gke_loadbalancer-render/skaffold.yaml b/integration/testdata/gke_loadbalancer-render/skaffold.yaml
new file mode 100644
index 000000000..2e4565dda
--- /dev/null
+++ b/integration/testdata/gke_loadbalancer-render/skaffold.yaml
@@ -0,0 +1,11 @@
+apiVersion: skaffold/v3alpha1
+kind: Config
+build:
+  artifacts:
+    - image: gke-loadbalancer
+      context: ./
+manifests:
+  helm:
+    releases:
+    - name: skaffold-helm
+      chartPath: loadbalancer-helm
diff --git a/integration/testdata/helm-render/.dockerignore b/integration/testdata/helm-render/.dockerignore
new file mode 100644
index 000000000..2928190a6
--- /dev/null
+++ b/integration/testdata/helm-render/.dockerignore
@@ -0,0 +1,2 @@
+skaffold-helm
+*.swp
diff --git a/integration/testdata/helm-render/Dockerfile b/integration/testdata/helm-render/Dockerfile
new file mode 100644
index 000000000..c1c9be3e2
--- /dev/null
+++ b/integration/testdata/helm-render/Dockerfile
@@ -0,0 +1,3 @@
+FROM nginx:stable
+
+COPY static /usr/share/nginx/html/
diff --git a/integration/testdata/helm-render/builds.out.json b/integration/testdata/helm-render/builds.out.json
new file mode 100644
index 000000000..f2975772a
--- /dev/null
+++ b/integration/testdata/helm-render/builds.out.json
@@ -0,0 +1,1 @@
+{"builds":[{"imageName":"gcr.io/k8s-skaffold/skaffold-helm","tag":"gcr.io/k8s-skaffold/skaffold-helm:sha256-nonsenselettersandnumbers"}]}
\ No newline at end of file
diff --git a/integration/testdata/helm-render/skaffold-helm/.helmignore b/integration/testdata/helm-render/skaffold-helm/.helmignore
new file mode 100644
index 000000000..f0c131944
--- /dev/null
+++ b/integration/testdata/helm-render/skaffold-helm/.helmignore
@@ -0,0 +1,21 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
diff --git a/integration/testdata/helm-render/skaffold-helm/Chart.yaml b/integration/testdata/helm-render/skaffold-helm/Chart.yaml
new file mode 100644
index 000000000..f4fcfadbe
--- /dev/null
+++ b/integration/testdata/helm-render/skaffold-helm/Chart.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+description: A Helm chart for Kubernetes
+name: skaffold-helm
+version: 0.1.0
diff --git a/integration/testdata/helm-render/skaffold-helm/templates/_helpers.tpl b/integration/testdata/helm-render/skaffold-helm/templates/_helpers.tpl
new file mode 100644
index 000000000..537a73998
--- /dev/null
+++ b/integration/testdata/helm-render/skaffold-helm/templates/_helpers.tpl
@@ -0,0 +1,16 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "skaffold-helm.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+*/}}
+{{- define "skaffold-helm.fullname" -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
diff --git a/integration/testdata/helm-render/skaffold-helm/templates/deployment.yaml b/integration/testdata/helm-render/skaffold-helm/templates/deployment.yaml
new file mode 100644
index 000000000..0fbca871f
--- /dev/null
+++ b/integration/testdata/helm-render/skaffold-helm/templates/deployment.yaml
@@ -0,0 +1,33 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ .Release.Name }}
+  labels:
+    app: {{ template "skaffold-helm.name" . }}
+    chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      app: {{ template "skaffold-helm.name" . }}
+      release: {{ .Release.Name }}
+  template:
+    metadata:
+      labels:
+        app: {{ template "skaffold-helm.name" . }}
+        release: {{ .Release.Name }}
+    spec:
+      containers:
+        - name: {{ .Chart.Name }}
+          image: {{ .Values.image }}
+          imagePullPolicy: {{ .Values.pullPolicy }}
+          ports:
+            - containerPort: {{ .Values.service.internalPort }}
+          resources:
+{{ toYaml .Values.resources | indent 12 }}
+    {{- if .Values.nodeSelector }}
+      nodeSelector:
+{{ toYaml .Values.nodeSelector | indent 8 }}
+    {{- end }}
diff --git a/integration/testdata/helm-render/skaffold-helm/templates/ingress.yaml b/integration/testdata/helm-render/skaffold-helm/templates/ingress.yaml
new file mode 100644
index 000000000..fe1edab05
--- /dev/null
+++ b/integration/testdata/helm-render/skaffold-helm/templates/ingress.yaml
@@ -0,0 +1,41 @@
+{{- if .Values.ingress.enabled -}}
+{{- $serviceName := include "skaffold-helm.fullname" . -}}
+{{- $servicePort := .Values.service.externalPort -}}
+apiVersion: extensions/v1beta1
+kind: Ingress
+metadata:
+  name: {{ template "skaffold-helm.fullname" . }}
+  labels:
+    app: {{ template "skaffold-helm.name" . }}
+    chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+  annotations:
+    {{- range $key, $value := .Values.ingress.annotations }}
+      {{ $key }}: {{ $value | quote }}
+    {{- end }}
+spec:
+  rules:
+    {{- if .Values.ingress.hosts }}
+    {{- range $host := .Values.ingress.hosts }}
+    - host: {{ $host }}
+      http:
+        paths:
+          - path: /
+            backend:
+              serviceName: {{ $serviceName }}
+              servicePort: {{ $servicePort }}
+    {{- end -}}
+    {{- else }}
+    - http:
+        paths:
+          - path: /
+            backend:
+              serviceName: {{ $serviceName }}
+              servicePort: {{ $servicePort }}
+    {{- end -}}
+  {{- if .Values.ingress.tls }}
+  tls:
+{{ toYaml .Values.ingress.tls | indent 4 }}
+  {{- end -}}
+{{- end -}}
diff --git a/integration/testdata/helm-render/skaffold-helm/templates/service.yaml b/integration/testdata/helm-render/skaffold-helm/templates/service.yaml
new file mode 100644
index 000000000..da7b3ef86
--- /dev/null
+++ b/integration/testdata/helm-render/skaffold-helm/templates/service.yaml
@@ -0,0 +1,19 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ template "skaffold-helm.fullname" . }}
+  labels:
+    app: {{ template "skaffold-helm.name" . }}
+    chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
+    release: {{ .Release.Name }}
+    heritage: {{ .Release.Service }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: {{ .Values.service.externalPort }}
+      targetPort: {{ .Values.service.internalPort }}
+      protocol: TCP
+      name: {{ .Values.service.name }}
+  selector:
+    app: {{ template "skaffold-helm.name" . }}
+    release: {{ .Release.Name }}
diff --git a/integration/testdata/helm-render/skaffold-helm/values.yaml b/integration/testdata/helm-render/skaffold-helm/values.yaml
new file mode 100644
index 000000000..a041f07a4
--- /dev/null
+++ b/integration/testdata/helm-render/skaffold-helm/values.yaml
@@ -0,0 +1,39 @@
+# Default values for skaffold-helm.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+replicaCount: 1
+image: gcr.io/k8s-skaffold/skaffold-helm
+# This is the helm convention on declaring images
+# image:
+#   repository: nginx
+#   tag: stable
+#   pullPolicy: IfNotPresent
+service:
+  name: nginx
+  type: ClusterIP
+  externalPort: 80
+  internalPort: 80
+ingress:
+  enabled: true
+  # Used to create an Ingress record.
+  hosts:
+    # - chart-example.local
+  annotations:
+    # kubernetes.io/ingress.class: nginx
+    # kubernetes.io/tls-acme: "true"
+  tls:
+    # Secrets must be manually created in the namespace.
+    # - secretName: chart-example-tls
+    #   hosts:
+    #     - chart-example.local
+resources: {}
+  # We usually recommend not to specify default resources and to leave this as a conscious
+  # choice for the user. This also increases chances charts run on environments with little
+  # resources, such as Minikube. If you do want to specify resources, uncomment the following
+  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+  # limits:
+  #  cpu: 100m
+  #  memory: 128Mi
+  # requests:
+  #  cpu: 100m
+  #  memory: 128Mi
diff --git a/integration/testdata/helm-render/skaffold.yaml b/integration/testdata/helm-render/skaffold.yaml
new file mode 100644
index 000000000..ed61724ea
--- /dev/null
+++ b/integration/testdata/helm-render/skaffold.yaml
@@ -0,0 +1,21 @@
+apiVersion: skaffold/v3alpha1
+kind: Config
+build:
+  tagPolicy:
+    sha256: {}
+  artifacts:
+  - image: gcr.io/k8s-skaffold/skaffold-helm
+manifests:
+  helm:
+    releases:
+    - name: skaffold-helm
+      chartPath: skaffold-helm
+profiles:
+- name: helm-render
+  manifests:
+    helm:
+      releases:
+      - name: skaffold-helm
+        chartPath: skaffold-helm
+        setValues:
+          pullPolicy: always
\ No newline at end of file
diff --git a/integration/testdata/helm-render/static/index.html b/integration/testdata/helm-render/static/index.html
new file mode 100644
index 000000000..297794387
--- /dev/null
+++ b/integration/testdata/helm-render/static/index.html
@@ -0,0 +1,9 @@
+<html lang="en">
+<head>
+    <title>Skaffold Helm</title>
+    <link rel="stylesheet" href="style.css">
+</head>
+<body>
+    <h1>Hello world!</h1>
+</body>
+</html>
diff --git a/integration/testdata/helm-render/static/style.css b/integration/testdata/helm-render/static/style.css
new file mode 100644
index 000000000..c6b88d966
--- /dev/null
+++ b/integration/testdata/helm-render/static/style.css
@@ -0,0 +1,6 @@
+h1 {
+    color: #27aedb;
+    text-align: center;
+    margin-top: 40vh;
+    font-size: 120pt;
+}
diff --git a/integration/testdata/helm/skaffold.yaml b/integration/testdata/helm/skaffold.yaml
index 0cd1afd74..3c0ce7041 100644
--- a/integration/testdata/helm/skaffold.yaml
+++ b/integration/testdata/helm/skaffold.yaml
@@ -1,4 +1,4 @@
-apiVersion: skaffold/v1beta10
+apiVersion: skaffold/v3alpha1
 kind: Config
 build:
   tagPolicy:
@@ -14,14 +14,13 @@ deploy:
       #wait: true
       #valuesFiles:
       #- helm-skaffold-values.yaml
-      values:
-        image: gcr.io/k8s-skaffold/skaffold-helm
-      #recreatePods will pass --recreate-pods to helm upgrade
-      #recreatePods: true
-      #overrides builds an override values.yaml file to run with the helm deploy
-      #overrides:
-      # some:
-      #   key: someValue
-      #setValues get appended to the helm deploy with --set.
-      #setValues:
-        #some.key: someValue
+profiles:
+- name: helm-render
+  deploy:
+    helm:
+      releases:
+      # seed test namespace in the release name.
+      - name: skaffold-helm-{{.TEST_NS}}
+        chartPath: skaffold-helm
+        setValues:
+          pullPolicy: always
\ No newline at end of file
diff --git a/pkg/skaffold/deploy/helm/helm_test.go b/pkg/skaffold/deploy/helm/helm_test.go
index 38b09a14f..d57afc088 100644
--- a/pkg/skaffold/deploy/helm/helm_test.go
+++ b/pkg/skaffold/deploy/helm/helm_test.go
@@ -33,6 +33,7 @@ import (
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/label"
 	deployutil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/util"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/helm"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/hooks"
 	ctl "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubectl"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
@@ -349,32 +350,6 @@ var (
 	version35   = `version.BuildInfo{Version:"3.5.2", GitCommit:"c4e74854886b2efe3321e185578e6db9be0a6e29", GitTreeState:"clean", GoVersion:"go1.14.15"}`
 )
 
-func TestBinVer(t *testing.T) {
-	tests := []struct {
-		description string
-		helmVersion string
-		expected    string
-		shouldErr   bool
-	}{
-		{"Helm 2.0RC1", version20rc, "2.0.0-rc.1", false},
-		{"Helm 2.15.1", version21, "2.15.1", false},
-		{"Helm 3.0b3", version30b, "3.0.0-beta.3", false},
-		{"Helm 3.0", version30, "3.0.0", false},
-		{"Helm 3.1.1", version31, "3.1.1", false},
-		{"Helm 3.5.2 without leading 'v'", version35, "3.5.2", false},
-		{"Custom Helm 3.3 build from Manjaro", "v3.3", "3.3.0", false}, // not semver compliant
-	}
-
-	for _, test := range tests {
-		testutil.Run(t, test.description, func(t *testutil.T) {
-			t.Override(&util.DefaultExecCommand, testutil.CmdRunWithOutput("helm version --client", test.helmVersion))
-			ver, err := binVer(context.Background())
-
-			t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expected, ver.String())
-		})
-	}
-}
-
 func TestNewDeployer(t *testing.T) {
 	tests := []struct {
 		description string
@@ -942,7 +917,7 @@ func TestHelmDeploy(t *testing.T) {
 
 	for _, test := range tests {
 		testutil.Run(t, test.description, func(t *testutil.T) {
-			t.Override(&writeBuildArtifactsFunc, func([]graph.Artifact) (string, func(), error) { return "TMPFILE", func() {}, nil })
+			t.Override(&helm.WriteBuildArtifacts, func([]graph.Artifact) (string, func(), error) { return "TMPFILE", func() {}, nil })
 			t.Override(&client.Client, deployutil.MockK8sClient)
 			fakeWarner := &warnings.Collect{}
 			env := test.env
@@ -952,7 +927,7 @@ func TestHelmDeploy(t *testing.T) {
 			t.Override(&warnings.Printf, fakeWarner.Warnf)
 			t.Override(&util.OSEnviron, func() []string { return env })
 			t.Override(&util.DefaultExecCommand, test.commands)
-			t.Override(&osExecutable, func() (string, error) { return "SKAFFOLD-BINARY", nil })
+			t.Override(&helm.OSExecutable, func() (string, error) { return "SKAFFOLD-BINARY", nil })
 			t.Override(&kubectx.CurrentConfig, func() (api.Config, error) {
 				return api.Config{CurrentContext: ""}, nil
 			})
@@ -1412,7 +1387,7 @@ func TestHelmRender(t *testing.T) {
 			}
 
 			t.Override(&util.OSEnviron, func() []string { return append([]string{"FOO=FOOBAR"}, test.env...) })
-			t.Override(&osExecutable, func() (string, error) { return "SKAFFOLD-BINARY", nil })
+			t.Override(&helm.OSExecutable, func() (string, error) { return "SKAFFOLD-BINARY", nil })
 			t.Override(&util.DefaultExecCommand, test.commands)
 			deployer, err := NewDeployer(context.Background(), &helmConfig{
 				namespace: test.namespace,
@@ -1429,77 +1404,6 @@ func TestHelmRender(t *testing.T) {
 	}
 }
 
-func TestWriteBuildArtifacts(t *testing.T) {
-	tests := []struct {
-		description string
-		builds      []graph.Artifact
-		result      string
-	}{
-		{
-			description: "nil",
-			builds:      nil,
-			result:      `{"builds":null}`,
-		},
-		{
-			description: "empty",
-			builds:      []graph.Artifact{},
-			result:      `{"builds":[]}`,
-		},
-		{
-			description: "multiple images with tags",
-			builds:      []graph.Artifact{{ImageName: "name", Tag: "name:tag"}, {ImageName: "name2", Tag: "name2:tag"}},
-			result:      `{"builds":[{"imageName":"name","tag":"name:tag"},{"imageName":"name2","tag":"name2:tag"}]}`,
-		},
-	}
-	for _, test := range tests {
-		testutil.Run(t, test.description, func(t *testutil.T) {
-			file, cleanup, err := writeBuildArtifacts(test.builds)
-			t.CheckError(false, err)
-			if content, err := ioutil.ReadFile(file); err != nil {
-				t.Errorf("error reading file %q: %v", file, err)
-			} else {
-				t.CheckDeepEqual(test.result, string(content))
-			}
-			cleanup()
-		})
-	}
-}
-
-func TestGenerateSkaffoldFilter(t *testing.T) {
-	tests := []struct {
-		description string
-		enableDebug bool
-		buildFile   string
-		result      []string
-	}{
-		{
-			description: "empty buildfile is skipped",
-			buildFile:   "",
-			result:      []string{"filter", "--kube-context", "kubecontext", "--kubeconfig", "kubeconfig"},
-		},
-		{
-			description: "buildfile is added",
-			buildFile:   "buildfile",
-			result:      []string{"filter", "--kube-context", "kubecontext", "--build-artifacts", "buildfile", "--kubeconfig", "kubeconfig"},
-		},
-		{
-			description: "debugging brings --debugging",
-			enableDebug: true,
-			result:      []string{"filter", "--kube-context", "kubecontext", "--debugging", "--kubeconfig", "kubeconfig"},
-		},
-	}
-	for _, test := range tests {
-		testutil.Run(t, test.description, func(t *testutil.T) {
-			t.Override(&util.DefaultExecCommand, testutil.CmdRunWithOutput("helm version --client", version31))
-			h, err := NewDeployer(context.Background(), &helmConfig{}, &label.DefaultLabeller{}, &testDeployConfig, nil)
-			h.enableDebug = test.enableDebug
-			t.RequireNoError(err)
-			result := h.generateSkaffoldFilter(test.buildFile)
-			t.CheckDeepEqual(test.result, result)
-		})
-	}
-}
-
 func TestHelmHooks(t *testing.T) {
 	tests := []struct {
 		description string
diff --git a/pkg/skaffold/deploy/helm/args_test.go b/pkg/skaffold/helm/args_test.go
similarity index 97%
rename from pkg/skaffold/deploy/helm/args_test.go
rename to pkg/skaffold/helm/args_test.go
index c73706b04..f9e8e3e63 100644
--- a/pkg/skaffold/deploy/helm/args_test.go
+++ b/pkg/skaffold/helm/args_test.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2021 The Skaffold Authors
+Copyright 2022 The Skaffold Authors
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -117,7 +117,7 @@ func TestSanitizeFilePath(t *testing.T) {
 	}
 	for _, test := range tests {
 		testutil.Run(t, test.description, func(t *testutil.T) {
-			output := sanitizeFilePath(test.input, test.isWindowsOS)
+			output := SanitizeFilePath(test.input, test.isWindowsOS)
 			t.CheckDeepEqual(test.output, output)
 		})
 	}
diff --git a/pkg/skaffold/helm/bin_test.go b/pkg/skaffold/helm/bin_test.go
new file mode 100644
index 000000000..8d96e1ec4
--- /dev/null
+++ b/pkg/skaffold/helm/bin_test.go
@@ -0,0 +1,116 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package helm
+
+import (
+	"context"
+	"testing"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
+	"github.com/GoogleContainerTools/skaffold/testutil"
+)
+
+var (
+	// Output strings to emulate different versions of Helm
+	version20rc = `Client: &version.Version{SemVer:"v2.0.0-rc.1", GitCommit:"92be174acf51e60a33287fb7011f4571eaa5cb98", GitTreeState:"clean"}\nError: cannot connect to Tiller\n`
+	version21   = `Client: &version.Version{SemVer:"v2.15.1", GitCommit:"cf1de4f8ba70eded310918a8af3a96bfe8e7683b", GitTreeState:"clean"}\nServer: &version.Version{SemVer:"v2.16.1", GitCommit:"bbdfe5e7803a12bbdf97e94cd847859890cf4050", GitTreeState:"clean"}\n`
+	version30b  = `version.BuildInfo{Version:"v3.0.0-beta.3", GitCommit:"5cb923eecbe80d1ad76399aee234717c11931d9a", GitTreeState:"clean", GoVersion:"go1.12.9"}`
+	version30   = `version.BuildInfo{Version:"v3.0.0", GitCommit:"e29ce2a54e96cd02ccfce88bee4f58bb6e2a28b6", GitTreeState:"clean", GoVersion:"go1.13.4"}`
+	version31   = `version.BuildInfo{Version:"v3.1.1", GitCommit:"afe70585407b420d0097d07b21c47dc511525ac8", GitTreeState:"clean", GoVersion:"go1.13.8"}`
+	version35   = `version.BuildInfo{Version:"3.5.2", GitCommit:"c4e74854886b2efe3321e185578e6db9be0a6e29", GitTreeState:"clean", GoVersion:"go1.14.15"}`
+)
+
+type mockClient struct {
+	enableDebug bool
+	configFile  string
+	kubeContext string
+	kubeConfig  string
+	labels      map[string]string
+	globalFlags []string
+}
+
+func (h mockClient) EnableDebug() bool         { return h.enableDebug }
+func (h mockClient) ConfigFile() string        { return h.configFile }
+func (h mockClient) KubeContext() string       { return h.kubeContext }
+func (h mockClient) KubeConfig() string        { return h.kubeConfig }
+func (h mockClient) Labels() map[string]string { return h.labels }
+func (h mockClient) GlobalFlags() []string     { return h.globalFlags }
+
+func TestBinVer(t *testing.T) {
+	tests := []struct {
+		description string
+		helmVersion string
+		expected    string
+		shouldErr   bool
+	}{
+		{"Helm 2.0RC1", version20rc, "2.0.0-rc.1", false},
+		{"Helm 2.15.1", version21, "2.15.1", false},
+		{"Helm 3.0b3", version30b, "3.0.0-beta.3", false},
+		{"Helm 3.0", version30, "3.0.0", false},
+		{"Helm 3.1.1", version31, "3.1.1", false},
+		{"Helm 3.5.2 without leading 'v'", version35, "3.5.2", false},
+		{"Custom Helm 3.3 build from Manjaro", "v3.3", "3.3.0", false}, // not semver compliant
+	}
+
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			t.Override(&util.DefaultExecCommand, testutil.CmdRunWithOutput("helm version --client", test.helmVersion))
+			ver, err := BinVer(context.Background())
+
+			t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expected, ver.String())
+		})
+	}
+}
+
+func TestGenerateSkaffoldFilter(t *testing.T) {
+	tests := []struct {
+		description string
+		enableDebug bool
+		buildFile   string
+		result      []string
+	}{
+		{
+			description: "empty buildfile is skipped",
+			buildFile:   "",
+			result:      []string{"filter", "--kube-context", "kubecontext", "--kubeconfig", "kubeconfig"},
+		},
+		{
+			description: "buildfile is added",
+			buildFile:   "buildfile",
+			result:      []string{"filter", "--kube-context", "kubecontext", "--build-artifacts", "buildfile", "--kubeconfig", "kubeconfig"},
+		},
+		{
+			description: "debugging brings --debugging",
+			enableDebug: true,
+			result:      []string{"filter", "--kube-context", "kubecontext", "--debugging", "--kubeconfig", "kubeconfig"},
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			t.Override(&util.DefaultExecCommand, testutil.CmdRunWithOutput("helm version --client", version31))
+
+			h := mockClient{
+				enableDebug: test.enableDebug,
+				kubeContext: "kubecontext",
+				kubeConfig:  "kubeconfig",
+			}
+
+			result := generateSkaffoldFilter(h, test.buildFile)
+			t.CheckDeepEqual(test.result, result)
+		})
+	}
+}
diff --git a/pkg/skaffold/helm/util_test.go b/pkg/skaffold/helm/util_test.go
new file mode 100644
index 000000000..ba6053a85
--- /dev/null
+++ b/pkg/skaffold/helm/util_test.go
@@ -0,0 +1,61 @@
+/*
+Copyright 2022 The Skaffold Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package helm
+
+import (
+	"io/ioutil"
+	"testing"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/testutil"
+)
+
+func TestWriteBuildArtifacts(t *testing.T) {
+	tests := []struct {
+		description string
+		builds      []graph.Artifact
+		result      string
+	}{
+		{
+			description: "nil",
+			builds:      nil,
+			result:      `{"builds":null}`,
+		},
+		{
+			description: "empty",
+			builds:      []graph.Artifact{},
+			result:      `{"builds":[]}`,
+		},
+		{
+			description: "multiple images with tags",
+			builds:      []graph.Artifact{{ImageName: "name", Tag: "name:tag"}, {ImageName: "name2", Tag: "name2:tag"}},
+			result:      `{"builds":[{"imageName":"name","tag":"name:tag"},{"imageName":"name2","tag":"name2:tag"}]}`,
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			file, cleanup, err := WriteBuildArtifacts(test.builds)
+			t.CheckError(false, err)
+			if content, err := ioutil.ReadFile(file); err != nil {
+				t.Errorf("error reading file %q: %v", file, err)
+			} else {
+				t.CheckDeepEqual(test.result, string(content))
+			}
+			cleanup()
+		})
+	}
+}
diff --git a/pkg/skaffold/render/renderer/kpt/kpt_test.go b/pkg/skaffold/render/renderer/kpt/kpt_test.go
index 829d1468c..fe259fc29 100644
--- a/pkg/skaffold/render/renderer/kpt/kpt_test.go
+++ b/pkg/skaffold/render/renderer/kpt/kpt_test.go
@@ -24,6 +24,7 @@ import (
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render/kptfile"
 	runcontext "github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext/v2"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
@@ -170,8 +171,8 @@ pipeline:
 				Write(filepath.Join(constants.DefaultHydrationDir, kptfile.KptFileName), test.originalKptfile).
 				Touch("empty.ignored").
 				Chdir()
-			mockCfg := mockConfig{
-				workingDir: tmpDirObj.Root(),
+			mockCfg := render.MockConfig{
+				WorkingDir: tmpDirObj.Root(),
 			}
 			r, err := New(mockCfg, test.renderConfig, filepath.Join(tmpDirObj.Root(), constants.DefaultHydrationDir), map[string]string{})
 			t.CheckNoError(err)
@@ -241,8 +242,8 @@ inventory:
 				Generate: latest.Generate{RawK8s: []string{"pod.yaml"}},
 				Validate: &[]latest.Validator{{Name: "kubeval"}},
 			}
-			mockCfg := mockConfig{
-				workingDir: tmpDirObj.Root(),
+			mockCfg := render.MockConfig{
+				WorkingDir: tmpDirObj.Root(),
 			}
 			r, err := New(mockCfg, renderConfig, filepath.Join(tmpDirObj.Root(), constants.DefaultHydrationDir), map[string]string{})
 			t.CheckNoError(err)
@@ -260,12 +261,3 @@ inventory:
 		})
 	}
 }
-
-type mockConfig struct {
-	workingDir string
-}
-
-func (mc mockConfig) GetWorkingDir() string                       { return mc.workingDir }
-func (mc mockConfig) TransformAllowList() []latest.ResourceFilter { return nil }
-func (mc mockConfig) TransformDenyList() []latest.ResourceFilter  { return nil }
-func (mc mockConfig) TransformRulesFile() string                  { return "" }
diff --git a/pkg/skaffold/render/renderer/kubectl/kubectl_test.go b/pkg/skaffold/render/renderer/kubectl/kubectl_test.go
index 1ac697ac8..f33ea7c2b 100644
--- a/pkg/skaffold/render/renderer/kubectl/kubectl_test.go
+++ b/pkg/skaffold/render/renderer/kubectl/kubectl_test.go
@@ -21,6 +21,7 @@ import (
 	"testing"
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/render"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
 	"github.com/GoogleContainerTools/skaffold/testutil"
 )
@@ -86,7 +87,7 @@ func TestRender(t *testing.T) {
 			tmpDirObj.Write("pod.yaml", podYaml).
 				Touch("empty.ignored").
 				Chdir()
-			mockCfg := mockConfig{workingDir: tmpDirObj.Root()}
+			mockCfg := render.MockConfig{WorkingDir: tmpDirObj.Root()}
 			r, err := New(mockCfg, test.renderConfig, test.labels)
 			t.CheckNoError(err)
 			var b bytes.Buffer
@@ -97,12 +98,3 @@ func TestRender(t *testing.T) {
 		})
 	}
 }
-
-type mockConfig struct {
-	workingDir string
-}
-
-func (mc mockConfig) GetWorkingDir() string                       { return mc.workingDir }
-func (mc mockConfig) TransformAllowList() []latest.ResourceFilter { return nil }
-func (mc mockConfig) TransformDenyList() []latest.ResourceFilter  { return nil }
-func (mc mockConfig) TransformRulesFile() string                  { return "" }
diff --git a/pkg/skaffold/schema/versions_test.go b/pkg/skaffold/schema/versions_test.go
index eb8a096b1..eca275472 100644
--- a/pkg/skaffold/schema/versions_test.go
+++ b/pkg/skaffold/schema/versions_test.go
@@ -211,7 +211,6 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 							},
 						},
 					})),
-				withKubectlDeploy("k8s/*.yaml"),
 				withLogsPrefix("container"),
 			)},
 		},
@@ -223,7 +222,6 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 				withLocalBuild(
 					withGitTagger(),
 				),
-				withKubectlDeploy("k8s/*.yaml"),
 				withLogsPrefix("container"),
 			)},
 		},
@@ -235,7 +233,6 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 				withLocalBuild(
 					withGitTagger(),
 				),
-				withKubectlDeploy("k8s/*.yaml"),
 				withLogsPrefix("container"),
 			)},
 		},
@@ -245,10 +242,11 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 			config:      []string{simpleConfig},
 			expected: []util.VersionedConfig{config(
 				withLocalBuild(
+					withLocalBuildConcurrency(&constants.DefaultLocalConcurrency),
 					withGitTagger(),
 					withDockerArtifact("example", ".", "Dockerfile"),
 				),
-				withKubectlDeploy("k8s/*.yaml"),
+				withKubectlDeploy(),
 				withLogsPrefix("container"),
 			)},
 		},
@@ -262,8 +260,9 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 					withDockerArtifact("image1", "./examples/app1", "Dockerfile.dev"),
 					withBazelArtifact(),
 				),
-				withKubectlDeploy("dep.yaml", "svc.yaml"),
+				withRawK8s("dep.yaml", "svc.yaml"),
 				withLogsPrefix("container"),
+				withKubectlDeploy(),
 			)},
 		},
 		{
@@ -276,8 +275,9 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 					withDockerArtifact("image1", "./examples/app1", "Dockerfile.dev"),
 					withBazelArtifact(),
 				),
-				withKubectlDeploy("dep.yaml", "svc.yaml"),
+				withRawK8s("dep.yaml", "svc.yaml"),
 				withLogsPrefix("container"),
+				withKubectlDeploy(),
 			)},
 		},
 		{
@@ -290,15 +290,15 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 					withDockerArtifact("image1", "./examples/app1", "Dockerfile.dev"),
 					withBazelArtifact(),
 				),
-				withKubectlDeploy("dep.yaml", "svc.yaml"),
+				withRawK8s("dep.yaml", "svc.yaml"),
 				withLogsPrefix("container"),
+				withKubectlDeploy(),
 			), config(
 				withClusterBuild("secret-name", "/secret", "nskaniko", "/secret.json", "120m",
 					withGitTagger(),
 					withDockerConfig("config-name", "/kaniko/.docker"),
 					withKanikoArtifact(),
 				),
-				withKubectlDeploy("k8s/*.yaml"),
 				withLogsPrefix("container"),
 			)},
 		},
@@ -312,15 +312,15 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 					withDockerArtifact("image1", "./examples/app1", "Dockerfile.dev"),
 					withBazelArtifact(),
 				),
-				withKubectlDeploy("dep.yaml", "svc.yaml"),
+				withRawK8s("dep.yaml", "svc.yaml"),
 				withLogsPrefix("container"),
+				withKubectlDeploy(),
 			), config(
 				withClusterBuild("secret-name", "/secret", "nskaniko", "/secret.json", "120m",
 					withGitTagger(),
 					withDockerConfig("config-name", "/kaniko/.docker"),
 					withKanikoArtifact(),
 				),
-				withKubectlDeploy("k8s/*.yaml"),
 				withLogsPrefix("container"),
 			)},
 		},
@@ -333,7 +333,6 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 					withGitTagger(),
 					withKanikoArtifact(),
 				),
-				withKubectlDeploy("k8s/*.yaml"),
 				withLogsPrefix("container"),
 			)},
 		},
@@ -347,7 +346,6 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 					withDockerConfig("config-name", "/kaniko/.docker"),
 					withKanikoArtifact(),
 				),
-				withKubectlDeploy("k8s/*.yaml"),
 				withLogsPrefix("container"),
 			)},
 		},
@@ -383,7 +381,6 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 				withLocalBuild(
 					withGitTagger(),
 				),
-				withKubectlDeploy("k8s/*.yaml"),
 				withStatusCheckDeadline(10),
 				withLogsPrefix("container"),
 			)},
@@ -396,7 +393,6 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 				withLocalBuild(
 					withGitTagger(),
 				),
-				withKubectlDeploy("k8s/*.yaml"),
 				withLogsPrefix("none"),
 			)},
 		},
@@ -410,11 +406,6 @@ func TestParseConfigAndUpgrade(t *testing.T) {
 
 			cfgs, err := ParseConfigAndUpgrade(tmpDir.Path("skaffold.yaml"))
 			for _, cfg := range cfgs {
-				if _, ok := SchemaVersionsV2.Find(test.apiVersion[0]); !ok {
-					// TODO: the "defaults" package below only accept latestV2 schema.
-					t.SkipNow()
-				}
-
 				err := defaults.Set(cfg.(*latest.SkaffoldConfig))
 				t.CheckNoError(err)
 			}
@@ -447,7 +438,6 @@ func TestMarshalConfig(t *testing.T) {
 							},
 						},
 					})),
-				withKubectlDeploy("k8s/*.yaml"),
 				withLogsPrefix("container"),
 			),
 		},
@@ -550,6 +540,12 @@ func withKubectlDeploy(manifests ...string) func(*latest.SkaffoldConfig) {
 	}
 }
 
+func withRawK8s(manifests ...string) func(*latest.SkaffoldConfig) {
+	return func(cfg *latest.SkaffoldConfig) {
+		cfg.Render.RawK8s = manifests
+	}
+}
+
 func withKubeContext(kubeContext string) func(*latest.SkaffoldConfig) {
 	return func(cfg *latest.SkaffoldConfig) {
 		cfg.Deploy = latest.DeployConfig{
@@ -632,6 +628,10 @@ func withVolume(v v1.Volume) func(*latest.BuildConfig) {
 	}
 }
 
+func withLocalBuildConcurrency(concurrency *int) func(*latest.BuildConfig) {
+	return func(cfg *latest.BuildConfig) { cfg.LocalBuild.Concurrency = concurrency }
+}
+
 func withTagPolicy(tagPolicy latest.TagPolicy) func(*latest.BuildConfig) {
 	return func(cfg *latest.BuildConfig) { cfg.TagPolicy = tagPolicy }
 }