From 4ee996af6851bfcfb90f836ad5bd93d6590bec35 Mon Sep 17 00:00:00 2001 From: Abdulrahman Fikry Date: Thu, 12 Mar 2026 17:49:29 +0200 Subject: [PATCH 1/6] feat: add builtin runtime support Signed-off-by: Abdulrahman Fikry --- go.mod | 9 ++ go.sum | 92 +++++++++++++ .../applyreplacements/apply_replacements.go | 122 +++++++++++++++++ internal/builtins/builtin_runtime.go | 22 +++ internal/builtins/registry/registry.go | 67 ++++++++++ internal/builtins/starlark/config.go | 126 ++++++++++++++++++ internal/builtins/starlark/config_test.go | 101 ++++++++++++++ internal/builtins/starlark/processor.go | 41 ++++++ internal/builtins/starlark/starlark.go | 53 ++++++++ internal/fnruntime/runner.go | 3 + 10 files changed, 636 insertions(+) create mode 100644 internal/builtins/applyreplacements/apply_replacements.go create mode 100644 internal/builtins/builtin_runtime.go create mode 100644 internal/builtins/registry/registry.go create mode 100644 internal/builtins/starlark/config.go create mode 100644 internal/builtins/starlark/config_test.go create mode 100644 internal/builtins/starlark/processor.go create mode 100644 internal/builtins/starlark/starlark.go diff --git a/go.mod b/go.mod index b5a1effca2..fb4e4f1fd9 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -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 @@ -92,6 +97,7 @@ 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 @@ -99,12 +105,14 @@ require ( 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 @@ -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 diff --git a/go.sum b/go.sum index e7dab440ff..31a635745d 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,19 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/360EntSecGroup-Skylar/excelize v1.4.1 h1:l55mJb6rkkaUzOpSsgEeKYtS6/0gHwBYyfo5Jcjv/Ks= +github.com/360EntSecGroup-Skylar/excelize v1.4.1/go.mod h1:vnax29X2usfl7HHkBrX5EvSCJcmH3dT9luvxzu8iGAE= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -14,10 +22,15 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bytecodealliance/wasmtime-go v1.0.0 h1:9u9gqaUiaJeN5IoD1L7egD8atOnTGyJcNp8BhkL9cUU= github.com/bytecodealliance/wasmtime-go v1.0.0/go.mod h1:jjlqQbWUfVSbehpErw3UoWFndBXRRMvfikYH6KsCwOg= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/containerd/stargz-snapshotter/estargz v0.18.0 h1:Ny5yptQgEXSkDFKvlKJGTvf1YJ+4xD8V+hXqoRG0n74= github.com/containerd/stargz-snapshotter/estargz v0.18.0/go.mod h1:7hfU1BO2KB3axZl0dRQCdnHrIWw7TRDdK6L44Rdeuo0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -37,8 +50,12 @@ github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/dustmop/soup v1.1.2-0.20190516214245-38228baa104e h1:44fmjqDtdCiUNlSjJVp+w1AOs6na3Y6Ai0aIeseFjkI= +github.com/dustmop/soup v1.1.2-0.20190516214245-38228baa104e/go.mod h1:CgNC6SGbT+Xb8wGGvzilttZL1mc5sQ/5KkcxsZttMIk= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= @@ -85,12 +102,29 @@ github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91o github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= @@ -112,12 +146,15 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kptdev/krm-functions-catalog/functions/go/apply-setters v0.2.2 h1:PZ4TcVzgad1OFuH4gHg4j2LKC2KXTuzfsQWil2knSlk= github.com/kptdev/krm-functions-catalog/functions/go/apply-setters v0.2.2/go.mod h1:S8Vrp3yPDp4ga2TOPfZzoO/Y7UGF7KPHS1S0taJ0XOc= +github.com/kptdev/krm-functions-catalog/functions/go/starlark v0.5.5 h1:2fVPRn0knqm4XoXfYN7mWt99MOevHhR8eoKvqnmhzY4= +github.com/kptdev/krm-functions-catalog/functions/go/starlark v0.5.5/go.mod h1:PE/l25mFdKm9MibK2sh/vO1YdFvrMIc3MXwyJW/scB0= github.com/kptdev/krm-functions-sdk/go/fn v1.0.2 h1:g9N6SW5axEXMagUbHliH14XpfvvvwkAVDLcN3ApVh2M= github.com/kptdev/krm-functions-sdk/go/fn v1.0.2/go.mod h1:NSfdmtQ9AwNg5wdS9gE/H9SQs7Vomzq7E7N9hyEju1U= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -147,12 +184,15 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olareg/olareg v0.1.2 h1:75G8X6E9FUlzL/CSjgFcYfMgNzlc7CxULpUUNsZBIvI= github.com/olareg/olareg v0.1.2/go.mod h1:TWs+N6pO1S4bdB6eerzUm/ITRQ6kw91mVf9ZYeGtw+Y= github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= @@ -167,10 +207,13 @@ github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/paulmach/orb v0.1.5 h1:GUcATabvxciqEzGd+c01/9ek3B6pUp9OdcIHFSDDSSg= +github.com/paulmach/orb v0.1.5/go.mod h1:pPwxxs3zoAyosNSbNKn1jiXV2+oovRDObDKfTvRegDI= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/philopon/go-toposort v0.0.0-20170620085441-9be86dbd762f h1:WyCn68lTiytVSkk7W1K9nBiSGTSRlUOdyTnSjwrIlok= github.com/philopon/go-toposort v0.0.0-20170620085441-9be86dbd762f/go.mod h1:/iRjX3DdSK956SzsUdV55J+wIsQ+2IBWmBrB4RvZfk4= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -180,12 +223,15 @@ github.com/prep/wasmexec v0.0.0-20220807105708-6554945c1dec h1:Yz85KaIsPzF0YRrzn github.com/prep/wasmexec v0.0.0-20220807105708-6554945c1dec/go.mod h1:/AG7CoBOwtk42jdCTLbd280PA4h50oaFK88jee+BaVA= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/qri-io/starlib v0.5.0 h1:NlveoBAhO6mNgM7+JpM9QlHh3/3pOtOiH6iXaqSdVK0= +github.com/qri-io/starlib v0.5.0/go.mod h1:FpVumyB2CMrKIrjf39fAi4uydYWVvnWEvXEOwfzZRHY= github.com/regclient/regclient v0.11.1 h1:MtxUaEVh2bgBzAX9wqH71cB4NWom4EdZ/31Z9f7ZwCU= github.com/regclient/regclient v0.11.1/go.mod h1:4Wu8lxr/v0QzrIId6cJj/2BH8gP3dUHes37lZJP0J90= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -206,8 +252,10 @@ github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHd github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -226,6 +274,9 @@ go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.starlark.net v0.0.0-20210406145628-7a1108eaa012/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= +go.starlark.net v0.0.0-20250417143717-f57e51f710eb h1:zOg9DxxrorEmgGUr5UPdCEwKqiqG0MlZciuCuA3XiDE= +go.starlark.net v0.0.0-20250417143717-f57e51f710eb/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -239,25 +290,41 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -272,6 +339,11 @@ golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -281,10 +353,27 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= @@ -292,6 +381,7 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWM gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -301,6 +391,8 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= diff --git a/internal/builtins/applyreplacements/apply_replacements.go b/internal/builtins/applyreplacements/apply_replacements.go new file mode 100644 index 0000000000..eeede3aea1 --- /dev/null +++ b/internal/builtins/applyreplacements/apply_replacements.go @@ -0,0 +1,122 @@ +// 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" +) + +func init() { + registry.Register(&ApplyReplacementsRunner{}) +} + +type ApplyReplacementsRunner struct{} + +func (a *ApplyReplacementsRunner) ImageName() string { return ImageName } + +func (a *ApplyReplacementsRunner) 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, nil + } + transformedItems, err := r.Transform(rl.Items) + if err != nil { + rl.LogResult(err) + return false, nil + } + 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 +} diff --git a/internal/builtins/builtin_runtime.go b/internal/builtins/builtin_runtime.go new file mode 100644 index 0000000000..f62b290fd4 --- /dev/null +++ b/internal/builtins/builtin_runtime.go @@ -0,0 +1,22 @@ +// 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. +package builtins + +import ( + // Register built-in functions via init() + _ "github.com/kptdev/kpt/internal/builtins/applyreplacements" + _ "github.com/kptdev/kpt/internal/builtins/starlark" +) diff --git a/internal/builtins/registry/registry.go b/internal/builtins/registry/registry.go new file mode 100644 index 0000000000..683abc6b35 --- /dev/null +++ b/internal/builtins/registry/registry.go @@ -0,0 +1,67 @@ +// 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" +) + +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() + return registry[normalizeImage(imageName)] +} + +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, "/") +} diff --git a/internal/builtins/starlark/config.go b/internal/builtins/starlark/config.go new file mode 100644 index 0000000000..be399124f4 --- /dev/null +++ b/internal/builtins/starlark/config.go @@ -0,0 +1,126 @@ +// Copyright 2026 Google LLC +// Modifications Copyright (C) 2025-2026 OpenInfra Foundation Europe +// +// 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 starlark + +import ( + "fmt" + + "github.com/kptdev/krm-functions-catalog/functions/go/starlark/third_party/sigs.k8s.io/kustomize/kyaml/fn/runtime/starlark" + "github.com/kptdev/krm-functions-sdk/go/fn" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +const ( + starlarkRunGroup = "fn.kpt.dev" + starlarkRunVersion = "v1alpha1" + starlarkRunAPIVersion = starlarkRunGroup + "/" + starlarkRunVersion + starlarkRunKind = "StarlarkRun" + + configMapAPIVersion = "v1" + configMapKind = "ConfigMap" + + sourceKey = "source" + + defaultProgramName = "starlark-function-run" +) + +type Run struct { + yaml.ResourceMeta `json:",inline" yaml:",inline"` + // Source is a required field for providing a starlark script inline. + Source string `json:"source" yaml:"source"` + // Params are the parameters in key-value pairs format. + Params map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"` +} + +func (sr *Run) Config(fnCfg *fn.KubeObject) error { + switch { + case fnCfg.IsEmpty(): + return fmt.Errorf("FunctionConfig is missing. Expect `ConfigMap` or `StarlarkRun`") + case fnCfg.IsGVK("", configMapAPIVersion, configMapKind): + cm := &corev1.ConfigMap{} + if err := fnCfg.As(cm); err != nil { + return err + } + // Convert ConfigMap to StarlarkRun + sr.Name = cm.Name + sr.Namespace = cm.Namespace + sr.Params = map[string]interface{}{} + for k, v := range cm.Data { + if k == sourceKey { + sr.Source = v + } + sr.Params[k] = v + } + case fnCfg.IsGVK(starlarkRunGroup, starlarkRunVersion, starlarkRunKind), + fnCfg.IsGVK(starlarkRunGroup, starlarkRunVersion, "Run"): + if err := fnCfg.As(sr); err != nil { + return err + } + default: + return fmt.Errorf("`functionConfig` must be either %v or %v, but we got: %v", + schema.FromAPIVersionAndKind(configMapAPIVersion, configMapKind).String(), + schema.FromAPIVersionAndKind(starlarkRunAPIVersion, starlarkRunKind).String(), + schema.FromAPIVersionAndKind(fnCfg.GetAPIVersion(), fnCfg.GetKind()).String()) + } + + // Defaulting + if sr.Name == "" { + sr.Name = defaultProgramName + } + // Validation + if sr.Source == "" { + return fmt.Errorf("`source` must not be empty") + } + return nil +} + +func (sr *Run) Transform(rl *fn.ResourceList) error { + var transformedObjects []*fn.KubeObject + var nodes []*yaml.RNode + + fcRN, err := yaml.Parse(rl.FunctionConfig.String()) + if err != nil { + return err + } + for _, obj := range rl.Items { + objRN, err := yaml.Parse(obj.String()) + if err != nil { + return err + } + nodes = append(nodes, objRN) + } + + starFltr := &starlark.SimpleFilter{ + Name: sr.Name, + Program: sr.Source, + FunctionConfig: fcRN, + } + transformedNodes, err := starFltr.Filter(nodes) + if err != nil { + return err + } + + for _, n := range transformedNodes { + obj, err := fn.ParseKubeObject([]byte(n.MustString())) + if err != nil { + return err + } + transformedObjects = append(transformedObjects, obj) + } + rl.Items = transformedObjects + return nil +} diff --git a/internal/builtins/starlark/config_test.go b/internal/builtins/starlark/config_test.go new file mode 100644 index 0000000000..67a0bdb4c6 --- /dev/null +++ b/internal/builtins/starlark/config_test.go @@ -0,0 +1,101 @@ +// 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 starlark + +import ( + "testing" + + "github.com/kptdev/krm-functions-sdk/go/fn" + "github.com/stretchr/testify/assert" +) + +func TestStarlarkConfig(t *testing.T) { + testcases := []struct { + name string + config string + expectErrMsg string + }{ + { + name: "valid Run", + config: `apiVersion: fn.kpt.dev/v1alpha1 +kind: Run +metadata: + name: my-star-fn + namespace: foo +source: | + def run(r, ns_value): + for resource in r: + resource["metadata"]["namespace"] = ns_value + run(ctx.resource_list["items"], "baz") +`, + }, + { + name: "Run missing Source", + config: `apiVersion: fn.kpt.dev/v1alpha1 +kind: Run +metadata: + name: my-star-fn +`, + expectErrMsg: "`source` must not be empty", + }, + { + name: "valid ConfigMap", + config: `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-star-fn +data: + source: | + def run(r, ns_value): + for resource in r: + resource["metadata"]["namespace"] = ns_value + run(ctx.resource_list["items"], "baz") +`, + }, + { + name: "ConfigMap missing source", + config: `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-star-fn +`, + expectErrMsg: "`source` must not be empty", + }, + { + name: "ConfigMap with parameter but missing source", + config: `apiVersion: v1 +kind: ConfigMap +metadata: + name: my-star-fn +data: + param1: foo +`, + expectErrMsg: "`source` must not be empty", + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + sr := &Run{} + ko, err := fn.ParseKubeObject([]byte(tc.config)) + assert.NoError(t, err) + err = sr.Config(ko) + if tc.expectErrMsg == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectErrMsg) + } + }) + } +} diff --git a/internal/builtins/starlark/processor.go b/internal/builtins/starlark/processor.go new file mode 100644 index 0000000000..050b332f2d --- /dev/null +++ b/internal/builtins/starlark/processor.go @@ -0,0 +1,41 @@ +// 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 starlark + +import ( + "github.com/kptdev/krm-functions-sdk/go/fn" +) + +// Process runs the Starlark function on the given ResourceList +func Process(resourceList *fn.ResourceList) (bool, error) { + err := func() error { + sr := &Run{} + if err := sr.Config(resourceList.FunctionConfig); err != nil { + return err + } + return sr.Transform(resourceList) + }() + + if err != nil { + resourceList.Results = []*fn.Result{ + { + Message: err.Error(), + Severity: fn.Error, + }, + } + return false, nil + } + + return true, nil +} diff --git a/internal/builtins/starlark/starlark.go b/internal/builtins/starlark/starlark.go new file mode 100644 index 0000000000..e5a97046a9 --- /dev/null +++ b/internal/builtins/starlark/starlark.go @@ -0,0 +1,53 @@ +// 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 starlark + +import ( + "fmt" + "io" + + "github.com/kptdev/kpt/internal/builtins/registry" + "github.com/kptdev/krm-functions-sdk/go/fn" +) + +const ImageName = "ghcr.io/kptdev/krm-functions-catalog/starlark" + +func init() { + registry.Register(&StarlarkRunner{}) +} + +type StarlarkRunner struct{} + +func (s *StarlarkRunner) ImageName() string { return ImageName } + +func (s *StarlarkRunner) 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 := Process(rl); err != nil { + return err + } + out, err := rl.ToYAML() + if err != nil { + return err + } + _, err = w.Write(out) + return err +} diff --git a/internal/fnruntime/runner.go b/internal/fnruntime/runner.go index ca9c968b81..40e9cf51fd 100644 --- a/internal/fnruntime/runner.go +++ b/internal/fnruntime/runner.go @@ -27,6 +27,7 @@ import ( "github.com/google/shlex" "github.com/kptdev/kpt/internal/builtins" + builtinsregistry "github.com/kptdev/kpt/internal/builtins/registry" "github.com/kptdev/kpt/internal/pkg" "github.com/kptdev/kpt/internal/types" fnresult "github.com/kptdev/kpt/pkg/api/fnresult/v1" @@ -90,6 +91,8 @@ func NewRunner( if f.Image == runneroptions.FuncGenPkgContext { pkgCtxGenerator := &builtins.PackageContextGenerator{} fltr.Run = pkgCtxGenerator.Run + } else if builtinFn := builtinsregistry.Lookup(f.Image); builtinFn != nil { + fltr.Run = builtinFn.Run } else { switch { case f.Image != "": From e51e4c53bcd1ab486d0c88c1bb52421a7cd05fb3 Mon Sep 17 00:00:00 2001 From: Abdulrahman Fikry Date: Thu, 12 Mar 2026 17:49:29 +0200 Subject: [PATCH 2/6] feat: add builtin runtime support Signed-off-by: Abdulrahman Fikry Signed-off-by: abdulrahman11a --- .../applyreplacements/apply_replacements.go | 13 +-- .../apply_replacements_test.go | 80 +++++++++++++++++++ internal/builtins/builtin_runtime.go | 6 +- internal/builtins/registry/registry.go | 9 ++- internal/builtins/registry/registry_test.go | 70 ++++++++++++++++ internal/builtins/starlark/config.go | 4 +- internal/builtins/starlark/processor.go | 2 +- internal/builtins/starlark/processor_test.go | 72 +++++++++++++++++ internal/builtins/starlark/starlark.go | 23 +++--- 9 files changed, 258 insertions(+), 21 deletions(-) create mode 100644 internal/builtins/applyreplacements/apply_replacements_test.go create mode 100644 internal/builtins/registry/registry_test.go create mode 100644 internal/builtins/starlark/processor_test.go diff --git a/internal/builtins/applyreplacements/apply_replacements.go b/internal/builtins/applyreplacements/apply_replacements.go index eeede3aea1..a3d46b06cd 100644 --- a/internal/builtins/applyreplacements/apply_replacements.go +++ b/internal/builtins/applyreplacements/apply_replacements.go @@ -30,15 +30,18 @@ const ( fnConfigAPIVersion = "fn.kpt.dev/v1alpha1" ) -func init() { - registry.Register(&ApplyReplacementsRunner{}) +//nolint:gochecknoinits +func init() { Register() } + +func Register() { + registry.Register(&Runner{}) } -type ApplyReplacementsRunner struct{} +type Runner struct{} -func (a *ApplyReplacementsRunner) ImageName() string { return ImageName } +func (a *Runner) ImageName() string { return ImageName } -func (a *ApplyReplacementsRunner) Run(r io.Reader, w io.Writer) error { +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) diff --git a/internal/builtins/applyreplacements/apply_replacements_test.go b/internal/builtins/applyreplacements/apply_replacements_test.go new file mode 100644 index 0000000000..5322743c66 --- /dev/null +++ b/internal/builtins/applyreplacements/apply_replacements_test.go @@ -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") +} diff --git a/internal/builtins/builtin_runtime.go b/internal/builtins/builtin_runtime.go index f62b290fd4..586be1e63b 100644 --- a/internal/builtins/builtin_runtime.go +++ b/internal/builtins/builtin_runtime.go @@ -12,11 +12,13 @@ // 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. +// Package builtins registers all built-in KRM functions into the builtin registry +// via their init() functions. package builtins import ( - // Register built-in functions via init() + // 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" ) diff --git a/internal/builtins/registry/registry.go b/internal/builtins/registry/registry.go index 683abc6b35..98b91da2ff 100644 --- a/internal/builtins/registry/registry.go +++ b/internal/builtins/registry/registry.go @@ -16,6 +16,7 @@ package registry import ( "io" + "log" "strings" "sync" ) @@ -39,7 +40,13 @@ func Register(fn BuiltinFunction) { func Lookup(imageName string) BuiltinFunction { mu.RLock() defer mu.RUnlock() - return registry[normalizeImage(imageName)] + normalized := normalizeImage(imageName) + fn := registry[normalized] + if fn != nil && imageName != normalized { + log.Printf("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) + } + return fn } func List() []string { diff --git a/internal/builtins/registry/registry_test.go b/internal/builtins/registry/registry_test.go new file mode 100644 index 0000000000..5725acbfb6 --- /dev/null +++ b/internal/builtins/registry/registry_test.go @@ -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) 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) +} diff --git a/internal/builtins/starlark/config.go b/internal/builtins/starlark/config.go index be399124f4..2b3f8d7bb5 100644 --- a/internal/builtins/starlark/config.go +++ b/internal/builtins/starlark/config.go @@ -17,7 +17,7 @@ package starlark import ( "fmt" - "github.com/kptdev/krm-functions-catalog/functions/go/starlark/third_party/sigs.k8s.io/kustomize/kyaml/fn/runtime/starlark" + starlarkruntime "github.com/kptdev/krm-functions-catalog/functions/go/starlark/third_party/sigs.k8s.io/kustomize/kyaml/fn/runtime/starlark" "github.com/kptdev/krm-functions-sdk/go/fn" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -104,7 +104,7 @@ func (sr *Run) Transform(rl *fn.ResourceList) error { nodes = append(nodes, objRN) } - starFltr := &starlark.SimpleFilter{ + starFltr := &starlarkruntime.SimpleFilter{ Name: sr.Name, Program: sr.Source, FunctionConfig: fcRN, diff --git a/internal/builtins/starlark/processor.go b/internal/builtins/starlark/processor.go index 050b332f2d..0bb8ad507e 100644 --- a/internal/builtins/starlark/processor.go +++ b/internal/builtins/starlark/processor.go @@ -34,7 +34,7 @@ func Process(resourceList *fn.ResourceList) (bool, error) { Severity: fn.Error, }, } - return false, nil + return false, err } return true, nil diff --git a/internal/builtins/starlark/processor_test.go b/internal/builtins/starlark/processor_test.go new file mode 100644 index 0000000000..f9a5ef8977 --- /dev/null +++ b/internal/builtins/starlark/processor_test.go @@ -0,0 +1,72 @@ +// 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 starlark + +import ( + "testing" + + "github.com/kptdev/krm-functions-sdk/go/fn" + "github.com/stretchr/testify/assert" +) + +func TestProcess_SetNamespace(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: StarlarkRun + metadata: + name: set-namespace + source: | + def run(r, ns_value): + for resource in r: + resource["metadata"]["namespace"] = ns_value + run(ctx.resource_list["items"], "new-namespace") +` + rl, err := fn.ParseResourceList([]byte(input)) + assert.NoError(t, err) + + ok, err := Process(rl) + assert.NoError(t, err) + assert.True(t, ok) + assert.Equal(t, "new-namespace", rl.Items[0].GetNamespace()) +} + +func TestProcess_InvalidScript(t *testing.T) { + input := `apiVersion: config.kubernetes.io/v1 +kind: ResourceList +items: [] +functionConfig: + apiVersion: fn.kpt.dev/v1alpha1 + kind: StarlarkRun + metadata: + name: bad-script + source: | + this is not valid starlark!!! +` + rl, err := fn.ParseResourceList([]byte(input)) + assert.NoError(t, err) + + ok, err := Process(rl) + assert.Error(t, err) + assert.False(t, ok) + assert.Len(t, rl.Results, 1) +} diff --git a/internal/builtins/starlark/starlark.go b/internal/builtins/starlark/starlark.go index e5a97046a9..7ea0125d82 100644 --- a/internal/builtins/starlark/starlark.go +++ b/internal/builtins/starlark/starlark.go @@ -24,15 +24,18 @@ import ( const ImageName = "ghcr.io/kptdev/krm-functions-catalog/starlark" -func init() { - registry.Register(&StarlarkRunner{}) +//nolint:gochecknoinits +func init() { Register() } + +func Register() { + registry.Register(&Runner{}) } -type StarlarkRunner struct{} +type Runner struct{} -func (s *StarlarkRunner) ImageName() string { return ImageName } +func (s *Runner) ImageName() string { return ImageName } -func (s *StarlarkRunner) Run(r io.Reader, w io.Writer) error { +func (s *Runner) Run(r io.Reader, w io.Writer) error { input, err := io.ReadAll(r) if err != nil { return fmt.Errorf("reading input: %w", err) @@ -41,13 +44,13 @@ func (s *StarlarkRunner) Run(r io.Reader, w io.Writer) error { if err != nil { return fmt.Errorf("parsing ResourceList: %w", err) } - if _, err := Process(rl); err != nil { - return err - } + _, processErr := Process(rl) out, err := rl.ToYAML() if err != nil { return err } - _, err = w.Write(out) - return err + if _, err = w.Write(out); err != nil { + return err + } + return processErr } From c4f465b0bce095ab9180e085de0d02dde73ee735 Mon Sep 17 00:00:00 2001 From: abdulrahman11a Date: Wed, 1 Apr 2026 20:41:19 +0200 Subject: [PATCH 3/6] Fix builtin starlark runtime to return stderr like Docker Signed-off-by: abdulrahman11a --- .../builtins/applyreplacements/apply_replacements.go | 4 ++-- internal/builtins/registry/registry.go | 9 +++++++-- internal/builtins/starlark/config.go | 5 +++-- internal/builtins/starlark/processor.go | 4 +++- internal/fnruntime/runner.go | 9 +++++++++ pkg/api/kptfile/v1/types.go | 4 ++-- 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/internal/builtins/applyreplacements/apply_replacements.go b/internal/builtins/applyreplacements/apply_replacements.go index a3d46b06cd..35de7aa981 100644 --- a/internal/builtins/applyreplacements/apply_replacements.go +++ b/internal/builtins/applyreplacements/apply_replacements.go @@ -87,12 +87,12 @@ func (r *Replacements) Config(functionConfig *fn.KubeObject) error { func (r *Replacements) Process(rl *fn.ResourceList) (bool, error) { if err := r.Config(rl.FunctionConfig); err != nil { rl.LogResult(err) - return false, nil + return false, err } transformedItems, err := r.Transform(rl.Items) if err != nil { rl.LogResult(err) - return false, nil + return false, err } rl.Items = transformedItems return true, nil diff --git a/internal/builtins/registry/registry.go b/internal/builtins/registry/registry.go index 98b91da2ff..f16fb34d3f 100644 --- a/internal/builtins/registry/registry.go +++ b/internal/builtins/registry/registry.go @@ -16,9 +16,10 @@ package registry import ( "io" - "log" "strings" "sync" + + "k8s.io/klog/v2" ) type BuiltinFunction interface { @@ -41,9 +42,13 @@ 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 { - log.Printf("WARNING: builtin function %q is being used instead of the requested image %q. "+ + 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) } return fn diff --git a/internal/builtins/starlark/config.go b/internal/builtins/starlark/config.go index 2b3f8d7bb5..bb5bb8c68e 100644 --- a/internal/builtins/starlark/config.go +++ b/internal/builtins/starlark/config.go @@ -49,7 +49,7 @@ type Run struct { func (sr *Run) Config(fnCfg *fn.KubeObject) error { switch { case fnCfg.IsEmpty(): - return fmt.Errorf("FunctionConfig is missing. Expect `ConfigMap` or `StarlarkRun`") + return fmt.Errorf("FunctionConfig is missing. Expect `ConfigMap`, `StarlarkRun`, or `Run`") case fnCfg.IsGVK("", configMapAPIVersion, configMapKind): cm := &corev1.ConfigMap{} if err := fnCfg.As(cm); err != nil { @@ -71,9 +71,10 @@ func (sr *Run) Config(fnCfg *fn.KubeObject) error { return err } default: - return fmt.Errorf("`functionConfig` must be either %v or %v, but we got: %v", + return fmt.Errorf("`functionConfig` must be either %v, %v, or %v but we got: %v", schema.FromAPIVersionAndKind(configMapAPIVersion, configMapKind).String(), schema.FromAPIVersionAndKind(starlarkRunAPIVersion, starlarkRunKind).String(), + schema.FromAPIVersionAndKind(starlarkRunAPIVersion, "Run").String(), schema.FromAPIVersionAndKind(fnCfg.GetAPIVersion(), fnCfg.GetKind()).String()) } diff --git a/internal/builtins/starlark/processor.go b/internal/builtins/starlark/processor.go index 0bb8ad507e..a3e7f59770 100644 --- a/internal/builtins/starlark/processor.go +++ b/internal/builtins/starlark/processor.go @@ -14,6 +14,8 @@ package starlark import ( + "fmt" + "github.com/kptdev/krm-functions-sdk/go/fn" ) @@ -34,7 +36,7 @@ func Process(resourceList *fn.ResourceList) (bool, error) { Severity: fn.Error, }, } - return false, err + return false, fmt.Errorf("failed to evaluate function: error: %v", err) } return true, nil diff --git a/internal/fnruntime/runner.go b/internal/fnruntime/runner.go index 40e9cf51fd..9389b9654a 100644 --- a/internal/fnruntime/runner.go +++ b/internal/fnruntime/runner.go @@ -225,6 +225,12 @@ func (fr *FunctionRunner) Filter(input []*yaml.RNode) (output []*yaml.RNode, err printFnExecErr(fr.ctx, fnErr) return nil, errors.ErrAlreadyHandled } + // for builtin functions, print stderr from fnResult if available + if fr.fnResult.Stderr != "" { + printFnStderr(fr.ctx, fr.fnResult.Stderr) + pr.Printf(" Exit code: %d\n\n", fr.fnResult.ExitCode) + return nil, errors.ErrAlreadyHandled + } return nil, err } if !fr.disableCLIOutput { @@ -280,6 +286,9 @@ func (fr *FunctionRunner) do(input []*yaml.RNode) (output []*yaml.RNode, err err if goerrors.As(err, &execErr) { fnResult.ExitCode = execErr.ExitCode fnResult.Stderr = execErr.Stderr + } else { + // builtin functions don't return ExecError, populate stderr from error message + fnResult.Stderr = err.Error() } // accumulate the results fr.fnResults.Items = append(fr.fnResults.Items, *fnResult) diff --git a/pkg/api/kptfile/v1/types.go b/pkg/api/kptfile/v1/types.go index 3fff13720c..0d6ba518be 100644 --- a/pkg/api/kptfile/v1/types.go +++ b/pkg/api/kptfile/v1/types.go @@ -437,8 +437,8 @@ type PipelineStepResult struct { // ResultItem mirrors framework.Result with only the fields needed for Kptfile status. type ResultItem struct { - Message string `yaml:"message,omitempty" json:"message,omitempty"` - Severity string `yaml:"severity,omitempty" json:"severity,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + Severity string `yaml:"severity,omitempty" json:"severity,omitempty"` ResourceRef *ResourceRef `yaml:"resourceRef,omitempty" json:"resourceRef,omitempty"` Field *FieldRef `yaml:"field,omitempty" json:"field,omitempty"` File *FileRef `yaml:"file,omitempty" json:"file,omitempty"` From a1fce5e96d1ff4c78b6fb4b8f6f6249b0bc505e2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 7 Apr 2026 20:58:15 +0200 Subject: [PATCH 4/6] 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 --- go.mod | 5 +- go.sum | 2 - .../applyreplacements/apply_replacements.go | 104 ++---- .../apply_replacements_test.go | 9 +- internal/builtins/registry/registry.go | 2 +- internal/builtins/registry/registry_test.go | 4 +- internal/builtins/starlark/config.go | 82 ++--- internal/builtins/starlark/config_test.go | 8 +- internal/builtins/starlark/processor.go | 37 +-- internal/builtins/starlark/processor_test.go | 57 +++- internal/builtins/starlark/runtime/context.go | 125 ++++++++ internal/builtins/starlark/runtime/doc.go | 36 +++ .../builtins/starlark/runtime/krmfn/krmfn.go | 86 +++++ internal/builtins/starlark/runtime/loadlib.go | 58 ++++ .../builtins/starlark/runtime/starlark.go | 298 ++++++++++++++++++ internal/builtins/starlark/starlark.go | 35 +- internal/fnruntime/runner.go | 9 +- 17 files changed, 770 insertions(+), 187 deletions(-) create mode 100644 internal/builtins/starlark/runtime/context.go create mode 100644 internal/builtins/starlark/runtime/doc.go create mode 100644 internal/builtins/starlark/runtime/krmfn/krmfn.go create mode 100644 internal/builtins/starlark/runtime/loadlib.go create mode 100644 internal/builtins/starlark/runtime/starlark.go diff --git a/go.mod b/go.mod index fb4e4f1fd9..fe3581d3d6 100644 --- a/go.mod +++ b/go.mod @@ -11,17 +11,18 @@ 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 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 @@ -112,7 +113,6 @@ require ( 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 @@ -122,7 +122,6 @@ 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 diff --git a/go.sum b/go.sum index 31a635745d..0a71ce1d70 100644 --- a/go.sum +++ b/go.sum @@ -153,8 +153,6 @@ github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uq github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kptdev/krm-functions-catalog/functions/go/apply-setters v0.2.2 h1:PZ4TcVzgad1OFuH4gHg4j2LKC2KXTuzfsQWil2knSlk= github.com/kptdev/krm-functions-catalog/functions/go/apply-setters v0.2.2/go.mod h1:S8Vrp3yPDp4ga2TOPfZzoO/Y7UGF7KPHS1S0taJ0XOc= -github.com/kptdev/krm-functions-catalog/functions/go/starlark v0.5.5 h1:2fVPRn0knqm4XoXfYN7mWt99MOevHhR8eoKvqnmhzY4= -github.com/kptdev/krm-functions-catalog/functions/go/starlark v0.5.5/go.mod h1:PE/l25mFdKm9MibK2sh/vO1YdFvrMIc3MXwyJW/scB0= github.com/kptdev/krm-functions-sdk/go/fn v1.0.2 h1:g9N6SW5axEXMagUbHliH14XpfvvvwkAVDLcN3ApVh2M= github.com/kptdev/krm-functions-sdk/go/fn v1.0.2/go.mod h1:NSfdmtQ9AwNg5wdS9gE/H9SQs7Vomzq7E7N9hyEju1U= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/internal/builtins/applyreplacements/apply_replacements.go b/internal/builtins/applyreplacements/apply_replacements.go index 35de7aa981..c95d728c11 100644 --- a/internal/builtins/applyreplacements/apply_replacements.go +++ b/internal/builtins/applyreplacements/apply_replacements.go @@ -4,13 +4,14 @@ // 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 +// 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 ( @@ -18,9 +19,10 @@ import ( "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/fn/framework" + "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -41,85 +43,47 @@ 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) +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, + }, + ) } -type Replacements struct { - Replacements []types.Replacement `json:"replacements,omitempty" yaml:"replacements,omitempty"` -} +func Process(rl *framework.ResourceList) error { + rep := &Replacements{} -func (r *Replacements) Config(functionConfig *fn.KubeObject) error { - if functionConfig.IsEmpty() { + if rl.FunctionConfig == nil { 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) + + meta, err := rl.FunctionConfig.GetMeta() + if err != nil { + return fmt.Errorf("reading functionConfig metadata: %w", err) } - r.Replacements = []types.Replacement{} - if err := functionConfig.As(r); err != nil { - return fmt.Errorf("unable to convert functionConfig to replacements:\n%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) } - 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 + if err := yaml.Unmarshal([]byte(rl.FunctionConfig.MustString()), rep); err != nil { + return fmt.Errorf("unable to convert functionConfig to replacements: %w", err) } - transformedItems, err := r.Transform(rl.Items) + + transformed, err := replacement.Filter{ + Replacements: rep.Replacements, + }.Filter(rl.Items) if err != nil { - rl.LogResult(err) - return false, err + return err } - rl.Items = transformedItems - return true, nil + rl.Items = transformed + return 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 +type Replacements struct { + Replacements []types.Replacement `json:"replacements,omitempty" yaml:"replacements,omitempty"` } diff --git a/internal/builtins/applyreplacements/apply_replacements_test.go b/internal/builtins/applyreplacements/apply_replacements_test.go index 5322743c66..e1f6b073d1 100644 --- a/internal/builtins/applyreplacements/apply_replacements_test.go +++ b/internal/builtins/applyreplacements/apply_replacements_test.go @@ -16,6 +16,7 @@ package applyreplacements import ( "bytes" + "io" "testing" "github.com/stretchr/testify/assert" @@ -51,7 +52,7 @@ functionConfig: w := &bytes.Buffer{} runner := &Runner{} - err := runner.Run(r, w) + err := runner.Run(r, w, io.Discard) assert.NoError(t, err) assert.Contains(t, w.String(), "namespace: my-app") } @@ -74,7 +75,7 @@ functionConfig: w := &bytes.Buffer{} runner := &Runner{} - err := runner.Run(r, w) - assert.NoError(t, err) - assert.Contains(t, w.String(), "only functionConfig of kind ApplyReplacements") + err := runner.Run(r, w, io.Discard) + assert.Error(t, err) + assert.Contains(t, err.Error(), "only functionConfig of kind ApplyReplacements") } diff --git a/internal/builtins/registry/registry.go b/internal/builtins/registry/registry.go index f16fb34d3f..78c61a887c 100644 --- a/internal/builtins/registry/registry.go +++ b/internal/builtins/registry/registry.go @@ -24,7 +24,7 @@ import ( type BuiltinFunction interface { ImageName() string - Run(r io.Reader, w io.Writer) error + Run(r io.Reader, w io.Writer, stderr io.Writer) error } var ( diff --git a/internal/builtins/registry/registry_test.go b/internal/builtins/registry/registry_test.go index 5725acbfb6..9be2a6c8e8 100644 --- a/internal/builtins/registry/registry_test.go +++ b/internal/builtins/registry/registry_test.go @@ -25,8 +25,8 @@ type fakeBuiltin struct { name string } -func (f *fakeBuiltin) ImageName() string { return f.name } -func (f *fakeBuiltin) Run(_ io.Reader, _ io.Writer) error { return nil } +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 { diff --git a/internal/builtins/starlark/config.go b/internal/builtins/starlark/config.go index bb5bb8c68e..9b58d3a37d 100644 --- a/internal/builtins/starlark/config.go +++ b/internal/builtins/starlark/config.go @@ -17,10 +17,10 @@ package starlark import ( "fmt" - starlarkruntime "github.com/kptdev/krm-functions-catalog/functions/go/starlark/third_party/sigs.k8s.io/kustomize/kyaml/fn/runtime/starlark" - "github.com/kptdev/krm-functions-sdk/go/fn" + starlarkruntime "github.com/kptdev/kpt/internal/builtins/starlark/runtime" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/kustomize/kyaml/fn/framework" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -40,22 +40,29 @@ const ( type Run struct { yaml.ResourceMeta `json:",inline" yaml:",inline"` - // Source is a required field for providing a starlark script inline. - Source string `json:"source" yaml:"source"` - // Params are the parameters in key-value pairs format. - Params map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"` + Source string `json:"source" yaml:"source"` + Params map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"` } -func (sr *Run) Config(fnCfg *fn.KubeObject) error { - switch { - case fnCfg.IsEmpty(): +func (sr *Run) Config(fnCfg *yaml.RNode) error { + if fnCfg == nil { return fmt.Errorf("FunctionConfig is missing. Expect `ConfigMap`, `StarlarkRun`, or `Run`") - case fnCfg.IsGVK("", configMapAPIVersion, configMapKind): + } + + meta, err := fnCfg.GetMeta() + if err != nil { + return fmt.Errorf("reading functionConfig metadata: %w", err) + } + + apiVersion := meta.APIVersion + kind := meta.Kind + + switch { + case apiVersion == configMapAPIVersion && kind == configMapKind: cm := &corev1.ConfigMap{} - if err := fnCfg.As(cm); err != nil { + if err := fnCfg.YNode().Decode(cm); err != nil { return err } - // Convert ConfigMap to StarlarkRun sr.Name = cm.Name sr.Namespace = cm.Namespace sr.Params = map[string]interface{}{} @@ -65,63 +72,34 @@ func (sr *Run) Config(fnCfg *fn.KubeObject) error { } sr.Params[k] = v } - case fnCfg.IsGVK(starlarkRunGroup, starlarkRunVersion, starlarkRunKind), - fnCfg.IsGVK(starlarkRunGroup, starlarkRunVersion, "Run"): - if err := fnCfg.As(sr); err != nil { + + case (apiVersion == starlarkRunAPIVersion && kind == starlarkRunKind) || + (apiVersion == starlarkRunAPIVersion && kind == "Run"): + if err := fnCfg.YNode().Decode(sr); err != nil { return err } + default: return fmt.Errorf("`functionConfig` must be either %v, %v, or %v but we got: %v", schema.FromAPIVersionAndKind(configMapAPIVersion, configMapKind).String(), schema.FromAPIVersionAndKind(starlarkRunAPIVersion, starlarkRunKind).String(), schema.FromAPIVersionAndKind(starlarkRunAPIVersion, "Run").String(), - schema.FromAPIVersionAndKind(fnCfg.GetAPIVersion(), fnCfg.GetKind()).String()) + schema.FromAPIVersionAndKind(apiVersion, kind).String()) } - // Defaulting if sr.Name == "" { sr.Name = defaultProgramName } - // Validation if sr.Source == "" { return fmt.Errorf("`source` must not be empty") } return nil } -func (sr *Run) Transform(rl *fn.ResourceList) error { - var transformedObjects []*fn.KubeObject - var nodes []*yaml.RNode - - fcRN, err := yaml.Parse(rl.FunctionConfig.String()) - if err != nil { - return err - } - for _, obj := range rl.Items { - objRN, err := yaml.Parse(obj.String()) - if err != nil { - return err - } - nodes = append(nodes, objRN) - } - - starFltr := &starlarkruntime.SimpleFilter{ - Name: sr.Name, - Program: sr.Source, - FunctionConfig: fcRN, - } - transformedNodes, err := starFltr.Filter(nodes) - if err != nil { - return err - } - - for _, n := range transformedNodes { - obj, err := fn.ParseKubeObject([]byte(n.MustString())) - if err != nil { - return err - } - transformedObjects = append(transformedObjects, obj) +func (sr *Run) Transform(rl *framework.ResourceList) error { + starFltr := &starlarkruntime.Filter{ + Name: sr.Name, + Program: sr.Source, } - rl.Items = transformedObjects - return nil + return rl.Filter(starFltr) } diff --git a/internal/builtins/starlark/config_test.go b/internal/builtins/starlark/config_test.go index 67a0bdb4c6..a24d1271e9 100644 --- a/internal/builtins/starlark/config_test.go +++ b/internal/builtins/starlark/config_test.go @@ -16,8 +16,8 @@ package starlark import ( "testing" - "github.com/kptdev/krm-functions-sdk/go/fn" "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/kyaml/yaml" ) func TestStarlarkConfig(t *testing.T) { @@ -84,12 +84,14 @@ data: expectErrMsg: "`source` must not be empty", }, } + for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { sr := &Run{} - ko, err := fn.ParseKubeObject([]byte(tc.config)) + node, err := yaml.Parse(tc.config) assert.NoError(t, err) - err = sr.Config(ko) + + err = sr.Config(node) if tc.expectErrMsg == "" { assert.NoError(t, err) } else { diff --git a/internal/builtins/starlark/processor.go b/internal/builtins/starlark/processor.go index a3e7f59770..0880369302 100644 --- a/internal/builtins/starlark/processor.go +++ b/internal/builtins/starlark/processor.go @@ -16,28 +16,29 @@ package starlark import ( "fmt" - "github.com/kptdev/krm-functions-sdk/go/fn" + "sigs.k8s.io/kustomize/kyaml/fn/framework" ) -// Process runs the Starlark function on the given ResourceList -func Process(resourceList *fn.ResourceList) (bool, error) { - err := func() error { - sr := &Run{} - if err := sr.Config(resourceList.FunctionConfig); err != nil { - return err - } - return sr.Transform(resourceList) - }() +func Process(rl *framework.ResourceList) error { + sr := &Run{} - if err != nil { - resourceList.Results = []*fn.Result{ - { - Message: err.Error(), - Severity: fn.Error, - }, + if rl.FunctionConfig != nil { + if err := sr.Config(rl.FunctionConfig); err != nil { + rl.Results = append(rl.Results, &framework.Result{ + Message: fmt.Sprintf("failed to configure starlark: %v", err), + Severity: framework.Error, + }) + return rl.Results } - return false, fmt.Errorf("failed to evaluate function: error: %v", err) } - return true, nil + if err := sr.Transform(rl); err != nil { + rl.Results = append(rl.Results, &framework.Result{ + Message: fmt.Sprintf("starlark transform failed: %v", err), + Severity: framework.Error, + }) + return rl.Results + } + + return nil } diff --git a/internal/builtins/starlark/processor_test.go b/internal/builtins/starlark/processor_test.go index f9a5ef8977..868ba41bef 100644 --- a/internal/builtins/starlark/processor_test.go +++ b/internal/builtins/starlark/processor_test.go @@ -15,12 +15,32 @@ package starlark import ( + "strings" "testing" - "github.com/kptdev/krm-functions-sdk/go/fn" "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" ) +func parseResourceList(t *testing.T, input string) *framework.ResourceList { + t.Helper() + rw := &kio.ByteReader{Reader: strings.NewReader(input)} + nodes, err := rw.Read() + assert.NoError(t, err) + + rl := &framework.ResourceList{Items: nodes} + node, err := yaml.Parse(input) + if err == nil { + fc := node.Field("functionConfig") + if fc != nil && fc.Value != nil { + rl.FunctionConfig = fc.Value + } + } + return rl +} + func TestProcess_SetNamespace(t *testing.T) { input := `apiVersion: config.kubernetes.io/v1 kind: ResourceList @@ -41,13 +61,25 @@ functionConfig: resource["metadata"]["namespace"] = ns_value run(ctx.resource_list["items"], "new-namespace") ` - rl, err := fn.ParseResourceList([]byte(input)) + rw := &kio.ByteReadWriter{ + Reader: strings.NewReader(input), + WrappingAPIVersion: kio.ResourceListAPIVersion, + WrappingKind: kio.ResourceListKind, + } + nodes, err := rw.Read() assert.NoError(t, err) - ok, err := Process(rl) + rl := &framework.ResourceList{ + Items: nodes, + FunctionConfig: rw.FunctionConfig, + } + + err = Process(rl) + assert.NoError(t, err) + + ns, err := rl.Items[0].GetString("metadata.namespace") assert.NoError(t, err) - assert.True(t, ok) - assert.Equal(t, "new-namespace", rl.Items[0].GetNamespace()) + assert.Equal(t, "new-namespace", ns) } func TestProcess_InvalidScript(t *testing.T) { @@ -62,11 +94,20 @@ functionConfig: source: | this is not valid starlark!!! ` - rl, err := fn.ParseResourceList([]byte(input)) + rw := &kio.ByteReadWriter{ + Reader: strings.NewReader(input), + WrappingAPIVersion: kio.ResourceListAPIVersion, + WrappingKind: kio.ResourceListKind, + } + nodes, err := rw.Read() assert.NoError(t, err) - ok, err := Process(rl) + rl := &framework.ResourceList{ + Items: nodes, + FunctionConfig: rw.FunctionConfig, + } + + err = Process(rl) assert.Error(t, err) - assert.False(t, ok) assert.Len(t, rl.Results, 1) } diff --git a/internal/builtins/starlark/runtime/context.go b/internal/builtins/starlark/runtime/context.go new file mode 100644 index 0000000000..0f3b5b497d --- /dev/null +++ b/internal/builtins/starlark/runtime/context.go @@ -0,0 +1,125 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package starlarkruntime + +import ( + "encoding/json" + "os" + "strings" + "sync" + + "github.com/qri-io/starlib/util" + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/openapi" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +type Context struct { + resourceList starlark.Value +} + +func (c *Context) predeclared() (starlark.StringDict, error) { + e, err := env() + if err != nil { + return nil, err + } + dict := starlark.StringDict{ + "resource_list": c.resourceList, + "open_api": &LazyInitializationOpenapi{}, + "environment": e, + } + + return starlark.StringDict{ + "ctx": starlarkstruct.FromStringDict(starlarkstruct.Default, dict), + }, nil +} + +func oa() (starlark.Value, error) { + return interfaceToValue(openapi.Schema()) +} + +func env() (starlark.Value, error) { + env := map[string]interface{}{} + for _, e := range os.Environ() { + pair := strings.SplitN(e, "=", 2) + if len(pair) < 2 { + continue + } + env[pair[0]] = pair[1] + } + value, err := util.Marshal(env) + if err != nil { + return nil, errors.Wrap(err) + } + return value, nil +} + +type LazyInitializationOpenapi struct { + once sync.Once + val starlark.Value +} + +var _ starlark.Mapping = &LazyInitializationOpenapi{} + +func (v *LazyInitializationOpenapi) init() { + o, err := oa() + if err != nil { + panic(err) + } + v.val = o +} + +func (v *LazyInitializationOpenapi) String() string { + v.once.Do(v.init) + return v.val.String() +} + +func (v *LazyInitializationOpenapi) Type() string { + v.once.Do(v.init) + return v.val.Type() +} + +func (v *LazyInitializationOpenapi) Freeze() { + v.once.Do(v.init) + v.val.Freeze() +} + +func (v *LazyInitializationOpenapi) Truth() starlark.Bool { + v.once.Do(v.init) + return v.val.Truth() +} + +func (v *LazyInitializationOpenapi) Hash() (uint32, error) { + v.once.Do(v.init) + return v.val.Hash() +} + +func (v *LazyInitializationOpenapi) Get(val starlark.Value) (starlark.Value, bool, error) { + v.once.Do(v.init) + m, ok := v.val.(starlark.Mapping) + if ok { + return m.Get(val) + } + return nil, false, nil +} + +func interfaceToValue(i interface{}) (starlark.Value, error) { + b, err := json.Marshal(i) + if err != nil { + return nil, err + } + + var in map[string]interface{} + if err := yaml.Unmarshal(b, &in); err != nil { + return nil, errors.Wrap(err) + } + + value, err := util.Marshal(in) + if err != nil { + return nil, errors.Wrap(err) + } + return value, nil +} diff --git a/internal/builtins/starlark/runtime/doc.go b/internal/builtins/starlark/runtime/doc.go new file mode 100644 index 0000000000..53024e115b --- /dev/null +++ b/internal/builtins/starlark/runtime/doc.go @@ -0,0 +1,36 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Package starlark contains a kio.Filter which can be applied to resources to transform +// them through starlark program. +// +// Starlark has become a popular runtime embedding in go programs, especially for Kubernetes +// and data processing. +// Examples: https://github.com/cruise-automation/isopod, https://qri.io/docs/starlark/starlib, +// https://github.com/stripe/skycfg, https://github.com/k14s/ytt +// +// The resources are provided to the starlark program through the global variable "resourceList". +// "resourceList" is a dictionary containing an "items" field with a list of resources. +// The starlark modified "resourceList" is the Filter output. +// +// After being run through the starlark program, the filter will copy the comments from the input +// resources to restore them -- due to them being dropped as a result of serializing the resources +// as starlark values. +// +// "resourceList" may also contain a "functionConfig" entry to configure the starlark script itself. +// Changes made by the starlark program to the "functionConfig" will be reflected in the +// Filter.FunctionConfig value. +// +// The Filter will also format the output so that output has the preferred field ordering +// rather than an alphabetical field ordering. +// +// The resourceList variable adheres to the kustomize function spec as specified by: +// https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md +// +// All items in the resourceList are resources represented as starlark dictionaries/ +// The items in the resourceList respect the io spec specified by: +// https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/config-io.md +// +// The starlark language spec can be found here: +// https://github.com/google/starlark-go/blob/master/doc/spec.md +package starlarkruntime diff --git a/internal/builtins/starlark/runtime/krmfn/krmfn.go b/internal/builtins/starlark/runtime/krmfn/krmfn.go new file mode 100644 index 0000000000..c3512f05a3 --- /dev/null +++ b/internal/builtins/starlark/runtime/krmfn/krmfn.go @@ -0,0 +1,86 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package krmfn + +import ( + "go.starlark.net/starlark" + "go.starlark.net/starlarkstruct" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +const ModuleName = "krmfn.star" + +var Module = &starlarkstruct.Module{ + Name: "krmfn", + Members: starlark.StringDict{ + "match_gvk": starlark.NewBuiltin("match_gvk", matchGVK), + "match_name": starlark.NewBuiltin("match_name", matchName), + "match_namespace": starlark.NewBuiltin("match_namespace", matchNamespace), + }, +} + +func matchGVK(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var resource starlark.Value + var apiVersion, kind string + if err := starlark.UnpackPositionalArgs("match_gvk", args, kwargs, 3, + &resource, &apiVersion, &kind); err != nil { + return nil, err + } + gv, err := schema.ParseGroupVersion(apiVersion) + if err != nil { + return nil, err + } + rn, err := yaml.Parse(resource.String()) + if err != nil { + return nil, err + } + meta, err := rn.GetMeta() + if err != nil { + return nil, err + } + parsedGV, err := schema.ParseGroupVersion(meta.APIVersion) + if err != nil { + return starlark.False, nil + } + return starlark.Bool(parsedGV.Group == gv.Group && + parsedGV.Version == gv.Version && + meta.Kind == kind), nil +} + +func matchName(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var resource starlark.Value + var name string + if err := starlark.UnpackPositionalArgs("match_name", args, kwargs, 2, + &resource, &name); err != nil { + return nil, err + } + rn, err := yaml.Parse(resource.String()) + if err != nil { + return nil, err + } + meta, err := rn.GetMeta() + if err != nil { + return nil, err + } + return starlark.Bool(meta.Name == name), nil +} + +func matchNamespace(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var resource starlark.Value + var namespace string + if err := starlark.UnpackPositionalArgs("match_namespace", args, kwargs, 2, + &resource, &namespace); err != nil { + return nil, err + } + rn, err := yaml.Parse(resource.String()) + if err != nil { + return nil, err + } + meta, err := rn.GetMeta() + if err != nil { + return nil, err + } + return starlark.Bool(meta.Namespace == namespace), nil +} diff --git a/internal/builtins/starlark/runtime/loadlib.go b/internal/builtins/starlark/runtime/loadlib.go new file mode 100644 index 0000000000..5dd5a46580 --- /dev/null +++ b/internal/builtins/starlark/runtime/loadlib.go @@ -0,0 +1,58 @@ +package starlarkruntime + +import ( + "github.com/kptdev/kpt/internal/builtins/starlark/runtime/krmfn" + "github.com/qri-io/starlib/bsoup" + "github.com/qri-io/starlib/encoding/base64" + "github.com/qri-io/starlib/encoding/csv" + "github.com/qri-io/starlib/encoding/json" + "github.com/qri-io/starlib/encoding/yaml" + "github.com/qri-io/starlib/geo" + "github.com/qri-io/starlib/hash" + "github.com/qri-io/starlib/html" + "github.com/qri-io/starlib/http" + "github.com/qri-io/starlib/math" + "github.com/qri-io/starlib/re" + "github.com/qri-io/starlib/time" + "github.com/qri-io/starlib/xlsx" + "github.com/qri-io/starlib/zipfile" + "go.starlark.net/starlark" +) + +// load loads starlark libraries from https://github.com/qri-io/starlib#packages and from +// our own custom libraries. +func load(_ *starlark.Thread, module string) (starlark.StringDict, error) { + switch module { + case bsoup.ModuleName: + return bsoup.LoadModule() + case base64.ModuleName: + return base64.LoadModule() + case csv.ModuleName: + return csv.LoadModule() + case json.ModuleName: + return starlark.StringDict{"json": json.Module}, nil + case yaml.ModuleName: + return yaml.LoadModule() + case geo.ModuleName: + return geo.LoadModule() + case hash.ModuleName: + return hash.LoadModule() + case html.ModuleName: + return html.LoadModule() + case http.ModuleName: + return http.LoadModule() + case math.ModuleName: + return starlark.StringDict{"math": math.Module}, nil + case re.ModuleName: + return re.LoadModule() + case time.ModuleName: + return starlark.StringDict{"time": time.Module}, nil + case xlsx.ModuleName: + return xlsx.LoadModule() + case zipfile.ModuleName: + return zipfile.LoadModule() + case krmfn.ModuleName: + return starlark.StringDict{"krmfn": krmfn.Module}, nil + } + return nil, nil +} diff --git a/internal/builtins/starlark/runtime/starlark.go b/internal/builtins/starlark/runtime/starlark.go new file mode 100644 index 0000000000..063a096e6a --- /dev/null +++ b/internal/builtins/starlark/runtime/starlark.go @@ -0,0 +1,298 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package starlarkruntime + +import ( + "bytes" + "fmt" + "io" + "net/http" + "os" + + "github.com/qri-io/starlib/util" + "go.starlark.net/resolve" + "go.starlark.net/starlark" + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/kio/filters" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// Filter transforms a set of resources through the provided program +type Filter struct { + Name string + + // Program is a starlark script which will be run against the resources + Program string + + // URL is the url of a starlark program to fetch and run + URL string + + // Path is the path to a starlark program to read and run + Path string + + runtimeutil.FunctionFilter +} + +func (sf *Filter) String() string { + return fmt.Sprintf( + "name: %v path: %v url: %v program: %v", sf.Name, sf.Path, sf.URL, sf.Program) +} + +func (sf *Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + err := sf.setup() + if err != nil { + return nil, err + } + sf.FunctionFilter.Run = sf.Run + + return sf.FunctionFilter.Filter(nodes) +} + +func (sf *Filter) setup() error { + if (sf.URL != "" && sf.Path != "") || + (sf.URL != "" && sf.Program != "") || + (sf.Path != "" && sf.Program != "") { + return errors.Errorf("Filter Path, Program and URL are mutually exclusive") + } + + // read the program from a file + if sf.Path != "" { + b, err := os.ReadFile(sf.Path) + if err != nil { + return err + } + sf.Program = string(b) + } + + // read the program from a URL + if sf.URL != "" { + err := func() error { + resp, err := http.Get(sf.URL) + if err != nil { + return err + } + + defer func() { + _ = resp.Body.Close() + }() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + sf.Program = string(b) + return nil + }() + if err != nil { + return err + } + } + + return nil +} + +func (sf *Filter) Run(reader io.Reader, writer io.Writer) error { + // retain map of inputs to outputs by id so if the name is changed by the + // starlark program, we are able to match the same resources + value, err := sf.readResourceList(reader) + if err != nil { + return errors.Wrap(err) + } + + err = runStarlark(sf.Name, sf.Program, value) + if err != nil { + return errors.Wrap(err) + } + + return sf.writeResourceList(value, writer) +} + +// runStarlark runs the starlark script +func runStarlark(name, starlarkProgram string, resourceList starlark.Value) error { + // Enabled some non-standard starlark features (https://pkg.go.dev/go.starlark.net/resolve#pkg-variables). + // LoadBindsGlobally is not enabled, since it has been deprecated. + //nolint:staticcheck + resolve.AllowSet = true + //nolint:staticcheck + resolve.AllowGlobalReassign = true + //nolint:staticcheck + resolve.AllowRecursion = true + + // run the starlark as program as transformation function + thread := &starlark.Thread{Name: name, Load: load} + + ctx := &Context{resourceList: resourceList} + pd, err := ctx.predeclared() + if err != nil { + return errors.Wrap(err) + } + //nolint:staticcheck + _, err = starlark.ExecFile(thread, name, starlarkProgram, pd) + if err != nil { + return errors.Wrap(err) + } + return nil +} + +// inputToResourceList transforms input into a starlark.Value +func (sf *Filter) readResourceList(reader io.Reader) (starlark.Value, error) { + // read and parse the inputs + rl := bytes.Buffer{} + _, err := rl.ReadFrom(reader) + if err != nil { + return nil, errors.Wrap(err) + } + rn, err := yaml.Parse(rl.String()) + if err != nil { + return nil, errors.Wrap(err) + } + return rnodeToStarlarkValue(rn) +} + +// rnodeToStarlarkValue converts a RNode to a starlark value. +func rnodeToStarlarkValue(rn *yaml.RNode) (starlark.Value, error) { + m, err := rn.Map() + if err != nil { + return nil, errors.Wrap(err) + } + return util.Marshal(m) // convert to starlark value +} + +// starlarkValueToRNode converts the output of the starlark program to a RNode. +func starlarkValueToRNode(value starlark.Value) (*yaml.RNode, error) { + // convert the modified resourceList back into a slice of RNodes + // by first converting to a map[string]interface{} + out, err := util.Unmarshal(value) + if err != nil { + return nil, errors.Wrap(err) + } + b, err := yaml.Marshal(out) + if err != nil { + return nil, errors.Wrap(err) + } + + return yaml.Parse(string(b)) +} + +// writeResourceList converts the output of the starlark program to bytes and +// write to the writer. +func (sf *Filter) writeResourceList(value starlark.Value, writer io.Writer) error { + rl, err := starlarkValueToRNode(value) + if err != nil { + return errors.Wrap(err) + } + + // preserve the comments from the input + items, err := rl.Pipe(yaml.Lookup("items")) + if err != nil { + return errors.Wrap(err) + } + err = items.VisitElements(func(node *yaml.RNode) error { + // starlark will serialize the resources sorting the fields alphabetically, + // format them to have a better ordering + _, err := filters.FormatFilter{}.Filter([]*yaml.RNode{node}) + return err + }) + if err != nil { + return errors.Wrap(err) + } + + s, err := rl.String() + if err != nil { + return errors.Wrap(err) + } + + _, err = writer.Write([]byte(s)) + return err +} + +// SimpleFilter transforms a set of resources through the provided starlark +// program. It doesn't touch the id annotation. It doesn't copy comments. +type SimpleFilter struct { + // Name of the starlark program + Name string + // Program is a starlark script which will be run against the resources + Program string + // FunctionConfig is the functionConfig for the function. + FunctionConfig *yaml.RNode +} + +func (sf *SimpleFilter) String() string { + return fmt.Sprintf( + "name: %v program: %v", sf.Name, sf.Program) +} + +func (sf *SimpleFilter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + in, err := WrapResources(nodes, sf.FunctionConfig) + if err != nil { + return nil, errors.Wrap(err) + } + value, err := rnodeToStarlarkValue(in) + if err != nil { + return nil, errors.Wrap(err) + } + + err = runStarlark(sf.Name, sf.Program, value) + if err != nil { + return nil, errors.Wrap(err) + } + + rn, err := starlarkValueToRNode(value) + if err != nil { + return nil, errors.Wrap(err) + } + updatedNodes, _, err := UnwrapResources(rn) + return updatedNodes, err +} + +// WrapResources wraps resources and an optional functionConfig in a resourceList +func WrapResources(nodes []*yaml.RNode, fc *yaml.RNode) (*yaml.RNode, error) { + var ynodes []*yaml.Node + for _, rnode := range nodes { + ynodes = append(ynodes, rnode.YNode()) + } + m := map[string]interface{}{ + "apiVersion": kio.ResourceListAPIVersion, + "kind": kio.ResourceListKind, + "items": []interface{}{}, + } + out, err := yaml.FromMap(m) + if err != nil { + return nil, err + } + _, err = out.Pipe( + yaml.Lookup("items"), + yaml.Append(ynodes...)) + if err != nil { + return nil, err + } + if fc != nil { + _, err = out.Pipe( + yaml.SetField("functionConfig", fc)) + if err != nil { + return nil, err + } + } + + return out, nil +} + +// UnwrapResources unwraps the resources and the functionConfig from a resourceList +func UnwrapResources(in *yaml.RNode) ([]*yaml.RNode, *yaml.RNode, error) { + items, err := in.Pipe(yaml.Lookup("items")) + if err != nil { + return nil, nil, errors.Wrap(err) + } + nodes, err := items.Elements() + if err != nil { + return nil, nil, errors.Wrap(err) + } + fc, err := in.Pipe(yaml.Lookup("functionConfig")) + if err != nil { + return nil, nil, errors.Wrap(err) + } + return nodes, fc, nil +} diff --git a/internal/builtins/starlark/starlark.go b/internal/builtins/starlark/starlark.go index 7ea0125d82..371aa63968 100644 --- a/internal/builtins/starlark/starlark.go +++ b/internal/builtins/starlark/starlark.go @@ -15,11 +15,11 @@ package starlark import ( - "fmt" "io" "github.com/kptdev/kpt/internal/builtins/registry" - "github.com/kptdev/krm-functions-sdk/go/fn" + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/kio" ) const ImageName = "ghcr.io/kptdev/krm-functions-catalog/starlark" @@ -27,30 +27,19 @@ const ImageName = "ghcr.io/kptdev/krm-functions-catalog/starlark" //nolint:gochecknoinits func init() { Register() } -func Register() { - registry.Register(&Runner{}) -} +func Register() { registry.Register(&Runner{}) } type Runner struct{} func (s *Runner) ImageName() string { return ImageName } -func (s *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) - } - _, processErr := Process(rl) - out, err := rl.ToYAML() - if err != nil { - return err - } - if _, err = w.Write(out); err != nil { - return err - } - return processErr +func (s *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, + }, + ) } diff --git a/internal/fnruntime/runner.go b/internal/fnruntime/runner.go index 9389b9654a..e140953339 100644 --- a/internal/fnruntime/runner.go +++ b/internal/fnruntime/runner.go @@ -92,7 +92,14 @@ func NewRunner( pkgCtxGenerator := &builtins.PackageContextGenerator{} fltr.Run = pkgCtxGenerator.Run } else if builtinFn := builtinsregistry.Lookup(f.Image); builtinFn != nil { - fltr.Run = builtinFn.Run + fltr.Run = func(r io.Reader, w io.Writer) error { + var stderrBuf strings.Builder + err := builtinFn.Run(r, w, &stderrBuf) + if stderrBuf.Len() > 0 { + fnResult.Stderr = stderrBuf.String() + } + return err + } } else { switch { case f.Image != "": From 390ab8473849688754674ec4e493bdafa4c31057 Mon Sep 17 00:00:00 2001 From: Abdulrahman Fikry Ahmed El-Badry <160083555+abdulrahman11a@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:35:23 +0200 Subject: [PATCH 5/6] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- internal/builtins/registry/registry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/builtins/registry/registry.go b/internal/builtins/registry/registry.go index 78c61a887c..d4333fd1be 100644 --- a/internal/builtins/registry/registry.go +++ b/internal/builtins/registry/registry.go @@ -43,7 +43,7 @@ func Lookup(imageName string) BuiltinFunction { defer mu.RUnlock() normalized := normalizeImage(imageName) if strings.HasSuffix(imageName, ":latest") || - strings.HasSuffix(imageName, "@sha256:") { + strings.Contains(imageName, "@sha256:") { return nil } fn := registry[normalized] From 839d2ffab827aa0658a3c0c143be9fd26ea318d1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 7 Apr 2026 20:58:15 +0200 Subject: [PATCH 6/6] 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. 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. 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) - apply-replacements: ghcr.io/kptdev/krm-functions-catalog/apply-replacements - starlark: ghcr.io/kptdev/krm-functions-catalog/starlark - Removed dependency on krm-functions-sdk/go/fn and krm-functions-catalog/starlark - Vendored starlark runtime locally using kyaml/yaml instead of SDK [PASS] apply-replacements in 0s (no Docker) [PASS] starlark in 0s (no Docker) Signed-off-by: abdulrahman11a --- internal/builtins/registry/registry.go | 2 +- internal/builtins/starlark/processor_test.go | 18 ----------------- internal/builtins/starlark/runtime/context.go | 12 ++++++++++- internal/builtins/starlark/runtime/doc.go | 20 ++++++++----------- .../builtins/starlark/runtime/starlark.go | 12 ++++++----- internal/fnruntime/runner.go | 13 ++++++++++-- 6 files changed, 38 insertions(+), 39 deletions(-) diff --git a/internal/builtins/registry/registry.go b/internal/builtins/registry/registry.go index d4333fd1be..00286d5b31 100644 --- a/internal/builtins/registry/registry.go +++ b/internal/builtins/registry/registry.go @@ -48,7 +48,7 @@ func Lookup(imageName string) BuiltinFunction { } fn := registry[normalized] if fn != nil && imageName != normalized { - klog.Warningf("WARNING: builtin function %q is being used instead of the requested image %q. "+ + klog.V(4).Infof("builtin function %q is being used instead of the requested image %q. "+ "The built-in implementation may differ from the pinned version.", normalized, imageName) } return fn diff --git a/internal/builtins/starlark/processor_test.go b/internal/builtins/starlark/processor_test.go index 868ba41bef..2cf7d8b323 100644 --- a/internal/builtins/starlark/processor_test.go +++ b/internal/builtins/starlark/processor_test.go @@ -21,26 +21,8 @@ import ( "github.com/stretchr/testify/assert" "sigs.k8s.io/kustomize/kyaml/fn/framework" "sigs.k8s.io/kustomize/kyaml/kio" - "sigs.k8s.io/kustomize/kyaml/yaml" ) -func parseResourceList(t *testing.T, input string) *framework.ResourceList { - t.Helper() - rw := &kio.ByteReader{Reader: strings.NewReader(input)} - nodes, err := rw.Read() - assert.NoError(t, err) - - rl := &framework.ResourceList{Items: nodes} - node, err := yaml.Parse(input) - if err == nil { - fc := node.Field("functionConfig") - if fc != nil && fc.Value != nil { - rl.FunctionConfig = fc.Value - } - } - return rl -} - func TestProcess_SetNamespace(t *testing.T) { input := `apiVersion: config.kubernetes.io/v1 kind: ResourceList diff --git a/internal/builtins/starlark/runtime/context.go b/internal/builtins/starlark/runtime/context.go index 0f3b5b497d..ed144a1f72 100644 --- a/internal/builtins/starlark/runtime/context.go +++ b/internal/builtins/starlark/runtime/context.go @@ -41,6 +41,14 @@ func oa() (starlark.Value, error) { return interfaceToValue(openapi.Schema()) } +// allowlist of safe environment variables to expose to starlark scripts +var allowedEnvVars = map[string]bool{ + "HOME": true, + "PATH": true, + "USER": true, + "HOSTNAME": true, +} + func env() (starlark.Value, error) { env := map[string]interface{}{} for _, e := range os.Environ() { @@ -48,7 +56,9 @@ func env() (starlark.Value, error) { if len(pair) < 2 { continue } - env[pair[0]] = pair[1] + if allowedEnvVars[pair[0]] { + env[pair[0]] = pair[1] + } } value, err := util.Marshal(env) if err != nil { diff --git a/internal/builtins/starlark/runtime/doc.go b/internal/builtins/starlark/runtime/doc.go index 53024e115b..2cb029d3f3 100644 --- a/internal/builtins/starlark/runtime/doc.go +++ b/internal/builtins/starlark/runtime/doc.go @@ -9,26 +9,22 @@ // Examples: https://github.com/cruise-automation/isopod, https://qri.io/docs/starlark/starlib, // https://github.com/stripe/skycfg, https://github.com/k14s/ytt // -// The resources are provided to the starlark program through the global variable "resourceList". -// "resourceList" is a dictionary containing an "items" field with a list of resources. -// The starlark modified "resourceList" is the Filter output. +// The resources are provided to the starlark program through ctx.resource_list. +// ctx.resource_list is a dictionary containing an "items" field with a list of resources. +// The starlark-modified ctx.resource_list is the Filter output. // -// After being run through the starlark program, the filter will copy the comments from the input -// resources to restore them -- due to them being dropped as a result of serializing the resources -// as starlark values. -// -// "resourceList" may also contain a "functionConfig" entry to configure the starlark script itself. -// Changes made by the starlark program to the "functionConfig" will be reflected in the +// ctx.resource_list may also contain a "functionConfig" entry to configure the starlark script +// itself. Changes made by the starlark program to the "functionConfig" will be reflected in the // Filter.FunctionConfig value. // // The Filter will also format the output so that output has the preferred field ordering // rather than an alphabetical field ordering. // -// The resourceList variable adheres to the kustomize function spec as specified by: +// The ctx.resource_list value adheres to the kustomize function spec as specified by: // https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md // -// All items in the resourceList are resources represented as starlark dictionaries/ -// The items in the resourceList respect the io spec specified by: +// All items in ctx.resource_list are resources represented as starlark dictionaries/ +// The items in ctx.resource_list respect the io spec specified by: // https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/config-io.md // // The starlark language spec can be found here: diff --git a/internal/builtins/starlark/runtime/starlark.go b/internal/builtins/starlark/runtime/starlark.go index 063a096e6a..cad6b199db 100644 --- a/internal/builtins/starlark/runtime/starlark.go +++ b/internal/builtins/starlark/runtime/starlark.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "os" + "time" "github.com/qri-io/starlib/util" "go.starlark.net/resolve" @@ -58,7 +59,6 @@ func (sf *Filter) setup() error { return errors.Errorf("Filter Path, Program and URL are mutually exclusive") } - // read the program from a file if sf.Path != "" { b, err := os.ReadFile(sf.Path) if err != nil { @@ -67,18 +67,20 @@ func (sf *Filter) setup() error { sf.Program = string(b) } - // read the program from a URL if sf.URL != "" { err := func() error { - resp, err := http.Get(sf.URL) + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Get(sf.URL) if err != nil { return err } - defer func() { _ = resp.Body.Close() }() - + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("failed to fetch starlark program from %s: HTTP %d", + sf.URL, resp.StatusCode) + } b, err := io.ReadAll(resp.Body) if err != nil { return err diff --git a/internal/fnruntime/runner.go b/internal/fnruntime/runner.go index e140953339..5a1967e833 100644 --- a/internal/fnruntime/runner.go +++ b/internal/fnruntime/runner.go @@ -86,6 +86,7 @@ func NewRunner( } else if runner != nil { fltr.Run = runner.Run } + } if fltr.Run == nil { if f.Image == runneroptions.FuncGenPkgContext { @@ -233,7 +234,7 @@ func (fr *FunctionRunner) Filter(input []*yaml.RNode) (output []*yaml.RNode, err return nil, errors.ErrAlreadyHandled } // for builtin functions, print stderr from fnResult if available - if fr.fnResult.Stderr != "" { + if err != nil && fr.fnResult.Stderr != "" { printFnStderr(fr.ctx, fr.fnResult.Stderr) pr.Printf(" Exit code: %d\n\n", fr.fnResult.ExitCode) return nil, errors.ErrAlreadyHandled @@ -295,7 +296,15 @@ func (fr *FunctionRunner) do(input []*yaml.RNode) (output []*yaml.RNode, err err fnResult.Stderr = execErr.Stderr } else { // builtin functions don't return ExecError, populate stderr from error message - fnResult.Stderr = err.Error() + if fnResult.Stderr == "" { + fnResult.Stderr = err.Error() + } else if !strings.Contains(fnResult.Stderr, err.Error()) { + if strings.HasSuffix(fnResult.Stderr, "\n") { + fnResult.Stderr += err.Error() + } else { + fnResult.Stderr += "\n" + err.Error() + } + } } // accumulate the results fr.fnResults.Items = append(fr.fnResults.Items, *fnResult)