Skip to content

Commit 6fda706

Browse files
author
Patrick J. McNerthney
committed
Implement ConfigMap and Secret based python packages.
1 parent fb6f050 commit 6fda706

File tree

8 files changed

+372
-48
lines changed

8 files changed

+372
-48
lines changed

Dockerfile

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# syntax=docker/dockerfile:1
22

3-
# It's important that this is Debian 12 to match the distroless image.
4-
FROM debian:12-slim AS build
3+
# It's important that this is Debian to match the python image.
4+
FROM debian:trixie-slim AS build
55

66
#RUN --mount=type=cache,target=/var/lib/apt/lists \
77
# --mount=type=cache,target=/var/cache/apt \
@@ -35,9 +35,13 @@ RUN \
3535

3636
# Copy the pythonic venv to our runtime stage. It's important that the path be
3737
# the same as in the build stage, to avoid shebang paths and symlinks breaking.
38-
FROM gcr.io/distroless/python3-debian12 AS image
39-
WORKDIR /
40-
USER nonroot:nonroot
41-
COPY --from=build --chown=nonroot:nonroot /venv/fn /venv/fn
38+
FROM python:3.13-slim-trixie AS image
39+
RUN \
40+
addgroup --gid 2000 pythonic && \
41+
adduser --uid 2000 --ingroup pythonic --disabled-password --no-create-home --disabled-login pythonic
42+
USER pythonic:pythonic
43+
COPY --from=build --chown=pythonic:pythonic /venv/fn /venv/fn
44+
RUN \
45+
ln -sf /usr/local/bin/python3 /venv/fn/bin/python3
4246
EXPOSE 9443
43-
ENTRYPOINT ["/venv/fn/bin/pythonic"]
47+
ENTRYPOINT ["/venv/fn/bin/python", "-m", "crossplane.pythonic.main"]

crossplane/pythonic/function.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import builtins
66
import importlib
77
import inspect
8+
import sys
89

910
import grpc
1011
import crossplane.function.logging
@@ -32,6 +33,14 @@ def __init__(self, debug=False):
3233
self.logger = crossplane.function.logging.get_logger()
3334
self.clazzes = {}
3435

36+
def invalidate_module(self, module):
37+
if module in sys.modules:
38+
del sys.modules[module]
39+
for composite in [composite for composite in self.clazzes.keys()]:
40+
if '\n' not in composite:
41+
if composite.rsplit('.', 1)[0] == module:
42+
del self.clazzes[composite]
43+
3544
async def RunFunction(
3645
self, request: fnv1.RunFunctionRequest, _: grpc.aio.ServicerContext
3746
) -> fnv1.RunFunctionResponse:
@@ -70,52 +79,52 @@ async def RunFunction(
7079
try:
7180
exec(composite, module.__dict__)
7281
except Exception as e:
73-
crossplane.function.response.fatal(response, f"Exec exception: {e}")
7482
logger.exception('Exec exception')
83+
crossplane.function.response.fatal(response, f"Exec exception: {e}")
7584
return response
7685
composite = ['<script>', 'Composite']
7786
else:
7887
composite = composite.rsplit('.', 1)
7988
if len(composite) == 1:
80-
crossplane.function.response.fatal(response, f"Composite class name does not include module: {composite[0]}")
8189
logger.error(f"Composite class name does not include module: {composite[0]}")
90+
crossplane.function.response.fatal(response, f"Composite class name does not include module: {composite[0]}")
8291
return response
8392
try:
8493
module = importlib.import_module(composite[0])
8594
except Exception as e:
95+
logger.error(str(e))
8696
crossplane.function.response.fatal(response, f"Import module exception: {e}")
87-
logger.exception('Import module exception')
8897
return response
8998
clazz = getattr(module, composite[1], None)
9099
if not clazz:
91-
crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
92100
logger.error(f"{composite[0]} did not define: {composite[1]}")
101+
crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
93102
return response
94103
composite = '.'.join(composite)
95104
if not inspect.isclass(clazz):
96-
crossplane.function.response.fatal(response, f"{composite} is not a class")
97105
logger.error(f"{composite} is not a class")
106+
crossplane.function.response.fatal(response, f"{composite} is not a class")
98107
return response
99108
if not issubclass(clazz, BaseComposite):
100-
crossplane.function.response.fatal(response, f"{composite} is not a subclass of BaseComposite")
101109
logger.error(f"{composite} is not a subclass of BaseComposite")
110+
crossplane.function.response.fatal(response, f"{composite} is not a subclass of BaseComposite")
102111
return response
103112
self.clazzes[composite] = clazz
104113

105114
try:
106115
composite = clazz(request, response, logger)
107116
except Exception as e:
108-
crossplane.function.response.fatal(response, f"Instatiate exception: {e}")
109117
logger.exception('Instatiate exception')
118+
crossplane.function.response.fatal(response, f"Instatiate exception: {e}")
110119
return response
111120

112121
try:
113122
result = composite.compose()
114123
if asyncio.iscoroutine(result):
115124
await result
116125
except Exception as e:
117-
crossplane.function.response.fatal(response, f"Compose exception: {e}")
118126
logger.exception('Compose exception')
127+
crossplane.function.response.fatal(response, f"Compose exception: {e}")
119128
return response
120129

121130
unknownResources = []

crossplane/pythonic/main.py

Lines changed: 84 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
"""The composition function's main CLI."""
22

3-
import warnings
4-
warnings.filterwarnings('ignore', module='^google[.]protobuf[.]runtime_version$', lineno=98)
5-
63
import argparse
4+
import asyncio
75
import os
6+
import pathlib
87
import shlex
8+
import signal
99
import sys
10+
import traceback
11+
12+
import crossplane.function.logging
13+
import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1
14+
import grpc
1015
import pip._internal.cli.main
11-
from crossplane.function import logging, runtime
1216

1317
from . import function
1418

1519

16-
def main():
20+
async def main():
1721
parser = argparse.ArgumentParser('Forta Crossplane Function')
1822
parser.add_argument(
1923
'--debug', '-d',
@@ -27,20 +31,33 @@ def main():
2731
)
2832
parser.add_argument(
2933
'--tls-certs-dir',
34+
default=os.getenv('TLS_SERVER_CERTS_DIR'),
3035
help='Serve using mTLS certificates.',
3136
)
3237
parser.add_argument(
3338
'--insecure',
3439
action='store_true',
3540
help='Run without mTLS credentials. If you supply this flag --tls-certs-dir will be ignored.',
3641
)
42+
parser.add_argument(
43+
'--packages',
44+
action='store_true',
45+
help='Discover python packages from function-pythonic ConfigMaps and Secrets.'
46+
)
47+
parser.add_argument(
48+
'--packages-namespace',
49+
action='append',
50+
default=[],
51+
help='Namespaces to discover function-pythonic ConfigMaps and Secrets in, default is cluster wide.',
52+
)
3753
parser.add_argument(
3854
'--pip-install',
3955
help='Pip install command to install additional Python packages.'
4056
)
4157
parser.add_argument(
4258
'--python-path',
4359
action='append',
60+
default=[],
4461
help='Filing system directories to add to the python path',
4562
)
4663
parser.add_argument(
@@ -49,33 +66,80 @@ def main():
4966
help='Allow oversized protobuf messages'
5067
)
5168
args = parser.parse_args()
52-
if not args.tls_certs_dir:
53-
args.tls_certs_dir = os.getenv('TLS_SERVER_CERTS_DIR')
69+
70+
if args.debug:
71+
crossplane.function.logging.configure(crossplane.function.logging.Level.DEBUG)
72+
else:
73+
crossplane.function.logging.configure(crossplane.function.logging.Level.INFO)
5474

5575
if args.pip_install:
5676
pip._internal.cli.main.main(['install', *shlex.split(args.pip_install)])
5777

58-
if args.python_path:
59-
for path in reversed(args.python_path):
60-
sys.path.insert(0, path)
78+
# enables read only volumes or mismatched uid volumes
79+
sys.dont_write_bytecode = True
80+
for path in reversed(args.python_path):
81+
sys.path.insert(0, path)
6182

6283
if args.allow_oversize_protos:
6384
from google.protobuf.internal import api_implementation
6485
if api_implementation._c_module:
6586
api_implementation._c_module.SetAllowOversizeProtos(True)
6687

67-
logging.configure(logging.Level.DEBUG if args.debug else logging.Level.INFO)
68-
runtime.serve(
69-
function.FunctionRunner(args.debug),
70-
args.address,
71-
creds=runtime.load_credentials(args.tls_certs_dir),
72-
insecure=args.insecure,
73-
)
88+
grpc.aio.init_grpc_aio()
89+
grpc_runner = function.FunctionRunner(args.debug)
90+
grpc_server = grpc.aio.server()
91+
grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
92+
if args.tls_certs_dir:
93+
certs = pathlib.Path(args.tls_certs_dir)
94+
grpc_server.add_secure_port(
95+
args.address,
96+
grpc.ssl_server_credentials(
97+
private_key_certificate_chain_pairs=[(
98+
(certs / 'tls.key').read_bytes(),
99+
(certs / 'tls.crt').read_bytes(),
100+
)],
101+
root_certificates=(certs / 'ca.crt').read_bytes(),
102+
require_client_auth=True,
103+
),
104+
)
105+
else:
106+
if not args.insecure:
107+
raise ValueError('Either --tls-certs-dir or --insecure must be specified')
108+
grpc_server.add_insecure_port(args.address)
109+
await grpc_server.start()
110+
111+
if args.packages:
112+
import kopf._core.actions.loggers
113+
import kopf._core.reactor.running
114+
from . import packages
115+
sys.path.insert(0, str(packages.PACKAGES_DIR))
116+
packages.register_grpc_runner(grpc_runner)
117+
kopf._core.actions.loggers.configure()
118+
@kopf.on.startup()
119+
async def startup(settings, **_):
120+
settings.scanning.disabled = True
121+
@kopf.on.cleanup()
122+
async def cleanup(logger=None, **_):
123+
await grpc_server.stop(5)
124+
async with asyncio.TaskGroup() as tasks:
125+
tasks.create_task(grpc_server.wait_for_termination())
126+
tasks.create_task(kopf._core.reactor.running.operator(
127+
standalone=True,
128+
clusterwide=not args.packages_namespace,
129+
namespaces=args.packages_namespace,
130+
))
131+
else:
132+
def stop():
133+
asyncio.ensure_future(grpc_server.stop(5))
134+
loop = asyncio.get_event_loop()
135+
loop.add_signal_handler(signal.SIGINT, stop)
136+
loop.add_signal_handler(signal.SIGTERM, stop)
137+
await grpc_server.wait_for_termination()
74138

75139

76140
if __name__ == "__main__":
77141
try:
78-
main()
79-
except Exception as e:
80-
print(f"Exception running main: {e}", file=sys.stderr)
142+
asyncio.run(main())
143+
except:
144+
print(traceback.format_exc())
81145
sys.exit(1)

0 commit comments

Comments
 (0)