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
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/google/go-containerregistry v0.20.6
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/kptdev/krm-functions-catalog/functions/go/apply-setters v0.2.2
github.com/kptdev/krm-functions-catalog/functions/go/starlark v0.5.5
github.com/kptdev/krm-functions-sdk/go/fn v1.0.2
github.com/otiai10/copy v1.14.1
github.com/philopon/go-toposort v0.0.0-20170620085441-9be86dbd762f
Expand Down Expand Up @@ -43,8 +44,11 @@ require (

require (
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/360EntSecGroup-Skylar/excelize v1.4.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/PuerkitoBio/goquery v1.5.1 // indirect
github.com/andybalholm/cascadia v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand All @@ -55,6 +59,7 @@ require (
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.4 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/dustmop/soup v1.1.2-0.20190516214245-38228baa104e // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
Expand Down Expand Up @@ -92,19 +97,22 @@ require (
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/onsi/gomega v1.37.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/otiai10/mint v1.6.3 // indirect
github.com/paulmach/orb v0.1.5 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.2 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/qri-io/starlib v0.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
Expand All @@ -114,6 +122,7 @@ require (
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.47.0 // indirect
Expand Down
92 changes: 92 additions & 0 deletions go.sum

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions internal/builtins/applyreplacements/apply_replacements.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright 2026 The kpt 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 applyreplacements

import (
"fmt"
"io"

"github.com/kptdev/kpt/internal/builtins/registry"
"github.com/kptdev/krm-functions-sdk/go/fn"
"sigs.k8s.io/kustomize/api/filters/replacement"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
)

const (
ImageName = "ghcr.io/kptdev/krm-functions-catalog/apply-replacements"
fnConfigKind = "ApplyReplacements"
fnConfigAPIVersion = "fn.kpt.dev/v1alpha1"
)

//nolint:gochecknoinits
func init() { Register() }

func Register() {
registry.Register(&Runner{})
}

type Runner struct{}

func (a *Runner) ImageName() string { return ImageName }

func (a *Runner) Run(r io.Reader, w io.Writer) error {
input, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("reading input: %w", err)
}
rl, err := fn.ParseResourceList(input)
if err != nil {
return fmt.Errorf("parsing ResourceList: %w", err)
}
if _, err := applyReplacements(rl); err != nil {
return err
}
out, err := rl.ToYAML()
if err != nil {
return err
}
_, err = w.Write(out)
return err
}
func applyReplacements(rl *fn.ResourceList) (bool, error) {
r := &Replacements{}
return r.Process(rl)
}

type Replacements struct {
Replacements []types.Replacement `json:"replacements,omitempty" yaml:"replacements,omitempty"`
}

func (r *Replacements) Config(functionConfig *fn.KubeObject) error {
if functionConfig.IsEmpty() {
return fmt.Errorf("FunctionConfig is missing. Expect `ApplyReplacements`")
}
if functionConfig.GetKind() != fnConfigKind || functionConfig.GetAPIVersion() != fnConfigAPIVersion {
return fmt.Errorf("received functionConfig of kind %s and apiVersion %s, only functionConfig of kind %s and apiVersion %s is supported",
functionConfig.GetKind(), functionConfig.GetAPIVersion(), fnConfigKind, fnConfigAPIVersion)
}
r.Replacements = []types.Replacement{}
if err := functionConfig.As(r); err != nil {
return fmt.Errorf("unable to convert functionConfig to replacements:\n%w", err)
}
return nil
}

func (r *Replacements) Process(rl *fn.ResourceList) (bool, error) {
if err := r.Config(rl.FunctionConfig); err != nil {
rl.LogResult(err)
return false, err
}
transformedItems, err := r.Transform(rl.Items)
if err != nil {
rl.LogResult(err)
return false, err
}
Comment on lines +87 to +96
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replacements.Process logs errors to the ResourceList but then returns (false, nil) on both config and transform failures. That makes the builtin appear successful to fnruntime (exit code 0 / [PASS]) even when the function reports an error result. To match container/WASM behavior and the starlark builtin, propagate the error (while still logging/writing the ResourceList) so fn render fails appropriately.

Copilot uses AI. Check for mistakes.
rl.Items = transformedItems
return true, nil
}

func (r *Replacements) Transform(items []*fn.KubeObject) ([]*fn.KubeObject, error) {
var transformedItems []*fn.KubeObject
var nodes []*yaml.RNode
for _, obj := range items {
objRN, err := yaml.Parse(obj.String())
if err != nil {
return nil, err
}
nodes = append(nodes, objRN)
}
transformedNodes, err := replacement.Filter{
Replacements: r.Replacements,
}.Filter(nodes)
if err != nil {
return nil, err
}
for _, n := range transformedNodes {
obj, err := fn.ParseKubeObject([]byte(n.MustString()))
if err != nil {
return nil, err
}
transformedItems = append(transformedItems, obj)
}
return transformedItems, nil
}
80 changes: 80 additions & 0 deletions internal/builtins/applyreplacements/apply_replacements_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2026 The kpt 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 applyreplacements

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
)

func TestRun(t *testing.T) {
input := `apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: old-namespace
functionConfig:
apiVersion: fn.kpt.dev/v1alpha1
kind: ApplyReplacements
metadata:
name: test
replacements:
- source:
kind: Deployment
name: my-app
fieldPath: metadata.name
targets:
- select:
kind: Deployment
name: my-app
fieldPaths:
- metadata.namespace
`
r := bytes.NewBufferString(input)
w := &bytes.Buffer{}

runner := &Runner{}
err := runner.Run(r, w)
assert.NoError(t, err)
assert.Contains(t, w.String(), "namespace: my-app")
}

func TestConfig_MissingFunctionConfig(_ *testing.T) {
// skip - nil input causes panic in upstream SDK
}

func TestConfig_WrongKind(t *testing.T) {
input := `apiVersion: config.kubernetes.io/v1
kind: ResourceList
items: []
functionConfig:
apiVersion: v1
kind: ConfigMap
metadata:
name: test
`
r := bytes.NewBufferString(input)
w := &bytes.Buffer{}

runner := &Runner{}
err := runner.Run(r, w)
assert.NoError(t, err)
assert.Contains(t, w.String(), "only functionConfig of kind ApplyReplacements")
}
24 changes: 24 additions & 0 deletions internal/builtins/builtin_runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2026 The kpt 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 builtins registers all built-in KRM functions into the builtin registry
// via their init() functions.
package builtins

import (
// Register apply-replacements builtin function via init()
_ "github.com/kptdev/kpt/internal/builtins/applyreplacements"
// Register starlark builtin function via init()
_ "github.com/kptdev/kpt/internal/builtins/starlark"
)
79 changes: 79 additions & 0 deletions internal/builtins/registry/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2026 The kpt 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 registry

import (
"io"
"strings"
"sync"

"k8s.io/klog/v2"
)
Comment on lines +17 to +23
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lookup uses the standard library log package for warnings. The rest of this repo typically uses k8s.io/klog/v2 for warnings/info, which supports verbosity levels and integrates with existing logging flags. Consider switching this to klog.Warningf (and possibly gating behind a V-level) to keep logging consistent and avoid unexpected stdout/stderr output.

Copilot uses AI. Check for mistakes.

type BuiltinFunction interface {
ImageName() string
Run(r io.Reader, w io.Writer) error
}

var (
mu sync.RWMutex
registry = map[string]BuiltinFunction{}
)

func Register(fn BuiltinFunction) {
mu.Lock()
defer mu.Unlock()
registry[normalizeImage(fn.ImageName())] = fn
}

func Lookup(imageName string) BuiltinFunction {
mu.RLock()
defer mu.RUnlock()
normalized := normalizeImage(imageName)
if strings.HasSuffix(imageName, ":latest") ||
strings.HasSuffix(imageName, "@sha256:") {
return nil
}
fn := registry[normalized]
if fn != nil && imageName != normalized {
klog.Warningf("WARNING: builtin function %q is being used instead of the requested image %q. "+
"The built-in implementation may differ from the pinned version.", normalized, imageName)
}
Comment on lines +50 to +53
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warning currently triggers for any tag/digest mismatch but is emitted unconditionally once a builtin is found. If you keep the warning, consider downgrading it to a verbose log level (or only warning when the request was explicitly pinned with a tag/digest) to avoid noisy logs for users who intentionally rely on the builtin fallback.

Copilot uses AI. Check for mistakes.
return fn
}

func List() []string {
mu.RLock()
defer mu.RUnlock()
names := make([]string, 0, len(registry))
for name := range registry {
names = append(names, name)
}
return names
}

func normalizeImage(image string) string {
if idx := strings.Index(image, "@"); idx != -1 {
image = image[:idx]
}
parts := strings.Split(image, "/")
if len(parts) > 0 {
last := parts[len(parts)-1]
if idx := strings.Index(last, ":"); idx != -1 {
parts[len(parts)-1] = last[:idx]
}
}
return strings.Join(parts, "/")
}
Loading