Skip to content

Commit b8a1b11

Browse files
author
Patrick J. McNerthney
committed
Allow for multiple function-pythonic steps in a single composition.
1 parent 4c459f8 commit b8a1b11

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+891
-324
lines changed

.devcontainer/devcontainer.json

Lines changed: 0 additions & 17 deletions
This file was deleted.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,4 @@ __marimo__/
214214
crossplane/pythonic/__version__.py
215215
pocs/
216216
pythonic-packages/
217+
tests/protobuf/pytest_pb2*

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"python.analysis.typeCheckingMode": "off"
3+
}

crossplane/pythonic/composite.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
import datetime
3+
from google.protobuf.duration_pb2 import Duration
34
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
45

56
from . import protobuf
@@ -9,8 +10,18 @@
910

1011

1112
class BaseComposite:
12-
def __init__(self, request, response, logger):
13+
def __init__(self, request, logger):
1314
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+
)
1425
self.response = protobuf.Message(None, 'response', response.DESCRIPTOR, response)
1526
self.logger = logger
1627
self.credentials = Credentials(self.request)
@@ -36,11 +47,23 @@ def __init__(self, request, response, logger):
3647

3748
@property
3849
def ttl(self):
50+
if self.response.meta.ttl.nanos:
51+
return float(self.response.meta.ttl.seconds) + (float(self.response.meta.ttl.nanos) / 1000000000.0)
3952
return self.response.meta.ttl.seconds
4053

4154
@ttl.setter
4255
def ttl(self, ttl):
43-
self.response.meta.ttl.seconds = ttl
56+
if isinstance(ttl, int):
57+
self.response.meta.ttl.seconds = ttl
58+
self.response.meta.ttl.nanos = 0
59+
elif isinstance(ttl, float):
60+
self.response.meta.ttl.seconds = int(ttl)
61+
if ttl.is_integer():
62+
self.response.meta.ttl.nanos = 0
63+
else:
64+
self.response.meta.ttl.nanos = int((ttl - self.response.meta.ttl.seconds) * 1000000000)
65+
else:
66+
raise ValueError('ttl must be an int or float')
4467

4568
@property
4669
def ready(self):
@@ -73,22 +96,46 @@ def __getattr__(self, key):
7396
return self[key]
7497

7598
def __getitem__(self, key):
76-
return self._request.credentials[key].credentials_data.data
99+
return Credential(self._request.credentials[key])
77100

78101
def __bool__(self):
79-
return bool(_request.credentials)
102+
return bool(self._request.credentials)
80103

81104
def __len__(self):
82105
return len(self._request.credentials)
83106

84107
def __contains__(self, key):
85-
return key in _request.credentials
108+
return key in self._request.credentials
86109

87110
def __iter__(self):
88111
for key, resource in self._request.credentials:
89112
yield key, self[key]
90113

91114

115+
class Credential:
116+
def __init__(self, credential):
117+
self.__dict__['_credential'] = credential
118+
119+
def __getattr__(self, key):
120+
return self[key]
121+
122+
def __getitem__(self, key):
123+
return self._credential.credential_data.data[key]
124+
125+
def __bool__(self):
126+
return bool(self._credential.credential_data.data)
127+
128+
def __len__(self):
129+
return len(self._credential.credential_data.data)
130+
131+
def __contains__(self, key):
132+
return key in self._credential.credential_data.data
133+
134+
def __iter__(self):
135+
for key, resource in self._credential.credential_data.data:
136+
yield key, self[key]
137+
138+
92139
class Resources:
93140
def __init__(self, composite):
94141
self.__dict__['_composite'] = composite
@@ -587,7 +634,7 @@ def __len__(self):
587634
def __getitem__(self, key):
588635
if key >= len(self._results):
589636
return Event()
590-
return Event(self._results[ix])
637+
return Event(self._results[key])
591638

592639
def __iter__(self):
593640
for ix in range(len(self._results)):

crossplane/pythonic/function.py

Lines changed: 67 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,16 @@
11
"""A Crossplane composition function."""
22

33
import asyncio
4-
import base64
5-
import builtins
64
import importlib
75
import inspect
86
import logging
97
import sys
108

119
import grpc
12-
import crossplane.function.response
1310
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
1411
from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1
1512
from .. import pythonic
1613

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-
2714
logger = logging.getLogger(__name__)
2815

2916

@@ -46,119 +33,99 @@ async def RunFunction(
4633
) -> fnv1.RunFunctionResponse:
4734
try:
4835
return await self.run_function(request)
49-
except:
50-
logger.exception('Exception thrown in run fuction')
51-
raise
36+
except Exception as e:
37+
return self.fatal(request, logger, 'RunFunction', e)
5238

5339
async def run_function(self, request):
5440
composite = request.observed.composite.resource
5541
name = list(reversed(composite['apiVersion'].split('/')[0].split('.')))
5642
name.append(composite['kind'])
5743
name.append(composite['metadata']['name'])
5844
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)
6645

6746
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
47+
if 'spec' not in composite or 'composite' not in composite['spec']:
48+
return self.fatal(request, logger, 'Missing spec "composite"')
7249
composite = composite['spec']['composite']
7350
else:
7451
if 'composite' not in request.input:
75-
logger.error('Missing input "composite"')
76-
crossplane.function.response.fatal(response, 'Missing input "composite"')
77-
return response
52+
return self.fatal(request, logger, 'Missing input "composite"')
7853
composite = request.input['composite']
7954

55+
# Ideally this is something the Function API provides
56+
if 'step' in request.input:
57+
step = request.input['step']
58+
else:
59+
step = str(hash(composite))
60+
8061
clazz = self.clazzes.get(composite)
8162
if not clazz:
8263
if '\n' in composite:
8364
module = Module()
8465
try:
8566
exec(composite, module.__dict__)
8667
except Exception as e:
87-
logger.exception('Exec exception')
88-
crossplane.function.response.fatal(response, f"Exec exception: {e}")
89-
return response
68+
return self.fatal(request, logger, 'Exec', e)
9069
for field in dir(module):
9170
value = getattr(module, field)
92-
if inspect.isclass(value) and issubclass(value, BaseComposite) and value != BaseComposite:
71+
if inspect.isclass(value) and issubclass(value, pythonic.BaseComposite) and value != pythonic.BaseComposite:
9372
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
73+
return self.fatal(request, logger, 'Composite script has multiple BaseComposite classes')
9774
clazz = value
9875
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
76+
return self.fatal(request, logger, 'Composite script does not have a BaseComposite class')
10277
else:
10378
composite = composite.rsplit('.', 1)
10479
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
80+
return self.fatal(request, logger, f"Composite class name does not include module: {composite[0]}")
10881
try:
10982
module = importlib.import_module(composite[0])
11083
except Exception as e:
111-
logger.error(str(e))
112-
crossplane.function.response.fatal(response, f"Import module exception: {e}")
113-
return response
84+
return self.fatal(request, logger, 'Import module', e)
11485
clazz = getattr(module, composite[1], None)
11586
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
87+
return self.fatal(request, logger, f"{composite[0]} does not define: {composite[1]}")
11988
composite = '.'.join(composite)
12089
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
90+
return self.fatal(request, logger, f"{composite} is not a class")
91+
if not issubclass(clazz, pythonic.BaseComposite):
92+
return self.fatal(request, logger, f"{composite} is not a subclass of BaseComposite")
12893
self.clazzes[composite] = clazz
12994

13095
try:
131-
composite = clazz(request, response, logger)
96+
composite = clazz(request, logger)
13297
except Exception as e:
133-
logger.exception('Instatiate exception')
134-
crossplane.function.response.fatal(response, f"Instatiate exception: {e}")
135-
return response
98+
return self.fatal(request, logger, 'Instantiate', e)
99+
100+
step = composite.context._pythonic[step]
101+
iteration = (step.iteration or 0) + 1
102+
step.iteration = iteration
103+
composite.context.iteration = iteration
104+
logger.debug(f"Starting compose, {ordinal(len(composite.context._pythonic))} step, {ordinal(iteration)} pass")
136105

137106
try:
138107
result = composite.compose()
139108
if asyncio.iscoroutine(result):
140109
await result
141110
except Exception as e:
142-
logger.exception('Compose exception')
143-
crossplane.function.response.fatal(response, f"Compose exception: {e}")
144-
return response
111+
return self.fatal(request, logger, 'Compose', e)
145112

146113
requested = []
147114
for name, required in composite.requireds:
148115
if required.apiVersion and required.kind:
149-
r = Map(apiVersion=required.apiVersion, kind=required.kind)
116+
r = pythonic.Map(apiVersion=required.apiVersion, kind=required.kind)
150117
if required.namespace:
151118
r.namespace = required.namespace
152119
if required.matchName:
153120
r.matchName = required.matchName
154121
for key, value in required.matchLabels:
155122
r.matchLabels[key] = value
156-
if r != composite.context._requireds[name]:
157-
composite.context._requireds[name] = r
123+
if r != step.requireds[name]:
124+
step.requireds[name] = r
158125
requested.append(name)
159126
if requested:
160127
logger.info(f"Requireds requested: {','.join(requested)}")
161-
return response
128+
return composite.response._message
162129

163130
unknownResources = []
164131
warningResources = []
@@ -227,7 +194,29 @@ async def run_function(self, request):
227194
resource.ready = True
228195

229196
logger.info('Completed compose')
230-
return response
197+
return composite.response._message
198+
199+
def fatal(self, request, logger, message, exception=None):
200+
if exception:
201+
message += ' exceptiion'
202+
logger.exception(message)
203+
m = str(exception)
204+
if not m:
205+
m = exception.__class__.__name__
206+
message += ': ' + m
207+
else:
208+
logger.error(message)
209+
return fnv1.RunFunctionResponse(
210+
meta=fnv1.ResponseMeta(
211+
tag=request.meta.tag,
212+
),
213+
results=[
214+
fnv1.Result(
215+
severity=fnv1.SEVERITY_FATAL,
216+
message=message,
217+
)
218+
]
219+
)
231220

232221
def trimFullName(self, name):
233222
name = name.split('.')
@@ -272,4 +261,13 @@ def ordinal(ix):
272261

273262

274263
class Module:
275-
pass
264+
def __init__(self):
265+
self.BaseComposite = pythonic.BaseComposite
266+
self.append = pythonic.append
267+
self.Map = pythonic.Map
268+
self.List = pythonic.List
269+
self.Unknown = pythonic.Unknown
270+
self.Yaml = pythonic.Yaml
271+
self.Json = pythonic.Json
272+
self.B64Encode = pythonic.B64Encode
273+
self.B64Decode = pythonic.B64Decode

0 commit comments

Comments
 (0)