Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions documentation/content/en/reference/cli/fn/eval/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion thirdparty/kyaml/runfn/runfn.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2019 The Kubernetes Authors.
// Copyright 2019, 2026 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package runfn
Expand All @@ -10,6 +10,7 @@ import (
"os"
"os/user"
"path/filepath"
"slices"
"strings"

fnresultv1 "github.com/kptdev/kpt/api/fnresult/v1"
Expand All @@ -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"
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down
118 changes: 116 additions & 2 deletions thirdparty/kyaml/runfn/runfn_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2019 The Kubernetes Authors.
// Copyright 2019, 2026 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package runfn
Expand All @@ -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"
Expand Down Expand Up @@ -471,6 +471,120 @@ 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)
// 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
}

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
Expand Down