|
1 | 1 | """A Crossplane composition function.""" |
2 | 2 |
|
3 | 3 | import asyncio |
4 | | -import base64 |
5 | | -import builtins |
6 | 4 | import importlib |
7 | 5 | import inspect |
8 | 6 | import logging |
9 | 7 | import sys |
10 | 8 |
|
11 | 9 | import grpc |
12 | | -import crossplane.function.response |
13 | 10 | from crossplane.function.proto.v1 import run_function_pb2 as fnv1 |
14 | 11 | from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1 |
15 | 12 | from .. import pythonic |
16 | 13 |
|
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 | | - |
27 | 14 | logger = logging.getLogger(__name__) |
28 | 15 |
|
29 | 16 |
|
@@ -56,109 +43,90 @@ async def run_function(self, request): |
56 | 43 | name.append(composite['kind']) |
57 | 44 | name.append(composite['metadata']['name']) |
58 | 45 | 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) |
66 | 46 |
|
67 | 47 | if composite['apiVersion'] == 'pythonic.fortra.com/v1alpha1' and composite['kind'] == 'Composite': |
68 | 48 | 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"') |
72 | 50 | composite = composite['spec']['composite'] |
73 | 51 | else: |
74 | 52 | 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"') |
78 | 54 | composite = request.input['composite'] |
79 | 55 |
|
| 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 | + |
80 | 62 | clazz = self.clazzes.get(composite) |
81 | 63 | if not clazz: |
82 | 64 | if '\n' in composite: |
83 | 65 | module = Module() |
84 | 66 | try: |
85 | 67 | exec(composite, module.__dict__) |
86 | 68 | 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) |
90 | 70 | for field in dir(module): |
91 | 71 | 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: |
93 | 73 | 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') |
97 | 75 | clazz = value |
98 | 76 | 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') |
102 | 78 | else: |
103 | 79 | composite = composite.rsplit('.', 1) |
104 | 80 | 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]}") |
108 | 82 | try: |
109 | 83 | module = importlib.import_module(composite[0]) |
110 | 84 | 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) |
114 | 86 | clazz = getattr(module, composite[1], None) |
115 | 87 | 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]}") |
119 | 89 | composite = '.'.join(composite) |
120 | 90 | 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") |
128 | 94 | self.clazzes[composite] = clazz |
129 | 95 |
|
130 | 96 | try: |
131 | | - composite = clazz(request, response, logger) |
| 97 | + composite = clazz(request, logger) |
132 | 98 | 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") |
136 | 106 |
|
137 | 107 | try: |
138 | 108 | result = composite.compose() |
139 | 109 | if asyncio.iscoroutine(result): |
140 | 110 | await result |
141 | 111 | 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) |
145 | 113 |
|
146 | 114 | requested = [] |
147 | 115 | for name, required in composite.requireds: |
148 | 116 | 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) |
150 | 118 | if required.namespace: |
151 | 119 | r.namespace = required.namespace |
152 | 120 | if required.matchName: |
153 | 121 | r.matchName = required.matchName |
154 | 122 | for key, value in required.matchLabels: |
155 | 123 | 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 |
158 | 126 | requested.append(name) |
159 | 127 | if requested: |
160 | 128 | logger.info(f"Requireds requested: {','.join(requested)}") |
161 | | - return response |
| 129 | + return composite.response._message |
162 | 130 |
|
163 | 131 | unknownResources = [] |
164 | 132 | warningResources = [] |
@@ -227,7 +195,26 @@ async def run_function(self, request): |
227 | 195 | resource.ready = True |
228 | 196 |
|
229 | 197 | 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 | + ) |
231 | 218 |
|
232 | 219 | def trimFullName(self, name): |
233 | 220 | name = name.split('.') |
@@ -272,4 +259,13 @@ def ordinal(ix): |
272 | 259 |
|
273 | 260 |
|
274 | 261 | 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