|
1 | | -"""The function-pythonic's main CLI.""" |
| 1 | +"""The composition function's main CLI.""" |
2 | 2 |
|
3 | 3 | import argparse |
4 | 4 | import asyncio |
| 5 | +import logging |
| 6 | +import os |
| 7 | +import pathlib |
| 8 | +import shlex |
| 9 | +import signal |
5 | 10 | import sys |
| 11 | +import traceback |
6 | 12 |
|
7 | | -from . import ( |
8 | | - grpc, |
9 | | - render, |
10 | | - version, |
11 | | -) |
| 13 | +import crossplane.function.logging |
| 14 | +import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1 |
| 15 | +import grpc |
| 16 | + |
| 17 | +from . import function |
12 | 18 |
|
13 | 19 |
|
14 | 20 | def main(): |
15 | | - parser = argparse.ArgumentParser('Crossplane Function Pythonic') |
16 | | - subparsers = parser.add_subparsers(title='Command', metavar='') |
17 | | - grpc.Command.create(subparsers) |
18 | | - render.Command.create(subparsers) |
19 | | - version.Command.create(subparsers) |
20 | | - args = parser.parse_args() |
21 | | - if not hasattr(args, 'command'): |
22 | | - parser.print_help() |
23 | | - sys.exit(1) |
24 | | - asyncio.run(args.command(args).run()) |
| 21 | + asyncio.run(Main().main()) |
| 22 | + |
| 23 | + |
| 24 | +class Main: |
| 25 | + async def main(self): |
| 26 | + parser = argparse.ArgumentParser('Crossplane Function Pythonic') |
| 27 | + parser.add_argument( |
| 28 | + '--debug', '-d', |
| 29 | + action='store_true', |
| 30 | + help='Emit debug logs.', |
| 31 | + ) |
| 32 | + parser.add_argument( |
| 33 | + '--log-name-width', |
| 34 | + type=int, |
| 35 | + default=40, |
| 36 | + metavar='WIDTH', |
| 37 | + help='Width of the logger name in the log output, default 40', |
| 38 | + ) |
| 39 | + parser.add_argument( |
| 40 | + '--address', |
| 41 | + default='0.0.0.0:9443', |
| 42 | + help='Address to listen on for gRPC connections, default: 0.0.0.0:9443', |
| 43 | + ) |
| 44 | + parser.add_argument( |
| 45 | + '--tls-certs-dir', |
| 46 | + default=os.getenv('TLS_SERVER_CERTS_DIR'), |
| 47 | + metavar='DIRECTORY', |
| 48 | + help='Serve using TLS certificates.', |
| 49 | + ) |
| 50 | + parser.add_argument( |
| 51 | + '--insecure', |
| 52 | + action='store_true', |
| 53 | + help='Run without mTLS credentials, --tls-certs-dir will be ignored.', |
| 54 | + ) |
| 55 | + parser.add_argument( |
| 56 | + '--packages', |
| 57 | + action='store_true', |
| 58 | + help='Discover python packages from function-pythonic ConfigMaps.' |
| 59 | + ) |
| 60 | + parser.add_argument( |
| 61 | + '--packages-secrets', |
| 62 | + action='store_true', |
| 63 | + help='Also Discover python packages from function-pythonic Secrets.' |
| 64 | + ) |
| 65 | + parser.add_argument( |
| 66 | + '--packages-namespace', |
| 67 | + action='append', |
| 68 | + default=[], |
| 69 | + metavar='NAMESPACE', |
| 70 | + help='Namespaces to discover function-pythonic ConfigMaps in, default is cluster wide.', |
| 71 | + ) |
| 72 | + parser.add_argument( |
| 73 | + '--packages-dir', |
| 74 | + default='./pythonic-packages', |
| 75 | + metavar='DIRECTORY', |
| 76 | + help='Directory to store discovered function-pythonic ConfigMaps to, defaults "<cwd>/pythonic-packages"' |
| 77 | + ) |
| 78 | + parser.add_argument( |
| 79 | + '--pip-install', |
| 80 | + metavar='COMMAND', |
| 81 | + help='Pip install command to install additional Python packages.' |
| 82 | + ) |
| 83 | + parser.add_argument( |
| 84 | + '--python-path', |
| 85 | + action='append', |
| 86 | + default=[], |
| 87 | + metavar='DIRECTORY', |
| 88 | + help='Filing system directories to add to the python path', |
| 89 | + ) |
| 90 | + parser.add_argument( |
| 91 | + '--allow-oversize-protos', |
| 92 | + action='store_true', |
| 93 | + help='Allow oversized protobuf messages' |
| 94 | + ) |
| 95 | + parser.add_argument( |
| 96 | + '--render-unknowns', |
| 97 | + action='store_true', |
| 98 | + help='Render resources with unknowns, useful during local develomment' |
| 99 | + ) |
| 100 | + args = parser.parse_args() |
| 101 | + if not args.tls_certs_dir and not args.insecure: |
| 102 | + print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr) |
| 103 | + sys.exit(1) |
| 104 | + |
| 105 | + if args.pip_install: |
| 106 | + import pip._internal.cli.main |
| 107 | + pip._internal.cli.main.main(['install', '--user', *shlex.split(args.pip_install)]) |
| 108 | + |
| 109 | + for path in reversed(args.python_path): |
| 110 | + sys.path.insert(0, str(pathlib.Path(path).expanduser().resolve())) |
| 111 | + |
| 112 | + self.configure_logging(args) |
| 113 | + # enables read only volumes or mismatched uid volumes |
| 114 | + sys.dont_write_bytecode = True |
| 115 | + await self.run(args) |
| 116 | + |
| 117 | + # Allow for independent running of function-pythonic |
| 118 | + async def run(self, args): |
| 119 | + if args.allow_oversize_protos: |
| 120 | + from google.protobuf.internal import api_implementation |
| 121 | + if api_implementation._c_module: |
| 122 | + api_implementation._c_module.SetAllowOversizeProtos(True) |
| 123 | + |
| 124 | + grpc.aio.init_grpc_aio() |
| 125 | + grpc_runner = function.FunctionRunner(args.debug, args.render_unknowns) |
| 126 | + grpc_server = grpc.aio.server() |
| 127 | + grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server) |
| 128 | + if args.insecure: |
| 129 | + grpc_server.add_insecure_port(args.address) |
| 130 | + else: |
| 131 | + certs = pathlib.Path(args.tls_certs_dir).expanduser().resolve() |
| 132 | + grpc_server.add_secure_port( |
| 133 | + args.address, |
| 134 | + grpc.ssl_server_credentials( |
| 135 | + private_key_certificate_chain_pairs=[( |
| 136 | + (certs / 'tls.key').read_bytes(), |
| 137 | + (certs / 'tls.crt').read_bytes(), |
| 138 | + )], |
| 139 | + root_certificates=(certs / 'ca.crt').read_bytes(), |
| 140 | + require_client_auth=True, |
| 141 | + ), |
| 142 | + ) |
| 143 | + await grpc_server.start() |
| 144 | + |
| 145 | + if args.packages: |
| 146 | + from . import packages |
| 147 | + async with asyncio.TaskGroup() as tasks: |
| 148 | + tasks.create_task(grpc_server.wait_for_termination()) |
| 149 | + tasks.create_task(packages.operator( |
| 150 | + grpc_server, |
| 151 | + grpc_runner, |
| 152 | + args.packages_secrets, |
| 153 | + args.packages_namespace, |
| 154 | + args.packages_dir, |
| 155 | + )) |
| 156 | + else: |
| 157 | + def stop(): |
| 158 | + asyncio.ensure_future(grpc_server.stop(5)) |
| 159 | + loop = asyncio.get_event_loop() |
| 160 | + loop.add_signal_handler(signal.SIGINT, stop) |
| 161 | + loop.add_signal_handler(signal.SIGTERM, stop) |
| 162 | + await grpc_server.wait_for_termination() |
| 163 | + |
| 164 | + def configure_logging(self, args): |
| 165 | + formatter = Formatter(args.log_name_width) |
| 166 | + handler = logging.StreamHandler() |
| 167 | + handler.setFormatter(formatter) |
| 168 | + logger = logging.getLogger() |
| 169 | + logger.handlers = [handler] |
| 170 | + logger.setLevel(logging.DEBUG if args.debug else logging.INFO) |
| 171 | + |
| 172 | + |
| 173 | +class Formatter(logging.Formatter): |
| 174 | + def __init__(self, name_width): |
| 175 | + super(Formatter, self).__init__( |
| 176 | + f"[{{asctime}}.{{msecs:03.0f}}] {{sname:{name_width}.{name_width}}} [{{levelname:8.8}}] {{message}}", |
| 177 | + '%Y-%m-%d %H:%M:%S', |
| 178 | + '{', |
| 179 | + ) |
| 180 | + self.name_width = name_width |
| 181 | + |
| 182 | + def format(self, record): |
| 183 | + record.sname = record.name |
| 184 | + extra = len(record.sname) - self.name_width |
| 185 | + if extra > 0: |
| 186 | + names = record.sname.split('.') |
| 187 | + for ix, name in enumerate(names): |
| 188 | + if len(name) > extra: |
| 189 | + names[ix] = name[extra:] |
| 190 | + break |
| 191 | + names[ix] = name[:1] |
| 192 | + extra -= len(name) - 1 |
| 193 | + record.sname = '.'.join(names) |
| 194 | + return super(Formatter, self).format(record) |
25 | 195 |
|
26 | 196 |
|
27 | 197 | if __name__ == '__main__': |
|
0 commit comments