Skip to content
This repository was archived by the owner on Dec 10, 2025. It is now read-only.

Commit eb6cec0

Browse files
Patrick J. McNerthneyiciclespider
authored andcommitted
Refactor to use the crossplane.pythonic package.
Upgrade to use crossplane-sdk-python v0.9.0. Implement being able to specify the full path to the Composite class.
1 parent 8172f55 commit eb6cec0

29 files changed

+458
-123
lines changed

.github/workflows/ci.yaml

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ jobs:
127127
build-args:
128128
PYTHON_VERSION=${{ env.PYTHON_VERSION }}
129129
outputs: type=docker,dest=runtime-${{ matrix.arch }}.tar
130-
130+
131131
- name: Setup the Crossplane CLI
132-
run: "curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh"
132+
run: "mkdir bin && cd bin && curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh"
133133

134134
- name: Build Package
135-
run: ./crossplane xpkg build --package-file=${{ matrix.arch }}.xpkg --package-root=package/ --embed-runtime-image-tarball=runtime-${{ matrix.arch }}.tar
136-
135+
run: bin/crossplane xpkg build --package-file=${{ matrix.arch }}.xpkg --package-root=package/ --embed-runtime-image-tarball=runtime-${{ matrix.arch }}.tar
136+
137137
- name: Upload Single-Platform Package
138138
uses: actions/upload-artifact@v4
139139
with:
@@ -165,7 +165,15 @@ jobs:
165165
merge-multiple: true
166166

167167
- name: Setup the Crossplane CLI
168-
run: "curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh"
168+
run: "mkdir bin && cd bin && curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh"
169+
170+
# If a version wasn't explicitly passed as a workflow_dispatch input we
171+
# default to version v0.0.0-<git-commit-date>-<git-short-sha>, for example
172+
# v0.0.0-20231101115142-1091066df799. This is a simple implementation of
173+
# Go's pseudo-versions: https://go.dev/ref/mod#pseudo-versions.
174+
- name: Set Default Multi-Platform Package Version
175+
if: env.XPKG_VERSION == ''
176+
run: echo "XPKG_VERSION=v0.0.0-$(date -d@$(git show -s --format=%ct) +%Y%m%d%H%M%S)-$(git rev-parse --short=12 HEAD)" >> $GITHUB_ENV
169177

170178
# - name: Login to Upbound
171179
# uses: docker/login-action@v3
@@ -175,17 +183,9 @@ jobs:
175183
# username: ${{ secrets.XPKG_ACCESS_ID }}
176184
# password: ${{ secrets.XPKG_TOKEN }}
177185

178-
# If a version wasn't explicitly passed as a workflow_dispatch input we
179-
# default to version v0.0.0-<git-commit-date>-<git-short-sha>, for example
180-
# v0.0.0-20231101115142-1091066df799. This is a simple implementation of
181-
# Go's pseudo-versions: https://go.dev/ref/mod#pseudo-versions.
182-
# - name: Set Default Multi-Platform Package Version
183-
# if: env.XPKG_VERSION == ''
184-
# run: echo "XPKG_VERSION=v0.0.0-$(date -d@$(git show -s --format=%ct) +%Y%m%d%H%M%S)-$(git rev-parse --short=12 HEAD)" >> $GITHUB_ENV
185-
186186
# - name: Push Multi-Platform Package to Upbound
187187
# if: env.XPKG_ACCESS_ID != ''
188-
# run: "./crossplane --verbose xpkg push --package-files $(echo *.xpkg|tr ' ' ,) ${{ env.XPKG }}:${{ env.XPKG_VERSION }}"
188+
# run: "bin/crossplane --verbose xpkg push --package-files $(echo *.xpkg|tr ' ' ,) ${{ env.XPKG }}:${{ env.XPKG_VERSION }}"
189189

190190
- name: Login to GHCR
191191
uses: docker/login-action@v3
@@ -195,4 +195,4 @@ jobs:
195195
password: ${{ secrets.GITHUB_TOKEN }}
196196

197197
- name: Push Multi-Platform Package to GHCR
198-
run: "./crossplane --verbose xpkg push --package-files $(echo *.xpkg|tr ' ' ,) ${{ env.CROSSPLANE_REGORG }}:${{ env.XPKG_VERSION }}"
198+
run: "bin/crossplane --verbose xpkg push --package-files $(echo *.xpkg|tr ' ' ,) ${{ env.CROSSPLANE_REGORG }}:${{ env.XPKG_VERSION }}"

CloudOps.groovy

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

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,17 @@ RUN \
2727
&& /venv/build/bin/pip install hatch \
2828
&& /venv/build/bin/hatch build -t wheel /whl
2929

30-
# Create a fresh venv and install only the function wheel into it.
30+
# Create a fresh venv and install only the pythonic wheel into it.
3131
#RUN --mount=type=cache,target=/root/.cache/pip \
3232
RUN \
3333
python3 -m venv /venv/fn \
3434
&& /venv/fn/bin/pip install /whl/*.whl
3535

36-
# Copy the function venv to our runtime stage. It's important that the path be
36+
# 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.
3838
FROM gcr.io/distroless/python3-debian12 AS image
3939
WORKDIR /
4040
USER nonroot:nonroot
4141
COPY --from=build --chown=nonroot:nonroot /venv/fn /venv/fn
4242
EXPOSE 9443
43-
ENTRYPOINT ["/venv/fn/bin/function"]
43+
ENTRYPOINT ["/venv/fn/bin/pythonic"]

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ spec:
3535
vpc.spec.forProvider.cidrBlock = self.spec.cidr
3636
self.status.vpcId = vpc.status.atProvider.vpcId
3737
```
38+
In addtion to an inline script, the python implementation can be specified
39+
as the complete path to a python class. See [Filing system Composites](#filing-system-composites).
3840
3941
## Examples
4042
@@ -270,6 +272,38 @@ spec:
270272
self.status.composite = 'Hello, World!'
271273
```
272274
275+
## Filing system Composites
276+
277+
Composition Composite implementations can be coded in a stand alone python files
278+
by configuring the function-pythonic deployment with the code mounted into
279+
the package-runtime container, and then adding the mount point to the python
280+
path using the --python-path command line option.
281+
```yaml
282+
apiVersion: pkg.crossplane.io/v1beta1
283+
kind: DeploymentRuntimeConfig
284+
metadata:
285+
name: function-pythonic
286+
spec:
287+
deploymentTemplate:
288+
spec:
289+
template:
290+
spec:
291+
containers:
292+
- name: package-runtime
293+
args:
294+
- --debug
295+
- --python-path
296+
- /mnt/composites
297+
volumeMounts:
298+
- name: composites
299+
mountPath: /mnt/composites
300+
volumes:
301+
- name: composites
302+
configMap:
303+
name: pythonic-composites
304+
```
305+
See the [filing-system](examples/filing-system) example.
306+
273307
## Install Additional Python Packages
274308
275309
function-pythonic supports a `--pip-install` command line option which will run pip install

crossplane/pythonic/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import base64
2+
3+
from .composite import BaseComposite
4+
from .protobuf import Map, List, Unknown, Yaml, Json
5+
B64Encode = lambda s: base64.b64encode(s.encode('utf-8')).decode('utf-8')
6+
B64Decode = lambda s: base64.b64decode(s.encode('utf-8')).decode('utf-8')
7+
8+
__all__ = [
9+
'BaseComposite',
10+
'Map',
11+
'List',
12+
'Unknown',
13+
'Yaml',
14+
'Json',
15+
'B64Encode',
16+
'B64Decode',
17+
]
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
import datetime
33
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
44

5-
import function.protobuf
5+
from . import protobuf
66

77

88
_notset = object()
99

1010

1111
class BaseComposite:
1212
def __init__(self, request, response, logger):
13-
self.request = function.protobuf.Message(None, None, request.DESCRIPTOR, request, 'Function Request')
14-
self.response = function.protobuf.Message(None, None, response.DESCRIPTOR, response)
13+
self.request = protobuf.Message(None, None, request.DESCRIPTOR, request, 'Function Request')
14+
self.response = protobuf.Message(None, None, response.DESCRIPTOR, response)
1515
self.logger = logger
1616
self.autoReady = True
1717
self.credentials = Credentials(self.request)
@@ -54,7 +54,7 @@ def ready(self):
5454
def ready(self, ready):
5555
if ready:
5656
ready = fnv1.Ready.READY_TRUE
57-
elif ready == None or (isinstance(ready, function.protobuf.Values) and ready._isUnknown):
57+
elif ready == None or (isinstance(ready, protobuf.Values) and ready._isUnknown):
5858
ready = fnv1.Ready.READY_UNSPECIFIED
5959
else:
6060
ready = fnv1.Ready.READY_FALSE
@@ -115,7 +115,6 @@ def __setattr__(self, key, resource):
115115
self[key] = resource
116116

117117
def __setitem__(self, key, resource):
118-
print('SETITEM', key, resource)
119118
self._composite.response.desired.resources[key].resource = resource
120119

121120
def __delattr__(self, key):
@@ -216,7 +215,7 @@ def ready(self):
216215
def ready(self, ready):
217216
if ready:
218217
ready = fnv1.Ready.READY_TRUE
219-
elif ready == None or (isinstance(ready, function.protobuf.Values) and ready._isUnknown):
218+
elif ready == None or (isinstance(ready, protobuf.Values) and ready._isUnknown):
220219
ready = fnv1.Ready.READY_UNSPECIFIED
221220
else:
222221
ready = fnv1.Ready.READY_FALSE
@@ -422,7 +421,7 @@ def claim(self, claim):
422421
if bool(self):
423422
if claim:
424423
self._result.target = fnv1.Target.TARGET_COMPOSITE_AND_CLAIM
425-
elif claim == None or (isinstance(claim, function.protobuf.Values) and claim._isUnknown):
424+
elif claim == None or (isinstance(claim, protobuf.Values) and claim._isUnknown):
426425
self._result.target = fnv1.Target.TARGET_UNSPECIFIED
427426
else:
428427
self._result.target = fnv1.Target.TARGET_COMPOSITE
@@ -461,7 +460,7 @@ def __getitem__(self, type):
461460
return Condition(self, type)
462461

463462

464-
class Condition(function.protobuf.ProtobufValue):
463+
class Condition(protobuf.ProtobufValue):
465464
def __init__(self, conditions, type):
466465
self._conditions = conditions
467466
self.type = type
@@ -509,7 +508,7 @@ def status(self, status):
509508
condition.status = fnv1.Status.STATUS_CONDITION_TRUE
510509
elif status == None:
511510
condition.status = fnv1.Status.STATUS_CONDITION_UNKNOWN
512-
elif isinstance(status, function.protobuf.Values) and status._isUnknown:
511+
elif isinstance(status, protobuf.Values) and status._isUnknown:
513512
condition.status = fnv1.Status.STATUS_CONDITION_UNSPECIFIED
514513
else:
515514
condition.status = fnv1.Status.STATUS_CONDITION_FALSE
@@ -556,7 +555,7 @@ def claim(self, claim):
556555
condition = self._find_condition(True)
557556
if claim:
558557
condition.target = fnv1.Target.TARGET_COMPOSITE_AND_CLAIM
559-
elif claim == None or (isinstance(claim, function.protobuf.Values) and claim._isUnknown):
558+
elif claim == None or (isinstance(claim, protobuf.Values) and claim._isUnknown):
560559
condition.target = fnv1.Target.TARGET_UNSPECIFIED
561560
else:
562561
condition.target = fnv1.Target.TARGET_COMPOSITE
Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,25 @@
22

33
import asyncio
44
import base64
5+
import builtins
6+
import importlib
57
import inspect
68

79
import grpc
810
import crossplane.function.logging
911
import crossplane.function.response
1012
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
1113
from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1
12-
import function.composite
13-
import function.protobuf
14+
from .. import pythonic
15+
16+
builtins.BaseComposite = pythonic.BaseComposite
17+
builtins.Map = pythonic.Map
18+
builtins.List = pythonic.List
19+
builtins.Unknown = pythonic.Unknown
20+
builtins.Yaml = pythonic.Yaml
21+
builtins.Json = pythonic.Json
22+
builtins.B64Encode = pythonic.B64Encode
23+
builtins.B64Decode = pythonic.B64Decode
1424

1525

1626
class FunctionRunner(grpcv1.FunctionRunnerService):
@@ -19,7 +29,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
1929
def __init__(self):
2030
"""Create a new FunctionRunner."""
2131
self.logger = crossplane.function.logging.get_logger()
22-
self.modules = {}
32+
self.clazzes = {}
2333

2434
async def RunFunction(
2535
self, request: fnv1.RunFunctionRequest, _: grpc.aio.ServicerContext
@@ -52,23 +62,47 @@ async def RunFunction(
5262
return response
5363
composite = input['composite']
5464

55-
module = self.modules.get(composite)
56-
if not module:
57-
module = Module()
58-
try:
59-
exec(composite, module.__dict__)
60-
except Exception as e:
61-
crossplane.function.response.fatal(response, f"Exec exception: {e}")
62-
logger.exception('Exec exception')
65+
clazz = self.clazzes.get(composite)
66+
if not clazz:
67+
if '\n' in composite:
68+
module = Module()
69+
try:
70+
exec(composite, module.__dict__)
71+
except Exception as e:
72+
crossplane.function.response.fatal(response, f"Exec exception: {e}")
73+
logger.exception('Exec exception')
74+
return response
75+
composite = ['<script>', 'Composite']
76+
else:
77+
composite = composite.rsplit('.', 1)
78+
if len(composite) == 1:
79+
crossplane.function.response.fatal(response, f"Composite class name does not include module: {composite[0]}")
80+
logger.error(f"Composite class name does not include module: {composite[0]}")
81+
return response
82+
try:
83+
module = importlib.import_module(composite[0])
84+
except Exception as e:
85+
crossplane.function.response.fatal(response, f"Import module exception: {e}")
86+
logger.exception('Import module exception')
87+
return response
88+
clazz = getattr(module, composite[1], None)
89+
if not clazz:
90+
crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
91+
logger.error(f"{composite[0]} did not define: {composite[1]}")
92+
return response
93+
composite = '.'.join(composite)
94+
if not inspect.isclass(clazz):
95+
crossplane.function.response.fatal(response, f"{composite} is not a class")
96+
logger.error(f"{composite} is not a class")
6397
return response
64-
if not hasattr(module, 'Composite') or not inspect.isclass(module.Composite):
65-
crossplane.function.response.fatal(response, 'Function did not define "class Composite')
66-
logger.error('Composite did not define "class Composite"')
98+
if not issubclass(clazz, BaseComposite):
99+
crossplane.function.response.fatal(response, f"{composite} is not a subclass of BaseComposite")
100+
logger.error(f"{composite} is not a subclass of BaseComposite")
67101
return response
68-
self.modules[composite] = module
102+
self.clazzes[composite] = clazz
69103

70104
try:
71-
composite = module.Composite(request, response, logger)
105+
composite = clazz(request, response, logger)
72106
except Exception as e:
73107
crossplane.function.response.fatal(response, f"Instatiate exception: {e}")
74108
logger.exception('Instatiate exception')
@@ -101,12 +135,4 @@ async def RunFunction(
101135

102136

103137
class Module:
104-
def __init__(self):
105-
self.BaseComposite = function.composite.BaseComposite
106-
self.Map = function.protobuf.Map
107-
self.List = function.protobuf.List
108-
self.Unknown = function.protobuf.Unknown
109-
self.Yaml = function.protobuf.Yaml
110-
self.Json = function.protobuf.Json
111-
self.B64Encode = lambda s: base64.b64encode(s.encode('utf-8')).decode('utf-8')
112-
self.B64Decode = lambda s: base64.b64decode(s.encode('utf-8')).decode('utf-8')
138+
pass

0 commit comments

Comments
 (0)