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
@@ -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
274263class 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