diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4cc95a6..d63227a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,7 +30,7 @@ env:
# The package to push, without a version tag. The default matches GitHub. For
# example xpkg.upbound.io/crossplane/function-template-go.
- XPKG: xpkg.upbound.io/${{ github.repository}}
+ XPKG: xpkg.upbound.io/borrelli-org/function-conditional-patch-and-transform
# The package version to push. The default is 0.0.0-gitsha.
XPKG_VERSION: ${{ inputs.version }}
@@ -144,7 +144,7 @@ jobs:
run: "curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh"
- name: Login to Upbound
- uses: docker/login-action@v3
+ uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3
if: env.XPKG_ACCESS_ID != ''
with:
registry: xpkg.upbound.io
diff --git a/.golangci.yml b/.golangci.yml
index 1fd5806..33be29b 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -38,7 +38,7 @@ linters-settings:
- default
- prefix(github.com/crossplane/crossplane-runtime)
- prefix(github.com/crossplane/function-sdk-go)
- - prefix(github.com/crossplane-contrib/function-patch-and-transform)
+ - prefix(github.com/upboundcare/function-conditional-patch-and-transform)
- blank
- dot
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6d5ebc8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+
+DOCKER := docker
+GO := go
+
+GOLANGCI_VERSION := 1.55.2
+
+generate:
+ $(GO) generate ./...
+
+test:
+ $(GO) test ./...
+
+lint:
+ $(DOCKER) run --rm -v $(CURDIR):/app -v ~/.cache/golangci-lint/v$(GOLANGCI_VERSION):/root/.cache -w /app golangci/golangci-lint:v$(GOLANGCI_VERSION) golangci-lint run --fix
+
+reviewable: generate test lint
+
+# run a local process for crossplane render
+run-local:
+ $(GO) run . --debug --insecure
\ No newline at end of file
diff --git a/README.md b/README.md
index 8a41aa7..dd21f92 100644
--- a/README.md
+++ b/README.md
@@ -1,140 +1,222 @@
-# function-patch-and-transform
-[![CI](https://github.com/crossplane-contrib/function-patch-and-transform/actions/workflows/ci.yml/badge.svg)](https://github.com/crossplane-contrib/function-patch-and-transform/actions/workflows/ci.yml) ![GitHub release (latest SemVer)](https://img.shields.io/github/release/crossplane-contrib/function-patch-and-transform)
+# function-conditional-patch-and-transform
-This [composition function][docs-functions] does everything Crossplane's
-built-in [patch & transform][docs-pandt] (P&T) composition does. Instead of
-specifying `spec.resources` in your Composition, you can use this function.
+[![CI](https://github.com/upboundcare/function-conditional-patch-and-transform/actions/workflows/ci.yml/badge.svg)](https://github.com/upboundcare/function-conditional-patch-and-transform/actions/workflows/ci.yml) ![GitHub release (latest SemVer)](https://img.shields.io/github/release/crossplane-contrib/function-conditional-patch-and-transform)
-Using this function, P&T looks like this:
+This composition function is a fork of the upstream [function-patch-and-transform](https://github.com/crossplane-contrib/function-patch-and-transform)
+that adds support for Conditional invocation of the function and the rendering
+of individual resources.
+
+## Installing this Function
+
+The function can be installed as a Crossplane package, and runs in a [Composition Function](https://docs.crossplane.io/latest/concepts/composition-functions/). This feature requires a minium Crossplane version of 1.14.
```yaml
-apiVersion: apiextensions.crossplane.io/v1
-kind: Composition
+apiVersion: pkg.crossplane.io/v1beta1
+kind: Function
metadata:
- name: example
+ name: function-conditional-patch-and-transform
+ annotations:
+ render.crossplane.io/runtime: Development
spec:
- # Omitted for brevity.
- mode: Pipeline
+ package: xpkg.upbound.io/upboundcare/function-conditional-patch-and-transform:v0.4.0
+```
+
+## What this function does
+
+This function enables conditional rendering of the entire function or select resources.
+
+The language used for Conditionals is the [Common Expression Language (CEL)](https://github.com/google/cel-spec), which is widely used in the Kubernetes ecosystem.
+
+### Conditionally Running the Function
+
+Composition authors express a CEL condition, and if it returns `true`, patch-and-transforms defined in the `input` will be processed.
+
+```yaml
+ mode: Pipeline
pipeline:
- step: patch-and-transform
functionRef:
name: function-patch-and-transform
input:
- apiVersion: pt.fn.crossplane.io/v1beta1
+ apiVersion: conditional-pt.fn.crossplane.io/v1beta1
kind: Resources
- resources:
- - name: bucket
- base:
- apiVersion: s3.aws.upbound.io/v1beta1
- kind: Bucket
- spec:
- forProvider:
- region: us-east-2
- patches:
- - type: FromCompositeFieldPath
- fromFieldPath: "spec.location"
- toFieldPath: "spec.forProvider.region"
- transforms:
- - type: map
- map:
- EU: "eu-north-1"
- US: "us-east-2"
+ condition: observed.composite.resource.spec.env == "prod" && observed.composite.resource.spec.render == true
+ resources: [...all your resources...]
+```
+
+Using the following XR and RunFunctionRequest inputs (click to expand):
+
+
+```yaml
+apiVersion: nopexample.org/v1alpha1
+kind: XNopResource
+metadata:
+ name: test-resource
+spec:
+ env: dev
+ render: true
```
-Notice that it looks very similar to native P&T. The difference is that
-everything is under `spec.pipeline[0].input.resources`, not `spec.resources`.
-This is the Function's input.
+```json
+{
+ "desired": {
+ "composite": {
+ "resource": {
+ "apiVersion": "nopexample.org/v1alpha1",
+ "kind": "XNopResource",
+ "metadata": {
+ "name": "test-resource"
+ },
+ "spec": {
+ "env": "dev",
+ "render": true
+ }
+ }
+ },
+ "resources": {
+ "test": {
+ "resource": {
+ "apiVersion": "example.org/v1",
+ "kind": "CD",
+ "metadata": {
+ "name": "cool-42",
+ "namespace": "default"
+ }
+ }
+ }
+ }
+ },
+ "observed": {
+ "composite": {
+ "resource": {
+ "apiVersion": "nopexample.org/v1alpha1",
+ "kind": "XNopResource",
+ "metadata": {
+ "name": "test-resource"
+ },
+ "spec": {
+ "env": "dev",
+ "render": true
+ },
+ "status": {
+ "id": "123",
+ "ready": false
+ }
+ }
+ }
+ }
+}
+```
-## Okay, but why?
+
-There are a lot of good reasons to use a function to use a function to do P&T
-composition. In fact, it's so compelling that the Crossplane maintainers are
-considering deprecating native P&T. See Crossplane issue [#4746] for details.
+You can use the [CEL Playground](https://playcel.undistro.io/?content=H4sIAAAAAAAAA%2B1UPW%2FDIBT8K4g5SeW0U9Z27tCh6sDyYl5aVAwIsNUq8n%2BvMY4dGxx16dYNuON93D04Uw4e6IGemSKEMMrRCYuc0QOJR%2F1pqSujnfA4O%2B8hi07XtkyQHgQjXtE6oVWAGVXa4BdURuJO2%2Fe7pgBpPqBgdLO8%2BSkUj3fenrV5GZMkxAo9hB4y%2BXtcQYUxkEfnt1O5c26bBHYGy7WgqJoYk2OT1DTIojjaQPK2xkWuq%2B24ngqYNHWp3KGJrNQ3fCA5K%2BY%2B5JuYTHh8yjNuq0%2FmBpRay%2B3DPhdpZDoDJV60PUEt%2FdKphYCrevaLQVVG9dGhbf4H%2B28HO83lwdfJGF9QMShR7O%2FXkgH%2FDpwTSPerVxRdZ6qlm27ETadKMKn74Fjn1BH4lU5EKDJ8d7vxxdH2B6myt7YTBQAA) to test various queries.
-### Mix and match P&T with other functions
+Here are some example queries on the XR and RunFunctionRequest:
-With this function you can use P&T with other functions. For example you can
-create a desired resource using the [Go Templating][fn-go-templating] function,
-then patch the result using this function.
+- `desired.composite.resource.spec.env == "dev"` evaluates to `true`
+- `desired.composite.resource.spec.render == true,` evaluates to `true`
+- `desired.composite.resource.spec.render == false"` evaluates to `false`
+- `observed.composite.resource.status.ready == true"` evaluates to `false`
+- `size(desired.resources) == 0` evaluates to `false`
+- `"test" in desired.resources`evaluates to `true`
+- `"bad-resource" in desired.resources` evaluates to `false`
-To include results from previous functions, simply provide a `resources` entry
-for each and specify a `name` field that matches the name of the resource from
-the previous function. Also, do not specify any value for the `base` field of
-each resource.
+### Conditionally Rendering Managed Resources
-It's not just patches either. You can use P&T to derive composite resource
-connection details from a resource produced by another function, or use it to
-determine whether a resource produced by another function is ready.
+In a similar manner, individual Managed Resources can also
+be rendered conditionally, see the example at [examples/conditional-resources](examples/conditional-resources/).
-### Decouple P&T development from Crossplane core
+Each resource can have a `condition`.
-When P&T development happens in a function, it's not coupled to the Crossplane
-release cycle. The maintainers of this function can cut releases more frequently
-to add new features to P&T.
+```yaml
+ resources:
+ - name: blue-resource
+ condition: observed.composite.resource.spec.deployment.blue == true
+ base:
+ apiVersion: nop.crossplane.io/v1alpha1
+ kind: NopResource
+ spec:
+ forProvider:
+```
-It also becomes easier to fork. You could fork this function, add a new kind of
-transform and try it out for a few weeks before sending a PR upstream. Or, if
-your new feature is controversial, it's now a lot less work to maintain your own
-fork long term.
+If this condition is set in the Claim/XR, the resource will be rendered:
-### Test P&T locally using the Crossplane CLI
+```yaml
+apiVersion: nop.example.org/v1alpha1
+kind: XNopConditional
+metadata:
+ name: test-resource
+spec:
+ env: dev
+ render: true
+ deployment:
+ blue: true
+ green: false
+
+```
+
+### Test this function locally using the Crossplane CLI
You can use the Crossplane CLI to run any function locally and see what composed
resources it would create. This only works with functions - not native P&T.
-For example, using the files in the [example](example) directory:
+For example, using the files in the [examples](examples) directory:
```shell
-$ crossplane beta render xr.yaml composition.yaml functions.yaml
+cd examples/conditional-rendering
+crossplane beta render xr.yaml composition.yaml functions.yaml
```
+
Produces the following output, showing what resources Crossplane would compose:
```yaml
---
-apiVersion: example.crossplane.io/v1
-kind: XR
+apiVersion: nop.example.org/v1alpha1
+kind: XNopResource
metadata:
- name: example-xr
+ name: test-resource
---
-apiVersion: s3.aws.upbound.io/v1beta1
-kind: Bucket
+apiVersion: nop.crossplane.io/v1alpha1
+kind: NopResource
metadata:
annotations:
- crossplane.io/composition-resource-name: bucket
- generateName: example-xr-
+ crossplane.io/composition-resource-name: test-resource
+ generateName: test-resource-
labels:
- crossplane.io/composite: example-xr
+ crossplane.io/composite: test-resource
ownerReferences:
- # Omitted for brevity
+ - apiVersion: nop.example.org/v1alpha1
+ blockOwnerDeletion: true
+ controller: true
+ kind: XNopResource
+ name: test-resource
+ uid: ""
spec:
forProvider:
- region: us-east-2
+ conditionAfter:
+ - conditionStatus: "True"
+ conditionType: Ready
+ time: 5s
+ connectionDetails:
+ - name: username
+ value: fakeuser
+ - name: password
+ value: verysecurepassword
+ - name: endpoint
+ value: 127.0.0.1
+ fields:
+ arrayField:
+ - stringField: array
+ integerField: 42
+ objectField:
+ stringField: object
+ stringField: string
```
See the [composition functions documentation][docs-functions] to learn how to
use `crossplane beta render`.
-## Differences from the native implementation
-
-This function has a few small, intentional breaking changes compared to the
-native implementation.
-
-These fields are now required. This makes P&T configuration less ambiguous:
-
-* `resources[i].name`
-* `resources[i].connectionDetails[i].name`
-* `resources[i].connectionDetails[i].type`
-* `resources[i].patches[i].transforms[i].string.type`
-* `resources[i].patches[i].transforms[i].math.type`
-
-Also, the `resources[i].patches[i].policy.mergeOptions` field is no longer
-supported. The same capability can be achieved by setting
-`resources[i].patches[i].policy.toFieldPath` to:
-- `MergeObject` - equivalent to
- `resources[i].patches[i].policy.mergeOptions.keepMapValues: true`
-- `AppendArray` - equivalent to
- `resources[i].patches[i].policy.mergeOptions.appendSlice: false`
-
## Developing this function
This function uses [Go][go], [Docker][docker], and the [Crossplane CLI][cli] to
@@ -158,9 +240,9 @@ $ crossplane xpkg build -f package --embed-runtime-image=runtime
[docs-composition]: https://docs.crossplane.io/v1.14/getting-started/provider-aws-part-2/#create-a-deployment-template
[docs-functions]: https://docs.crossplane.io/v1.14/concepts/composition-functions/
[docs-pandt]: https://docs.crossplane.io/v1.14/concepts/patch-and-transform/
-[fn-go-templating]: https://github.com/crossplane-contrib/function-go-templating
+[fn-go-templating]: https://github.com/upboundcare/function-go-templating
[#4617]: https://github.com/crossplane/crossplane/issues/4617
[#4746]: https://github.com/crossplane/crossplane/issues/4746
[go]: https://go.dev
[docker]: https://www.docker.com
-[cli]: https://docs.crossplane.io/latest/cli
\ No newline at end of file
+[cli]: https://docs.crossplane.io/latest/cli
diff --git a/condition.go b/condition.go
new file mode 100644
index 0000000..941b46e
--- /dev/null
+++ b/condition.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+ "reflect"
+
+ "github.com/google/cel-go/cel"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+
+ fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
+)
+
+// NewCELEnvironment sets up the CEL Environment
+func NewCELEnvironment() (*cel.Env, error) {
+ return cel.NewEnv(
+ cel.Types(&fnv1beta1.State{}),
+ cel.Variable("observed", cel.ObjectType("apiextensions.fn.proto.v1beta1.State")),
+ cel.Variable("desired", cel.ObjectType("apiextensions.fn.proto.v1beta1.State")),
+ )
+}
+
+// ToCELVars formats a RunFunctionRequest for CEL evaluation
+func ToCELVars(req *fnv1beta1.RunFunctionRequest) map[string]any {
+ vars := make(map[string]any)
+ vars["desired"] = req.GetDesired()
+ vars["observed"] = req.GetObserved()
+ return vars
+}
+
+// EvaluateCondition will evaluate a CEL expression
+func EvaluateCondition(expression *string, req *fnv1beta1.RunFunctionRequest) (bool, error) {
+ if expression == nil {
+ return false, nil
+ }
+
+ env, err := NewCELEnvironment()
+ if err != nil {
+ return false, errors.Wrap(err, "CEL Env error")
+ }
+
+ ast, iss := env.Parse(*expression)
+ if iss.Err() != nil {
+ return false, errors.Wrap(iss.Err(), "CEL Parse error")
+ }
+
+ // Type-check the expression for correctness.
+ checked, iss := env.Check(ast)
+ // Report semantic errors, if present.
+ if iss.Err() != nil {
+ return false, errors.Wrap(iss.Err(), "CEL TypeCheck error")
+ }
+
+ // Ensure the output type is a bool.
+ if !reflect.DeepEqual(checked.OutputType(), cel.BoolType) {
+ return false, errors.Errorf(
+ "CEL Type error: expression '%s' must return a boolean, got %v instead",
+ *expression,
+ checked.OutputType())
+ }
+
+ // Plan the program.
+ program, err := env.Program(checked)
+ if err != nil {
+ return false, errors.Wrap(err, "CEL program plan")
+ }
+
+ // Convert our Function Request into map[string]any for CEL evaluation
+ vars := ToCELVars(req)
+
+ // Evaluate the program without any additional arguments.
+ result, _, err := program.Eval(vars)
+ if err != nil {
+ return false, errors.Wrap(err, "CEL program Evaluation")
+ }
+
+ ret, ok := result.Value().(bool)
+ if !ok {
+ return false, errors.Wrap(err, "CEL program did not return a bool")
+ }
+
+ return ret, nil
+}
diff --git a/condition_test.go b/condition_test.go
new file mode 100644
index 0000000..cf13e09
--- /dev/null
+++ b/condition_test.go
@@ -0,0 +1,363 @@
+package main
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "k8s.io/apimachinery/pkg/runtime"
+
+ "github.com/crossplane/crossplane-runtime/pkg/errors"
+
+ fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
+ "github.com/crossplane/function-sdk-go/resource"
+
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
+)
+
+func TestEvaluateCondition(t *testing.T) {
+ dxr := `{"apiVersion":"nopexample.org/v1alpha1","kind":"XNopResource","metadata":{"name":"test-resource"},"spec":{"env":"dev","render":true}}`
+ oxr := `{"apiVersion":"nopexample.org/v1alpha1","kind":"XNopResource","metadata":{"name":"test-resource"},"spec":{"env":"dev","render":true},"status":{"id":"123","ready":false} }`
+
+ type args struct {
+ condition v1beta1.Condition
+ req *fnv1beta1.RunFunctionRequest
+ }
+ type want struct {
+ ret bool
+ err error
+ }
+
+ cases := map[string]struct {
+ reason string
+ args args
+ want want
+ }{
+ "CELParseError": {
+ args: args{
+ condition: strPtr("field = value"),
+ req: &fnv1beta1.RunFunctionRequest{
+ Input: resource.MustStructObject(&v1beta1.Resources{
+ Resources: []v1beta1.ComposedTemplate{
+ {
+ Name: "cool-resource",
+ Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
+ },
+ },
+ }),
+ Observed: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(oxr),
+ },
+ },
+ Desired: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(dxr),
+ },
+ },
+ },
+ },
+ want: want{
+ ret: false,
+ err: errors.New("CEL Parse error"),
+ },
+ },
+ "CELTypeError": {
+ args: args{
+ condition: strPtr("size(desired.resources)"),
+ req: &fnv1beta1.RunFunctionRequest{
+ Input: resource.MustStructObject(&v1beta1.Resources{
+ Resources: []v1beta1.ComposedTemplate{
+ {
+ Name: "cool-resource",
+ Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
+ },
+ },
+ }),
+ Observed: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(oxr),
+ },
+ },
+ Desired: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(dxr),
+ },
+ },
+ },
+ },
+ want: want{
+ ret: false,
+ err: errors.New("CEL Type error: expression 'size(desired.resources)' must return a boolean, got int instead"),
+ },
+ },
+ "KeyError": {
+ args: args{
+ condition: strPtr("badkey"),
+ req: &fnv1beta1.RunFunctionRequest{
+ Input: resource.MustStructObject(&v1beta1.Resources{
+ Resources: []v1beta1.ComposedTemplate{
+ {
+ Name: "cool-resource",
+ Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
+ },
+ },
+ }),
+ Observed: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(oxr),
+ },
+ },
+ Desired: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(dxr),
+ },
+ },
+ },
+ },
+ want: want{
+ ret: false,
+ err: errors.New("CEL TypeCheck error: ERROR: :1:1: undeclared reference to 'badkey' (in container '')\n | badkey\n | ^"),
+ },
+ },
+ "TrueDesired": {
+ args: args{
+ condition: strPtr("desired.composite.resource.spec.env == \"dev\" "),
+ req: &fnv1beta1.RunFunctionRequest{
+ Input: resource.MustStructObject(&v1beta1.Resources{
+ Resources: []v1beta1.ComposedTemplate{
+ {
+ Name: "cool-resource",
+ Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
+ },
+ },
+ }),
+ Observed: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(oxr),
+ },
+ },
+ Desired: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(dxr),
+ },
+ },
+ },
+ },
+ want: want{
+ ret: true,
+ err: nil,
+ },
+ },
+ "TrueDesiredBool": {
+ args: args{
+ condition: strPtr("desired.composite.resource.spec.render == true"),
+ req: &fnv1beta1.RunFunctionRequest{
+ Input: resource.MustStructObject(&v1beta1.Resources{
+ Resources: []v1beta1.ComposedTemplate{
+ {
+ Name: "cool-resource",
+ Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
+ },
+ },
+ }),
+ Observed: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(oxr),
+ },
+ },
+ Desired: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(dxr),
+ },
+ },
+ },
+ },
+ want: want{
+ ret: true,
+ err: nil,
+ },
+ },
+ "FalseDesiredBool": {
+ args: args{
+ condition: strPtr("desired.composite.resource.spec.render == false"),
+ req: &fnv1beta1.RunFunctionRequest{
+ Input: resource.MustStructObject(&v1beta1.Resources{
+ Resources: []v1beta1.ComposedTemplate{
+ {
+ Name: "cool-resource",
+ Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
+ },
+ },
+ }),
+ Observed: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(oxr),
+ },
+ },
+ Desired: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(dxr),
+ },
+ },
+ },
+ },
+ want: want{
+ ret: false,
+ err: nil,
+ },
+ },
+ "FalseObservedBool": {
+ args: args{
+ condition: strPtr("observed.composite.resource.status.ready == true"),
+ req: &fnv1beta1.RunFunctionRequest{
+ Input: resource.MustStructObject(&v1beta1.Resources{
+ Resources: []v1beta1.ComposedTemplate{
+ {
+ Name: "cool-resource",
+ Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
+ },
+ },
+ }),
+ Observed: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(oxr),
+ },
+ },
+ Desired: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(dxr),
+ },
+ },
+ },
+ },
+ want: want{
+ ret: false,
+ err: nil,
+ },
+ },
+ "FalseLengthResources": {
+ args: args{
+ condition: strPtr("size(desired.resources) == 0"),
+ req: &fnv1beta1.RunFunctionRequest{
+ Input: resource.MustStructObject(&v1beta1.Resources{
+ Resources: []v1beta1.ComposedTemplate{
+ {
+ Name: "cool-resource",
+ Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
+ },
+ },
+ }),
+ Observed: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(oxr),
+ },
+ },
+ Desired: &fnv1beta1.State{
+ Resources: map[string]*fnv1beta1.Resource{
+ "test": {
+ Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","metadata":{"namespace":"default","name":"cool-42"}}`),
+ },
+ },
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(dxr),
+ },
+ },
+ },
+ },
+ want: want{
+ ret: false,
+ err: nil,
+ },
+ },
+ "TrueResourceMapKeyExists": {
+ args: args{
+ condition: strPtr("\"test-resource\" in desired.resources"),
+ req: &fnv1beta1.RunFunctionRequest{
+ Input: resource.MustStructObject(&v1beta1.Resources{
+ Resources: []v1beta1.ComposedTemplate{
+ {
+ Name: "cool-resource",
+ Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
+ },
+ },
+ }),
+ Observed: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(oxr),
+ },
+ },
+ Desired: &fnv1beta1.State{
+ Resources: map[string]*fnv1beta1.Resource{
+ "test-resource": {
+ Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","metadata":{"namespace":"default","name":"cool-42"}}`),
+ },
+ },
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(dxr),
+ },
+ },
+ },
+ },
+ want: want{
+ ret: true,
+ err: nil,
+ },
+ },
+ "FalseResourceMapKeyExists": {
+ args: args{
+ condition: strPtr("\"bad-resource\" in desired.resources"),
+ req: &fnv1beta1.RunFunctionRequest{
+ Input: resource.MustStructObject(&v1beta1.Resources{
+ Resources: []v1beta1.ComposedTemplate{
+ {
+ Name: "cool-resource",
+ Base: &runtime.RawExtension{Raw: []byte(`{"apiVersion":"example.org/v1","kind":"CD"}`)},
+ },
+ },
+ }),
+ Observed: &fnv1beta1.State{
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(oxr),
+ },
+ },
+ Desired: &fnv1beta1.State{
+ Resources: map[string]*fnv1beta1.Resource{
+ "test-resource": {
+ Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","metadata":{"namespace":"default","name":"cool-42"}}`),
+ },
+ },
+ Composite: &fnv1beta1.Resource{
+ Resource: resource.MustStructJSON(dxr),
+ },
+ },
+ },
+ },
+ want: want{
+ ret: false,
+ err: nil,
+ },
+ },
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ ret, err := EvaluateCondition(tc.args.condition, tc.args.req)
+
+ if diff := cmp.Diff(tc.want.ret, ret); diff != "" {
+ t.Errorf("%s\nEvaluateCondition(...): -want ret, +got ret:\n%s", tc.reason, diff)
+ }
+
+ if tc.want.err != nil || err != nil {
+ if !strings.HasPrefix(err.Error(), tc.want.err.Error()) {
+ t.Errorf("\nEvaluateCondition(...): -want err, +got err:\n-want (error starts with): %s\n-got: %s", tc.want.err.Error(), err)
+ }
+ }
+
+ })
+ }
+}
+
+func strPtr(str string) *string {
+ return &str
+}
diff --git a/connection.go b/connection.go
index abe77bc..fddaf85 100644
--- a/connection.go
+++ b/connection.go
@@ -9,7 +9,7 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
"github.com/crossplane/crossplane-runtime/pkg/resource"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
// ConnectionDetailsExtractor extracts the connection details of a resource.
diff --git a/connection_test.go b/connection_test.go
index 4ae6f63..4fa2fd6 100644
--- a/connection_test.go
+++ b/connection_test.go
@@ -14,7 +14,7 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
func TestExtractConnectionDetails(t *testing.T) {
diff --git a/example/composition.yaml b/example/composition.yaml
deleted file mode 100644
index f2b6d4b..0000000
--- a/example/composition.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-apiVersion: apiextensions.crossplane.io/v1
-kind: Composition
-metadata:
- name: function-patch-and-transform
-spec:
- compositeTypeRef:
- apiVersion: example.crossplane.io/v1
- kind: XR
- mode: Pipeline
- pipeline:
- - step: patch-and-transform
- functionRef:
- name: function-patch-and-transform
- input:
- apiVersion: pt.fn.crossplane.io/v1beta1
- kind: Resources
- resources:
- - name: bucket
- base:
- apiVersion: s3.aws.upbound.io/v1beta1
- kind: Bucket
- patches:
- - type: FromCompositeFieldPath
- fromFieldPath: "spec.location"
- toFieldPath: "spec.forProvider.region"
- transforms:
- - type: map
- map:
- EU: "eu-north-1"
- US: "us-east-2"
\ No newline at end of file
diff --git a/example/functions.yaml b/example/functions.yaml
deleted file mode 100644
index d8edb63..0000000
--- a/example/functions.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
----
-apiVersion: pkg.crossplane.io/v1beta1
-kind: Function
-metadata:
- name: function-patch-and-transform
-spec:
- package: xpkg.upbound.io/crossplane-contrib/function-patch-and-transform:v0.1.4
diff --git a/example/xr.yaml b/example/xr.yaml
deleted file mode 100644
index 0ca5a77..0000000
--- a/example/xr.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-# Replace this with your XR!
-apiVersion: example.crossplane.io/v1
-kind: XR
-metadata:
- name: example-xr
-spec:
- location: US
diff --git a/examples/conditional-rendering/README.md b/examples/conditional-rendering/README.md
new file mode 100644
index 0000000..125f1b3
--- /dev/null
+++ b/examples/conditional-rendering/README.md
@@ -0,0 +1,99 @@
+# Conditional Execution of Patch-and-Transform
+
+This is an experimental fork using [The Common Expression Language (CEL)](https://github.com/google/cel-spec) to conditionally running the patch and transform function.
+
+For example,if you have an XR with the following manifest:
+
+```yaml
+apiVersion: nop.example.org/v1alpha1
+kind: XNopResource
+metadata:
+ name: test-resource
+spec:
+ env: dev
+ render: false
+```
+
+and set a Condition of `observed.composite.resource.spec.render == true`, the function
+will not run.
+
+## Running this example
+
+- Install Crossplane version 1.14 or newer. See
+- Install the nop provider in `kubectl apply -f provider.yaml`
+- Install the XRD & Composition in `kubectl apply -f definition.yaml -f composition.yaml`
+- Install the Function `kubectl apply -f function.yaml`
+
+Finally install the xr: `kubectl apply -f xr`
+
+## Validate the Install
+
+Validate the Composite is ready:
+
+```shell
+$ kubectl get composite
+NAME SYNCED READY COMPOSITION AGE
+test-resource True True xnopresources.nop.example.org 2m9s
+```
+
+Validate the resource has been generated.
+
+```shell
+$ kubectl get nopresource
+NAME READY SYNCED AGE
+test-resource-mpw5s True True 27m
+```
+
+## Testing Conditions
+
+The `RunFunctionRequest` passed to the function looks similar to the following manifest.
+This can be used in the [CEL Playground](https://playcel.undistro.io) to test
+various conditions.
+
+Note that the function is passed an empty `desired{}` state as per design.
+
+```yaml
+---
+observed:
+ composite:
+ resource:
+ apiVersion: nop.example.org/v1alpha1
+ kind: XNopResource
+ metadata:
+ name: test-resource
+ spec:
+ env: dev
+ render: true
+desired: {}
+input:
+ apiVersion: pt.fn.crossplane.io/v1beta1
+ condition:
+ expression: observed.composite.resource.spec.env == "dev" && observed.composite.resource.spec.render
+ == true
+ kind: Resources
+ resources:
+ - base:
+ apiVersion: nop.crossplane.io/v1alpha1
+ kind: NopResource
+ spec:
+ forProvider:
+ conditionAfter:
+ - conditionStatus: 'True'
+ conditionType: Ready
+ time: 5s
+ connectionDetails:
+ - name: username
+ value: fakeuser
+ - name: password
+ value: verysecurepassword
+ - name: endpoint
+ value: 127.0.0.1
+ fields:
+ arrayField:
+ - stringField: array
+ integerField: 42
+ objectField:
+ stringField: object
+ stringField: string
+ name: test-resource
+```
diff --git a/examples/conditional-rendering/composition.yaml b/examples/conditional-rendering/composition.yaml
new file mode 100644
index 0000000..18e9c88
--- /dev/null
+++ b/examples/conditional-rendering/composition.yaml
@@ -0,0 +1,43 @@
+---
+apiVersion: apiextensions.crossplane.io/v1
+kind: Composition
+metadata:
+ name: xnopresources.nop.example.org
+spec:
+ compositeTypeRef:
+ apiVersion: nop.example.org/v1alpha1
+ kind: XNopResource
+ mode: Pipeline
+ pipeline:
+ - step: conditional-patch-and-transform
+ functionRef:
+ name: function-conditional-patch-and-transform
+ input:
+ apiVersion: conditional-pt.fn.crossplane.io/v1beta1
+ kind: Resources
+ condition: observed.composite.resource.spec.env == "dev" && observed.composite.resource.spec.render == true
+ resources:
+ - name: test-resource
+ base:
+ apiVersion: nop.crossplane.io/v1alpha1
+ kind: NopResource
+ spec:
+ forProvider:
+ fields:
+ integerField: 42
+ stringField: "string"
+ objectField:
+ stringField: "object"
+ arrayField:
+ - stringField: "array"
+ conditionAfter:
+ - time: 5s
+ conditionType: Ready
+ conditionStatus: "True"
+ connectionDetails:
+ - name: username
+ value: fakeuser
+ - name: password
+ value: verysecurepassword
+ - name: endpoint
+ value: 127.0.0.1
diff --git a/examples/conditional-rendering/definition.yaml b/examples/conditional-rendering/definition.yaml
new file mode 100644
index 0000000..6eb29c4
--- /dev/null
+++ b/examples/conditional-rendering/definition.yaml
@@ -0,0 +1,24 @@
+apiVersion: apiextensions.crossplane.io/v1
+kind: CompositeResourceDefinition
+metadata:
+ name: xnopresources.nop.example.org
+spec:
+ group: nop.example.org
+ names:
+ kind: XNopResource
+ plural: xnopresources
+ versions:
+ - name: v1alpha1
+ referenceable: true
+ served: true
+ schema:
+ openAPIV3Schema:
+ type: object
+ properties:
+ spec:
+ type: object
+ properties:
+ env:
+ type: string
+ render:
+ type: boolean
\ No newline at end of file
diff --git a/examples/conditional-rendering/functions.yaml b/examples/conditional-rendering/functions.yaml
new file mode 100644
index 0000000..121ad68
--- /dev/null
+++ b/examples/conditional-rendering/functions.yaml
@@ -0,0 +1,7 @@
+apiVersion: pkg.crossplane.io/v1beta1
+kind: Function
+metadata:
+ name: function-conditional-patch-and-transform
+spec:
+ package: xpkg.upbound.io/upboundcare/function-conditional-patch-and-transform:v0.4.0
+ packagePullPolicy: Always
diff --git a/examples/conditional-rendering/provider.yaml b/examples/conditional-rendering/provider.yaml
new file mode 100644
index 0000000..1e28f83
--- /dev/null
+++ b/examples/conditional-rendering/provider.yaml
@@ -0,0 +1,6 @@
+apiVersion: pkg.crossplane.io/v1
+kind: Provider
+metadata:
+ name: provider-nop
+spec:
+ package: xpkg.upbound.io/crossplane-contrib/provider-nop:v0.2.0
\ No newline at end of file
diff --git a/examples/conditional-rendering/xr.yaml b/examples/conditional-rendering/xr.yaml
new file mode 100644
index 0000000..a830c36
--- /dev/null
+++ b/examples/conditional-rendering/xr.yaml
@@ -0,0 +1,7 @@
+apiVersion: nop.example.org/v1alpha1
+kind: XNopResource
+metadata:
+ name: test-resource
+spec:
+ env: dev
+ render: true
\ No newline at end of file
diff --git a/examples/conditional-resources/README.md b/examples/conditional-resources/README.md
new file mode 100644
index 0000000..00afb2e
--- /dev/null
+++ b/examples/conditional-resources/README.md
@@ -0,0 +1,41 @@
+# Conditional Rendering of Individual Resources
+
+In this example, we show how each resource in a pipeline step can
+be individually rendered based on a condition. We will have a blue and a
+green deployment that the user can activate:
+
+```yaml
+apiVersion: nop.example.org/v1alpha1
+kind: XNopConditional
+metadata:
+ name: test-resource
+spec:
+ env: dev
+ render: true
+ deployment:
+ blue: true
+ green: false
+```
+
+In our Composition both the `blue-resource` and the `green-resource` have a
+`condition` that determines if they will be run.
+
+```yaml
+ resources:
+ - name: blue-resource
+ condition: observed.composite.resource.spec.deployment.blue == true
+ base:
+ apiVersion: nop.crossplane.io/v1alpha1
+ kind: NopResource
+ spec:
+ forProvider:
+```
+
+## Running this example
+
+- Install Crossplane version 1.14 or newer. See
+- Install the nop provider in `kubectl apply -f provider.yaml`
+- Install the XRD & Composition in `kubectl apply -f definition.yaml -f composition.yaml`
+- Install the Function `kubectl apply -f functions.yaml`
+
+Finally install the xr: `kubectl apply -f xr`
diff --git a/examples/conditional-resources/composition.yaml b/examples/conditional-resources/composition.yaml
new file mode 100644
index 0000000..7fc75ee
--- /dev/null
+++ b/examples/conditional-resources/composition.yaml
@@ -0,0 +1,70 @@
+---
+apiVersion: apiextensions.crossplane.io/v1
+kind: Composition
+metadata:
+ name: xnopconditionals.nop.example.org
+spec:
+ compositeTypeRef:
+ apiVersion: nop.example.org/v1alpha1
+ kind: XNopConditional
+ mode: Pipeline
+ pipeline:
+ - step: conditional-patch-and-transform
+ functionRef:
+ name: function-conditional-patch-and-transform
+ input:
+ apiVersion: conditional-pt.fn.crossplane.io/v1beta1
+ kind: Resources
+ condition: observed.composite.resource.spec.render == true
+ resources:
+ - name: blue-resource
+ condition: observed.composite.resource.spec.deployment.blue == true
+ base:
+ apiVersion: nop.crossplane.io/v1alpha1
+ kind: NopResource
+ spec:
+ forProvider:
+ fields:
+ integerField: 42
+ stringField: "blue"
+ objectField:
+ stringField: "blueObject"
+ arrayField:
+ - stringField: "blueArray"
+ conditionAfter:
+ - time: 5s
+ conditionType: Ready
+ conditionStatus: "True"
+ connectionDetails:
+ - name: username
+ value: fakeuser
+ - name: password
+ value: verysecurepassword
+ - name: endpoint
+ value: 127.0.0.1
+ - name: green-resource
+ condition: observed.composite.resource.spec.deployment.green == true
+ base:
+ apiVersion: nop.crossplane.io/v1alpha1
+ kind: NopResource
+ spec:
+ forProvider:
+ fields:
+ integerField: 42
+ stringField: "green"
+ objectField:
+ stringField: "greenObject"
+ arrayField:
+ - stringField: "greenArray"
+ conditionAfter:
+ - time: 5s
+ conditionType: Ready
+ conditionStatus: "True"
+ connectionDetails:
+ - name: username
+ value: fakeuser
+ - name: password
+ value: verysecurepassword
+ - name: endpoint
+ value: 127.0.0.1
+
diff --git a/examples/conditional-resources/definition.yaml b/examples/conditional-resources/definition.yaml
new file mode 100644
index 0000000..e88d9a6
--- /dev/null
+++ b/examples/conditional-resources/definition.yaml
@@ -0,0 +1,33 @@
+apiVersion: apiextensions.crossplane.io/v1
+kind: CompositeResourceDefinition
+metadata:
+ name: xnopconditionals.nop.example.org
+spec:
+ group: nop.example.org
+ names:
+ kind: XNopConditional
+ plural: xnopconditionals
+ versions:
+ - name: v1alpha1
+ referenceable: true
+ served: true
+ schema:
+ openAPIV3Schema:
+ type: object
+ properties:
+ spec:
+ type: object
+ properties:
+ env:
+ type: string
+ render:
+ type: boolean
+ deployment:
+ type: object
+ properties:
+ blue:
+ description: Activate the blue resource
+ type: boolean
+ green:
+ description: Activate the green resource
+ type: boolean
\ No newline at end of file
diff --git a/examples/conditional-resources/functions.yaml b/examples/conditional-resources/functions.yaml
new file mode 100644
index 0000000..121ad68
--- /dev/null
+++ b/examples/conditional-resources/functions.yaml
@@ -0,0 +1,7 @@
+apiVersion: pkg.crossplane.io/v1beta1
+kind: Function
+metadata:
+ name: function-conditional-patch-and-transform
+spec:
+ package: xpkg.upbound.io/upboundcare/function-conditional-patch-and-transform:v0.4.0
+ packagePullPolicy: Always
diff --git a/examples/conditional-resources/provider.yaml b/examples/conditional-resources/provider.yaml
new file mode 100644
index 0000000..1e28f83
--- /dev/null
+++ b/examples/conditional-resources/provider.yaml
@@ -0,0 +1,6 @@
+apiVersion: pkg.crossplane.io/v1
+kind: Provider
+metadata:
+ name: provider-nop
+spec:
+ package: xpkg.upbound.io/crossplane-contrib/provider-nop:v0.2.0
\ No newline at end of file
diff --git a/examples/conditional-resources/xr.yaml b/examples/conditional-resources/xr.yaml
new file mode 100644
index 0000000..360d6a2
--- /dev/null
+++ b/examples/conditional-resources/xr.yaml
@@ -0,0 +1,10 @@
+apiVersion: nop.example.org/v1alpha1
+kind: XNopConditional
+metadata:
+ name: test-resource
+spec:
+ env: dev
+ render: true
+ deployment:
+ blue: true
+ green: false
\ No newline at end of file
diff --git a/fn.go b/fn.go
index 6dc9e4b..2dc5ae1 100644
--- a/fn.go
+++ b/fn.go
@@ -19,9 +19,11 @@ import (
"github.com/crossplane/function-sdk-go/resource/composed"
"github.com/crossplane/function-sdk-go/response"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
+const conditionError = "Condition error"
+
// Function performs patch-and-transform style Composition.
type Function struct {
fnv1beta1.UnimplementedFunctionRunnerServiceServer
@@ -45,6 +47,21 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1beta1.RunFunctionRe
return rsp, nil
}
+ // Evaluate any Conditions using the values from the Observed XR
+ if input.Condition != nil {
+ // Evaluate the condition to see if we should run
+ run, err := EvaluateCondition(input.Condition, req)
+ if err != nil {
+ response.Fatal(rsp, errors.Wrap(err, conditionError))
+ return rsp, nil
+ }
+ if !run {
+ log.Debug("Condition evaluated to false. Skipping run.")
+ return rsp, nil
+ }
+ log.Debug("Condition evaluated to true.")
+ }
+
// Our input is an opaque object nested in a Composition, so unfortunately
// it won't handle validation for us.
if err := ValidateResources(input); err != nil {
@@ -140,6 +157,21 @@ func (f *Function) RunFunction(ctx context.Context, req *fnv1beta1.RunFunctionRe
dcd := &resource.DesiredComposed{Resource: composed.New()}
+ if t.Condition != nil {
+ // Evaluate the condition to see if we should skip this template.
+ run, err := EvaluateCondition(t.Condition, req)
+ if err != nil {
+ log.Info(err.Error())
+ response.Fatal(rsp, errors.Wrap(err, conditionError))
+ return rsp, nil
+ }
+ if !run {
+ log.Debug("Condition evaluated to false. Skipping template.")
+ continue
+ }
+ log.Debug("Condition evaluated to true.")
+ }
+
// If we have a base template, render it into our desired resource. If a
// previous Function produced a desired resource with this name we'll
// overwrite it. If we don't have a base template we'll try to patch to
diff --git a/fn_test.go b/fn_test.go
index 0f828b4..875f1ef 100644
--- a/fn_test.go
+++ b/fn_test.go
@@ -22,7 +22,7 @@ import (
"github.com/crossplane/function-sdk-go/resource"
"github.com/crossplane/function-sdk-go/response"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
func TestRunFunction(t *testing.T) {
diff --git a/go.mod b/go.mod
index 928887b..529a3ef 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/crossplane-contrib/function-patch-and-transform
+module github.com/upboundcare/function-conditional-patch-and-transform
go 1.21
@@ -8,6 +8,7 @@ require (
github.com/alecthomas/kong v0.8.1
github.com/crossplane/crossplane-runtime v1.15.1
github.com/crossplane/function-sdk-go v0.2.0
+ github.com/google/cel-go v0.19.0
github.com/google/go-cmp v0.6.0
github.com/pkg/errors v0.9.1
google.golang.org/protobuf v1.32.0
@@ -20,6 +21,7 @@ require (
require (
dario.cat/mergo v1.0.0 // indirect
+ github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -58,6 +60,7 @@ require (
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
+ github.com/stoewer/go-strcase v1.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
@@ -71,6 +74,7 @@ require (
golang.org/x/tools v0.17.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
google.golang.org/grpc v1.61.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
diff --git a/go.sum b/go.sum
index ce1c171..614244e 100644
--- a/go.sum
+++ b/go.sum
@@ -12,6 +12,8 @@ github.com/antchfx/htmlquery v1.2.4 h1:qLteofCMe/KGovBI6SQgmou2QNyedFUW+pE+BpeZ4
github.com/antchfx/htmlquery v1.2.4/go.mod h1:2xO6iu3EVWs7R2JYqBbp8YzG50gj/ofqs5/0VZoDZLc=
github.com/antchfx/xpath v1.2.0 h1:mbwv7co+x0RwgeGAOHdrKy89GvHaGvxxBtPK0uF9Zr8=
github.com/antchfx/xpath v1.2.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
+github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
+github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -66,6 +68,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/cel-go v0.19.0 h1:vVgaZoHPBDd1lXCYGQOh5A06L4EtuIfmqQ/qnSXSKiU=
+github.com/google/cel-go v0.19.0/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -179,6 +183,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -275,6 +281,8 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac h1:OZkkudMUu9LVQMCoRUbI/1p5VCo9BOrlvkqMvWtqa6s=
+google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
diff --git a/input/generate.go b/input/generate.go
index 551cc2a..187e557 100644
--- a/input/generate.go
+++ b/input/generate.go
@@ -4,6 +4,7 @@
// NOTE(negz): See the below link for details on what is happening here.
// https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module
+//go:generate rm -rf package/input
//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen paths=./v1beta1 object crd:crdVersions=v1 output:artifacts:config=../package/input
package input
diff --git a/input/v1beta1/conditions.go b/input/v1beta1/conditions.go
new file mode 100644
index 0000000..47038c5
--- /dev/null
+++ b/input/v1beta1/conditions.go
@@ -0,0 +1,6 @@
+package v1beta1
+
+// Condition defines the condition for rendering.
+// Conditions are defined using the Common Expression Language
+// For more information refer to https://github.com/google/cel-spec
+type Condition *string
diff --git a/input/v1beta1/resources.go b/input/v1beta1/resources.go
index c86f447..a06c61d 100644
--- a/input/v1beta1/resources.go
+++ b/input/v1beta1/resources.go
@@ -1,6 +1,6 @@
// Package v1beta1 contains the input type for the P&T Composition Function.
// +kubebuilder:object:generate=true
-// +groupName=pt.fn.crossplane.io
+// +groupName=conditional-pt.fn.crossplane.io
// +versionName=v1beta1
package v1beta1
@@ -20,6 +20,9 @@ type Resources struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
+ // Condition defines a CEL condition whether this function will render
+ Condition Condition `json:"condition,omitempty"`
+
// PatchSets define a named set of patches that may be included by any
// resource. PatchSets cannot themselves refer to other PatchSets.
// +optional
diff --git a/input/v1beta1/resources_common.go b/input/v1beta1/resources_common.go
index c1d750b..d5d4b5e 100644
--- a/input/v1beta1/resources_common.go
+++ b/input/v1beta1/resources_common.go
@@ -59,6 +59,9 @@ type ComposedTemplate struct {
// +optional
Base *runtime.RawExtension `json:"base,omitempty"`
+ // Condition defines a CEL condition whether this managed resource will render
+ Condition Condition `json:"condition,omitempty"`
+
// Patches to and from the composed resource.
// +optional
Patches []ComposedPatch `json:"patches,omitempty"`
diff --git a/input/v1beta1/zz_generated.deepcopy.go b/input/v1beta1/zz_generated.deepcopy.go
index bd97bd7..99aba00 100644
--- a/input/v1beta1/zz_generated.deepcopy.go
+++ b/input/v1beta1/zz_generated.deepcopy.go
@@ -78,6 +78,11 @@ func (in *ComposedTemplate) DeepCopyInto(out *ComposedTemplate) {
*out = new(runtime.RawExtension)
(*in).DeepCopyInto(*out)
}
+ if in.Condition != nil {
+ in, out := &in.Condition, &out.Condition
+ *out = new(string)
+ **out = **in
+ }
if in.Patches != nil {
in, out := &in.Patches, &out.Patches
*out = make([]ComposedPatch, len(*in))
@@ -460,6 +465,11 @@ func (in *Resources) DeepCopyInto(out *Resources) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ if in.Condition != nil {
+ in, out := &in.Condition, &out.Condition
+ *out = new(string)
+ **out = **in
+ }
if in.PatchSets != nil {
in, out := &in.PatchSets, &out.PatchSets
*out = make([]PatchSet, len(*in))
diff --git a/package/crossplane.yaml b/package/crossplane.yaml
index 866d2d3..5df4b7a 100644
--- a/package/crossplane.yaml
+++ b/package/crossplane.yaml
@@ -2,17 +2,17 @@
apiVersion: meta.pkg.crossplane.io/v1beta1
kind: Function
metadata:
- name: function-patch-and-transform
+ name: function-conditional-patch-and-transform
annotations:
meta.crossplane.io/maintainer: Crossplane Maintainers
- meta.crossplane.io/source: github.com/crossplane-contrib/function-patch-and-transform
+ meta.crossplane.io/source: github.com/upboundcare/function-conditional-patch-and-transform
meta.crossplane.io/license: Apache-2.0
- meta.crossplane.io/description: A patch & transform composition function
+ meta.crossplane.io/description: A patch & transform composition function that supports conditionals
meta.crossplane.io/readme: |
- This composition function does everything Crossplane's built-in
- [patch & transform](https://docs.crossplane.io/latest/concepts/patch-and-transform/)
- composition does. Instead of specifying `spec.resources` in your
- Composition, you can use this function. See the
- [README](https://github.com/crossplane-contrib/function-patch-and-transform)
+ This composition function is a fork of the upstream [function-patch-and-transform](https://github.com/crossplane-contrib/function-patch-and-transform)
+ that adds support for Conditional invocation of the function and the rendering
+ of individual resources.
+ See the
+ [README](https://github.com/upboundcare/function-conditional-patch-and-transform)
for examples and documentation.
spec: {}
diff --git a/package/function-conditional-patch-and-transform-9b3ddecdb8ff.xpkg b/package/function-conditional-patch-and-transform-9b3ddecdb8ff.xpkg
new file mode 100644
index 0000000..0c805bf
Binary files /dev/null and b/package/function-conditional-patch-and-transform-9b3ddecdb8ff.xpkg differ
diff --git a/package/input/pt.fn.crossplane.io_resources.yaml b/package/input/conditional-pt.fn.crossplane.io_resources.yaml
similarity index 99%
rename from package/input/pt.fn.crossplane.io_resources.yaml
rename to package/input/conditional-pt.fn.crossplane.io_resources.yaml
index 9734fdd..b5d234c 100644
--- a/package/input/pt.fn.crossplane.io_resources.yaml
+++ b/package/input/conditional-pt.fn.crossplane.io_resources.yaml
@@ -4,9 +4,9 @@ kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.14.0
- name: resources.pt.fn.crossplane.io
+ name: resources.conditional-pt.fn.crossplane.io
spec:
- group: pt.fn.crossplane.io
+ group: conditional-pt.fn.crossplane.io
names:
categories:
- crossplane
@@ -28,6 +28,10 @@ spec:
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
+ condition:
+ description: Condition defines a CEL condition whether this function will
+ render
+ type: string
environment:
description: |-
Environment represents the Composition environment.
@@ -736,6 +740,10 @@ spec:
type: object
x-kubernetes-embedded-resource: true
x-kubernetes-preserve-unknown-fields: true
+ condition:
+ description: Condition defines a CEL condition whether this managed
+ resource will render
+ type: string
connectionDetails:
description: |-
ConnectionDetails lists the propagation secret keys from this composed
diff --git a/patches.go b/patches.go
index 5e06603..a8458a3 100644
--- a/patches.go
+++ b/patches.go
@@ -15,7 +15,7 @@ import (
"github.com/crossplane/function-sdk-go/resource/composed"
"github.com/crossplane/function-sdk-go/resource/composite"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
const (
diff --git a/patches_test.go b/patches_test.go
index bc26cbe..1385ee0 100644
--- a/patches_test.go
+++ b/patches_test.go
@@ -17,7 +17,7 @@ import (
"github.com/crossplane/function-sdk-go/resource/composed"
"github.com/crossplane/function-sdk-go/resource/composite"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
func TestApplyFromFieldPathPatch(t *testing.T) {
diff --git a/ready.go b/ready.go
index bb0dc8a..59de7ab 100644
--- a/ready.go
+++ b/ready.go
@@ -8,7 +8,7 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/pkg/resource"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
// Error strings
diff --git a/ready_test.go b/ready_test.go
index 5d3bad8..a7e366a 100644
--- a/ready_test.go
+++ b/ready_test.go
@@ -14,7 +14,7 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
"github.com/crossplane/crossplane-runtime/pkg/test"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
var _ ReadinessChecker = ReadinessCheckerFn(IsReady)
diff --git a/transforms.go b/transforms.go
index c1a20c5..2a5ec4b 100644
--- a/transforms.go
+++ b/transforms.go
@@ -19,7 +19,7 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/errors"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
const (
diff --git a/transforms_test.go b/transforms_test.go
index be328e1..0ef111e 100644
--- a/transforms_test.go
+++ b/transforms_test.go
@@ -14,7 +14,7 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/test"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
func TestMapResolve(t *testing.T) {
diff --git a/validate.go b/validate.go
index c815a5c..f0bbb77 100644
--- a/validate.go
+++ b/validate.go
@@ -6,7 +6,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
// WrapFieldError wraps the given field.Error adding the given field.Path as root of the Field.
diff --git a/validate_test.go b/validate_test.go
index 5b46a07..f5078bd 100644
--- a/validate_test.go
+++ b/validate_test.go
@@ -9,7 +9,7 @@ import (
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
- "github.com/crossplane-contrib/function-patch-and-transform/input/v1beta1"
+ "github.com/upboundcare/function-conditional-patch-and-transform/input/v1beta1"
)
func TestValidateReadinessCheck(t *testing.T) {