Skip to content

Commit 92f0f00

Browse files
authored
Merge pull request #53 from bobh66/improve_exceptions
Add exception handling
2 parents 6b7adb5 + 28fe6d3 commit 92f0f00

2 files changed

Lines changed: 170 additions & 13 deletions

File tree

‎function/fn.py‎

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

33
import importlib.util
44
import inspect
5+
import traceback
56
import types
67

78
import grpc
@@ -24,12 +25,12 @@ async def RunFunction(
2425
) -> fnv1.RunFunctionResponse:
2526
"""Run the function."""
2627
log = self.log.bind(tag=req.meta.tag)
27-
log.info("Running function")
28+
log.debug("Running function")
2829

2930
rsp = response.to(req)
3031

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

3536
log.debug("Running script", script=req.input["script"])
@@ -44,22 +45,40 @@ async def RunFunction(
4445
log.debug(msg)
4546
response.fatal(rsp, msg)
4647
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)
48+
try:
49+
log.debug("running composition function")
50+
if inspect.iscoroutinefunction(script.compose):
51+
await script.compose(req, rsp)
52+
else:
53+
script.compose(req, rsp)
54+
except Exception as e:
55+
msg = (
56+
f"Exception: {type(e)}, "
57+
f"traceback: {traceback.format_tb(e.__traceback__.tb_next)}"
58+
)
59+
log.debug(msg)
60+
response.fatal(rsp, msg)
61+
5262
case (False, True):
5363
log.debug("running operation function")
54-
if inspect.iscoroutinefunction(script.operate):
55-
await script.operate(req, rsp)
56-
else:
57-
script.operate(req, rsp)
64+
try:
65+
if inspect.iscoroutinefunction(script.operate):
66+
await script.operate(req, rsp)
67+
else:
68+
script.operate(req, rsp)
69+
except Exception as e:
70+
msg = (
71+
f"Exception: {e}, "
72+
f"traceback: {traceback.format_tb(e.__traceback__.tb_next)}"
73+
)
74+
log.debug(msg)
75+
response.fatal(rsp, msg)
76+
5877
case (False, False):
5978
msg = "script must define a compose or operate function"
6079
log.debug(msg)
6180
response.fatal(rsp, msg)
62-
81+
log.debug(f"Response: {rsp}")
6382
return rsp
6483

6584

‎tests/test_fn.py‎

Lines changed: 138 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,112 @@ 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"
211+
" an exception.",
212+
req=fnv1.RunFunctionRequest(
213+
input=resource.dict_to_struct(
214+
{"script": composition_script_with_exception}
215+
),
216+
),
217+
want=fnv1.RunFunctionResponse(
218+
meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)),
219+
results=[
220+
{
221+
"message": "Exception: <class 'AttributeError'>, traceback:"
222+
" [' File \"<string>\", line 14, in compose\\n'"
223+
"]",
224+
"severity": "SEVERITY_FATAL",
225+
}
226+
],
227+
desired=fnv1.State(
228+
resources={
229+
"bucket": fnv1.Resource(
230+
resource=resource.dict_to_struct(
231+
{
232+
"apiVersion": "s3.aws.upbound.io/v1beta2",
233+
"kind": "Bucket",
234+
"spec": {
235+
"forProvider": {"region": "us-east-1"}
236+
},
237+
}
238+
)
239+
)
240+
}
241+
),
242+
context=structpb.Struct(),
243+
),
244+
),
245+
TestCase(
246+
reason="Function should fail gracefully when async compose script"
247+
" raises an exception.",
248+
req=fnv1.RunFunctionRequest(
249+
input=resource.dict_to_struct(
250+
{"script": async_composition_script_with_exception}
251+
),
252+
),
253+
want=fnv1.RunFunctionResponse(
254+
meta=fnv1.ResponseMeta(ttl=durationpb.Duration(seconds=60)),
255+
results=[
256+
{
257+
"message": "Exception: <class 'AttributeError'>, traceback:"
258+
" [' File \"<string>\", line 14, in compose\\n'"
259+
"]",
260+
"severity": "SEVERITY_FATAL",
261+
}
262+
],
263+
desired=fnv1.State(
264+
resources={
265+
"bucket": fnv1.Resource(
266+
resource=resource.dict_to_struct(
267+
{
268+
"apiVersion": "s3.aws.upbound.io/v1beta2",
269+
"kind": "Bucket",
270+
"spec": {
271+
"forProvider": {"region": "us-east-1"}
272+
},
273+
}
274+
)
275+
)
276+
}
277+
),
278+
context=structpb.Struct(),
279+
),
280+
),
143281
]
144282

145283
runner = fn.FunctionRunner()

0 commit comments

Comments
 (0)