crossplane-function-pythonic 0.0.11__py3-none-any.whl → 0.1.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,10 +1,7 @@
1
1
 
2
- import base64
3
2
 
4
3
  from .composite import BaseComposite
5
- from .protobuf import append, Map, List, Unknown, Yaml, Json
6
- B64Encode = lambda s: base64.b64encode(s.encode('utf-8')).decode('utf-8')
7
- B64Decode = lambda s: base64.b64decode(s.encode('utf-8')).decode('utf-8')
4
+ from .protobuf import append, Map, List, Unknown, Yaml, Json, B64Encode, B64Decode
8
5
 
9
6
  __all__ = [
10
7
  'BaseComposite',
@@ -31,6 +31,7 @@ class BaseComposite:
31
31
  self.resources = Resources(self)
32
32
  self.unknownsFatal = True
33
33
  self.autoReady = True
34
+ self.usages = False
34
35
 
35
36
  observed = self.request.observed.composite
36
37
  desired = self.response.desired.composite
@@ -49,7 +50,7 @@ class BaseComposite:
49
50
  def ttl(self):
50
51
  if self.response.meta.ttl.nanos:
51
52
  return float(self.response.meta.ttl.seconds) + (float(self.response.meta.ttl.nanos) / 1000000000.0)
52
- return self.response.meta.ttl.seconds
53
+ return int(self.response.meta.ttl.seconds)
53
54
 
54
55
  @ttl.setter
55
56
  def ttl(self, ttl):
@@ -61,7 +62,7 @@ class BaseComposite:
61
62
  if ttl.is_integer():
62
63
  self.response.meta.ttl.nanos = 0
63
64
  else:
64
- self.response.meta.ttl.nanos = int((ttl - self.response.meta.ttl.seconds) * 1000000000)
65
+ self.response.meta.ttl.nanos = int((ttl - int(self.response.meta.ttl.seconds)) * 1000000000)
65
66
  else:
66
67
  raise ValueError('ttl must be an int or float')
67
68
 
@@ -78,7 +79,7 @@ class BaseComposite:
78
79
  def ready(self, ready):
79
80
  if ready:
80
81
  ready = fnv1.Ready.READY_TRUE
81
- elif ready == None or (isinstance(ready, protobuf.Values) and ready._isUnknown):
82
+ elif ready == None or (isinstance(ready, protobuf.Value) and ready._isUnknown):
82
83
  ready = fnv1.Ready.READY_UNSPECIFIED
83
84
  else:
84
85
  ready = fnv1.Ready.READY_FALSE
@@ -184,6 +185,7 @@ class Resource:
184
185
  self.connection = Connection(observed)
185
186
  self.unknownsFatal = None
186
187
  self.autoReady = None
188
+ self.usages = None
187
189
 
188
190
  def __call__(self, apiVersion=_notset, kind=_notset, namespace=_notset, name=_notset):
189
191
  self.desired()
@@ -199,7 +201,7 @@ class Resource:
199
201
 
200
202
  @property
201
203
  def apiVersion(self):
202
- return self.observed.apiVersion
204
+ return self.desired.apiVersion
203
205
 
204
206
  @apiVersion.setter
205
207
  def apiVersion(self, apiVersion):
@@ -207,7 +209,7 @@ class Resource:
207
209
 
208
210
  @property
209
211
  def kind(self):
210
- return self.observed.kind
212
+ return self.desired.kind
211
213
 
212
214
  @kind.setter
213
215
  def kind(self, kind):
@@ -265,7 +267,7 @@ class Resource:
265
267
  def ready(self, ready):
266
268
  if ready:
267
269
  ready = fnv1.Ready.READY_TRUE
268
- elif ready == None or (isinstance(ready, protobuf.Values) and ready._isUnknown):
270
+ elif ready == None or (isinstance(ready, protobuf.Value) and ready._isUnknown):
269
271
  ready = fnv1.Ready.READY_UNSPECIFIED
270
272
  else:
271
273
  ready = fnv1.Ready.READY_FALSE
@@ -376,8 +378,8 @@ class RequiredResources:
376
378
  elif isinstance(entry, (list, tuple)):
377
379
  self._selector.match_labels.labels[entry[0]] = entry[1]
378
380
 
379
- def __getitem__(self, key):
380
- return RequiredResource(self.name, self._resources.items[key])
381
+ def __getitem__(self, ix):
382
+ return RequiredResource(self.name, ix, self._resources.items[ix])
381
383
 
382
384
  def __bool__(self):
383
385
  return bool(self._resources.items)
@@ -391,8 +393,9 @@ class RequiredResources:
391
393
 
392
394
 
393
395
  class RequiredResource:
394
- def __init__(self, name, resource):
396
+ def __init__(self, name, ix, resource):
395
397
  self.name = name
398
+ self.ix = ix
396
399
  self.observed = resource.resource
397
400
  self.apiVersion = self.observed.apiVersion
398
401
  self.kind = self.observed.kind
@@ -487,7 +490,7 @@ class Condition(protobuf.ProtobufValue):
487
490
  condition.status = fnv1.Status.STATUS_CONDITION_TRUE
488
491
  elif status == None:
489
492
  condition.status = fnv1.Status.STATUS_CONDITION_UNKNOWN
490
- elif isinstance(status, protobuf.Values) and status._isUnknown:
493
+ elif isinstance(status, protobuf.Value) and status._isUnknown:
491
494
  condition.status = fnv1.Status.STATUS_CONDITION_UNSPECIFIED
492
495
  else:
493
496
  condition.status = fnv1.Status.STATUS_CONDITION_FALSE
@@ -521,7 +524,7 @@ class Condition(protobuf.ProtobufValue):
521
524
  if observed.type == self.type:
522
525
  time = observed.lastTransitionTime
523
526
  if time:
524
- return datetime.datetime.fromisoformat(time)
527
+ return datetime.datetime.fromisoformat(str(time))
525
528
  return None
526
529
 
527
530
  @property
@@ -534,7 +537,7 @@ class Condition(protobuf.ProtobufValue):
534
537
  condition = self._find_condition(True)
535
538
  if claim:
536
539
  condition.target = fnv1.Target.TARGET_COMPOSITE_AND_CLAIM
537
- elif claim == None or (isinstance(claim, protobuf.Values) and claim._isUnknown):
540
+ elif claim == None or (isinstance(claim, protobuf.Value) and claim._isUnknown):
538
541
  condition.target = fnv1.Target.TARGET_UNSPECIFIED
539
542
  else:
540
543
  condition.target = fnv1.Target.TARGET_COMPOSITE
@@ -711,7 +714,7 @@ class Event:
711
714
  if bool(self):
712
715
  if claim:
713
716
  self._result.target = fnv1.Target.TARGET_COMPOSITE_AND_CLAIM
714
- elif claim == None or (isinstance(claim, protobuf.Values) and claim._isUnknown):
717
+ elif claim == None or (isinstance(claim, protobuf.Value) and claim._isUnknown):
715
718
  self._result.target = fnv1.Target.TARGET_UNSPECIFIED
716
719
  else:
717
720
  self._result.target = fnv1.Target.TARGET_COMPOSITE
@@ -99,7 +99,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
99
99
  return self.fatal(request, logger, 'Instantiate', e)
100
100
 
101
101
  step = composite.context._pythonic[step]
102
- iteration = (step.iteration or 0) + 1
102
+ iteration = int(step.iteration) + 1
103
103
  step.iteration = iteration
104
104
  composite.context.iteration = iteration
105
105
  logger.debug(f"Starting compose, {ordinal(len(composite.context._pythonic))} step, {ordinal(iteration)} pass")
@@ -111,7 +111,40 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
111
111
  except Exception as e:
112
112
  return self.fatal(request, logger, 'Compose', e)
113
113
 
114
- requested = []
114
+ if requireds := self.get_requireds(step, composite):
115
+ logger.info(f"Requireds requested: {','.join(requireds)}")
116
+ else:
117
+ self.process_usages(composite)
118
+ self.process_unknowns(composite)
119
+ self.process_auto_readies(composite)
120
+ logger.info('Completed compose')
121
+
122
+ return composite.response._message
123
+
124
+ def fatal(self, request, logger, message, exception=None):
125
+ if exception:
126
+ message += ' exceptiion'
127
+ logger.exception(message)
128
+ m = str(exception)
129
+ if not m:
130
+ m = exception.__class__.__name__
131
+ message += ': ' + m
132
+ else:
133
+ logger.error(message)
134
+ return fnv1.RunFunctionResponse(
135
+ meta=fnv1.ResponseMeta(
136
+ tag=request.meta.tag,
137
+ ),
138
+ results=[
139
+ fnv1.Result(
140
+ severity=fnv1.SEVERITY_FATAL,
141
+ message=message,
142
+ )
143
+ ]
144
+ )
145
+
146
+ def get_requireds(self, step, composite):
147
+ requireds = []
115
148
  for name, required in composite.requireds:
116
149
  if required.apiVersion and required.kind:
117
150
  r = pythonic.Map(apiVersion=required.apiVersion, kind=required.kind)
@@ -123,11 +156,84 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
123
156
  r.matchLabels[key] = value
124
157
  if r != step.requireds[name]:
125
158
  step.requireds[name] = r
126
- requested.append(name)
127
- if requested:
128
- logger.info(f"Requireds requested: {','.join(requested)}")
129
- return composite.response._message
159
+ requireds.append(name)
160
+ return requireds
161
+
162
+ def process_usages(self, composite):
163
+ for _, resource in sorted(entry for entry in composite.resources):
164
+ dependencies = resource.desired._getDependencies
165
+ if dependencies:
166
+ if self.debug:
167
+ for destination, source in sorted(dependencies.items()):
168
+ destination = self.trimFullName(destination)
169
+ source = self.trimFullName(source)
170
+ composite.logger.debug(f"Dependency: {destination} = {source}")
171
+ if resource.usages or (resource.usages is None and composite.usages):
172
+ resources = {}
173
+ requireds = {}
174
+ for destination, source in sorted(dependencies.items()):
175
+ name = source.split('.')
176
+ if (len(name) > 5 and
177
+ name[0] == 'request' and
178
+ name[1] == 'observed' and
179
+ name[2] == 'resources' and
180
+ name[4] == 'resource'
181
+ ):
182
+ if name[3] not in resources:
183
+ resources[name[3]] = []
184
+ resources[name[3]].append(f"{'.'.join(destination.split('.')[5:])} = {'.'.join(name[5:])}")
185
+ elif (len(name) > 5 and
186
+ name[0] == 'request' and
187
+ name[1] == 'extra_resources' and
188
+ name[3].startswith('items[') and name[3][-1] == ']' and
189
+ name[4] == 'resource'
190
+ ):
191
+ key = (name[2], int(name[3][6:-1]))
192
+ if key not in requireds:
193
+ requireds[key] = []
194
+ requireds[key].append(f"{'.'.join(destination.split('.')[5:])} = {'.'.join(name[5:])}")
195
+ for name, dependencies in resources.items():
196
+ source = composite.resources[name]
197
+ name = [resource.name, str(source.kind)]
198
+ if source.metadata.namespace:
199
+ name.append(str(source.metadata.namespace))
200
+ name.append(str(source.observed.metadata.name))
201
+ usage = composite.resources['_'.join(name)]('apiextensions.crossplane.io/v1beta1', 'Usage')
202
+ #usage = composite.resources['_'.join(name)]('protection.crossplane.io/v1beta1', 'Usage')
203
+ if resource.metadata.namespace:
204
+ usage.metadata.namespace = resource.metadata.namespace
205
+ usage.spec.reason = '\n'.join(dependencies)
206
+ usage.spec.replayDeletion = True
207
+ usage.spec.by.apiVersion = resource.apiVersion
208
+ usage.spec.by.kind = resource.kind
209
+ usage.spec.by.resourceRef.name = resource.observed.metadata.name
210
+ usage.spec.of.apiVersion = source.apiVersion
211
+ usage.spec.of.kind = source.kind
212
+ if source.metadata.namespace:
213
+ usage.spec.of.resourceRef.namespace = source.metadata.namespace
214
+ usage.spec.of.resourceRef.name = source.observed.metadata.name
215
+ for key, dependencies in requireds.items():
216
+ source = composite.requireds[key[0]][key[1]]
217
+ name = [resource.name, str(source.kind)]
218
+ if source.metadata.namespace:
219
+ name.append(str(source.metadata.namespace))
220
+ name.append(str(source.metadata.name))
221
+ usage = composite.resources['_'.join(name)]('apiextensions.crossplane.io/v1beta1', 'Usage')
222
+ #usage = composite.resources['_'.join(name)]('protection.crossplane.io/v1beta1', 'Usage')
223
+ if resource.metadata.namespace:
224
+ usage.metadata.namespace = resource.metadata.namespace
225
+ usage.spec.reason = '\n'.join(dependencies)
226
+ usage.spec.replayDeletion = True
227
+ usage.spec.by.apiVersion = resource.apiVersion
228
+ usage.spec.by.kind = resource.kind
229
+ usage.spec.by.resourceRef.name = resource.observed.metadata.name
230
+ usage.spec.of.apiVersion = source.apiVersion
231
+ usage.spec.of.kind = source.kind
232
+ if source.metadata.namespace:
233
+ usage.spec.of.resourceRef.namespace = source.metadata.namespace
234
+ usage.spec.of.resourceRef.name = source.observed.metadata.name
130
235
 
236
+ def process_unknowns(self, composite):
131
237
  unknownResources = []
132
238
  warningResources = []
133
239
  fatalResources = []
@@ -148,11 +254,11 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
148
254
  destination = self.trimFullName(destination)
149
255
  source = self.trimFullName(source)
150
256
  if fatal:
151
- logger.error(f'Observed unknown: {destination} = {source}')
257
+ composite.logger.error(f'Observed unknown: {destination} = {source}')
152
258
  elif warning:
153
- logger.warning(f'Observed unknown: {destination} = {source}')
259
+ composite.logger.warning(f'Observed unknown: {destination} = {source}')
154
260
  else:
155
- logger.debug(f'Desired unknown: {destination} = {source}')
261
+ composite.logger.debug(f'Desired unknown: {destination} = {source}')
156
262
  if resource.observed:
157
263
  resource.desired._patchUnknowns(resource.observed)
158
264
  elif self.renderUnknowns:
@@ -161,19 +267,19 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
161
267
  del composite.resources[name]
162
268
 
163
269
  if fatalResources:
164
- level = logger.error
270
+ level = composite.logger.error
165
271
  reason = 'FatalUnknowns'
166
272
  message = f"Observed resources with unknowns: {','.join(fatalResources)}"
167
273
  status = False
168
274
  event = composite.events.fatal
169
275
  elif warningResources:
170
- level = logger.warning
276
+ level = composite.logger.warning
171
277
  reason = 'ObservedUnknowns'
172
278
  message = f"Observed resources with unknowns: {','.join(warningResources)}"
173
279
  status = False
174
280
  event = composite.events.warning
175
281
  elif unknownResources:
176
- level = logger.info
282
+ level = composite.logger.info
177
283
  reason = 'DesiredUnknowns'
178
284
  message = f"Desired resources with unknowns: {','.join(unknownResources)}"
179
285
  status = False
@@ -190,42 +296,19 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
190
296
  if event:
191
297
  event(reason, message)
192
298
 
299
+ def process_auto_readies(self, composite):
193
300
  for name, resource in composite.resources:
194
301
  if resource.autoReady or (resource.autoReady is None and composite.autoReady):
195
302
  if resource.ready is None:
196
303
  if resource.conditions.Ready.status:
197
304
  resource.ready = True
198
305
 
199
- logger.info('Completed compose')
200
- return composite.response._message
201
-
202
- def fatal(self, request, logger, message, exception=None):
203
- if exception:
204
- message += ' exceptiion'
205
- logger.exception(message)
206
- m = str(exception)
207
- if not m:
208
- m = exception.__class__.__name__
209
- message += ': ' + m
210
- else:
211
- logger.error(message)
212
- return fnv1.RunFunctionResponse(
213
- meta=fnv1.ResponseMeta(
214
- tag=request.meta.tag,
215
- ),
216
- results=[
217
- fnv1.Result(
218
- severity=fnv1.SEVERITY_FATAL,
219
- message=message,
220
- )
221
- ]
222
- )
223
-
224
306
  def trimFullName(self, name):
225
307
  name = name.split('.')
226
308
  for values in (
309
+ ('request', 'observed', 'composite', 'resource'),
227
310
  ('request', 'observed', 'resources', None, 'resource'),
228
- ('request', 'extra_resources', None, 'items', 'resource'),
311
+ ('request', 'extra_resources', None, 'items', None, 'resource'),
229
312
  ('response', 'desired', 'resources', None, 'resource'),
230
313
  ):
231
314
  if len(values) < len(name):