Repository: GoogleContainerTools/skaffold
Pull request #8645 (2023-04-05 21:40:20)

feat: add template support for `chartPath`
Fixes: #8626 

**Description**
The `skaffold render` command doesn't support templating on `chartPath` field at the moment.
This PR attempts to enable templating on `chartPath` field.

I have added a basic unit test, please review it and let me know if I need to add/adjust something there.

**User facing changes**
We would get the following error when there is a template/environment variable
```bash
std out err: %!(EXTRA *errors.errorString=Error: repo kubernetes not found
exit status 1
)
```

With this PR, template/environment variables should render successfully.


Patch:
diff --git a/Makefile b/Makefile
index 514d4b5a9..e0529afa0 100644
--- a/Makefile
+++ b/Makefile
@@ -41,7 +41,6 @@ SKAFFOLD_TEST_PACKAGES = ./pkg/skaffold/... ./cmd/... ./hack/... ./pkg/webhook/.
 GO_FILES = $(shell find . -type f -name '*.go' -not -path "./pkg/diag/*")
 
 VERSION_PACKAGE = $(REPOPATH)/v2/pkg/skaffold/version
-FIRELOG_PACKAGE = $(REPOPATH)/v2/pkg/skaffold/instrumentation/firelog
 COMMIT = $(shell git rev-parse HEAD)
 
 ifeq "$(strip $(VERSION))" ""
@@ -58,9 +57,6 @@ endif
 GO_LDFLAGS = -X $(VERSION_PACKAGE).version=$(VERSION)
 GO_LDFLAGS += -X $(VERSION_PACKAGE).buildDate=$(BUILD_DATE)
 GO_LDFLAGS += -X $(VERSION_PACKAGE).gitCommit=$(COMMIT)
-ifdef FIRELOG_API_KEY
-    GO_LDFLAGS += -X $(VERSION_PACKAGE).APIKey=$(FIRELOG_API_KEY)
-endif
 GO_LDFLAGS += -s -w
 
 GO_BUILD_TAGS = timetzdata
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/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/hack/generate-embedded-files.sh b/hack/generate-embedded-files.sh
index 7a0ee7f8f..1566ded65 100755
--- a/hack/generate-embedded-files.sh
+++ b/hack/generate-embedded-files.sh
@@ -50,4 +50,10 @@ if [[ -d ${SECRET} ]]; then
    cp -R ${SECRET} "fs/assets/secrets_generated"
 fi
 
+if [ -n "${FIRELOG_API_KEY_FILE+x}" ] && [ -f ${FIRELOG_API_KEY_FILE} ]; then
+   echo "generating embedded files for firelog API key"
+   mkdir -p "fs/assets/firelog_generated"
+   cp ${FIRELOG_API_KEY_FILE} "fs/assets/firelog_generated/key.txt"
+fi
+
 echo "Used for marking generating embedded files task complete, don't modify this." > fs/assets/check.txt
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/instrumentation/export.go b/pkg/skaffold/instrumentation/export.go
index 43545581a..1ddf951cd 100644
--- a/pkg/skaffold/instrumentation/export.go
+++ b/pkg/skaffold/instrumentation/export.go
@@ -55,6 +55,7 @@ import (
 
 func ExportMetrics(exitCode int) error {
 	if !ShouldExportMetrics || meter.Command == "" {
+		log.Entry(context.TODO()).Debug("exporting metrics disabled")
 		return nil
 	}
 	home, err := homedir.Dir()
diff --git a/pkg/skaffold/instrumentation/firelog/exporter.go b/pkg/skaffold/instrumentation/firelog/exporter.go
index 3291eac2e..45ae24339 100644
--- a/pkg/skaffold/instrumentation/firelog/exporter.go
+++ b/pkg/skaffold/instrumentation/firelog/exporter.go
@@ -28,6 +28,9 @@ import (
 	"go.opentelemetry.io/otel/sdk/metric"
 	"go.opentelemetry.io/otel/sdk/metric/aggregation"
 	"go.opentelemetry.io/otel/sdk/metric/metricdata"
+
+	"github.com/GoogleContainerTools/skaffold/v2/fs"
+	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/output/log"
 )
 
 var (
@@ -40,17 +43,23 @@ type Exporter struct {
 }
 
 func NewFireLogExporter() (metric.Exporter, error) {
-	if APIKey == "" {
-		// export metrics to std out if local env is set.
-		if _, ok := os.LookupEnv("SKAFFOLD_EXPORT_TO_STDOUT"); ok {
-			enc := json.NewEncoder(os.Stdout)
-			enc.SetIndent("", "  ")
-			exporter, err := stdoutmetric.New(stdoutmetric.WithEncoder(enc))
-			return exporter, err
-		}
-		return nil, nil
+	b, err := fs.AssetsFS.ReadFile("assets/firelog_generated/key.txt")
+	if err == nil {
+		APIKey = string(b)
+		return &Exporter{}, nil
+	}
+	log.Entry(context.TODO()).Debugf("failed to create firelog exporter due to error: %v", err)
+
+	// export metrics to std out if local env is set.
+	if _, ok := os.LookupEnv("SKAFFOLD_EXPORT_TO_STDOUT"); ok {
+		enc := json.NewEncoder(os.Stdout)
+		enc.SetIndent("", "  ")
+		exporter, err := stdoutmetric.New(stdoutmetric.WithEncoder(enc))
+		log.Entry(context.TODO()).Debugln("created a stdout exporter instead")
+		return exporter, err
 	}
-	return &Exporter{}, nil
+	log.Entry(context.TODO()).Debugln("did not create any log exporter")
+	return nil, nil
 }
 
 // Temporality returns the Temporality to use for an instrument kind.
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 != "" {


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/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())
+		})
+	}
+}
diff --git a/pkg/skaffold/instrumentation/export_test.go b/pkg/skaffold/instrumentation/export_test.go
index 7b8cff6db..06182a503 100644
--- a/pkg/skaffold/instrumentation/export_test.go
+++ b/pkg/skaffold/instrumentation/export_test.go
@@ -31,7 +31,6 @@ import (
 	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
 
 	"github.com/GoogleContainerTools/skaffold/v2/fs"
-	"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/instrumentation/firelog"
 	"github.com/GoogleContainerTools/skaffold/v2/proto/v1"
 	"github.com/GoogleContainerTools/skaffold/v2/testutil"
 )
@@ -110,6 +109,7 @@ func TestExportMetrics(t *testing.T) {
 	fakeFS := testutil.FakeFileSystem{
 		Files: map[string][]byte{
 			"assets/secrets_generated/keys.json": []byte(testKey),
+			"assets/firelog_generated/key.txt":   []byte("no-empty"),
 		},
 	}
 
@@ -169,7 +169,6 @@ func TestExportMetrics(t *testing.T) {
 
 			fs.AssetsFS = fakeFS
 			t.Override(&isOnline, test.isOnline)
-			t.Override(&firelog.APIKey, "no-empty")
 
 			if test.isOnline {
 				tmpFile, err := os.OpenFile(tmp.Path(openTelFilename), os.O_RDWR|os.O_CREATE, os.ModePerm)
diff --git a/pkg/skaffold/instrumentation/firelog/exporter_test.go b/pkg/skaffold/instrumentation/firelog/exporter_test.go
index 802578b37..5b37bd4ef 100644
--- a/pkg/skaffold/instrumentation/firelog/exporter_test.go
+++ b/pkg/skaffold/instrumentation/firelog/exporter_test.go
@@ -27,29 +27,35 @@ import (
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/sdk/metric"
 
+	"github.com/GoogleContainerTools/skaffold/v2/fs"
 	"github.com/GoogleContainerTools/skaffold/v2/testutil"
 )
 
 func TestNewFireLogExporter(t *testing.T) {
 	var tests = []struct {
-		name     string
-		expected metric.Exporter
-		apiKey   string
-		wantErr  bool
+		name       string
+		expected   metric.Exporter
+		fileSystem *testutil.FakeFileSystem
+		wantErr    bool
 	}{
 		{
-			name:     "no api key",
+			name: "no api key",
+			fileSystem: &testutil.FakeFileSystem{
+				Files: map[string][]byte{},
+			},
 			expected: nil,
 		},
 		{
 			name:     "has api key",
 			expected: &Exporter{},
-			apiKey:   "test key",
+			fileSystem: &testutil.FakeFileSystem{
+				Files: map[string][]byte{"assets/firelog_generated/key.txt": []byte("test key")},
+			},
 		},
 	}
 	for _, test := range tests {
 		testutil.Run(t, test.name, func(t *testutil.T) {
-			APIKey = test.apiKey
+			fs.AssetsFS = test.fileSystem
 			out, err := NewFireLogExporter()
 			t.CheckError(test.wantErr, err)
 			t.CheckDeepEqual(test.expected, out)