Skip to content

Commit 9764220

Browse files
author
Patrick J. McNerthney
committed
Greatly improved Unknowns handling
1 parent eb6cec0 commit 9764220

File tree

10 files changed

+422
-99
lines changed

10 files changed

+422
-99
lines changed

README.md

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,16 @@ spec:
5656
package: ghcr.io/fortra/function-pythonic:v0.0.3
5757
```
5858
59-
## Managed Resource Dependencies
59+
## Composed Resource Dependencies
6060
61-
function-pythonic automatically handles dependencies between managed resources.
61+
function-pythonic automatically handles dependencies between composed resources.
6262
6363
Just compose everything as if it is immediately created and the framework will delay
6464
the creation of any resources which depend on other resources which do not exist yet.
6565
In other words, it accomplishes what [function-sequencer](https://github.com/crossplane-contrib/function-sequencer)
6666
provides, but it automatically detects the dependencies.
6767
68-
If a resource has been composed and a dependency no longer exists due to some unexpected
68+
If a resource has been created and a dependency no longer exists due to some unexpected
6969
condition, the observed value for that field will automatically be used.
7070
7171
Take the following example:
@@ -83,9 +83,12 @@ subnet.spec.forProvider.cidrBlock = '10.0.0.0/20'
8383
If the Subnet does not yet exist, the framework will detect if the vpcId set
8484
in the Subnet is unknown, and will delay the creation of the subnet.
8585

86-
Once the Subnet has been created, if for some mysterious reason the vpcId passed
87-
to the Subnet is unknown, the framework will automatically use the vpcId in the
88-
observed Subnet.
86+
Once the Subnet has been created, if for some unexpected reason the vpcId passed
87+
to the Subnet is unknown, the framework will detect it and either terminate
88+
the Composite composition or use the vpcId in the observed Subnet. The default
89+
action taken is to fail fail by terminating the composition. This can be
90+
overridden overall by setting the Composite.unknownsFatal field to False,
91+
or at the individual resource levelby setting the Resource.unknownsFatal to False.
8992

9093
## Pythonic access of Protobuf Messages
9194

@@ -186,37 +189,39 @@ The BaseComposite also provides access to the following Crossplane Function leve
186189
| self.response | Low level direct access to the RunFunctionResponse message |
187190
| self.logger | Python logger to log messages to the running function stdout |
188191
| self.ttl | Get or set the response TTL, in seconds |
189-
| self.autoReady | Perform auto ready processing after the compose method returns, default True |
190192
| self.credentials | The request credentials |
191193
| self.context | The response context, initialized from the request context |
192194
| self.environment | The response environment, initialized from the request context environment |
193195
| self.requireds | Request and read additional local Kubernetes resources |
194-
| self.resources | Define and process managed resources |
196+
| self.resources | Define and process composed resources |
195197
| self.results | Returned results on the Composite and optionally on the Claim |
198+
| self.unknownsFatal | Terminate the composition if already created resources are assigned unknown values, default True |
199+
| self.autoReady | Perform auto ready processing after the compose method returns, default True |
196200

197-
### Managed Resources
201+
### Composed Resources
198202

199-
Creating and accessing managed resources is performed using the `BaseComposite.resources` field.
200-
`BaseComposite.resources` is a dictionary of the managed resources whose key is the composition
203+
Creating and accessing composed resources is performed using the `BaseComposite.resources` field.
204+
`BaseComposite.resources` is a dictionary of the composed resources whose key is the composition
201205
resource name. The value returned when getting a resource from BaseComposite is the following
202206
Resource class:
203207

204208
| Field | Description |
205209
| ----- | ----------- |
206210
| Resource(apiVersion,kind,namespace,name) | Reset the resource and set the optional parameters |
207-
| Resource.name | The composition resource name of the managed resource |
208-
| Resource.observed | Low level direct access to the observed managed resource |
209-
| Resource.desired | Low level direct access to the desired managed resource |
210-
| Resource.apiVersion | The managed resource apiVersion |
211-
| Resource.kind | The managed resource kind |
212-
| Resource.externalName | The managed resource external name |
213-
| Resource.metadata | The managed resource desired metadata |
211+
| Resource.name | The composition resource name of the composed resource |
212+
| Resource.observed | Low level direct access to the observed composed resource |
213+
| Resource.desired | Low level direct access to the desired composed resource |
214+
| Resource.apiVersion | The composed resource apiVersion |
215+
| Resource.kind | The composed resource kind |
216+
| Resource.externalName | The composed resource external name |
217+
| Resource.metadata | The composed resource desired metadata |
214218
| Resource.spec | The resource spec |
215219
| Resource.data | The resource data |
216220
| Resource.status | The resource status |
217221
| Resource.conditions | The resource conditions |
218222
| Resource.connection | The resource connection details |
219223
| Resource.ready | The resource ready state |
224+
| Resource.unknownsFatal | Terminate the composition if this resource has been created and is assigned unknown values, default is Composite.unknownsFatal |
220225

221226
### Required Resources (AKA Extra Resources)
222227

crossplane/pythonic/composite.py

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
class BaseComposite:
1212
def __init__(self, request, response, logger):
13-
self.request = protobuf.Message(None, None, request.DESCRIPTOR, request, 'Function Request')
14-
self.response = protobuf.Message(None, None, response.DESCRIPTOR, response)
13+
self.request = protobuf.Message(None, 'request', request.DESCRIPTOR, request, 'Function Request')
14+
self.response = protobuf.Message(None, 'response', response.DESCRIPTOR, response)
1515
self.logger = logger
1616
self.autoReady = True
1717
self.credentials = Credentials(self.request)
@@ -20,6 +20,7 @@ def __init__(self, request, response, logger):
2020
self.requireds = Requireds(self)
2121
self.resources = Resources(self)
2222
self.results = Results(self.response)
23+
self.unknownsFatal = True
2324

2425
observed = self.request.observed.composite
2526
desired = self.response.desired.composite
@@ -134,6 +135,7 @@ def __init__(self, composite, name):
134135
self.desired = desired.resource
135136
self.conditions = Conditions(observed)
136137
self.connection = Connection(observed)
138+
self.unknownsFatal = None
137139

138140
def __call__(self, apiVersion=_notset, kind=_notset, namespace=_notset, name=_notset):
139141
self.desired()
@@ -333,22 +335,38 @@ class Results:
333335
def __init__(self, response):
334336
self._results = response.results
335337

336-
def __call__(self, message=_notset, fatal=_notset, warning=_notset, reaoson=_notset, claim=_notset):
337-
result = Result(self._results.add())
338-
if fatal != _notset:
339-
result.fatal = fatal
340-
elif warning != _notset:
341-
result.warning = warning
342-
if message != _notset:
343-
result.message = message
338+
def info(self, message, reason=_notset, claim=_notset):
339+
result = Result(self._results.append())
340+
result.info = True
341+
result.message = message
342+
if reason != _notset:
343+
result.reason = reason
344+
if claim != _notset:
345+
result.claim = claim
346+
return result
347+
348+
def warning(self, message, reason=_notset, claim=_notset):
349+
result = Result(self._results.append())
350+
result.warning = True
351+
result.message = message
352+
if reason != _notset:
353+
result.reason = reason
354+
if claim != _notset:
355+
result.claim = claim
356+
return result
357+
358+
def fatal(self, message, reason=_notset, claim=_notset):
359+
result = Result(self._results.append())
360+
result.fatal = True
361+
result.message = message
344362
if reason != _notset:
345363
result.reason = reason
346364
if claim != _notset:
347365
result.claim = claim
348366
return result
349367

350368
def __bool__(self):
351-
return len(self._results) > 0
369+
return len(self) > 0
352370

353371
def __len__(self):
354372
len(self._results)
@@ -364,35 +382,47 @@ def __iter__(self):
364382

365383

366384
class Result:
367-
def __init(self, result=None):
385+
def __init__(self, result=None):
368386
self._result = result
369387

370388
def __bool__(self):
371389
return self._result is not None
372390

373391
@property
374-
def fatal(self):
375-
return bool(self) and self._result == fnv1.Severity.SEVERITY_FATAL
392+
def info(self):
393+
return bool(self) and self._result.severity == fnv1.Severity.SEVERITY_NORMAL
376394

377-
@fatal.setter
378-
def fatal(self, fatal):
395+
@info.setter
396+
def info(self, info):
379397
if bool(self):
380-
if fatal:
381-
self._result = fnv1.Severity.SEVERITY_FATAL
398+
if info:
399+
self._result.severity = fnv1.Severity.SEVERITY_NORMAL
382400
else:
383-
self._result = fnv1.Severity.SEVERITY_NORMAL
401+
self._result.severity = fnv1.Severity.SEVERITY_UNSPECIFIED
384402

385403
@property
386404
def warning(self):
387-
return bool(self) and self._result == fnv1.Severity.SEVERITY_WARNING
405+
return bool(self) and self._result.severity == fnv1.Severity.SEVERITY_WARNING
388406

389407
@warning.setter
390408
def warning(self, warning):
391409
if bool(self):
392410
if warning:
393-
self._result = fnv1.Severity.SEVERITY_WARNING
411+
self._result.severity = fnv1.Severity.SEVERITY_WARNING
412+
else:
413+
self._result.severity = fnv1.Severity.SEVERITY_NORMAL
414+
415+
@property
416+
def fatal(self):
417+
return bool(self) and self._result.severity == fnv1.Severity.SEVERITY_FATAL
418+
419+
@fatal.setter
420+
def fatal(self, fatal):
421+
if bool(self):
422+
if fatal:
423+
self._result.severity = fnv1.Severity.SEVERITY_FATAL
394424
else:
395-
self._result = fnv1.Severity.SEVERITY_NORMAL
425+
self._result.severity = fnv1.Severity.SEVERITY_NORMAL
396426

397427
@property
398428
def message(self):

crossplane/pythonic/function.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
class FunctionRunner(grpcv1.FunctionRunnerService):
2727
"""A FunctionRunner handles gRPC RunFunctionRequests."""
2828

29-
def __init__(self):
29+
def __init__(self, debug=False):
3030
"""Create a new FunctionRunner."""
31+
self.debug = debug
3132
self.logger = crossplane.function.logging.get_logger()
3233
self.clazzes = {}
3334

@@ -117,12 +118,57 @@ async def RunFunction(
117118
logger.exception('Compose exception')
118119
return response
119120

120-
for name, resource in [entry for entry in composite.resources]:
121-
if resource.desired._hasUnknowns:
121+
unknownResources = []
122+
warningResources = []
123+
fatalResources = []
124+
for name, resource in sorted(entry for entry in composite.resources):
125+
unknowns = resource.desired._getUnknowns
126+
if unknowns:
127+
unknownResources.append(name)
128+
warning = False
129+
fatal = False
130+
if resource.observed:
131+
warningResources.append(name)
132+
warning = True
133+
if resource.unknownsFatal:
134+
fatalResources.append(name)
135+
fatal = True
136+
elif resource.unknownsFatal is None and composite.unknownsFatal:
137+
fatalResources.append(name)
138+
fatal = True
139+
if self.debug:
140+
for destination, source in sorted(unknowns.items()):
141+
destination = self._trimFullName('response', 'desired', destination)
142+
source = self._trimFullName('request', 'observed', source)
143+
if fatal:
144+
logger.error('Observed unknown assignment', destination=destination, source=source)
145+
elif warning:
146+
logger.warning('Observed unknown assignment', destination=destination, source=source)
147+
else:
148+
logger.debug('New unknown assignment', destination=destination, source=source)
122149
if resource.observed:
123150
resource.desired._patchUnknowns(resource.observed)
124151
else:
125152
del composite.resources[name]
153+
if fatalResources:
154+
if not self.debug:
155+
logger.error('Observed Resources with unknown assignments', resources=fatalResources)
156+
message = f"Observed Resources with unknown assignments: {', '.join(fatalResources)}"
157+
composite.conditions.Unknowns(False, 'FatalUnknowns', message)
158+
composite.results.fatal(message, 'FatalUnknowns')
159+
return response
160+
if warningResources:
161+
if not self.debug:
162+
logger.warning('Observed Resources with unknown assignments', resources=fatalResources)
163+
message = f"Observed Resources with unknown assignments: {', '.join(warningResources)}"
164+
composite.conditions.Unknowns(False, 'ObservedUnknowns', message)
165+
composite.results.warning(message, 'ObservedUnknowns')
166+
elif unknownResources:
167+
if not self.debug:
168+
logger.info('New Resources with unknown assignments', resources=unknownResources)
169+
message = f"New Resources with unknown assignments: {', '.join(unknownResources)}"
170+
composite.conditions.Unknowns(False, 'NewUnknowns', message)
171+
composite.results.info(message, 'NewUnknowns')
126172

127173
if composite.autoReady:
128174
for name, resource in composite.resources:
@@ -133,6 +179,16 @@ async def RunFunction(
133179
logger.debug('Returning')
134180
return response
135181

182+
def _trimFullName(self, message, state, name):
183+
name = name.split('.')
184+
ix = 0
185+
for value in (message, state, 'resources', None, 'resource'):
186+
if value and ix < len(value) and name[ix] == value:
187+
del name[ix]
188+
else:
189+
ix += 1
190+
return '.'.join(name)
191+
136192

137193
class Module:
138194
pass

crossplane/pythonic/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def main():
6666

6767
logging.configure(logging.Level.DEBUG if args.debug else logging.Level.INFO)
6868
runtime.serve(
69-
function.FunctionRunner(),
69+
function.FunctionRunner(args.debug),
7070
args.address,
7171
creds=runtime.load_credentials(args.tls_certs_dir),
7272
insecure=args.insecure,

0 commit comments

Comments
 (0)