From 34d96a584b1fe0c5fb8fe1954e7c2099053acfe5 Mon Sep 17 00:00:00 2001 From: Jared Watts Date: Tue, 16 Dec 2025 18:35:22 -0800 Subject: [PATCH 1/6] Add guide for XR connection details in v2 Signed-off-by: Jared Watts --- .../guides/connection-details-composition.md | 786 ++++++++++++++++++ 1 file changed, 786 insertions(+) create mode 100644 content/master/guides/connection-details-composition.md diff --git a/content/master/guides/connection-details-composition.md b/content/master/guides/connection-details-composition.md new file mode 100644 index 000000000..a53d05e4d --- /dev/null +++ b/content/master/guides/connection-details-composition.md @@ -0,0 +1,786 @@ +--- +title: Connection Details Composition +weight: 83 +description: "Expose connection details for composite resources aggregated from their composed resources" +--- + +This guide shows how to expose connection details for composite resources (XRs). +Because composite resources can compose multiple resources, the connection +details they expose are often an aggregate of the connection details from their +composed resources. + +The recommended approach to do this is by including a `Secret` resource in your +Composition that aggregates connection details from other resources and +exposes them for the XR. + +{{}} +Crossplane v1 included functionality that automatically created connection details +for XRs. + +To learn more about how to specify XR connection details in Crossplane v1, please see the +[v1 connection details]({{}}) docs page. + +{{}} + +## Example overview + +To demonstrate how composite resources can expose connection details, this guide +creates a `UserAccessKey` composite resource. This XR represents an AWS IAM user +with multiple access keys. + +When a user creates a `UserAccessKey`, Crossplane provisions an IAM User and two +AccessKeys in AWS. Each AccessKey produces their own connection details like a +username and password. The `UserAccessKey` also composes a `Secret` resource +that exposes the aggregated connection details of its composed resources, allowing +users and applications to easily consume them. + +An example `UserAccessKey` XR looks like this: + +```yaml +apiVersion: example.org/v1alpha1 +kind: UserAccessKey +metadata: + namespace: default + name: my-keys +``` + +**Behind the scenes, Crossplane:** + +1. Creates an AWS IAM `User` and two `AccessKeys` (the composed resources) +2. Collects connection details from both `AccessKeys` +3. Exposes them as the `UserAccessKey`'s connection details in a `Secret` + +The composite resource's connection details `Secret` looks like this: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + namespace: default + name: my-keys-connection-details +data: + user-0: + password-0: + user-1: + password-1: +``` + +Users and applications can consume the `UserAccessKey` connection details by +reading this Secret. + +{{}} +The pattern in this guide applies to any composite resource that needs to expose connection +details, for example: + +* Database connection strings and credentials +* Cluster client certificate and key data +* Application endpoints from services and ingress +{{}} + +## Prerequisites + +This guide requires: + +* A Kubernetes cluster +* Crossplane [installed on the Kubernetes cluster]({{}}) +* `provider-aws-iam` installed and configured with credentials + +{{}} +To set up the AWS provider, follow the [Get Started with Managed Resources]({{}}) guide, +but use provider `provider-aws-iam:v2.3.0` instead. + +Complete the steps to install the provider and configure credentials, then +return to this guide. +{{}} + +## Build the composite resource + +Follow these steps to create a composite resource that exposes connection +details: + +1. [Define](#define-the-schema) the schema of the composite resource +1. [Install](#install-the-function) the composition function you want to use +1. [Configure](#configure-the-composition) how the composition exposes + connection details + +After you complete these steps you can +[use the composite resource](#use-the-composite-resource). + +### Define the schema + +Composite resources are defined using a CompositeResourceDefinition (XRD). + +For this example, create an XRD for the `UserAccessKey` composite resource: + +```yaml +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: useraccesskeys.example.org +spec: + group: example.org + names: + kind: UserAccessKey + plural: useraccesskeys + scope: Namespaced + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object +``` + +Save the XRD as `xrd.yaml` and apply it: + +```shell +kubectl apply -f xrd.yaml +``` + +The Kubernetes API is now serving requests for the `UserAccessKey` composite +resource. + +### Install the function + +Composition functions provide general functionality to help you compose +resources and expose connection details. This guide shows how to compose +connection details with multiple functions. Pick the language you want to use +from the tabs below. + +{{< tabs >}} + +{{< tab "Templated YAML" >}} +Templated YAML is a good choice if you're used to writing +[Helm charts](https://helm.sh). + +Create this composition function to install templated YAML support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-go-templating +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.11.2 +``` + +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-go-templating True True xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.11.2 15s +``` +{{< /tab >}} + +{{< tab "Python" >}} + +Create this composition function to install Python support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-python +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-python:v0.2.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-python True True xpkg.crossplane.io/crossplane-contrib/function-python:v0.2.0 12s +``` +{{< /tab >}} + +{{< tab "KCL" >}} + +Create this composition function to install [KCL](https://kcl-lang.io) support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-kcl +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-kcl:v0.11.6 +``` + +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-kcl True True xpkg.crossplane.io/crossplane-contrib/function-kcl:v0.11.6 6s +``` +{{< /tab >}} + +{{}} + +This guide also uses `function-auto-ready`. This function automatically +marks composed resources as ready when they're healthy: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-auto-ready +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-auto-ready:v0.6.0 +``` + +Save this as `fn-auto-ready.yaml` and apply it: + +```shell +kubectl apply -f fn-auto-ready.yaml +``` + +### Configure the composition + +A Composition tells Crossplane how to compose resources for a composite +resource. This guide also includes a composed `Secret` resource to expose the +composite resource's connection details. + +The general pattern is: + +1. Composed resources write their connection details to individual secrets +2. The Composition reads those connection details during execution +3. The Composition creates a composed `Secret` representing the aggregated connection details for the XR + +{{}} +The composite resource's connection details secret can contain any data you want +and it can be transformed however you need. + +You're not limited to connection details from managed resources - you can +include data from any composed resource, including arbitrary Kubernetes +resources like `ConfigMaps` or `Services`. +{{}} + +Create a Composition that exposes connection details for the `UserAccessKey` +composite resource. + +In this example, the Composition creates two `AccessKey` managed resources and +exposes their credentials as the composite resource's connection details: + +{{< tabs >}} + +{{< tab "Templated YAML" >}} + +```yaml {label="comp-gotmpl"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-go-templating +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + --- + apiVersion: iam.aws.m.upbound.io/v1beta1 + kind: User + metadata: + annotations: + {{ setResourceNameAnnotation "user" }} + spec: + forProvider: {} + --- + apiVersion: iam.aws.m.upbound.io/v1beta1 + kind: AccessKey + metadata: + annotations: + {{ setResourceNameAnnotation "accesskey-0" }} + spec: + forProvider: + userSelector: + matchControllerRef: true + writeConnectionSecretToRef: + name: {{ $.observed.composite.resource.metadata.name }}-accesskey-secret-0 + --- + apiVersion: iam.aws.m.upbound.io/v1beta1 + kind: AccessKey + metadata: + annotations: + {{ setResourceNameAnnotation "accesskey-1" }} + spec: + forProvider: + userSelector: + matchControllerRef: true + writeConnectionSecretToRef: + name: {{ $.observed.composite.resource.metadata.name }}-accesskey-secret-1 + --- + apiVersion: v1 + kind: Secret + metadata: + annotations: + {{ setResourceNameAnnotation "connection-secret" }} + {{ if eq $.observed.resources nil }} + data: {} + {{ else }} + data: + user-0: {{ ( index $.observed.resources "accesskey-0" ).connectionDetails.username }} + user-1: {{ ( index $.observed.resources "accesskey-1" ).connectionDetails.username }} + password-0: {{ ( index $.observed.resources "accesskey-0" ).connectionDetails.password }} + password-1: {{ ( index $.observed.resources "accesskey-1" ).connectionDetails.password }} + {{ end }} + - step: ready + functionRef: + name: function-auto-ready +``` + +**How this Composition exposes connection details:** + +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This + tells each AccessKey to write its credentials to an individual Secret. +* The Composition creates an explicit + {{}}Secret{{}} resource that + represents the composite resource's connection details. +* Crossplane observes the connection details from each `AccessKey` and makes them + available to the composition when the function is executed. +* The Secret reads connection details via + {{}}$.observed.resources{{}} from + the observed composed resources. +* The {{}}{{ if eq $.observed.resources nil }}{{}} + check handles the initial phase when composed resources are still being created. +* In `function-go-templating`, connection details are **already base64-encoded**, so you + use them directly in the Secret's data field. + +{{< /tab >}} + +{{< tab "Python" >}} + +```yaml {label="comp-python"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-python +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-python + functionRef: + name: function-python + input: + apiVersion: python.fn.crossplane.io/v1beta1 + kind: Script + script: | + def compose(req, rsp): + # Get observed composite resource + oxr = req.observed.composite.resource + oxr_name = oxr["metadata"]["name"] + + # IAM User + rsp.desired.resources["user"].resource.update({ + "apiVersion": "iam.aws.m.upbound.io/v1beta1", + "kind": "User", + "spec": { + "forProvider": {} + } + }) + + # Access Key 0 + rsp.desired.resources["accesskey-0"].resource.update({ + "apiVersion": "iam.aws.m.upbound.io/v1beta1", + "kind": "AccessKey", + "spec": { + "forProvider": { + "userSelector": { + "matchControllerRef": True + } + }, + "writeConnectionSecretToRef": { + "name": f"{oxr_name}-accesskey-secret-0" + } + } + }) + + # Access Key 1 + rsp.desired.resources["accesskey-1"].resource.update({ + "apiVersion": "iam.aws.m.upbound.io/v1beta1", + "kind": "AccessKey", + "spec": { + "forProvider": { + "userSelector": { + "matchControllerRef": True + } + }, + "writeConnectionSecretToRef": { + "name": f"{oxr_name}-accesskey-secret-1" + } + } + }) + + # Secret representing the composite resource's connection details + secret_resource = { + "apiVersion": "v1", + "kind": "Secret" + } + + import base64 + + # Only add data if we have connection details to populate + secret_data = {} + if "accesskey-0" in req.observed.resources: + accesskey0_conn = req.observed.resources["accesskey-0"].connection_details + if "username" in accesskey0_conn: + secret_data["user-0"] = base64.b64encode(accesskey0_conn["username"]).decode("utf-8") + if "password" in accesskey0_conn: + secret_data["password-0"] = base64.b64encode(accesskey0_conn["password"]).decode("utf-8") + + if "accesskey-1" in req.observed.resources: + accesskey1_conn = req.observed.resources["accesskey-1"].connection_details + if "username" in accesskey1_conn: + secret_data["user-1"] = base64.b64encode(accesskey1_conn["username"]).decode("utf-8") + if "password" in accesskey1_conn: + secret_data["password-1"] = base64.b64encode(accesskey1_conn["password"]).decode("utf-8") + + if secret_data: + secret_resource["data"] = secret_data + + rsp.desired.resources["connection-secret"].resource.update(secret_resource) + - step: ready + functionRef: + name: function-auto-ready +``` + +**How this Composition exposes connection details:** + +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This + tells each AccessKey to write its credentials to an individual Secret. +* The Composition creates an explicit + {{}}Secret{{}} resource that + represents the composite resource's connection details. +* Crossplane observes the connection details from each AccessKey and makes them + available to the composition when the function is executed. +* The Secret reads connection details via + {{}}req.observed.resources["accesskey-0"].connection_details{{}} + from the observed composed resources. +* The {{}}if "accesskey-0" in req.observed.resources{{}} + check handles the initial phase when composed resources are still being created. +* In `function-python`, connection details are **plaintext bytes**, but the Secret's data field requires base64-encoded strings. + Therefore, you must first use {{}}b64encode(){{}} to encode + and then use {{}}.decode("utf-8"){{}} + to convert to a string. + +{{< /tab >}} + +{{< tab "KCL" >}} + +```yaml {label="comp-kcl"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-kcl +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-kcl + functionRef: + name: function-kcl + input: + apiVersion: krm.kcl.dev/v1alpha1 + kind: KCLInput + spec: + source: | + oxr = option("params").oxr + ocds = option("params").ocds + + user = { + apiVersion = "iam.aws.m.upbound.io/v1beta1" + kind = "User" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "user" + } + spec.forProvider = {} + } + + accesskey0 = { + apiVersion = "iam.aws.m.upbound.io/v1beta1" + kind = "AccessKey" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "accesskey-0" + } + spec.forProvider.userSelector.matchControllerRef = True + spec.writeConnectionSecretToRef.name = "${oxr.metadata.name}-accesskey-secret-0" + } + + accesskey1 = { + apiVersion = "iam.aws.m.upbound.io/v1beta1" + kind = "AccessKey" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "accesskey-1" + } + spec.forProvider.userSelector.matchControllerRef = True + spec.writeConnectionSecretToRef.name = "${oxr.metadata.name}-accesskey-secret-1" + } + + secret = { + apiVersion = "v1" + kind = "Secret" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "connection-secret" + } + data = { + "user-0" = ocds["accesskey-0"]?.ConnectionDetails?.username or "" + "user-1" = ocds["accesskey-1"]?.ConnectionDetails?.username or "" + "password-0" = ocds["accesskey-0"]?.ConnectionDetails?.password or "" + "password-1" = ocds["accesskey-1"]?.ConnectionDetails?.password or "" + } if ocds else {} + } + + items = [user, accesskey0, accesskey1, secret] + - step: ready + functionRef: + name: function-auto-ready +``` + +**How this Composition exposes connection details:** + +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This + tells each AccessKey to write its credentials to an individual Secret. +* The Composition creates an explicit + {{}}Secret{{}} resource that + represents the composite resource's connection details. +* Crossplane observes the connection details from each + `AccessKey` and makes them available to the composition when the function is executed. +* The Secret reads connection details via + {{}}ocds["accesskey-0"]?.ConnectionDetails?.username{{}} + from the observed composed resources, safely handling the case where connection details don't exist yet. +* The {{}}if ocds else {}{{}} handles + the phase when composed resources are still being created. +* In `function-kcl`, connection details are **already base64-encoded**, so you use them + directly in the Secret's data field. + +{{< /tab >}} + +{{}} + +Save the composition as `composition.yaml` and apply it: + +```shell +kubectl apply -f composition.yaml +``` + +## Use the composite resource + +The Composition now specifies how to compose connection details for the +`UserAccessKey` composite resource. + +Create a `UserAccessKey` to see it in action: + +```yaml +apiVersion: example.org/v1alpha1 +kind: UserAccessKey +metadata: + namespace: default + name: my-keys +spec: {} +``` + +Save the composite resource as `my-keys.yaml` and apply it: + +```shell +kubectl apply -f my-keys.yaml +``` + +Check that the composite resource is ready: + +```shell {copy-lines="1"} +kubectl get -f my-keys.yaml +NAME SYNCED READY COMPOSITION AGE +my-keys True True useraccesskeys-go-templating 45s +``` + +{{}} +It may take a minute for AWS to provision the IAM resources. The composite +resource becomes `READY` when all composed resources are healthy. +{{}} + +## Verify the connection details + +Composite resources expose their connection details through a `Secret`. Check that +Crossplane created the `Secret`. + +View all the composed resources and connection secrets together using the `crossplane` CLI. + +{{}} +See the [Crossplane CLI docs]({{}}) to +learn how to install and use the Crossplane CLI. +{{< /hint >}} + +```shell {copy-lines="1"} +crossplane beta trace useraccesskey.example.org/my-keys -s +NAME SYNCED READY STATUS +UserAccessKey/my-keys (default) True True Available +├─ AccessKey/my-keys-080cea13962f (default) True True Available +│ └─ Secret/my-keys-accesskey-secret-0 (default) - - +├─ AccessKey/my-keys-8204b6e191f5 (default) True True Available +│ └─ Secret/my-keys-accesskey-secret-1 (default) - - +├─ User/my-keys-2d87fa8c5609 (default) True True Available +└─ Secret/my-keys-586e2994bda1 (default) - - +``` + +Each composed `AccessKey` wrote its connection details to an individual `Secret` +and another `Secret` was composed that contains the aggregated connection details +for the `my-keys` composite resource. + +Check the composite resource's aggregated connection details `Secret`: + +```shell {copy-lines="1"} +kubectl get secret -n default -l crossplane.io/composite=my-keys +NAME TYPE DATA AGE +my-keys-586e2994bda1 Opaque 4 5m37s +``` + +{{}} +The composite resource's connection details Secret has a label +`crossplane.io/composite=my-keys` that makes it easy to find. +{{}} + +Verify the composite resource's connection details Secret contains all the +expected credentials: + +```shell +kubectl get secret -n default -l crossplane.io/composite=my-keys -o jsonpath='{.items[0].data}' | jq +``` + +You should see output like this: + +```json {copy-lines="none"} +{ + "password-0": "", + "password-1": "", + "user-0": "", + "user-1": "" +} +``` + +Decode one of the values to verify it contains the expected data: + +```shell +kubectl get secret -n default -l crossplane.io/composite=my-keys -o jsonpath='{.items[0].data.user-0}' | base64 -d +``` + +## Understanding how composing connection details works + +Let's review the basic steps to expose connection details for a composite resource: + +1. **Compose resources**: Create composed resources as usual in your + composition, such as IAM `User` and `AccessKeys`. These resources will expose + their connection details in a `Secret`. + +2. **Set `writeConnectionSecretToRef`**: Each composed resource that should have + connection details stored in their own individual `Secret` should have their + `writeConnectionSecretToRef` set in the composition. + +3. **Observed connection details**: Crossplane will observe the actual state of + each composed resource, including its connection details, and make this data + available when it calls the function. + +4. **Compose the aggregate `Secret`**: With the observed connection details of + your composed resources in hand, compose a `Secret` resource that combines + the important connection details you want to expose for the XR. + +5. **Safely handle transient state**: When your XR is first created, the + composed resources and/or their connection details may not exist yet. Your + Composition should safely handle these cases by checking if resources and + their connection details exist before accessing them. + + +## Troubleshooting + +### Composite resource's connection details Secret is empty + +**Causes:** + +* Composed resources don't have `writeConnectionSecretToRef` set +* Composed resources aren't ready/healthy yet +* Not handling initial nil state correctly in the Composition + +**Solutions**: + +* Verify `writeConnectionSecretToRef` is set on all composed managed resources +* Wait for composed resources to become ready (`kubectl get` and check `READY` column) +* Verify the composed resource is actually producing connection details: + `kubectl get secret -o yaml` +* Add nil/empty checks in your Composition logic to safeguard access to data that may not exist yet + +### Connection details are not encoded properly + +**Cause:** Not encoding the aggregate secret data properly in your Composition logic + +**Solution:** Ensure that your connection details data is properly encoded for +the function you're using. 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. + +## Clean up + +Delete the composite resource to clean-up: + +```shell +kubectl delete -f my-keys.yaml +``` + +When you delete the composite resource, Crossplane deletes: + +* The composed IAM `User` and `AccessKeys` from AWS +* The individual `Secrets` from composed resources +* The composite resource's connection details `Secret` + +{{}} +Make sure to delete your composite resources before uninstalling the provider +or shutting down your control plane. If those are no longer running, they can't +clean up composed resources and you would need to delete them manually. +{{}} + +## Learn more + +* [Composite resources]({{}}) +* [Compositions]({{}}) +* [Write a composition function in Go]({{}}) +* [Write a composition function in Python]({{}}) From 95bc6b03e71766e369c38e2df523361edda47117 Mon Sep 17 00:00:00 2001 From: Jared Watts Date: Wed, 17 Dec 2025 22:07:35 -0800 Subject: [PATCH 2/6] v2 XR connection details guide improvements * use stringData instead of base64 encoding ourselves for function-python * expose a writeConnectionSecretToRef.name field for the XR so the secret name can be set if desired * update all compositions to safely handle the secret name existing or not * add note with guidance about cluster scoped XRDs and setting the namespace for composed resources Signed-off-by: Jared Watts --- .../guides/connection-details-composition.md | 122 +++++++++++------- 1 file changed, 78 insertions(+), 44 deletions(-) diff --git a/content/master/guides/connection-details-composition.md b/content/master/guides/connection-details-composition.md index a53d05e4d..0fe0ee0c4 100644 --- a/content/master/guides/connection-details-composition.md +++ b/content/master/guides/connection-details-composition.md @@ -9,9 +9,9 @@ Because composite resources can compose multiple resources, the connection details they expose are often an aggregate of the connection details from their composed resources. -The recommended approach to do this is by including a `Secret` resource in your -Composition that aggregates connection details from other resources and -exposes them for the XR. +The recommended approach to do this is to simply include a Kubernetes `Secret` +resource in your Composition that aggregates the connection details from other +resources and exposes them for the XR. {{}} Crossplane v1 included functionality that automatically created connection details @@ -19,7 +19,6 @@ for XRs. To learn more about how to specify XR connection details in Crossplane v1, please see the [v1 connection details]({{}}) docs page. - {{}} ## Example overview @@ -133,8 +132,23 @@ spec: properties: spec: type: object + properties: + writeConnectionSecretToRef: + type: object + properties: + name: + type: string ``` +{{}} +This XRD schema defines a `.spec.writeConnectionSecretToRef.name` field that +allows the user to optionally set the name for the XR connection details secret. + +For a `Cluster` scoped XRD, a `.spec.writeConnectionSecretToRef.namespace` field +could also be added to allow the user to specify the namespace of the secret +too. +{{}} + Save the XRD as `xrd.yaml` and apply it: ```shell @@ -239,7 +253,7 @@ function-kcl True True xpkg.crossplane.io/cross ``` {{< /tab >}} -{{}} +{{< /tabs >}} This guide also uses `function-auto-ready`. This function automatically marks composed resources as ready when they're healthy: @@ -284,7 +298,7 @@ Create a Composition that exposes connection details for the `UserAccessKey` composite resource. In this example, the Composition creates two `AccessKey` managed resources and -exposes their credentials as the composite resource's connection details: +exposes their credentials as the composite resource's connection details `Secret`: {{< tabs >}} @@ -346,6 +360,7 @@ spec: apiVersion: v1 kind: Secret metadata: + name: {{ dig "spec" "writeConnectionSecretToRef" "name" "" $.observed.composite.resource}} annotations: {{ setResourceNameAnnotation "connection-secret" }} {{ if eq $.observed.resources nil }} @@ -370,12 +385,15 @@ spec: * The Composition creates an explicit {{}}Secret{{}} resource that represents the composite resource's connection details. +* The {{}}name{{}} of the `Secret` is set using the +{{}}dig{{}} function to safely read the XR's + `.spec.writeConnectionSecretToRef.name` field if it exists. * Crossplane observes the connection details from each `AccessKey` and makes them available to the composition when the function is executed. * The Secret reads connection details via - {{}}$.observed.resources{{}} from + {{}}$.observed.resources{{}} from the observed composed resources. -* The {{}}{{ if eq $.observed.resources nil }}{{}} +* The {{}}{{ if eq $.observed.resources nil }}{{}} check handles the initial phase when composed resources are still being created. * In `function-go-templating`, connection details are **already base64-encoded**, so you use them directly in the Secret's data field. @@ -451,55 +469,63 @@ spec: # Secret representing the composite resource's connection details secret_resource = { "apiVersion": "v1", - "kind": "Secret" + "kind": "Secret", + "metadata": {} } - import base64 + # If a secret name was provided then use it + secret_name = "" + if "writeConnectionSecretToRef" in oxr["spec"] and "name" in oxr["spec"]["writeConnectionSecretToRef"]: + secret_name = oxr["spec"]["writeConnectionSecretToRef"]["name"] + + secret_resource["metadata"]["name"] = secret_name # Only add data if we have connection details to populate - secret_data = {} + data = {} if "accesskey-0" in req.observed.resources: accesskey0_conn = req.observed.resources["accesskey-0"].connection_details if "username" in accesskey0_conn: - secret_data["user-0"] = base64.b64encode(accesskey0_conn["username"]).decode("utf-8") + data["user-0"] = accesskey0_conn["username"].decode("utf-8") if "password" in accesskey0_conn: - secret_data["password-0"] = base64.b64encode(accesskey0_conn["password"]).decode("utf-8") + data["password-0"] = accesskey0_conn["password"].decode("utf-8") if "accesskey-1" in req.observed.resources: accesskey1_conn = req.observed.resources["accesskey-1"].connection_details if "username" in accesskey1_conn: - secret_data["user-1"] = base64.b64encode(accesskey1_conn["username"]).decode("utf-8") + data["user-1"] = accesskey1_conn["username"].decode("utf-8") if "password" in accesskey1_conn: - secret_data["password-1"] = base64.b64encode(accesskey1_conn["password"]).decode("utf-8") + data["password-1"] = accesskey1_conn["password"].decode("utf-8") - if secret_data: - secret_resource["data"] = secret_data + if data: + secret_resource["stringData"] = data rsp.desired.resources["connection-secret"].resource.update(secret_resource) - step: ready functionRef: name: function-auto-ready + ``` **How this Composition exposes connection details:** -* Each composed {{}}AccessKey{{}} has - {{}}writeConnectionSecretToRef{{}} set. This +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This tells each AccessKey to write its credentials to an individual Secret. * The Composition creates an explicit - {{}}Secret{{}} resource that + {{}}Secret{{}} resource that represents the composite resource's connection details. +* The {{}}secret_name{{}} is set only after safely checking that the XR's + {{}}.spec.writeConnectionSecretToRef.name{{}} field exists. * Crossplane observes the connection details from each AccessKey and makes them available to the composition when the function is executed. * The Secret reads connection details via - {{}}req.observed.resources["accesskey-0"].connection_details{{}} + {{}}req.observed.resources["accesskey-0"].connection_details{{}} from the observed composed resources. -* The {{}}if "accesskey-0" in req.observed.resources{{}} +* The {{}}if "accesskey-0" in req.observed.resources{{}} check handles the initial phase when composed resources are still being created. -* In `function-python`, connection details are **plaintext bytes**, but the Secret's data field requires base64-encoded strings. - Therefore, you must first use {{}}b64encode(){{}} to encode - and then use {{}}.decode("utf-8"){{}} - to convert to a string. +* In `function-python`, connection details are **plaintext bytes**. To store them on the `Secret`, first + convert them to strings with {{}}.decode("utf-8"){{}} + and then save them using the secret's {{}}stringData{{}} field. {{< /tab >}} @@ -559,6 +585,7 @@ spec: secret = { apiVersion = "v1" kind = "Secret" + metadata.name = oxr?.spec?.writeConnectionSecretToRef?.name or "" metadata.annotations = { "krm.kcl.dev/composition-resource-name" = "connection-secret" } @@ -584,19 +611,22 @@ spec: * The Composition creates an explicit {{}}Secret{{}} resource that represents the composite resource's connection details. +* The {{}}name{{}} of the `Secret` is set using + {{}}?.{{}} optional chaining operators to safely read the XR's + {{}}.spec.writeConnectionSecretToRef.name{{}} field if it exists. * Crossplane observes the connection details from each `AccessKey` and makes them available to the composition when the function is executed. * The Secret reads connection details via - {{}}ocds["accesskey-0"]?.ConnectionDetails?.username{{}} + {{}}ocds["accesskey-0"]?.ConnectionDetails?.username{{}} from the observed composed resources, safely handling the case where connection details don't exist yet. -* The {{}}if ocds else {}{{}} handles +* The {{}}if ocds else {}{{}} handles the phase when composed resources are still being created. * In `function-kcl`, connection details are **already base64-encoded**, so you use them directly in the Secret's data field. {{< /tab >}} -{{}} +{{< /tabs >}} Save the composition as `composition.yaml` and apply it: @@ -617,7 +647,9 @@ kind: UserAccessKey metadata: namespace: default name: my-keys -spec: {} +spec: + writeConnectionSecretToRef: + name: my-keys-connection-details ``` Save the composite resource as `my-keys.yaml` and apply it: @@ -644,7 +676,8 @@ resource becomes `READY` when all composed resources are healthy. Composite resources expose their connection details through a `Secret`. Check that Crossplane created the `Secret`. -View all the composed resources and connection secrets together using the `crossplane` CLI. +View all the composed resources (including the connection details `Secret`) +together using the `crossplane` CLI. {{}} See the [Crossplane CLI docs]({{}}) to @@ -652,20 +685,18 @@ learn how to install and use the Crossplane CLI. {{< /hint >}} ```shell {copy-lines="1"} -crossplane beta trace useraccesskey.example.org/my-keys -s -NAME SYNCED READY STATUS -UserAccessKey/my-keys (default) True True Available -├─ AccessKey/my-keys-080cea13962f (default) True True Available -│ └─ Secret/my-keys-accesskey-secret-0 (default) - - -├─ AccessKey/my-keys-8204b6e191f5 (default) True True Available -│ └─ Secret/my-keys-accesskey-secret-1 (default) - - -├─ User/my-keys-2d87fa8c5609 (default) True True Available -└─ Secret/my-keys-586e2994bda1 (default) - - +crossplane beta trace useraccesskey.example.org/my-keys +NAME SYNCED READY STATUS +UserAccessKey/my-keys (default) True True Available +├─ AccessKey/my-keys-14c0578cad85 (default) True True Available +├─ AccessKey/my-keys-e420789d13a3 (default) True True Available +├─ User/my-keys-c63b530f8e68 (default) True True Available +└─ Secret/my-keys-connection-details (default) - - ``` -Each composed `AccessKey` wrote its connection details to an individual `Secret` -and another `Secret` was composed that contains the aggregated connection details -for the `my-keys` composite resource. +The `my-keys` composite resource created an IAM `User` and two IAM `AccessKeys`, +and a `Secret` was also created that contains the aggregated connection details +for the composite resource. Check the composite resource's aggregated connection details `Secret`: @@ -678,9 +709,12 @@ my-keys-586e2994bda1 Opaque 4 5m37s {{}} The composite resource's connection details Secret has a label `crossplane.io/composite=my-keys` that makes it easy to find. + +If `.spec.writeConnectionSecretToRef.name` was set on the XR, then the `Secret` +will have that exact name. {{}} -Verify the composite resource's connection details Secret contains all the +Verify the composite resource's connection details `Secret` contains all the expected credentials: ```shell From 5be0d3974019709af69603f13497b5d8c4280c6f Mon Sep 17 00:00:00 2001 From: Jared Watts Date: Wed, 17 Dec 2025 22:45:58 -0800 Subject: [PATCH 3/6] Vale fixes for v2 XR connection details guide Signed-off-by: Jared Watts --- .../guides/connection-details-composition.md | 86 ++++++++++++------- .../vale/styles/Crossplane/allowed-jargon.txt | 2 + .../vale/styles/Crossplane/provider-words.txt | 2 + 3 files changed, 58 insertions(+), 32 deletions(-) diff --git a/content/master/guides/connection-details-composition.md b/content/master/guides/connection-details-composition.md index 0fe0ee0c4..21e16bef1 100644 --- a/content/master/guides/connection-details-composition.md +++ b/content/master/guides/connection-details-composition.md @@ -6,15 +6,17 @@ description: "Expose connection details for composite resources aggregated from This guide shows how to expose connection details for composite resources (XRs). Because composite resources can compose multiple resources, the connection + details they expose are often an aggregate of the connection details from their + composed resources. -The recommended approach to do this is to simply include a Kubernetes `Secret` +The recommended approach is to include a Kubernetes `Secret` resource in your Composition that aggregates the connection details from other resources and exposes them for the XR. {{}} -Crossplane v1 included functionality that automatically created connection details +Crossplane v1 included a feature that automatically created connection details for XRs. To learn more about how to specify XR connection details in Crossplane v1, please see the @@ -23,15 +25,15 @@ To learn more about how to specify XR connection details in Crossplane v1, pleas ## Example overview -To demonstrate how composite resources can expose connection details, this guide -creates a `UserAccessKey` composite resource. This XR represents an AWS IAM user +This guide shows how composite resources can expose connection details by +creating a `UserAccessKey` composite resource. This XR represents an AWS IAM user with multiple access keys. When a user creates a `UserAccessKey`, Crossplane provisions an IAM User and two AccessKeys in AWS. Each AccessKey produces their own connection details like a username and password. The `UserAccessKey` also composes a `Secret` resource that exposes the aggregated connection details of its composed resources, allowing -users and applications to easily consume them. +users and applications to consume them. An example `UserAccessKey` XR looks like this: @@ -107,7 +109,7 @@ After you complete these steps you can ### Define the schema -Composite resources are defined using a CompositeResourceDefinition (XRD). +A CompositeResourceDefinition (XRD) defines composite resources. For this example, create an XRD for the `UserAccessKey` composite resource: @@ -144,9 +146,11 @@ spec: This XRD schema defines a `.spec.writeConnectionSecretToRef.name` field that allows the user to optionally set the name for the XR connection details secret. + For a `Cluster` scoped XRD, a `.spec.writeConnectionSecretToRef.namespace` field could also be added to allow the user to specify the namespace of the secret too. + {{}} Save the XRD as `xrd.yaml` and apply it: @@ -160,7 +164,7 @@ resource. ### Install the function -Composition functions provide general functionality to help you compose +Composition functions provide general features to help you compose resources and expose connection details. This guide shows how to compose connection details with multiple functions. Pick the language you want to use from the tabs below. @@ -282,12 +286,12 @@ composite resource's connection details. The general pattern is: 1. Composed resources write their connection details to individual secrets -2. The Composition reads those connection details during execution +2. The Composition reads those connection details when the function runs 3. The Composition creates a composed `Secret` representing the aggregated connection details for the XR {{}} The composite resource's connection details secret can contain any data you want -and it can be transformed however you need. +and you can transform it as needed. You're not limited to connection details from managed resources - you can include data from any composed resource, including arbitrary Kubernetes @@ -377,6 +381,8 @@ spec: name: function-auto-ready ``` + + **How this Composition exposes connection details:** * Each composed {{}}AccessKey{{}} has @@ -386,10 +392,10 @@ spec: {{}}Secret{{}} resource that represents the composite resource's connection details. * The {{}}name{{}} of the `Secret` is set using the -{{}}dig{{}} function to safely read the XR's +{{}}dig{{}} function to read the XR's `.spec.writeConnectionSecretToRef.name` field if it exists. * Crossplane observes the connection details from each `AccessKey` and makes them - available to the composition when the function is executed. + available to the composition when the function runs. * The Secret reads connection details via {{}}$.observed.resources{{}} from the observed composed resources. @@ -397,6 +403,8 @@ spec: check handles the initial phase when composed resources are still being created. * In `function-go-templating`, connection details are **already base64-encoded**, so you use them directly in the Secret's data field. + + {{< /tab >}} @@ -506,6 +514,8 @@ spec: ``` + + **How this Composition exposes connection details:** * Each composed {{}}AccessKey{{}} has @@ -514,10 +524,10 @@ spec: * The Composition creates an explicit {{}}Secret{{}} resource that represents the composite resource's connection details. -* The {{}}secret_name{{}} is set only after safely checking that the XR's +* The {{}}secret_name{{}} is set only after checking that the XR's {{}}.spec.writeConnectionSecretToRef.name{{}} field exists. * Crossplane observes the connection details from each AccessKey and makes them - available to the composition when the function is executed. + available to the composition when the function runs. * The Secret reads connection details via {{}}req.observed.resources["accesskey-0"].connection_details{{}} from the observed composed resources. @@ -526,6 +536,8 @@ spec: * In `function-python`, connection details are **plaintext bytes**. To store them on the `Secret`, first convert them to strings with {{}}.decode("utf-8"){{}} and then save them using the secret's {{}}stringData{{}} field. + + {{< /tab >}} @@ -603,6 +615,7 @@ spec: name: function-auto-ready ``` + **How this Composition exposes connection details:** * Each composed {{}}AccessKey{{}} has @@ -612,17 +625,18 @@ spec: {{}}Secret{{}} resource that represents the composite resource's connection details. * The {{}}name{{}} of the `Secret` is set using - {{}}?.{{}} optional chaining operators to safely read the XR's + {{}}?.{{}} optional chaining operators to read the XR's {{}}.spec.writeConnectionSecretToRef.name{{}} field if it exists. * Crossplane observes the connection details from each - `AccessKey` and makes them available to the composition when the function is executed. + `AccessKey` and makes them available to the composition when the function runs. * The Secret reads connection details via {{}}ocds["accesskey-0"]?.ConnectionDetails?.username{{}} - from the observed composed resources, safely handling the case where connection details don't exist yet. + from the observed composed resources, handling the case where connection details don't exist yet. * The {{}}if ocds else {}{{}} handles the phase when composed resources are still being created. * In `function-kcl`, connection details are **already base64-encoded**, so you use them directly in the Secret's data field. + {{< /tab >}} @@ -708,10 +722,10 @@ my-keys-586e2994bda1 Opaque 4 5m37s {{}} The composite resource's connection details Secret has a label -`crossplane.io/composite=my-keys` that makes it easy to find. +`crossplane.io/composite=my-keys` for convenient lookup. -If `.spec.writeConnectionSecretToRef.name` was set on the XR, then the `Secret` -will have that exact name. +If you set `.spec.writeConnectionSecretToRef.name` on the XR, the `Secret` +has that exact name. {{}} Verify the composite resource's connection details `Secret` contains all the @@ -740,27 +754,27 @@ kubectl get secret -n default -l crossplane.io/composite=my-keys -o jsonpath='{. ## Understanding how composing connection details works -Let's review the basic steps to expose connection details for a composite resource: +The basic steps to expose connection details for a composite resource are: 1. **Compose resources**: Create composed resources as usual in your - composition, such as IAM `User` and `AccessKeys`. These resources will expose + 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 in their own individual `Secret` should have their `writeConnectionSecretToRef` set in the composition. -3. **Observed connection details**: Crossplane will observe the actual state of - each composed resource, including its connection details, and make this data - available when it calls the function. +3. **Observed connection details**: Crossplane observes the actual state of + each composed resource, including its connection details, and makes this data + available when it runs the function. -4. **Compose the aggregate `Secret`**: With the observed connection details of +4. **Compose the combined `Secret`**: With the observed connection details of your composed resources in hand, compose a `Secret` resource that combines the important connection details you want to expose for the XR. -5. **Safely handle transient state**: When your XR is first created, the +5. **Handle transient state**: When your XR is first created, the composed resources and/or their connection details may not exist yet. Your - Composition should safely handle these cases by checking if resources and + Composition should handle these cases by checking if resources and their connection details exist before accessing them. @@ -770,31 +784,39 @@ Let's review the basic steps to expose connection details for a composite resour **Causes:** + * Composed resources don't have `writeConnectionSecretToRef` set * Composed resources aren't ready/healthy yet * Not handling initial nil state correctly in the Composition + + + **Solutions**: * Verify `writeConnectionSecretToRef` is set on all composed managed resources -* Wait for composed resources to become ready (`kubectl get` and check `READY` column) +* Wait for composed resources to become ready (`kubectl get` and check the `READY` column) * Verify the composed resource is actually producing connection details: `kubectl get secret -o yaml` * Add nil/empty checks in your Composition logic to safeguard access to data that may not exist yet + + -### Connection details are not encoded properly + +### Connection details aren't encoded correctly -**Cause:** Not encoding the aggregate secret data properly in your Composition logic +**Cause:** not encoding the combined secret data correctly in your Composition logic -**Solution:** Ensure that your connection details data is properly encoded for +**Solution:** Ensure that your connection details data is correctly encoded for the function you're using. 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. + ## Clean up -Delete the composite resource to clean-up: +Delete the composite resource to clean up: ```shell kubectl delete -f my-keys.yaml diff --git a/utils/vale/styles/Crossplane/allowed-jargon.txt b/utils/vale/styles/Crossplane/allowed-jargon.txt index 613fde1bc..036db7d4c 100644 --- a/utils/vale/styles/Crossplane/allowed-jargon.txt +++ b/utils/vale/styles/Crossplane/allowed-jargon.txt @@ -7,6 +7,7 @@ autoscaler backoff backported base64 +base64-encoded bool boolean booleans @@ -73,6 +74,7 @@ NOTES.txt OCI OIDC PersistentVolumeClaim +plaintext Prepopulate pre-releases Pre-releases diff --git a/utils/vale/styles/Crossplane/provider-words.txt b/utils/vale/styles/Crossplane/provider-words.txt index 2fb8fbad4..f2b11dd29 100644 --- a/utils/vale/styles/Crossplane/provider-words.txt +++ b/utils/vale/styles/Crossplane/provider-words.txt @@ -1,3 +1,5 @@ +AccessKey +AccessKeys crossplane-contrib Dataflow DynmoDB From 5cef84f24afa95c818592215964014eb777ff006 Mon Sep 17 00:00:00 2001 From: Jared Watts Date: Thu, 18 Dec 2025 11:10:08 -0800 Subject: [PATCH 4/6] Add troubleshooting guidance for empty namespace connection secrets Signed-off-by: Jared Watts --- .../guides/connection-details-composition.md | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/content/master/guides/connection-details-composition.md b/content/master/guides/connection-details-composition.md index 21e16bef1..264898ded 100644 --- a/content/master/guides/connection-details-composition.md +++ b/content/master/guides/connection-details-composition.md @@ -770,7 +770,9 @@ The basic steps to expose connection details for a composite resource are: 4. **Compose the combined `Secret`**: With the observed connection details of your composed resources in hand, compose a `Secret` resource that combines - the important connection details you want to expose for the XR. + the important connection details you want to expose for the XR. Consider + allowing the consumer of the XR to specify the name they want this secret to + have. 5. **Handle transient state**: When your XR is first created, the composed resources and/or their connection details may not exist yet. Your @@ -803,9 +805,10 @@ The basic steps to expose connection details for a composite resource are: + ### Connection details aren't encoded correctly -**Cause:** not encoding the combined secret data correctly in your Composition logic +**Cause:** Not encoding the combined secret data correctly in your Composition logic **Solution:** Ensure that your connection details data is correctly encoded for the function you're using. For example, `function-python` requires you to @@ -813,6 +816,19 @@ 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. + + +### Secret has an empty namespace + +**Cause:** Not setting the namespace of the `Secret` for a Cluster scoped XR, +resulting in an error message like `an empty namespace may not be set +when a resource name is provided` + +**Solution:** When Cluster scoped XRs compose namespace-scoped resources like a +`Secret`, you must explicitly set a namespace on the resource. Consider allowing +the XR consumer to specify the namespace value in your composition. Namespaced +XRs don't have this problem because Crossplane defaults any composed resource's +namespace to the XR's namespace if left empty. ## Clean up From 22223bd3da86b4cd313b861db5a91bbaf4b2d10d Mon Sep 17 00:00:00 2001 From: Jared Watts Date: Thu, 18 Dec 2025 13:05:49 -0800 Subject: [PATCH 5/6] v2 XR connection details guide polish Signed-off-by: Jared Watts --- content/master/guides/connection-details-composition.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/content/master/guides/connection-details-composition.md b/content/master/guides/connection-details-composition.md index 264898ded..12ac3f973 100644 --- a/content/master/guides/connection-details-composition.md +++ b/content/master/guides/connection-details-composition.md @@ -4,12 +4,12 @@ weight: 83 description: "Expose connection details for composite resources aggregated from their composed resources" --- + This guide shows how to expose connection details for composite resources (XRs). Because composite resources can compose multiple resources, the connection - details they expose are often an aggregate of the connection details from their - composed resources. + The recommended approach is to include a Kubernetes `Secret` resource in your Composition that aggregates the connection details from other @@ -37,7 +37,7 @@ users and applications to consume them. An example `UserAccessKey` XR looks like this: -```yaml +```yaml {copy-lines="none"} apiVersion: example.org/v1alpha1 kind: UserAccessKey metadata: @@ -53,7 +53,7 @@ metadata: The composite resource's connection details `Secret` looks like this: -```yaml +```yaml {copy-lines="none"} apiVersion: v1 kind: Secret metadata: From c1b60f1c6ca38b7c18ddbc37f534906efc51c1d2 Mon Sep 17 00:00:00 2001 From: Jared Watts Date: Thu, 18 Dec 2025 13:08:24 -0800 Subject: [PATCH 6/6] sync v2 XR connection details guide to v2.1 and v2.0 Signed-off-by: Jared Watts --- .../guides/connection-details-composition.md | 858 ++++++++++++++++++ .../guides/connection-details-composition.md | 858 ++++++++++++++++++ 2 files changed, 1716 insertions(+) create mode 100644 content/v2.0/guides/connection-details-composition.md create mode 100644 content/v2.1/guides/connection-details-composition.md diff --git a/content/v2.0/guides/connection-details-composition.md b/content/v2.0/guides/connection-details-composition.md new file mode 100644 index 000000000..12ac3f973 --- /dev/null +++ b/content/v2.0/guides/connection-details-composition.md @@ -0,0 +1,858 @@ +--- +title: Connection Details Composition +weight: 83 +description: "Expose connection details for composite resources aggregated from their composed resources" +--- + + +This guide shows how to expose connection details for composite resources (XRs). +Because composite resources can compose multiple resources, the connection +details they expose are often an aggregate of the connection details from their +composed resources. + + +The recommended approach is to include a Kubernetes `Secret` +resource in your Composition that aggregates the connection details from other +resources and exposes them for the XR. + +{{}} +Crossplane v1 included a feature that automatically created connection details +for XRs. + +To learn more about how to specify XR connection details in Crossplane v1, please see the +[v1 connection details]({{}}) docs page. +{{}} + +## Example overview + +This guide shows how composite resources can expose connection details by +creating a `UserAccessKey` composite resource. This XR represents an AWS IAM user +with multiple access keys. + +When a user creates a `UserAccessKey`, Crossplane provisions an IAM User and two +AccessKeys in AWS. Each AccessKey produces their own connection details like a +username and password. The `UserAccessKey` also composes a `Secret` resource +that exposes the aggregated connection details of its composed resources, allowing +users and applications to consume them. + +An example `UserAccessKey` XR looks like this: + +```yaml {copy-lines="none"} +apiVersion: example.org/v1alpha1 +kind: UserAccessKey +metadata: + namespace: default + name: my-keys +``` + +**Behind the scenes, Crossplane:** + +1. Creates an AWS IAM `User` and two `AccessKeys` (the composed resources) +2. Collects connection details from both `AccessKeys` +3. Exposes them as the `UserAccessKey`'s connection details in a `Secret` + +The composite resource's connection details `Secret` looks like this: + +```yaml {copy-lines="none"} +apiVersion: v1 +kind: Secret +metadata: + namespace: default + name: my-keys-connection-details +data: + user-0: + password-0: + user-1: + password-1: +``` + +Users and applications can consume the `UserAccessKey` connection details by +reading this Secret. + +{{}} +The pattern in this guide applies to any composite resource that needs to expose connection +details, for example: + +* Database connection strings and credentials +* Cluster client certificate and key data +* Application endpoints from services and ingress +{{}} + +## Prerequisites + +This guide requires: + +* A Kubernetes cluster +* Crossplane [installed on the Kubernetes cluster]({{}}) +* `provider-aws-iam` installed and configured with credentials + +{{}} +To set up the AWS provider, follow the [Get Started with Managed Resources]({{}}) guide, +but use provider `provider-aws-iam:v2.3.0` instead. + +Complete the steps to install the provider and configure credentials, then +return to this guide. +{{}} + +## Build the composite resource + +Follow these steps to create a composite resource that exposes connection +details: + +1. [Define](#define-the-schema) the schema of the composite resource +1. [Install](#install-the-function) the composition function you want to use +1. [Configure](#configure-the-composition) how the composition exposes + connection details + +After you complete these steps you can +[use the composite resource](#use-the-composite-resource). + +### Define the schema + +A CompositeResourceDefinition (XRD) defines composite resources. + +For this example, create an XRD for the `UserAccessKey` composite resource: + +```yaml +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: useraccesskeys.example.org +spec: + group: example.org + names: + kind: UserAccessKey + plural: useraccesskeys + scope: Namespaced + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + writeConnectionSecretToRef: + type: object + properties: + name: + type: string +``` + +{{}} +This XRD schema defines a `.spec.writeConnectionSecretToRef.name` field that +allows the user to optionally set the name for the XR connection details secret. + + +For a `Cluster` scoped XRD, a `.spec.writeConnectionSecretToRef.namespace` field +could also be added to allow the user to specify the namespace of the secret +too. + +{{}} + +Save the XRD as `xrd.yaml` and apply it: + +```shell +kubectl apply -f xrd.yaml +``` + +The Kubernetes API is now serving requests for the `UserAccessKey` composite +resource. + +### Install the function + +Composition functions provide general features to help you compose +resources and expose connection details. This guide shows how to compose +connection details with multiple functions. Pick the language you want to use +from the tabs below. + +{{< tabs >}} + +{{< tab "Templated YAML" >}} +Templated YAML is a good choice if you're used to writing +[Helm charts](https://helm.sh). + +Create this composition function to install templated YAML support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-go-templating +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.11.2 +``` + +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-go-templating True True xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.11.2 15s +``` +{{< /tab >}} + +{{< tab "Python" >}} + +Create this composition function to install Python support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-python +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-python:v0.2.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-python True True xpkg.crossplane.io/crossplane-contrib/function-python:v0.2.0 12s +``` +{{< /tab >}} + +{{< tab "KCL" >}} + +Create this composition function to install [KCL](https://kcl-lang.io) support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-kcl +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-kcl:v0.11.6 +``` + +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-kcl True True xpkg.crossplane.io/crossplane-contrib/function-kcl:v0.11.6 6s +``` +{{< /tab >}} + +{{< /tabs >}} + +This guide also uses `function-auto-ready`. This function automatically +marks composed resources as ready when they're healthy: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-auto-ready +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-auto-ready:v0.6.0 +``` + +Save this as `fn-auto-ready.yaml` and apply it: + +```shell +kubectl apply -f fn-auto-ready.yaml +``` + +### Configure the composition + +A Composition tells Crossplane how to compose resources for a composite +resource. This guide also includes a composed `Secret` resource to expose the +composite resource's connection details. + +The general pattern is: + +1. Composed resources write their connection details to individual secrets +2. The Composition reads those connection details when the function runs +3. The Composition creates a composed `Secret` representing the aggregated connection details for the XR + +{{}} +The composite resource's connection details secret can contain any data you want +and you can transform it as needed. + +You're not limited to connection details from managed resources - you can +include data from any composed resource, including arbitrary Kubernetes +resources like `ConfigMaps` or `Services`. +{{}} + +Create a Composition that exposes connection details for the `UserAccessKey` +composite resource. + +In this example, the Composition creates two `AccessKey` managed resources and +exposes their credentials as the composite resource's connection details `Secret`: + +{{< tabs >}} + +{{< tab "Templated YAML" >}} + +```yaml {label="comp-gotmpl"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-go-templating +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + --- + apiVersion: iam.aws.m.upbound.io/v1beta1 + kind: User + metadata: + annotations: + {{ setResourceNameAnnotation "user" }} + spec: + forProvider: {} + --- + apiVersion: iam.aws.m.upbound.io/v1beta1 + kind: AccessKey + metadata: + annotations: + {{ setResourceNameAnnotation "accesskey-0" }} + spec: + forProvider: + userSelector: + matchControllerRef: true + writeConnectionSecretToRef: + name: {{ $.observed.composite.resource.metadata.name }}-accesskey-secret-0 + --- + apiVersion: iam.aws.m.upbound.io/v1beta1 + kind: AccessKey + metadata: + annotations: + {{ setResourceNameAnnotation "accesskey-1" }} + spec: + forProvider: + userSelector: + matchControllerRef: true + writeConnectionSecretToRef: + name: {{ $.observed.composite.resource.metadata.name }}-accesskey-secret-1 + --- + apiVersion: v1 + kind: Secret + metadata: + name: {{ dig "spec" "writeConnectionSecretToRef" "name" "" $.observed.composite.resource}} + annotations: + {{ setResourceNameAnnotation "connection-secret" }} + {{ if eq $.observed.resources nil }} + data: {} + {{ else }} + data: + user-0: {{ ( index $.observed.resources "accesskey-0" ).connectionDetails.username }} + user-1: {{ ( index $.observed.resources "accesskey-1" ).connectionDetails.username }} + password-0: {{ ( index $.observed.resources "accesskey-0" ).connectionDetails.password }} + password-1: {{ ( index $.observed.resources "accesskey-1" ).connectionDetails.password }} + {{ end }} + - step: ready + functionRef: + name: function-auto-ready +``` + + + +**How this Composition exposes connection details:** + +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This + tells each AccessKey to write its credentials to an individual Secret. +* The Composition creates an explicit + {{}}Secret{{}} resource that + represents the composite resource's connection details. +* The {{}}name{{}} of the `Secret` is set using the +{{}}dig{{}} function to read the XR's + `.spec.writeConnectionSecretToRef.name` field if it exists. +* Crossplane observes the connection details from each `AccessKey` and makes them + available to the composition when the function runs. +* The Secret reads connection details via + {{}}$.observed.resources{{}} from + the observed composed resources. +* The {{}}{{ if eq $.observed.resources nil }}{{}} + check handles the initial phase when composed resources are still being created. +* In `function-go-templating`, connection details are **already base64-encoded**, so you + use them directly in the Secret's data field. + + + +{{< /tab >}} + +{{< tab "Python" >}} + +```yaml {label="comp-python"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-python +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-python + functionRef: + name: function-python + input: + apiVersion: python.fn.crossplane.io/v1beta1 + kind: Script + script: | + def compose(req, rsp): + # Get observed composite resource + oxr = req.observed.composite.resource + oxr_name = oxr["metadata"]["name"] + + # IAM User + rsp.desired.resources["user"].resource.update({ + "apiVersion": "iam.aws.m.upbound.io/v1beta1", + "kind": "User", + "spec": { + "forProvider": {} + } + }) + + # Access Key 0 + rsp.desired.resources["accesskey-0"].resource.update({ + "apiVersion": "iam.aws.m.upbound.io/v1beta1", + "kind": "AccessKey", + "spec": { + "forProvider": { + "userSelector": { + "matchControllerRef": True + } + }, + "writeConnectionSecretToRef": { + "name": f"{oxr_name}-accesskey-secret-0" + } + } + }) + + # Access Key 1 + rsp.desired.resources["accesskey-1"].resource.update({ + "apiVersion": "iam.aws.m.upbound.io/v1beta1", + "kind": "AccessKey", + "spec": { + "forProvider": { + "userSelector": { + "matchControllerRef": True + } + }, + "writeConnectionSecretToRef": { + "name": f"{oxr_name}-accesskey-secret-1" + } + } + }) + + # Secret representing the composite resource's connection details + secret_resource = { + "apiVersion": "v1", + "kind": "Secret", + "metadata": {} + } + + # If a secret name was provided then use it + secret_name = "" + if "writeConnectionSecretToRef" in oxr["spec"] and "name" in oxr["spec"]["writeConnectionSecretToRef"]: + secret_name = oxr["spec"]["writeConnectionSecretToRef"]["name"] + + secret_resource["metadata"]["name"] = secret_name + + # Only add data if we have connection details to populate + data = {} + if "accesskey-0" in req.observed.resources: + accesskey0_conn = req.observed.resources["accesskey-0"].connection_details + if "username" in accesskey0_conn: + data["user-0"] = accesskey0_conn["username"].decode("utf-8") + if "password" in accesskey0_conn: + data["password-0"] = accesskey0_conn["password"].decode("utf-8") + + if "accesskey-1" in req.observed.resources: + accesskey1_conn = req.observed.resources["accesskey-1"].connection_details + if "username" in accesskey1_conn: + data["user-1"] = accesskey1_conn["username"].decode("utf-8") + if "password" in accesskey1_conn: + data["password-1"] = accesskey1_conn["password"].decode("utf-8") + + if data: + secret_resource["stringData"] = data + + rsp.desired.resources["connection-secret"].resource.update(secret_resource) + - step: ready + functionRef: + name: function-auto-ready + +``` + + + +**How this Composition exposes connection details:** + +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This + tells each AccessKey to write its credentials to an individual Secret. +* The Composition creates an explicit + {{}}Secret{{}} resource that + represents the composite resource's connection details. +* The {{}}secret_name{{}} is set only after checking that the XR's + {{}}.spec.writeConnectionSecretToRef.name{{}} field exists. +* Crossplane observes the connection details from each AccessKey and makes them + available to the composition when the function runs. +* The Secret reads connection details via + {{}}req.observed.resources["accesskey-0"].connection_details{{}} + from the observed composed resources. +* The {{}}if "accesskey-0" in req.observed.resources{{}} + check handles the initial phase when composed resources are still being created. +* In `function-python`, connection details are **plaintext bytes**. To store them on the `Secret`, first + convert them to strings with {{}}.decode("utf-8"){{}} + and then save them using the secret's {{}}stringData{{}} field. + + + +{{< /tab >}} + +{{< tab "KCL" >}} + +```yaml {label="comp-kcl"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-kcl +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-kcl + functionRef: + name: function-kcl + input: + apiVersion: krm.kcl.dev/v1alpha1 + kind: KCLInput + spec: + source: | + oxr = option("params").oxr + ocds = option("params").ocds + + user = { + apiVersion = "iam.aws.m.upbound.io/v1beta1" + kind = "User" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "user" + } + spec.forProvider = {} + } + + accesskey0 = { + apiVersion = "iam.aws.m.upbound.io/v1beta1" + kind = "AccessKey" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "accesskey-0" + } + spec.forProvider.userSelector.matchControllerRef = True + spec.writeConnectionSecretToRef.name = "${oxr.metadata.name}-accesskey-secret-0" + } + + accesskey1 = { + apiVersion = "iam.aws.m.upbound.io/v1beta1" + kind = "AccessKey" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "accesskey-1" + } + spec.forProvider.userSelector.matchControllerRef = True + spec.writeConnectionSecretToRef.name = "${oxr.metadata.name}-accesskey-secret-1" + } + + secret = { + apiVersion = "v1" + kind = "Secret" + metadata.name = oxr?.spec?.writeConnectionSecretToRef?.name or "" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "connection-secret" + } + data = { + "user-0" = ocds["accesskey-0"]?.ConnectionDetails?.username or "" + "user-1" = ocds["accesskey-1"]?.ConnectionDetails?.username or "" + "password-0" = ocds["accesskey-0"]?.ConnectionDetails?.password or "" + "password-1" = ocds["accesskey-1"]?.ConnectionDetails?.password or "" + } if ocds else {} + } + + items = [user, accesskey0, accesskey1, secret] + - step: ready + functionRef: + name: function-auto-ready +``` + + +**How this Composition exposes connection details:** + +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This + tells each AccessKey to write its credentials to an individual Secret. +* The Composition creates an explicit + {{}}Secret{{}} resource that + represents the composite resource's connection details. +* The {{}}name{{}} of the `Secret` is set using + {{}}?.{{}} optional chaining operators to read the XR's + {{}}.spec.writeConnectionSecretToRef.name{{}} field if it exists. +* Crossplane observes the connection details from each + `AccessKey` and makes them available to the composition when the function runs. +* The Secret reads connection details via + {{}}ocds["accesskey-0"]?.ConnectionDetails?.username{{}} + from the observed composed resources, handling the case where connection details don't exist yet. +* The {{}}if ocds else {}{{}} handles + the phase when composed resources are still being created. +* In `function-kcl`, connection details are **already base64-encoded**, so you use them + directly in the Secret's data field. + + +{{< /tab >}} + +{{< /tabs >}} + +Save the composition as `composition.yaml` and apply it: + +```shell +kubectl apply -f composition.yaml +``` + +## Use the composite resource + +The Composition now specifies how to compose connection details for the +`UserAccessKey` composite resource. + +Create a `UserAccessKey` to see it in action: + +```yaml +apiVersion: example.org/v1alpha1 +kind: UserAccessKey +metadata: + namespace: default + name: my-keys +spec: + writeConnectionSecretToRef: + name: my-keys-connection-details +``` + +Save the composite resource as `my-keys.yaml` and apply it: + +```shell +kubectl apply -f my-keys.yaml +``` + +Check that the composite resource is ready: + +```shell {copy-lines="1"} +kubectl get -f my-keys.yaml +NAME SYNCED READY COMPOSITION AGE +my-keys True True useraccesskeys-go-templating 45s +``` + +{{}} +It may take a minute for AWS to provision the IAM resources. The composite +resource becomes `READY` when all composed resources are healthy. +{{}} + +## Verify the connection details + +Composite resources expose their connection details through a `Secret`. Check that +Crossplane created the `Secret`. + +View all the composed resources (including the connection details `Secret`) +together using the `crossplane` CLI. + +{{}} +See the [Crossplane CLI docs]({{}}) to +learn how to install and use the Crossplane CLI. +{{< /hint >}} + +```shell {copy-lines="1"} +crossplane beta trace useraccesskey.example.org/my-keys +NAME SYNCED READY STATUS +UserAccessKey/my-keys (default) True True Available +├─ AccessKey/my-keys-14c0578cad85 (default) True True Available +├─ AccessKey/my-keys-e420789d13a3 (default) True True Available +├─ User/my-keys-c63b530f8e68 (default) True True Available +└─ Secret/my-keys-connection-details (default) - - +``` + +The `my-keys` composite resource created an IAM `User` and two IAM `AccessKeys`, +and a `Secret` was also created that contains the aggregated connection details +for the composite resource. + +Check the composite resource's aggregated connection details `Secret`: + +```shell {copy-lines="1"} +kubectl get secret -n default -l crossplane.io/composite=my-keys +NAME TYPE DATA AGE +my-keys-586e2994bda1 Opaque 4 5m37s +``` + +{{}} +The composite resource's connection details Secret has a label +`crossplane.io/composite=my-keys` for convenient lookup. + +If you set `.spec.writeConnectionSecretToRef.name` on the XR, the `Secret` +has that exact name. +{{}} + +Verify the composite resource's connection details `Secret` contains all the +expected credentials: + +```shell +kubectl get secret -n default -l crossplane.io/composite=my-keys -o jsonpath='{.items[0].data}' | jq +``` + +You should see output like this: + +```json {copy-lines="none"} +{ + "password-0": "", + "password-1": "", + "user-0": "", + "user-1": "" +} +``` + +Decode one of the values to verify it contains the expected data: + +```shell +kubectl get secret -n default -l crossplane.io/composite=my-keys -o jsonpath='{.items[0].data.user-0}' | base64 -d +``` + +## Understanding how composing connection details works + +The basic steps to expose connection details for a composite resource are: + +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 in their own individual `Secret` should have their + `writeConnectionSecretToRef` set in the composition. + +3. **Observed connection details**: Crossplane observes the actual state of + each composed resource, including its connection details, and makes this data + available when it runs the function. + +4. **Compose the combined `Secret`**: With the observed connection details of + your composed resources in hand, compose a `Secret` resource that combines + the important connection details you want to expose for the XR. Consider + allowing the consumer of the XR to specify the name they want this secret to + have. + +5. **Handle transient state**: When your XR is first created, the + composed resources and/or their connection details may not exist yet. Your + Composition should handle these cases by checking if resources and + their connection details exist before accessing them. + + +## Troubleshooting + +### Composite resource's connection details Secret is empty + +**Causes:** + + +* Composed resources don't have `writeConnectionSecretToRef` set +* Composed resources aren't ready/healthy yet +* Not handling initial nil state correctly in the Composition + + + + +**Solutions**: + +* Verify `writeConnectionSecretToRef` is set on all composed managed resources +* Wait for composed resources to become ready (`kubectl get` and check the `READY` column) +* Verify the composed resource is actually producing connection details: + `kubectl get secret -o yaml` +* Add nil/empty checks in your Composition logic to safeguard access to data that may not exist yet + + + + + +### Connection details aren't encoded correctly + +**Cause:** Not encoding the combined secret data correctly in your Composition logic + +**Solution:** Ensure that your connection details data is correctly encoded for +the function you're using. 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. + + + +### Secret has an empty namespace + +**Cause:** Not setting the namespace of the `Secret` for a Cluster scoped XR, +resulting in an error message like `an empty namespace may not be set +when a resource name is provided` + +**Solution:** When Cluster scoped XRs compose namespace-scoped resources like a +`Secret`, you must explicitly set a namespace on the resource. Consider allowing +the XR consumer to specify the namespace value in your composition. Namespaced +XRs don't have this problem because Crossplane defaults any composed resource's +namespace to the XR's namespace if left empty. + +## Clean up + +Delete the composite resource to clean up: + +```shell +kubectl delete -f my-keys.yaml +``` + +When you delete the composite resource, Crossplane deletes: + +* The composed IAM `User` and `AccessKeys` from AWS +* The individual `Secrets` from composed resources +* The composite resource's connection details `Secret` + +{{}} +Make sure to delete your composite resources before uninstalling the provider +or shutting down your control plane. If those are no longer running, they can't +clean up composed resources and you would need to delete them manually. +{{}} + +## Learn more + +* [Composite resources]({{}}) +* [Compositions]({{}}) +* [Write a composition function in Go]({{}}) +* [Write a composition function in Python]({{}}) diff --git a/content/v2.1/guides/connection-details-composition.md b/content/v2.1/guides/connection-details-composition.md new file mode 100644 index 000000000..12ac3f973 --- /dev/null +++ b/content/v2.1/guides/connection-details-composition.md @@ -0,0 +1,858 @@ +--- +title: Connection Details Composition +weight: 83 +description: "Expose connection details for composite resources aggregated from their composed resources" +--- + + +This guide shows how to expose connection details for composite resources (XRs). +Because composite resources can compose multiple resources, the connection +details they expose are often an aggregate of the connection details from their +composed resources. + + +The recommended approach is to include a Kubernetes `Secret` +resource in your Composition that aggregates the connection details from other +resources and exposes them for the XR. + +{{}} +Crossplane v1 included a feature that automatically created connection details +for XRs. + +To learn more about how to specify XR connection details in Crossplane v1, please see the +[v1 connection details]({{}}) docs page. +{{}} + +## Example overview + +This guide shows how composite resources can expose connection details by +creating a `UserAccessKey` composite resource. This XR represents an AWS IAM user +with multiple access keys. + +When a user creates a `UserAccessKey`, Crossplane provisions an IAM User and two +AccessKeys in AWS. Each AccessKey produces their own connection details like a +username and password. The `UserAccessKey` also composes a `Secret` resource +that exposes the aggregated connection details of its composed resources, allowing +users and applications to consume them. + +An example `UserAccessKey` XR looks like this: + +```yaml {copy-lines="none"} +apiVersion: example.org/v1alpha1 +kind: UserAccessKey +metadata: + namespace: default + name: my-keys +``` + +**Behind the scenes, Crossplane:** + +1. Creates an AWS IAM `User` and two `AccessKeys` (the composed resources) +2. Collects connection details from both `AccessKeys` +3. Exposes them as the `UserAccessKey`'s connection details in a `Secret` + +The composite resource's connection details `Secret` looks like this: + +```yaml {copy-lines="none"} +apiVersion: v1 +kind: Secret +metadata: + namespace: default + name: my-keys-connection-details +data: + user-0: + password-0: + user-1: + password-1: +``` + +Users and applications can consume the `UserAccessKey` connection details by +reading this Secret. + +{{}} +The pattern in this guide applies to any composite resource that needs to expose connection +details, for example: + +* Database connection strings and credentials +* Cluster client certificate and key data +* Application endpoints from services and ingress +{{}} + +## Prerequisites + +This guide requires: + +* A Kubernetes cluster +* Crossplane [installed on the Kubernetes cluster]({{}}) +* `provider-aws-iam` installed and configured with credentials + +{{}} +To set up the AWS provider, follow the [Get Started with Managed Resources]({{}}) guide, +but use provider `provider-aws-iam:v2.3.0` instead. + +Complete the steps to install the provider and configure credentials, then +return to this guide. +{{}} + +## Build the composite resource + +Follow these steps to create a composite resource that exposes connection +details: + +1. [Define](#define-the-schema) the schema of the composite resource +1. [Install](#install-the-function) the composition function you want to use +1. [Configure](#configure-the-composition) how the composition exposes + connection details + +After you complete these steps you can +[use the composite resource](#use-the-composite-resource). + +### Define the schema + +A CompositeResourceDefinition (XRD) defines composite resources. + +For this example, create an XRD for the `UserAccessKey` composite resource: + +```yaml +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: useraccesskeys.example.org +spec: + group: example.org + names: + kind: UserAccessKey + plural: useraccesskeys + scope: Namespaced + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + writeConnectionSecretToRef: + type: object + properties: + name: + type: string +``` + +{{}} +This XRD schema defines a `.spec.writeConnectionSecretToRef.name` field that +allows the user to optionally set the name for the XR connection details secret. + + +For a `Cluster` scoped XRD, a `.spec.writeConnectionSecretToRef.namespace` field +could also be added to allow the user to specify the namespace of the secret +too. + +{{}} + +Save the XRD as `xrd.yaml` and apply it: + +```shell +kubectl apply -f xrd.yaml +``` + +The Kubernetes API is now serving requests for the `UserAccessKey` composite +resource. + +### Install the function + +Composition functions provide general features to help you compose +resources and expose connection details. This guide shows how to compose +connection details with multiple functions. Pick the language you want to use +from the tabs below. + +{{< tabs >}} + +{{< tab "Templated YAML" >}} +Templated YAML is a good choice if you're used to writing +[Helm charts](https://helm.sh). + +Create this composition function to install templated YAML support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-go-templating +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.11.2 +``` + +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-go-templating True True xpkg.crossplane.io/crossplane-contrib/function-go-templating:v0.11.2 15s +``` +{{< /tab >}} + +{{< tab "Python" >}} + +Create this composition function to install Python support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-python +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-python:v0.2.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-python True True xpkg.crossplane.io/crossplane-contrib/function-python:v0.2.0 12s +``` +{{< /tab >}} + +{{< tab "KCL" >}} + +Create this composition function to install [KCL](https://kcl-lang.io) support: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-kcl +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-kcl:v0.11.6 +``` + +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-kcl True True xpkg.crossplane.io/crossplane-contrib/function-kcl:v0.11.6 6s +``` +{{< /tab >}} + +{{< /tabs >}} + +This guide also uses `function-auto-ready`. This function automatically +marks composed resources as ready when they're healthy: + +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-auto-ready +spec: + package: xpkg.crossplane.io/crossplane-contrib/function-auto-ready:v0.6.0 +``` + +Save this as `fn-auto-ready.yaml` and apply it: + +```shell +kubectl apply -f fn-auto-ready.yaml +``` + +### Configure the composition + +A Composition tells Crossplane how to compose resources for a composite +resource. This guide also includes a composed `Secret` resource to expose the +composite resource's connection details. + +The general pattern is: + +1. Composed resources write their connection details to individual secrets +2. The Composition reads those connection details when the function runs +3. The Composition creates a composed `Secret` representing the aggregated connection details for the XR + +{{}} +The composite resource's connection details secret can contain any data you want +and you can transform it as needed. + +You're not limited to connection details from managed resources - you can +include data from any composed resource, including arbitrary Kubernetes +resources like `ConfigMaps` or `Services`. +{{}} + +Create a Composition that exposes connection details for the `UserAccessKey` +composite resource. + +In this example, the Composition creates two `AccessKey` managed resources and +exposes their credentials as the composite resource's connection details `Secret`: + +{{< tabs >}} + +{{< tab "Templated YAML" >}} + +```yaml {label="comp-gotmpl"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-go-templating +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplating.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + --- + apiVersion: iam.aws.m.upbound.io/v1beta1 + kind: User + metadata: + annotations: + {{ setResourceNameAnnotation "user" }} + spec: + forProvider: {} + --- + apiVersion: iam.aws.m.upbound.io/v1beta1 + kind: AccessKey + metadata: + annotations: + {{ setResourceNameAnnotation "accesskey-0" }} + spec: + forProvider: + userSelector: + matchControllerRef: true + writeConnectionSecretToRef: + name: {{ $.observed.composite.resource.metadata.name }}-accesskey-secret-0 + --- + apiVersion: iam.aws.m.upbound.io/v1beta1 + kind: AccessKey + metadata: + annotations: + {{ setResourceNameAnnotation "accesskey-1" }} + spec: + forProvider: + userSelector: + matchControllerRef: true + writeConnectionSecretToRef: + name: {{ $.observed.composite.resource.metadata.name }}-accesskey-secret-1 + --- + apiVersion: v1 + kind: Secret + metadata: + name: {{ dig "spec" "writeConnectionSecretToRef" "name" "" $.observed.composite.resource}} + annotations: + {{ setResourceNameAnnotation "connection-secret" }} + {{ if eq $.observed.resources nil }} + data: {} + {{ else }} + data: + user-0: {{ ( index $.observed.resources "accesskey-0" ).connectionDetails.username }} + user-1: {{ ( index $.observed.resources "accesskey-1" ).connectionDetails.username }} + password-0: {{ ( index $.observed.resources "accesskey-0" ).connectionDetails.password }} + password-1: {{ ( index $.observed.resources "accesskey-1" ).connectionDetails.password }} + {{ end }} + - step: ready + functionRef: + name: function-auto-ready +``` + + + +**How this Composition exposes connection details:** + +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This + tells each AccessKey to write its credentials to an individual Secret. +* The Composition creates an explicit + {{}}Secret{{}} resource that + represents the composite resource's connection details. +* The {{}}name{{}} of the `Secret` is set using the +{{}}dig{{}} function to read the XR's + `.spec.writeConnectionSecretToRef.name` field if it exists. +* Crossplane observes the connection details from each `AccessKey` and makes them + available to the composition when the function runs. +* The Secret reads connection details via + {{}}$.observed.resources{{}} from + the observed composed resources. +* The {{}}{{ if eq $.observed.resources nil }}{{}} + check handles the initial phase when composed resources are still being created. +* In `function-go-templating`, connection details are **already base64-encoded**, so you + use them directly in the Secret's data field. + + + +{{< /tab >}} + +{{< tab "Python" >}} + +```yaml {label="comp-python"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-python +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-python + functionRef: + name: function-python + input: + apiVersion: python.fn.crossplane.io/v1beta1 + kind: Script + script: | + def compose(req, rsp): + # Get observed composite resource + oxr = req.observed.composite.resource + oxr_name = oxr["metadata"]["name"] + + # IAM User + rsp.desired.resources["user"].resource.update({ + "apiVersion": "iam.aws.m.upbound.io/v1beta1", + "kind": "User", + "spec": { + "forProvider": {} + } + }) + + # Access Key 0 + rsp.desired.resources["accesskey-0"].resource.update({ + "apiVersion": "iam.aws.m.upbound.io/v1beta1", + "kind": "AccessKey", + "spec": { + "forProvider": { + "userSelector": { + "matchControllerRef": True + } + }, + "writeConnectionSecretToRef": { + "name": f"{oxr_name}-accesskey-secret-0" + } + } + }) + + # Access Key 1 + rsp.desired.resources["accesskey-1"].resource.update({ + "apiVersion": "iam.aws.m.upbound.io/v1beta1", + "kind": "AccessKey", + "spec": { + "forProvider": { + "userSelector": { + "matchControllerRef": True + } + }, + "writeConnectionSecretToRef": { + "name": f"{oxr_name}-accesskey-secret-1" + } + } + }) + + # Secret representing the composite resource's connection details + secret_resource = { + "apiVersion": "v1", + "kind": "Secret", + "metadata": {} + } + + # If a secret name was provided then use it + secret_name = "" + if "writeConnectionSecretToRef" in oxr["spec"] and "name" in oxr["spec"]["writeConnectionSecretToRef"]: + secret_name = oxr["spec"]["writeConnectionSecretToRef"]["name"] + + secret_resource["metadata"]["name"] = secret_name + + # Only add data if we have connection details to populate + data = {} + if "accesskey-0" in req.observed.resources: + accesskey0_conn = req.observed.resources["accesskey-0"].connection_details + if "username" in accesskey0_conn: + data["user-0"] = accesskey0_conn["username"].decode("utf-8") + if "password" in accesskey0_conn: + data["password-0"] = accesskey0_conn["password"].decode("utf-8") + + if "accesskey-1" in req.observed.resources: + accesskey1_conn = req.observed.resources["accesskey-1"].connection_details + if "username" in accesskey1_conn: + data["user-1"] = accesskey1_conn["username"].decode("utf-8") + if "password" in accesskey1_conn: + data["password-1"] = accesskey1_conn["password"].decode("utf-8") + + if data: + secret_resource["stringData"] = data + + rsp.desired.resources["connection-secret"].resource.update(secret_resource) + - step: ready + functionRef: + name: function-auto-ready + +``` + + + +**How this Composition exposes connection details:** + +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This + tells each AccessKey to write its credentials to an individual Secret. +* The Composition creates an explicit + {{}}Secret{{}} resource that + represents the composite resource's connection details. +* The {{}}secret_name{{}} is set only after checking that the XR's + {{}}.spec.writeConnectionSecretToRef.name{{}} field exists. +* Crossplane observes the connection details from each AccessKey and makes them + available to the composition when the function runs. +* The Secret reads connection details via + {{}}req.observed.resources["accesskey-0"].connection_details{{}} + from the observed composed resources. +* The {{}}if "accesskey-0" in req.observed.resources{{}} + check handles the initial phase when composed resources are still being created. +* In `function-python`, connection details are **plaintext bytes**. To store them on the `Secret`, first + convert them to strings with {{}}.decode("utf-8"){{}} + and then save them using the secret's {{}}stringData{{}} field. + + + +{{< /tab >}} + +{{< tab "KCL" >}} + +```yaml {label="comp-kcl"} +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-kcl +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-kcl + functionRef: + name: function-kcl + input: + apiVersion: krm.kcl.dev/v1alpha1 + kind: KCLInput + spec: + source: | + oxr = option("params").oxr + ocds = option("params").ocds + + user = { + apiVersion = "iam.aws.m.upbound.io/v1beta1" + kind = "User" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "user" + } + spec.forProvider = {} + } + + accesskey0 = { + apiVersion = "iam.aws.m.upbound.io/v1beta1" + kind = "AccessKey" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "accesskey-0" + } + spec.forProvider.userSelector.matchControllerRef = True + spec.writeConnectionSecretToRef.name = "${oxr.metadata.name}-accesskey-secret-0" + } + + accesskey1 = { + apiVersion = "iam.aws.m.upbound.io/v1beta1" + kind = "AccessKey" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "accesskey-1" + } + spec.forProvider.userSelector.matchControllerRef = True + spec.writeConnectionSecretToRef.name = "${oxr.metadata.name}-accesskey-secret-1" + } + + secret = { + apiVersion = "v1" + kind = "Secret" + metadata.name = oxr?.spec?.writeConnectionSecretToRef?.name or "" + metadata.annotations = { + "krm.kcl.dev/composition-resource-name" = "connection-secret" + } + data = { + "user-0" = ocds["accesskey-0"]?.ConnectionDetails?.username or "" + "user-1" = ocds["accesskey-1"]?.ConnectionDetails?.username or "" + "password-0" = ocds["accesskey-0"]?.ConnectionDetails?.password or "" + "password-1" = ocds["accesskey-1"]?.ConnectionDetails?.password or "" + } if ocds else {} + } + + items = [user, accesskey0, accesskey1, secret] + - step: ready + functionRef: + name: function-auto-ready +``` + + +**How this Composition exposes connection details:** + +* Each composed {{}}AccessKey{{}} has + {{}}writeConnectionSecretToRef{{}} set. This + tells each AccessKey to write its credentials to an individual Secret. +* The Composition creates an explicit + {{}}Secret{{}} resource that + represents the composite resource's connection details. +* The {{}}name{{}} of the `Secret` is set using + {{}}?.{{}} optional chaining operators to read the XR's + {{}}.spec.writeConnectionSecretToRef.name{{}} field if it exists. +* Crossplane observes the connection details from each + `AccessKey` and makes them available to the composition when the function runs. +* The Secret reads connection details via + {{}}ocds["accesskey-0"]?.ConnectionDetails?.username{{}} + from the observed composed resources, handling the case where connection details don't exist yet. +* The {{}}if ocds else {}{{}} handles + the phase when composed resources are still being created. +* In `function-kcl`, connection details are **already base64-encoded**, so you use them + directly in the Secret's data field. + + +{{< /tab >}} + +{{< /tabs >}} + +Save the composition as `composition.yaml` and apply it: + +```shell +kubectl apply -f composition.yaml +``` + +## Use the composite resource + +The Composition now specifies how to compose connection details for the +`UserAccessKey` composite resource. + +Create a `UserAccessKey` to see it in action: + +```yaml +apiVersion: example.org/v1alpha1 +kind: UserAccessKey +metadata: + namespace: default + name: my-keys +spec: + writeConnectionSecretToRef: + name: my-keys-connection-details +``` + +Save the composite resource as `my-keys.yaml` and apply it: + +```shell +kubectl apply -f my-keys.yaml +``` + +Check that the composite resource is ready: + +```shell {copy-lines="1"} +kubectl get -f my-keys.yaml +NAME SYNCED READY COMPOSITION AGE +my-keys True True useraccesskeys-go-templating 45s +``` + +{{}} +It may take a minute for AWS to provision the IAM resources. The composite +resource becomes `READY` when all composed resources are healthy. +{{}} + +## Verify the connection details + +Composite resources expose their connection details through a `Secret`. Check that +Crossplane created the `Secret`. + +View all the composed resources (including the connection details `Secret`) +together using the `crossplane` CLI. + +{{}} +See the [Crossplane CLI docs]({{}}) to +learn how to install and use the Crossplane CLI. +{{< /hint >}} + +```shell {copy-lines="1"} +crossplane beta trace useraccesskey.example.org/my-keys +NAME SYNCED READY STATUS +UserAccessKey/my-keys (default) True True Available +├─ AccessKey/my-keys-14c0578cad85 (default) True True Available +├─ AccessKey/my-keys-e420789d13a3 (default) True True Available +├─ User/my-keys-c63b530f8e68 (default) True True Available +└─ Secret/my-keys-connection-details (default) - - +``` + +The `my-keys` composite resource created an IAM `User` and two IAM `AccessKeys`, +and a `Secret` was also created that contains the aggregated connection details +for the composite resource. + +Check the composite resource's aggregated connection details `Secret`: + +```shell {copy-lines="1"} +kubectl get secret -n default -l crossplane.io/composite=my-keys +NAME TYPE DATA AGE +my-keys-586e2994bda1 Opaque 4 5m37s +``` + +{{}} +The composite resource's connection details Secret has a label +`crossplane.io/composite=my-keys` for convenient lookup. + +If you set `.spec.writeConnectionSecretToRef.name` on the XR, the `Secret` +has that exact name. +{{}} + +Verify the composite resource's connection details `Secret` contains all the +expected credentials: + +```shell +kubectl get secret -n default -l crossplane.io/composite=my-keys -o jsonpath='{.items[0].data}' | jq +``` + +You should see output like this: + +```json {copy-lines="none"} +{ + "password-0": "", + "password-1": "", + "user-0": "", + "user-1": "" +} +``` + +Decode one of the values to verify it contains the expected data: + +```shell +kubectl get secret -n default -l crossplane.io/composite=my-keys -o jsonpath='{.items[0].data.user-0}' | base64 -d +``` + +## Understanding how composing connection details works + +The basic steps to expose connection details for a composite resource are: + +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 in their own individual `Secret` should have their + `writeConnectionSecretToRef` set in the composition. + +3. **Observed connection details**: Crossplane observes the actual state of + each composed resource, including its connection details, and makes this data + available when it runs the function. + +4. **Compose the combined `Secret`**: With the observed connection details of + your composed resources in hand, compose a `Secret` resource that combines + the important connection details you want to expose for the XR. Consider + allowing the consumer of the XR to specify the name they want this secret to + have. + +5. **Handle transient state**: When your XR is first created, the + composed resources and/or their connection details may not exist yet. Your + Composition should handle these cases by checking if resources and + their connection details exist before accessing them. + + +## Troubleshooting + +### Composite resource's connection details Secret is empty + +**Causes:** + + +* Composed resources don't have `writeConnectionSecretToRef` set +* Composed resources aren't ready/healthy yet +* Not handling initial nil state correctly in the Composition + + + + +**Solutions**: + +* Verify `writeConnectionSecretToRef` is set on all composed managed resources +* Wait for composed resources to become ready (`kubectl get` and check the `READY` column) +* Verify the composed resource is actually producing connection details: + `kubectl get secret -o yaml` +* Add nil/empty checks in your Composition logic to safeguard access to data that may not exist yet + + + + + +### Connection details aren't encoded correctly + +**Cause:** Not encoding the combined secret data correctly in your Composition logic + +**Solution:** Ensure that your connection details data is correctly encoded for +the function you're using. 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. + + + +### Secret has an empty namespace + +**Cause:** Not setting the namespace of the `Secret` for a Cluster scoped XR, +resulting in an error message like `an empty namespace may not be set +when a resource name is provided` + +**Solution:** When Cluster scoped XRs compose namespace-scoped resources like a +`Secret`, you must explicitly set a namespace on the resource. Consider allowing +the XR consumer to specify the namespace value in your composition. Namespaced +XRs don't have this problem because Crossplane defaults any composed resource's +namespace to the XR's namespace if left empty. + +## Clean up + +Delete the composite resource to clean up: + +```shell +kubectl delete -f my-keys.yaml +``` + +When you delete the composite resource, Crossplane deletes: + +* The composed IAM `User` and `AccessKeys` from AWS +* The individual `Secrets` from composed resources +* The composite resource's connection details `Secret` + +{{}} +Make sure to delete your composite resources before uninstalling the provider +or shutting down your control plane. If those are no longer running, they can't +clean up composed resources and you would need to delete them manually. +{{}} + +## Learn more + +* [Composite resources]({{}}) +* [Compositions]({{}}) +* [Write a composition function in Go]({{}}) +* [Write a composition function in Python]({{}})