Skip to content

Commit a1fce5e

Browse files
Your Nameabdulrahman11a
authored andcommitted
feat(builtins): built-in runtime for apply-replacements and starlark (#4307)
Closes #4307 Implements a built-in runtime for curated KRM functions inside kpt, allowing apply-replacements and starlark to run without pulling images from Docker Hub, eliminating the external SDK dependency. ## Approach Avoids circular dependency between Porch and kpt by moving the built-in runtime concept into kpt directly using kyaml/fn/framework instead of krm-functions-sdk. ## Architecture A thread-safe self-registration registry (internal/builtins/registry) allows KRM function implementations to register themselves via init(). The fnruntime runner checks this registry before falling back to Docker or WASM, preserving existing behavior for unregistered functions. Priority order in fnruntime/runner.go: 1. pkg-context builtin (existing) 2. Builtin registry (new, no Docker needed) 3. Docker / WASM (fallback, unchanged) ## Functions included - apply-replacements: ghcr.io/kptdev/krm-functions-catalog/apply-replacements - starlark: ghcr.io/kptdev/krm-functions-catalog/starlark ## Implementation notes - Removed dependency on krm-functions-sdk/go/fn and krm-functions-catalog/starlark - Vendored starlark runtime locally using kyaml/yaml instead of SDK ## Verified locally [PASS] apply-replacements in 0s (no Docker) [PASS] starlark in 0s (no Docker) Signed-off-by: abdulrahman11a <[email protected]>
1 parent c4f465b commit a1fce5e

17 files changed

Lines changed: 770 additions & 187 deletions

File tree

go.mod

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ require (
1111
github.com/google/go-containerregistry v0.20.6
1212
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
1313
github.com/kptdev/krm-functions-catalog/functions/go/apply-setters v0.2.2
14-
github.com/kptdev/krm-functions-catalog/functions/go/starlark v0.5.5
1514
github.com/kptdev/krm-functions-sdk/go/fn v1.0.2
1615
github.com/otiai10/copy v1.14.1
1716
github.com/philopon/go-toposort v0.0.0-20170620085441-9be86dbd762f
1817
github.com/pkg/errors v0.9.1
1918
github.com/prep/wasmexec v0.0.0-20220807105708-6554945c1dec
19+
github.com/qri-io/starlib v0.5.0
2020
github.com/regclient/regclient v0.11.1
2121
github.com/spf13/cobra v1.10.2
2222
github.com/spf13/pflag v1.0.10
2323
github.com/stretchr/testify v1.11.1
2424
github.com/xlab/treeprint v1.2.0
25+
go.starlark.net v0.0.0-20250417143717-f57e51f710eb
2526
golang.org/x/mod v0.29.0
2627
golang.org/x/text v0.31.0
2728
gopkg.in/yaml.v2 v2.4.0
@@ -112,7 +113,6 @@ require (
112113
github.com/prometheus/client_model v0.6.2 // indirect
113114
github.com/prometheus/common v0.67.2 // indirect
114115
github.com/prometheus/procfs v0.19.2 // indirect
115-
github.com/qri-io/starlib v0.5.0 // indirect
116116
github.com/russross/blackfriday/v2 v2.1.0 // indirect
117117
github.com/sergi/go-diff v1.4.0 // indirect
118118
github.com/sirupsen/logrus v1.9.3 // indirect
@@ -122,7 +122,6 @@ require (
122122
github.com/x448/float16 v0.8.4 // indirect
123123
go.opentelemetry.io/otel v1.38.0 // indirect
124124
go.opentelemetry.io/otel/trace v1.38.0 // indirect
125-
go.starlark.net v0.0.0-20250417143717-f57e51f710eb // indirect
126125
go.yaml.in/yaml/v2 v2.4.3 // indirect
127126
go.yaml.in/yaml/v3 v3.0.4 // indirect
128127
golang.org/x/net v0.47.0 // indirect

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,6 @@ github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uq
153153
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
154154
github.com/kptdev/krm-functions-catalog/functions/go/apply-setters v0.2.2 h1:PZ4TcVzgad1OFuH4gHg4j2LKC2KXTuzfsQWil2knSlk=
155155
github.com/kptdev/krm-functions-catalog/functions/go/apply-setters v0.2.2/go.mod h1:S8Vrp3yPDp4ga2TOPfZzoO/Y7UGF7KPHS1S0taJ0XOc=
156-
github.com/kptdev/krm-functions-catalog/functions/go/starlark v0.5.5 h1:2fVPRn0knqm4XoXfYN7mWt99MOevHhR8eoKvqnmhzY4=
157-
github.com/kptdev/krm-functions-catalog/functions/go/starlark v0.5.5/go.mod h1:PE/l25mFdKm9MibK2sh/vO1YdFvrMIc3MXwyJW/scB0=
158156
github.com/kptdev/krm-functions-sdk/go/fn v1.0.2 h1:g9N6SW5axEXMagUbHliH14XpfvvvwkAVDLcN3ApVh2M=
159157
github.com/kptdev/krm-functions-sdk/go/fn v1.0.2/go.mod h1:NSfdmtQ9AwNg5wdS9gE/H9SQs7Vomzq7E7N9hyEju1U=
160158
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=

internal/builtins/applyreplacements/apply_replacements.go

Lines changed: 34 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,25 @@
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
66
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
7+
// http://www.apache.org/licenses/LICENSE-2.0
88
//
99
// Unless required by applicable law or agreed to in writing, software
1010
// distributed under the License is distributed on an "AS IS" BASIS,
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
14+
1415
package applyreplacements
1516

1617
import (
1718
"fmt"
1819
"io"
1920

2021
"github.com/kptdev/kpt/internal/builtins/registry"
21-
"github.com/kptdev/krm-functions-sdk/go/fn"
2222
"sigs.k8s.io/kustomize/api/filters/replacement"
2323
"sigs.k8s.io/kustomize/api/types"
24+
"sigs.k8s.io/kustomize/kyaml/fn/framework"
25+
"sigs.k8s.io/kustomize/kyaml/kio"
2426
"sigs.k8s.io/kustomize/kyaml/yaml"
2527
)
2628

@@ -41,85 +43,47 @@ type Runner struct{}
4143

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

44-
func (a *Runner) Run(r io.Reader, w io.Writer) error {
45-
input, err := io.ReadAll(r)
46-
if err != nil {
47-
return fmt.Errorf("reading input: %w", err)
48-
}
49-
rl, err := fn.ParseResourceList(input)
50-
if err != nil {
51-
return fmt.Errorf("parsing ResourceList: %w", err)
52-
}
53-
if _, err := applyReplacements(rl); err != nil {
54-
return err
55-
}
56-
out, err := rl.ToYAML()
57-
if err != nil {
58-
return err
59-
}
60-
_, err = w.Write(out)
61-
return err
62-
}
63-
func applyReplacements(rl *fn.ResourceList) (bool, error) {
64-
r := &Replacements{}
65-
return r.Process(rl)
46+
func (a *Runner) Run(r io.Reader, w io.Writer, _ io.Writer) error {
47+
return framework.Execute(
48+
framework.ResourceListProcessorFunc(Process),
49+
&kio.ByteReadWriter{
50+
Reader: r,
51+
Writer: w,
52+
KeepReaderAnnotations: true,
53+
},
54+
)
6655
}
6756

68-
type Replacements struct {
69-
Replacements []types.Replacement `json:"replacements,omitempty" yaml:"replacements,omitempty"`
70-
}
57+
func Process(rl *framework.ResourceList) error {
58+
rep := &Replacements{}
7159

72-
func (r *Replacements) Config(functionConfig *fn.KubeObject) error {
73-
if functionConfig.IsEmpty() {
60+
if rl.FunctionConfig == nil {
7461
return fmt.Errorf("FunctionConfig is missing. Expect `ApplyReplacements`")
7562
}
76-
if functionConfig.GetKind() != fnConfigKind || functionConfig.GetAPIVersion() != fnConfigAPIVersion {
77-
return fmt.Errorf("received functionConfig of kind %s and apiVersion %s, only functionConfig of kind %s and apiVersion %s is supported",
78-
functionConfig.GetKind(), functionConfig.GetAPIVersion(), fnConfigKind, fnConfigAPIVersion)
63+
64+
meta, err := rl.FunctionConfig.GetMeta()
65+
if err != nil {
66+
return fmt.Errorf("reading functionConfig metadata: %w", err)
7967
}
80-
r.Replacements = []types.Replacement{}
81-
if err := functionConfig.As(r); err != nil {
82-
return fmt.Errorf("unable to convert functionConfig to replacements:\n%w", err)
68+
if meta.Kind != fnConfigKind || meta.APIVersion != fnConfigAPIVersion {
69+
return fmt.Errorf("received functionConfig of kind %s and apiVersion %s, only functionConfig of kind %s and apiVersion %s is supported",
70+
meta.Kind, meta.APIVersion, fnConfigKind, fnConfigAPIVersion)
8371
}
84-
return nil
85-
}
8672

87-
func (r *Replacements) Process(rl *fn.ResourceList) (bool, error) {
88-
if err := r.Config(rl.FunctionConfig); err != nil {
89-
rl.LogResult(err)
90-
return false, err
73+
if err := yaml.Unmarshal([]byte(rl.FunctionConfig.MustString()), rep); err != nil {
74+
return fmt.Errorf("unable to convert functionConfig to replacements: %w", err)
9175
}
92-
transformedItems, err := r.Transform(rl.Items)
76+
77+
transformed, err := replacement.Filter{
78+
Replacements: rep.Replacements,
79+
}.Filter(rl.Items)
9380
if err != nil {
94-
rl.LogResult(err)
95-
return false, err
81+
return err
9682
}
97-
rl.Items = transformedItems
98-
return true, nil
83+
rl.Items = transformed
84+
return nil
9985
}
10086

101-
func (r *Replacements) Transform(items []*fn.KubeObject) ([]*fn.KubeObject, error) {
102-
var transformedItems []*fn.KubeObject
103-
var nodes []*yaml.RNode
104-
for _, obj := range items {
105-
objRN, err := yaml.Parse(obj.String())
106-
if err != nil {
107-
return nil, err
108-
}
109-
nodes = append(nodes, objRN)
110-
}
111-
transformedNodes, err := replacement.Filter{
112-
Replacements: r.Replacements,
113-
}.Filter(nodes)
114-
if err != nil {
115-
return nil, err
116-
}
117-
for _, n := range transformedNodes {
118-
obj, err := fn.ParseKubeObject([]byte(n.MustString()))
119-
if err != nil {
120-
return nil, err
121-
}
122-
transformedItems = append(transformedItems, obj)
123-
}
124-
return transformedItems, nil
87+
type Replacements struct {
88+
Replacements []types.Replacement `json:"replacements,omitempty" yaml:"replacements,omitempty"`
12589
}

internal/builtins/applyreplacements/apply_replacements_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package applyreplacements
1616

1717
import (
1818
"bytes"
19+
"io"
1920
"testing"
2021

2122
"github.com/stretchr/testify/assert"
@@ -51,7 +52,7 @@ functionConfig:
5152
w := &bytes.Buffer{}
5253

5354
runner := &Runner{}
54-
err := runner.Run(r, w)
55+
err := runner.Run(r, w, io.Discard)
5556
assert.NoError(t, err)
5657
assert.Contains(t, w.String(), "namespace: my-app")
5758
}
@@ -74,7 +75,7 @@ functionConfig:
7475
w := &bytes.Buffer{}
7576

7677
runner := &Runner{}
77-
err := runner.Run(r, w)
78-
assert.NoError(t, err)
79-
assert.Contains(t, w.String(), "only functionConfig of kind ApplyReplacements")
78+
err := runner.Run(r, w, io.Discard)
79+
assert.Error(t, err)
80+
assert.Contains(t, err.Error(), "only functionConfig of kind ApplyReplacements")
8081
}

internal/builtins/registry/registry.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424

2525
type BuiltinFunction interface {
2626
ImageName() string
27-
Run(r io.Reader, w io.Writer) error
27+
Run(r io.Reader, w io.Writer, stderr io.Writer) error
2828
}
2929

3030
var (

internal/builtins/registry/registry_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ type fakeBuiltin struct {
2525
name string
2626
}
2727

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

3131
func TestNormalizeImage(t *testing.T) {
3232
tests := []struct {

internal/builtins/starlark/config.go

Lines changed: 30 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ package starlark
1717
import (
1818
"fmt"
1919

20-
starlarkruntime "github.com/kptdev/krm-functions-catalog/functions/go/starlark/third_party/sigs.k8s.io/kustomize/kyaml/fn/runtime/starlark"
21-
"github.com/kptdev/krm-functions-sdk/go/fn"
20+
starlarkruntime "github.com/kptdev/kpt/internal/builtins/starlark/runtime"
2221
corev1 "k8s.io/api/core/v1"
2322
"k8s.io/apimachinery/pkg/runtime/schema"
23+
"sigs.k8s.io/kustomize/kyaml/fn/framework"
2424
"sigs.k8s.io/kustomize/kyaml/yaml"
2525
)
2626

@@ -40,22 +40,29 @@ const (
4040

4141
type Run struct {
4242
yaml.ResourceMeta `json:",inline" yaml:",inline"`
43-
// Source is a required field for providing a starlark script inline.
44-
Source string `json:"source" yaml:"source"`
45-
// Params are the parameters in key-value pairs format.
46-
Params map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"`
43+
Source string `json:"source" yaml:"source"`
44+
Params map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"`
4745
}
4846

49-
func (sr *Run) Config(fnCfg *fn.KubeObject) error {
50-
switch {
51-
case fnCfg.IsEmpty():
47+
func (sr *Run) Config(fnCfg *yaml.RNode) error {
48+
if fnCfg == nil {
5249
return fmt.Errorf("FunctionConfig is missing. Expect `ConfigMap`, `StarlarkRun`, or `Run`")
53-
case fnCfg.IsGVK("", configMapAPIVersion, configMapKind):
50+
}
51+
52+
meta, err := fnCfg.GetMeta()
53+
if err != nil {
54+
return fmt.Errorf("reading functionConfig metadata: %w", err)
55+
}
56+
57+
apiVersion := meta.APIVersion
58+
kind := meta.Kind
59+
60+
switch {
61+
case apiVersion == configMapAPIVersion && kind == configMapKind:
5462
cm := &corev1.ConfigMap{}
55-
if err := fnCfg.As(cm); err != nil {
63+
if err := fnCfg.YNode().Decode(cm); err != nil {
5664
return err
5765
}
58-
// Convert ConfigMap to StarlarkRun
5966
sr.Name = cm.Name
6067
sr.Namespace = cm.Namespace
6168
sr.Params = map[string]interface{}{}
@@ -65,63 +72,34 @@ func (sr *Run) Config(fnCfg *fn.KubeObject) error {
6572
}
6673
sr.Params[k] = v
6774
}
68-
case fnCfg.IsGVK(starlarkRunGroup, starlarkRunVersion, starlarkRunKind),
69-
fnCfg.IsGVK(starlarkRunGroup, starlarkRunVersion, "Run"):
70-
if err := fnCfg.As(sr); err != nil {
75+
76+
case (apiVersion == starlarkRunAPIVersion && kind == starlarkRunKind) ||
77+
(apiVersion == starlarkRunAPIVersion && kind == "Run"):
78+
if err := fnCfg.YNode().Decode(sr); err != nil {
7179
return err
7280
}
81+
7382
default:
7483
return fmt.Errorf("`functionConfig` must be either %v, %v, or %v but we got: %v",
7584
schema.FromAPIVersionAndKind(configMapAPIVersion, configMapKind).String(),
7685
schema.FromAPIVersionAndKind(starlarkRunAPIVersion, starlarkRunKind).String(),
7786
schema.FromAPIVersionAndKind(starlarkRunAPIVersion, "Run").String(),
78-
schema.FromAPIVersionAndKind(fnCfg.GetAPIVersion(), fnCfg.GetKind()).String())
87+
schema.FromAPIVersionAndKind(apiVersion, kind).String())
7988
}
8089

81-
// Defaulting
8290
if sr.Name == "" {
8391
sr.Name = defaultProgramName
8492
}
85-
// Validation
8693
if sr.Source == "" {
8794
return fmt.Errorf("`source` must not be empty")
8895
}
8996
return nil
9097
}
9198

92-
func (sr *Run) Transform(rl *fn.ResourceList) error {
93-
var transformedObjects []*fn.KubeObject
94-
var nodes []*yaml.RNode
95-
96-
fcRN, err := yaml.Parse(rl.FunctionConfig.String())
97-
if err != nil {
98-
return err
99-
}
100-
for _, obj := range rl.Items {
101-
objRN, err := yaml.Parse(obj.String())
102-
if err != nil {
103-
return err
104-
}
105-
nodes = append(nodes, objRN)
106-
}
107-
108-
starFltr := &starlarkruntime.SimpleFilter{
109-
Name: sr.Name,
110-
Program: sr.Source,
111-
FunctionConfig: fcRN,
112-
}
113-
transformedNodes, err := starFltr.Filter(nodes)
114-
if err != nil {
115-
return err
116-
}
117-
118-
for _, n := range transformedNodes {
119-
obj, err := fn.ParseKubeObject([]byte(n.MustString()))
120-
if err != nil {
121-
return err
122-
}
123-
transformedObjects = append(transformedObjects, obj)
99+
func (sr *Run) Transform(rl *framework.ResourceList) error {
100+
starFltr := &starlarkruntime.Filter{
101+
Name: sr.Name,
102+
Program: sr.Source,
124103
}
125-
rl.Items = transformedObjects
126-
return nil
104+
return rl.Filter(starFltr)
127105
}

internal/builtins/starlark/config_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ package starlark
1616
import (
1717
"testing"
1818

19-
"github.com/kptdev/krm-functions-sdk/go/fn"
2019
"github.com/stretchr/testify/assert"
20+
"sigs.k8s.io/kustomize/kyaml/yaml"
2121
)
2222

2323
func TestStarlarkConfig(t *testing.T) {
@@ -84,12 +84,14 @@ data:
8484
expectErrMsg: "`source` must not be empty",
8585
},
8686
}
87+
8788
for _, tc := range testcases {
8889
t.Run(tc.name, func(t *testing.T) {
8990
sr := &Run{}
90-
ko, err := fn.ParseKubeObject([]byte(tc.config))
91+
node, err := yaml.Parse(tc.config)
9192
assert.NoError(t, err)
92-
err = sr.Config(ko)
93+
94+
err = sr.Config(node)
9395
if tc.expectErrMsg == "" {
9496
assert.NoError(t, err)
9597
} else {

0 commit comments

Comments
 (0)