Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ require (
github.com/philopon/go-toposort v0.0.0-20170620085441-9be86dbd762f
github.com/pkg/errors v0.9.1
github.com/prep/wasmexec v0.0.0-20220807105708-6554945c1dec
github.com/qri-io/starlib v0.5.0
github.com/regclient/regclient v0.11.1
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/xlab/treeprint v1.2.0
go.starlark.net v0.0.0-20250417143717-f57e51f710eb
golang.org/x/mod v0.29.0
golang.org/x/text v0.31.0
gopkg.in/yaml.v2 v2.4.0
Expand All @@ -43,8 +45,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 +60,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,13 +98,15 @@ 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
Expand Down
90 changes: 90 additions & 0 deletions go.sum

Large diffs are not rendered by default.

89 changes: 89 additions & 0 deletions internal/builtins/applyreplacements/apply_replacements.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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"
"sigs.k8s.io/kustomize/api/filters/replacement"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/fn/framework"
"sigs.k8s.io/kustomize/kyaml/kio"
"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, _ io.Writer) error {
return framework.Execute(
framework.ResourceListProcessorFunc(Process),
&kio.ByteReadWriter{
Reader: r,
Writer: w,
KeepReaderAnnotations: true,
},
)
}

func Process(rl *framework.ResourceList) error {
rep := &Replacements{}

if rl.FunctionConfig == nil {
return fmt.Errorf("FunctionConfig is missing. Expect `ApplyReplacements`")
}

meta, err := rl.FunctionConfig.GetMeta()
if err != nil {
return fmt.Errorf("reading functionConfig metadata: %w", err)
}
if meta.Kind != fnConfigKind || meta.APIVersion != fnConfigAPIVersion {
return fmt.Errorf("received functionConfig of kind %s and apiVersion %s, only functionConfig of kind %s and apiVersion %s is supported",
meta.Kind, meta.APIVersion, fnConfigKind, fnConfigAPIVersion)
}

if err := yaml.Unmarshal([]byte(rl.FunctionConfig.MustString()), rep); err != nil {
return fmt.Errorf("unable to convert functionConfig to replacements: %w", err)
}

transformed, err := replacement.Filter{
Replacements: rep.Replacements,
}.Filter(rl.Items)
if err != nil {
return err
}
rl.Items = transformed
return nil
}

type Replacements struct {
Replacements []types.Replacement `json:"replacements,omitempty" yaml:"replacements,omitempty"`
}
81 changes: 81 additions & 0 deletions internal/builtins/applyreplacements/apply_replacements_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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"
"io"
"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, io.Discard)
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, io.Discard)
assert.Error(t, err)
assert.Contains(t, err.Error(), "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, stderr 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.Contains(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, "/")
}
70 changes: 70 additions & 0 deletions internal/builtins/registry/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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"
"testing"

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

type fakeBuiltin struct {
name string
}

func (f *fakeBuiltin) ImageName() string { return f.name }
func (f *fakeBuiltin) Run(_ io.Reader, _ io.Writer, _ io.Writer) error { return nil }

func TestNormalizeImage(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"ghcr.io/kptdev/starlark:v0.5.0", "ghcr.io/kptdev/starlark"},
{"ghcr.io/kptdev/starlark@sha256:abc123", "ghcr.io/kptdev/starlark"},
{"ghcr.io/kptdev/starlark:v0.5.0@sha256:abc123", "ghcr.io/kptdev/starlark"},
{"ghcr.io/kptdev/starlark", "ghcr.io/kptdev/starlark"},
}
for _, tc := range tests {
t.Run(tc.input, func(t *testing.T) {
assert.Equal(t, tc.expected, normalizeImage(tc.input))
})
}
}

func TestRegisterAndLookup(t *testing.T) {
registry = map[string]BuiltinFunction{}

fn := &fakeBuiltin{name: "ghcr.io/kptdev/test:v1.0"}
Register(fn)

result := Lookup("ghcr.io/kptdev/test:v2.0")
assert.NotNil(t, result)
assert.Equal(t, fn, result)

result = Lookup("ghcr.io/kptdev/unknown")
assert.Nil(t, result)
}

func TestList(t *testing.T) {
registry = map[string]BuiltinFunction{}

Register(&fakeBuiltin{name: "ghcr.io/kptdev/fn1:v1"})
Register(&fakeBuiltin{name: "ghcr.io/kptdev/fn2:v1"})

list := List()
assert.Len(t, list, 2)
}
Loading
Loading