Skip to content

Commit cdfc5d3

Browse files
author
Patrick J. McNerthney
committed
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 377704a commit cdfc5d3

18 files changed

Lines changed: 374 additions & 107 deletions

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: 104 additions & 4 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
@@ -43,6 +45,17 @@ function-go-templating examples implemented using function-pythonic.
4345
The [eks-cluster](./examples/eks-cluster/composition.yaml) example is a good
4446
complex example creating the entire vpc structure needed for an EKS cluster.
4547
48+
## Installing function-pythonic
49+
50+
```yaml
51+
apiVersion: pkg.crossplane.io/v1
52+
kind: Function
53+
metadata:
54+
name: function-pythonic
55+
spec:
56+
package: ghcr.io/fortra/function-pythonic:v0.0.3
57+
```
58+
4659
## Managed Resource Dependencies
4760
4861
function-pythonic automatically handles dependencies between managed resources.
@@ -117,8 +130,8 @@ The following functions are provided to create Protobuf structures:
117130
| Unknown | Create a new Protobuf unknown placeholder |
118131
| Yaml | Create a new Protobuf structure from a yaml string |
119132
| Json | Create a new Protobuf structure from a json string |
120-
| Base64Encode | Encode a string into base 64 |
121-
| Base64Decode | Decode a string from base 64 |
133+
| B64Encode | Encode a string into base 64 |
134+
| B64Decode | Decode a string from base 64 |
122135

123136
The following items are supported in all the Protobuf Message wrapper classes: `bool`,
124137
`len`, `contains`, `iter`, `hash`, `==`, `str`, `format`
@@ -259,10 +272,73 @@ spec:
259272
self.status.composite = 'Hello, World!'
260273
```
261274
262-
## Installing Python Packages
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. For example:
281+
```yaml
282+
apiVersion: v1
283+
kind: ConfigMap
284+
metadata:
285+
namespace: crossplane-system
286+
name: pythonic-composites
287+
data:
288+
bucket.py: |
289+
class BucketComposite(BaseComposite):
290+
def compose(self):
291+
self.resources.bucket.apiVersion = 's3.aws.upbound.io/v1beta2'
292+
self.resources.bucket.kind = 'Bucket'
293+
self.resources.bucket.spec.forProvider.region = 'us-east-1'
294+
```
295+
```yaml
296+
apiVersion: pkg.crossplane.io/v1beta1
297+
kind: DeploymentRuntimeConfig
298+
metadata:
299+
name: function-pythonic
300+
spec:
301+
deploymentTemplate:
302+
spec:
303+
template:
304+
spec:
305+
containers:
306+
- name: package-runtime
307+
args:
308+
- --debug
309+
- --python-path
310+
- /mnt/composites
311+
volumeMounts:
312+
- name: composites
313+
mountPath: /mnt/composites
314+
volumes:
315+
- name: composites
316+
configMap:
317+
name: pythonic-composites
318+
```
319+
```yaml
320+
apiVersion: apiextensions.crossplane.io/v1
321+
kind: Composition
322+
metadata:
323+
name: buckets.example.pythonic.io/v1
324+
spec:
325+
compositeTypeRef:
326+
apiVersion: example.pythonic.io/v1
327+
kind: Bucket
328+
mode: Pipeline
329+
pipeline:
330+
- step: create-bucket
331+
functionRef:
332+
name: function-pythonic
333+
input:
334+
apiVersion: pythonic.fn.fortra.com/v1alpha1
335+
kind: Composite
336+
composite: bucket.BucketComposite
337+
```
338+
## Install Additional Python Packages
263339
264340
function-pythonic supports a `--pip-install` command line option which will run pip install
265-
with the configured pip install command. For example, the following DeploymentRuntimeConfig:
341+
with the configured pip install command. For example:
266342
```yaml
267343
apiVersion: pkg.crossplane.io/v1beta1
268344
kind: DeploymentRuntimeConfig
@@ -280,3 +356,27 @@ spec:
280356
- --pip-install
281357
- --quiet aiobotocore==2.23.2
282358
```
359+
360+
## Enable Oversize Protos
361+
362+
The Protobuf python package used by function-pythonic limits the depth of yaml
363+
elements and the total size of yaml parsed. This results in a limit of approximately
364+
30 levels of nested yaml fields. This check can be disabled using the `--allow-oversize-protos`
365+
command line option. For example:
366+
367+
```yaml
368+
apiVersion: pkg.crossplane.io/v1beta1
369+
kind: DeploymentRuntimeConfig
370+
metadata:
371+
name: function-pythonic
372+
spec:
373+
deploymentTemplate:
374+
spec:
375+
template:
376+
spec:
377+
containers:
378+
- name: package-runtime
379+
args:
380+
- --debug
381+
- --allow-oversize-protos
382+
```

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 & 9 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
@@ -216,7 +216,7 @@ def ready(self):
216216
def ready(self, ready):
217217
if ready:
218218
ready = fnv1.Ready.READY_TRUE
219-
elif ready == None or (isinstance(ready, function.protobuf.Values) and ready._isUnknown):
219+
elif ready == None or (isinstance(ready, protobuf.Values) and ready._isUnknown):
220220
ready = fnv1.Ready.READY_UNSPECIFIED
221221
else:
222222
ready = fnv1.Ready.READY_FALSE
@@ -422,7 +422,7 @@ def claim(self, claim):
422422
if bool(self):
423423
if claim:
424424
self._result.target = fnv1.Target.TARGET_COMPOSITE_AND_CLAIM
425-
elif claim == None or (isinstance(claim, function.protobuf.Values) and claim._isUnknown):
425+
elif claim == None or (isinstance(claim, protobuf.Values) and claim._isUnknown):
426426
self._result.target = fnv1.Target.TARGET_UNSPECIFIED
427427
else:
428428
self._result.target = fnv1.Target.TARGET_COMPOSITE
@@ -461,7 +461,7 @@ def __getitem__(self, type):
461461
return Condition(self, type)
462462

463463

464-
class Condition(function.protobuf.ProtobufValue):
464+
class Condition(protobuf.ProtobufValue):
465465
def __init__(self, conditions, type):
466466
self._conditions = conditions
467467
self.type = type
@@ -509,7 +509,7 @@ def status(self, status):
509509
condition.status = fnv1.Status.STATUS_CONDITION_TRUE
510510
elif status == None:
511511
condition.status = fnv1.Status.STATUS_CONDITION_UNKNOWN
512-
elif isinstance(status, function.protobuf.Values) and status._isUnknown:
512+
elif isinstance(status, protobuf.Values) and status._isUnknown:
513513
condition.status = fnv1.Status.STATUS_CONDITION_UNSPECIFIED
514514
else:
515515
condition.status = fnv1.Status.STATUS_CONDITION_FALSE
@@ -556,7 +556,7 @@ def claim(self, claim):
556556
condition = self._find_condition(True)
557557
if claim:
558558
condition.target = fnv1.Target.TARGET_COMPOSITE_AND_CLAIM
559-
elif claim == None or (isinstance(claim, function.protobuf.Values) and claim._isUnknown):
559+
elif claim == None or (isinstance(claim, protobuf.Values) and claim._isUnknown):
560560
condition.target = fnv1.Target.TARGET_UNSPECIFIED
561561
else:
562562
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)