Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Ensure consistent LF line endings for test data files
*.patch text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.sh text eol=lf
*.go text eol=lf
*.md text eol=lf
3 changes: 3 additions & 0 deletions commands/fn/render/cmdrender.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ type Runner struct {

func (r *Runner) InitDefaults() {
r.RunnerOptions.InitDefaults(runneroptions.GHCRImagePrefix)
// Initialize CEL environment for condition evaluation
// Ignore error as conditions are optional; if CEL init fails, conditions will error at runtime
_ = r.RunnerOptions.InitCELEnvironment()
Comment thread
SurbhiAgarwal1 marked this conversation as resolved.
}

func (r *Runner) preRunE(_ *cobra.Command, args []string) error {
Expand Down
7 changes: 5 additions & 2 deletions commands/fn/render/cmdrender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package render
import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/kptdev/kpt/internal/testutil"
Expand All @@ -32,15 +33,17 @@ func TestCmd_flagAndArgParsing_Symlink(t *testing.T) {
err := os.MkdirAll(filepath.Join(dir, "path", "to", "pkg", "dir"), 0700)
assert.NoError(t, err)
err = os.Symlink(filepath.Join("path", "to", "pkg", "dir"), "foo")
assert.NoError(t, err)
if err != nil {
t.Skipf("skipping test due to symlink creation failure (requires admin/developer mode on Windows): %v", err)
}

// verify the branch ref is set to the correct value
r := NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
r.Command.RunE = NoOpRunE
r.Command.SetArgs([]string{"foo"})
err = r.Command.Execute()
assert.NoError(t, err)
assert.Equal(t, filepath.Join("path", "to", "pkg", "dir"), r.pkgPath)
assert.Equal(t, strings.ToLower(filepath.Join("path", "to", "pkg", "dir")), strings.ToLower(r.pkgPath))
}

// NoOpRunE is a noop function to replace the run function of a command. Useful for testing argument parsing.
Expand Down
8 changes: 6 additions & 2 deletions commands/pkg/diff/cmddiff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package diff_test
import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/kptdev/kpt/commands/pkg/diff"
Expand Down Expand Up @@ -78,7 +79,9 @@ func TestCmd_flagAndArgParsing_Symlink(t *testing.T) {
err := os.MkdirAll(filepath.Join(dir, "path", "to", "pkg", "dir"), 0700)
assert.NoError(t, err)
err = os.Symlink(filepath.Join("path", "to", "pkg", "dir"), "foo")
assert.NoError(t, err)
if err != nil {
t.Skipf("skipping test due to symlink creation failure (requires admin/developer mode on Windows): %v", err)
}

// verify the branch ref is set to the correct value
r := diff.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
Expand All @@ -88,7 +91,8 @@ func TestCmd_flagAndArgParsing_Symlink(t *testing.T) {
assert.NoError(t, err)
cwd, err := os.Getwd()
assert.NoError(t, err)
assert.Equal(t, filepath.Join(cwd, "path", "to", "pkg", "dir"), r.Path)
expected := filepath.Join(cwd, "path", "to", "pkg", "dir")
assert.Equal(t, strings.ToLower(expected), strings.ToLower(r.Path))
}

var NoOpRunE = func(_ *cobra.Command, _ []string) error { return nil }
4 changes: 3 additions & 1 deletion commands/pkg/get/cmdget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,9 @@ func TestCmd_flagAndArgParsing_Symlink(t *testing.T) {
err := os.MkdirAll(filepath.Join(dir, "path", "to", "pkg", "dir"), 0700)
assert.NoError(t, err)
err = os.Symlink(filepath.Join("path", "to", "pkg", "dir"), "link")
assert.NoError(t, err)
if err != nil {
t.Skipf("skipping test due to symlink creation failure (requires admin/developer mode on Windows): %v", err)
}

r := get.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
r.Command.RunE = NoOpRunE
Expand Down
7 changes: 5 additions & 2 deletions commands/pkg/update/cmdupdate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ func TestCmd_flagAndArgParsing_Symlink(t *testing.T) {
err := os.MkdirAll(filepath.Join(dir, "path", "to", "pkg", "dir"), 0700)
assert.NoError(t, err)
err = os.Symlink(filepath.Join("path", "to", "pkg", "dir"), "foo")
assert.NoError(t, err)
if err != nil {
t.Skipf("skipping test due to symlink creation failure (requires admin/developer mode on Windows): %v", err)
}

// verify the branch ref is set to the correct value
r := update.NewRunner(fake.CtxWithDefaultPrinter(), "kpt")
Expand All @@ -363,7 +365,8 @@ func TestCmd_flagAndArgParsing_Symlink(t *testing.T) {
assert.Equal(t, kptfilev1.ResourceMerge, r.Update.Strategy)
cwd, err := os.Getwd()
assert.NoError(t, err)
assert.Equal(t, filepath.Join(cwd, "path", "to", "pkg", "dir"), r.Update.Pkg.UniquePath.String())
expected := filepath.Join(cwd, "path", "to", "pkg", "dir")
assert.Equal(t, strings.ToLower(expected), strings.ToLower(r.Update.Pkg.UniquePath.String()))
}

// TestCmd_fail verifies that that command returns an error when it fails rather than exiting the process
Expand Down
4 changes: 2 additions & 2 deletions documentation/content/en/book/01-getting-started/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ documents for [`kpt fn render`](../../reference/cli/fn/render/) and [`kpt fn eva

### Kubernetes cluster

To deploy the examples, you need a Kubernetes cluster and a configured kubectl context.
To deploy the examples, you need a Kubernetes cluster and a configured kubeconfig context.

For testing purposes, the [kind](https://kind.sigs.k8s.io/docs/user/quick-start/) tool is useful for running an ephemeral Kubernetes
cluster on your local host.
Expand Down Expand Up @@ -106,7 +106,7 @@ vim deployment.yaml
#### Automating one-time edits with functions

The [`kpt fn`](../../reference/cli/fn/) set of commands enables you to execute programs called _kpt functions_. These programs are
packaged as containers and take YAML files as input, mutate or validate them, and then output YAML.
packaged as containers and take in YAML files, mutate or validate them, and then output YAML.

For example, you can use a function (`ghcr.io/kptdev/krm-functions-catalog/search-replace:latest`) to search for and replace all the occurrences of the `app` key, in the `spec` section of the YAML document (`spec.**.app`), and set the value to `my-nginx`.

Expand Down
63 changes: 63 additions & 0 deletions documentation/content/en/book/04-using-functions/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,69 @@ will merge each function pipeline list as an associative list, using
`name` as the merge key. An unspecified `name` or duplicated names may
result in unexpected merges.

### Specifying `condition`

The `condition` field lets you skip a function based on the current state of the resources in the package.
It takes a [CEL](https://cel.dev/) expression that is evaluated against the resource list. If the expression
returns `true`, the function runs. If it returns `false`, the function is skipped.

The expression receives a variable called `resources`, which is a list of all KRM resources passed to
this function step (after `selectors` and `exclude` have been applied). Each resource is a map with
the standard fields `apiVersion`, `kind`, and `metadata`. Depending on the resource, fields such as
`spec` and `status` may also be present.

For example, only run the `set-labels` function if a `ConfigMap` named `app-config` exists in the package:

```yaml
# wordpress/Kptfile (Excerpt)
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
name: wordpress
pipeline:
mutators:
- image: ghcr.io/kptdev/krm-functions-catalog/set-labels:latest
configMap:
app: wordpress
condition: resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')
```

When you render the package, kpt shows whether the function ran or was skipped:

```shell
$ kpt fn render wordpress
Package "wordpress":

[RUNNING] "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest"
[PASS] "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest"

Successfully executed 1 function(s) in 1 package(s).
```

If the condition is not met:

```shell
$ kpt fn render wordpress
Package "wordpress":

[SKIPPED] "ghcr.io/kptdev/krm-functions-catalog/set-labels:latest" (condition not met)

Successfully executed 0 function(s) in 1 package(s).
```

Some useful CEL expression patterns:

- Check if a resource of a specific kind exists:
`resources.exists(r, r.kind == 'Deployment')`
- Check if a specific resource exists by name:
`resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'my-config')`
- Check the count of resources:
`resources.filter(r, r.kind == 'Deployment').size() > 0`

The `condition` field can be combined with `selectors` and `exclude`. The condition is evaluated
after selectors and exclusions are applied, so `resources` only contains the resources that
passed the selection criteria.

### Specifying `selectors`

In some cases, you want to invoke the function only on a subset of resources based on a
Expand Down
10 changes: 10 additions & 0 deletions documentation/content/en/reference/schema/kptfile/kptfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ definitions:
this is primarily used for merging function declaration with upstream counterparts
type: string
x-go-name: Name
condition:
description: |-
`Condition` is an optional CEL expression that determines whether this
function should be executed. The expression is evaluated against the list
of KRM resources passed to this function step (after `Selectors` and
`Exclude` have been applied) and should return a boolean value.
If omitted or evaluates to true, the function executes normally.
If evaluates to false, the function is skipped.
type: string
x-go-name: Condition
selectors:
description: |-
`Selectors` are used to specify resources on which the function should be executed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
actualStripLines:
- " stderr: 'WARNING: The requested image''s platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested'"

stdErrStripLines:
- " Stderr:"
- " \"WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested\""

stdErr: |
Package: "condition-met"
[RUNNING] "ghcr.io/kptdev/krm-functions-catalog/no-op:latest"
[PASS] "ghcr.io/kptdev/krm-functions-catalog/no-op:latest" in 0s
Successfully executed 1 function(s) in 1 package(s).
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
diff --git a/Kptfile b/Kptfile
index eb90ac3..5ccde1c 100644
--- a/Kptfile
+++ b/Kptfile
@@ -5,4 +5,13 @@ metadata:
pipeline:
mutators:
- image: ghcr.io/kptdev/krm-functions-catalog/no-op
- condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')"
+ condition: resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')
+status:
+ conditions:
+ - type: Rendered
+ status: "True"
+ reason: RenderSuccess
+ renderStatus:
+ mutationSteps:
+ - image: ghcr.io/kptdev/krm-functions-catalog/no-op:latest
+ exitCode: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.expected
8 changes: 8 additions & 0 deletions e2e/testdata/fn-render/condition/condition-met/Kptfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
name: app
pipeline:
mutators:
- image: ghcr.io/kptdev/krm-functions-catalog/no-op
condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')"
13 changes: 13 additions & 0 deletions e2e/testdata/fn-render/condition/condition-met/resources.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
key: value
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
actualStripLines:
- " stderr: 'WARNING: The requested image''s platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested'"

stdErrStripLines:
- " Stderr:"
- " \"WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested\""

stdErr: |
Package: "condition-not-met"
[SKIPPED] "ghcr.io/kptdev/krm-functions-catalog/no-op:latest" (condition not met)
Successfully executed 0 function(s) in 1 package(s).
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
diff --git a/Kptfile b/Kptfile
index eb90ac3..ac7ae33 100644
--- a/Kptfile
+++ b/Kptfile
@@ -5,4 +5,14 @@ metadata:
pipeline:
mutators:
- image: ghcr.io/kptdev/krm-functions-catalog/no-op
- condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')"
+ condition: resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')
+status:
+ conditions:
+ - type: Rendered
+ status: "True"
+ reason: RenderSuccess
Comment thread
liamfallon marked this conversation as resolved.
+ renderStatus:
+ mutationSteps:
+ - image: ghcr.io/kptdev/krm-functions-catalog/no-op:latest
+ exitCode: 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also be worth capturing the condition field in status.renderStatus, so the skip condition is recorded alongside the render result.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @nagygergo and @aravindtga - the render condition should reflect the overall pipeline result, not individual function execution. Since the pipeline completed successfully (just with some functions skipped), keeping status: "True" makes sense.

I can update the reason to RenderedWithSkippedFunctions when at least one function was skipped, so consumers can distinguish between a fully executed render and one with skipped functions. I can also add the condition field to status.renderStatus so the skip reason is recorded alongside the result.

Let me know if you'd like me to go ahead with these changes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also be worth capturing the condition field in status.renderStatus, so the skip condition is recorded alongside the render result.

A condition field has been introduced in the mutation pipeline configuration:

mutators:
  - image: ghcr.io/kptdev/krm-functions-catalog/no-op
    condition: resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')

Can you also add the condition field to status.renderStatus.mutationSteps so it gets recorded when the pipeline execution is captured? e.g:

status:
  conditions:
    - type: Rendered
      status: "True"
      reason: RenderSuccess
  renderStatus:
    mutationSteps:
      - image: ghcr.io/kptdev/krm-functions-catalog/no-op:latest
        condition: resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')
        exitCode: 0
      ....

+ skipped: true
diff --git a/resources.yaml b/resources.yaml
index 7806994..c8cdecf 100644
--- a/resources.yaml
+++ b/resources.yaml
@@ -3,4 +3,4 @@ kind: Deployment
metadata:
name: my-app
spec:
- replicas: 1
\ No newline at end of file
+ replicas: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.expected
8 changes: 8 additions & 0 deletions e2e/testdata/fn-render/condition/condition-not-met/Kptfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
name: app
pipeline:
mutators:
- image: ghcr.io/kptdev/krm-functions-catalog/no-op
condition: "resources.exists(r, r.kind == 'ConfigMap' && r.metadata.name == 'app-config')"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 1
Loading
Loading