crossplane-function-pythonic 0.2.1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
1
  # This is set at build time, using "hatch version"
2
- __version__ = "0.2.1"
2
+ __version__ = "0.3.0"
@@ -0,0 +1,153 @@
1
+
2
+
3
+ def process(composite):
4
+ for name, resource in composite.resources:
5
+ if resource.observed:
6
+ if resource.autoReady or (resource.autoReady is None and composite.autoReady):
7
+ if resource.ready is None:
8
+ if _checks.get((resource.apiVersion, resource.kind), _check_default).ready(resource):
9
+ resource.ready = True
10
+
11
+
12
+ class ConditionReady:
13
+ def ready(self, resource):
14
+ return bool(resource.conditions.Ready.status)
15
+
16
+ _checks = {}
17
+ _check_default = ConditionReady()
18
+
19
+ class Check:
20
+ @classmethod
21
+ def __init_subclass__(cls, **kwargs):
22
+ super().__init_subclass__(**kwargs)
23
+ if hasattr(cls, 'apiVersion'):
24
+ _checks[(cls.apiVersion, cls.__name__)] = cls()
25
+
26
+ def ready(self, resource):
27
+ raise NotImplementedError()
28
+
29
+ class AlwaysReady(Check):
30
+ def ready(self, resource):
31
+ return True
32
+
33
+
34
+ class ClusterRole(AlwaysReady):
35
+ apiVersion = 'rbac.authorization.k8s.io/v1'
36
+
37
+ class ClusterRoleBinding(AlwaysReady):
38
+ apiVersion = 'rbac.authorization.k8s.io/v1'
39
+
40
+ class ConfigMap(AlwaysReady):
41
+ apiVersion = 'v1'
42
+
43
+ class CronJob(Check):
44
+ apiVersion = 'batch/v1'
45
+ def ready(self, resource):
46
+ if resource.observed.spec.suspend and len(resource.observed.spec.suspend):
47
+ return True
48
+ if not resource.status.lastScheduleTime:
49
+ return False
50
+ if resource.status.active:
51
+ return True
52
+ if not resource.status.lastSuccessfulTime:
53
+ return False
54
+ return str(resource.status.lastSuccessfulTime) >= str(resource.status.lastScheduleTime)
55
+
56
+ class DaemonSet(Check):
57
+ apiVersion = 'apps/v1'
58
+ def ready(self, resource):
59
+ if not resource.status.desiredNumberScheduled:
60
+ return False
61
+ scheduled = resource.status.desiredNumberScheduled
62
+ return (scheduled == resource.status.numberReady and
63
+ scheduled == resource.status.updatedNumberScheduled and
64
+ scheduled == resource.status.numberAvailable
65
+ )
66
+
67
+ class Deployment(Check):
68
+ apiVersion = 'apps/v1'
69
+ def ready(self, resource):
70
+ replicas = resource.observed.spec.replicas or 1
71
+ if resource.status.updatedReplicas != replicas or resource.status.availableReplicas != replicas:
72
+ return False
73
+ return bool(resource.conditions.Available.status)
74
+
75
+ class HorizontalPodAutoscaler(Check):
76
+ apiVersion = 'autoscaling/v2'
77
+ def ready(self, resource):
78
+ for type in ('FailedGetScale', 'FailedUpdateScale', 'FailedGetResourceMetric', 'InvalidSelector'):
79
+ if resource.conditions[type].status:
80
+ return False
81
+ for type in ('ScalingActive', 'ScalingLimited'):
82
+ if resource.conditions[type].status:
83
+ return True
84
+ return False
85
+
86
+ class Ingress(Check):
87
+ apiVersion = 'networking.k8s.io/v1'
88
+ def ready(self, resource):
89
+ return len(resource.status.loadBalancer.ingress) > 0
90
+
91
+ class Job(Check):
92
+ apiVersion = 'batch/v1'
93
+ def ready(self, resource):
94
+ for type in ('Failed', 'Suspended'):
95
+ if resource.conditions[type].status:
96
+ return False
97
+ return bool(resource.conditions.Complete.status)
98
+
99
+ class Namespace(AlwaysReady):
100
+ apiVersion = 'v1'
101
+
102
+ class PersistentVolumeClaim(Check):
103
+ apiVersion = 'v1'
104
+ def ready(self, resource):
105
+ return resource.status.phase == 'Bound'
106
+
107
+ class Pod(Check):
108
+ apiVersion = 'v1'
109
+ def ready(self, resource):
110
+ if resource.status.phase == 'Succeeded':
111
+ return True
112
+ if resource.status.phase == 'Running':
113
+ if resource.observed.spec.restartPolicy == 'Always':
114
+ if resource.conditions.Ready.status:
115
+ return True
116
+ return False
117
+
118
+ class ReplicaSet(Check):
119
+ apiVersion = 'v1'
120
+ def ready(self, resource):
121
+ if int(resource.status.observedGeneration) < int(resource.observed.metadata.generation):
122
+ return False
123
+ if resource.conditions.ReplicaFailure.status:
124
+ return False
125
+ return int(resource.status.availableReplicas) >= int(resource.observed.spec.replicas or 1)
126
+
127
+ class Role(AlwaysReady):
128
+ apiVersion = 'rbac.authorization.k8s.io/v1'
129
+
130
+ class RoleBinding(AlwaysReady):
131
+ apiVersion = 'rbac.authorization.k8s.io/v1'
132
+
133
+ class Secret(AlwaysReady):
134
+ apiVersion = 'v1'
135
+
136
+ class Service(Check):
137
+ apiVersion = 'v1'
138
+ def ready(self, resource):
139
+ if resource.observed.spec.type != 'LoadBalancer':
140
+ return True
141
+ return len(resource.status.loadBalancer.ingress) > 0
142
+
143
+ class ServiceAccount(AlwaysReady):
144
+ apiVersion = 'v1'
145
+
146
+ class StatefulSet(Check):
147
+ apiVersion = 'apps/v1'
148
+ def ready(self, resource):
149
+ replicas = resource.observed.spec.replicas or 1
150
+ return (resource.status.readyReplicas == replicas and
151
+ resource.status.currentReplicas == replicas and
152
+ resource.status.currentRevision == resource.status.updateRevision
153
+ )
@@ -50,6 +50,11 @@ class Command:
50
50
  action='store_true',
51
51
  help='Allow oversized protobuf messages',
52
52
  )
53
+ parser.add_argument(
54
+ '--crossplane-v1',
55
+ action='store_true',
56
+ help='Enable Crossplane V1 compatibility mode',
57
+ )
53
58
 
54
59
  def __init__(self, args):
55
60
  self.args = args
@@ -9,8 +9,86 @@ from . import protobuf
9
9
  _notset = object()
10
10
 
11
11
 
12
+ class ConnectionSecret:
13
+ def __get__(self, composite, objtype=None):
14
+ if composite.crossplane_v1:
15
+ return composite.spec.writeConnectionSecretToRef
16
+ secret = getattr(composite, '_connectionSecret', None)
17
+ if not secret:
18
+ secret = protobuf.Map()
19
+ for key, value in composite.request.input.writeConnectionSecretToRef:
20
+ secret[key] = value
21
+ composite._connectionSecret = secret
22
+ return secret
23
+
24
+ def __set__(self, composite, values):
25
+ if composite.crossplane_v1:
26
+ if values != composite.spec.writeConnectionSecretToRef:
27
+ raise NotImplementedError('Connection Secret cannot be set in Crossplane V1')
28
+ return
29
+ secret = protobuf.Map()
30
+ for key, value in values:
31
+ secret[key] = value
32
+ composite._connectionSecret = secret
33
+
34
+
35
+ class Connection:
36
+ def __get__(self, composite, objtype=None):
37
+ connection = getattr(composite, '_connection', None)
38
+ if not connection:
39
+ connection = _Connection(composite)
40
+ composite._connection = connection
41
+ return connection
42
+
43
+ def __set__(self, composite, values):
44
+ connection = self.__get__(composite)
45
+ coneection()
46
+ for key, value in values:
47
+ connection[key] = value
48
+
49
+
50
+ class TTL:
51
+ def __get__(self, composite, objtype=None):
52
+ if composite.response.meta.ttl.nanos:
53
+ return float(composite.response.meta.ttl.seconds) + (float(composite.response.meta.ttl.nanos) / 1000000000.0)
54
+ return int(composite.response.meta.ttl.seconds)
55
+
56
+ def __set__(self, composite, ttl):
57
+ if isinstance(ttl, int):
58
+ composite.response.meta.ttl.seconds = ttl
59
+ composite.response.meta.ttl.nanos = 0
60
+ elif isinstance(ttl, float):
61
+ composite.response.meta.ttl.seconds = int(ttl)
62
+ if ttl.is_integer():
63
+ composite.response.meta.ttl.nanos = 0
64
+ else:
65
+ composite.response.meta.ttl.nanos = int((ttl - int(composite.response.meta.ttl.seconds)) * 1000000000)
66
+ else:
67
+ raise ValueError('ttl must be an int or float')
68
+
69
+
70
+ class Ready:
71
+ def __get__(self, composite, objtype=None):
72
+ ready = composite.desired._parent.ready
73
+ if ready == fnv1.Ready.READY_TRUE:
74
+ return True
75
+ if ready == fnv1.Ready.READY_FALSE:
76
+ return False
77
+ return None
78
+
79
+ def __set__(self, composite, ready):
80
+ if ready:
81
+ ready = fnv1.Ready.READY_TRUE
82
+ elif ready == None or (isinstance(ready, protobuf.Value) and ready._isUnknown):
83
+ ready = fnv1.Ready.READY_UNSPECIFIED
84
+ else:
85
+ ready = fnv1.Ready.READY_FALSE
86
+ composite.desired._parent.ready = ready
87
+
88
+
12
89
  class BaseComposite:
13
- def __init__(self, request, single_use, logger):
90
+ def __init__(self, crossplane_v1, request, single_use, logger):
91
+ self.crossplane_v1 = crossplane_v1
14
92
  self.request = protobuf.Message(None, 'request', request.DESCRIPTOR, request, 'Function Request')
15
93
  response = fnv1.RunFunctionResponse(
16
94
  meta=fnv1.ResponseMeta(
@@ -40,7 +118,6 @@ class BaseComposite:
40
118
  observed = self.request.observed.composite
41
119
  desired = self.response.desired.composite
42
120
  self.observed = observed.resource
43
- self.observed._set_attribute('connection', self.request.observed.composite.connection_details)
44
121
  self.desired = desired.resource
45
122
  self.apiVersion = self.observed.apiVersion
46
123
  self.kind = self.observed.kind
@@ -51,54 +128,10 @@ class BaseComposite:
51
128
  self.results = Results(self.response)
52
129
  self.events = Results(self.response) # Deprecated, use self.results
53
130
 
54
- @property
55
- def ttl(self):
56
- if self.response.meta.ttl.nanos:
57
- return float(self.response.meta.ttl.seconds) + (float(self.response.meta.ttl.nanos) / 1000000000.0)
58
- return int(self.response.meta.ttl.seconds)
59
-
60
- @ttl.setter
61
- def ttl(self, ttl):
62
- if isinstance(ttl, int):
63
- self.response.meta.ttl.seconds = ttl
64
- self.response.meta.ttl.nanos = 0
65
- elif isinstance(ttl, float):
66
- self.response.meta.ttl.seconds = int(ttl)
67
- if ttl.is_integer():
68
- self.response.meta.ttl.nanos = 0
69
- else:
70
- self.response.meta.ttl.nanos = int((ttl - int(self.response.meta.ttl.seconds)) * 1000000000)
71
- else:
72
- raise ValueError('ttl must be an int or float')
73
-
74
- @property
75
- def connection(self):
76
- return self.response.desired.composite.connection_details
77
-
78
- @connection.setter
79
- def connection(self, connection):
80
- self.response.desired.composite.connection_details()
81
- for key, value in connection:
82
- self.response.desired.composite.connection_details[key] = value
83
-
84
- @property
85
- def ready(self):
86
- ready = self.desired._parent.ready
87
- if ready == fnv1.Ready.READY_TRUE:
88
- return True
89
- if ready == fnv1.Ready.READY_FALSE:
90
- return False
91
- return None
92
-
93
- @ready.setter
94
- def ready(self, ready):
95
- if ready:
96
- ready = fnv1.Ready.READY_TRUE
97
- elif ready == None or (isinstance(ready, protobuf.Value) and ready._isUnknown):
98
- ready = fnv1.Ready.READY_UNSPECIFIED
99
- else:
100
- ready = fnv1.Ready.READY_FALSE
101
- self.desired._parent.ready = ready
131
+ ttl = TTL()
132
+ connectionSecret = ConnectionSecret()
133
+ connection = Connection()
134
+ ready = Ready()
102
135
 
103
136
  async def compose(self):
104
137
  raise NotImplementedError()
@@ -263,6 +296,14 @@ class Resource:
263
296
  def spec(self, spec):
264
297
  self.desired.spec = spec
265
298
 
299
+ @property
300
+ def type(self):
301
+ return self.desired.type
302
+
303
+ @type.setter
304
+ def type(self, type):
305
+ self.desired.type = type
306
+
266
307
  @property
267
308
  def data(self):
268
309
  return self.desired.data
@@ -315,25 +356,43 @@ class Requireds:
315
356
 
316
357
  def __len__(self):
317
358
  names = set()
318
- for name, resource in self._composite.request.extra_resources:
319
- names.add(name)
320
- for name, resource in self._composite.response.requirements.extra_resources:
321
- names.add(name)
359
+ if self._composite.crossplane_v1:
360
+ for name, resource in self._composite.request.extra_resources:
361
+ names.add(name)
362
+ for name, resource in self._composite.response.requirements.extra_resources:
363
+ names.add(name)
364
+ else:
365
+ for name, resource in self._composite.request.required_resources:
366
+ names.add(name)
367
+ for name, resource in self._composite.response.requirements.resources:
368
+ names.add(name)
322
369
  return len(names)
323
370
 
324
371
  def __contains__(self, key):
325
- if key in self._composite.request.extra_resources:
326
- return True
327
- if key in self._composite.response.desired.resources:
328
- return True
372
+ if self._composite.crossplane_v1:
373
+ if key in self._composite.request.extra_resources:
374
+ return True
375
+ if key in self._composite.response.requirements.extra_resources:
376
+ return True
377
+ else:
378
+ if key in self._composite.request.required_resources:
379
+ return True
380
+ if key in self._composite.response.requirements.resources:
381
+ return True
329
382
  return False
330
383
 
331
384
  def __iter__(self):
332
385
  names = set()
333
- for name, resource in self._composite.request.extra_resources:
334
- names.add(name)
335
- for name, resource in self._composite.response.requirements.extra_resources:
336
- names.add(name)
386
+ if self._composite.crossplane_v1:
387
+ for name, resource in self._composite.request.extra_resources:
388
+ names.add(name)
389
+ for name, resource in self._composite.response.requirements.extra_resources:
390
+ names.add(name)
391
+ else:
392
+ for name, resource in self._composite.request.required_resources:
393
+ names.add(name)
394
+ for name, resource in self._composite.response.requirements.resources:
395
+ names.add(name)
337
396
  for name in sorted(names):
338
397
  yield name, self[name]
339
398
 
@@ -341,8 +400,12 @@ class Requireds:
341
400
  class RequiredResources:
342
401
  def __init__(self, composite, name):
343
402
  self.name = name
344
- self._selector = composite.response.requirements.extra_resources[name]
345
- self._resources = composite.request.extra_resources[name]
403
+ if composite.crossplane_v1:
404
+ self._selector = composite.response.requirements.extra_resources[name]
405
+ self._resources = composite.request.extra_resources[name]
406
+ else:
407
+ self._selector = composite.response.requirements.resources[name]
408
+ self._resources = composite.request.required_resources[name]
346
409
  self._cache = {}
347
410
 
348
411
  def __call__(self, apiVersion=_notset, kind=_notset, namespace=_notset, name=_notset, labels=_notset):
@@ -432,6 +495,7 @@ class RequiredResource:
432
495
  self.kind = self.observed.kind
433
496
  self.metadata = self.observed.metadata
434
497
  self.spec = self.observed.spec
498
+ self.type = self.observed.type
435
499
  self.data = self.observed.data
436
500
  self.status = self.observed.status
437
501
  self.conditions = Conditions(resource)
@@ -723,3 +787,119 @@ class Result:
723
787
  self._result.target = fnv1.Target.TARGET_UNSPECIFIED
724
788
  else:
725
789
  self._result.target = fnv1.Target.TARGET_COMPOSITE
790
+
791
+
792
+ class _Connection:
793
+ def __init__(self, composite):
794
+ self._set_attribute('_composite', composite)
795
+
796
+ def _set_attribute(self, key, value):
797
+ self.__dict__[key] = value
798
+
799
+ @property
800
+ def _resource_name(self):
801
+ return self._composite.connectionSecret.resourceName or 'connection-secret'
802
+
803
+ @property
804
+ def observed(self):
805
+ if self._composite.crossplane_v1:
806
+ return self._composite.response.observed.composite.connection_details
807
+ data = protobuf.Map()
808
+ for key, value in self._composite.resources[self._resource_name].observed.data:
809
+ data[key] = protobuf.B64Decode(value)
810
+ return data
811
+
812
+ def __getattr__(self, key):
813
+ return self[key]
814
+
815
+ def __getitem__(self, key):
816
+ if self._composite.crossplane_v1:
817
+ return self._composite.response.desired.composite.connection_details[key]
818
+ value = self._composite.resources[self._resource_name].data[key]
819
+ if value:
820
+ value = protobuf.B64Decode(value)
821
+ return value
822
+
823
+ def __bool__(self):
824
+ if self._composite.crossplane_v1:
825
+ return bool(self._composite.response.desired.composite.connection_details)
826
+ return bool(self._composite.resources[self._resource_name].data)
827
+
828
+ def __len__(self):
829
+ if self._composite.crossplane_v1:
830
+ return len(self._composite.response.desired.composite.connection_details)
831
+ return len(self._composite.resources[self._resource_name].data)
832
+
833
+ def __contains__(self, key):
834
+ if self._composite.crossplane_v1:
835
+ return key in self._composite.response.desired.composite.connection_details
836
+
837
+ def __iter__(self):
838
+ keys = set()
839
+ if self._composite.crossplane_v1:
840
+ for key, value in self._composite.response.desired.composite.connection_details:
841
+ yield key, value
842
+ for key, value in self._composite.resources[self._resource_name].data:
843
+ yield key, protobuf.B64Decode(value)
844
+
845
+ def __str__(self):
846
+ return format(self)
847
+
848
+ def __format__(self, spec='yaml'):
849
+ if self._composite.crossplane_v1:
850
+ return format(self._composite.response.desired.composite.connection_details, spec)
851
+ data = protobuf.Map()
852
+ for key, value in self._composite.resources[self._resource_name].data:
853
+ data[key] = protobuf.B64Decode(value)
854
+ return format(data, spec)
855
+
856
+ def __call__(self, **kwargs):
857
+ if self._composite_v1:
858
+ self._composite.response.desired.composite.connection_details(**kwargs)
859
+ return
860
+ del self._composite.resources[self._resource_name]
861
+ for key, value in kwargs:
862
+ self[key] = value
863
+
864
+ def __setattr__(self, key, value):
865
+ self[key] = value
866
+
867
+ def __setitem__(self, key, value):
868
+ if not isinstance(value, str):
869
+ if value is None:
870
+ return
871
+ if isinstance(value, (protobuf.FieldMessage, protobuf.Value)):
872
+ if not value:
873
+ return
874
+ value = str(value)
875
+ if self._composite.crossplane_v1:
876
+ self._composite.response.desired.composite.connection_details[key] = value
877
+ return
878
+ #if not self._composite.connectionSecret.name:
879
+ # return
880
+ if self._resource_name in self._composite.resources:
881
+ secret = self._composite.resources[self._resource_name]
882
+ else:
883
+ secret = self._composite.resources[self._resource_name]('v1', 'Secret')
884
+ print(bool(self._composite.connectionSecret.name), len(self._composite.connectionSecret.name))
885
+ if self._composite.connectionSecret.name and len(self._composite.connectionSecret.name):
886
+ secret.metadata.name = self._composite.connectionSecret.name
887
+ if not self._composite.metadata.namespace:
888
+ if not self._composite.connectionSecret.namespace:
889
+ self._composite.results.fatal('ConnectionNoNamespace', 'Cluster scoped XR must specify connection secret namespace')
890
+ return
891
+ secret.metadata.namespace = self._composite.connectionSecret.namespace
892
+ secret.type = 'connection.crossplane.io/v1alpha1'
893
+ secret.data[key] = protobuf.B64Encode(value)
894
+
895
+ def __delattr__(self, key):
896
+ del self[key]
897
+
898
+ def __delitem__(self, key):
899
+ if self._composite.crossplane_v1:
900
+ del self._composite.response.desired.composite.connection_details[key]
901
+ return
902
+ if self._resource_name in self._composite.resources:
903
+ del self._composite.resources[self._resource_name].data[key]
904
+ if not len(self._composite.resources[self._resource_name].data):
905
+ del self._composite.resources[self._resource_name]
@@ -9,6 +9,7 @@ import sys
9
9
  import grpc
10
10
  from crossplane.function.proto.v1 import run_function_pb2 as fnv1
11
11
  from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1
12
+ from . import auto_ready
12
13
  from .. import pythonic
13
14
 
14
15
  logger = logging.getLogger(__name__)
@@ -17,10 +18,11 @@ logger = logging.getLogger(__name__)
17
18
  class FunctionRunner(grpcv1.FunctionRunnerService):
18
19
  """A FunctionRunner handles gRPC RunFunctionRequests."""
19
20
 
20
- def __init__(self, debug=False, renderUnknowns=False):
21
+ def __init__(self, debug=False, renderUnknowns=False, crossplane_v1=False):
21
22
  """Create a new FunctionRunner."""
22
23
  self.debug = debug
23
24
  self.renderUnknowns = renderUnknowns
25
+ self.crossplane_v1 = crossplane_v1
24
26
  self.clazzes = {}
25
27
 
26
28
  def invalidate_module(self, module):
@@ -100,7 +102,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
100
102
  self.clazzes[composite] = clazz
101
103
 
102
104
  try:
103
- composite = clazz(request, single_use, logger)
105
+ composite = clazz(self.crossplane_v1, request, single_use, logger)
104
106
  except Exception as e:
105
107
  return self.fatal(request, logger, 'Instantiate', e)
106
108
 
@@ -122,7 +124,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
122
124
  else:
123
125
  self.process_usages(composite)
124
126
  self.process_unknowns(composite)
125
- self.process_auto_readies(composite)
127
+ auto_ready.process(composite)
126
128
  logger.info('Completed compose')
127
129
 
128
130
  return composite.response._message
@@ -152,11 +154,11 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
152
154
  def get_requireds(self, step, composite):
153
155
  requireds = []
154
156
  for name, required in composite.requireds:
155
- if required.apiVersion and required.kind:
157
+ if len(required.apiVersion) and len(required.kind):
156
158
  r = pythonic.Map(apiVersion=required.apiVersion, kind=required.kind)
157
- if required.namespace:
159
+ if len(required.namespace):
158
160
  r.namespace = required.namespace
159
- if required.matchName:
161
+ if len(required.matchName):
160
162
  r.matchName = required.matchName
161
163
  for key, value in required.matchLabels:
162
164
  r.matchLabels[key] = value
@@ -166,6 +168,10 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
166
168
  return requireds
167
169
 
168
170
  def process_usages(self, composite):
171
+ if self.crossplane_v1:
172
+ apiVersion = 'apiextensions.crossplane.io/v1beta1'
173
+ else:
174
+ apiVersion = 'protection.crossplane.io/v1beta1'
169
175
  for _, resource in sorted(entry for entry in composite.resources):
170
176
  dependencies = resource.desired._getDependencies
171
177
  if dependencies:
@@ -175,7 +181,6 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
175
181
  source = self.trimFullName(source)
176
182
  composite.logger.debug(f"Dependency: {destination} = {source}")
177
183
  if resource.usages or (resource.usages is None and composite.usages):
178
- apiVersion = 'protection.crossplane.io/v1beta1' if composite.metadata.namespace else 'apiextensions.crossplane.io/v1beta1'
179
184
  resources = {}
180
185
  requireds = {}
181
186
  for destination, source in sorted(dependencies.items()):
@@ -191,7 +196,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
191
196
  resources[name[3]].append(f"{'.'.join(destination.split('.')[5:])} = {'.'.join(name[5:])}")
192
197
  elif (len(name) > 5 and
193
198
  name[0] == 'request' and
194
- name[1] == 'extra_resources' and
199
+ name[1] in ('required_resources', 'extra_resources') and
195
200
  name[3].startswith('items[') and name[3][-1] == ']' and
196
201
  name[4] == 'resource'
197
202
  ):
@@ -206,8 +211,6 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
206
211
  name.append(str(source.metadata.namespace))
207
212
  name.append(str(source.observed.metadata.name))
208
213
  usage = composite.resources['_'.join(name)](apiVersion, 'Usage')
209
- if resource.metadata.namespace:
210
- usage.metadata.namespace = resource.metadata.namespace
211
214
  usage.spec.reason = '\n'.join(dependencies)
212
215
  usage.spec.replayDeletion = True
213
216
  usage.spec.by.apiVersion = resource.apiVersion
@@ -215,9 +218,17 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
215
218
  usage.spec.by.resourceRef.name = resource.observed.metadata.name
216
219
  usage.spec.of.apiVersion = source.apiVersion
217
220
  usage.spec.of.kind = source.kind
218
- if source.metadata.namespace:
219
- usage.spec.of.resourceRef.namespace = source.metadata.namespace
220
221
  usage.spec.of.resourceRef.name = source.observed.metadata.name
222
+ if not self.crossplane_v1:
223
+ if composite.metadata.namespace:
224
+ if source.metadata.namespace and source.metadata.namespace != composite.metadata.namespace:
225
+ usage.spec.of.resourceRef.namespace = source.metadata.namespace
226
+ elif resource.metadata.namespace:
227
+ usage.metadata.namespace = resource.metadata.namespace
228
+ if source.metadata.namespace and source.metadata.namespace != resource.metadata.namespace:
229
+ usage.spec.of.resourceRef.namespace = source.metadata.namespace
230
+ else:
231
+ usage.kind = 'ClusterUsage'
221
232
  for key, dependencies in requireds.items():
222
233
  source = composite.requireds[key[0]][key[1]]
223
234
  name = [resource.name, str(source.kind)]
@@ -225,8 +236,6 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
225
236
  name.append(str(source.metadata.namespace))
226
237
  name.append(str(source.metadata.name))
227
238
  usage = composite.resources['_'.join(name)](apiVersion, 'Usage')
228
- if resource.metadata.namespace:
229
- usage.metadata.namespace = resource.metadata.namespace
230
239
  usage.spec.reason = '\n'.join(dependencies)
231
240
  usage.spec.replayDeletion = True
232
241
  usage.spec.by.apiVersion = resource.apiVersion
@@ -234,9 +243,17 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
234
243
  usage.spec.by.resourceRef.name = resource.observed.metadata.name
235
244
  usage.spec.of.apiVersion = source.apiVersion
236
245
  usage.spec.of.kind = source.kind
237
- if source.metadata.namespace:
238
- usage.spec.of.resourceRef.namespace = source.metadata.namespace
239
246
  usage.spec.of.resourceRef.name = source.observed.metadata.name
247
+ if not self.crossplane_v1:
248
+ if composite.metadata.namespace:
249
+ if source.metadata.namespace and source.metadata.namespace != composite.metadata.namespace:
250
+ usage.spec.of.resourceRef.namespace = source.metadata.namespace
251
+ elif resource.metadata.namespace:
252
+ usage.metadata.namespace = resource.metadata.namespace
253
+ if source.metadata.namespace and source.metadata.namespace != resource.metadata.namespace:
254
+ usage.spec.of.resourceRef.namespace = source.metadata.namespace
255
+ else:
256
+ usage.kind = 'ClusterUsage'
240
257
 
241
258
  def process_unknowns(self, composite):
242
259
  unknownResources = []
@@ -301,13 +318,6 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
301
318
  if result:
302
319
  result(reason, message)
303
320
 
304
- def process_auto_readies(self, composite):
305
- for name, resource in composite.resources:
306
- if resource.autoReady or (resource.autoReady is None and composite.autoReady):
307
- if resource.ready is None:
308
- if resource.conditions.Ready.status:
309
- resource.ready = True
310
-
311
321
  def trimFullName(self, name):
312
322
  name = name.split('.')
313
323
  for values in (
@@ -88,7 +88,7 @@ class Command(command.Command):
88
88
 
89
89
  async def run(self):
90
90
  grpc.aio.init_grpc_aio()
91
- grpc_runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns)
91
+ grpc_runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns, self.args.crossplane_v1)
92
92
  grpc_server = grpc.aio.server()
93
93
  grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
94
94
  if self.args.insecure:
@@ -568,39 +568,57 @@ class FieldMessage:
568
568
  self._value = value
569
569
 
570
570
  def __bool__(self):
571
- return bool(self._value)
571
+ return self._value is not _Unknown
572
572
 
573
573
  def __len__(self):
574
+ if self._value is _Unknown:
575
+ return 0
574
576
  return len(self._value)
575
577
 
576
578
  def __contains__(self, key):
579
+ if self._value is _Unknown:
580
+ return False
577
581
  return key in self._value
578
582
 
579
583
  def __hash__(self):
584
+ if self._value is _Unknown:
585
+ return 0
580
586
  return hash(self._value)
581
587
 
582
588
  def __eq__(self, other):
589
+ if self._value is _Unknown:
590
+ return False
583
591
  if isinstance(other, FieldMessage):
584
592
  return self._value == other._value
585
593
  return self._value == other
586
594
 
587
595
  def __bytes__(self):
596
+ if self._value is _Unknown:
597
+ return None
588
598
  if isinstance(self._value, str):
589
599
  return self._value.encode('utf-8')
590
600
  return bytes(self._value)
591
601
 
592
602
  def __str__(self):
603
+ if self._value is _Unknown:
604
+ return None
593
605
  if isinstance(self._value, bytes):
594
606
  return self._value.decode('utf-8')
595
607
  return str(self._value)
596
608
 
597
609
  def __format__(self, spec=''):
610
+ if self._value is _Unknown:
611
+ return None
598
612
  return format(self._value, spec)
599
613
 
600
614
  def __int__(self):
615
+ if self._value is _Unknown:
616
+ return None
601
617
  return int(self._value)
602
618
 
603
619
  def __float__(self):
620
+ if self._value is _Unknown:
621
+ return None
604
622
  return float(self._value)
605
623
 
606
624
  def _fullName(self, key=None):
@@ -715,6 +733,10 @@ class Value:
715
733
  return len(self._value.list_value.values) + len(self._unknowns)
716
734
  case 'ListValue':
717
735
  return len(self._value.values) + len(self._unknowns)
736
+ case 'string_value':
737
+ return len(self._value.string_value)
738
+ case 'bool_value':
739
+ return 1 if self._value.bool_value else 0
718
740
  return 0
719
741
 
720
742
  def __contains__(self, item):
@@ -1224,13 +1246,14 @@ class Value:
1224
1246
  for key, value in self:
1225
1247
  if isinstance(value, Value) and len(value):
1226
1248
  patch = patches[key]
1227
- if isinstance(patch, Value) and patch._type == value._type and len(patch):
1249
+ print(patch.__class__, str(patch))
1250
+ if isinstance(patch, Value) and patch._kind == value._kind and len(patch):
1228
1251
  value._patchUnknowns(patch)
1229
1252
  elif self._isList:
1230
1253
  for ix, value in enumerate(self):
1231
1254
  if isinstance(value, Value) and len(value):
1232
1255
  patch = patches[ix]
1233
- if isinstance(patch, Value) and patch._type == value._type and len(patch):
1256
+ if isinstance(patch, Value) and patch._kind == value._kind and len(patch):
1234
1257
  value._patchUnknowns(patch)
1235
1258
 
1236
1259
  def _renderUnknowns(self, trimFullName):
@@ -158,7 +158,7 @@ class Command(command.Command):
158
158
  # Collect specified required/extra resources. Sort for stable order when processed.
159
159
  requireds = sorted(
160
160
  self.collect_resources(self.args.required_resources),
161
- key=lambda required: str(resource.metadata.name),
161
+ key=lambda required: str(required.metadata.name),
162
162
  )
163
163
 
164
164
  # Collect specified connection and credential secrets.
@@ -181,7 +181,7 @@ class Command(command.Command):
181
181
  results = protobuf.List()
182
182
 
183
183
  # Create a function-pythonic function runner used to run pipeline steps.
184
- runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns)
184
+ runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns, self.args.crossplane_v1)
185
185
  fatal = False
186
186
 
187
187
  # Process the composition pipeline steps.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossplane-function-pythonic
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: A Python centric Crossplane Function
5
5
  Project-URL: Documentation, https://github.com/crossplane-contrib/function-pythonic#readme
6
6
  Project-URL: Issues, https://github.com/crossplane-contrib/function-pythonic/issues
@@ -17,7 +17,7 @@ Requires-Python: <3.15,>=3.12
17
17
  Requires-Dist: crossplane-function-sdk-python==0.10.0
18
18
  Requires-Dist: pyyaml==6.0.3
19
19
  Provides-Extra: packages
20
- Requires-Dist: kopf==1.39.1; extra == 'packages'
20
+ Requires-Dist: kopf==1.40.0; extra == 'packages'
21
21
  Provides-Extra: pip-install
22
22
  Requires-Dist: pip==25.3; extra == 'pip-install'
23
23
  Description-Content-Type: text/markdown
@@ -81,8 +81,39 @@ kind: Function
81
81
  metadata:
82
82
  name: function-pythonic
83
83
  spec:
84
- package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1
84
+ package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0
85
85
  ```
86
+
87
+ ### Crossplane V1
88
+ When running function-pythonic in Crossplane V1, the `--crossplane-v1` command line
89
+ option should be specified. This requires using a Crossplane DeploymentRuntimeConfig.
90
+ ```yaml
91
+ apiVersion: pkg.crossplane.io/v1
92
+ kind: Function
93
+ metadata:
94
+ name: function-pythonic
95
+ spec:
96
+ package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0
97
+ runtimeConfigRef:
98
+ name: function-pythonic
99
+ --
100
+ apiVersion: pkg.crossplane.io/v1beta1
101
+ kind: DeploymentRuntimeConfig
102
+ metadata:
103
+ name: function-pythonic
104
+ spec:
105
+ deploymentTemplate:
106
+ spec:
107
+ selector: {}
108
+ template:
109
+ spec:
110
+ containers:
111
+ - name: package-runtime
112
+ args:
113
+ - --debug
114
+ - --crossplane-v1
115
+ ```
116
+
86
117
  ## Composed Resource Dependencies
87
118
 
88
119
  function-pythonic automatically handles dependencies between composed resources.
@@ -228,9 +259,10 @@ The BaseComposite class provides the following fields for manipulating the Compo
228
259
  | self.status | Map | The composite desired and observed status, read from observed if not in desired |
229
260
  | self.conditions | Conditions | The composite desired and observed conditions, read from observed if not in desired |
230
261
  | self.results | Results | Returned results applied to the Composite and optionally on the Claim |
262
+ | self.connectionSecret | Map | The name, namespace, and resourceName to use when generating the connection secret in Crossplane v2 |
231
263
  | self.connection | Map | The composite desired connection detials |
264
+ | self.connection.observed | Map | The composite observed connection detials |
232
265
  | self.ready | Boolean | The composite desired ready state |
233
- | self.observed.connection | Map | The composite observed connection detials |
234
266
 
235
267
  The BaseComposite also provides access to the following Crossplane Function level features:
236
268
 
@@ -277,7 +309,7 @@ Resource class:
277
309
  | Resource.usages | Boolean | Generate Crossplane Usages for this resource, default is Composite.autoReady |
278
310
  | Resource.autoReady | Boolean | Perform auto ready processing on this resource, default is Composite.autoReady |
279
311
 
280
- ### Required Resources (AKA Extra Resources)
312
+ ### Required Resources
281
313
 
282
314
  Creating and accessing required resources is performed using the `BaseComposite.requireds` field.
283
315
  `BaseComposite.requireds` is a dictionary of the required resources whose key is the required
@@ -547,7 +579,7 @@ kind: Function
547
579
  metadata:
548
580
  name: function-pythonic
549
581
  spec:
550
- package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1
582
+ package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0
551
583
  runtimeConfigRef:
552
584
  name: function-pythonic
553
585
  ---
@@ -0,0 +1,18 @@
1
+ crossplane/pythonic/__about__.py,sha256=aySM7NSterQ9iNm2zapljwvbJuWDyviv3Ku2kts5ThU,73
2
+ crossplane/pythonic/__init__.py,sha256=A9U4-azc4DjSsOnOnjQxCkoTzsZMRBb_AvqzR_Bd95A,268
3
+ crossplane/pythonic/__main__.py,sha256=6vYRlYDJtqFgLyiTamnl3htiNOtz8QlDl5WlIP98I8o,31
4
+ crossplane/pythonic/auto_ready.py,sha256=FrbIePmj96Jl51fjVt1ctj26jTksimRCXGsNTFaMh9A,5026
5
+ crossplane/pythonic/command.py,sha256=QC1Z4j5-MyfH41V7Zc9tJgaTZ7IVR0h8xL4bbXOE9Fg,3309
6
+ crossplane/pythonic/composite.py,sha256=WiDV5OCJ8-BDQ_4i-3FYvflpGxLoCLde-fRjDF0ib9w,29377
7
+ crossplane/pythonic/function.py,sha256=KYOFzCGp59trKGl5M5JxjnOWWEPDL3hmoXYXC6kGljw,17433
8
+ crossplane/pythonic/grpc.py,sha256=8hQZZsNrcbiCGfEdPWRrG9SYfTrmuPgMmAIHwWhCdt8,4468
9
+ crossplane/pythonic/main.py,sha256=ujUa_FYElQSGqnhZ-0NJrD3kSyYjfRbIp79FV2Yl7hs,599
10
+ crossplane/pythonic/packages.py,sha256=4TxyT6V79R0m4tJbC8R1gwU_vgHGLXKSBzeTTKd8xGo,5120
11
+ crossplane/pythonic/protobuf.py,sha256=Dc4gYWPusQYGIR0k0xawkHCV0a-sqX2y7eAAc7whqME,51304
12
+ crossplane/pythonic/render.py,sha256=h9S59R_cM1eerOuDL2h6fQ79f-cmaA9OYs8CE2AGj7E,22263
13
+ crossplane/pythonic/version.py,sha256=-RiB0p146ayqJj0SXfYxTNv49u9Fx9pPgm59Ji2blhc,214
14
+ crossplane_function_pythonic-0.3.0.dist-info/METADATA,sha256=Un9AsXhViJ_1YOx3wxXe1d6BDNzZgcf6_VczndW6OXg,29502
15
+ crossplane_function_pythonic-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
+ crossplane_function_pythonic-0.3.0.dist-info/entry_points.txt,sha256=jJ4baywFDviB9WyAhyhNYF2VOCb6XtbRSjKf7bnBwhg,68
17
+ crossplane_function_pythonic-0.3.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
18
+ crossplane_function_pythonic-0.3.0.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- crossplane/pythonic/__about__.py,sha256=QBcrW1XnbdqF5kVtJfA-yvTFAU-t00iGaaoo6kjjIDY,73
2
- crossplane/pythonic/__init__.py,sha256=A9U4-azc4DjSsOnOnjQxCkoTzsZMRBb_AvqzR_Bd95A,268
3
- crossplane/pythonic/__main__.py,sha256=6vYRlYDJtqFgLyiTamnl3htiNOtz8QlDl5WlIP98I8o,31
4
- crossplane/pythonic/command.py,sha256=s69oOVCgbm39gowvTy7ieaE-Vosckf24vcznJ6fO0Q4,3146
5
- crossplane/pythonic/composite.py,sha256=PJD3uRbanrkUgCUxnN_lKhg5n3QO1OpGfLC71UkEwRg,22129
6
- crossplane/pythonic/function.py,sha256=ShHhSJYblTB9BRx7NEtHgiyOconax7Lro0tLzzRyZdY,16564
7
- crossplane/pythonic/grpc.py,sha256=yc-hVnw7K0iuuJS25z6aZkfXjUuvciWfGCdHnWv4I78,4443
8
- crossplane/pythonic/main.py,sha256=ujUa_FYElQSGqnhZ-0NJrD3kSyYjfRbIp79FV2Yl7hs,599
9
- crossplane/pythonic/packages.py,sha256=4TxyT6V79R0m4tJbC8R1gwU_vgHGLXKSBzeTTKd8xGo,5120
10
- crossplane/pythonic/protobuf.py,sha256=Wha3hMSDbJPALtMu-ZcZEHaV5CosfYW8SJh7ueAbqVk,50528
11
- crossplane/pythonic/render.py,sha256=yqqk9DambTBRgQ51ZEQWOLg1xvos1KGETHei6rimus8,22238
12
- crossplane/pythonic/version.py,sha256=-RiB0p146ayqJj0SXfYxTNv49u9Fx9pPgm59Ji2blhc,214
13
- crossplane_function_pythonic-0.2.1.dist-info/METADATA,sha256=pIsTA_rzQ_HBpVlYZZI67AmHz27qcqAfxYoY5j1iNW0,28659
14
- crossplane_function_pythonic-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
- crossplane_function_pythonic-0.2.1.dist-info/entry_points.txt,sha256=jJ4baywFDviB9WyAhyhNYF2VOCb6XtbRSjKf7bnBwhg,68
16
- crossplane_function_pythonic-0.2.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
17
- crossplane_function_pythonic-0.2.1.dist-info/RECORD,,