Skip to content

Commit 702cc2a

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

File tree

10 files changed

+387
-114
lines changed

10 files changed

+387
-114
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,6 @@ cython_debug/
209209
marimo/_static/
210210
marimo/_lsp/
211211
__marimo__/
212+
213+
# function-pythonic
214+
pythonic-packages/

Dockerfile

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,20 @@
1-
# syntax=docker/dockerfile:1
1+
FROM python:3.13-slim-trixie AS image
22

3-
# It's important that this is Debian 12 to match the distroless image.
4-
FROM debian:12-slim AS build
5-
6-
#RUN --mount=type=cache,target=/var/lib/apt/lists \
7-
# --mount=type=cache,target=/var/cache/apt \
8-
RUN \
9-
rm -f /etc/apt/apt.conf.d/docker-clean \
10-
&& apt-get update \
11-
&& apt-get install --no-install-recommends --yes python3-venv git
12-
13-
# Don't write .pyc bytecode files. These speed up imports when the program is
14-
# loaded. There's no point doing that in a container where they'll never be
15-
# persisted across restarts.
16-
ENV PYTHONDONTWRITEBYTECODE=true
17-
18-
# Use Hatch to build a wheel. The build stage must do this in a venv because
19-
# Debian doesn't have a hatch package, and it won't let you install one globally
20-
# using pip.
21-
WORKDIR /build
22-
#RUN --mount=target=. \
23-
# --mount=type=cache,target=/root/.cache/pip \
24-
COPY . /build
25-
RUN \
26-
python3 -m venv /venv/build \
27-
&& /venv/build/bin/pip install hatch \
28-
&& /venv/build/bin/hatch build -t wheel /whl
29-
30-
# Create a fresh venv and install only the pythonic wheel into it.
31-
#RUN --mount=type=cache,target=/root/.cache/pip \
3+
WORKDIR /root/pythonic
4+
COPY pyproject.toml /root/pythonic
5+
COPY crossplane /root/pythonic/crossplane
6+
WORKDIR /
327
RUN \
33-
python3 -m venv /venv/fn \
34-
&& /venv/fn/bin/pip install /whl/*.whl
8+
set -eux && \
9+
cd /root/pythonic && \
10+
pip install --root-user-action ignore --no-build-isolation setuptools==80.9.0 && \
11+
pip install --root-user-action ignore --no-build-isolation . && \
12+
cd .. && \
13+
rm -rf .cache pythonic && \
14+
groupadd --gid 2000 pythonic && \
15+
useradd --uid 2000 --gid pythonic --home-dir /opt/pythonic --create-home --shell /usr/sbin/nologin pythonic
3516

36-
# Copy the pythonic venv to our runtime stage. It's important that the path be
37-
# 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
17+
USER pythonic:pythonic
18+
WORKDIR /opt/pythonic
4219
EXPOSE 9443
43-
ENTRYPOINT ["/venv/fn/bin/pythonic"]
20+
ENTRYPOINT ["python", "-m", "crossplane.pythonic.main"]

crossplane/pythonic/__version__.py

Lines changed: 0 additions & 18 deletions
This file was deleted.

crossplane/pythonic/function.py

Lines changed: 15 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,12 @@ def __init__(self, debug=False):
3233
self.logger = crossplane.function.logging.get_logger()
3334
self.clazzes = {}
3435

36+
def invalidate_module(self, module):
37+
self.clazzes.clear()
38+
if module in sys.modules:
39+
del sys.modules[module]
40+
importlib.invalidate_caches()
41+
3542
async def RunFunction(
3643
self, request: fnv1.RunFunctionRequest, _: grpc.aio.ServicerContext
3744
) -> fnv1.RunFunctionResponse:
@@ -70,52 +77,52 @@ async def RunFunction(
7077
try:
7178
exec(composite, module.__dict__)
7279
except Exception as e:
73-
crossplane.function.response.fatal(response, f"Exec exception: {e}")
7480
logger.exception('Exec exception')
81+
crossplane.function.response.fatal(response, f"Exec exception: {e}")
7582
return response
7683
composite = ['<script>', 'Composite']
7784
else:
7885
composite = composite.rsplit('.', 1)
7986
if len(composite) == 1:
80-
crossplane.function.response.fatal(response, f"Composite class name does not include module: {composite[0]}")
8187
logger.error(f"Composite class name does not include module: {composite[0]}")
88+
crossplane.function.response.fatal(response, f"Composite class name does not include module: {composite[0]}")
8289
return response
8390
try:
8491
module = importlib.import_module(composite[0])
8592
except Exception as e:
93+
logger.error(str(e))
8694
crossplane.function.response.fatal(response, f"Import module exception: {e}")
87-
logger.exception('Import module exception')
8895
return response
8996
clazz = getattr(module, composite[1], None)
9097
if not clazz:
91-
crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
9298
logger.error(f"{composite[0]} did not define: {composite[1]}")
99+
crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
93100
return response
94101
composite = '.'.join(composite)
95102
if not inspect.isclass(clazz):
96-
crossplane.function.response.fatal(response, f"{composite} is not a class")
97103
logger.error(f"{composite} is not a class")
104+
crossplane.function.response.fatal(response, f"{composite} is not a class")
98105
return response
99106
if not issubclass(clazz, BaseComposite):
100-
crossplane.function.response.fatal(response, f"{composite} is not a subclass of BaseComposite")
101107
logger.error(f"{composite} is not a subclass of BaseComposite")
108+
crossplane.function.response.fatal(response, f"{composite} is not a subclass of BaseComposite")
102109
return response
103110
self.clazzes[composite] = clazz
104111

105112
try:
106113
composite = clazz(request, response, logger)
107114
except Exception as e:
108-
crossplane.function.response.fatal(response, f"Instatiate exception: {e}")
109115
logger.exception('Instatiate exception')
116+
crossplane.function.response.fatal(response, f"Instatiate exception: {e}")
110117
return response
111118

112119
try:
113120
result = composite.compose()
114121
if asyncio.iscoroutine(result):
115122
await result
116123
except Exception as e:
117-
crossplane.function.response.fatal(response, f"Compose exception: {e}")
118124
logger.exception('Compose exception')
125+
crossplane.function.response.fatal(response, f"Compose exception: {e}")
119126
return response
120127

121128
unknownResources = []

crossplane/pythonic/main.py

Lines changed: 90 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,38 @@ 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+
)
53+
parser.add_argument(
54+
'--packages-dir',
55+
default='./pythonic-packages',
56+
help='Directory to store discovered function-pythonic ConfigMaps and Secrets to, defaults "<cwd>/pythonic-packages"'
57+
)
3758
parser.add_argument(
3859
'--pip-install',
3960
help='Pip install command to install additional Python packages.'
4061
)
4162
parser.add_argument(
4263
'--python-path',
4364
action='append',
65+
default=[],
4466
help='Filing system directories to add to the python path',
4567
)
4668
parser.add_argument(
@@ -49,33 +71,81 @@ def main():
4971
help='Allow oversized protobuf messages'
5072
)
5173
args = parser.parse_args()
52-
if not args.tls_certs_dir:
53-
args.tls_certs_dir = os.getenv('TLS_SERVER_CERTS_DIR')
74+
75+
if args.debug:
76+
crossplane.function.logging.configure(crossplane.function.logging.Level.DEBUG)
77+
else:
78+
crossplane.function.logging.configure(crossplane.function.logging.Level.INFO)
5479

5580
if args.pip_install:
5681
pip._internal.cli.main.main(['install', *shlex.split(args.pip_install)])
5782

58-
if args.python_path:
59-
for path in reversed(args.python_path):
60-
sys.path.insert(0, path)
83+
# enables read only volumes or mismatched uid volumes
84+
sys.dont_write_bytecode = True
85+
for path in reversed(args.python_path):
86+
sys.path.insert(0, str(pathlib.Path(path).resolve()))
6187

6288
if args.allow_oversize_protos:
6389
from google.protobuf.internal import api_implementation
6490
if api_implementation._c_module:
6591
api_implementation._c_module.SetAllowOversizeProtos(True)
6692

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-
)
93+
grpc.aio.init_grpc_aio()
94+
grpc_runner = function.FunctionRunner(args.debug)
95+
grpc_server = grpc.aio.server()
96+
grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
97+
if args.tls_certs_dir:
98+
certs = pathlib.Path(args.tls_certs_dir)
99+
grpc_server.add_secure_port(
100+
args.address,
101+
grpc.ssl_server_credentials(
102+
private_key_certificate_chain_pairs=[(
103+
(certs / 'tls.key').read_bytes(),
104+
(certs / 'tls.crt').read_bytes(),
105+
)],
106+
root_certificates=(certs / 'ca.crt').read_bytes(),
107+
require_client_auth=True,
108+
),
109+
)
110+
else:
111+
if not args.insecure:
112+
raise ValueError('Either --tls-certs-dir or --insecure must be specified')
113+
grpc_server.add_insecure_port(args.address)
114+
await grpc_server.start()
115+
116+
if args.packages:
117+
import kopf._core.actions.loggers
118+
import kopf._core.reactor.running
119+
from . import packages
120+
packages_dir = pathlib.Path(args.packages_dir).expanduser().resolve()
121+
sys.path.insert(0, str(packages_dir))
122+
packages.setup(packages_dir, grpc_runner)
123+
kopf._core.actions.loggers.configure()
124+
@kopf.on.startup()
125+
async def startup(settings, **_):
126+
settings.scanning.disabled = True
127+
@kopf.on.cleanup()
128+
async def cleanup(logger=None, **_):
129+
await grpc_server.stop(5)
130+
async with asyncio.TaskGroup() as tasks:
131+
tasks.create_task(grpc_server.wait_for_termination())
132+
tasks.create_task(kopf._core.reactor.running.operator(
133+
standalone=True,
134+
clusterwide=not args.packages_namespace,
135+
namespaces=args.packages_namespace,
136+
))
137+
else:
138+
def stop():
139+
asyncio.ensure_future(grpc_server.stop(5))
140+
loop = asyncio.get_event_loop()
141+
loop.add_signal_handler(signal.SIGINT, stop)
142+
loop.add_signal_handler(signal.SIGTERM, stop)
143+
await grpc_server.wait_for_termination()
74144

75145

76146
if __name__ == "__main__":
77147
try:
78-
main()
79-
except Exception as e:
80-
print(f"Exception running main: {e}", file=sys.stderr)
148+
asyncio.run(main())
149+
except:
150+
print(traceback.format_exc())
81151
sys.exit(1)

0 commit comments

Comments
 (0)