diff --git a/e2e/testdata/fn-render/basicpipeline-semver/.expected/diff.patch b/e2e/testdata/fn-render/basicpipeline-semver/.expected/diff.patch index e38282bfdb..e1c701ed7b 100644 --- a/e2e/testdata/fn-render/basicpipeline-semver/.expected/diff.patch +++ b/e2e/testdata/fn-render/basicpipeline-semver/.expected/diff.patch @@ -1,5 +1,5 @@ diff --git a/Kptfile b/Kptfile -index 2336da4..c1090e8 100644 +index 2336da4..ca2bcea 100644 --- a/Kptfile +++ b/Kptfile @@ -2,13 +2,34 @@ apiVersion: kpt.dev/v1 @@ -27,14 +27,14 @@ index 2336da4..c1090e8 100644 + reason: RenderSuccess + renderStatus: + mutationSteps: -+ - image: ghcr.io/kptdev/krm-functions-catalog/set-namespace ++ - image: ghcr.io/kptdev/krm-functions-catalog/set-namespace:v0.4.3 + exitCode: 0 + results: + - message: namespace [default] updated to "staging", 1 value(s) changed + severity: info + - message: all `depends-on` annotations are up-to-date. no `namespace` changed + severity: info -+ - image: ghcr.io/kptdev/krm-functions-catalog/set-labels ++ - image: ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.2.4 + exitCode: 0 + results: + - message: set 4 labels in total diff --git a/e2e/testdata/fn-render/krm-check-exclude-kustomize/.expected/diff.patch b/e2e/testdata/fn-render/krm-check-exclude-kustomize/.expected/diff.patch index 1176e6104b..3c45e99dc8 100644 --- a/e2e/testdata/fn-render/krm-check-exclude-kustomize/.expected/diff.patch +++ b/e2e/testdata/fn-render/krm-check-exclude-kustomize/.expected/diff.patch @@ -1,5 +1,5 @@ diff --git a/Kptfile b/Kptfile -index 2985a1a..1cc880e 100644 +index 2985a1a..30b4376 100644 --- a/Kptfile +++ b/Kptfile @@ -2,8 +2,19 @@ apiVersion: kpt.dev/v1 @@ -20,7 +20,7 @@ index 2985a1a..1cc880e 100644 + reason: RenderSuccess + renderStatus: + mutationSteps: -+ - image: set-labels:v0.1.5 ++ - image: ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.1.5 + exitCode: 0 diff --git a/kustomization.yaml b/kustomization.yaml index f3f0207..6c517af 100644 diff --git a/e2e/testdata/fn-render/missing-fn-image/.expected/diff.patch b/e2e/testdata/fn-render/missing-fn-image/.expected/diff.patch index b34b260136..c772c52649 100644 --- a/e2e/testdata/fn-render/missing-fn-image/.expected/diff.patch +++ b/e2e/testdata/fn-render/missing-fn-image/.expected/diff.patch @@ -1,5 +1,5 @@ diff --git a/Kptfile b/Kptfile -index 11012de..9fadb6e 100644 +index 11012de..a0f4634 100644 --- a/Kptfile +++ b/Kptfile @@ -7,6 +7,26 @@ pipeline: @@ -22,11 +22,11 @@ index 11012de..9fadb6e 100644 + mutationSteps: + - image: ghcr.io/kptdev/krm-functions-catalog/set-namespace:v0.2.0 + exitCode: 0 -+ - image: ghcr.io/kptdev/krm-functions-catalog/dne ++ - image: ghcr.io/kptdev/krm-functions-catalog/dne:latest + stderr: |- + docker: Error response from daemon: error from registry: denied + denied + + Run 'docker run --help' for more information + exitCode: 125 -+ errorSummary: 'ghcr.io/kptdev/krm-functions-catalog/dne: exit code 125' ++ errorSummary: 'ghcr.io/kptdev/krm-functions-catalog/dne:latest: exit code 125' diff --git a/e2e/testdata/fn-render/no-op/.expected/diff.patch b/e2e/testdata/fn-render/no-op/.expected/diff.patch index a32f35e4c2..36cb50a2e7 100644 --- a/e2e/testdata/fn-render/no-op/.expected/diff.patch +++ b/e2e/testdata/fn-render/no-op/.expected/diff.patch @@ -1,5 +1,5 @@ diff --git a/Kptfile b/Kptfile -index a7a2d0b..ed39ce3 100644 +index a7a2d0b..3dbfee4 100644 --- a/Kptfile +++ b/Kptfile @@ -5,3 +5,12 @@ metadata: @@ -13,5 +13,5 @@ index a7a2d0b..ed39ce3 100644 + reason: RenderSuccess + renderStatus: + mutationSteps: -+ - image: ghcr.io/kptdev/krm-functions-catalog/no-op ++ - image: ghcr.io/kptdev/krm-functions-catalog/no-op:latest + exitCode: 0 diff --git a/e2e/testdata/fn-render/short-image-path/.expected/diff.patch b/e2e/testdata/fn-render/short-image-path/.expected/diff.patch index 60e4a2c463..5d0f6b9ea7 100644 --- a/e2e/testdata/fn-render/short-image-path/.expected/diff.patch +++ b/e2e/testdata/fn-render/short-image-path/.expected/diff.patch @@ -1,5 +1,5 @@ diff --git a/Kptfile b/Kptfile -index d4e5935..0759cb0 100644 +index d4e5935..24022da 100644 --- a/Kptfile +++ b/Kptfile @@ -2,6 +2,9 @@ apiVersion: kpt.dev/v1 @@ -23,9 +23,9 @@ index d4e5935..0759cb0 100644 + reason: RenderSuccess + renderStatus: + mutationSteps: -+ - image: set-namespace:v0.2.0 ++ - image: ghcr.io/kptdev/krm-functions-catalog/set-namespace:v0.2.0 + exitCode: 0 -+ - image: set-labels:v0.1.5 ++ - image: ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.1.5 + exitCode: 0 diff --git a/resources.yaml b/resources.yaml index f2eec52..84cfb26 100644 diff --git a/internal/fnruntime/container.go b/internal/fnruntime/container.go index 2b2dc0cee2..cd675e0d7a 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 ca9c968b81..035e8ad701 100644 --- a/internal/fnruntime/runner.go +++ b/internal/fnruntime/runner.go @@ -35,6 +35,7 @@ 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" "sigs.k8s.io/kustomize/kyaml/filesys" "sigs.k8s.io/kustomize/kyaml/fn/framework" "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" @@ -60,6 +61,23 @@ func NewRunner( if f.Image != "" { img := opts.ResolveToImage(f.Image) f.Image = img + + if f.Tag != "" { + 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 { + return nil, err + } + } else if !hasTagOrDigest(f.Image) { + f.Image += ":latest" + } } fnResult := &fnresult.Result{ @@ -103,7 +121,6 @@ func NewRunner( } else { cfn := &ContainerFn{ Image: f.Image, - Tag: f.Tag, ImagePullPolicy: opts.ImagePullPolicy, Perm: ContainerFnPermission{ AllowNetwork: opts.AllowNetwork, @@ -505,3 +522,15 @@ func newFnConfig(fsys filesys.FileSystem, f *kptfilev1.Function, pkgPath types.U // no need to return ConfigMap if no config given return nil, nil } + +// hasTagOrDigest reports whether the image reference contains an explicit tag or digest. +func hasTagOrDigest(image string) bool { + if strings.Contains(image, "@") { + return true + } + lastSlash := strings.LastIndex(image, "/") + if lastSlash == -1 { + return strings.Contains(image, ":") + } + return strings.Contains(image[lastSlash:], ":") +} diff --git a/internal/fnruntime/runner_test.go b/internal/fnruntime/runner_test.go index 7253c6c862..0c009b7f97 100644 --- a/internal/fnruntime/runner_test.go +++ b/internal/fnruntime/runner_test.go @@ -658,3 +658,38 @@ func getExpectedPrefix(prefix string) string { } return prefix } + +func TestHasTagOrDigest(t *testing.T) { + tests := []struct { + name string + image string + want bool + }{ + // With explicit tag + {"tag", "nginx:1.25", true}, + {"latest tag", "nginx:latest", true}, + {"full path with tag", "ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.1.5", true}, + + // With digest + {"digest only", "nginx@sha256:abc123", true}, + {"full path with digest", "ghcr.io/kptdev/krm-functions-catalog/set-labels@sha256:abc123", true}, + {"tag and digest", "nginx:1.25@sha256:abc123", true}, + + // Without tag or digest + {"bare image", "nginx", false}, + {"full path no tag", "ghcr.io/kptdev/krm-functions-catalog/set-labels", false}, + {"two-part no tag", "library/nginx", false}, + + // Registry with port (should not confuse port colon with tag colon) + {"registry port no tag", "localhost:5000/myimage", false}, + {"registry port with tag", "localhost:5000/myimage:v1", true}, + {"registry port with digest", "localhost:5000/myimage@sha256:abc123", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasTagOrDigest(tt.image); got != tt.want { + t.Errorf("hasTagOrDigest(%q) = %v, want %v", tt.image, got, tt.want) + } + }) + } +} diff --git a/internal/util/render/executor.go b/internal/util/render/executor.go index 8bd5c652da..10406fd6e5 100644 --- a/internal/util/render/executor.go +++ b/internal/util/render/executor.go @@ -982,18 +982,17 @@ func fnChain(ctx context.Context, hctx *hydrationContext, pkgPath types.UniquePa for i := range fns { var err error var runner *fnruntime.FunctionRunner - function := fns[i] displayResourceCount := false - if len(function.Selectors) > 0 || len(function.Exclusions) > 0 { + if len(fns[i].Selectors) > 0 || len(fns[i].Exclusions) > 0 { displayResourceCount = true } - if function.Exec != "" && !hctx.runnerOptions.AllowExec { + if fns[i].Exec != "" && !hctx.runnerOptions.AllowExec { return nil, i, errAllowedExecNotSpecified } opts := hctx.runnerOptions opts.SetPkgPathAnnotation = true opts.DisplayResourceCount = displayResourceCount - runner, err = fnruntime.NewRunner(ctx, hctx.fileSystem, &function, pkgPath, hctx.fnResults, opts, hctx.runtime) + runner, err = fnruntime.NewRunner(ctx, hctx.fileSystem, &fns[i], pkgPath, hctx.fnResults, opts, hctx.runtime) if err != nil { return nil, i, err } 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"`