|
| 1 | +# composite.py Reference |
| 2 | + |
| 3 | +`composite.py` defines the primary authoring model for function-pythonic. It provides: |
| 4 | + |
| 5 | +- `BaseComposite`: the base class your composition classes subclass. |
| 6 | +- Resource wrappers for composed and required resources. |
| 7 | +- Condition and result helpers. |
| 8 | +- Connection secret and connection-detail handling. |
| 9 | +- Convenience descriptors for TTL and ready state. |
| 10 | + |
| 11 | +## High-Level Model |
| 12 | + |
| 13 | +At runtime, function-pythonic instantiates your `BaseComposite` subclass with a |
| 14 | +`RunFunctionRequest`, then your `compose()` implementation mutates desired state via |
| 15 | +wrapper objects. |
| 16 | + |
| 17 | +Key object graph: |
| 18 | +- `self.request`: wrapped incoming request (read-oriented). |
| 19 | +- `self.response`: wrapped outgoing response (write-oriented). |
| 20 | +- `self.resources`: composed resource collection (`desired.resources`). |
| 21 | +- `self.requireds`: required/extra resource selectors and resolved items. |
| 22 | +- `self.conditions`: composite condition helpers. |
| 23 | +- `self.results`: function result helpers. |
| 24 | + |
| 25 | +## Core Descriptors |
| 26 | + |
| 27 | +## `TTL` |
| 28 | + |
| 29 | +Exposed as `BaseComposite.ttl`. |
| 30 | + |
| 31 | +- Getter reads `response.meta.ttl` and returns `int` or fractional `float`. |
| 32 | +- Setter accepts `int` or `float` and maps into `seconds`/`nanos`. |
| 33 | +- Invalid type raises `ValueError`. |
| 34 | + |
| 35 | +## `Ready` |
| 36 | + |
| 37 | +Exposed as `BaseComposite.ready`. |
| 38 | + |
| 39 | +- Getter maps protobuf ready enum to `True`, `False`, or `None`. |
| 40 | +- Setter writes protobuf ready enum: |
| 41 | + - `True` -> `READY_TRUE` |
| 42 | + - `False` -> `READY_FALSE` |
| 43 | + - `None` -> `READY_UNSPECIFIED` |
| 44 | + |
| 45 | +## `ConnectionSecret` |
| 46 | + |
| 47 | +Exposed as `BaseComposite.connectionSecret`. |
| 48 | + |
| 49 | +- Crossplane v1: |
| 50 | + - Reads from `spec.writeConnectionSecretToRef`. |
| 51 | + - Setting to a different value raises `NotImplementedError`. |
| 52 | +- Crossplane v2: |
| 53 | + - Reads/writes `input.writeConnectionSecretToRef` as a protobuf `Map` wrapper. |
| 54 | + |
| 55 | +## `Connection` |
| 56 | + |
| 57 | +Exposed as `BaseComposite.connection`; returns `_Connection`. |
| 58 | + |
| 59 | +- Manages `desired.composite.connection_details`. |
| 60 | +- In Crossplane v2, mirrors connection details into a composed `v1/Secret`. |
| 61 | +- Ignores non-string/unknown/`None` values when appropriate. |
| 62 | + |
| 63 | +## `BaseComposite` |
| 64 | + |
| 65 | +Subclass this class and implement: |
| 66 | + |
| 67 | +```python |
| 68 | +class MyComposite(BaseComposite): |
| 69 | + def compose(self): |
| 70 | + ... |
| 71 | +``` |
| 72 | + |
| 73 | +or async: |
| 74 | + |
| 75 | +```python |
| 76 | +class MyComposite(BaseComposite): |
| 77 | + async def compose(self): |
| 78 | + ... |
| 79 | +``` |
| 80 | + |
| 81 | +Initialization behavior: |
| 82 | +- Creates `self.request` (`protobuf.Message`) from input request. |
| 83 | +- Creates `self.response` with: |
| 84 | + - copied `meta.tag` |
| 85 | + - default TTL of 60s |
| 86 | + - cloned `desired` and `context` from request |
| 87 | +- Selects `self.parameters` from: |
| 88 | + - `observed.composite.resource.spec.parameters` for single-use composites |
| 89 | + - otherwise `input.parameters` |
| 90 | +- Initializes: |
| 91 | + - `self.credentials` |
| 92 | + - `self.context` |
| 93 | + - `self.environment` (`apiextensions.crossplane.io/environment`) |
| 94 | + - `self.requireds` |
| 95 | + - `self.resources` |
| 96 | + - defaults: `autoReady=True`, `usages=False`, `unknownsFatal=False` |
| 97 | +- Binds composite-focused shortcuts: |
| 98 | + - `self.observed`, `self.desired`, `self.apiVersion`, `self.kind`, |
| 99 | + `self.metadata`, `self.spec`, `self.status` |
| 100 | + - `self.conditions`, `self.results`, `self.events` (`events` is deprecated alias) |
| 101 | + |
| 102 | +`compose()` in `BaseComposite` is abstract and raises `NotImplementedError`. |
| 103 | + |
| 104 | +## Credentials API |
| 105 | + |
| 106 | +## `Credentials` |
| 107 | + |
| 108 | +Collection wrapper around `request.credentials`. |
| 109 | + |
| 110 | +- Access: `self.credentials['name']` or `self.credentials.name` |
| 111 | +- Supports `bool`, `len`, `contains`, and iteration of `(name, Credential)` |
| 112 | + |
| 113 | +## `Credential` |
| 114 | + |
| 115 | +Wrapper around a single credential’s `credential_data.data`. |
| 116 | + |
| 117 | +- Access: `credential['key']` or `credential.key` |
| 118 | +- Supports `bool`, `len`, `contains`, and iteration of `(key, value)` |
| 119 | + |
| 120 | +## Composed Resource API |
| 121 | + |
| 122 | +## `Resources` |
| 123 | + |
| 124 | +Collection wrapper for `response.desired.resources`. |
| 125 | + |
| 126 | +- Access/create by name: `self.resources.vpc` or `self.resources['vpc']` |
| 127 | +- Assign raw resource object: `self.resources['x'] = resource` |
| 128 | +- Delete by name. |
| 129 | +- Iteration yields `(name, Resource)` |
| 130 | + |
| 131 | +Instances are cached by composition resource name. |
| 132 | + |
| 133 | +## `Resource` |
| 134 | + |
| 135 | +Represents one composed resource entry. |
| 136 | + |
| 137 | +Construction binds: |
| 138 | +- `observed` = `request.observed.resources[name].resource` |
| 139 | +- `desired` = `response.desired.resources[name].resource` |
| 140 | +- `conditions`, `connection` |
| 141 | +- toggles: `autoReady`, `usages`, `unknownsFatal` (default `None`, inherit from composite) |
| 142 | + |
| 143 | +### `Resource(...)` call semantics |
| 144 | + |
| 145 | +`resource(kind?, apiVersion?, namespace?, name?)`: |
| 146 | +- Clears desired resource (`self.desired()`). |
| 147 | +- Supports swapped first two args when first arg looks like `apiVersion`. |
| 148 | +- Optionally sets `metadata.namespace` and `metadata.name`. |
| 149 | +- Returns `self`. |
| 150 | + |
| 151 | +### Key properties |
| 152 | + |
| 153 | +- `apiVersion`, `kind`, `metadata`, `spec`, `type`, `data` map to desired resource. |
| 154 | +- `status` is read from observed resource status. |
| 155 | +- `externalName` reads desired annotation, falls back to observed annotation, and |
| 156 | + writes back into desired. |
| 157 | + |
| 158 | +### Ready behavior |
| 159 | + |
| 160 | +- `ready` getter: |
| 161 | + - returns cached explicit value if set |
| 162 | + - otherwise maps desired ready enum |
| 163 | + - if unspecified and auto-ready is enabled, computes via `auto_ready.resource_ready()` |
| 164 | + and sets desired ready true when determined. |
| 165 | +- `ready` setter writes ready enum similarly to composite-level setter. |
| 166 | + |
| 167 | +### Methods |
| 168 | + |
| 169 | +- `setReadyCondition(type='Ready')`: |
| 170 | + - reads `conditions[type]` |
| 171 | + - if condition true -> `ready` set to observed name |
| 172 | + - else sets `ready` to a `status.not<type>Condition...` path |
| 173 | +- `addDependency(resource, field=_notset)`: |
| 174 | + - Adds explicit dependency annotation: |
| 175 | + `metadata.annotations["pythonic.dependency/<resource.name>"] = <field>` |
| 176 | + - If field omitted, derives from dependent resource readiness. |
| 177 | + - Converts non-wrapper truthy/falsey values into observed-name or status-based tokens. |
| 178 | + |
| 179 | +## Required Resource API |
| 180 | + |
| 181 | +## `Requireds` |
| 182 | + |
| 183 | +Collection wrapper for required resources. |
| 184 | + |
| 185 | +- Crossplane v1 uses `extra_resources`. |
| 186 | +- Crossplane v2 uses `required_resources`. |
| 187 | +- Supports `bool`, `len`, `contains`, iteration of `(name, RequiredResources)`. |
| 188 | + |
| 189 | +## `RequiredResources` |
| 190 | + |
| 191 | +Selector + resolved-items wrapper for a named required resource request. |
| 192 | + |
| 193 | +- Callable reset/update: |
| 194 | + - `required(kind?, apiVersion?, namespace?, name?, labels?)` |
| 195 | +- Selector properties: |
| 196 | + - `apiVersion`, `kind`, `namespace`, `matchName`, `matchLabels` |
| 197 | +- Resolved results: |
| 198 | + - index access `required[ix] -> RequiredResource` |
| 199 | + - `bool`, `len`, iteration |
| 200 | + |
| 201 | +`matchLabels` setter accepts mapping-like iteration or `(key, value)` pairs. |
| 202 | + |
| 203 | +## `RequiredResource` |
| 204 | + |
| 205 | +Read-only wrapper for one returned required resource item. |
| 206 | + |
| 207 | +Fields: |
| 208 | +- `name`, `ix` |
| 209 | +- `observed`, `apiVersion`, `kind`, `metadata`, `spec`, `type`, `data`, `status` |
| 210 | +- `conditions`, `connection` |
| 211 | + |
| 212 | +Truthiness reflects existence of `observed`. |
| 213 | + |
| 214 | +## Conditions API |
| 215 | + |
| 216 | +## `Conditions` |
| 217 | + |
| 218 | +Wrapper for condition access on composite/resource/required resource. |
| 219 | + |
| 220 | +- Access by type: `conditions['Ready']` or `conditions.Ready` |
| 221 | +- Supports `bool`, `len`, iteration |
| 222 | +- Merges condition types from observed and response (when response exists) |
| 223 | + |
| 224 | +## `Condition` |
| 225 | + |
| 226 | +Represents one condition type. Subclasses `protobuf.ProtobufValue`. |
| 227 | + |
| 228 | +`_protobuf_value` serialization includes: |
| 229 | +- `type`, `status`, `reason`, `message`, optional RFC3339 `lastTransitionTime`. |
| 230 | + |
| 231 | +Mutation: |
| 232 | +- `condition(reason=?, message=?, status=?, claim=?)` updates in one call. |
| 233 | +- `status` maps between bool/unknown and protobuf status enums. |
| 234 | +- `reason`, `message` read/write strings. |
| 235 | +- `lastTransitionTime` reads from observed condition timestamp (read-only). |
| 236 | +- `claim` toggles target: |
| 237 | + - composite only |
| 238 | + - composite + claim |
| 239 | + - unspecified |
| 240 | + |
| 241 | +Creation rules: |
| 242 | +- If response exists, setting fields creates condition entry when missing. |
| 243 | +- Without response, creation is disallowed (`ValueError: Condition is read only`). |
| 244 | + |
| 245 | +## Results API |
| 246 | + |
| 247 | +## `Results` |
| 248 | + |
| 249 | +Wrapper for `response.results`. |
| 250 | + |
| 251 | +Factory methods: |
| 252 | +- `info(reason?, message?, claim?)` |
| 253 | +- `warning(reason?, message?, claim?)` |
| 254 | +- `fatal(reason?, message?, claim?)` |
| 255 | + |
| 256 | +Also supports `bool`, `len`, index, and iteration. |
| 257 | + |
| 258 | +## `Result` |
| 259 | + |
| 260 | +Wrapper around one result entry. |
| 261 | + |
| 262 | +Properties: |
| 263 | +- severity flags: `info`, `warning`, `fatal` |
| 264 | +- `reason` |
| 265 | +- `message` |
| 266 | +- `claim` target (composite vs composite+claim vs unspecified) |
| 267 | + |
| 268 | +A `Result()` without backing entry is falsey and ignores setters. |
| 269 | + |
| 270 | +## `_Connection` |
| 271 | + |
| 272 | +Internal helper returned by `BaseComposite.connection`. |
| 273 | + |
| 274 | +Behavior: |
| 275 | +- Reads/writes composite connection details map. |
| 276 | +- `observed` exposes observed connection details. |
| 277 | +- `__call__(**kwargs)` clears and resets connection details. |
| 278 | +- On v2, keeps a composed secret resource synchronized: |
| 279 | + - secret name defaults to `connection-secret` or `connectionSecret.resourceName` |
| 280 | + - sets type `connection.crossplane.io/v1alpha1` |
| 281 | + - base64-encodes values into `secret.data` |
| 282 | + - removes secret when last key removed |
| 283 | +- For cluster-scoped XR without secret namespace, emits fatal result: |
| 284 | + - reason: `ConnectionNoNamespace` |
| 285 | + - message: `Cluster scoped XR must specify connection secret namespace` |
| 286 | + |
| 287 | +## Minimal Example |
| 288 | + |
| 289 | +```python |
| 290 | +from crossplane.pythonic import BaseComposite |
| 291 | + |
| 292 | +class ExampleComposite(BaseComposite): |
| 293 | + def compose(self): |
| 294 | + bucket = self.resources.bucket("Bucket", "s3.aws.crossplane.io/v1beta1") |
| 295 | + bucket.spec.forProvider.region = self.spec.region |
| 296 | + |
| 297 | + # propagate observed id when available |
| 298 | + self.status.bucketId = bucket.status.atProvider.id |
| 299 | + |
| 300 | + # optional condition/result |
| 301 | + self.conditions.Ready(status=True, reason="Available", message="Composed") |
| 302 | + self.results.info("Composed", "Bucket desired state generated") |
| 303 | +``` |
| 304 | + |
| 305 | +## Crossplane v1 vs v2 Differences in This Module |
| 306 | + |
| 307 | +- Required resources: |
| 308 | + - v1: `extra_resources` |
| 309 | + - v2: `required_resources` |
| 310 | +- Connection secret handling: |
| 311 | + - v1: relies on XR `writeConnectionSecretToRef`, no write-through override |
| 312 | + - v2: allows `connectionSecret` overrides and manages composed secret mirror |
0 commit comments