Skip to content

Commit 8dfdd41

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

File tree

18 files changed

+494
-180
lines changed

18 files changed

+494
-180
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: 65 additions & 25 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,9 +33,22 @@ 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:
45+
try:
46+
return await self.run_function(request)
47+
except:
48+
self.logger.exception('Exception thrown in run fuction')
49+
raise
50+
51+
async def run_function(self, request):
3852
composite = request.observed.composite.resource
3953
logger = self.logger.bind(
4054
apiVersion=composite['apiVersion'],
@@ -70,54 +84,71 @@ async def RunFunction(
7084
try:
7185
exec(composite, module.__dict__)
7286
except Exception as e:
73-
crossplane.function.response.fatal(response, f"Exec exception: {e}")
7487
logger.exception('Exec exception')
88+
crossplane.function.response.fatal(response, f"Exec exception: {e}")
7589
return response
7690
composite = ['<script>', 'Composite']
7791
else:
7892
composite = composite.rsplit('.', 1)
7993
if len(composite) == 1:
80-
crossplane.function.response.fatal(response, f"Composite class name does not include module: {composite[0]}")
8194
logger.error(f"Composite class name does not include module: {composite[0]}")
95+
crossplane.function.response.fatal(response, f"Composite class name does not include module: {composite[0]}")
8296
return response
8397
try:
8498
module = importlib.import_module(composite[0])
8599
except Exception as e:
100+
logger.error(str(e))
86101
crossplane.function.response.fatal(response, f"Import module exception: {e}")
87-
logger.exception('Import module exception')
88102
return response
89103
clazz = getattr(module, composite[1], None)
90104
if not clazz:
91-
crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
92105
logger.error(f"{composite[0]} did not define: {composite[1]}")
106+
crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
93107
return response
94108
composite = '.'.join(composite)
95109
if not inspect.isclass(clazz):
96-
crossplane.function.response.fatal(response, f"{composite} is not a class")
97110
logger.error(f"{composite} is not a class")
111+
crossplane.function.response.fatal(response, f"{composite} is not a class")
98112
return response
99113
if not issubclass(clazz, BaseComposite):
100-
crossplane.function.response.fatal(response, f"{composite} is not a subclass of BaseComposite")
101114
logger.error(f"{composite} is not a subclass of BaseComposite")
115+
crossplane.function.response.fatal(response, f"{composite} is not a subclass of BaseComposite")
102116
return response
103117
self.clazzes[composite] = clazz
104118

105119
try:
106120
composite = clazz(request, response, logger)
107121
except Exception as e:
108-
crossplane.function.response.fatal(response, f"Instatiate exception: {e}")
109122
logger.exception('Instatiate exception')
123+
crossplane.function.response.fatal(response, f"Instatiate exception: {e}")
110124
return response
111125

112126
try:
113127
result = composite.compose()
114128
if asyncio.iscoroutine(result):
115129
await result
116130
except Exception as e:
117-
crossplane.function.response.fatal(response, f"Compose exception: {e}")
118131
logger.exception('Compose exception')
132+
crossplane.function.response.fatal(response, f"Compose exception: {e}")
119133
return response
120134

135+
if len(composite.response.requirements.extra_resources):
136+
requireds = Map()
137+
for name, selector in composite.response.requirements.extra_resources:
138+
requireds[name].apiVersion = selector.api_version
139+
requireds[name].kind = selector.kind
140+
if selector.namespace:
141+
requireds[name].namespace = selector.namespace
142+
if selector.match_name:
143+
requireds[name].name = selector.match_name
144+
if len(selector.match_labels.labels):
145+
for key, value in selector.match_labels.labels:
146+
requireds[name].labels[key] = value
147+
if requireds != composite.context._requireds:
148+
composite.context._requireds = requireds
149+
logger.debug('Requireds requsted')
150+
return response
151+
121152
unknownResources = []
122153
warningResources = []
123154
fatalResources = []
@@ -145,32 +176,41 @@ async def RunFunction(
145176
elif warning:
146177
logger.warning('Observed unknown', destination=destination, source=source)
147178
else:
148-
logger.debug('New unknown', destination=destination, source=source)
179+
logger.debug('Desired unknown', destination=destination, source=source)
149180
if resource.observed:
150181
resource.desired._patchUnknowns(resource.observed)
151182
else:
152183
del composite.resources[name]
184+
153185
if fatalResources:
154-
if not self.debug:
155-
logger.error('Observed resources with unknowns', resources=fatalResources)
186+
level = logger.error
187+
reason = 'FatalUnknowns'
156188
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)
189+
status = False
190+
event = composite.events.fatal
191+
elif warningResources:
192+
level = logger.warning
193+
reason = 'ObservedUnknowns'
163194
message = f"Observed resources with unknowns: {','.join(warningResources)}"
164-
composite.conditions.NoUnknowns(False, 'ObservedUnknowns', message)
165-
composite.results.warning(message, 'ObservedUnknowns')
195+
status = False
196+
event = composite.events.warning
166197
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')
198+
level = logger.info
199+
reason = 'DesiredUnknowns'
200+
message = f"Desired resources with unknowns: {','.join(unknownResources)}"
201+
status = False
202+
event = composite.events.info
172203
else:
173-
composite.conditions.NoUnknowns(True, 'AllResolved', 'All resources are resolved')
204+
level = None
205+
reason = 'AllComposed'
206+
message = 'All resources are composed'
207+
status = True
208+
event = None
209+
if not self.debug and level:
210+
level(message)
211+
composite.conditions.ResourcesComposed(reason, message, status)
212+
if event:
213+
event(reason, message)
174214

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

0 commit comments

Comments
 (0)