11"""A Crossplane composition function."""
22
33import asyncio
4- import base64
5- import builtins
64import importlib
75import inspect
86import logging
97import sys
108
119import grpc
12- import crossplane .function .response
1310from crossplane .function .proto .v1 import run_function_pb2 as fnv1
1411from crossplane .function .proto .v1 import run_function_pb2_grpc as grpcv1
1512from .. 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-
2714logger = logging .getLogger (__name__ )
2815
2916
3017class FunctionRunner (grpcv1 .FunctionRunnerService ):
3118 """A FunctionRunner handles gRPC RunFunctionRequests."""
3219
33- def __init__ (self , debug = False ):
20+ def __init__ (self , debug = False , renderUnknowns = False ):
3421 """Create a new FunctionRunner."""
3522 self .debug = debug
23+ self .renderUnknowns = renderUnknowns
3624 self .clazzes = {}
3725
3826 def invalidate_module (self , module ):
@@ -46,119 +34,99 @@ async def RunFunction(
4634 ) -> fnv1 .RunFunctionResponse :
4735 try :
4836 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 )
5239
5340 async def run_function (self , request ):
5441 composite = request .observed .composite .resource
5542 name = list (reversed (composite ['apiVersion' ].split ('/' )[0 ].split ('.' )))
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' :
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"' )
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 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 ]} does 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 , 'Instantiate' , 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 = []
@@ -187,6 +155,8 @@ async def run_function(self, request):
187155 logger .debug (f'Desired unknown: { destination } = { source } ' )
188156 if resource .observed :
189157 resource .desired ._patchUnknowns (resource .observed )
158+ elif self .renderUnknowns :
159+ resource .desired ._renderUnknowns (self .trimFullName )
190160 else :
191161 del composite .resources [name ]
192162
@@ -227,7 +197,29 @@ async def run_function(self, request):
227197 resource .ready = True
228198
229199 logger .info ('Completed compose' )
230- return response
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+ )
231223
232224 def trimFullName (self , name ):
233225 name = name .split ('.' )
@@ -272,4 +264,13 @@ def ordinal(ix):
272264
273265
274266class Module :
275- pass
267+ def __init__ (self ):
268+ self .BaseComposite = pythonic .BaseComposite
269+ self .append = pythonic .append
270+ self .Map = pythonic .Map
271+ self .List = pythonic .List
272+ self .Unknown = pythonic .Unknown
273+ self .Yaml = pythonic .Yaml
274+ self .Json = pythonic .Json
275+ self .B64Encode = pythonic .B64Encode
276+ self .B64Decode = pythonic .B64Decode
0 commit comments