Repository: GoogleContainerTools/skaffold
Pull request #8649 (2023-04-06 17:49:08)

fix: create new docker network only when the --docker-network flag is not set
**Description**
Change behaviour so we don't create a new docker network when `skaffold verify` is run with the `--docker-network` flag, instead we assume the docker network with the given name already exists.

codecov[bot]: ## [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) Report
> Merging [#8649](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (463e591) 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 `6.33%`.
> The diff coverage is `54.47%`.

```diff
@@            Coverage Diff             @@
##             main    #8649      +/-   ##
==========================================
- Coverage   70.48%   64.15%   -6.33%     
==========================================
  Files         515      615     +100     
  Lines       23150    30800    +7650     
==========================================
+ Hits        16317    19761    +3444     
- Misses       5776     9558    +3782     
- Partials     1057     1481     +424     
```


| [Impacted Files](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?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/completion.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY29tcGxldGlvbi5nbw==) | `13.04% <0.00%> (-1.25%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/config/list.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY29uZmlnL2xpc3QuZ28=) | `65.21% <ø> (ø)` | |
| [cmd/skaffold/app/cmd/config/set.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY29uZmlnL3NldC5nbw==) | `88.72% <ø> (ø)` | |
| [cmd/skaffold/app/cmd/config/util.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY29uZmlnL3V0aWwuZ28=) | `54.28% <ø> (ø)` | |
| [cmd/skaffold/app/cmd/credits.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY3JlZGl0cy5nbw==) | `100.00% <ø> (ø)` | |
| [cmd/skaffold/app/cmd/credits/export.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?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/8649?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZGVwbG95Lmdv) | `40.90% <0.00%> (-12.94%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/generate\_pipeline.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZ2VuZXJhdGVfcGlwZWxpbmUuZ28=) | `60.00% <ø> (ø)` | |
| [cmd/skaffold/app/cmd/inspect\_modules.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvaW5zcGVjdF9tb2R1bGVzLmdv) | `65.00% <ø> (ø)` | |
| [cmd/skaffold/app/cmd/inspect\_profiles.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvaW5zcGVjdF9wcm9maWxlcy5nbw==) | `66.66% <ø> (ø)` | |
| ... and [39 more](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | |

... and [395 files with indirect coverage changes](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/8649/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools)

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools)


Patch:
diff --git a/cmd/skaffold/app/cmd/flags.go b/cmd/skaffold/app/cmd/flags.go
index 53cdde1db..8ff26be39 100644
--- a/cmd/skaffold/app/cmd/flags.go
+++ b/cmd/skaffold/app/cmd/flags.go
@@ -675,7 +675,7 @@ The build result from a previous 'skaffold build --file-output' run can be used
 	{
 		Name:          "docker-network",
 		Shorthand:     "",
-		Usage:         "Run verify tests in the specified docker network",
+		Usage:         "Name of an existing docker network to use when running the verify tests. If not specified, Skaffold will create a new network to use of the form 'skaffold-network-<uuid>'",
 		Value:         &opts.VerifyDockerNetwork,
 		DefValue:      "",
 		FlagAddMethod: "StringVar",
diff --git a/cmd/skaffold/app/cmd/inspect.go b/cmd/skaffold/app/cmd/inspect.go
index 8a6de4cf6..edfb1a6b3 100644
--- a/cmd/skaffold/app/cmd/inspect.go
+++ b/cmd/skaffold/app/cmd/inspect.go
@@ -43,7 +43,8 @@ func NewCmdInspect() *cobra.Command {
 		WithDescription("Helper commands for Cloud Code IDEs to interact with and modify skaffold configuration files.").
 		WithPersistentFlagAdder(cmdInspectFlags).
 		Hidden().
-		WithCommands(cmdModules(), cmdProfiles(), cmdBuildEnv(), cmdTests(), cmdNamespaces(), cmdJobManifestPaths())
+		WithCommands(cmdModules(), cmdProfiles(), cmdBuildEnv(), cmdTests(), cmdNamespaces(),
+			cmdJobManifestPaths(), cmdExecutionModes())
 }
 
 func cmdInspectFlags(f *pflag.FlagSet) {
diff --git a/cmd/skaffold/app/cmd/inspect_execution_modes.go b/cmd/skaffold/app/cmd/inspect_execution_modes.go
new file mode 100644
index 000000000..66caa15a0
--- /dev/null
+++ b/cmd/skaffold/app/cmd/inspect_execution_modes.go
@@ -0,0 +1,55 @@
+/*
+Copyright 2021 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 cmd
+
+import (
+	"context"
+	"io"
+
+	"github.com/spf13/cobra"
+
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/inspect"
+	executionModes "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/inspect/executionModes"
+)
+
+func cmdExecutionModes() *cobra.Command {
+	return NewCmd("executionModes").
+		WithDescription("View skaffold executionMode information defined in the specified skaffold configuration").
+		WithCommands(cmdExecutionModesList())
+}
+
+func cmdExecutionModesList() *cobra.Command {
+	return NewCmd("list").
+		WithExample("Get list of executionModes", "inspect executionModes list --format json").
+		WithExample("Get list of executionModes targeting a specific configuration", "inspect executionModes list --profile local --format json").
+		WithDescription("Print the list of executionModes that would be run for a given configuration (default skaffold configuration, specific module, specific profile, etc).").
+		WithArgs(func(cmd *cobra.Command, args []string) error {
+			// skaffold inspect executionModes list take in an optional list of customActions as well
+			return nil
+		}, listExecutionModes)
+}
+
+func listExecutionModes(ctx context.Context, out io.Writer, args []string) error {
+	return executionModes.PrintExecutionModesList(ctx, out, inspect.Options{
+		Filename:          inspectFlags.filename,
+		RepoCacheDir:      inspectFlags.repoCacheDir,
+		OutFormat:         inspectFlags.outFormat,
+		Modules:           inspectFlags.modules,
+		Profiles:          inspectFlags.profiles,
+		PropagateProfiles: inspectFlags.propagateProfiles,
+	}, args)
+}
diff --git a/docs-v2/content/en/docs/environment/templating.md b/docs-v2/content/en/docs/environment/templating.md
index a63bd58db..c78308d5d 100644
--- a/docs-v2/content/en/docs/environment/templating.md
+++ b/docs-v2/content/en/docs/environment/templating.md
@@ -15,14 +15,15 @@ will be `gcr.io/k8s-skaffold/example:v1`.
 
 List of fields that support templating:
 
-* `build.artifacts.[].docker.buildArgs` (see [builders]({{< relref "/docs/builders" >}}))
-* `build.artifacts.[].ko.{env,flags,labels,ldflags}` (see [`ko` builder]({{< relref "/docs/builders/builder-types/ko" >}}))
+* `build.artifacts[].docker.buildArgs` (see [builders]({{< relref "/docs/builders" >}}))
+* `build.artifacts[].ko.{env,flags,labels,ldflags}` (see [`ko` builder]({{< relref "/docs/builders/builder-types/ko" >}}))
 * `build.tagPolicy.envTemplate.template` (see [envTemplate tagger]({{< relref "/docs/taggers#envtemplate-using-values-of-environment-variables-as-tags)" >}}))
-* `deploy.helm.releases.setValueTemplates` (see [Deploying with helm]({{< relref "/docs/deployers#deploying-with-helm)" >}}))
-* `deploy.helm.releases.name` (see [Deploying with helm]({{< relref "/docs/deployers#deploying-with-helm)" >}}))
-* `deploy.helm.releases.namespace` (see [Deploying with helm]({{< relref "/docs/deployers#deploying-with-helm)" >}}))
+* `deploy.helm.releases[].chartPath` (see [Deploying with helm]({{< relref "/docs/deployers#deploying-with-helm)" >}}))
+* `deploy.helm.releases[].name` (see [Deploying with helm]({{< relref "/docs/deployers#deploying-with-helm)" >}}))
+* `deploy.helm.releases[].namespace` (see [Deploying with helm]({{< relref "/docs/deployers#deploying-with-helm)" >}}))
 * `deploy.helm.releases[].repo` (see [Deploying with helm]({{< relref "/docs/deployers#deploying-with-helm)" >}}))
-* `deploy.helm.releases.version` (see [Deploying with helm]({{< relref "/docs/deployers#deploying-with-helm)" >}}))
+* `deploy.helm.releases[].setValueTemplates` (see [Deploying with helm]({{< relref "/docs/deployers#deploying-with-helm)" >}}))
+* `deploy.helm.releases[].version` (see [Deploying with helm]({{< relref "/docs/deployers#deploying-with-helm)" >}}))
 * `deploy.kubectl.defaultNamespace`
 * `deploy.kustomize.defaultNamespace`
 * `manifests.kustomize.paths.[]`
diff --git a/docs-v2/content/en/docs/references/cli/_index.md b/docs-v2/content/en/docs/references/cli/_index.md
index 29beaf86a..2fa75ead1 100644
--- a/docs-v2/content/en/docs/references/cli/_index.md
+++ b/docs-v2/content/en/docs/references/cli/_index.md
@@ -1355,7 +1355,7 @@ Options:
 }
 The build result from a previous 'skaffold build --file-output' run can be used here
   -d, --default-repo='': Default repository value (overrides global config)
-      --docker-network='': Run verify tests in the specified docker network
+      --docker-network='': Name of an existing docker network to use when running the verify tests. If not specified, Skaffold will create a new network to use of the form 'skaffold-network-<uuid>'
       --env-file='': File containing env var key-value pairs that will be set in all verify container envs
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
   -m, --module=[]: Filter Skaffold configs to only the provided named modules
diff --git a/docs-v2/content/en/docs/renderers/_index.md b/docs-v2/content/en/docs/renderers/_index.md
index fc11b7a43..5cf62e611 100644
--- a/docs-v2/content/en/docs/renderers/_index.md
+++ b/docs-v2/content/en/docs/renderers/_index.md
@@ -7,7 +7,7 @@ aliases: [/docs/how-tos/renderers, /docs/pipeline-stages/renderers/]
 no_list: true
 ---
 
-When Skaffold renders your application to Kubernetes, it goes throught the following process:
+When Skaffold renders your application to Kubernetes, it goes through the following process:
 
 * the Skaffold renderer _renders_ the final Kubernetes manifests: Skaffold replaces untagged image names in the Kubernetes manifests with the final tagged image names.
 It also might go through the extra intermediate step of expanding templates (for helm) or calculating overlays (for kustomize).
@@ -28,4 +28,4 @@ a combination of multiple renderers in a single project.
 
 For a detailed discussion on Skaffold configuration, see
 [Skaffold Concepts]({{< relref "/docs/design/config.md" >}}) and
-[skaffold.yaml References]({{< relref "/docs/references/yaml" >}}).
\ No newline at end of file
+[skaffold.yaml References]({{< relref "/docs/references/yaml" >}}).
diff --git a/pkg/skaffold/actions/action.go b/pkg/skaffold/actions/action.go
new file mode 100644
index 000000000..99f6adc3d
--- /dev/null
+++ b/pkg/skaffold/actions/action.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2023 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 actions
+
+import (
+	"context"
+	"io"
+	"time"
+)
+
+type Action struct {
+	name     string
+	timeout  time.Duration
+	tasks    []Task
+	execFunc ExecStrategy
+}
+
+func NewAction(name string, timeoutSeconds int, isFailFast bool, tasks []Task) *Action {
+	return &Action{
+		name:     name,
+		timeout:  time.Duration(timeoutSeconds * int(time.Second)),
+		execFunc: getExecFunc(isFailFast),
+		tasks:    tasks,
+	}
+}
+
+func (a Action) Name() string {
+	return a.name
+}
+
+func (a Action) Exec(ctx context.Context, out io.Writer) error {
+	ctxTimeout := ctx
+	cancel := func() {}
+
+	if a.timeout.Seconds() > 0 {
+		ctxTimeout, cancel = context.WithTimeout(ctx, a.timeout)
+	}
+
+	defer cancel()
+	return a.execFunc(ctxTimeout, out, a.tasks)
+}
+
+func (a Action) Cleanup(ctx context.Context, out io.Writer) error {
+	for _, t := range a.tasks {
+		if err := t.Cleanup(ctx, out); err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/pkg/skaffold/actions/runner.go b/pkg/skaffold/actions/runner.go
new file mode 100644
index 000000000..9422ab284
--- /dev/null
+++ b/pkg/skaffold/actions/runner.go
@@ -0,0 +1,160 @@
+/*
+Copyright 2023 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 actions
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"math"
+
+	"github.com/fatih/semgroup"
+	"golang.org/x/sync/errgroup"
+
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/output/log"
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/runner"
+)
+
+var _ runner.ActionsRunner = Runner{}
+
+type Runner struct {
+	// Map to access the associated Exec environment of a given action.
+	execEnvByAction map[string]ExecEnv
+
+	// List with all the created exec environments; this helps to execute all the actions in the same order always.
+	orderedExecEnvs []ExecEnv
+
+	// Map to access the list of associated actions of a given Execution environment.
+	acsByExecEnv map[ExecEnv][]string
+}
+
+func NewRunner(execEnvByAction map[string]ExecEnv, orderedExecEnvs []ExecEnv, acsByExecEnv map[ExecEnv][]string) Runner {
+	return Runner{execEnvByAction, orderedExecEnvs, acsByExecEnv}
+}
+
+func (r Runner) ExecAll(ctx context.Context, out io.Writer, allbuilds []graph.Artifact) error {
+	acs, err := r.prepareAllActions(ctx, out, allbuilds)
+	if err != nil {
+		return err
+	}
+
+	defer r.cleanup(ctx, out, acs, r.orderedExecEnvs)
+	return execWithFailingSafe(ctx, out, acs)
+}
+
+func (r Runner) Exec(ctx context.Context, out io.Writer, allbuilds []graph.Artifact, aName string) error {
+	execEnv, found := r.execEnvByAction[aName]
+	if !found {
+		return fmt.Errorf("custom action %v not found", aName)
+	}
+
+	acs, err := execEnv.PrepareActions(ctx, out, allbuilds, []string{aName})
+	if err != nil {
+		return err
+	}
+
+	// We expect only one action to be created.
+	if len(acs) != 1 {
+		return fmt.Errorf("failed to create %v action", aName)
+	}
+
+	a := acs[0]
+
+	defer r.cleanup(context.TODO(), out, []Task{a}, []ExecEnv{execEnv})
+	return a.Exec(ctx, out)
+}
+
+func (r Runner) prepareAllActions(ctx context.Context, out io.Writer, allbuilds []graph.Artifact) ([]Task, error) {
+	preparedAcs := []Task{}
+	for _, execEnv := range r.orderedExecEnvs {
+		acsNames := r.acsByExecEnv[execEnv]
+		acs, err := execEnv.PrepareActions(ctx, out, allbuilds, acsNames)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, a := range acs {
+			preparedAcs = append(preparedAcs, a)
+		}
+	}
+
+	return preparedAcs, nil
+}
+
+func (r Runner) cleanup(ctx context.Context, out io.Writer, ts []Task, execEnvs []ExecEnv) {
+	log.Entry(ctx).Debugf("Starting execution cleanup")
+	for _, t := range ts {
+		if t == nil {
+			continue
+		}
+		if err := t.Cleanup(ctx, out); err != nil {
+			// TODO(renzor): known issue related with Docker client deleting containers + prune will cause some
+			// warnings here. We need to fix https://github.com/GoogleContainerTools/skaffold/issues/8605
+			log.Entry(ctx).Warnf("%v cleanup error:%v", t.Name(), err)
+		}
+	}
+	for _, execEnv := range execEnvs {
+		if execEnv == nil {
+			continue
+		}
+		if err := execEnv.Cleanup(ctx, out); err != nil {
+			log.Entry(ctx).Warnf("Execution environment cleanup error:%v", err)
+		}
+	}
+}
+
+func getExecFunc(isFailFast bool) ExecStrategy {
+	if isFailFast {
+		return execWithFailingFast
+	}
+	return execWithFailingSafe
+}
+
+func execWithFailingFast(ctx context.Context, out io.Writer, ts []Task) error {
+	g, gCtx := errgroup.WithContext(ctx)
+
+	for _, t := range ts {
+		t := t
+		g.Go(func() error {
+			return execAndLog(gCtx, out, t)
+		})
+	}
+
+	return g.Wait()
+}
+
+func execWithFailingSafe(ctx context.Context, out io.Writer, ts []Task) error {
+	const maxWorkers = math.MaxInt64
+	g := semgroup.NewGroup(context.Background(), maxWorkers)
+
+	for _, t := range ts {
+		t := t
+		g.Go(func() error {
+			return execAndLog(ctx, out, t)
+		})
+	}
+
+	return g.Wait()
+}
+
+func execAndLog(ctx context.Context, out io.Writer, t Task) error {
+	log.Entry(ctx).Debugf("Starting execution for %v", t.Name())
+	err := t.Exec(ctx, out)
+	log.Entry(ctx).Debugf("Execution finished for %v", t.Name())
+	return err
+}
diff --git a/pkg/skaffold/actions/types.go b/pkg/skaffold/actions/types.go
new file mode 100644
index 000000000..64886b4e4
--- /dev/null
+++ b/pkg/skaffold/actions/types.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2023 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 actions
+
+import (
+	"context"
+	"io"
+
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/graph"
+)
+
+// ExecStrategy represents the functions to use to execute a list of tasks.
+type ExecStrategy func(ctx context.Context, out io.Writer, ts []Task) error
+
+// Task represents a single container from a custom action.
+type Task interface {
+	// Name is the unique name of the tasks across all other tasks and actions.
+	Name() string
+
+	// Exec triggers the execution of the task.
+	Exec(ctx context.Context, out io.Writer) error
+
+	// Cleanup frees the resources created by the task to execute itself.
+	Cleanup(ctx context.Context, out io.Writer) error
+}
+
+// ExecEnv represents every execution mode available for custom actions.
+type ExecEnv interface {
+	// PrepareActions creates the shared resources needed for the actions of an
+	// specific execution mode. It returns the actions of this execution mode.
+	PrepareActions(ctx context.Context, out io.Writer, allbuilds []graph.Artifact, acsNames []string) ([]Action, error)
+
+	// Cleanup frees the shared resources created during PrepareActions.
+	Cleanup(ctx context.Context, out io.Writer) error
+}
diff --git a/pkg/skaffold/inspect/executionModes/list.go b/pkg/skaffold/inspect/executionModes/list.go
new file mode 100644
index 000000000..011129644
--- /dev/null
+++ b/pkg/skaffold/inspect/executionModes/list.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2021 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 inspect
+
+import (
+	"context"
+	"io"
+
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/config"
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/inspect"
+)
+
+type executionModeList struct {
+	VerifyExecutionModes        map[string]string `json:"verifyExecutionModes"`
+	CustomActionsExecutionModes map[string]string `json:"customActionsExecutionModes"`
+}
+
+func PrintExecutionModesList(ctx context.Context, out io.Writer, opts inspect.Options, customActions []string) error {
+	formatter := inspect.OutputFormatter(out, opts.OutFormat)
+	cfgs, err := inspect.GetConfigSet(ctx, config.SkaffoldOptions{
+		ConfigurationFile:   opts.Filename,
+		ConfigurationFilter: opts.Modules,
+		RepoCacheDir:        opts.RepoCacheDir,
+		Profiles:            opts.Profiles,
+		PropagateProfiles:   opts.PropagateProfiles,
+	})
+	if err != nil {
+		formatter.WriteErr(err)
+		return err
+	}
+
+	l := &executionModeList{
+		VerifyExecutionModes:        map[string]string{},
+		CustomActionsExecutionModes: map[string]string{},
+	}
+
+	for _, c := range cfgs {
+		for _, tc := range c.Verify {
+			if tc.ExecutionMode.KubernetesClusterExecutionMode != nil {
+				l.VerifyExecutionModes[tc.Name] = "kubernetesCluster"
+			} else {
+				l.VerifyExecutionModes[tc.Name] = "local"
+			}
+		}
+	}
+	// TODO(#8572) add similar logic for customAction schema fields when they are complete
+
+	return formatter.Write(l)
+}
diff --git a/pkg/skaffold/kubernetes/manifest/visitor.go b/pkg/skaffold/kubernetes/manifest/visitor.go
index 81fca664d..8103530e5 100644
--- a/pkg/skaffold/kubernetes/manifest/visitor.go
+++ b/pkg/skaffold/kubernetes/manifest/visitor.go
@@ -194,6 +194,10 @@ var TransformDenylist = map[apimachinery.GroupKind]latest.ResourceFilter{
 		GroupKind: "StatefulSet.apps",
 		Labels:    []string{".spec.volumeClaimTemplates.metadata.labels"},
 	},
+	{Group: "batch", Kind: "Job"}: {
+		GroupKind: "Job.batch",
+		Labels:    []string{".spec.template.metadata.labels"},
+	},
 }
 
 // FieldVisitor represents the aggregation/transformation that should be performed on each traversed field.
diff --git a/pkg/skaffold/kubernetes/status/resource/deployment.go b/pkg/skaffold/kubernetes/status/resource/deployment.go
index e766cdeaf..15238ad58 100644
--- a/pkg/skaffold/kubernetes/status/resource/deployment.go
+++ b/pkg/skaffold/kubernetes/status/resource/deployment.go
@@ -24,12 +24,16 @@ import (
 	"strings"
 	"time"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+
 	"github.com/GoogleContainerTools/skaffold/v2/pkg/diag"
 	"github.com/GoogleContainerTools/skaffold/v2/pkg/diag/validator"
 	sErrors "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/errors"
 	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/event"
 	eventV2 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/event/v2"
 	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/kubectl"
+	kubernetesclient "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/kubernetes/client"
 	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/output/log"
 	"github.com/GoogleContainerTools/skaffold/v2/proto/v1"
 	protoV2 "github.com/GoogleContainerTools/skaffold/v2/proto/v2"
@@ -210,11 +214,42 @@ func (r *Resource) checkRolloutStatus(ctx context.Context, cfg kubectl.Config) *
 	if ctx.Err() != nil {
 		return &proto.ActionableErr{ErrCode: proto.StatusCode_STATUSCHECK_USER_CANCELLED}
 	}
-
 	details := r.cleanupStatus(string(b))
+	if err != nil {
+		return parseKubectlRolloutError(details, r.deadline, r.tolerateFailures, err)
+	}
+
+	client, cErr := kubernetesclient.Client(cfg.GetKubeConfig())
+	if cErr != nil {
+		log.Entry(ctx).Debugf("error attempting to create kubernetes client for k8s event listing: %s", err)
+	} else {
+		err = checkK8sEventsForPodFailedCreateEvent(ctx, client, r.namespace, r.name)
+	}
+
+	// additional logic added here which checks kubernetes events to see if skaffold managed pod has a FailedCreatEvent
+	// this can be raised by an admission controller and if we don't error here, skaffold will wait for the pod to come up
+	// indefinitely even thought the admission controller has denied it
 	return parseKubectlRolloutError(details, r.deadline, r.tolerateFailures, err)
 }
 
+func checkK8sEventsForPodFailedCreateEvent(ctx context.Context, client kubernetes.Interface, namespace string, deploymentName string) error {
+	// Create a watcher for events
+	eventList, err := client.CoreV1().Events(namespace).List(ctx, metav1.ListOptions{})
+	if err != nil {
+		return fmt.Errorf("error attempting to list kubernetes events in namespace: %s, %w", namespace, err)
+	}
+
+	for _, event := range eventList.Items {
+		if event.Reason == "FailedCreate" {
+			if strings.HasPrefix(event.InvolvedObject.Name, deploymentName+"-") {
+				errMsg := fmt.Sprintf("Failed to create Pod for Deployment %s: %s\n", deploymentName, event.Message)
+				return fmt.Errorf(errMsg)
+			}
+		}
+	}
+	return nil
+}
+
 func (r *Resource) CheckStatus(ctx context.Context, cfg kubectl.Config) {
 	var ae *proto.ActionableErr
 	switch r.rType {
diff --git a/pkg/skaffold/render/renderer/helm/helm.go b/pkg/skaffold/render/renderer/helm/helm.go
index f473dd29b..747b77de7 100644
--- a/pkg/skaffold/render/renderer/helm/helm.go
+++ b/pkg/skaffold/render/renderer/helm/helm.go
@@ -125,6 +125,11 @@ func (h Helm) generateHelmManifests(ctx context.Context, builds []graph.Artifact
 			return nil, helm.UserErr(fmt.Sprintf("cannot expand release name %q", release.Name), err)
 		}
 
+		release.ChartPath, err = sUtil.ExpandEnvTemplateOrFail(release.ChartPath, nil)
+		if err != nil {
+			return nil, helm.UserErr(fmt.Sprintf("cannot expand chart path %q", release.ChartPath), err)
+		}
+
 		args := []string{"template", releaseName, helm.ChartSource(release)}
 		args = append(args, postRendererArgs...)
 		if release.Packaged == nil && release.Version != "" {
diff --git a/pkg/skaffold/runner/actions_runner.go b/pkg/skaffold/runner/actions_runner.go
new file mode 100644
index 000000000..084b52cfe
--- /dev/null
+++ b/pkg/skaffold/runner/actions_runner.go
@@ -0,0 +1,30 @@
+/*
+Copyright 2021 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 runner
+
+import (
+	"context"
+	"io"
+
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/graph"
+)
+
+// ActionsRunner defines the API used to run custom actions.
+type ActionsRunner interface {
+	Exec(ctx context.Context, out io.Writer, allbuilds []graph.Artifact, action string) error
+	ExecAll(ctx context.Context, out io.Writer, allbuilds []graph.Artifact) error
+}
diff --git a/pkg/skaffold/runner/verifier.go b/pkg/skaffold/runner/verifier.go
index bd019ce67..dd761da80 100644
--- a/pkg/skaffold/runner/verifier.go
+++ b/pkg/skaffold/runner/verifier.go
@@ -53,7 +53,7 @@ func GetVerifier(ctx context.Context, runCtx *runcontext.RunContext, labeller *l
 	}
 
 	if len(kubernetesTestCases) != 0 {
-		nv, err := k8sjob.NewVerifier(ctx, runCtx, labeller, kubernetesTestCases, runCtx.Artifacts(), envMap)
+		nv, err := k8sjob.NewVerifier(ctx, runCtx, labeller, kubernetesTestCases, runCtx.Artifacts(), envMap, runCtx.GetNamespace())
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/skaffold/verify/docker/verify.go b/pkg/skaffold/verify/docker/verify.go
index 0875f0e3f..7a7166a6c 100644
--- a/pkg/skaffold/verify/docker/verify.go
+++ b/pkg/skaffold/verify/docker/verify.go
@@ -118,11 +118,14 @@ func (v *Verifier) TrackContainerFromBuild(artifact graph.Artifact, container tr
 // from each specified image, executing them, and waiting for execution to complete.
 func (v *Verifier) Verify(ctx context.Context, out io.Writer, allbuilds []graph.Artifact) error {
 	var err error
-	v.once.Do(func() {
-		err = v.client.NetworkCreate(ctx, v.network)
-	})
-	if err != nil {
-		return fmt.Errorf("creating skaffold network %s: %w", v.network, err)
+
+	if !v.networkFlagPassed {
+		v.once.Do(func() {
+			err = v.client.NetworkCreate(ctx, v.network)
+		})
+		if err != nil {
+			return fmt.Errorf("creating skaffold network %s: %w", v.network, err)
+		}
 	}
 
 	builds := []graph.Artifact{}
diff --git a/pkg/skaffold/verify/k8sjob/verify.go b/pkg/skaffold/verify/k8sjob/verify.go
index 53f79be02..e746e6884 100644
--- a/pkg/skaffold/verify/k8sjob/verify.go
+++ b/pkg/skaffold/verify/k8sjob/verify.go
@@ -22,8 +22,6 @@ import (
 	"io"
 	"io/ioutil"
 	"math"
-	"os/exec"
-	"strings"
 	"sync"
 	"time"
 
@@ -53,7 +51,6 @@ import (
 	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/log"
 	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
 	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/status"
-	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/util"
 )
 
 // Verifier verifies deployments using kubernetes libs/CLI.
@@ -75,15 +72,7 @@ type Verifier struct {
 
 // NewVerifier returns a new Verifier for a VerifyConfig filled
 // with the needed configuration for `kubectl apply`
-func NewVerifier(ctx context.Context, cfg kubectl.Config, labeller *label.DefaultLabeller, testCases []*latest.VerifyTestCase, artifacts []*latest.Artifact, envMap map[string]string) (*Verifier, error) {
-	defaultNamespace := ""
-	b, err := (&util.Commander{}).RunCmdOut(context.Background(), exec.Command("kubectl", "config", "view", "--minify", "-o", "jsonpath='{..namespace}'"))
-	if err == nil {
-		defaultNamespace = strings.Trim(string(b), "'")
-		if defaultNamespace == "default" {
-			defaultNamespace = ""
-		}
-	}
+func NewVerifier(ctx context.Context, cfg kubectl.Config, labeller *label.DefaultLabeller, testCases []*latest.VerifyTestCase, artifacts []*latest.Artifact, envMap map[string]string, defaultNamespace string) (*Verifier, error) {
 	kubectl := kubectl.NewCLI(cfg, latest.KubectlFlags{}, defaultNamespace)
 	// default namespace must be "default" not "" when used to create and stream logs from Job(s)
 	if defaultNamespace == "" {
@@ -197,7 +186,6 @@ func (v *Verifier) Verify(ctx context.Context, out io.Writer, allbuilds []graph.
 func (v *Verifier) createAndRunJob(ctx context.Context, tc latest.VerifyTestCase) error {
 	// TODO(aaron-prindle) look for and delete existing job w/ same name?
 	// - must be done before logger starts or else confusing output
-
 	clientset, err := kubernetesclient.Client(v.kubectl.KubeContext)
 	if err != nil {
 		return fmt.Errorf("getting Kubernetes client: %w", err)


Test patch:
diff --git a/integration/binauthz_test.go b/integration/binauthz_test.go
new file mode 100644
index 000000000..5205bfe98
--- /dev/null
+++ b/integration/binauthz_test.go
@@ -0,0 +1,48 @@
+/*
+Copyright 2023 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 integration
+
+import (
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/GoogleContainerTools/skaffold/v2/integration/skaffold"
+)
+
+const (
+	binauthzClusterName = "integration-tests-binauthz"
+)
+
+// TestBinAuthZWithDeploy targets the integration-tests-binauthz cluster on k8s-skaffold which has '--binauthz-evaluation-mode=PROJECT_SINGLETON_POLICY_ENFORCE' set
+func TestBinAuthZWithDeploy(t *testing.T) {
+	MarkIntegrationTest(t, NeedsGcp)
+	if os.Getenv("GKE_CLUSTER_NAME") != binauthzClusterName {
+		t.Skip("TestBinAuthZWithDeploy only runs against integration-tests-binauthz cluster")
+	}
+
+	ns, _ := SetupNamespace(t)
+	expected := "Failed to create Pod for Deployment"
+
+	// We're not providing a tag for the getting-started image
+	output, err := skaffold.Deploy("--images", "index.docker.io/library/busybox:1", "--default-repo=").InDir("examples/kustomize").InNs(ns.Name).RunWithCombinedOutput(t)
+	if err == nil {
+		t.Errorf("expected to see an error since the image deployed to cluster should be denied based on binauthz policy: %s", output)
+	} else if !strings.Contains(string(output), expected) {
+		t.Errorf("unexpected error text - expected: %s, got: %s", expected, output)
+	}
+}
diff --git a/integration/verify_test.go b/integration/verify_test.go
index c147b998d..ac514ba3e 100644
--- a/integration/verify_test.go
+++ b/integration/verify_test.go
@@ -56,6 +56,14 @@ func TestLocalVerifyPassingTestsWithEnvVar(t *testing.T) {
 	// TODO(aaron-prindle) verify that SUCCEEDED event is found where expected
 }
 
+func TestVerifyWithNotCreatedNetwork(t *testing.T) {
+	MarkIntegrationTest(t, CanRunWithoutGcp)
+	// `--default-repo=` is used to cancel the default repo that is set by default.
+	logs, err := skaffold.Verify("--default-repo=", "--env-file", "verify.env", "--docker-network", "not-created-network").InDir("testdata/verify-succeed").RunWithCombinedOutput(t)
+	testutil.CheckError(t, true, err)
+	testutil.CheckContains(t, "network not-created-network not found", string(logs))
+}
+
 func TestLocalVerifyOneTestFailsWithEnvVar(t *testing.T) {
 	MarkIntegrationTest(t, CanRunWithoutGcp)
 	tmp := t.TempDir()
diff --git a/pkg/skaffold/actions/runner_test.go b/pkg/skaffold/actions/runner_test.go
new file mode 100644
index 000000000..592b6e373
--- /dev/null
+++ b/pkg/skaffold/actions/runner_test.go
@@ -0,0 +1,369 @@
+/*
+Copyright 2023 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 actions
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"testing"
+	"time"
+
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/graph"
+	"github.com/GoogleContainerTools/skaffold/v2/testutil"
+)
+
+type mockTask struct {
+	ExecF    func(ctx context.Context, out io.Writer, m *mockTask) error
+	Finished bool
+}
+
+func (ma *mockTask) Name() string { return "" }
+
+func (ma *mockTask) Exec(ctx context.Context, out io.Writer) error {
+	if ma.ExecF != nil {
+		return ma.ExecF(ctx, out, ma)
+	}
+	ma.Finished = true
+	return nil
+}
+
+func (ma *mockTask) Cleanup(ctx context.Context, out io.Writer) error { return nil }
+
+type mockExecEnv struct {
+	Actions     []string
+	MockPrepAcs func(ctx context.Context, out io.Writer, allbuilds []graph.Artifact, acsNames []string) ([]Action, error)
+	MockActions []Action
+}
+
+func (me mockExecEnv) PrepareActions(ctx context.Context, out io.Writer, allbuilds []graph.Artifact, acsNames []string) ([]Action, error) {
+	if me.MockPrepAcs != nil {
+		return me.MockPrepAcs(ctx, out, allbuilds, acsNames)
+	}
+	return me.MockActions, nil
+}
+
+func (me mockExecEnv) Cleanup(ctx context.Context, out io.Writer) error {
+	return nil
+}
+
+func newRunner(mExecEnvs []mockExecEnv) Runner {
+	execEnvByAction := map[string]ExecEnv{}
+	acsByExecEnv := map[ExecEnv][]string{}
+	execEnvs := []ExecEnv{}
+
+	for _, execEnv := range mExecEnvs {
+		for _, a := range execEnv.Actions {
+			execEnvByAction[a] = &execEnv
+			acsByExecEnv[&execEnv] = append(acsByExecEnv[&execEnv], a)
+		}
+		execEnvs = append(execEnvs, execEnv)
+	}
+
+	return NewRunner(execEnvByAction, execEnvs, acsByExecEnv)
+}
+
+func TestActionsRunner_Exec(t *testing.T) {
+	tests := []struct {
+		description  string
+		actionToExec string
+		shouldErr    bool
+		err          string
+		execEnvs     []mockExecEnv
+	}{
+		{
+			description:  "action not found",
+			actionToExec: "action3",
+			shouldErr:    true,
+			err:          "custom action action3 not found",
+			execEnvs: []mockExecEnv{
+				{
+					Actions: []string{"action1", "action2"},
+				},
+			},
+		},
+		{
+			description:  "zero actions returned",
+			actionToExec: "action1",
+			shouldErr:    true,
+			err:          "failed to create action1 action",
+			execEnvs: []mockExecEnv{
+				{
+					Actions: []string{"action1", "action2"},
+					MockPrepAcs: func(ctx context.Context, out io.Writer, allbuilds []graph.Artifact, acsNames []string) ([]Action, error) {
+						return []Action{}, nil
+					},
+				},
+			},
+		},
+		{
+			description:  "more than one action returned",
+			actionToExec: "action1",
+			shouldErr:    true,
+			err:          "failed to create action1 action",
+			execEnvs: []mockExecEnv{
+				{
+					Actions: []string{"action1", "action2"},
+					MockPrepAcs: func(ctx context.Context, out io.Writer, allbuilds []graph.Artifact, acsNames []string) ([]Action, error) {
+						return []Action{{}, {}}, nil
+					},
+				},
+			},
+		},
+		{
+			description:  "forward exec error",
+			actionToExec: "action1",
+			shouldErr:    true,
+			err:          "error from action execution",
+			execEnvs: []mockExecEnv{
+				{
+					Actions: []string{"action1", "action2"},
+					MockPrepAcs: func(ctx context.Context, out io.Writer, allbuilds []graph.Artifact, acsNames []string) ([]Action, error) {
+						return []Action{
+							{
+								execFunc: func(ctx context.Context, out io.Writer, ts []Task) error {
+									return fmt.Errorf("error from action execution")
+								},
+							},
+						}, nil
+					},
+				},
+			},
+		},
+		{
+			description:  "forward prepare actions error",
+			actionToExec: "action1",
+			shouldErr:    true,
+			err:          "error from prepare actions",
+			execEnvs: []mockExecEnv{
+				{
+					Actions: []string{"action1", "action2"},
+					MockPrepAcs: func(ctx context.Context, out io.Writer, allbuilds []graph.Artifact, acsNames []string) ([]Action, error) {
+						return nil, fmt.Errorf("error from prepare actions")
+					},
+				},
+			},
+		},
+		{
+			description:  "exec successfully",
+			actionToExec: "action1",
+			execEnvs: []mockExecEnv{
+				{
+					Actions: []string{"action1", "action2"},
+					MockPrepAcs: func(ctx context.Context, out io.Writer, allbuilds []graph.Artifact, acsNames []string) ([]Action, error) {
+						return []Action{
+							{
+								execFunc: func(ctx context.Context, out io.Writer, ts []Task) error {
+									return nil
+								},
+							},
+						}, nil
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			runner := newRunner(test.execEnvs)
+			err := runner.Exec(context.TODO(), nil, nil, test.actionToExec)
+
+			if test.shouldErr {
+				t.CheckErrorContains(test.err, err)
+			} else {
+				t.CheckNoError(err)
+			}
+		})
+	}
+}
+
+func TestActionsRunner_ExecFailFast(t *testing.T) {
+	tests := []struct {
+		description  string
+		actionToExec string
+		shouldErr    bool
+		err          string
+		action       Action
+	}{
+		{
+			description:  "interrupt other tasks when one fails",
+			actionToExec: "action1",
+			shouldErr:    true,
+			err:          "error from mock task",
+			action: Action{
+				tasks: []Task{
+					&mockTask{
+						// This task will fail, interrupting the others.
+						ExecF: func(ctx context.Context, out io.Writer, m *mockTask) error {
+							m.Finished = false
+							return fmt.Errorf("error from mock task")
+						},
+					},
+					&mockTask{
+						ExecF: func(ctx context.Context, out io.Writer, m *mockTask) error {
+							m.Finished = false
+							var err error
+							select {
+							// This task will run for 10 secs, but should be interrupted
+							// in 2 seconds due to the other task failed, so this never finish.
+							case <-time.After(time.Second * 5):
+								m.Finished = true
+							case <-ctx.Done():
+								err = ctx.Err()
+							}
+							return err
+						},
+					},
+				},
+			},
+		},
+		{
+			description:  "interrupt all tasks when on action timeout",
+			actionToExec: "action1",
+			shouldErr:    true,
+			err:          "context deadline",
+			action: Action{
+				timeout: 1,
+				tasks: []Task{
+					&mockTask{
+						ExecF: func(ctx context.Context, out io.Writer, m *mockTask) error {
+							m.Finished = false
+							var err error
+							select {
+							case <-time.After(time.Second * 2):
+								m.Finished = true
+							case <-ctx.Done():
+								err = ctx.Err()
+							}
+							return err
+						},
+					},
+					&mockTask{
+						ExecF: func(ctx context.Context, out io.Writer, m *mockTask) error {
+							m.Finished = false
+							var err error
+							select {
+							case <-time.After(time.Second * 2):
+								m.Finished = true
+							case <-ctx.Done():
+								err = ctx.Err()
+							}
+							return err
+						},
+					},
+				},
+			},
+		},
+		{
+			description:  "execute all tasks till end",
+			actionToExec: "action1",
+			shouldErr:    false,
+			action: Action{
+				tasks: []Task{&mockTask{}, &mockTask{}},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			test.action.execFunc = execWithFailingFast
+			execEnvs := []mockExecEnv{
+				{
+					Actions:     []string{test.actionToExec},
+					MockActions: []Action{test.action},
+				},
+			}
+
+			runner := newRunner(execEnvs)
+			err := runner.Exec(context.TODO(), nil, nil, test.actionToExec)
+
+			if test.shouldErr {
+				t.CheckErrorContains(test.err, err)
+				for _, task := range test.action.tasks {
+					t.CheckFalse(task.(*mockTask).Finished)
+				}
+			} else {
+				for _, task := range test.action.tasks {
+					t.CheckTrue(task.(*mockTask).Finished)
+				}
+				t.CheckNoError(err)
+			}
+		})
+	}
+}
+
+func TestActionsRunner_ExecFailSafe(t *testing.T) {
+	tests := []struct {
+		description  string
+		actionToExec string
+		shouldErr    bool
+		err          string
+		action       Action
+	}{
+		{
+			description:  "run all tasks till end if one fails",
+			actionToExec: "action1",
+			shouldErr:    true,
+			err:          "error from mock task",
+			action: Action{
+				tasks: []Task{
+					&mockTask{},
+					&mockTask{
+						// This task will fail first.
+						ExecF: func(ctx context.Context, out io.Writer, m *mockTask) error {
+							m.Finished = true
+							return fmt.Errorf("error from mock task")
+						},
+					},
+				},
+			},
+		},
+		{
+			description:  "execute all tasks till end",
+			actionToExec: "action1",
+			shouldErr:    false,
+			action: Action{
+				tasks: []Task{&mockTask{}, &mockTask{}},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			test.action.execFunc = execWithFailingFast
+			execEnvs := []mockExecEnv{
+				{
+					Actions:     []string{test.actionToExec},
+					MockActions: []Action{test.action},
+				},
+			}
+
+			runner := newRunner(execEnvs)
+			err := runner.Exec(context.TODO(), nil, nil, test.actionToExec)
+
+			if test.shouldErr {
+				t.CheckErrorContains(test.err, err)
+			} else {
+				t.CheckNoError(err)
+			}
+			for _, task := range test.action.tasks {
+				t.CheckTrue(task.(*mockTask).Finished)
+			}
+		})
+	}
+}
diff --git a/pkg/skaffold/deploy/helm/helm_test.go b/pkg/skaffold/deploy/helm/helm_test.go
index efb7e31f8..702984d7c 100644
--- a/pkg/skaffold/deploy/helm/helm_test.go
+++ b/pkg/skaffold/deploy/helm/helm_test.go
@@ -89,6 +89,17 @@ var testDeployEnvTemplateNamespacedConfig = latest.LegacyHelmDeploy{
 	}},
 }
 
+var testDeployWithTemplatedChartPath = latest.LegacyHelmDeploy{
+	Releases: []latest.HelmRelease{{
+		Name:      "skaffold-helm",
+		ChartPath: "examples/{{.FOO}}",
+		Overrides: schemautil.HelmOverrides{Values: map[string]interface{}{"foo": "bar"}},
+		SetValues: map[string]string{
+			"some.key": "somevalue",
+		},
+	}},
+}
+
 var testDeployConfigRemoteRepo = latest.LegacyHelmDeploy{
 	Releases: []latest.HelmRelease{{
 		Name:      "skaffold-helm",
@@ -1212,6 +1223,15 @@ func TestHelmRender(t *testing.T) {
 			helm:   testDeployWithTemplatedName,
 			builds: testBuilds,
 		},
+		{
+			description: "render with templated chart path",
+			shouldErr:   false,
+			commands: testutil.
+				CmdRun("helm --kube-context kubecontext dep build examples/FOOBAR --kubeconfig kubeconfig").
+				AndRun("helm --kube-context kubecontext template skaffold-helm examples/FOOBAR --post-renderer SKAFFOLD-BINARY --set some.key=somevalue --kubeconfig kubeconfig"),
+			helm:   testDeployWithTemplatedChartPath,
+			builds: testBuilds,
+		},
 		{
 			description: "render with namespace",
 			shouldErr:   false,
diff --git a/pkg/skaffold/inspect/executionModes/list_test.go b/pkg/skaffold/inspect/executionModes/list_test.go
new file mode 100644
index 000000000..a0c924a81
--- /dev/null
+++ b/pkg/skaffold/inspect/executionModes/list_test.go
@@ -0,0 +1,146 @@
+/*
+Copyright 2021 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 inspect
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"testing"
+
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/config"
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/inspect"
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/parser"
+	sErrors "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/errors"
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/util/stringslice"
+	"github.com/GoogleContainerTools/skaffold/v2/testutil"
+)
+
+func TestPrintExecutionModesList(t *testing.T) {
+	tests := []struct {
+		description   string
+		profiles      []string
+		module        []string
+		customActions []string
+		err           error
+		expected      string
+	}{
+		{
+			description: "print all executionModes where no executionMode is set in the verify config",
+			expected:    "{\"verifyExecutionModes\":{},\"customActionsExecutionModes\":{}}" + "\n",
+			module:      []string{"cfg-without-executionModes"},
+		},
+		{
+			description: "print all executionModes where one executionMode is set in the verify and customAction config via a profile but no customAction arg",
+			expected:    "{\"verifyExecutionModes\":{\"foo\":\"kubernetesCluster\"},\"customActionsExecutionModes\":{}}" + "\n",
+			profiles:    []string{"has-verify-executionMode"},
+			module:      []string{"cfg-without-executionModes"},
+		},
+		{
+			description: "print all executionModes where one executionMode is set in the verify config via a module",
+			expected:    "{\"verifyExecutionModes\":{\"bar\":\"kubernetesCluster\"},\"customActionsExecutionModes\":{}}" + "\n",
+			module:      []string{"cfg-with-executionModes"},
+		},
+		{
+			description: "actionable error",
+			err:         sErrors.MainConfigFileNotFoundErr("path/to/skaffold.yaml", fmt.Errorf("failed to read file : %q", "skaffold.yaml")),
+			expected:    `{"errorCode":"CONFIG_FILE_NOT_FOUND_ERR","errorMessage":"unable to find configuration file \"path/to/skaffold.yaml\": failed to read file : \"skaffold.yaml\". Check that the specified configuration file exists at \"path/to/skaffold.yaml\"."}` + "\n",
+		},
+		{
+			description: "generic error",
+			err:         errors.New("some error occurred"),
+			expected:    `{"errorCode":"INSPECT_UNKNOWN_ERR","errorMessage":"some error occurred"}` + "\n",
+		},
+	}
+
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			configSet := parser.SkaffoldConfigSet{
+				&parser.SkaffoldConfigEntry{SkaffoldConfig: &latest.SkaffoldConfig{
+					Metadata: latest.Metadata{Name: "cfg-without-executionModes"},
+					Pipeline: latest.Pipeline{},
+					Profiles: []latest.Profile{
+						{Name: "has-verify-executionMode",
+							Pipeline: latest.Pipeline{
+								Verify: []*latest.VerifyTestCase{
+									{
+										Name: "foo",
+										Container: latest.VerifyContainer{
+											Name:  "foo",
+											Image: "foo",
+										},
+										ExecutionMode: latest.VerifyExecutionModeConfig{
+											VerifyExecutionModeType: latest.VerifyExecutionModeType{
+												KubernetesClusterExecutionMode: &latest.KubernetesClusterVerifier{},
+											},
+										},
+									},
+								},
+							},
+						}},
+				}, SourceFile: "path/to/cfg-without-executionModes"},
+
+				&parser.SkaffoldConfigEntry{SkaffoldConfig: &latest.SkaffoldConfig{
+					Metadata: latest.Metadata{Name: "cfg-with-executionModes"},
+					Pipeline: latest.Pipeline{
+						Verify: []*latest.VerifyTestCase{
+							{
+								Name: "bar",
+								Container: latest.VerifyContainer{
+									Name:  "bar",
+									Image: "bar",
+								},
+								ExecutionMode: latest.VerifyExecutionModeConfig{
+									VerifyExecutionModeType: latest.VerifyExecutionModeType{
+										KubernetesClusterExecutionMode: &latest.KubernetesClusterVerifier{},
+									},
+								},
+							},
+						},
+					},
+				}, SourceFile: "path/to/cfg-with-default-namespace"},
+			}
+			t.Override(&inspect.GetConfigSet, func(_ context.Context, opts config.SkaffoldOptions) (parser.SkaffoldConfigSet, error) {
+				// mock profile activation
+				var set parser.SkaffoldConfigSet
+				for _, c := range configSet {
+					if len(opts.ConfigurationFilter) > 0 && !stringslice.Contains(opts.ConfigurationFilter, c.Metadata.Name) {
+						continue
+					}
+					for _, pName := range opts.Profiles {
+						for _, profile := range c.Profiles {
+							if profile.Name != pName {
+								continue
+							}
+							c.Verify = profile.Verify
+							c.CustomActions = profile.CustomActions
+						}
+					}
+					set = append(set, c)
+				}
+				return set, test.err
+			})
+			var buf bytes.Buffer
+			err := PrintExecutionModesList(context.Background(), &buf, inspect.Options{
+				OutFormat: "json", Modules: test.module, Profiles: test.profiles}, test.customActions)
+			t.CheckError(test.err != nil, err)
+			t.CheckDeepEqual(test.expected, buf.String())
+		})
+	}
+}