crossplane-function-pythonic 0.0.10__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',
@@ -1,5 +1,6 @@
1
1
 
2
2
  import datetime
3
+ from google.protobuf.duration_pb2 import Duration
3
4
  from crossplane.function.proto.v1 import run_function_pb2 as fnv1
4
5
 
5
6
  from . import protobuf
@@ -9,8 +10,18 @@ _notset = object()
9
10
 
10
11
 
11
12
  class BaseComposite:
12
- def __init__(self, request, response, logger):
13
+ def __init__(self, request, logger):
13
14
  self.request = protobuf.Message(None, 'request', request.DESCRIPTOR, request, 'Function Request')
15
+ response = fnv1.RunFunctionResponse(
16
+ meta=fnv1.ResponseMeta(
17
+ tag=request.meta.tag,
18
+ ttl=Duration(
19
+ seconds=60,
20
+ ),
21
+ ),
22
+ desired=request.desired,
23
+ context=request.context,
24
+ )
14
25
  self.response = protobuf.Message(None, 'response', response.DESCRIPTOR, response)
15
26
  self.logger = logger
16
27
  self.credentials = Credentials(self.request)
@@ -20,6 +31,7 @@ class BaseComposite:
20
31
  self.resources = Resources(self)
21
32
  self.unknownsFatal = True
22
33
  self.autoReady = True
34
+ self.usages = False
23
35
 
24
36
  observed = self.request.observed.composite
25
37
  desired = self.response.desired.composite
@@ -36,11 +48,23 @@ class BaseComposite:
36
48
 
37
49
  @property
38
50
  def ttl(self):
39
- return self.response.meta.ttl.seconds
51
+ if self.response.meta.ttl.nanos:
52
+ return float(self.response.meta.ttl.seconds) + (float(self.response.meta.ttl.nanos) / 1000000000.0)
53
+ return int(self.response.meta.ttl.seconds)
40
54
 
41
55
  @ttl.setter
42
56
  def ttl(self, ttl):
43
- self.response.meta.ttl.seconds = ttl
57
+ if isinstance(ttl, int):
58
+ self.response.meta.ttl.seconds = ttl
59
+ self.response.meta.ttl.nanos = 0
60
+ elif isinstance(ttl, float):
61
+ self.response.meta.ttl.seconds = int(ttl)
62
+ if ttl.is_integer():
63
+ self.response.meta.ttl.nanos = 0
64
+ else:
65
+ self.response.meta.ttl.nanos = int((ttl - int(self.response.meta.ttl.seconds)) * 1000000000)
66
+ else:
67
+ raise ValueError('ttl must be an int or float')
44
68
 
45
69
  @property
46
70
  def ready(self):
@@ -55,7 +79,7 @@ class BaseComposite:
55
79
  def ready(self, ready):
56
80
  if ready:
57
81
  ready = fnv1.Ready.READY_TRUE
58
- elif ready == None or (isinstance(ready, protobuf.Values) and ready._isUnknown):
82
+ elif ready == None or (isinstance(ready, protobuf.Value) and ready._isUnknown):
59
83
  ready = fnv1.Ready.READY_UNSPECIFIED
60
84
  else:
61
85
  ready = fnv1.Ready.READY_FALSE
@@ -73,22 +97,46 @@ class Credentials:
73
97
  return self[key]
74
98
 
75
99
  def __getitem__(self, key):
76
- return self._request.credentials[key].credentials_data.data
100
+ return Credential(self._request.credentials[key])
77
101
 
78
102
  def __bool__(self):
79
- return bool(_request.credentials)
103
+ return bool(self._request.credentials)
80
104
 
81
105
  def __len__(self):
82
106
  return len(self._request.credentials)
83
107
 
84
108
  def __contains__(self, key):
85
- return key in _request.credentials
109
+ return key in self._request.credentials
86
110
 
87
111
  def __iter__(self):
88
112
  for key, resource in self._request.credentials:
89
113
  yield key, self[key]
90
114
 
91
115
 
116
+ class Credential:
117
+ def __init__(self, credential):
118
+ self.__dict__['_credential'] = credential
119
+
120
+ def __getattr__(self, key):
121
+ return self[key]
122
+
123
+ def __getitem__(self, key):
124
+ return self._credential.credential_data.data[key]
125
+
126
+ def __bool__(self):
127
+ return bool(self._credential.credential_data.data)
128
+
129
+ def __len__(self):
130
+ return len(self._credential.credential_data.data)
131
+
132
+ def __contains__(self, key):
133
+ return key in self._credential.credential_data.data
134
+
135
+ def __iter__(self):
136
+ for key, resource in self._credential.credential_data.data:
137
+ yield key, self[key]
138
+
139
+
92
140
  class Resources:
93
141
  def __init__(self, composite):
94
142
  self.__dict__['_composite'] = composite
@@ -137,6 +185,7 @@ class Resource:
137
185
  self.connection = Connection(observed)
138
186
  self.unknownsFatal = None
139
187
  self.autoReady = None
188
+ self.usages = None
140
189
 
141
190
  def __call__(self, apiVersion=_notset, kind=_notset, namespace=_notset, name=_notset):
142
191
  self.desired()
@@ -152,7 +201,7 @@ class Resource:
152
201
 
153
202
  @property
154
203
  def apiVersion(self):
155
- return self.observed.apiVersion
204
+ return self.desired.apiVersion
156
205
 
157
206
  @apiVersion.setter
158
207
  def apiVersion(self, apiVersion):
@@ -160,7 +209,7 @@ class Resource:
160
209
 
161
210
  @property
162
211
  def kind(self):
163
- return self.observed.kind
212
+ return self.desired.kind
164
213
 
165
214
  @kind.setter
166
215
  def kind(self, kind):
@@ -218,7 +267,7 @@ class Resource:
218
267
  def ready(self, ready):
219
268
  if ready:
220
269
  ready = fnv1.Ready.READY_TRUE
221
- elif ready == None or (isinstance(ready, protobuf.Values) and ready._isUnknown):
270
+ elif ready == None or (isinstance(ready, protobuf.Value) and ready._isUnknown):
222
271
  ready = fnv1.Ready.READY_UNSPECIFIED
223
272
  else:
224
273
  ready = fnv1.Ready.READY_FALSE
@@ -329,8 +378,8 @@ class RequiredResources:
329
378
  elif isinstance(entry, (list, tuple)):
330
379
  self._selector.match_labels.labels[entry[0]] = entry[1]
331
380
 
332
- def __getitem__(self, key):
333
- return RequiredResource(self.name, self._resources.items[key])
381
+ def __getitem__(self, ix):
382
+ return RequiredResource(self.name, ix, self._resources.items[ix])
334
383
 
335
384
  def __bool__(self):
336
385
  return bool(self._resources.items)
@@ -344,8 +393,9 @@ class RequiredResources:
344
393
 
345
394
 
346
395
  class RequiredResource:
347
- def __init__(self, name, resource):
396
+ def __init__(self, name, ix, resource):
348
397
  self.name = name
398
+ self.ix = ix
349
399
  self.observed = resource.resource
350
400
  self.apiVersion = self.observed.apiVersion
351
401
  self.kind = self.observed.kind
@@ -440,7 +490,7 @@ class Condition(protobuf.ProtobufValue):
440
490
  condition.status = fnv1.Status.STATUS_CONDITION_TRUE
441
491
  elif status == None:
442
492
  condition.status = fnv1.Status.STATUS_CONDITION_UNKNOWN
443
- elif isinstance(status, protobuf.Values) and status._isUnknown:
493
+ elif isinstance(status, protobuf.Value) and status._isUnknown:
444
494
  condition.status = fnv1.Status.STATUS_CONDITION_UNSPECIFIED
445
495
  else:
446
496
  condition.status = fnv1.Status.STATUS_CONDITION_FALSE
@@ -474,7 +524,7 @@ class Condition(protobuf.ProtobufValue):
474
524
  if observed.type == self.type:
475
525
  time = observed.lastTransitionTime
476
526
  if time:
477
- return datetime.datetime.fromisoformat(time)
527
+ return datetime.datetime.fromisoformat(str(time))
478
528
  return None
479
529
 
480
530
  @property
@@ -487,7 +537,7 @@ class Condition(protobuf.ProtobufValue):
487
537
  condition = self._find_condition(True)
488
538
  if claim:
489
539
  condition.target = fnv1.Target.TARGET_COMPOSITE_AND_CLAIM
490
- elif claim == None or (isinstance(claim, protobuf.Values) and claim._isUnknown):
540
+ elif claim == None or (isinstance(claim, protobuf.Value) and claim._isUnknown):
491
541
  condition.target = fnv1.Target.TARGET_UNSPECIFIED
492
542
  else:
493
543
  condition.target = fnv1.Target.TARGET_COMPOSITE
@@ -587,7 +637,7 @@ class Events:
587
637
  def __getitem__(self, key):
588
638
  if key >= len(self._results):
589
639
  return Event()
590
- return Event(self._results[ix])
640
+ return Event(self._results[key])
591
641
 
592
642
  def __iter__(self):
593
643
  for ix in range(len(self._results)):
@@ -664,7 +714,7 @@ class Event:
664
714
  if bool(self):
665
715
  if claim:
666
716
  self._result.target = fnv1.Target.TARGET_COMPOSITE_AND_CLAIM
667
- elif claim == None or (isinstance(claim, protobuf.Values) and claim._isUnknown):
717
+ elif claim == None or (isinstance(claim, protobuf.Value) and claim._isUnknown):
668
718
  self._result.target = fnv1.Target.TARGET_UNSPECIFIED
669
719
  else:
670
720
  self._result.target = fnv1.Target.TARGET_COMPOSITE
@@ -1,38 +1,26 @@
1
1
  """A Crossplane composition function."""
2
2
 
3
3
  import asyncio
4
- import base64
5
- import builtins
6
4
  import importlib
7
5
  import inspect
8
6
  import logging
9
7
  import sys
10
8
 
11
9
  import grpc
12
- import crossplane.function.response
13
10
  from crossplane.function.proto.v1 import run_function_pb2 as fnv1
14
11
  from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1
15
12
  from .. import pythonic
16
13
 
17
- builtins.BaseComposite = pythonic.BaseComposite
18
- builtins.append = pythonic.append
19
- builtins.Map = pythonic.Map
20
- builtins.List = pythonic.List
21
- builtins.Unknown = pythonic.Unknown
22
- builtins.Yaml = pythonic.Yaml
23
- builtins.Json = pythonic.Json
24
- builtins.B64Encode = pythonic.B64Encode
25
- builtins.B64Decode = pythonic.B64Decode
26
-
27
14
  logger = logging.getLogger(__name__)
28
15
 
29
16
 
30
17
  class FunctionRunner(grpcv1.FunctionRunnerService):
31
18
  """A FunctionRunner handles gRPC RunFunctionRequests."""
32
19
 
33
- def __init__(self, debug=False):
20
+ def __init__(self, debug=False, renderUnknowns=False):
34
21
  """Create a new FunctionRunner."""
35
22
  self.debug = debug
23
+ self.renderUnknowns = renderUnknowns
36
24
  self.clazzes = {}
37
25
 
38
26
  def invalidate_module(self, module):
@@ -46,9 +34,8 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
46
34
  ) -> fnv1.RunFunctionResponse:
47
35
  try:
48
36
  return await self.run_function(request)
49
- except:
50
- logger.exception('Exception thrown in run fuction')
51
- raise
37
+ except Exception as e:
38
+ return self.fatal(request, logger, 'RunFunction', e)
52
39
 
53
40
  async def run_function(self, request):
54
41
  composite = request.observed.composite.resource
@@ -56,27 +43,22 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
56
43
  name.append(composite['kind'])
57
44
  name.append(composite['metadata']['name'])
58
45
  logger = logging.getLogger('.'.join(name))
59
- if 'iteration' in request.context:
60
- request.context['iteration'] = request.context['iteration'] + 1
61
- else:
62
- request.context['iteration'] = 1
63
- logger.debug(f"Starting compose, {ordinal(request.context['iteration'])} pass")
64
-
65
- response = crossplane.function.response.to(request)
66
46
 
67
47
  if composite['apiVersion'] == 'pythonic.fortra.com/v1alpha1' and composite['kind'] == 'Composite':
68
- if 'composite' not in composite['spec']:
69
- logger.error('Missing spec "composite"')
70
- crossplane.function.response.fatal(response, 'Missing spec "composite"')
71
- return response
48
+ if 'spec' not in composite or 'composite' not in composite['spec']:
49
+ return self.fatal(request, logger, 'Missing spec "composite"')
72
50
  composite = composite['spec']['composite']
73
51
  else:
74
52
  if 'composite' not in request.input:
75
- logger.error('Missing input "composite"')
76
- crossplane.function.response.fatal(response, 'Missing input "composite"')
77
- return response
53
+ return self.fatal(request, logger, 'Missing input "composite"')
78
54
  composite = request.input['composite']
79
55
 
56
+ # Ideally this is something the Function API provides
57
+ if 'step' in request.input:
58
+ step = request.input['step']
59
+ else:
60
+ step = str(hash(composite))
61
+
80
62
  clazz = self.clazzes.get(composite)
81
63
  if not clazz:
82
64
  if '\n' in composite:
@@ -84,82 +66,174 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
84
66
  try:
85
67
  exec(composite, module.__dict__)
86
68
  except Exception as e:
87
- logger.exception('Exec exception')
88
- crossplane.function.response.fatal(response, f"Exec exception: {e}")
89
- return response
69
+ return self.fatal(request, logger, 'Exec', e)
90
70
  for field in dir(module):
91
71
  value = getattr(module, field)
92
- if inspect.isclass(value) and issubclass(value, BaseComposite) and value != BaseComposite:
72
+ if inspect.isclass(value) and issubclass(value, pythonic.BaseComposite) and value != pythonic.BaseComposite:
93
73
  if clazz:
94
- logger.error('Composite script has multiple BaseComposite classes')
95
- crossplane.function.response.fatal(response, 'Composite script has multiple BaseComposite classes')
96
- return response
74
+ return self.fatal(request, logger, 'Composite script has multiple BaseComposite classes')
97
75
  clazz = value
98
76
  if not clazz:
99
- logger.error('Composite script does not have have a BaseComposite class')
100
- crossplane.function.response.fatal(response, 'Composite script does have have a BaseComposite class')
101
- return response
77
+ return self.fatal(request, logger, 'Composite script does not have a BaseComposite class')
102
78
  else:
103
79
  composite = composite.rsplit('.', 1)
104
80
  if len(composite) == 1:
105
- logger.error(f"Composite class name does not include module: {composite[0]}")
106
- crossplane.function.response.fatal(response, f"Composite class name does not include module: {composite[0]}")
107
- return response
81
+ return self.fatal(request, logger, f"Composite class name does not include module: {composite[0]}")
108
82
  try:
109
83
  module = importlib.import_module(composite[0])
110
84
  except Exception as e:
111
- logger.error(str(e))
112
- crossplane.function.response.fatal(response, f"Import module exception: {e}")
113
- return response
85
+ return self.fatal(request, logger, 'Import module', e)
114
86
  clazz = getattr(module, composite[1], None)
115
87
  if not clazz:
116
- logger.error(f"{composite[0]} did not define: {composite[1]}")
117
- crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
118
- return response
88
+ return self.fatal(request, logger, f"{composite[0]} does not define: {composite[1]}")
119
89
  composite = '.'.join(composite)
120
90
  if not inspect.isclass(clazz):
121
- logger.error(f"{composite} is not a class")
122
- crossplane.function.response.fatal(response, f"{composite} is not a class")
123
- return response
124
- if not issubclass(clazz, BaseComposite):
125
- logger.error(f"{composite} is not a subclass of BaseComposite")
126
- crossplane.function.response.fatal(response, f"{composite} is not a subclass of BaseComposite")
127
- return response
91
+ return self.fatal(request, logger, f"{composite} is not a class")
92
+ if not issubclass(clazz, pythonic.BaseComposite):
93
+ return self.fatal(request, logger, f"{composite} is not a subclass of BaseComposite")
128
94
  self.clazzes[composite] = clazz
129
95
 
130
96
  try:
131
- composite = clazz(request, response, logger)
97
+ composite = clazz(request, logger)
132
98
  except Exception as e:
133
- logger.exception('Instatiate exception')
134
- crossplane.function.response.fatal(response, f"Instatiate exception: {e}")
135
- return response
99
+ return self.fatal(request, logger, 'Instantiate', e)
100
+
101
+ step = composite.context._pythonic[step]
102
+ iteration = int(step.iteration) + 1
103
+ step.iteration = iteration
104
+ composite.context.iteration = iteration
105
+ logger.debug(f"Starting compose, {ordinal(len(composite.context._pythonic))} step, {ordinal(iteration)} pass")
136
106
 
137
107
  try:
138
108
  result = composite.compose()
139
109
  if asyncio.iscoroutine(result):
140
110
  await result
141
111
  except Exception as e:
142
- logger.exception('Compose exception')
143
- crossplane.function.response.fatal(response, f"Compose exception: {e}")
144
- return response
112
+ return self.fatal(request, logger, 'Compose', e)
113
+
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')
145
121
 
146
- requested = []
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 = []
147
148
  for name, required in composite.requireds:
148
149
  if required.apiVersion and required.kind:
149
- r = Map(apiVersion=required.apiVersion, kind=required.kind)
150
+ r = pythonic.Map(apiVersion=required.apiVersion, kind=required.kind)
150
151
  if required.namespace:
151
152
  r.namespace = required.namespace
152
153
  if required.matchName:
153
154
  r.matchName = required.matchName
154
155
  for key, value in required.matchLabels:
155
156
  r.matchLabels[key] = value
156
- if r != composite.context._requireds[name]:
157
- composite.context._requireds[name] = r
158
- requested.append(name)
159
- if requested:
160
- logger.info(f"Requireds requested: {','.join(requested)}")
161
- return response
157
+ if r != step.requireds[name]:
158
+ step.requireds[name] = r
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
162
235
 
236
+ def process_unknowns(self, composite):
163
237
  unknownResources = []
164
238
  warningResources = []
165
239
  fatalResources = []
@@ -180,30 +254,32 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
180
254
  destination = self.trimFullName(destination)
181
255
  source = self.trimFullName(source)
182
256
  if fatal:
183
- logger.error(f'Observed unknown: {destination} = {source}')
257
+ composite.logger.error(f'Observed unknown: {destination} = {source}')
184
258
  elif warning:
185
- logger.warning(f'Observed unknown: {destination} = {source}')
259
+ composite.logger.warning(f'Observed unknown: {destination} = {source}')
186
260
  else:
187
- logger.debug(f'Desired unknown: {destination} = {source}')
261
+ composite.logger.debug(f'Desired unknown: {destination} = {source}')
188
262
  if resource.observed:
189
263
  resource.desired._patchUnknowns(resource.observed)
264
+ elif self.renderUnknowns:
265
+ resource.desired._renderUnknowns(self.trimFullName)
190
266
  else:
191
267
  del composite.resources[name]
192
268
 
193
269
  if fatalResources:
194
- level = logger.error
270
+ level = composite.logger.error
195
271
  reason = 'FatalUnknowns'
196
272
  message = f"Observed resources with unknowns: {','.join(fatalResources)}"
197
273
  status = False
198
274
  event = composite.events.fatal
199
275
  elif warningResources:
200
- level = logger.warning
276
+ level = composite.logger.warning
201
277
  reason = 'ObservedUnknowns'
202
278
  message = f"Observed resources with unknowns: {','.join(warningResources)}"
203
279
  status = False
204
280
  event = composite.events.warning
205
281
  elif unknownResources:
206
- level = logger.info
282
+ level = composite.logger.info
207
283
  reason = 'DesiredUnknowns'
208
284
  message = f"Desired resources with unknowns: {','.join(unknownResources)}"
209
285
  status = False
@@ -220,26 +296,30 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
220
296
  if event:
221
297
  event(reason, message)
222
298
 
299
+ def process_auto_readies(self, composite):
223
300
  for name, resource in composite.resources:
224
301
  if resource.autoReady or (resource.autoReady is None and composite.autoReady):
225
302
  if resource.ready is None:
226
303
  if resource.conditions.Ready.status:
227
304
  resource.ready = True
228
305
 
229
- logger.info('Completed compose')
230
- return response
231
-
232
306
  def trimFullName(self, name):
233
307
  name = name.split('.')
234
308
  for values in (
309
+ ('request', 'observed', 'composite', 'resource'),
235
310
  ('request', 'observed', 'resources', None, 'resource'),
236
- ('request', 'extra_resources', None, 'items', 'resource'),
311
+ ('request', 'extra_resources', None, 'items', None, 'resource'),
237
312
  ('response', 'desired', 'resources', None, 'resource'),
238
313
  ):
239
- if len(values) <= len(name):
240
- for ix, value in enumerate(values):
241
- if value and value != name[ix] and not name[ix].startswith(f"{value}["):
242
- break
314
+ if len(values) < len(name):
315
+ ix = 0
316
+ for iv, value in enumerate(values):
317
+ if value:
318
+ if value != name[ix]:
319
+ if not name[ix].startswith(f"{values[iv]}[") or iv+1 >= len(values) or values[iv+1]:
320
+ break
321
+ continue
322
+ ix += 1
243
323
  else:
244
324
  ix = 0
245
325
  for value in values:
@@ -251,7 +331,6 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
251
331
  del name[ix]
252
332
  else:
253
333
  name[ix] = name[ix][len(value):]
254
- ix += 1
255
334
  else:
256
335
  ix += 1
257
336
  break
@@ -268,4 +347,13 @@ def ordinal(ix):
268
347
 
269
348
 
270
349
  class Module:
271
- pass
350
+ def __init__(self):
351
+ self.BaseComposite = pythonic.BaseComposite
352
+ self.append = pythonic.append
353
+ self.Map = pythonic.Map
354
+ self.List = pythonic.List
355
+ self.Unknown = pythonic.Unknown
356
+ self.Yaml = pythonic.Yaml
357
+ self.Json = pythonic.Json
358
+ self.B64Encode = pythonic.B64Encode
359
+ self.B64Decode = pythonic.B64Decode
@@ -92,6 +92,11 @@ class Main:
92
92
  action='store_true',
93
93
  help='Allow oversized protobuf messages'
94
94
  )
95
+ parser.add_argument(
96
+ '--render-unknowns',
97
+ action='store_true',
98
+ help='Render resources with unknowns, useful during local develomment'
99
+ )
95
100
  args = parser.parse_args()
96
101
  if not args.tls_certs_dir and not args.insecure:
97
102
  print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr)
@@ -117,7 +122,7 @@ class Main:
117
122
  api_implementation._c_module.SetAllowOversizeProtos(True)
118
123
 
119
124
  grpc.aio.init_grpc_aio()
120
- grpc_runner = function.FunctionRunner(args.debug)
125
+ grpc_runner = function.FunctionRunner(args.debug, args.render_unknowns)
121
126
  grpc_server = grpc.aio.server()
122
127
  grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
123
128
  if args.insecure: