diff --git a/pkg/lib/kptops/helpers_test.go b/pkg/lib/kptops/helpers_test.go index 26a58c92e1..6b4bfdce5d 100644 --- a/pkg/lib/kptops/helpers_test.go +++ b/pkg/lib/kptops/helpers_test.go @@ -37,6 +37,7 @@ import ( var testFunctions = map[string]framework.ResourceListProcessorFunc{ "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest": setLabels, "ghcr.io/kptdev/krm-functions-catalog/set-namespace:latest": setNamespace, + "ghcr.io/test/check-fnconfig-labels:latest": checkFnConfigLabels, } // runtime is a test-only FunctionRuntime that resolves functions from testFunctions. @@ -120,3 +121,17 @@ func validGVK(rn *kyaml.RNode, apiVersion, kind string) bool { } return meta.APIVersion == apiVersion && meta.Kind == kind } + +// checkFnConfigLabels is a test-only validator that verifies its functionConfig +// has a specific label. This is used to test that validators receive updated +// functionConfig from in-memory input after mutators have modified it. +func checkFnConfigLabels(rl *framework.ResourceList) error { + if rl.FunctionConfig == nil { + return errors.New("functionConfig is nil") + } + labels := rl.FunctionConfig.GetLabels() + if _, ok := labels["env"]; !ok { + return fmt.Errorf("functionConfig is stale - missing 'env' label that mutator should have added; labels: %v", labels) + } + return nil +} diff --git a/pkg/lib/kptops/render_executor.go b/pkg/lib/kptops/render_executor.go index 05d4e81556..953789cd29 100644 --- a/pkg/lib/kptops/render_executor.go +++ b/pkg/lib/kptops/render_executor.go @@ -858,11 +858,8 @@ func (pn *pkgNode) runValidators(ctx context.Context, hctx *hydrationContext, in if err != nil { return err } - var validator kio.Filter - displayResourceCount := false - if len(function.Selectors) > 0 || len(function.Exclusions) > 0 { - displayResourceCount = true - } + var validator *fnruntime.FunctionRunner + displayResourceCount := len(function.Selectors) > 0 || len(function.Exclusions) > 0 if function.Exec != "" && !hctx.runnerOptions.AllowExec { hctx.validationSteps = append(hctx.validationSteps, preExecFailureStep(function, errAllowedExecNotSpecified)) return errAllowedExecNotSpecified @@ -875,6 +872,11 @@ func (pn *pkgNode) runValidators(ctx context.Context, hctx *hydrationContext, in hctx.validationSteps = append(hctx.validationSteps, preExecFailureStep(function, err)) return err } + + if err := pn.refreshFnConfig(validator, input, function.ConfigPath); err != nil { + return err + } + if _, err = validator.Filter(cloneResources(selectedResources)); err != nil { hctx.validationSteps = append(hctx.validationSteps, captureStepResult(function, hctx.fnResults, resultCountBeforeExec, err)) return err @@ -892,6 +894,29 @@ func cloneResources(input []*yaml.RNode) (output []*yaml.RNode) { return } +// refreshFnConfig updates the runner's functionConfig from the in-memory input +// to pick up any mutations applied earlier in the pipeline. +func (pn *pkgNode) refreshFnConfig(runner *fnruntime.FunctionRunner, input []*yaml.RNode, configPath string) error { + if configPath == "" { + return nil + } + for _, r := range input { + pkgPath, err := pkg.GetPkgPathAnnotation(r) + if err != nil { + return err + } + currPath, _, err := kioutil.GetFileAnnotations(r) + if err != nil { + return err + } + if pkgPath == pn.pkg.UniquePath.String() && currPath == configPath { + runner.SetFnConfig(r) + break + } + } + return nil +} + // clearAnnotationsOnMutFailure removes annotations that are added during mutation when mutation fails. func clearAnnotationsOnMutFailure(input []*yaml.RNode) { annotations := []string{ diff --git a/pkg/lib/kptops/render_executor_test.go b/pkg/lib/kptops/render_executor_test.go index 56e310d863..a9def45c19 100644 --- a/pkg/lib/kptops/render_executor_test.go +++ b/pkg/lib/kptops/render_executor_test.go @@ -27,7 +27,9 @@ import ( fnruntime "github.com/kptdev/kpt/pkg/fn/runtime" "github.com/kptdev/kpt/pkg/kptfile/kptfileutil" "github.com/kptdev/kpt/pkg/lib/pkg" + "github.com/kptdev/kpt/pkg/lib/runneroptions" "github.com/kptdev/kpt/pkg/printer" + "github.com/kptdev/kpt/pkg/printer/fake" "github.com/stretchr/testify/assert" "sigs.k8s.io/kustomize/kyaml/filesys" "sigs.k8s.io/kustomize/kyaml/fn/framework" @@ -1050,3 +1052,79 @@ metadata: }) } } + +func TestRunValidators_RefreshFnConfigFromInMemoryInput(t *testing.T) { + // This test verifies that when a mutator modifies a resource that is used as + // a validator's functionConfig (via configPath), the validator sees the + // updated in-memory version rather than the stale on-disk version. + + tests := []struct { + name string + renderBfs bool + }{ + {name: "DFS rendering", renderBfs: false}, + {name: "BFS rendering", renderBfs: true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockFS := filesys.MakeFsInMemory() + pkgPath := rootString + assert.NoError(t, mockFS.Mkdir(pkgPath)) + + kptfileContent := fmt.Appendf(nil, `apiVersion: kpt.dev/v1 +kind: Kptfile +metadata: + name: test-pkg + annotations: + kpt.dev/bfs-rendering: %q +pipeline: + mutators: + - image: ghcr.io/kptdev/krm-functions-catalog/set-labels:latest + configMap: + env: production + validators: + - image: ghcr.io/test/check-fnconfig-labels:latest + configPath: validator-config.yaml +`, fmt.Sprintf("%t", tc.renderBfs)) + assert.NoError(t, mockFS.WriteFile(filepath.Join(pkgPath, "Kptfile"), kptfileContent)) + + validatorConfig := `apiVersion: v1 +kind: ConfigMap +metadata: + name: validator-config + annotations: + config.kubernetes.io/local-config: "true" +data: + check: labels +` + assert.NoError(t, mockFS.WriteFile(filepath.Join(pkgPath, "validator-config.yaml"), []byte(validatorConfig))) + + deployment := `apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + replicas: 1 +` + assert.NoError(t, mockFS.WriteFile(filepath.Join(pkgPath, "deployment.yaml"), []byte(deployment))) + + r := &Renderer{ + PkgPath: pkgPath, + FileSystem: mockFS, + Runtime: &runtime{}, + } + r.RunnerOptions.InitDefaults(runneroptions.GHCRImagePrefix) + r.RunnerOptions.ImagePullPolicy = runneroptions.IfNotPresentPull + + ctx := fake.CtxWithDefaultPrinter() + _, err := r.Execute(ctx) + assert.NoError(t, err, "validator should pass because it receives the mutated functionConfig with 'env' label") + + // Verify the validator config was indeed mutated (written with labels) + res, err := mockFS.ReadFile(filepath.Join(pkgPath, "validator-config.yaml")) + assert.NoError(t, err) + assert.Contains(t, string(res), "env: production", "validator-config.yaml should have the label added by the mutator") + }) + } +}