function.runtime
Utilities to create a composition function runtime.
1# Copyright 2023 The Crossplane Authors. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Utilities to create a composition function runtime.""" 16 17import asyncio 18import os 19import signal 20from collections.abc import Sequence 21from typing import Any 22 23import grpc 24from grpc_reflection.v1alpha import reflection 25 26import crossplane.function.proto.v1.run_function_pb2 as fnv1 27import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1 28import crossplane.function.proto.v1beta1.run_function_pb2 as fnv1beta1 29import crossplane.function.proto.v1beta1.run_function_pb2_grpc as grpcv1beta1 30 31SERVICE_NAMES = ( 32 reflection.SERVICE_NAME, 33 fnv1.DESCRIPTOR.services_by_name["FunctionRunnerService"].full_name, 34 fnv1beta1.DESCRIPTOR.services_by_name["FunctionRunnerService"].full_name, 35) 36 37SHUTDOWN_GRACE_PERIOD_SECONDS = 5 38 39 40def load_credentials(tls_certs_dir: str) -> grpc.ServerCredentials: 41 """Load TLS credentials for a composition function gRPC server. 42 43 Args: 44 tls_certs_dir: A directory containing tls.crt, tls.key, and ca.crt. 45 46 Returns: 47 gRPC mTLS server credentials. 48 49 tls.crt and tls.key must be the function's PEM-encoded certificate and 50 private key. ca.cert must be a PEM-encoded CA certificate used to 51 authenticate callers (i.e. Crossplane). 52 """ 53 if tls_certs_dir is None: 54 return None 55 56 with open(os.path.join(tls_certs_dir, "tls.crt"), "rb") as f: 57 crt = f.read() 58 59 with open(os.path.join(tls_certs_dir, "tls.key"), "rb") as f: 60 key = f.read() 61 62 with open(os.path.join(tls_certs_dir, "ca.crt"), "rb") as f: 63 ca = f.read() 64 65 return grpc.ssl_server_credentials( 66 private_key_certificate_chain_pairs=[(key, crt)], 67 root_certificates=ca, 68 require_client_auth=True, 69 ) 70 71 72def serve( 73 function: grpcv1.FunctionRunnerServiceServicer, 74 address: str, 75 *, 76 creds: grpc.ServerCredentials, 77 insecure: bool, 78 options: Sequence[tuple[str, Any]] | None = None, 79) -> None: 80 """Start a gRPC server and serve requests asychronously. 81 82 Args: 83 function: The function (class) to use to serve requests. 84 address: The address at which to listen for requests. 85 creds: The credentials used to authenticate requests. 86 insecure: Serve insecurely, without credentials or encryption. 87 options: Additional gRPC server options. Eg. set max receive message 88 size to 5Mb (default is 4Mb): 89 [("grpc.max_receive_message_length", 1024 * 1024 * 5)] 90 91 Raises: 92 ValueError if creds is None and insecure is False. 93 94 If insecure is true requests will be served insecurely, even if credentials 95 are supplied. 96 """ 97 # Define the loop before the server so everything uses the same loop. 98 try: 99 loop = asyncio.get_event_loop() 100 except RuntimeError: 101 loop = asyncio.new_event_loop() 102 asyncio.set_event_loop(loop) 103 104 server = grpc.aio.server(options=options) 105 106 loop.add_signal_handler( 107 signal.SIGTERM, 108 lambda: asyncio.ensure_future(server.stop(grace=SHUTDOWN_GRACE_PERIOD_SECONDS)), 109 ) 110 111 grpcv1.add_FunctionRunnerServiceServicer_to_server(function, server) 112 grpcv1beta1.add_FunctionRunnerServiceServicer_to_server( 113 BetaFunctionRunner(wrapped=function), server 114 ) 115 reflection.enable_server_reflection(SERVICE_NAMES, server) 116 117 if creds is None and insecure is False: 118 msg = ( 119 "no credentials were provided - did you provide credentials or use " 120 "the insecure flag?" 121 ) 122 raise ValueError(msg) 123 124 if creds is not None: 125 server.add_secure_port(address, creds) 126 127 if insecure: 128 server.add_insecure_port(address) 129 130 async def start(): 131 await server.start() 132 await server.wait_for_termination() 133 134 try: 135 loop.run_until_complete(start()) 136 finally: 137 loop.run_until_complete(server.stop(grace=SHUTDOWN_GRACE_PERIOD_SECONDS)) 138 loop.close() 139 140 141class BetaFunctionRunner(grpcv1beta1.FunctionRunnerServiceServicer): 142 """A BetaFunctionRunner handles beta gRPC RunFunctionRequests. 143 144 It handles requests by passing them to a wrapped v1.FunctionRunnerServiceServicer. 145 Incoming v1beta1 requests are converted to v1 by round-tripping them through 146 serialization. Outgoing requests are converted from v1 to v1beta1 the same 147 way. 148 """ 149 150 def __init__(self, wrapped: grpcv1.FunctionRunnerServiceServicer): 151 """Create a new BetaFunctionRunner.""" 152 self.wrapped = wrapped 153 154 async def RunFunction( # noqa: N802 # gRPC requires this name. # pyright: ignore[reportIncompatibleMethodOverride] 155 self, req: fnv1beta1.RunFunctionRequest, context: grpc.aio.ServicerContext 156 ) -> fnv1beta1.RunFunctionResponse: 157 """Run the underlying function.""" 158 gareq = fnv1.RunFunctionRequest() 159 gareq.ParseFromString(req.SerializeToString()) 160 161 garsp = await self.wrapped.RunFunction(gareq, context) 162 rsp = fnv1beta1.RunFunctionResponse() 163 rsp.ParseFromString(garsp.SerializeToString()) 164 165 return rsp
41def load_credentials(tls_certs_dir: str) -> grpc.ServerCredentials: 42 """Load TLS credentials for a composition function gRPC server. 43 44 Args: 45 tls_certs_dir: A directory containing tls.crt, tls.key, and ca.crt. 46 47 Returns: 48 gRPC mTLS server credentials. 49 50 tls.crt and tls.key must be the function's PEM-encoded certificate and 51 private key. ca.cert must be a PEM-encoded CA certificate used to 52 authenticate callers (i.e. Crossplane). 53 """ 54 if tls_certs_dir is None: 55 return None 56 57 with open(os.path.join(tls_certs_dir, "tls.crt"), "rb") as f: 58 crt = f.read() 59 60 with open(os.path.join(tls_certs_dir, "tls.key"), "rb") as f: 61 key = f.read() 62 63 with open(os.path.join(tls_certs_dir, "ca.crt"), "rb") as f: 64 ca = f.read() 65 66 return grpc.ssl_server_credentials( 67 private_key_certificate_chain_pairs=[(key, crt)], 68 root_certificates=ca, 69 require_client_auth=True, 70 )
Load TLS credentials for a composition function gRPC server.
Arguments:
- tls_certs_dir: A directory containing tls.crt, tls.key, and ca.crt.
Returns:
gRPC mTLS server credentials.
tls.crt and tls.key must be the function's PEM-encoded certificate and private key. ca.cert must be a PEM-encoded CA certificate used to authenticate callers (i.e. Crossplane).
73def serve( 74 function: grpcv1.FunctionRunnerServiceServicer, 75 address: str, 76 *, 77 creds: grpc.ServerCredentials, 78 insecure: bool, 79 options: Sequence[tuple[str, Any]] | None = None, 80) -> None: 81 """Start a gRPC server and serve requests asychronously. 82 83 Args: 84 function: The function (class) to use to serve requests. 85 address: The address at which to listen for requests. 86 creds: The credentials used to authenticate requests. 87 insecure: Serve insecurely, without credentials or encryption. 88 options: Additional gRPC server options. Eg. set max receive message 89 size to 5Mb (default is 4Mb): 90 [("grpc.max_receive_message_length", 1024 * 1024 * 5)] 91 92 Raises: 93 ValueError if creds is None and insecure is False. 94 95 If insecure is true requests will be served insecurely, even if credentials 96 are supplied. 97 """ 98 # Define the loop before the server so everything uses the same loop. 99 try: 100 loop = asyncio.get_event_loop() 101 except RuntimeError: 102 loop = asyncio.new_event_loop() 103 asyncio.set_event_loop(loop) 104 105 server = grpc.aio.server(options=options) 106 107 loop.add_signal_handler( 108 signal.SIGTERM, 109 lambda: asyncio.ensure_future(server.stop(grace=SHUTDOWN_GRACE_PERIOD_SECONDS)), 110 ) 111 112 grpcv1.add_FunctionRunnerServiceServicer_to_server(function, server) 113 grpcv1beta1.add_FunctionRunnerServiceServicer_to_server( 114 BetaFunctionRunner(wrapped=function), server 115 ) 116 reflection.enable_server_reflection(SERVICE_NAMES, server) 117 118 if creds is None and insecure is False: 119 msg = ( 120 "no credentials were provided - did you provide credentials or use " 121 "the insecure flag?" 122 ) 123 raise ValueError(msg) 124 125 if creds is not None: 126 server.add_secure_port(address, creds) 127 128 if insecure: 129 server.add_insecure_port(address) 130 131 async def start(): 132 await server.start() 133 await server.wait_for_termination() 134 135 try: 136 loop.run_until_complete(start()) 137 finally: 138 loop.run_until_complete(server.stop(grace=SHUTDOWN_GRACE_PERIOD_SECONDS)) 139 loop.close()
Start a gRPC server and serve requests asychronously.
Arguments:
- function: The function (class) to use to serve requests.
- address: The address at which to listen for requests.
- creds: The credentials used to authenticate requests.
- insecure: Serve insecurely, without credentials or encryption.
- options: Additional gRPC server options. Eg. set max receive message size to 5Mb (default is 4Mb): [("grpc.max_receive_message_length", 1024 * 1024 * 5)]
Raises:
- ValueError if creds is None and insecure is False.
If insecure is true requests will be served insecurely, even if credentials are supplied.
142class BetaFunctionRunner(grpcv1beta1.FunctionRunnerServiceServicer): 143 """A BetaFunctionRunner handles beta gRPC RunFunctionRequests. 144 145 It handles requests by passing them to a wrapped v1.FunctionRunnerServiceServicer. 146 Incoming v1beta1 requests are converted to v1 by round-tripping them through 147 serialization. Outgoing requests are converted from v1 to v1beta1 the same 148 way. 149 """ 150 151 def __init__(self, wrapped: grpcv1.FunctionRunnerServiceServicer): 152 """Create a new BetaFunctionRunner.""" 153 self.wrapped = wrapped 154 155 async def RunFunction( # noqa: N802 # gRPC requires this name. # pyright: ignore[reportIncompatibleMethodOverride] 156 self, req: fnv1beta1.RunFunctionRequest, context: grpc.aio.ServicerContext 157 ) -> fnv1beta1.RunFunctionResponse: 158 """Run the underlying function.""" 159 gareq = fnv1.RunFunctionRequest() 160 gareq.ParseFromString(req.SerializeToString()) 161 162 garsp = await self.wrapped.RunFunction(gareq, context) 163 rsp = fnv1beta1.RunFunctionResponse() 164 rsp.ParseFromString(garsp.SerializeToString()) 165 166 return rsp
A BetaFunctionRunner handles beta gRPC RunFunctionRequests.
It handles requests by passing them to a wrapped v1.FunctionRunnerServiceServicer. Incoming v1beta1 requests are converted to v1 by round-tripping them through serialization. Outgoing requests are converted from v1 to v1beta1 the same way.
151 def __init__(self, wrapped: grpcv1.FunctionRunnerServiceServicer): 152 """Create a new BetaFunctionRunner.""" 153 self.wrapped = wrapped
Create a new BetaFunctionRunner.
155 async def RunFunction( # noqa: N802 # gRPC requires this name. # pyright: ignore[reportIncompatibleMethodOverride] 156 self, req: fnv1beta1.RunFunctionRequest, context: grpc.aio.ServicerContext 157 ) -> fnv1beta1.RunFunctionResponse: 158 """Run the underlying function.""" 159 gareq = fnv1.RunFunctionRequest() 160 gareq.ParseFromString(req.SerializeToString()) 161 162 garsp = await self.wrapped.RunFunction(gareq, context) 163 rsp = fnv1beta1.RunFunctionResponse() 164 rsp.ParseFromString(garsp.SerializeToString()) 165 166 return rsp
Run the underlying function.