Skip to content

Commit 5a708cd

Browse files
committed
Implement support for connection information during function-pythonic render
Signed-off-by: Patrick J. McNerthney <[email protected]>
1 parent 7270768 commit 5a708cd

File tree

13 files changed

+291
-86
lines changed

13 files changed

+291
-86
lines changed

README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,9 @@ The BaseComposite class provides the following fields for manipulating the Compo
204204
| self.status | Map | The composite desired and observed status, read from observed if not in desired |
205205
| self.conditions | Conditions | The composite desired and observed conditions, read from observed if not in desired |
206206
| self.results | Results | Returned results applied to the Composite and optionally on the Claim |
207-
| self.connection | Connection | The composite desired and observed connection detials, read from observed if not in desired |
207+
| self.connection | Map | The composite desired connection detials |
208208
| self.ready | Boolean | The composite desired ready state |
209+
| self.observed.connection | Map | The composite observed connection detials |
209210

210211
The BaseComposite also provides access to the following Crossplane Function level features:
211212

@@ -246,7 +247,7 @@ Resource class:
246247
| Resource.data | Map | The resource data |
247248
| Resource.status | Map | The resource status |
248249
| Resource.conditions | Conditions | The resource conditions |
249-
| Resource.connection | Connection | The resource connection details |
250+
| Resource.connection | Map | The resource observed connection details |
250251
| Resource.ready | Boolean | The resource ready state |
251252
| Resource.unknownsFatal | Boolean | Terminate the composition if this resource has been created and is assigned unknown values, default is Composite.unknownsFatal |
252253
| Resource.usages | Boolean | Generate Crossplane Usages for this resource, default is Composite.autoReady |
@@ -286,6 +287,7 @@ Each resource in the list is the following RequiredResource class:
286287
| RequiredResource.data | Map | The required resource data |
287288
| RequiredResource.status | Map | The required resource status |
288289
| RequiredResource.conditions | Map | The required resource conditions |
290+
| RequiredResource.connection | Map | The required resource connection details |
289291

290292
### Conditions
291293

@@ -348,11 +350,11 @@ $ pip install crossplane-function-pythonic
348350
Then to render function-pythonic Compositions, use the `function-pythonic render ...`
349351
command.
350352
```shell
351-
$ function-pythonic render --help
353+
$ function-pythonic render -h
352354
usage: Crossplane Function Pythonic render [-h] [--debug] [--log-name-width WIDTH] [--python-path DIRECTORY] [--render-unknowns]
353355
[--allow-oversize-protos] [--context-files KEY=PATH] [--context-values KEY=VALUE]
354-
[--observed-resources PATH] [--extra-resources PATH] [--required-resources PATH]
355-
[--function-credentials PATH] [--include-full-xr] [--include-function-results] [--include-context]
356+
[--observed-resources PATH] [--required-resources PATH] [--secret-store PATH] [--include-full-xr]
357+
[--include-connection-xr] [--include-function-results] [--include-context]
356358
PATH [PATH/CLASS]
357359
358360
positional arguments:
@@ -376,14 +378,14 @@ options:
376378
Context key-value pairs to pass to the Function pipeline. Values must be YAML/JSON. Keys take precedence over --context-files.
377379
--observed-resources, -o PATH
378380
A YAML file or directory of YAML files specifying the observed state of composed resources.
379-
--extra-resources PATH
380-
A YAML file or directory of YAML files specifying required resources (deprecated, use --required-resources).
381381
--required-resources, -e PATH
382382
A YAML file or directory of YAML files specifying required resources to pass to the Function pipeline.
383-
--function-credentials PATH
384-
A YAML file or directory of YAML files specifying credentials to use for Functions to render the XR.
383+
--secret-store, -s PATH
384+
A YAML file or directory of YAML files specifying Secrets to use to resolve connections and credentials.
385385
--include-full-xr, -x
386386
Include a direct copy of the input XR's spedc and metadata fields in the rendered output.
387+
--include-connection-xr
388+
Include the Composite connection values in the rendered output as a resource of kind: Connection.
387389
--include-function-results, -r
388390
Include informational and warning messages from Functions in the rendered output as resources of kind: Result..
389391
--include-context, -c

crossplane/pythonic/composite.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def __init__(self, request, single_use, logger):
4040
observed = self.request.observed.composite
4141
desired = self.response.desired.composite
4242
self.observed = observed.resource
43+
self.observed._set_attribute('connection', self.request.observed.composite.connection_details)
4344
self.desired = desired.resource
4445
self.apiVersion = self.observed.apiVersion
4546
self.kind = self.observed.kind
@@ -434,6 +435,7 @@ def __init__(self, name, ix, resource):
434435
self.data = self.observed.data
435436
self.status = self.observed.status
436437
self.conditions = Conditions(resource)
438+
self.connection = self.observed.connection_details
437439

438440
def __bool__(self):
439441
return bool(self.observed)

crossplane/pythonic/protobuf.py

Lines changed: 99 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,21 @@ def B64Decode(string):
5858

5959
class Message:
6060
def __init__(self, parent, key, descriptor, message=_Unknown, readOnly=False):
61-
self.__dict__['_parent'] = parent
62-
self.__dict__['_key'] = key
63-
self.__dict__['_descriptor'] = descriptor
64-
self.__dict__['_message'] = message
65-
self.__dict__['_readOnly'] = readOnly
66-
self.__dict__['_cache'] = {}
61+
self._set_attribute('_parent', parent)
62+
self._set_attribute('_key', key)
63+
self._set_attribute('_descriptor', descriptor)
64+
self._set_attribute('_message', message)
65+
self._set_attribute('_readOnly', readOnly)
66+
self._set_attribute('_cache', {})
67+
68+
def _set_attribute(self, key, value):
69+
self.__dict__[key] = value
6770

6871
def __getattr__(self, key):
6972
return self[key]
7073

7174
def __getitem__(self, key):
75+
key = self._validate_key(key)
7276
if key in self._cache:
7377
return self._cache[key]
7478
field = self._descriptor.fields_by_name.get(key)
@@ -156,6 +160,7 @@ def _fullName(self, key=None):
156160
def _create_child(self, key):
157161
if self._readOnly:
158162
raise ValueError(f"{self._readOnly} is read only")
163+
key = self._validate_key(key)
159164
if self._message is _Unknown:
160165
self.__dict__['_message'] = self._parent._create_child(self._key)
161166
return getattr(self._message, key)
@@ -177,6 +182,7 @@ def __setattr__(self, key, value):
177182
def __setitem__(self, key, value):
178183
if self._readOnly:
179184
raise ValueError(f"{self._readOnly} is read only")
185+
key = self._validate_key(key)
180186
if key not in self._descriptor.fields_by_name:
181187
raise AttributeError(obj=self, name=key)
182188
field = self._descriptor.fields_by_name[key]
@@ -201,26 +207,40 @@ def __delattr__(self, key):
201207
def __delitem__(self, key):
202208
if self._readOnly:
203209
raise ValueError(f"{self._readOnly} is read only")
210+
key = self._validate_key(key)
204211
if key not in self._descriptor.fields_by_name:
205212
raise AttributeError(obj=self, name=key)
206213
if self._message is not _Unknown:
207214
self._message.ClearField(key)
208215
self._cache.pop(key, None)
209216

217+
def _validate_key(self, key):
218+
if isinstance(key, FieldMessage):
219+
key = key._value
220+
elif isinstance(key, Value):
221+
key = key._raw
222+
if not isinstance(key, str):
223+
raise TypeError(f"Unexpected key type: {key.__class__}")
224+
return key
225+
210226

211227
class MapMessage:
212228
def __init__(self, parent, key, field, messages=_Unknown, readOnly=False):
213-
self.__dict__['_parent'] = parent
214-
self.__dict__['_key'] = key
215-
self.__dict__['_field'] = field
216-
self.__dict__['_messages'] = messages
217-
self.__dict__['_readOnly'] = readOnly
218-
self.__dict__['_cache'] = {}
229+
self._set_attribute('_parent', parent)
230+
self._set_attribute('_key', key)
231+
self._set_attribute('_field', field)
232+
self._set_attribute('_messages', messages)
233+
self._set_attribute('_readOnly', readOnly)
234+
self._set_attribute('_cache', {})
235+
236+
def _set_attribute(self, key, value):
237+
self.__dict__[key] = value
219238

220239
def __getattr__(self, key):
221240
return self[key]
222241

223242
def __getitem__(self, key):
243+
key = self._validate_key(key)
224244
if key in self._cache:
225245
return self._cache[key]
226246
if self._messages is _Unknown or key not in self._messages:
@@ -304,6 +324,7 @@ def _fullName(self, key=None):
304324
def _create_child(self, key):
305325
if self._readOnly:
306326
raise ValueError(f"{self._readOnly} is read only")
327+
key = self._validate_key(key)
307328
if self._messages is _Unknown:
308329
self.__dict__['_messages'] = self._parent._create_child(self._key)
309330
return self._messages[key]
@@ -325,6 +346,7 @@ def __setattr__(self, key, message):
325346
def __setitem__(self, key, message):
326347
if self._readOnly:
327348
raise ValueError(f"{self._readOnly} is read only")
349+
key = self._validate_key(key)
328350
if self._messages is _Unknown:
329351
self._messages = self._parent._create_child(self._key)
330352
if isinstance(message, Message):
@@ -349,11 +371,21 @@ def __delattr__(self, key):
349371
def __delitem__(self, key):
350372
if self._readOnly:
351373
raise ValueError(f"{self._readOnly} is read only")
374+
key = self._validate_key(key)
352375
if self._messages is not _Unknown:
353376
if key in self._messages:
354377
del self._messages[key]
355378
self._cache.pop(key, None)
356379

380+
def _validate_key(self, key):
381+
if isinstance(key, FieldMessage):
382+
key = key._value
383+
elif isinstance(key, Value):
384+
key = key._raw
385+
if not isinstance(key, str):
386+
raise TypeError(f"Unexpected key type: {key.__class__}")
387+
return key
388+
357389

358390
class RepeatedMessage:
359391
def __init__(self, parent, key, field, messages=_Unknown, readOnly=False):
@@ -365,6 +397,7 @@ def __init__(self, parent, key, field, messages=_Unknown, readOnly=False):
365397
self._cache = {}
366398

367399
def __getitem__(self, key):
400+
key = self._validate_key(key)
368401
if key in self._cache:
369402
return self._cache[key]
370403
if self._messages is _Unknown or key >= len(self._messages):
@@ -447,6 +480,7 @@ def _fullName(self, key=None):
447480
def _create_child(self, key):
448481
if self._readOnly:
449482
raise ValueError(f"{self._readOnly} is read only")
483+
key = self._validate_key(key)
450484
if self._messages is _Unknown:
451485
self.__dict__['_messages'] = self._parent._create_child(self._key)
452486
if key == append:
@@ -471,6 +505,7 @@ def __call__(self, *args):
471505
def __setitem__(self, key, message):
472506
if self._readOnly:
473507
raise ValueError(f"{self._readOnly} is read only")
508+
key = self._validate_key(key)
474509
if self._messages is _Unknown:
475510
self._messages = self._parent._create_child(self._key)
476511
if key < 0:
@@ -499,6 +534,7 @@ def __setitem__(self, key, message):
499534
def __delitem__(self, key):
500535
if self._readOnly:
501536
raise ValueError(f"{self._readOnly} is read only")
537+
key = self._validate_key(key)
502538
if self._messages is not _Unknown:
503539
del self._messages[key]
504540
self._cache.pop(key, None)
@@ -514,13 +550,22 @@ def append(self, message=_Unknown):
514550
message = self._messages.append(message)
515551
return self[len(self._messages) - 1]
516552

553+
def _validate_key(self, key):
554+
if isinstance(key, FieldMessage):
555+
key = key._value
556+
elif isinstance(key, Value):
557+
key = key._raw
558+
if not isinstance(key, int):
559+
raise TypeError(f"Unexpected key type: {key.__class__}")
560+
return key
561+
517562

518563
class FieldMessage:
519564
def __init__(self, parent, key, kind, value):
520-
self.__dict__['_parent'] = parent
521-
self.__dict__['_key'] = key
522-
self.__dict__['_kind'] = kind
523-
self.__dict__['_value'] = value
565+
self._parent = parent
566+
self._key = key
567+
self._kind = kind
568+
self._value = value
524569

525570
def __bool__(self):
526571
return bool(self._value)
@@ -539,7 +584,14 @@ def __eq__(self, other):
539584
return self._value == other._value
540585
return self._value == other
541586

587+
def __bytes__(self):
588+
if isinstance(self._value, str):
589+
return self._value.encode('utf-8')
590+
return bytes(self._value)
591+
542592
def __str__(self):
593+
if isinstance(self._value, bytes):
594+
return self._value.decode('utf-8')
543595
return str(self._value)
544596

545597
def __format__(self, spec=''):
@@ -576,16 +628,16 @@ def _protobuf_value(self):
576628

577629
class Value:
578630
def __init__(self, parent, key, value=_Unknown, readOnly=None):
579-
self.__dict__['_parent'] = parent
580-
self.__dict__['_key'] = key
581-
self.__dict__['_dependencies'] = {}
582-
self.__dict__['_unknowns'] = {}
583-
self.__dict__['_cache'] = {}
584-
self.__dict__['_readOnly'] = None
631+
self._set_attribute('_parent', parent)
632+
self._set_attribute('_key', key)
633+
self._set_attribute('_dependencies', {})
634+
self._set_attribute('_unknowns', {})
635+
self._set_attribute('_cache', {})
636+
self._set_attribute('_readOnly', None)
585637
if isinstance(value, (google.protobuf.struct_pb2.Value, google.protobuf.struct_pb2.Struct, google.protobuf.struct_pb2.ListValue)) or value is _Unknown:
586-
self.__dict__['_value'] = value
638+
self._set_attribute('_value', value)
587639
else:
588-
self.__dict__['_value'] = google.protobuf.struct_pb2.Value()
640+
self._set_attribute('_value', google.protobuf.struct_pb2.Value())
589641
if value is None:
590642
self._value.null_value = 0
591643
elif isinstance(value, dict):
@@ -604,12 +656,16 @@ def __init__(self, parent, key, value=_Unknown, readOnly=None):
604656
self._value.string_value = value
605657
else:
606658
raise ValueError(f"Unexpected Value type: {value.__class__}")
607-
self.__dict__['_readOnly'] = readOnly
659+
self._set_attribute('_readOnly', readOnly)
660+
661+
def _set_attribute(self, key, value):
662+
self.__dict__[key] = value
608663

609664
def __getattr__(self, key):
610665
return self[key]
611666

612667
def __getitem__(self, key):
668+
key = self._validate_key(key)
613669
if key in self._cache:
614670
return self._cache[key]
615671
if key in self._unknowns:
@@ -641,7 +697,7 @@ def __getitem__(self, key):
641697
case _:
642698
raise ValueError(f"Invalid key \"{key}\" for kind: {self._kind}")
643699
else:
644-
raise ValueError(f"Unexpected key type: {key.__class__}")
700+
raise NotImplementedError()
645701
value = Value(self, key, value, self._readOnly)
646702
self._cache[key] = value
647703
return value
@@ -860,6 +916,7 @@ def __setattr__(self, key, value):
860916
def __setitem__(self, key, value):
861917
if self._readOnly:
862918
raise ValueError(f"{self._readOnly} is read only")
919+
key = self._validate_key(key)
863920
if isinstance(key, str):
864921
if self._ensure_map() == 'struct_value':
865922
values = self._value.struct_value.fields
@@ -877,7 +934,7 @@ def __setitem__(self, key, value):
877934
while key >= len(values):
878935
values.add()
879936
else:
880-
raise ValueError('Unexpected key type')
937+
raise NotImplementedError()
881938
self._cache.pop(key, None)
882939
self._dependencies.pop(key, None)
883940
self._unknowns.pop(key, None)
@@ -887,6 +944,8 @@ def __setitem__(self, key, value):
887944
values[key].null_value = 0
888945
elif isinstance(value, bool): # Must be before int check
889946
values[key].bool_value = value
947+
elif isinstance(value, bytes):
948+
values[key].string_value = value._value.decode('utf-8')
890949
elif isinstance(value, str):
891950
values[key].string_value = value
892951
elif isinstance(value, (int, float)):
@@ -995,6 +1054,7 @@ def __delitem__(self, key):
9951054
kind = self._kind
9961055
if kind == 'Unknown':
9971056
return
1057+
key = self._validate_key(key)
9981058
if isinstance(key, str):
9991059
match kind:
10001060
case 'struct_value':
@@ -1036,11 +1096,12 @@ def __delitem__(self, key):
10361096
break
10371097
del values[ix]
10381098
else:
1039-
raise ValueError('Unexpected key type')
1099+
raise NotImplementedError()
10401100

10411101
def _create_child(self, key):
10421102
if self._readOnly:
10431103
raise ValueError(f"{self._readOnly} is read only")
1104+
key = self._validate_key(key)
10441105
if isinstance(key, str):
10451106
if self._ensure_map() == 'struct_value':
10461107
fields = self._value.struct_value.fields
@@ -1061,7 +1122,16 @@ def _create_child(self, key):
10611122
values.add()
10621123
values[key].Clear()
10631124
return values[key]
1064-
raise ValueError('Unexpected key type')
1125+
raise NotImplementedError()
1126+
1127+
def _validate_key(self, key):
1128+
if isinstance(key, FieldMessage):
1129+
key = key._value
1130+
elif isinstance(key, Value):
1131+
key = key._raw
1132+
if not isinstance(key, (str, int)):
1133+
raise TypeError(f"Unexpected key type: {key.__class__}")
1134+
return key
10651135

10661136
def _ensure_map(self):
10671137
kind = self._kind

0 commit comments

Comments
 (0)