From ab61d5a233012ceab2d26813ab142cbf7f6984db Mon Sep 17 00:00:00 2001 From: "Patrick J. McNerthney" Date: Fri, 16 Jan 2026 16:15:19 -1000 Subject: [PATCH] backport function-pythonic content to v2.1/v2.0 Signed-off-by: Patrick J. McNerthney --- .../get-started-with-composition.md | 78 ++++++++++++ .../guides/connection-details-composition.md | 115 +++++++++++++++++- .../learn/community-extension-projects.md | 1 + .../get-started-with-composition.md | 78 ++++++++++++ .../guides/connection-details-composition.md | 115 +++++++++++++++++- .../learn/community-extension-projects.md | 1 + 6 files changed, 384 insertions(+), 4 deletions(-) diff --git a/content/v2.0/get-started/get-started-with-composition.md b/content/v2.0/get-started/get-started-with-composition.md index 44bcc7cc4..ad93e45f8 100644 --- a/content/v2.0/get-started/get-started-with-composition.md +++ b/content/v2.0/get-started/get-started-with-composition.md @@ -312,6 +312,38 @@ crossplane-contrib-function-kcl True True xpkg.crossplane.io/cross ``` {{< /tab >}} +{{< tab "Pythonic" >}} +[Pythonic](https://github.com/crossplane-contrib/function-pythonic?tab=readme-ov-file#function-pythonic) +is an excellent choice for compositions with dynamic logic. The full flexibility and power of python is +available using a set of python classes with an elegant and terse syntax that hides the details of the low level +Crossplane function APIs. + +Create this composition function to install Pythonic support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-pythonic +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-pythonic:v0.3.0 +``` + +Save the function as `fn.yaml` and apply it: + +```shell +kubectl apply -f fn.yaml +``` + +Check that Crossplane installed the function: + +```shell {copy-lines="1"} +kubectl get -f fn.yaml +NAME INSTALLED HEALTHY PACKAGE AGE +function-pythonic True True xpkg.crossplane.io/crossplane-contrib/function-pythonic:v0.3.0 1m +``` +{{< /tab >}} + {{}} ### Configure the composition @@ -646,6 +678,52 @@ spec: ``` {{< /tab >}} +{{< tab "Pythonic" >}} +Create this composition to use Pythonic to configure Crossplane: + +```yaml +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: app-pythonic +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: App + mode: Pipeline + pipeline: + - step: create-deployment-and-service + functionRef: + name: function-pythonic + input: + apiVersion: pythonic.fn.crossplane.io/v1alpha1 + kind: Composite + composite: | + class Composite(BaseComposite): + def compose(self): + labels = {'example.crossplane.io/app': self.metadata.name} + + d = self.resources.deployment('apps/v1', 'Deployment') + d.metadata.labels = labels + d.spec.replicas = 2 + d.spec.selector.matchLabels = labels + d.spec.template.metadata.labels = labels + d.spec.template.spec.containers[0].name = 'app' + d.spec.template.spec.containers[0].image = self.spec.image + d.spec.template.spec.containers[0].ports[0].containerPort = 80 + + s = self.resources.service('v1', 'Service') + s.metadata.labels = labels + s.spec.selector = labels + s.spec.ports[0].protocol = 'TCP' + s.spec.ports[0].port = 8080 + s.spec.ports[0].targetPort = 80 + + self.status.replicas = d.status.availableReplicas + self.status.address = s.observed.spec.clusterIP +``` +{{< /tab >}} + {{}} Save the composition as `composition.yaml` and apply it: diff --git a/content/v2.0/guides/connection-details-composition.md b/content/v2.0/guides/connection-details-composition.md index c01fd2ff9..d4eaca673 100644 --- a/content/v2.0/guides/connection-details-composition.md +++ b/content/v2.0/guides/connection-details-composition.md @@ -283,6 +283,34 @@ kubectl get -f fn.yaml NAME INSTALLED HEALTHY PACKAGE AGE function-kcl True True xpkg.crossplane.io/crossplane-contrib/function-kcl:v0.11.6 6s ``` +{{< /tab>}} + +{{< tab "Pythonic" >}} + +Create this composition function to install [Pythonic](https://github.com/crossplane-contrib/function-pythonic?tab=readme-ov-file#function-pythonic) support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-pythonic +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-pythonic:v0.3.0 +``` + +Save the function as `fn.yaml` and apply it: + +```shell +kubectl apply -f fn.yaml +``` + +Check that Crossplane installed the function: + +```shell {copy-lines="1"} +kubectl get -f fn.yaml +NAME INSTALLED HEALTHY PACKAGE AGE +function-pythonic True True xpkg.crossplane.io/crossplane-contrib/function-pythonic:v0.3.0 1m +``` {{< /tab >}} {{< /tabs >}} @@ -786,6 +814,67 @@ spec: {{< /tab >}} +{{< tab "Pythonic" >}} + +```yaml {label="comp-pythonic"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-pythonic +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-pythonic + functionRef: + name: function-pythonic + input: + apiVersion: pythonic.fn.crossplane.io/v1alpha1 + kind: Composite + composite: | + class Composite(BaseComposite): + def compose(self): + self.connectionSecret = self.spec.writeConnectionSecretToRef + + user = self.resources.user('iam.aws.m.upbound.io/v1beta1', 'User') + user.spec.forProvider = {} + + for ix in range(2): + key = self.resources[f"access-key-{ix}"]('iam.aws.m.upbound.io/v1beta1', 'AccessKey') + key.spec.forProvider.user = user.status.atProvider.id + key.spec.writeConnectionSecretToRef.name = f"{self.metadata.name}-accesskey-{ix}" + self.connection[f"user-{ix}"] = key.connection.username + self.connection[f"password-{ix}"] = key.connection.password +``` + + + +**How this Composition exposes connection details:** + +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This + tells each AccessKey to write its credentials to an individual Secret. +* Crossplane observes the connection details from each `AccessKey` and makes them + available to the composition when the function runs. +* The Secret reads `AccessKey`'s connection details via + {{}}connection.username{{}} and + {{}}connection.password{{}}. +* The function establishes the connection `Secret` name from the XR + {{}}spec.writeConnectionSecretToRef{{}} + if it exists. +* The function automatically includes a `Secret` object in the XR's composed + resources that represents the XR's aggregated connection details. +* You don't need to create or compose this `Secret` yourself, it's done + automatically for you. +* In `function-pythonic`, connection details base64 encoding and decoding is handled + automatically for you. + + + +{{< /tab >}} + {{< /tabs >}} Save the composition as `composition.yaml` and apply it: @@ -955,6 +1044,28 @@ You don't need to manually compose a `Secret` resource yourself. needed. Use patches to configure these values using data from the XR if needed. +### Automatic aggregation (`function-pythonic`) + +`function-pythonic` automatically observes connection details from +composed resources and creates the aggregated connection secret to +maintain backward compatibility with v1 behavior. + +You don't need to manually compose a `Secret` resource yourself. + +1. **Compose resources**: Create composed resources as usual in your + composition, such as IAM `User` and `AccessKeys`. These resources expose + their connection details in a `Secret`. + +2. **Set `writeConnectionSecretToRef`**: Each composed resource that should have + connection details stored should have their `resource.spec.writeConnectionSecretToRef` set + in the composition. + +3. **Define `connection`**: For each composed resource, assign the connection secret + values wanted to the aggregated secret using `self.connection[key] = resource.connection[key]`. + +4. **Configure the `Secret`**: Set the XR `self.connectionSecret` fields + to override the aggregated secret's default name and namespace. + ## Troubleshooting ### Composite resource's connection details Secret is empty @@ -995,8 +1106,8 @@ For example, `function-python` requires you to convert connection details to base64-encoded strings, while connection details in `function-go-templating` and `function-kcl` are already encoded this way and require no conversion logic. -`function-patch-and-transform` handles encoding when automatically creating the -composed connection secret. +`function-patch-and-transform` and `function-pythonic` handle encoding when automatically +creating the composed connection secret. diff --git a/content/v2.0/learn/community-extension-projects.md b/content/v2.0/learn/community-extension-projects.md index c0882034e..71f6d13ad 100644 --- a/content/v2.0/learn/community-extension-projects.md +++ b/content/v2.0/learn/community-extension-projects.md @@ -93,6 +93,7 @@ use by Crossplane adopters. - [function-kcl](https://github.com/crossplane-contrib/function-kcl) - [function-patch-and-transform](https://github.com/crossplane-contrib/function-patch-and-transform) - [function-python](https://github.com/crossplane-contrib/function-python) +- [function-pythonic](https://github.com/crossplane-contrib/function-pythonic) - [function-sequencer](https://github.com/crossplane-contrib/function-sequencer) - [function-shell](https://github.com/crossplane-contrib/function-shell) - [function-status-transformer](https://github.com/crossplane-contrib/function-status-transformer) diff --git a/content/v2.1/get-started/get-started-with-composition.md b/content/v2.1/get-started/get-started-with-composition.md index 44bcc7cc4..ad93e45f8 100644 --- a/content/v2.1/get-started/get-started-with-composition.md +++ b/content/v2.1/get-started/get-started-with-composition.md @@ -312,6 +312,38 @@ crossplane-contrib-function-kcl True True xpkg.crossplane.io/cross ``` {{< /tab >}} +{{< tab "Pythonic" >}} +[Pythonic](https://github.com/crossplane-contrib/function-pythonic?tab=readme-ov-file#function-pythonic) +is an excellent choice for compositions with dynamic logic. The full flexibility and power of python is +available using a set of python classes with an elegant and terse syntax that hides the details of the low level +Crossplane function APIs. + +Create this composition function to install Pythonic support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-pythonic +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-pythonic:v0.3.0 +``` + +Save the function as `fn.yaml` and apply it: + +```shell +kubectl apply -f fn.yaml +``` + +Check that Crossplane installed the function: + +```shell {copy-lines="1"} +kubectl get -f fn.yaml +NAME INSTALLED HEALTHY PACKAGE AGE +function-pythonic True True xpkg.crossplane.io/crossplane-contrib/function-pythonic:v0.3.0 1m +``` +{{< /tab >}} + {{}} ### Configure the composition @@ -646,6 +678,52 @@ spec: ``` {{< /tab >}} +{{< tab "Pythonic" >}} +Create this composition to use Pythonic to configure Crossplane: + +```yaml +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: app-pythonic +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: App + mode: Pipeline + pipeline: + - step: create-deployment-and-service + functionRef: + name: function-pythonic + input: + apiVersion: pythonic.fn.crossplane.io/v1alpha1 + kind: Composite + composite: | + class Composite(BaseComposite): + def compose(self): + labels = {'example.crossplane.io/app': self.metadata.name} + + d = self.resources.deployment('apps/v1', 'Deployment') + d.metadata.labels = labels + d.spec.replicas = 2 + d.spec.selector.matchLabels = labels + d.spec.template.metadata.labels = labels + d.spec.template.spec.containers[0].name = 'app' + d.spec.template.spec.containers[0].image = self.spec.image + d.spec.template.spec.containers[0].ports[0].containerPort = 80 + + s = self.resources.service('v1', 'Service') + s.metadata.labels = labels + s.spec.selector = labels + s.spec.ports[0].protocol = 'TCP' + s.spec.ports[0].port = 8080 + s.spec.ports[0].targetPort = 80 + + self.status.replicas = d.status.availableReplicas + self.status.address = s.observed.spec.clusterIP +``` +{{< /tab >}} + {{}} Save the composition as `composition.yaml` and apply it: diff --git a/content/v2.1/guides/connection-details-composition.md b/content/v2.1/guides/connection-details-composition.md index c01fd2ff9..d4eaca673 100644 --- a/content/v2.1/guides/connection-details-composition.md +++ b/content/v2.1/guides/connection-details-composition.md @@ -283,6 +283,34 @@ kubectl get -f fn.yaml NAME INSTALLED HEALTHY PACKAGE AGE function-kcl True True xpkg.crossplane.io/crossplane-contrib/function-kcl:v0.11.6 6s ``` +{{< /tab>}} + +{{< tab "Pythonic" >}} + +Create this composition function to install [Pythonic](https://github.com/crossplane-contrib/function-pythonic?tab=readme-ov-file#function-pythonic) support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-pythonic +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-pythonic:v0.3.0 +``` + +Save the function as `fn.yaml` and apply it: + +```shell +kubectl apply -f fn.yaml +``` + +Check that Crossplane installed the function: + +```shell {copy-lines="1"} +kubectl get -f fn.yaml +NAME INSTALLED HEALTHY PACKAGE AGE +function-pythonic True True xpkg.crossplane.io/crossplane-contrib/function-pythonic:v0.3.0 1m +``` {{< /tab >}} {{< /tabs >}} @@ -786,6 +814,67 @@ spec: {{< /tab >}} +{{< tab "Pythonic" >}} + +```yaml {label="comp-pythonic"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-pythonic +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-pythonic + functionRef: + name: function-pythonic + input: + apiVersion: pythonic.fn.crossplane.io/v1alpha1 + kind: Composite + composite: | + class Composite(BaseComposite): + def compose(self): + self.connectionSecret = self.spec.writeConnectionSecretToRef + + user = self.resources.user('iam.aws.m.upbound.io/v1beta1', 'User') + user.spec.forProvider = {} + + for ix in range(2): + key = self.resources[f"access-key-{ix}"]('iam.aws.m.upbound.io/v1beta1', 'AccessKey') + key.spec.forProvider.user = user.status.atProvider.id + key.spec.writeConnectionSecretToRef.name = f"{self.metadata.name}-accesskey-{ix}" + self.connection[f"user-{ix}"] = key.connection.username + self.connection[f"password-{ix}"] = key.connection.password +``` + + + +**How this Composition exposes connection details:** + +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This + tells each AccessKey to write its credentials to an individual Secret. +* Crossplane observes the connection details from each `AccessKey` and makes them + available to the composition when the function runs. +* The Secret reads `AccessKey`'s connection details via + {{}}connection.username{{}} and + {{}}connection.password{{}}. +* The function establishes the connection `Secret` name from the XR + {{}}spec.writeConnectionSecretToRef{{}} + if it exists. +* The function automatically includes a `Secret` object in the XR's composed + resources that represents the XR's aggregated connection details. +* You don't need to create or compose this `Secret` yourself, it's done + automatically for you. +* In `function-pythonic`, connection details base64 encoding and decoding is handled + automatically for you. + + + +{{< /tab >}} + {{< /tabs >}} Save the composition as `composition.yaml` and apply it: @@ -955,6 +1044,28 @@ You don't need to manually compose a `Secret` resource yourself. needed. Use patches to configure these values using data from the XR if needed. +### Automatic aggregation (`function-pythonic`) + +`function-pythonic` automatically observes connection details from +composed resources and creates the aggregated connection secret to +maintain backward compatibility with v1 behavior. + +You don't need to manually compose a `Secret` resource yourself. + +1. **Compose resources**: Create composed resources as usual in your + composition, such as IAM `User` and `AccessKeys`. These resources expose + their connection details in a `Secret`. + +2. **Set `writeConnectionSecretToRef`**: Each composed resource that should have + connection details stored should have their `resource.spec.writeConnectionSecretToRef` set + in the composition. + +3. **Define `connection`**: For each composed resource, assign the connection secret + values wanted to the aggregated secret using `self.connection[key] = resource.connection[key]`. + +4. **Configure the `Secret`**: Set the XR `self.connectionSecret` fields + to override the aggregated secret's default name and namespace. + ## Troubleshooting ### Composite resource's connection details Secret is empty @@ -995,8 +1106,8 @@ For example, `function-python` requires you to convert connection details to base64-encoded strings, while connection details in `function-go-templating` and `function-kcl` are already encoded this way and require no conversion logic. -`function-patch-and-transform` handles encoding when automatically creating the -composed connection secret. +`function-patch-and-transform` and `function-pythonic` handle encoding when automatically +creating the composed connection secret. diff --git a/content/v2.1/learn/community-extension-projects.md b/content/v2.1/learn/community-extension-projects.md index c0882034e..71f6d13ad 100644 --- a/content/v2.1/learn/community-extension-projects.md +++ b/content/v2.1/learn/community-extension-projects.md @@ -93,6 +93,7 @@ use by Crossplane adopters. - [function-kcl](https://github.com/crossplane-contrib/function-kcl) - [function-patch-and-transform](https://github.com/crossplane-contrib/function-patch-and-transform) - [function-python](https://github.com/crossplane-contrib/function-python) +- [function-pythonic](https://github.com/crossplane-contrib/function-pythonic) - [function-sequencer](https://github.com/crossplane-contrib/function-sequencer) - [function-shell](https://github.com/crossplane-contrib/function-shell) - [function-status-transformer](https://github.com/crossplane-contrib/function-status-transformer)