Skip to content

Commit 487c5a2

Browse files
committed
Add exception handling
Signed-off-by: Bob Haddleton <bob.haddleton@nokia.com>
1 parent d987e12 commit 487c5a2

File tree

2 files changed

+159
-13
lines changed

2 files changed

+159
-13
lines changed

function/fn.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import importlib.util
44
import inspect
5+
import sys
6+
import traceback
57
import types
68

79
import grpc
@@ -24,12 +26,12 @@ async def RunFunction(
2426
) -> fnv1.RunFunctionResponse:
2527
"""Run the function."""
2628
log = self.log.bind(tag=req.meta.tag)
27-
log.info("Running function")
29+
log.debug("Running function")
2830

2931
rsp = response.to(req)
3032

31-
if req.input["script"] is None:
32-
response.fatal(rsp, "missing script")
33+
if "script" not in req.input or not req.input["script"]:
34+
response.fatal(rsp, "missing script in function input")
3335
return rsp
3436

3537
log.debug("Running script", script=req.input["script"])
@@ -44,22 +46,34 @@ async def RunFunction(
4446
log.debug(msg)
4547
response.fatal(rsp, msg)
4648
case (True, False):
47-
log.debug("running composition function")
48-
if inspect.iscoroutinefunction(script.compose):
49-
await script.compose(req, rsp)
50-
else:
51-
script.compose(req, rsp)
49+
try:
50+
log.debug("running composition function")
51+
if inspect.iscoroutinefunction(script.compose):
52+
await script.compose(req, rsp)
53+
else:
54+
script.compose(req, rsp)
55+
except Exception as e:
56+
msg = f"Exception: {type(e)}, traceback: {traceback.format_tb(e.__traceback__.tb_next)}"
57+
log.debug(msg)
58+
response.fatal(rsp, msg)
59+
5260
case (False, True):
5361
log.debug("running operation function")
54-
if inspect.iscoroutinefunction(script.operate):
55-
await script.operate(req, rsp)
56-
else:
57-
script.operate(req, rsp)
62+
try:
63+
if inspect.iscoroutinefunction(script.operate):
64+
await script.operate(req, rsp)
65+
else:
66+
script.operate(req, rsp)
67+
except Exception as e:
68+
msg = f"Exception: {e}, traceback: {traceback.format_tb(e.__traceback__.tb_next)}"
69+
log.debug(msg)
70+
response.fatal(rsp, msg)
71+
5872
case (False, False):
5973
msg = "script must define a compose or operate function"
6074
log.debug(msg)
6175
response.fatal(rsp, msg)
62-
76+
log.debug(f"Response: {rsp}")
6377
return rsp
6478

6579

tests/test_fn.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
2424
})
2525
"""
2626

27+
composition_script_with_exception = """
28+
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
29+
30+
def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
31+
rsp.desired.resources["bucket"].resource.update({
32+
"apiVersion": "s3.aws.upbound.io/v1beta2",
33+
"kind": "Bucket",
34+
"spec": {
35+
"forProvider": {
36+
"region": "us-east-1"
37+
}
38+
},
39+
})
40+
raise AttributeError
41+
"""
42+
2743
async_composition_script = """
2844
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
2945
@@ -39,6 +55,22 @@ async def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
3955
})
4056
"""
4157

58+
async_composition_script_with_exception = """
59+
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
60+
61+
async def compose(req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse):
62+
rsp.desired.resources["bucket"].resource.update({
63+
"apiVersion": "s3.aws.upbound.io/v1beta2",
64+
"kind": "Bucket",
65+
"spec": {
66+
"forProvider": {
67+
"region": "us-east-1"
68+
}
69+
},
70+
})
71+
raise AttributeError
72+
"""
73+
4274
operation_script = """
4375
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
4476
@@ -140,6 +172,106 @@ class TestCase:
140172
context=structpb.Struct(),
141173
),
142174
),
175+
TestCase(
176+
reason="Function should fail gracefully when script is missing.",
177+
req=fnv1.RunFunctionRequest(
178+
input=resource.dict_to_struct({}),
179+
),
180+
want=fnv1.RunFunctionResponse(
181+
meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)),
182+
results=[
183+
{
184+
"message": "missing script in function input",
185+
"severity": "SEVERITY_FATAL",
186+
}
187+
],
188+
desired=fnv1.State(),
189+
context=structpb.Struct(),
190+
),
191+
),
192+
TestCase(
193+
reason="Function should fail gracefully when script is empty.",
194+
req=fnv1.RunFunctionRequest(
195+
input=resource.dict_to_struct({"script": ""}),
196+
),
197+
want=fnv1.RunFunctionResponse(
198+
meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)),
199+
results=[
200+
{
201+
"message": "missing script in function input",
202+
"severity": "SEVERITY_FATAL",
203+
}
204+
],
205+
desired=fnv1.State(),
206+
context=structpb.Struct(),
207+
),
208+
),
209+
TestCase(
210+
reason="Function should fail gracefully when compose script raises an exception.",
211+
req=fnv1.RunFunctionRequest(
212+
input=resource.dict_to_struct(
213+
{"script": composition_script_with_exception}
214+
),
215+
),
216+
want=fnv1.RunFunctionResponse(
217+
meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)),
218+
results=[
219+
{
220+
"message": "Exception: <class 'AttributeError'>, traceback: [' File \"<string>\", line 14, in compose\\n']",
221+
"severity": "SEVERITY_FATAL",
222+
}
223+
],
224+
desired=fnv1.State(
225+
resources={
226+
"bucket": fnv1.Resource(
227+
resource=resource.dict_to_struct(
228+
{
229+
"apiVersion": "s3.aws.upbound.io/v1beta2",
230+
"kind": "Bucket",
231+
"spec": {
232+
"forProvider": {"region": "us-east-1"}
233+
},
234+
}
235+
)
236+
)
237+
}
238+
),
239+
context=structpb.Struct(),
240+
),
241+
),
242+
TestCase(
243+
reason="Function should fail gracefully when async compose script raises an exception.",
244+
req=fnv1.RunFunctionRequest(
245+
input=resource.dict_to_struct(
246+
{"script": async_composition_script_with_exception}
247+
),
248+
),
249+
want=fnv1.RunFunctionResponse(
250+
meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)),
251+
results=[
252+
{
253+
"message": "Exception: <class 'AttributeError'>, traceback: [' File \"<string>\", line 14, in compose\\n']",
254+
"severity": "SEVERITY_FATAL",
255+
}
256+
],
257+
desired=fnv1.State(
258+
resources={
259+
"bucket": fnv1.Resource(
260+
resource=resource.dict_to_struct(
261+
{
262+
"apiVersion": "s3.aws.upbound.io/v1beta2",
263+
"kind": "Bucket",
264+
"spec": {
265+
"forProvider": {"region": "us-east-1"}
266+
},
267+
}
268+
)
269+
)
270+
}
271+
),
272+
context=structpb.Struct(),
273+
),
274+
),
143275
]
144276

145277
runner = fn.FunctionRunner()

0 commit comments

Comments
 (0)