Skip to content

Commit f5aef77

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

File tree

18 files changed

+469
-179
lines changed

18 files changed

+469
-179
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: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,21 @@
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+
pip uninstall --root-user-action ignore --yes setuptools && \
13+
cd .. && \
14+
rm -rf .cache pythonic && \
15+
groupadd --gid 2000 pythonic && \
16+
useradd --uid 2000 --gid pythonic --home-dir /opt/pythonic --create-home --shell /usr/sbin/nologin pythonic
3517

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
18+
USER pythonic:pythonic
19+
WORKDIR /opt/pythonic
4220
EXPOSE 9443
43-
ENTRYPOINT ["/venv/fn/bin/pythonic"]
21+
ENTRYPOINT ["python", "-m", "crossplane.pythonic.main"]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ The BaseComposite class provides the following fields for manipulating the Compo
181181
| self.status | Map | The composite desired and observed status, read from observed if not in desired |
182182
| self.conditions | Conditions | The composite desired and observed conditions, read from observed if not in desired |
183183
| self.connection | Connection | The composite desired and observed connection detials, read from observed if not in desired |
184-
| self.results | Results | Returned results on the Composite and optionally on the Claim |
184+
| self.events | Events | Returned events against the Composite and optionally on the Claim |
185185
| self.ready | Boolean | The composite desired ready state |
186186

187187
The BaseComposite also provides access to the following Crossplane Function level features:

crossplane/pythonic/__version__.py

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

crossplane/pythonic/composite.py

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self, request, response, logger):
3232
self.status = Status(self.observed.status, self.desired.status)
3333
self.conditions = Conditions(observed, self.response)
3434
self.connection = Connection(observed, desired)
35-
self.results = Results(self.response)
35+
self.events = Events(self.response)
3636

3737
@property
3838
def ttl(self):
@@ -384,14 +384,14 @@ def _protobuf_value(self):
384384
value['lastTransitionTime'] = time.isoformat().replace('+00:00', 'Z')
385385
return value
386386

387-
def __call__(self, status=_notset, reason=_notset, message=_notset, claim=_notset):
387+
def __call__(self, reason=_notset, message=_notset, status=_notset, claim=_notset):
388388
self._find_condition(True)
389-
if status != _notset:
390-
self.status = status
391389
if reason != _notset:
392390
self.reason = reason
393391
if message != _notset:
394392
self.message = message
393+
if status != _notset:
394+
self.status = status
395395
if claim != _notset:
396396
self.claim = claim
397397
return self
@@ -514,39 +514,42 @@ def __setitem__(self, key, value):
514514
self._desired.connection_details[key] = value
515515

516516

517-
class Results:
517+
class Events:
518518
def __init__(self, response):
519519
self._results = response.results
520520

521-
def info(self, message, reason=_notset, claim=_notset):
522-
result = Result(self._results.append())
523-
result.info = True
524-
result.message = message
521+
def info(self, reason=_notset, message=_notset, claim=_notset):
522+
event = Event(self._results.append())
523+
event.info = True
525524
if reason != _notset:
526-
result.reason = reason
525+
event.reason = reason
526+
if message != _notset:
527+
event.message = message
527528
if claim != _notset:
528-
result.claim = claim
529-
return result
529+
event.claim = claim
530+
return event
530531

531-
def warning(self, message, reason=_notset, claim=_notset):
532-
result = Result(self._results.append())
533-
result.warning = True
534-
result.message = message
532+
def warning(self, reason=_notset, message=_notset, claim=_notset):
533+
event = Event(self._results.append())
534+
event.warning = True
535535
if reason != _notset:
536-
result.reason = reason
536+
event.reason = reason
537+
if message != _notset:
538+
event.message = message
537539
if claim != _notset:
538-
result.claim = claim
539-
return result
540+
event.claim = claim
541+
return event
540542

541-
def fatal(self, message, reason=_notset, claim=_notset):
542-
result = Result(self._results.append())
543-
result.fatal = True
544-
result.message = message
543+
def fatal(self, reason=_notset, message=_notset, claim=_notset):
544+
event = Event(self._results.append())
545+
event.fatal = True
545546
if reason != _notset:
546-
result.reason = reason
547+
event.reason = reason
548+
if message != _notset:
549+
event.message = message
547550
if claim != _notset:
548-
result.claim = claim
549-
return result
551+
event.claim = claim
552+
return event
550553

551554
def __bool__(self):
552555
return len(self) > 0
@@ -556,15 +559,15 @@ def __len__(self):
556559

557560
def __getitem__(self, key):
558561
if key >= len(self._results):
559-
return Result()
560-
return Result(self._results[ix])
562+
return Event()
563+
return Event(self._results[ix])
561564

562565
def __iter__(self):
563566
for ix in range(len(self._results)):
564567
yield self[ix]
565568

566569

567-
class Result:
570+
class Event:
568571
def __init__(self, result=None):
569572
self._result = result
570573

crossplane/pythonic/function.py

Lines changed: 40 additions & 24 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 = []
@@ -150,27 +157,36 @@ async def RunFunction(
150157
resource.desired._patchUnknowns(resource.observed)
151158
else:
152159
del composite.resources[name]
160+
153161
if fatalResources:
154-
if not self.debug:
155-
logger.error('Observed resources with unknowns', resources=fatalResources)
162+
level = logger.error
163+
reason = 'FatalUnknowns'
156164
message = f"Observed resources with unknowns: {','.join(fatalResources)}"
157-
composite.conditions.NoUnknowns(False, 'FatalUnknowns', message)
158-
composite.results.fatal(message, 'FatalUnknowns')
159-
return response
160-
if warningResources:
161-
if not self.debug:
162-
logger.warning('Observed resources with unknowns', resources=fatalResources)
165+
status = False
166+
event = composite.events.fatal
167+
elif warningResources:
168+
level = logger.warning
169+
reason = 'ObservedUnknowns'
163170
message = f"Observed resources with unknowns: {','.join(warningResources)}"
164-
composite.conditions.NoUnknowns(False, 'ObservedUnknowns', message)
165-
composite.results.warning(message, 'ObservedUnknowns')
171+
status = False
172+
event = composite.events.warning
166173
elif unknownResources:
167-
if not self.debug:
168-
logger.info('New resources with unknowns', resources=unknownResources)
169-
message = f"New resources with unknowns: {','.join(unknownResources)}"
170-
composite.conditions.NoUnknowns(False, 'NewUnknowns', message)
171-
composite.results.info(message, 'NewUnknowns')
174+
level = logger.info
175+
reason = 'DesiredUnknowns'
176+
message = f"Desired resources with unknowns: {','.join(unknownResources)}"
177+
status = False
178+
event = composite.events.info
172179
else:
173-
composite.conditions.NoUnknowns(True, 'AllResolved', 'All resources are resolved')
180+
level = None
181+
reason = 'AllComposed'
182+
message = 'All resources are composed'
183+
status = True
184+
event = None
185+
if not self.debug and level:
186+
level(message)
187+
composite.conditions.ResourcesComposed(reason, message, status)
188+
if event:
189+
event(reason, message)
174190

175191
for name, resource in composite.resources:
176192
if resource.autoReady or (resource.autoReady is None and composite.autoReady):

0 commit comments

Comments
 (0)