Skip to content

Commit 7270768

Browse files
authored
Implement "function-pythonic render ..." (#14)
Signed-off-by: Patrick J. McNerthney <[email protected]>
1 parent c9c2a72 commit 7270768

File tree

93 files changed

+1093
-411
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+1093
-411
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ RUN \
1515
USER pythonic:pythonic
1616
WORKDIR /opt/pythonic
1717
EXPOSE 9443
18-
ENTRYPOINT ["function-pythonic"]
18+
ENTRYPOINT ["function-pythonic", "grpc"]

README.md

Lines changed: 81 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ spec:
2525
functionRef:
2626
name: function-pythonic
2727
input:
28-
apiVersion: pythonic.fn.fortra.com/v1alpha1
28+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
2929
kind: Composite
3030
composite: |
3131
class VpcComposite(BaseComposite):
@@ -57,7 +57,7 @@ kind: Function
5757
metadata:
5858
name: function-pythonic
5959
spec:
60-
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5
60+
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0
6161
```
6262
## Composed Resource Dependencies
6363
@@ -203,7 +203,7 @@ The BaseComposite class provides the following fields for manipulating the Compo
203203
| self.spec | Map | The composite observed spec |
204204
| self.status | Map | The composite desired and observed status, read from observed if not in desired |
205205
| self.conditions | Conditions | The composite desired and observed conditions, read from observed if not in desired |
206-
| self.events | Events | Returned events against the Composite and optionally on the Claim |
206+
| self.results | Results | Returned results applied to the Composite and optionally on the Claim |
207207
| self.connection | Connection | The composite desired and observed connection detials, read from observed if not in desired |
208208
| self.ready | Boolean | The composite desired ready state |
209209

@@ -302,19 +302,19 @@ The fields are read only for `Resource.conditions` and `RequiredResource.conditi
302302
| Condition.lastTransitionTime | Timestamp | Last transition time, read only |
303303
| Condition.claim | Boolean | Also apply the condition the claim |
304304

305-
### Events
305+
### Results
306306

307-
The `BaseComposite.events` field is a list of events to apply to the Composite and
307+
The `BaseComposite.results` field is a list of results to apply to the Composite and
308308
optionally to the Claim.
309309

310310
| Field | Type | Description |
311311
| ----- | ---- | ----------- |
312-
| Event.info | Boolean | Normal informational event |
313-
| Event.warning | Boolean | Warning level event |
314-
| Event.fatal | Boolean | Fatal events also terminate composing the Composite |
315-
| Event.reason | String | PascalCase, machine-readable reason for this event |
316-
| Event.message | String | Human-readable details about the event |
317-
| Event.claim | Boolean | Also apply the event to the claim |
312+
| Result.info | Boolean | Normal informational result |
313+
| Result.warning | Boolean | Warning level result |
314+
| Result.fatal | Boolean | Fatal results also terminate composing the Composite |
315+
| Result.reason | String | PascalCase, machine-readable reason for this result |
316+
| Result.message | String | Human-readable details about the result |
317+
| Result.claim | Boolean | Also apply the result to the claim |
318318

319319
## Single use Composites
320320

@@ -324,7 +324,7 @@ just to run that Composition once in a single use or initialize task?
324324
function-pythonic installs a `Composite` CompositeResourceDefinition that enables
325325
creating such tasks using a single Composite resource:
326326
```yaml
327-
apiVersion: pythonic.fortra.com/v1alpha1
327+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
328328
kind: Composite
329329
metadata:
330330
name: composite-example
@@ -337,16 +337,62 @@ spec:
337337

338338
## Quick Start Development
339339

340-
The following example demonstrates how to locally render function-python
341-
compositions. First, install the `crossplane-function-pythonic` python
342-
package into the python environment:
340+
function-pythonic includes a pure python implementation of the `crossplane render ...`
341+
command, which can be used to render Compositions that only use function-pythonic. This
342+
makes it very easy to test and debug using your IDE of choice. It is also blindingly
343+
fast compared to `crossplane render`. To use, install the `crossplane-function-pythonic`
344+
python package into the python environment.
343345
```shell
344346
$ pip install crossplane-function-pythonic
345347
```
346-
Next, create the following files:
348+
Then to render function-pythonic Compositions, use the `function-pythonic render ...`
349+
command.
350+
```shell
351+
$ function-pythonic render --help
352+
usage: Crossplane Function Pythonic render [-h] [--debug] [--log-name-width WIDTH] [--python-path DIRECTORY] [--render-unknowns]
353+
[--allow-oversize-protos] [--context-files KEY=PATH] [--context-values KEY=VALUE]
354+
[--observed-resources PATH] [--extra-resources PATH] [--required-resources PATH]
355+
[--function-credentials PATH] [--include-full-xr] [--include-function-results] [--include-context]
356+
PATH [PATH/CLASS]
357+
358+
positional arguments:
359+
PATH A YAML file containing the Composite resource to render.
360+
PATH/CLASS A YAML file containing the Composition resource or the complete path of a function=-pythonic BaseComposite subclass.
361+
362+
options:
363+
-h, --help show this help message and exit
364+
--debug, -d Emit debug logs.
365+
--log-name-width WIDTH
366+
Width of the logger name in the log output, default 40.
367+
--python-path DIRECTORY
368+
Filing system directories to add to the python path.
369+
--render-unknowns, -u
370+
Render resources with unknowns, useful during local development.
371+
--allow-oversize-protos
372+
Allow oversized protobuf messages
373+
--context-files KEY=PATH
374+
Context key-value pairs to pass to the Function pipeline. Values must be files containing YAML/JSON.
375+
--context-values KEY=VALUE
376+
Context key-value pairs to pass to the Function pipeline. Values must be YAML/JSON. Keys take precedence over --context-files.
377+
--observed-resources, -o PATH
378+
A YAML file or directory of YAML files specifying the observed state of composed resources.
379+
--extra-resources PATH
380+
A YAML file or directory of YAML files specifying required resources (deprecated, use --required-resources).
381+
--required-resources, -e PATH
382+
A YAML file or directory of YAML files specifying required resources to pass to the Function pipeline.
383+
--function-credentials PATH
384+
A YAML file or directory of YAML files specifying credentials to use for Functions to render the XR.
385+
--include-full-xr, -x
386+
Include a direct copy of the input XR's spedc and metadata fields in the rendered output.
387+
--include-function-results, -r
388+
Include informational and warning messages from Functions in the rendered output as resources of kind: Result..
389+
--include-context, -c
390+
Include the context in the rendered output as a resource of kind: Context.
391+
```
392+
The following example demonstrates how to locally render function-python compositions. First, create the following files:
347393
#### xr.yaml
348394
```yaml
349-
apiVersion: pythonic.fortra.com/v1alpha1
395+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
350396
kind: Hello
351397
metadata:
352398
name: world
@@ -358,61 +404,49 @@ spec:
358404
apiVersion: apiextensions.crossplane.io/v1
359405
kind: Composition
360406
metadata:
361-
name: hellos.pythonic.fortra.com
407+
name: hellos.pythonic.crossplane.io
362408
spec:
363409
compositeTypeRef:
364-
apiVersion: pythonic.fortra.com/v1alpha1
410+
apiVersion: pythonic.crossplane.io/v1alpha1
365411
kind: Hello
366412
mode: Pipeline
367413
pipeline:
368414
- step: pythonic
369415
functionRef:
370416
name: function-pythonic
371417
input:
372-
apiVersion: pythonic.fn.fortra.com/v1alpha1
418+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
373419
kind: Composite
374420
composite: |
375421
class GreetingComposite(BaseComposite):
376422
def compose(self):
377423
self.status.greeting = f"Hello, {self.spec.who}!"
378424
```
379-
#### functions.yaml
380-
```yaml
381-
apiVersion: pkg.crossplane.io/v1
382-
kind: Function
383-
metadata:
384-
name: function-pythonic
385-
annotations:
386-
render.crossplane.io/runtime: Development
387-
spec:
388-
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5
389-
```
390-
In one terminal session, run function-pythonic:
391-
```shell
392-
$ function-pythonic --insecure --debug --render-unknowns
393-
[2025-08-21 15:32:37.966] grpc._cython.cygrpc [DEBUG ] Using AsyncIOEngine.POLLER as I/O engine
394-
```
395-
In another terminal session, render the Composite:
425+
Then, to render the above composite and composition, run:
396426
```shell
397-
$ crossplane render xr.yaml composition.yaml functions.yaml
427+
$ function-pythonic render --debug --render-unknowns xr.yaml composition.yaml
428+
[2025-12-29 09:44:57.949] io.crossplane.fn.pythonic.Hello.world [DEBUG ] Starting compose, 1st step, 1st pass
429+
[2025-12-29 09:44:57.949] io.crossplane.fn.pythonic.Hello.world [INFO ] Completed compose
398430
---
399-
apiVersion: pythonic.fortra.com/v1alpha1
431+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
400432
kind: Hello
401433
metadata:
402434
name: world
403435
status:
404436
conditions:
405-
- lastTransitionTime: "2024-01-01T00:00:00Z"
437+
- lastTransitionTime: '2026-01-01T00:00:00Z'
406438
reason: Available
407-
status: "True"
439+
status: 'True'
408440
type: Ready
409-
- lastTransitionTime: "2024-01-01T00:00:00Z"
441+
- lastTransitionTime: '2026-01-01T00:00:00Z'
410442
message: All resources are composed
411443
reason: AllComposed
412-
status: "True"
444+
status: 'True'
413445
type: ResourcesComposed
414446
greeting: Hello, World!
415447
```
448+
Most of the examples contain a `render.sh` command which uses `function-pythonic render` to
449+
render the example.
416450

417451
## ConfigMap Packages
418452

@@ -441,7 +475,7 @@ Then, in your Composition:
441475
functionRef:
442476
name: function-pythonic
443477
input:
444-
apiVersion: pythonic.fn.fortra.com/v1alpha1
478+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
445479
kind: Composite
446480
composite: |
447481
from example.pythonic import features
@@ -473,7 +507,7 @@ data:
473507
functionRef:
474508
name: function-pythonic
475509
input:
476-
apiVersion: pythonic.fn.fortra.com/v1alpha1
510+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
477511
kind: Composite
478512
composite: example.pythonic.features.FeatureOneComposite
479513
...
@@ -487,7 +521,7 @@ kind: Function
487521
metadata:
488522
name: function-pythonic
489523
spec:
490-
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5
524+
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0
491525
runtimeConfigRef:
492526
name: function-pythonic
493527
---
@@ -580,7 +614,7 @@ data:
580614
functionRef:
581615
name: function-pythonic
582616
input:
583-
apiVersion: pythonic.fn.fortra.com/v1alpha1
617+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
584618
kind: Composite
585619
parameters:
586620
who: World

crossplane/pythonic/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import main
2+
main.main()

crossplane/pythonic/command.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
2+
import logging
3+
import pathlib
4+
import sys
5+
6+
7+
class Command:
8+
name = None
9+
command = None
10+
description = None
11+
12+
@classmethod
13+
def create(cls, subparsers):
14+
parser = subparsers.add_parser(cls.name, help=cls.help, description=cls.description)
15+
parser.set_defaults(command=cls)
16+
cls.add_parser_arguments(parser)
17+
18+
@classmethod
19+
def add_parser_arguments(cls, parser):
20+
pass
21+
22+
@classmethod
23+
def add_function_arguments(cls, parser):
24+
parser.add_argument(
25+
'--debug', '-d',
26+
action='store_true',
27+
help='Emit debug logs.',
28+
)
29+
parser.add_argument(
30+
'--log-name-width',
31+
type=int,
32+
default=40,
33+
metavar='WIDTH',
34+
help='Width of the logger name in the log output, default 40.',
35+
)
36+
parser.add_argument(
37+
'--python-path',
38+
action='append',
39+
default=[],
40+
metavar='DIRECTORY',
41+
help='Filing system directories to add to the python path.',
42+
)
43+
parser.add_argument(
44+
'--render-unknowns', '-u',
45+
action='store_true',
46+
help='Render resources with unknowns, useful during local development.'
47+
)
48+
parser.add_argument(
49+
'--allow-oversize-protos',
50+
action='store_true',
51+
help='Allow oversized protobuf messages',
52+
)
53+
54+
def __init__(self, args):
55+
self.args = args
56+
self.initialize()
57+
58+
def initialize(self):
59+
pass
60+
61+
def initialize_function(self):
62+
formatter = Formatter(self.args.log_name_width)
63+
handler = logging.StreamHandler(sys.stdout)
64+
handler.setFormatter(formatter)
65+
logger = logging.getLogger()
66+
logger.handlers = [handler]
67+
logger.setLevel(logging.DEBUG if self.args.debug else logging.INFO)
68+
69+
for path in reversed(self.args.python_path):
70+
sys.path.insert(0, str(pathlib.Path(path).expanduser().resolve()))
71+
72+
if self.args.allow_oversize_protos:
73+
from google.protobuf.internal import api_implementation
74+
if api_implementation._c_module:
75+
api_implementation._c_module.SetAllowOversizeProtos(True)
76+
77+
async def run(self):
78+
raise NotImplementedError()
79+
80+
81+
class Formatter(logging.Formatter):
82+
def __init__(self, name_width):
83+
super(Formatter, self).__init__(
84+
f"[{{asctime}}.{{msecs:03.0f}}] {{sname:{name_width}.{name_width}}} [{{levelname:8.8}}] {{message}}",
85+
'%Y-%m-%d %H:%M:%S',
86+
'{',
87+
)
88+
self.name_width = name_width
89+
90+
def format(self, record):
91+
record.sname = record.name
92+
extra = len(record.sname) - self.name_width
93+
if extra > 0:
94+
names = record.sname.split('.')
95+
for ix, name in enumerate(names):
96+
if len(name) > extra:
97+
names[ix] = name[extra:]
98+
break
99+
names[ix] = name[:1]
100+
extra -= len(name) - 1
101+
record.sname = '.'.join(names)
102+
return super(Formatter, self).format(record)

0 commit comments

Comments
 (0)