Skip to content

Commit 216022e

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

26 files changed

Lines changed: 173 additions & 170 deletions

.devcontainer/devcontainer.json

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

crossplane/pythonic/composite.py

Lines changed: 15 additions & 4 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)
@@ -76,13 +87,13 @@ def __getitem__(self, key):
7687
return self._request.credentials[key].credentials_data.data
7788

7889
def __bool__(self):
79-
return bool(_request.credentials)
90+
return bool(self._request.credentials)
8091

8192
def __len__(self):
8293
return len(self._request.credentials)
8394

8495
def __contains__(self, key):
85-
return key in _request.credentials
96+
return key in self._request.credentials
8697

8798
def __iter__(self):
8899
for key, resource in self._request.credentials:
@@ -587,7 +598,7 @@ def __len__(self):
587598
def __getitem__(self, key):
588599
if key >= len(self._results):
589600
return Event()
590-
return Event(self._results[ix])
601+
return Event(self._results[key])
591602

592603
def __iter__(self):
593604
for ix in range(len(self._results)):

crossplane/pythonic/function.py

Lines changed: 61 additions & 65 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

@@ -56,109 +43,90 @@ async def run_function(self, request):
5643
name.append(composite['kind'])
5744
name.append(composite['metadata']['name'])
5845
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)
6646

6747
if composite['apiVersion'] == 'pythonic.fortra.com/v1alpha1' and composite['kind'] == 'Composite':
6848
if 'composite' not in composite['spec']:
69-
logger.error('Missing spec "composite"')
70-
crossplane.function.response.fatal(response, 'Missing spec "composite"')
71-
return response
49+
return self.fatal(request, logger, 'Missing spec "composite"')
7250
composite = composite['spec']['composite']
7351
else:
7452
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"')
7854
composite = request.input['composite']
7955

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+
8062
clazz = self.clazzes.get(composite)
8163
if not clazz:
8264
if '\n' in composite:
8365
module = Module()
8466
try:
8567
exec(composite, module.__dict__)
8668
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)
9070
for field in dir(module):
9171
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:
9373
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')
9775
clazz = value
9876
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 have a BaseComposite class')
10278
else:
10379
composite = composite.rsplit('.', 1)
10480
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]}")
10882
try:
10983
module = importlib.import_module(composite[0])
11084
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)
11486
clazz = getattr(module, composite[1], None)
11587
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]} did not define: {composite[1]}")
11989
composite = '.'.join(composite)
12090
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")
12894
self.clazzes[composite] = clazz
12995

13096
try:
131-
composite = clazz(request, response, logger)
97+
composite = clazz(request, logger)
13298
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, 'Instatiate', e)
100+
101+
step = composite.context._pythonic[step]
102+
iteration = (step.iteration or 0) + 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")
136106

137107
try:
138108
result = composite.compose()
139109
if asyncio.iscoroutine(result):
140110
await result
141111
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)
145113

146114
requested = []
147115
for name, required in composite.requireds:
148116
if required.apiVersion and required.kind:
149-
r = Map(apiVersion=required.apiVersion, kind=required.kind)
117+
r = pythonic.Map(apiVersion=required.apiVersion, kind=required.kind)
150118
if required.namespace:
151119
r.namespace = required.namespace
152120
if required.matchName:
153121
r.matchName = required.matchName
154122
for key, value in required.matchLabels:
155123
r.matchLabels[key] = value
156-
if r != composite.context._requireds[name]:
157-
composite.context._requireds[name] = r
124+
if r != step.requireds[name]:
125+
step.requireds[name] = r
158126
requested.append(name)
159127
if requested:
160128
logger.info(f"Requireds requested: {','.join(requested)}")
161-
return response
129+
return composite.response._message
162130

163131
unknownResources = []
164132
warningResources = []
@@ -227,7 +195,26 @@ async def run_function(self, request):
227195
resource.ready = True
228196

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

232219
def trimFullName(self, name):
233220
name = name.split('.')
@@ -272,4 +259,13 @@ def ordinal(ix):
272259

273260

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

0 commit comments

Comments
 (0)