Skip to content

Commit 803c209

Browse files
committed
Replace all "fortra.com" references with "crossplane.io", add "function-pythonic render"
1 parent 9b55869 commit 803c209

Some content is hidden

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

48 files changed

+803
-249
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: 11 additions & 11 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):
@@ -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
@@ -346,7 +346,7 @@ $ pip install crossplane-function-pythonic
346346
Next, create the following files:
347347
#### xr.yaml
348348
```yaml
349-
apiVersion: pythonic.fortra.com/v1alpha1
349+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
350350
kind: Hello
351351
metadata:
352352
name: world
@@ -358,18 +358,18 @@ spec:
358358
apiVersion: apiextensions.crossplane.io/v1
359359
kind: Composition
360360
metadata:
361-
name: hellos.pythonic.fortra.com
361+
name: hellos.pythonic.crossplane.io
362362
spec:
363363
compositeTypeRef:
364-
apiVersion: pythonic.fortra.com/v1alpha1
364+
apiVersion: pythonic.crossplane.io/v1alpha1
365365
kind: Hello
366366
mode: Pipeline
367367
pipeline:
368368
- step: pythonic
369369
functionRef:
370370
name: function-pythonic
371371
input:
372-
apiVersion: pythonic.fn.fortra.com/v1alpha1
372+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
373373
kind: Composite
374374
composite: |
375375
class GreetingComposite(BaseComposite):
@@ -389,14 +389,14 @@ spec:
389389
```
390390
In one terminal session, run function-pythonic:
391391
```shell
392-
$ function-pythonic --insecure --debug --render-unknowns
392+
$ function-pythonic grpc --insecure --debug --render-unknowns
393393
[2025-08-21 15:32:37.966] grpc._cython.cygrpc [DEBUG ] Using AsyncIOEngine.POLLER as I/O engine
394394
```
395395
In another terminal session, render the Composite:
396396
```shell
397397
$ crossplane render xr.yaml composition.yaml functions.yaml
398398
---
399-
apiVersion: pythonic.fortra.com/v1alpha1
399+
apiVersion: pythonic.crossplane.io/v1alpha1
400400
kind: Hello
401401
metadata:
402402
name: world
@@ -441,7 +441,7 @@ Then, in your Composition:
441441
functionRef:
442442
name: function-pythonic
443443
input:
444-
apiVersion: pythonic.fn.fortra.com/v1alpha1
444+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
445445
kind: Composite
446446
composite: |
447447
from example.pythonic import features
@@ -473,7 +473,7 @@ data:
473473
functionRef:
474474
name: function-pythonic
475475
input:
476-
apiVersion: pythonic.fn.fortra.com/v1alpha1
476+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
477477
kind: Composite
478478
composite: example.pythonic.features.FeatureOneComposite
479479
...
@@ -580,7 +580,7 @@ data:
580580
functionRef:
581581
name: function-pythonic
582582
input:
583-
apiVersion: pythonic.fn.fortra.com/v1alpha1
583+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
584584
kind: Composite
585585
parameters:
586586
who: World

crossplane/pythonic/command.py

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

crossplane/pythonic/function.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async def run_function(self, request):
4848
name.append(composite['metadata']['name'])
4949
logger = logging.getLogger('.'.join(name))
5050

51-
if composite['apiVersion'] == 'pythonic.fortra.com/v1alpha1' and composite['kind'] == 'Composite':
51+
if composite['apiVersion'] in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and composite['kind'] == 'Composite':
5252
if 'spec' not in composite or 'composite' not in composite['spec']:
5353
return self.fatal(request, logger, 'Missing spec "composite"')
5454
single_use = True

crossplane/pythonic/grpc.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
2+
import asyncio
3+
import os
4+
import pathlib
5+
import shlex
6+
import signal
7+
import sys
8+
9+
import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1
10+
import grpc
11+
12+
from . import (
13+
command,
14+
function,
15+
)
16+
17+
18+
class Command(command.Command):
19+
name = 'grpc'
20+
help = 'Run function-pythonic gRPC server'
21+
22+
@classmethod
23+
def add_parser_arguments(cls, parser):
24+
parser.add_argument(
25+
'--address',
26+
default='0.0.0.0:9443',
27+
help='Address to listen on for gRPC connections, default: 0.0.0.0:9443',
28+
)
29+
parser.add_argument(
30+
'--tls-certs-dir',
31+
default=os.getenv('TLS_SERVER_CERTS_DIR'),
32+
metavar='DIRECTORY',
33+
help='Serve using TLS certificates.',
34+
)
35+
parser.add_argument(
36+
'--insecure',
37+
action='store_true',
38+
help='Run without mTLS credentials, --tls-certs-dir will be ignored.',
39+
)
40+
parser.add_argument(
41+
'--packages',
42+
action='store_true',
43+
help='Discover python packages from function-pythonic ConfigMaps.'
44+
)
45+
parser.add_argument(
46+
'--packages-secrets',
47+
action='store_true',
48+
help='Also Discover python packages from function-pythonic Secrets.'
49+
)
50+
parser.add_argument(
51+
'--packages-namespace',
52+
action='append',
53+
default=[],
54+
metavar='NAMESPACE',
55+
help='Namespaces to discover function-pythonic ConfigMaps in, default is cluster wide.',
56+
)
57+
parser.add_argument(
58+
'--packages-dir',
59+
default='./pythonic-packages',
60+
metavar='DIRECTORY',
61+
help='Directory to store discovered function-pythonic ConfigMaps to, defaults "<cwd>/pythonic-packages"'
62+
)
63+
parser.add_argument(
64+
'--pip-install',
65+
metavar='INSTALL',
66+
help='Pip install command to install additional Python packages.'
67+
)
68+
69+
async def run(self):
70+
if not self.args.tls_certs_dir and not self.args.insecure:
71+
print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr)
72+
sys.exit(1)
73+
74+
if self.args.pip_install:
75+
import pip._internal.cli.main
76+
pip._internal.cli.main.main(['install', '--user', *shlex.split(self.args.pip_install)])
77+
78+
# enables read only volumes or mismatched uid volumes
79+
sys.dont_write_bytecode = True
80+
81+
grpc.aio.init_grpc_aio()
82+
grpc_runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns)
83+
grpc_server = grpc.aio.server()
84+
grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
85+
if self.args.insecure:
86+
grpc_server.add_insecure_port(self.args.address)
87+
else:
88+
certs = pathlib.Path(self.args.tls_certs_dir).expanduser().resolve()
89+
grpc_server.add_secure_port(
90+
self.args.address,
91+
grpc.ssl_server_credentials(
92+
private_key_certificate_chain_pairs=[(
93+
(certs / 'tls.key').read_bytes(),
94+
(certs / 'tls.crt').read_bytes(),
95+
)],
96+
root_certificates=(certs / 'ca.crt').read_bytes(),
97+
require_client_auth=True,
98+
),
99+
)
100+
await grpc_server.start()
101+
102+
if self.args.packages:
103+
from . import packages
104+
async with asyncio.TaskGroup() as tasks:
105+
tasks.create_task(grpc_server.wait_for_termination())
106+
tasks.create_task(packages.operator(
107+
grpc_server,
108+
grpc_runner,
109+
self.args.packages_secrets,
110+
self.args.packages_namespace,
111+
self.args.packages_dir,
112+
))
113+
else:
114+
def stop():
115+
asyncio.ensure_future(grpc_server.stop(5))
116+
loop = asyncio.get_event_loop()
117+
loop.add_signal_handler(signal.SIGINT, stop)
118+
loop.add_signal_handler(signal.SIGTERM, stop)
119+
await grpc_server.wait_for_termination()

0 commit comments

Comments
 (0)