diff --git a/e2e/testdata/fn-render/basicpipeline-print-fn-versions/.expected/config.yaml b/e2e/testdata/fn-render/basicpipeline-print-fn-versions/.expected/config.yaml new file mode 100644 index 000000000..5731d1f3c --- /dev/null +++ b/e2e/testdata/fn-render/basicpipeline-print-fn-versions/.expected/config.yaml @@ -0,0 +1,44 @@ +# 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. + +allowNetwork: true +stdErr: | + Package: "basicpipeline-print-fn-versions" + [RUNNING] "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest" + [PASS] "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest" in 0s + [Results]: [info]: set 4 labels in total + [RUNNING] "ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.2.4" + [PASS] "ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.2.4" in 0s + [Results]: [info]: set 4 labels in total + [RUNNING] "ghcr.io/kptdev/krm-functions-catalog/set-namespace:v0.4.3" + [PASS] "ghcr.io/kptdev/krm-functions-catalog/set-namespace:v0.4.3" in 0s + [Results]: [info]: namespace [default] updated to "staging", 1 value(s) changed, [info]: all `depends-on` annotations are up-to-date. no `namespace` changed + [RUNNING] "ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.2.4" + [PASS] "ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.2.4" in 0s + [Results]: [info]: set 4 labels in total + [RUNNING] "ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.2.4" + [PASS] "ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.2.4" in 0s + [Results]: [info]: set 4 labels in total + [RUNNING] "ghcr.io/kptdev/krm-functions-catalog/set-labels@sha256:b087fe8968d1641f495ee382f8f5b8c8e23b062903042775cc28fa6c6a64f6c1" + [PASS] "ghcr.io/kptdev/krm-functions-catalog/set-labels@sha256:b087fe8968d1641f495ee382f8f5b8c8e23b062903042775cc28fa6c6a64f6c1" in 0s + [Results]: [info]: set 4 labels in total + [RUNNING] "ghcr.io:443/kptdev/krm-functions-catalog/set-labels:latest" + [PASS] "ghcr.io:443/kptdev/krm-functions-catalog/set-labels:latest" in 0s + [Results]: [info]: set 4 labels in total + [RUNNING] "ghcr.io:443/kptdev/krm-functions-catalog/set-labels:latest" + [PASS] "ghcr.io:443/kptdev/krm-functions-catalog/set-labels:latest" in 0s + [Results]: [info]: set 4 labels in total + [RUNNING] "builtins/gen-pkg-context" + [PASS] "builtins/gen-pkg-context" in 0s + [Results]: [info]: generated package context \ No newline at end of file diff --git a/e2e/testdata/fn-render/basicpipeline-print-fn-versions/.expected/diff.patch b/e2e/testdata/fn-render/basicpipeline-print-fn-versions/.expected/diff.patch new file mode 100644 index 000000000..27a9b0c56 --- /dev/null +++ b/e2e/testdata/fn-render/basicpipeline-print-fn-versions/.expected/diff.patch @@ -0,0 +1,52 @@ +diff --git a/Kptfile b/Kptfile +index abd5626..15c9ac1 100644 +--- a/Kptfile ++++ b/Kptfile +@@ -2,6 +2,8 @@ apiVersion: kpt.dev/v1 + kind: Kptfile + metadata: + name: app ++ labels: ++ tier: backend + pipeline: + mutators: + - image: ghcr.io/kptdev/krm-functions-catalog/set-labels +diff --git a/package-context.yaml b/package-context.yaml +new file mode 100644 +index 0000000..5607ec6 +--- /dev/null ++++ b/package-context.yaml +@@ -0,0 +1,8 @@ ++apiVersion: v1 ++kind: ConfigMap ++metadata: ++ name: kptfile.kpt.dev ++ annotations: ++ config.kubernetes.io/local-config: "true" ++data: ++ name: app +diff --git a/resources.yaml b/resources.yaml +index 1f15150..faa5100 100644 +--- a/resources.yaml ++++ b/resources.yaml +@@ -15,12 +15,20 @@ apiVersion: apps/v1 + kind: Deployment + metadata: + name: nginx-deployment ++ labels: ++ tier: backend ++ namespace: staging + spec: + replicas: 3 ++ selector: ++ matchLabels: ++ tier: backend + --- + apiVersion: custom.io/v1 + kind: Custom + metadata: + name: custom ++ labels: ++ tier: backend + spec: + image: nginx:1.2.3 \ No newline at end of file diff --git a/e2e/testdata/fn-render/basicpipeline-print-fn-versions/.krmignore b/e2e/testdata/fn-render/basicpipeline-print-fn-versions/.krmignore new file mode 100644 index 000000000..9d7a4007d --- /dev/null +++ b/e2e/testdata/fn-render/basicpipeline-print-fn-versions/.krmignore @@ -0,0 +1 @@ +.expected diff --git a/e2e/testdata/fn-render/basicpipeline-print-fn-versions/Kptfile b/e2e/testdata/fn-render/basicpipeline-print-fn-versions/Kptfile new file mode 100644 index 000000000..abd562631 --- /dev/null +++ b/e2e/testdata/fn-render/basicpipeline-print-fn-versions/Kptfile @@ -0,0 +1,36 @@ +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: app +pipeline: + mutators: + - image: ghcr.io/kptdev/krm-functions-catalog/set-labels + configMap: + tier: backend + - image: ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.2.4 + configMap: + tier: backend + - image: ghcr.io/kptdev/krm-functions-catalog/set-namespace + tag: "0.4.1 - 0.4.3" + configMap: + namespace: staging + - image: ghcr.io/kptdev/krm-functions-catalog/set-labels + tag: ">=0.2.4 <0.2.5" + configMap: + tier: backend + - image: ghcr.io/kptdev/krm-functions-catalog/set-labels@sha256:b087fe8968d1641f495ee382f8f5b8c8e23b062903042775cc28fa6c6a64f6c1 + tag: ">=v0.2.4 =0.5.5, <0.5.6" + configPath: starlark-failure-fn.yaml + - image: ghcr.io/kptdev/krm-functions-catalog/set-namespace:v0.2.0 + configMap: + namespace: staging diff --git a/e2e/testdata/fn-render/fn-failure-print-fn-version/deployment_httpbin.yaml b/e2e/testdata/fn-render/fn-failure-print-fn-version/deployment_httpbin.yaml new file mode 100644 index 000000000..d909fa0a9 --- /dev/null +++ b/e2e/testdata/fn-render/fn-failure-print-fn-version/deployment_httpbin.yaml @@ -0,0 +1,37 @@ +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: backend + tier: db + name: httpbin + namespace: staging +spec: + replicas: 4 + selector: + matchLabels: + app: backend + tier: db + template: + metadata: + labels: + app: backend + tier: db + spec: + containers: + - image: kennethreitz/httpbin + name: httpbin diff --git a/e2e/testdata/fn-render/fn-failure-print-fn-version/resources.yaml b/e2e/testdata/fn-render/fn-failure-print-fn-version/resources.yaml new file mode 100644 index 000000000..1f1515040 --- /dev/null +++ b/e2e/testdata/fn-render/fn-failure-print-fn-version/resources.yaml @@ -0,0 +1,26 @@ +# 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. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 +--- +apiVersion: custom.io/v1 +kind: Custom +metadata: + name: custom +spec: + image: nginx:1.2.3 diff --git a/e2e/testdata/fn-render/fn-failure-print-fn-version/starlark-failure-fn.yaml b/e2e/testdata/fn-render/fn-failure-print-fn-version/starlark-failure-fn.yaml new file mode 100644 index 000000000..9a7064c15 --- /dev/null +++ b/e2e/testdata/fn-render/fn-failure-print-fn-version/starlark-failure-fn.yaml @@ -0,0 +1,26 @@ +# 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. + +apiVersion: fn.kpt.dev/v1alpha1 +kind: StarlarkRun +metadata: + name: httpbin-gen +source: | + # AND conditional has syntax error forcing fn to fail + def isHTTPBin(r): + return r["apiVersion"] == "apps/v1" and r["kind"] == "Deployment" and + r["metadata"]["name"] == "httpbin" + + # filter out the httpbin deployment + ctx.resource_list["items"] = [r for r in ctx.resource_list["items"] if not isHTTPBin(r)] diff --git a/e2e/testdata/fn-render/missing-fn-image-print-tag/.expected/config.yaml b/e2e/testdata/fn-render/missing-fn-image-print-tag/.expected/config.yaml new file mode 100644 index 000000000..6274171d1 --- /dev/null +++ b/e2e/testdata/fn-render/missing-fn-image-print-tag/.expected/config.yaml @@ -0,0 +1,18 @@ +# 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. + +exitCode: 1 +stdErr: | + [RESOLUTION FAIL] "ghcr.io/kptdev/krm-functions-catalog/dne:0.4.x" + Error: failed to list tags for image "ghcr.io/kptdev/krm-functions-catalog/dne": failed to list tags for ghcr.io/kptdev/krm-functions-catalog/dne:latest: unauthorized \ No newline at end of file diff --git a/e2e/testdata/fn-render/missing-fn-image-print-tag/.krmignore b/e2e/testdata/fn-render/missing-fn-image-print-tag/.krmignore new file mode 100644 index 000000000..9d7a4007d --- /dev/null +++ b/e2e/testdata/fn-render/missing-fn-image-print-tag/.krmignore @@ -0,0 +1 @@ +.expected diff --git a/e2e/testdata/fn-render/missing-fn-image-print-tag/Kptfile b/e2e/testdata/fn-render/missing-fn-image-print-tag/Kptfile new file mode 100644 index 000000000..3466a07e4 --- /dev/null +++ b/e2e/testdata/fn-render/missing-fn-image-print-tag/Kptfile @@ -0,0 +1,10 @@ +apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: app +pipeline: + mutators: + - image: ghcr.io/kptdev/krm-functions-catalog/dne # non-existing image + tag: "0.4.x" + configMap: + tier: backend diff --git a/e2e/testdata/fn-render/missing-fn-image-print-tag/resources.yaml b/e2e/testdata/fn-render/missing-fn-image-print-tag/resources.yaml new file mode 100644 index 000000000..1f1515040 --- /dev/null +++ b/e2e/testdata/fn-render/missing-fn-image-print-tag/resources.yaml @@ -0,0 +1,26 @@ +# 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. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 +--- +apiVersion: custom.io/v1 +kind: Custom +metadata: + name: custom +spec: + image: nginx:1.2.3 diff --git a/internal/fnruntime/container.go b/internal/fnruntime/container.go index 2b2dc0cee..cd675e0d7 100644 --- a/internal/fnruntime/container.go +++ b/internal/fnruntime/container.go @@ -80,7 +80,6 @@ type ContainerFn struct { // Image is the container image to run Image string - Tag string // ImagePullPolicy controls the image pulling behavior. ImagePullPolicy runneroptions.ImagePullPolicy // Container function will be killed after this timeour. @@ -152,21 +151,6 @@ func (f *ContainerFn) Run(reader io.Reader, writer io.Writer) error { return err } - if f.Tag != "" { - tagResolver := &TagResolver{ - lister: &RegClientLister{ - client: regclient.New( - regclient.WithUserAgent(UserAgent), - regclient.WithDockerCreds(), - ), - }, - } - f.Image, err = tagResolver.ResolveFunctionImage(f.Ctx, f.Image, f.Tag) - if err != nil { - return err - } - } - switch runtime { case Podman: return f.runCLI(reader, writer, podmanBin, filterPodmanCLIOutput) diff --git a/internal/fnruntime/runner.go b/internal/fnruntime/runner.go index 8695367cc..15b881440 100644 --- a/internal/fnruntime/runner.go +++ b/internal/fnruntime/runner.go @@ -35,6 +35,8 @@ import ( "github.com/kptdev/kpt/pkg/lib/errors" "github.com/kptdev/kpt/pkg/lib/runneroptions" "github.com/kptdev/kpt/pkg/printer" + "github.com/regclient/regclient" + regclientref "github.com/regclient/regclient/types/ref" "sigs.k8s.io/kustomize/kyaml/filesys" "sigs.k8s.io/kustomize/kyaml/fn/framework" "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" @@ -43,7 +45,7 @@ import ( ) // NewRunner returns a FunctionRunner given a specification of a function -// and it's config. +// and its config. func NewRunner( ctx context.Context, fsys filesys.FileSystem, @@ -96,6 +98,32 @@ func NewRunner( } else { switch { case f.Image != "": + if f.Tag != "" { + // Combine Image and Tag config to actual image that + // will be executed + tagResolver := &TagResolver{ + lister: &RegClientLister{ + client: regclient.New( + regclient.WithUserAgent(UserAgent), + regclient.WithDockerCreds(), + ), + }, + } + f.Image, err = tagResolver.ResolveFunctionImage(ctx, f.Image, f.Tag) + if err != nil { + pr := printer.FromContextOrDie(ctx) + pr.Printf("[RESOLUTION FAIL] %q\n", buildFunctionDisplayName(fnResult.Image, f.Tag)) + return nil, err + } + + fnResult.Image = f.Image + } else if noTagOrDigestSpecified(f) { + // No Tag specified, either exactly or with a semver constraint; + // no tag or digest already in image string. + // kpt resolution defaults to "latest": we reflect that in the FnResult + fnResult.Image = f.Image + ":latest" + } + // If allowWasm is true, we will use wasm runtime for image field. if opts.AllowWasm { wFn, err := NewWasmFn(NewOciLoader(filepath.Join(os.TempDir(), "krm-fn-wasm"), f.Image)) @@ -106,7 +134,6 @@ func NewRunner( } else { cfn := &ContainerFn{ Image: f.Image, - Tag: f.Tag, ImagePullPolicy: opts.ImagePullPolicy, Perm: ContainerFnPermission{ AllowNetwork: opts.AllowNetwork, @@ -156,8 +183,15 @@ func NewRunner( return NewFunctionRunner(ctx, fltr, pkgPath, fnResult, fnResults, opts) } +func noTagOrDigestSpecified(f *kptfilev1.Function) bool { + ref, err := regclientref.New(f.Image) + return err == nil && + ref.Tag == "latest" && ref.Digest == "" && + !strings.Contains(f.Image, ":latest") +} + // NewFunctionRunner returns a FunctionRunner given a specification of a function -// and it's config. +// and its config. func NewFunctionRunner(ctx context.Context, fltr *runtimeutil.FunctionFilter, pkgPath types.UniquePath, @@ -249,6 +283,11 @@ func (fr *FunctionRunner) do(input []*yaml.RNode) (output []*yaml.RNode, err err fnResult := fr.fnResult output, err = fr.filter.Filter(input) + if fr.fnResult.Image != "" { + fr.name = fr.fnResult.Image + } else { + fr.name = fr.fnResult.ExecPath + } if fr.opts.SetPkgPathAnnotation { if pkgPathErr := setPkgPathAnnotationIfNotExist(output, fr.pkgPath); pkgPathErr != nil { @@ -508,3 +547,38 @@ func newFnConfig(fsys filesys.FileSystem, f *kptfilev1.Function, pkgPath types.U // no need to return ConfigMap if no config given return nil, nil } + +// buildFunctionDisplayName constructs the display name for a function from its result metadata. +// It combines the image name with the appropriate tag, handling cases where the tag +// is already present in the image name or needs to be appended. +func buildFunctionDisplayName(name, tag string) string { + if name == "" { + return "" + } + if strings.HasPrefix(name, "builtins/") { + // Built-in pseudo-image - no tagging + return name + } + + // Find digest separator first (takes precedence over tag) + separatorIndex := strings.LastIndex(name, "@") + if separatorIndex == -1 { + // No digest - look for tag separator + separatorIndex = strings.LastIndex(name, ":") + } + nameHasTagOrDigest := separatorIndex != -1 && !strings.Contains(name[separatorIndex+1:], "/") + + // If no tag specified and name already has tag, use as-is + if tag == "" { + if nameHasTagOrDigest { + return name + } + return name + ":latest" + } + + // Tag specified: replace existing tag or append + if nameHasTagOrDigest { + return name[:separatorIndex] + ":" + tag + } + return name + ":" + tag +} diff --git a/internal/fnruntime/runner_test.go b/internal/fnruntime/runner_test.go index 3c22980e4..70f95ad17 100644 --- a/internal/fnruntime/runner_test.go +++ b/internal/fnruntime/runner_test.go @@ -660,3 +660,39 @@ func getExpectedPrefix(prefix string) string { } return prefix } + +func TestBuildFunctionDisplayName(t *testing.T) { + tests := []struct { + name string + image string + tag string + expected string + }{ + {"no tag, no colon", "myimage", "", "myimage:latest"}, + {"no tag, has colon", "myimage:v1", "", "myimage:v1"}, + { + "no tag, has digest", + "myimage@sha256:b087fe8968d1641f495ee382f8f5b8c8e23b062903042775cc28fa6c6a64f6c1", + "", + "myimage@sha256:b087fe8968d1641f495ee382f8f5b8c8e23b062903042775cc28fa6c6a64f6c1", + }, + {"has tag, no colon", "myimage", "v2", "myimage:v2"}, + {"has semver tag, no colon", "myimage", "~0.2", "myimage:~0.2"}, + {"has tag, has colon", "myimage:v1", "v2", "myimage:v2"}, + {"has semver tag, has colon", "myimage:v1", "~0.2", "myimage:~0.2"}, + {"has semver tag, has digest", "myimage@sha256:b087fe8968d1641f495ee382f8f5b8c8e23b062903042775cc28fa6c6a64f6c1", "~0.2", "myimage:~0.2"}, + {"empty image", "", "v1", ""}, + {"registry with port", "localhost:5000/myimage", "v1", "localhost:5000/myimage:v1"}, + {"registry with port, no tag", "localhost:5000/myimage", "", "localhost:5000/myimage:latest"}, + {"registry with port and digest", "localhost:5000/myimage@sha256:b087fe8968d1641f495ee382f8f5b8c8e23b062903042775cc28fa6c6a64f6c1", "v1", "localhost:5000/myimage:v1"}, + {"registry with port and tag", "localhost:5000/myimage:old", "new", "localhost:5000/myimage:new"}, + {"registry with port, semver tag", "localhost:5000/myimage:old", "~0.2", "localhost:5000/myimage:~0.2"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := buildFunctionDisplayName(tc.image, tc.tag) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/internal/util/render/executor.go b/internal/util/render/executor.go index d74ccf495..1a583cd40 100644 --- a/internal/util/render/executor.go +++ b/internal/util/render/executor.go @@ -250,7 +250,7 @@ type pkgNode struct { state hydrationState // KRM resources that we have gathered post hydration for this package. - // These inludes resources at this pkg as well all it's children. + // These include this package's resources, as well as all its children. resources []*yaml.RNode }