From a9fe03a9da36f15c343d86f3e69c86277738b9a9 Mon Sep 17 00:00:00 2001 From: Aravindhan Ayyanathan Date: Thu, 16 Jul 2026 13:27:54 +0100 Subject: [PATCH 1/2] Exclude fn-config from function input items in kpt fn eval Signed-off-by: Aravindhan Ayyanathan --- .../en/reference/cli/fn/eval/_index.md | 3 + .../custom-pkg-path/.expected/config.yaml | 2 +- .../custom-pkg-path/.expected/diff.patch | 11 -- thirdparty/kyaml/runfn/runfn.go | 26 +++- thirdparty/kyaml/runfn/runfn_test.go | 112 +++++++++++++++++- 5 files changed, 139 insertions(+), 15 deletions(-) diff --git a/documentation/content/en/reference/cli/fn/eval/_index.md b/documentation/content/en/reference/cli/fn/eval/_index.md index 8591ea0cf0..47386318b6 100644 --- a/documentation/content/en/reference/cli/fn/eval/_index.md +++ b/documentation/content/en/reference/cli/fn/eval/_index.md @@ -73,6 +73,9 @@ fn-args: --fn-config: Path to the file containing `functionConfig` for the function. + The fn-config resource is used to configure the function but is not + treated as an input resource. The function will not modify the fn-config + file, even if it resides inside the package directory. --image, i: Container image of the function to execute e.g. `ghcr.io/kptdev/krm-functions-catalog/set-namespace:latest`. diff --git a/e2e/testdata/fn-eval/save-fn/custom-pkg-path/.expected/config.yaml b/e2e/testdata/fn-eval/save-fn/custom-pkg-path/.expected/config.yaml index 4977437f92..f7c51e8cc4 100644 --- a/e2e/testdata/fn-eval/save-fn/custom-pkg-path/.expected/config.yaml +++ b/e2e/testdata/fn-eval/save-fn/custom-pkg-path/.expected/config.yaml @@ -26,5 +26,5 @@ args: stdErr: | [RUNNING] "ghcr.io/kptdev/krm-functions-catalog/set-namespace:latest" [PASS] "ghcr.io/kptdev/krm-functions-catalog/set-namespace:latest" in 0s - [Results]: [info]: namespace [default] updated to "staging", 2 value(s) changed, [info]: all `depends-on` annotations are up-to-date. no `namespace` changed + [Results]: [info]: namespace [default] updated to "staging", 1 value(s) changed, [info]: all `depends-on` annotations are up-to-date. no `namespace` changed Added "ghcr.io/kptdev/krm-functions-catalog/set-namespace:latest" as mutator in the Kptfile. diff --git a/e2e/testdata/fn-eval/save-fn/custom-pkg-path/.expected/diff.patch b/e2e/testdata/fn-eval/save-fn/custom-pkg-path/.expected/diff.patch index 92a2116f6d..63791d8e1c 100644 --- a/e2e/testdata/fn-eval/save-fn/custom-pkg-path/.expected/diff.patch +++ b/e2e/testdata/fn-eval/save-fn/custom-pkg-path/.expected/diff.patch @@ -10,17 +10,6 @@ index d9e2f05..e699ad9 100644 + mutators: + - image: ghcr.io/kptdev/krm-functions-catalog/set-namespace:latest + configPath: fn-config.yaml -diff --git a/sub-pkg/fn-config.yaml b/sub-pkg/fn-config.yaml -index 21135f9..1000dd9 100644 ---- a/sub-pkg/fn-config.yaml -+++ b/sub-pkg/fn-config.yaml -@@ -15,5 +15,6 @@ apiVersion: v1 - kind: ConfigMap - metadata: - name: fn-config -+ namespace: staging - data: - namespace: staging diff --git a/sub-pkg/resources.yaml b/sub-pkg/resources.yaml index eed43d6..4593a1b 100644 --- a/sub-pkg/resources.yaml diff --git a/thirdparty/kyaml/runfn/runfn.go b/thirdparty/kyaml/runfn/runfn.go index 855758abc3..126277b086 100644 --- a/thirdparty/kyaml/runfn/runfn.go +++ b/thirdparty/kyaml/runfn/runfn.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Kubernetes Authors. +// Copyright 2019, 2026 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package runfn @@ -10,6 +10,7 @@ import ( "os" "os/user" "path/filepath" + "slices" "strings" fnresultv1 "github.com/kptdev/kpt/api/fnresult/v1" @@ -21,6 +22,7 @@ import ( "sigs.k8s.io/kustomize/kyaml/filesys" "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/kio/kioutil" "sigs.k8s.io/kustomize/kyaml/yaml" "github.com/kptdev/kpt/pkg/lib/pkg" @@ -189,6 +191,23 @@ func (r RunFns) runFunctions(input kio.Reader, output kio.Writer, fltrs []kio.Fi return err } + // Exclude the fn-config file from processing when it resides inside the + // package. The fn-config is already passed separately via + // resourceList.functionConfig and should not be processed as a regular + // input resource. The excluded node is written back unmodified so the + // file is preserved on disk. + var fnConfigNode *yaml.RNode + if r.FnConfigPath != "" { + inputResources = slices.DeleteFunc(inputResources, func(node *yaml.RNode) bool { + p, _, _ := kioutil.GetFileAnnotations(node) + if p != "" && filepath.Join(string(r.uniquePath), p) == r.FnConfigPath { + fnConfigNode = node + return true + } + return false + }) + } + selectedInput := inputResources if !r.Selector.IsEmpty() || !r.Exclusion.IsEmpty() { @@ -226,6 +245,11 @@ func (r RunFns) runFunctions(input kio.Reader, output kio.Writer, fltrs []kio.Fi } } + // Add fn-config resource back (unmodified) so the writer preserves the file. + if fnConfigNode != nil { + outputResources = append(outputResources, fnConfigNode) + } + if err == nil { writeErr := outputs[0].Write(outputResources) if writeErr != nil { diff --git a/thirdparty/kyaml/runfn/runfn_test.go b/thirdparty/kyaml/runfn/runfn_test.go index d1096e4025..c4e0bee925 100644 --- a/thirdparty/kyaml/runfn/runfn_test.go +++ b/thirdparty/kyaml/runfn/runfn_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Kubernetes Authors. +// Copyright 2019, 2026 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package runfn @@ -14,7 +14,7 @@ import ( kptfilev1 "github.com/kptdev/kpt/api/kptfile/v1" "github.com/kptdev/kpt/pkg/printer/fake" "github.com/stretchr/testify/assert" - + "github.com/stretchr/testify/require" "sigs.k8s.io/kustomize/kyaml/copyutil" "sigs.k8s.io/kustomize/kyaml/filesys" "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" @@ -471,6 +471,114 @@ func getMetaResourceFilterProvider() func(runtimeutil.FunctionSpec, *yaml.RNode, } } +// TestCmd_Execute_fnConfigExclusion verifies that when --fn-config points to a file +// inside the package directory, that resource is excluded from the function input items +// and the file is preserved unmodified on disk. +func TestCmd_Execute_fnConfigExclusion(t *testing.T) { + fnConfigContent := `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-fn-config +data: + name: foo +` + tests := []struct { + name string + fnConfigInPackage bool + useRelativePath bool + wantResourceCount int + }{ + { + name: "fn-config inside package is excluded", + fnConfigInPackage: true, + wantResourceCount: 3, + }, + { + name: "fn-config inside package with relative path is excluded", + fnConfigInPackage: true, + useRelativePath: true, + wantResourceCount: 3, + }, + { + name: "fn-config outside package does not exclude any items", + fnConfigInPackage: false, + wantResourceCount: 3, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + dir := setupTest(t) + defer os.RemoveAll(dir) + + var fnConfigPath string + if tc.fnConfigInPackage { + fnConfigPath = filepath.Join(dir, "fn-config.yaml") + require.NoError(t, os.WriteFile(fnConfigPath, []byte(fnConfigContent), 0600)) + if tc.useRelativePath { + rel, err := filepath.Rel(filepath.Dir(dir), fnConfigPath) + require.NoError(t, err) + fnConfigPath = rel + } + } else { + tmpF, err := os.CreateTemp("", "fn-config*.yaml") + require.NoError(t, err) + defer os.Remove(tmpF.Name()) + require.NoError(t, os.WriteFile(tmpF.Name(), []byte(fnConfigContent), 0600)) + fnConfigPath = tmpF.Name() + } + + fnConfig, err := yaml.Parse(fnConfigContent) + require.NoError(t, err) + + var receivedNames []string + instance := RunFns{ + Ctx: fake.CtxWithDefaultPrinter(), + FnConfigPath: fnConfigPath, + Path: dir, + Function: &runtimeutil.FunctionSpec{ + Container: runtimeutil.ContainerSpec{Image: "ghcr.io/example.com/image:version"}, + }, + FnConfig: fnConfig, + fnResults: fnresultv1.NewResultList(), + } + instance.functionFilterProvider = func( + f runtimeutil.FunctionSpec, node *yaml.RNode, currentUser currentUserFunc, + ) (kio.Filter, error) { + return kio.FilterFunc(func(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + for _, n := range nodes { + meta, _ := n.GetMeta() + receivedNames = append(receivedNames, meta.Name) + } + return nodes, nil + }), nil + } + + require.NoError(t, instance.init()) + require.NoError(t, instance.Execute()) + + assert.Equal(t, tc.wantResourceCount, len(receivedNames)) + if tc.fnConfigInPackage { + assert.NotContains(t, receivedNames, "my-fn-config") + } + assert.Contains(t, receivedNames, "app") + + if tc.fnConfigInPackage { + absPath := fnConfigPath + if !filepath.IsAbs(absPath) { + absPath, _ = filepath.Abs(filepath.Join(filepath.Dir(dir), fnConfigPath)) + } + _, statErr := os.Stat(absPath) + require.NoError(t, statErr, "fn-config file should still exist on disk") + content, readErr := os.ReadFile(absPath) + require.NoError(t, readErr) + assert.Equal(t, fnConfigContent, string(content), + "fn-config file content should be unchanged") + } + }) + } +} + func TestRunFns_mergeContainerEnv(t *testing.T) { testcases := []struct { name string From ce5ad899c1b32407259d9ab7cb2f0d817b361421 Mon Sep 17 00:00:00 2001 From: Aravindhan Ayyanathan Date: Thu, 16 Jul 2026 14:47:26 +0100 Subject: [PATCH 2/2] Address review comments Signed-off-by: Aravindhan Ayyanathan --- thirdparty/kyaml/runfn/runfn_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/thirdparty/kyaml/runfn/runfn_test.go b/thirdparty/kyaml/runfn/runfn_test.go index c4e0bee925..72dc0d1dce 100644 --- a/thirdparty/kyaml/runfn/runfn_test.go +++ b/thirdparty/kyaml/runfn/runfn_test.go @@ -549,6 +549,12 @@ data: for _, n := range nodes { meta, _ := n.GetMeta() receivedNames = append(receivedNames, meta.Name) + // Mutate all received items so the "file unchanged" + // assertion would catch a regression if fn-config were + // accidentally included in items. + if err := n.PipeE(yaml.SetAnnotation("mutated", "true")); err != nil { + return nil, err + } } return nodes, nil }), nil