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 >}}
+
{{ tabs >}}
### 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 >}}
+
{{ tabs >}}
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 >}}
+
{{ tabs >}}
### 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 >}}
+
{{ tabs >}}
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)