diff --git a/Dockerfile b/Dockerfile index 54b15ae..644ec2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,4 @@ RUN \ USER pythonic:pythonic WORKDIR /opt/pythonic EXPOSE 9443 -ENTRYPOINT ["function-pythonic"] +ENTRYPOINT ["function-pythonic", "grpc"] diff --git a/README.md b/README.md index 27f1963..27b014e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class VpcComposite(BaseComposite): @@ -57,7 +57,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 ``` ## Composed Resource Dependencies @@ -203,7 +203,7 @@ The BaseComposite class provides the following fields for manipulating the Compo | self.spec | Map | The composite observed spec | | self.status | Map | The composite desired and observed status, read from observed if not in desired | | self.conditions | Conditions | The composite desired and observed conditions, read from observed if not in desired | -| self.events | Events | Returned events against the Composite and optionally on the Claim | +| self.results | Results | Returned results applied to the Composite and optionally on the Claim | | self.connection | Connection | The composite desired and observed connection detials, read from observed if not in desired | | self.ready | Boolean | The composite desired ready state | @@ -302,19 +302,19 @@ The fields are read only for `Resource.conditions` and `RequiredResource.conditi | Condition.lastTransitionTime | Timestamp | Last transition time, read only | | Condition.claim | Boolean | Also apply the condition the claim | -### Events +### Results -The `BaseComposite.events` field is a list of events to apply to the Composite and +The `BaseComposite.results` field is a list of results to apply to the Composite and optionally to the Claim. | Field | Type | Description | | ----- | ---- | ----------- | -| Event.info | Boolean | Normal informational event | -| Event.warning | Boolean | Warning level event | -| Event.fatal | Boolean | Fatal events also terminate composing the Composite | -| Event.reason | String | PascalCase, machine-readable reason for this event | -| Event.message | String | Human-readable details about the event | -| Event.claim | Boolean | Also apply the event to the claim | +| Result.info | Boolean | Normal informational result | +| Result.warning | Boolean | Warning level result | +| Result.fatal | Boolean | Fatal results also terminate composing the Composite | +| Result.reason | String | PascalCase, machine-readable reason for this result | +| Result.message | String | Human-readable details about the result | +| Result.claim | Boolean | Also apply the result to the claim | ## Single use Composites @@ -324,7 +324,7 @@ just to run that Composition once in a single use or initialize task? function-pythonic installs a `Composite` CompositeResourceDefinition that enables creating such tasks using a single Composite resource: ```yaml -apiVersion: pythonic.fortra.com/v1alpha1 +apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite metadata: name: composite-example @@ -337,16 +337,62 @@ spec: ## Quick Start Development -The following example demonstrates how to locally render function-python -compositions. First, install the `crossplane-function-pythonic` python -package into the python environment: +function-pythonic includes a pure python implementation of the `crossplane render ...` +command, which can be used to render Compositions that only use function-pythonic. This +makes it very easy to test and debug using your IDE of choice. It is also blindingly +fast compared to `crossplane render`. To use, install the `crossplane-function-pythonic` +python package into the python environment. ```shell $ pip install crossplane-function-pythonic ``` -Next, create the following files: +Then to render function-pythonic Compositions, use the `function-pythonic render ...` +command. +```shell +$ function-pythonic render --help +usage: Crossplane Function Pythonic render [-h] [--debug] [--log-name-width WIDTH] [--python-path DIRECTORY] [--render-unknowns] + [--allow-oversize-protos] [--context-files KEY=PATH] [--context-values KEY=VALUE] + [--observed-resources PATH] [--extra-resources PATH] [--required-resources PATH] + [--function-credentials PATH] [--include-full-xr] [--include-function-results] [--include-context] + PATH [PATH/CLASS] + +positional arguments: + PATH A YAML file containing the Composite resource to render. + PATH/CLASS A YAML file containing the Composition resource or the complete path of a function=-pythonic BaseComposite subclass. + +options: + -h, --help show this help message and exit + --debug, -d Emit debug logs. + --log-name-width WIDTH + Width of the logger name in the log output, default 40. + --python-path DIRECTORY + Filing system directories to add to the python path. + --render-unknowns, -u + Render resources with unknowns, useful during local development. + --allow-oversize-protos + Allow oversized protobuf messages + --context-files KEY=PATH + Context key-value pairs to pass to the Function pipeline. Values must be files containing YAML/JSON. + --context-values KEY=VALUE + Context key-value pairs to pass to the Function pipeline. Values must be YAML/JSON. Keys take precedence over --context-files. + --observed-resources, -o PATH + A YAML file or directory of YAML files specifying the observed state of composed resources. + --extra-resources PATH + A YAML file or directory of YAML files specifying required resources (deprecated, use --required-resources). + --required-resources, -e PATH + A YAML file or directory of YAML files specifying required resources to pass to the Function pipeline. + --function-credentials PATH + A YAML file or directory of YAML files specifying credentials to use for Functions to render the XR. + --include-full-xr, -x + Include a direct copy of the input XR's spedc and metadata fields in the rendered output. + --include-function-results, -r + Include informational and warning messages from Functions in the rendered output as resources of kind: Result.. + --include-context, -c + Include the context in the rendered output as a resource of kind: Context. +``` +The following example demonstrates how to locally render function-python compositions. First, create the following files: #### xr.yaml ```yaml -apiVersion: pythonic.fortra.com/v1alpha1 +apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Hello metadata: name: world @@ -358,10 +404,10 @@ spec: apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: - name: hellos.pythonic.fortra.com + name: hellos.pythonic.crossplane.io spec: compositeTypeRef: - apiVersion: pythonic.fortra.com/v1alpha1 + apiVersion: pythonic.crossplane.io/v1alpha1 kind: Hello mode: Pipeline pipeline: @@ -369,50 +415,38 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class GreetingComposite(BaseComposite): def compose(self): self.status.greeting = f"Hello, {self.spec.who}!" ``` -#### functions.yaml -```yaml -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - render.crossplane.io/runtime: Development -spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 -``` -In one terminal session, run function-pythonic: -```shell -$ function-pythonic --insecure --debug --render-unknowns -[2025-08-21 15:32:37.966] grpc._cython.cygrpc [DEBUG ] Using AsyncIOEngine.POLLER as I/O engine -``` -In another terminal session, render the Composite: +Then, to render the above composite and composition, run: ```shell -$ crossplane render xr.yaml composition.yaml functions.yaml +$ function-pythonic render --debug --render-unknowns xr.yaml composition.yaml +[2025-12-29 09:44:57.949] io.crossplane.fn.pythonic.Hello.world [DEBUG ] Starting compose, 1st step, 1st pass +[2025-12-29 09:44:57.949] io.crossplane.fn.pythonic.Hello.world [INFO ] Completed compose --- -apiVersion: pythonic.fortra.com/v1alpha1 +apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Hello metadata: name: world status: conditions: - - lastTransitionTime: "2024-01-01T00:00:00Z" + - lastTransitionTime: '2026-01-01T00:00:00Z' reason: Available - status: "True" + status: 'True' type: Ready - - lastTransitionTime: "2024-01-01T00:00:00Z" + - lastTransitionTime: '2026-01-01T00:00:00Z' message: All resources are composed reason: AllComposed - status: "True" + status: 'True' type: ResourcesComposed greeting: Hello, World! ``` +Most of the examples contain a `render.sh` command which uses `function-pythonic render` to +render the example. ## ConfigMap Packages @@ -441,7 +475,7 @@ Then, in your Composition: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | from example.pythonic import features @@ -473,7 +507,7 @@ data: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: example.pythonic.features.FeatureOneComposite ... @@ -487,7 +521,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 runtimeConfigRef: name: function-pythonic --- @@ -580,7 +614,7 @@ data: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite parameters: who: World diff --git a/crossplane/pythonic/__version__.py b/crossplane/pythonic/__about__.py similarity index 100% rename from crossplane/pythonic/__version__.py rename to crossplane/pythonic/__about__.py diff --git a/crossplane/pythonic/__main__.py b/crossplane/pythonic/__main__.py new file mode 100644 index 0000000..8433982 --- /dev/null +++ b/crossplane/pythonic/__main__.py @@ -0,0 +1,2 @@ +from . import main +main.main() diff --git a/crossplane/pythonic/command.py b/crossplane/pythonic/command.py new file mode 100644 index 0000000..287f8bc --- /dev/null +++ b/crossplane/pythonic/command.py @@ -0,0 +1,102 @@ + +import logging +import pathlib +import sys + + +class Command: + name = None + command = None + description = None + + @classmethod + def create(cls, subparsers): + parser = subparsers.add_parser(cls.name, help=cls.help, description=cls.description) + parser.set_defaults(command=cls) + cls.add_parser_arguments(parser) + + @classmethod + def add_parser_arguments(cls, parser): + pass + + @classmethod + def add_function_arguments(cls, parser): + parser.add_argument( + '--debug', '-d', + action='store_true', + help='Emit debug logs.', + ) + parser.add_argument( + '--log-name-width', + type=int, + default=40, + metavar='WIDTH', + help='Width of the logger name in the log output, default 40.', + ) + parser.add_argument( + '--python-path', + action='append', + default=[], + metavar='DIRECTORY', + help='Filing system directories to add to the python path.', + ) + parser.add_argument( + '--render-unknowns', '-u', + action='store_true', + help='Render resources with unknowns, useful during local development.' + ) + parser.add_argument( + '--allow-oversize-protos', + action='store_true', + help='Allow oversized protobuf messages', + ) + + def __init__(self, args): + self.args = args + self.initialize() + + def initialize(self): + pass + + def initialize_function(self): + formatter = Formatter(self.args.log_name_width) + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(formatter) + logger = logging.getLogger() + logger.handlers = [handler] + logger.setLevel(logging.DEBUG if self.args.debug else logging.INFO) + + for path in reversed(self.args.python_path): + sys.path.insert(0, str(pathlib.Path(path).expanduser().resolve())) + + if self.args.allow_oversize_protos: + from google.protobuf.internal import api_implementation + if api_implementation._c_module: + api_implementation._c_module.SetAllowOversizeProtos(True) + + async def run(self): + raise NotImplementedError() + + +class Formatter(logging.Formatter): + def __init__(self, name_width): + super(Formatter, self).__init__( + f"[{{asctime}}.{{msecs:03.0f}}] {{sname:{name_width}.{name_width}}} [{{levelname:8.8}}] {{message}}", + '%Y-%m-%d %H:%M:%S', + '{', + ) + self.name_width = name_width + + def format(self, record): + record.sname = record.name + extra = len(record.sname) - self.name_width + if extra > 0: + names = record.sname.split('.') + for ix, name in enumerate(names): + if len(name) > extra: + names[ix] = name[extra:] + break + names[ix] = name[:1] + extra -= len(name) - 1 + record.sname = '.'.join(names) + return super(Formatter, self).format(record) diff --git a/crossplane/pythonic/composite.py b/crossplane/pythonic/composite.py index a06c92b..6b7dcd5 100644 --- a/crossplane/pythonic/composite.py +++ b/crossplane/pythonic/composite.py @@ -47,7 +47,8 @@ def __init__(self, request, single_use, logger): self.spec = self.observed.spec self.status = self.desired.status self.conditions = Conditions(observed, self.response) - self.events = Events(self.response) + self.results = Results(self.response) + self.events = Results(self.response) # Deprecated, use self.results @property def ttl(self): @@ -593,42 +594,42 @@ def _find_condition(self, create=False): return self._conditions._response.conditions.append(condition) -class Events: +class Results: def __init__(self, response): self._results = response.results def info(self, reason=_notset, message=_notset, claim=_notset): - event = Event(self._results.append()) - event.info = True + result = Result(self._results.append()) + result.info = True if reason != _notset: - event.reason = reason + result.reason = reason if message != _notset: - event.message = message + result.message = message if claim != _notset: - event.claim = claim - return event + result.claim = claim + return result def warning(self, reason=_notset, message=_notset, claim=_notset): - event = Event(self._results.append()) - event.warning = True + result = Result(self._results.append()) + result.warning = True if reason != _notset: - event.reason = reason + result.reason = reason if message != _notset: - event.message = message + result.message = message if claim != _notset: - event.claim = claim - return event + result.claim = claim + return result def fatal(self, reason=_notset, message=_notset, claim=_notset): - event = Event(self._results.append()) - event.fatal = True + result = Result(self._results.append()) + result.fatal = True if reason != _notset: - event.reason = reason + result.reason = reason if message != _notset: - event.message = message + result.message = message if claim != _notset: - event.claim = claim - return event + result.claim = claim + return result def __bool__(self): return len(self) > 0 @@ -638,15 +639,15 @@ def __len__(self): def __getitem__(self, key): if key >= len(self._results): - return Event() - return Event(self._results[key]) + return Result() + return Result(self._results[key]) def __iter__(self): for ix in range(len(self._results)): yield self[ix] -class Event: +class Result: def __init__(self, result=None): self._result = result diff --git a/crossplane/pythonic/function.py b/crossplane/pythonic/function.py index e1448bf..11547dd 100644 --- a/crossplane/pythonic/function.py +++ b/crossplane/pythonic/function.py @@ -48,7 +48,7 @@ async def run_function(self, request): name.append(composite['metadata']['name']) logger = logging.getLogger('.'.join(name)) - if composite['apiVersion'] == 'pythonic.fortra.com/v1alpha1' and composite['kind'] == 'Composite': + if composite['apiVersion'] in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and composite['kind'] == 'Composite': if 'spec' not in composite or 'composite' not in composite['spec']: return self.fatal(request, logger, 'Missing spec "composite"') single_use = True @@ -276,30 +276,30 @@ def process_unknowns(self, composite): reason = 'FatalUnknowns' message = f"Observed resources with unknowns: {','.join(fatalResources)}" status = False - event = composite.events.fatal + result = composite.results.fatal elif warningResources: level = composite.logger.warning reason = 'ObservedUnknowns' message = f"Observed resources with unknowns: {','.join(warningResources)}" status = False - event = composite.events.warning + result = composite.results.warning elif unknownResources: level = composite.logger.info reason = 'DesiredUnknowns' message = f"Desired resources with unknowns: {','.join(unknownResources)}" status = False - event = composite.events.info + result = composite.results.info else: level = None reason = 'AllComposed' message = 'All resources are composed' status = True - event = None + result = None if not self.debug and level: level(message) composite.conditions.ResourcesComposed(reason, message, status) - if event: - event(reason, message) + if result: + result(reason, message) def process_auto_readies(self, composite): for name, resource in composite.resources: diff --git a/crossplane/pythonic/grpc.py b/crossplane/pythonic/grpc.py new file mode 100644 index 0000000..ca26a81 --- /dev/null +++ b/crossplane/pythonic/grpc.py @@ -0,0 +1,123 @@ + +import asyncio +import os +import pathlib +import shlex +import signal +import sys + +import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1 +import grpc + +from . import ( + command, + function, +) + + +class Command(command.Command): + name = 'grpc' + help = 'Run function-pythonic gRPC server' + + @classmethod + def add_parser_arguments(cls, parser): + cls.add_function_arguments(parser) + parser.add_argument( + '--address', + default='0.0.0.0:9443', + help='Address to listen on for gRPC connections, default: 0.0.0.0:9443', + ) + parser.add_argument( + '--tls-certs-dir', + default=os.getenv('TLS_SERVER_CERTS_DIR'), + metavar='DIRECTORY', + help='Serve using TLS certificates.', + ) + parser.add_argument( + '--insecure', + action='store_true', + help='Run without mTLS credentials, --tls-certs-dir will be ignored.', + ) + parser.add_argument( + '--packages', + action='store_true', + help='Discover python packages from function-pythonic ConfigMaps.' + ) + parser.add_argument( + '--packages-secrets', + action='store_true', + help='Also Discover python packages from function-pythonic Secrets.' + ) + parser.add_argument( + '--packages-namespace', + action='append', + default=[], + metavar='NAMESPACE', + help='Namespaces to discover function-pythonic ConfigMaps in, default is cluster wide.', + ) + parser.add_argument( + '--packages-dir', + default='./pythonic-packages', + metavar='DIRECTORY', + help='Directory to store discovered function-pythonic ConfigMaps to, defaults "/pythonic-packages"' + ) + parser.add_argument( + '--pip-install', + metavar='INSTALL', + help='Pip install command to install additional Python packages.' + ) + + def initialize(self): + if not self.args.tls_certs_dir and not self.args.insecure: + print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr) + sys.exit(1) + + if self.args.pip_install: + import pip._internal.cli.main + pip._internal.cli.main.main(['install', '--user', *shlex.split(self.args.pip_install)]) + + self.initialize_function() + + # enables read only volumes or mismatched uid volumes + sys.dont_write_bytecode = True + + async def run(self): + grpc.aio.init_grpc_aio() + grpc_runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns) + grpc_server = grpc.aio.server() + grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server) + if self.args.insecure: + grpc_server.add_insecure_port(self.args.address) + else: + certs = pathlib.Path(self.args.tls_certs_dir).expanduser().resolve() + grpc_server.add_secure_port( + self.args.address, + grpc.ssl_server_credentials( + private_key_certificate_chain_pairs=[( + (certs / 'tls.key').read_bytes(), + (certs / 'tls.crt').read_bytes(), + )], + root_certificates=(certs / 'ca.crt').read_bytes(), + require_client_auth=True, + ), + ) + await grpc_server.start() + + if self.args.packages: + from . import packages + async with asyncio.TaskGroup() as tasks: + tasks.create_task(grpc_server.wait_for_termination()) + tasks.create_task(packages.operator( + grpc_server, + grpc_runner, + self.args.packages_secrets, + self.args.packages_namespace, + self.args.packages_dir, + )) + else: + def stop(): + asyncio.ensure_future(grpc_server.stop(5)) + loop = asyncio.get_event_loop() + loop.add_signal_handler(signal.SIGINT, stop) + loop.add_signal_handler(signal.SIGTERM, stop) + await grpc_server.wait_for_termination() diff --git a/crossplane/pythonic/main.py b/crossplane/pythonic/main.py index 004c653..811f851 100644 --- a/crossplane/pythonic/main.py +++ b/crossplane/pythonic/main.py @@ -1,197 +1,27 @@ -"""The composition function's main CLI.""" +"""The function-pythonic's main CLI.""" import argparse import asyncio -import logging -import os -import pathlib -import shlex -import signal import sys -import traceback -import crossplane.function.logging -import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1 -import grpc - -from . import function +from . import ( + grpc, + render, + version, +) def main(): - asyncio.run(Main().main()) - - -class Main: - async def main(self): - parser = argparse.ArgumentParser('Crossplane Function Pythonic') - parser.add_argument( - '--debug', '-d', - action='store_true', - help='Emit debug logs.', - ) - parser.add_argument( - '--log-name-width', - type=int, - default=40, - metavar='WIDTH', - help='Width of the logger name in the log output, default 40', - ) - parser.add_argument( - '--address', - default='0.0.0.0:9443', - help='Address to listen on for gRPC connections, default: 0.0.0.0:9443', - ) - parser.add_argument( - '--tls-certs-dir', - default=os.getenv('TLS_SERVER_CERTS_DIR'), - metavar='DIRECTORY', - help='Serve using TLS certificates.', - ) - parser.add_argument( - '--insecure', - action='store_true', - help='Run without mTLS credentials, --tls-certs-dir will be ignored.', - ) - parser.add_argument( - '--packages', - action='store_true', - help='Discover python packages from function-pythonic ConfigMaps.' - ) - parser.add_argument( - '--packages-secrets', - action='store_true', - help='Also Discover python packages from function-pythonic Secrets.' - ) - parser.add_argument( - '--packages-namespace', - action='append', - default=[], - metavar='NAMESPACE', - help='Namespaces to discover function-pythonic ConfigMaps in, default is cluster wide.', - ) - parser.add_argument( - '--packages-dir', - default='./pythonic-packages', - metavar='DIRECTORY', - help='Directory to store discovered function-pythonic ConfigMaps to, defaults "/pythonic-packages"' - ) - parser.add_argument( - '--pip-install', - metavar='COMMAND', - help='Pip install command to install additional Python packages.' - ) - parser.add_argument( - '--python-path', - action='append', - default=[], - metavar='DIRECTORY', - help='Filing system directories to add to the python path', - ) - parser.add_argument( - '--allow-oversize-protos', - action='store_true', - help='Allow oversized protobuf messages' - ) - parser.add_argument( - '--render-unknowns', - action='store_true', - help='Render resources with unknowns, useful during local develomment' - ) - args = parser.parse_args() - if not args.tls_certs_dir and not args.insecure: - print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr) - sys.exit(1) - - if args.pip_install: - import pip._internal.cli.main - pip._internal.cli.main.main(['install', '--user', *shlex.split(args.pip_install)]) - - for path in reversed(args.python_path): - sys.path.insert(0, str(pathlib.Path(path).expanduser().resolve())) - - self.configure_logging(args) - # enables read only volumes or mismatched uid volumes - sys.dont_write_bytecode = True - await self.run(args) - - # Allow for independent running of function-pythonic - async def run(self, args): - if args.allow_oversize_protos: - from google.protobuf.internal import api_implementation - if api_implementation._c_module: - api_implementation._c_module.SetAllowOversizeProtos(True) - - grpc.aio.init_grpc_aio() - grpc_runner = function.FunctionRunner(args.debug, args.render_unknowns) - grpc_server = grpc.aio.server() - grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server) - if args.insecure: - grpc_server.add_insecure_port(args.address) - else: - certs = pathlib.Path(args.tls_certs_dir).expanduser().resolve() - grpc_server.add_secure_port( - args.address, - grpc.ssl_server_credentials( - private_key_certificate_chain_pairs=[( - (certs / 'tls.key').read_bytes(), - (certs / 'tls.crt').read_bytes(), - )], - root_certificates=(certs / 'ca.crt').read_bytes(), - require_client_auth=True, - ), - ) - await grpc_server.start() - - if args.packages: - from . import packages - async with asyncio.TaskGroup() as tasks: - tasks.create_task(grpc_server.wait_for_termination()) - tasks.create_task(packages.operator( - grpc_server, - grpc_runner, - args.packages_secrets, - args.packages_namespace, - args.packages_dir, - )) - else: - def stop(): - asyncio.ensure_future(grpc_server.stop(5)) - loop = asyncio.get_event_loop() - loop.add_signal_handler(signal.SIGINT, stop) - loop.add_signal_handler(signal.SIGTERM, stop) - await grpc_server.wait_for_termination() - - def configure_logging(self, args): - formatter = Formatter(args.log_name_width) - handler = logging.StreamHandler() - handler.setFormatter(formatter) - logger = logging.getLogger() - logger.handlers = [handler] - logger.setLevel(logging.DEBUG if args.debug else logging.INFO) - - -class Formatter(logging.Formatter): - def __init__(self, name_width): - super(Formatter, self).__init__( - f"[{{asctime}}.{{msecs:03.0f}}] {{sname:{name_width}.{name_width}}} [{{levelname:8.8}}] {{message}}", - '%Y-%m-%d %H:%M:%S', - '{', - ) - self.name_width = name_width - - def format(self, record): - record.sname = record.name - extra = len(record.sname) - self.name_width - if extra > 0: - names = record.sname.split('.') - for ix, name in enumerate(names): - if len(name) > extra: - names[ix] = name[extra:] - break - names[ix] = name[:1] - extra -= len(name) - 1 - record.sname = '.'.join(names) - return super(Formatter, self).format(record) + parser = argparse.ArgumentParser('Crossplane Function Pythonic') + subparsers = parser.add_subparsers(title='Command', metavar='') + grpc.Command.create(subparsers) + render.Command.create(subparsers) + version.Command.create(subparsers) + args = parser.parse_args() + if not hasattr(args, 'command'): + parser.print_help() + sys.exit(1) + asyncio.run(args.command(args).run()) if __name__ == '__main__': diff --git a/crossplane/pythonic/protobuf.py b/crossplane/pythonic/protobuf.py index 23da6c2..19c2292 100644 --- a/crossplane/pythonic/protobuf.py +++ b/crossplane/pythonic/protobuf.py @@ -55,7 +55,6 @@ def B64Decode(string): string = str(string) return base64.b64decode(string.encode('utf-8')).decode('utf-8') -B64Decode = lambda s: base64.b64decode(s.encode('utf-8')).decode('utf-8') class Message: def __init__(self, parent, key, descriptor, message=_Unknown, readOnly=False): @@ -450,6 +449,10 @@ def _create_child(self, key): raise ValueError(f"{self._readOnly} is read only") if self._messages is _Unknown: self.__dict__['_messages'] = self._parent._create_child(self._key) + if key == append: + key = len(self._messages) + elif key < 0: + key = len(self._messages) + key while key >= len(self._messages): self._messages.add() return self._messages[key] @@ -1050,6 +1053,10 @@ def _create_child(self, key): values = self._value.list_value.values else: values = self._value.values + if key == append: + key = len(values) + elif key < 0: + key = len(values) + key while key >= len(values): values.add() values[key].Clear() diff --git a/crossplane/pythonic/render.py b/crossplane/pythonic/render.py new file mode 100644 index 0000000..9669690 --- /dev/null +++ b/crossplane/pythonic/render.py @@ -0,0 +1,432 @@ + +import pathlib +import sys +import yaml +from crossplane.function.proto.v1 import run_function_pb2 as fnv1 + +from . import ( + command, + function, + protobuf, +) + + +class Command(command.Command): + name = 'render' + help = 'Render a function-pythonic Composition' + + @classmethod + def add_parser_arguments(cls, parser): + cls.add_function_arguments(parser) + parser.add_argument( + 'composite', + type=pathlib.Path, + metavar='PATH', + help='A YAML file containing the Composite resource to render.', + ) + parser.add_argument( + 'composition', + type=pathlib.Path, + nargs='?', + metavar='PATH/CLASS', + help='A YAML file containing the Composition resource or the complete path of a function=-pythonic BaseComposite subclass.', + ) + parser.add_argument( + '--context-files', + action='append', + default=[], + metavar='KEY=PATH', + help='Context key-value pairs to pass to the Function pipeline. Values must be files containing YAML/JSON.', + ) + parser.add_argument( + '--context-values', + action='append', + default=[], + metavar='KEY=VALUE', + help='Context key-value pairs to pass to the Function pipeline. Values must be YAML/JSON. Keys take precedence over --context-files.', + ) + parser.add_argument( + '--observed-resources', '-o', + action='append', + type=pathlib.Path, + default=[], + metavar='PATH', + help='A YAML file or directory of YAML files specifying the observed state of composed resources.' + ) + parser.add_argument( + '--extra-resources', + action='append', + type=pathlib.Path, + default=[], + metavar='PATH', + help='A YAML file or directory of YAML files specifying required resources (deprecated, use --required-resources).', + ) + parser.add_argument( + '--required-resources', '-e', + action='append', + type=pathlib.Path, + default=[], + metavar='PATH', + help='A YAML file or directory of YAML files specifying required resources to pass to the Function pipeline.', + ) + parser.add_argument( + '--function-credentials', + action='append', + type=pathlib.Path, + default=[], + metavar='PATH', + help='A YAML file or directory of YAML files specifying credentials to use for Functions to render the XR.', + ) + parser.add_argument( + '--include-full-xr', '-x', + action='store_true', + help="Include a direct copy of the input XR's spedc and metadata fields in the rendered output.", + ) + parser.add_argument( + '--include-function-results', '-r', + action='store_true', + help='Include informational and warning messages from Functions in the rendered output as resources of kind: Result..', + ) + parser.add_argument( + '--include-context', '-c', + action='store_true', + help='Include the context in the rendered output as a resource of kind: Context.', + ) + + def initialize(self): + self.initialize_function() + + async def run(self): + # Obtain the Composite to render. + if not self.args.composite.is_file(): + print(f"Composite \"{self.args.composite}\" is not a file", file=sys.stderr) + sys.exit(1) + composite = protobuf.Yaml(self.args.composite.read_text()) + + # Obtain the Composition that will be used to render the Composite. + if composite.apiVersion in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and composite.kind == 'Composite': + if self.args.composition: + print('Composite type of "composite.pythonic.crossplane.io" does not use "composition" argument', file=sys.stderr) + sys.exit(1) + composition = self.create_composition(composite, '') + else: + if not self.args.composition: + print('"composition" argument required', file=sys.stderr) + sys.exit(1) + if self.args.composition.is_file(): + composition = protobuf.Yaml(self.args.composition.read_text()) + else: + composite = self.args.composition.rsplit('.', 1) + if len(composite) == 1: + print(f"Composition class name does not include module: {self.args.composition}", file=sys.stderr) + sys.exit(1) + try: + module = importlib.import_module(composite[0]) + except Exception as e: + print(f"Unable to import composition class: {composite[0]}", file=sys.stderr) + sys.exit(1) + clazz = getattr(module, composite[1], None) + if not clazz: + print(f"Composition class {composite[0]} does not define: {composite[1]}", file=sys.stderr) + sys.exit(1) + if not inspect.isclass(clazz): + print(f"Composition class {self.args.composition} is not a class", file=sys.stderr) + sys.exit(1) + if not issubclass(clazz, pythonic.BaseComposite): + print(f"Composition class {self.args.composition} is not a subclass of BaseComposite", file=sys.stderr) + sys.exit(1) + composition = self.create_composition(composite, str(self.args.composition)) + + # Build up the RunFunctionRequest protobuf message used to call function-pythonic. + request = protobuf.Message(None, 'request', fnv1.RunFunctionRequest.DESCRIPTOR, fnv1.RunFunctionRequest()) + + # Load the request context with any specified command line options. + for entry in self.args.context_files: + key_path = entry.split('=', 1) + if len(key_path) != 2: + print(f"Invalid --context-files: {entry}", file=sys.stderr) + sys.exit(1) + path = pathlib.Path(key_path[1]) + if not path.is_file(): + print(f"Invalid --context-files {path} is not a file", file=sys.stderr) + sys.exit(1) + request.context[key_path[0]] = protobuf.Yaml(path.read_text()) + for entry in self.args.context_values: + key_value = entry.split('=', 1) + if len(key_value) != 2: + print(f"Invalid --context-values: {entry}", file=sys.stderr) + sys.exit(1) + request.context[key_value[0]] = protobuf.Yaml(key_value[1]) + + # Establish the request observed composite and specifed observed resources. + request.observed.composite.resource = composite + for resource in self.collect_resources(self.args.observed_resources): + name = resource.metadata.annotations['crossplane.io/composition-resource-name'] + if name: + request.observed.resources[str(name)].resource = resource + + # Collect specified required/extra resources. + requireds = [resource for resource in self.collect_resources(self.args.required_resources)] + requireds += [resource for resource in self.collect_resources(self.args.extra_resources)] + + # Collect specified credential secrets. + credentials = [] + for credential in self.collect_resources(self.args.function_credentials): + if credential.apiVersion == 'v1' and credential.kind == 'Secret': + credentials.append(credential) + + # These will hold the response conditions and results. + conditions = protobuf.List() + results = protobuf.List() + + # Create a function-pythonic function runner used to run pipeline steps. + runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns) + fatal = False + + # Process the composition pipeline steps. + for step in composition.spec.pipeline: + if step.functionRef.name != 'function-pythonic': + print(f"Only function-pythonic functions can be run: {step.functionRef.name}", file=sys.stderr) + sys.exit(1) + if not step.input.step: + step.input.step = step.step + request.input = step.input + + # Supply step requested credentials. + request.credentials() + for fn_credential in step.credentials: + if fn_credential.source == 'Secret' and fn_credential.secretRef: + for credential in credentials: + if credential.metadata.namespace == fn_credential.secretRef.namespace and credential.metadata.name == fn_credential.secretRef.name: + data = request.credentials[str(fn_credential.name)].credential_data.data + data() + for key, value in credential.data: + data[key] = protobuf.B64Decode(value) + break + else: + print(f"Step \"{step.step}\" secret not found: {fn_credential.secretRef.namespace} {fn_credential.secretRef.name}", file=sys.stderr) + sys.exit(1) + + # Track what extra/required resources have been processed. + requirements = protobuf.Message(None, 'requirements', fnv1.Requirements.DESCRIPTOR, fnv1.Requirements()) + for _ in range(5): + # Fetch the step bootstrap resources specified. + request.required_resources() + for requirement in step.requirements: + self.fetch_requireds(requireds, requirement.requirementName, requirement, request.required_resources) + # Fetch the required resources requested. + for name, selector in requirements.resources: + self.fetch_requireds(requireds, name, selector, request.required_resources) + # Fetch the now deprecated extra resources requested. + request.extra_resources() + for name, selector in requirements.extra_resources: + self.fetch_requireds(requireds, name, selector, request.extra_resources) + # Run the step using the function-pythonic function runner. + response = protobuf.Message( + None, + 'response', + fnv1.RunFunctionResponse.DESCRIPTOR, + await runner.RunFunction(request._message, None), + ) + # All done if there is a fatal result. + for result in response.results: + if result.severity == fnv1.Severity.SEVERITY_FATAL: + fatal = True + break + # Copy the response context to the request context to use in subsequent steps. + request.context = response.context + # Exit this loop if the function has not requested additional extra/required resources. + if response.requirements == requirements: + break + # Establish the new set of requested extra/required resoruces. + requirements = response.requirements + + # Copy the response desired state to the request desired state to use in subsequent steps. + request.desired.resources() + self.copy_resource(response.desired.composite, request.desired.composite) + for name, resource in response.desired.resources: + self.copy_resource(resource, request.desired.resources[name]) + + # Collect the step's returned conditions. + for condition in response.conditions: + if condition.type not in ('Ready', 'Synced', 'Healthy'): + conditions[protobuf.append] = self.create_condition(condition.type, condition.status, condition.reason, condition.message) + # Collect the step's returned results. + for result in response.results: + ix = len(results) + results[ix].apiVersion = 'render.crossplane.io/v1beta1' + results[ix].kind = 'Result' + results[ix].step = step.step + results[ix].severity = fnv1.Severity.Name(result.severity._value) + if result.reason: + results[ix].reason = result.reason + if result.message: + results[ix].message = result.message + + # All done if a fatal result was returned + if fatal: + break + + # Collect and format all the returned desired composed resources. + resources = protobuf.List() + unready = protobuf.List() + prefix = composite.metadata.labels['crossplane.io/composite'] + if not prefix: + prefix = composite.metadata.name + for name, resource in request.desired.resources: + if resource.ready != fnv1.Ready.READY_TRUE: + unready[protobuf.append] = name + resource = resource.resource + observed = request.observed.resources[name].resource + if observed: + for key in ('namespace', 'generateName', 'name'): + if observed.metadata[key]: + resource.metadata[key] = observed.metadata[key] + if not resource.metadata.name and not resource.metadata.generateName: + resource.metadata.generateName = f"{prefix}-" + if composite.metadata.namespace: + resource.metadata.namespace = composite.metadata.namespace + resource.metadata.annotations['crossplane.io/composition-resource-name'] = name + resource.metadata.labels['crossplane.io/composite'] = prefix + if composite.metadata.labels['crossplane.io/claim-name'] and composite.metadata.labels['crossplane.io/claim-namespace']: + resource.metadata.labels['crossplane.io/claim-namespace'] = composite.metadata.labels['crossplane.io/claim-namespace'] + resource.metadata.labels['crossplane.io/claim-name'] = composite.metadata.labels['crossplane.io/claim-name'] + elif composite.spec.claimRef.namespace and composite.spec.claimRef.name: + resource.metadata.labels['crossplane.io/claim-namespace'] = composite.spec.claimRef.namespace + resource.metadata.labels['crossplane.io/claim-name'] = composite.spec.claimRef.name + resource.metadata.ownerReferences[0].controller = True + resource.metadata.ownerReferences[0].blockOwnerDeletion = True + resource.metadata.ownerReferences[0].apiVersion = composite.apiVersion + resource.metadata.ownerReferences[0].kind = composite.kind + resource.metadata.ownerReferences[0].name = composite.metadata.name + resource.metadata.ownerReferences[0].uid = '' + resources[protobuf.append] = resource + + # Format the returned desired composite + composite = protobuf.Map() + for name, value in request.desired.composite.resource: + composite[name] = value + composite.apiVersion = request.observed.composite.resource.apiVersion + composite.kind = request.observed.composite.resource.kind + if self.args.include_full_xr: + composite.metadata = request.observed.composite.resource.metadata + if request.observed.composite.resource.spec: + composite.spec = request.observed.composite.resource.spec + else: + if request.observed.composite.resource.metadata.namespace: + composite.metadata.namespace = request.observed.composite.resource.metadata.namespace + composite.metadata.name = request.observed.composite.resource.metadata.name + # Add in the composite's status.conditions. + if request.desired.composite.ready == fnv1.Ready.READY_FALSE: + condition = self.create_condition('Ready', False, 'Creating') + elif request.desired.composite.ready == fnv1.Ready.READY_UNSPECIFIED and len(unready): + condition = self.create_condition('Ready', False, 'Creating', f"Unready resources: {', '.join(str(name) for name in unready)}") + else: + condition = self.create_condition('Ready', True, 'Available') + composite.status.conditions[protobuf.append] = condition + for condition in conditions: + composite.status.conditions[protobuf.append] = condition + + # Print the composite. + print('---') + print(str(composite), end='') + # Print the composed resources. + for resource in sorted(resources, key=lambda resource: str(resource.metadata.annotations['crossplane.io/composition-resource-name'])): + print('---') + print(str(resource), end='') + # Print the results (AKA events) if requested. + if self.args.include_function_results: + for result in results: + print('---') + print(str(result), end='') + # Print the final context if requested. + if self.args.include_context: + print('---') + print( + str(protobuf.Map( + apiVersion = 'render.crossplane.io/v1beta1', + kind = 'Context', + fields = request.context, + )), + end='', + ) + + def create_composition(self, composite, module): + composition = protobuf.Map() + composition.apiVersion = 'apiextensions.crossplane.io/v1' + composition.kind = 'Composition' + composition.metadata.name = 'function-pythonic-render' + composition.spec.compositeTypeRef.apiVersion = composite.apiVersion + composition.spec.compositeTypeRef.kind = composite.kind + composition.spec.mode = 'Pipeline' + composition.spec.pipeline[0].step = 'function-pythonic-render' + composition.spec.pipeline[0].functionRef.name = 'function-pythonic' + composition.spec.pipeline[0].input.apiVersion = 'pythonic.fn.crossplane.io/v1alpha1' + composition.spec.pipeline[0].input.kind = 'Composite' + composition.spec.pipeline[0].input.composite = module + return composition + + def collect_resources(self, resources): + files = [] + for resource in resources: + if resource.is_file(): + files.append(resource) + elif resource.is_dir(): + for file in resource.iterdir(): + if file.suffix in ('.yaml', '.yml'): + files.append(file) + else: + print(f"Specified resource is not a file or a directory: {resource}", file=sys.stderr) + sys.exit(1) + for file in files: + for document in yaml.safe_load_all(file.read_text()): + yield protobuf.Value(None, None, document) + + def fetch_requireds(self, requireds, name, selector, resources): + if not name: + return + name = str(name) + items = resources[name].items + items() # Force this to get created + for required in requireds: + if selector.api_version == required.apiVersion and selector.kind == required.kind: + if selector.match_name == required.metadata.name: + items[protobuf.append].resource = required + elif selector.match_labels.labels: + for key, value in selector.match_labels.labels: + if value != required.metadata.labels[key]: + break + else: + items[protobuf.append].resource = required + + def copy_resource(self, source, destination): + destination.resource = source.resource + destination.connection_details() + for key, value in source.connection_details: + destination.connection_details[key] = value + destination.ready = source.ready + + def create_condition(self, type, status, reason, message=None): + if isinstance(status, protobuf.FieldMessage): + if status._value == fnv1.Status.STATUS_CONDITION_TRUE: + status = 'True' + elif status._value == fnv1.Status.STATUS_CONDITION_FALSE: + status = 'False' + else: + status = 'Unknown' + elif isinstance(status, bool): + if status: + status = 'True' + else: + status = 'False' + elif status is None: + status = 'Unknown' + condition = { + 'type': type, + 'status': status, + 'reason': reason, + 'lastTransitionTime': '2026-01-01T00:00:00Z' + } + if message: + condition['message'] = message + return condition diff --git a/crossplane/pythonic/version.py b/crossplane/pythonic/version.py new file mode 100644 index 0000000..c400f89 --- /dev/null +++ b/crossplane/pythonic/version.py @@ -0,0 +1,13 @@ + +from . import ( + command, + __about__, +) + + +class Command(command.Command): + name = 'version' + help = 'Print the function-oythonic version' + + async def run(self): + print(__about__.__version__) diff --git a/examples/.dev/functions.yaml b/examples/.dev/functions.yaml index bfad87a..d161629 100644 --- a/examples/.dev/functions.yaml +++ b/examples/.dev/functions.yaml @@ -10,4 +10,4 @@ metadata: render.crossplane.io/runtime: Development render.crossplane.io/runtime-development-target: localhost:9443 spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/aks-cluster/cluster-function-pythonic.yaml b/examples/aks-cluster/cluster-function-pythonic.yaml index 4726b5f..679ae1c 100644 --- a/examples/aks-cluster/cluster-function-pythonic.yaml +++ b/examples/aks-cluster/cluster-function-pythonic.yaml @@ -3,7 +3,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 runtimeConfigRef: name: function-pythonic --- diff --git a/examples/aks-cluster/composition.yaml b/examples/aks-cluster/composition.yaml index c4449dc..de4bdaf 100644 --- a/examples/aks-cluster/composition.yaml +++ b/examples/aks-cluster/composition.yaml @@ -12,13 +12,13 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: aks.resourcegroup.ResourceGroupComposite - step: create-aks-cluster functionRef: name: function-pythonic input: - apiVersion: aks.pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: aks.kubernetescluster.KubernetesClusterComposite diff --git a/examples/aks-cluster/functions.yaml b/examples/aks-cluster/functions.yaml index 9e57754..e7492f7 100644 --- a/examples/aks-cluster/functions.yaml +++ b/examples/aks-cluster/functions.yaml @@ -5,6 +5,6 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 runtimeConfigRef: name: function-pythonic diff --git a/examples/aks-cluster/render.sh b/examples/aks-cluster/render.sh index 2a42755..93a521a 100755 --- a/examples/aks-cluster/render.sh +++ b/examples/aks-cluster/render.sh @@ -1,9 +1,10 @@ #!/usr/bin/env bash -# In one terminal -# PYTHONPATH=. function-pythonic --insecure --debug +cd $(dirname $(realpath $0)) +# In one terminal +# PYTHONPATH=. function-pythonic grpc --insecure --debug # In another terminal: +#exec crossplane render xr.yaml composition.yaml functions.yaml -cd $(dirname $(realpath $0)) -exec crossplane render xr.yaml composition.yaml functions.yaml +exec function-pythonic render --python-path=. xr.yaml composition.yaml diff --git a/examples/eks-cluster/composition-v2.yaml b/examples/eks-cluster/composition-v2.yaml index 05c8a21..3889681 100644 --- a/examples/eks-cluster/composition-v2.yaml +++ b/examples/eks-cluster/composition-v2.yaml @@ -1,10 +1,10 @@ apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: - name: eksclusters.pythonic.fortra.com + name: eksclusters.pythonic.crossplane.io spec: compositeTypeRef: - apiVersion: pythonic.fortra.com/v1alpha1 + apiVersion: pythonic.crossplane.io/v1alpha1 kind: EksCluster mode: Pipeline pipeline: @@ -13,7 +13,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): diff --git a/examples/eks-cluster/composition.yaml b/examples/eks-cluster/composition.yaml index 4c90298..fd8355d 100644 --- a/examples/eks-cluster/composition.yaml +++ b/examples/eks-cluster/composition.yaml @@ -1,10 +1,10 @@ apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: - name: eksclusters.pythonic.fortra.com + name: eksclusters.pythonic.crossplane.io spec: compositeTypeRef: - apiVersion: pythonic.fortra.com/v1alpha1 + apiVersion: pythonic.crossplane.io/v1alpha1 kind: EksCluster mode: Pipeline pipeline: @@ -13,7 +13,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): diff --git a/examples/eks-cluster/definition.yaml b/examples/eks-cluster/definition.yaml index 73f442b..ce3a990 100644 --- a/examples/eks-cluster/definition.yaml +++ b/examples/eks-cluster/definition.yaml @@ -1,14 +1,14 @@ apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: - name: eksclusters.pythonic.fortra.com + name: eksclusters.pythonic.crossplane.io spec: - group: pythonic.fortra.com + group: pythonic.crossplane.io names: kind: EksCluster plural: eksclusters defaultCompositionRef: - name: eksclusters.pythonic.fortra.com + name: eksclusters.pythonic.crossplane.io versions: - name: v1alpha1 served: true diff --git a/examples/eks-cluster/functions.yaml b/examples/eks-cluster/functions.yaml index 651ccc9..fa1ec3d 100644 --- a/examples/eks-cluster/functions.yaml +++ b/examples/eks-cluster/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/eks-cluster/render-v2.sh b/examples/eks-cluster/render-v2.sh index ad8751e..69df3f6 100755 --- a/examples/eks-cluster/render-v2.sh +++ b/examples/eks-cluster/render-v2.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render xr.yaml composition-v2.yaml functions.yaml +#exec crossplane render xr.yaml composition-v2.yaml functions.yaml +exec function-pythonic render xr.yaml composition-v2.yaml diff --git a/examples/eks-cluster/render.sh b/examples/eks-cluster/render.sh index 2fe6816..17aec4e 100755 --- a/examples/eks-cluster/render.sh +++ b/examples/eks-cluster/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render xr.yaml composition.yaml functions.yaml +#exec crossplane render xr.yaml composition.yaml functions.yaml +exec function-pythonic render xr.yaml composition.yaml diff --git a/examples/eks-cluster/xr.yaml b/examples/eks-cluster/xr.yaml index 233a1e0..dd5a0a1 100644 --- a/examples/eks-cluster/xr.yaml +++ b/examples/eks-cluster/xr.yaml @@ -1,4 +1,4 @@ -apiVersion: pythonic.fortra.com/v1alpha1 +apiVersion: pythonic.crossplane.io/v1alpha1 kind: EksCluster metadata: name: pythonic @@ -10,4 +10,4 @@ spec: tags: project: development environment: stage - 'fortra:cloudops': 'owned' + 'crossplane-contrib:cloudops': 'owned' diff --git a/examples/filing-system/composition.yaml b/examples/filing-system/composition.yaml index 4d2466b..77c13f1 100644 --- a/examples/filing-system/composition.yaml +++ b/examples/filing-system/composition.yaml @@ -12,6 +12,6 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: vcluster.VClusterComposite diff --git a/examples/filing-system/function.yaml b/examples/filing-system/function.yaml index 088daeb..56cf80d 100644 --- a/examples/filing-system/function.yaml +++ b/examples/filing-system/function.yaml @@ -3,7 +3,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 runtimeConfigRef: apiVersion: pkg.crossplane.io/v1beta1 kind: DeploymentRuntimeConfig diff --git a/examples/function-go-templating/conditions/composition.yaml b/examples/function-go-templating/conditions/composition.yaml index 7a25ce4..8a3ec8b 100644 --- a/examples/function-go-templating/conditions/composition.yaml +++ b/examples/function-go-templating/conditions/composition.yaml @@ -13,7 +13,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite step: conditions composite: | @@ -29,7 +29,3 @@ spec: #self.conditions.TestCondition4.reason = 'InstallFail4' #self.conditions.TestCondition4.message = 'failed to install 4' #self.conditions.TestCondition4.status = False - - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/examples/function-go-templating/conditions/functions.yaml b/examples/function-go-templating/conditions/functions.yaml index d271b77..4dedf69 100644 --- a/examples/function-go-templating/conditions/functions.yaml +++ b/examples/function-go-templating/conditions/functions.yaml @@ -1,25 +1,10 @@ --- apiVersion: pkg.crossplane.io/v1 kind: Function -metadata: - name: function-environment-configs -spec: - # This is ignored when using the Development runtime. - package: xpkg.upbound.io/crossplane-contrib/function-environment-configs:v0.2.0 ---- -apiVersion: pkg.crossplane.io/v1 -kind: Function metadata: name: function-pythonic annotations: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 ---- -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-auto-ready -spec: - package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.4.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/function-go-templating/conditions/render.sh b/examples/function-go-templating/conditions/render.sh index 2fe6816..17aec4e 100755 --- a/examples/function-go-templating/conditions/render.sh +++ b/examples/function-go-templating/conditions/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render xr.yaml composition.yaml functions.yaml +#exec crossplane render xr.yaml composition.yaml functions.yaml +exec function-pythonic render xr.yaml composition.yaml diff --git a/examples/function-go-templating/context/composition.yaml b/examples/function-go-templating/context/composition.yaml index e4c7bc1..e5749a4 100644 --- a/examples/function-go-templating/context/composition.yaml +++ b/examples/function-go-templating/context/composition.yaml @@ -9,34 +9,23 @@ spec: mode: Pipeline pipeline: - - step: environmentConfigs - functionRef: - name: function-environment-configs - input: - apiVersion: environmentconfigs.fn.crossplane.io/v1beta1 - kind: Input - spec: - environmentConfigs: - - type: Reference - ref: - name: example-config - - step: python-update-conditions functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): def compose(self): + self.environment.complex = self.requireds['example-config']( + 'apiextensions.crossplane.io/v1alpha1', + 'EnvironmentConfig', + name='example-config', + )[0].data.complex self.environment.update = 'environment' self.environment.nestedEnvUpdate.hello = 'world' self.environment.array = ['1', '2'] self.context['other-context-key'].complex = self.environment.complex self.context.newKey.hello = 'world' self.status.fromEnv = self.environment.complex.c.d - - - step: automatically-detect-ready-composed-resources - functionRef: - name: function-auto-ready diff --git a/examples/function-go-templating/context/environmentConfigs.yaml b/examples/function-go-templating/context/environmentConfigs.yaml index 46dc62b..1604f2a 100644 --- a/examples/function-go-templating/context/environmentConfigs.yaml +++ b/examples/function-go-templating/context/environmentConfigs.yaml @@ -7,4 +7,4 @@ data: a: b c: d: e - f: "1" \ No newline at end of file + f: "1" diff --git a/examples/function-go-templating/context/functions.yaml b/examples/function-go-templating/context/functions.yaml index d271b77..4dedf69 100644 --- a/examples/function-go-templating/context/functions.yaml +++ b/examples/function-go-templating/context/functions.yaml @@ -1,25 +1,10 @@ --- apiVersion: pkg.crossplane.io/v1 kind: Function -metadata: - name: function-environment-configs -spec: - # This is ignored when using the Development runtime. - package: xpkg.upbound.io/crossplane-contrib/function-environment-configs:v0.2.0 ---- -apiVersion: pkg.crossplane.io/v1 -kind: Function metadata: name: function-pythonic annotations: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 ---- -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-auto-ready -spec: - package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.4.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/function-go-templating/context/render.sh b/examples/function-go-templating/context/render.sh index 28f62f9..8316b63 100755 --- a/examples/function-go-templating/context/render.sh +++ b/examples/function-go-templating/context/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render --extra-resources environmentConfigs.yaml --include-context xr.yaml composition.yaml functions.yaml +#exec crossplane render --extra-resources environmentConfigs.yaml --include-context xr.yaml composition.yaml functions.yaml +exec function-pythonic render --extra-resources=environmentConfigs.yaml --include-context xr.yaml composition.yaml diff --git a/examples/function-go-templating/extra-resources/composition.yaml b/examples/function-go-templating/extra-resources/composition.yaml index 2c99563..97211e5 100644 --- a/examples/function-go-templating/extra-resources/composition.yaml +++ b/examples/function-go-templating/extra-resources/composition.yaml @@ -12,7 +12,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): diff --git a/examples/function-go-templating/extra-resources/functions.yaml b/examples/function-go-templating/extra-resources/functions.yaml index 1e35fc3..44113c0 100644 --- a/examples/function-go-templating/extra-resources/functions.yaml +++ b/examples/function-go-templating/extra-resources/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2,0 diff --git a/examples/function-go-templating/extra-resources/render.sh b/examples/function-go-templating/extra-resources/render.sh index 0ff9a0e..08e8e1a 100755 --- a/examples/function-go-templating/extra-resources/render.sh +++ b/examples/function-go-templating/extra-resources/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render --extra-resources extraResources.yaml xr.yaml composition.yaml functions.yaml +#exec crossplane render --extra-resources extraResources.yaml xr.yaml composition.yaml functions.yaml +exec function-pythonic render --extra-resources extraResources.yaml xr.yaml composition.yaml diff --git a/examples/function-go-templating/functions/fromYaml/composition.yaml b/examples/function-go-templating/functions/fromYaml/composition.yaml index 375f75a..f55d2d8 100644 --- a/examples/function-go-templating/functions/fromYaml/composition.yaml +++ b/examples/function-go-templating/functions/fromYaml/composition.yaml @@ -13,7 +13,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): diff --git a/examples/function-go-templating/functions/fromYaml/functions.yaml b/examples/function-go-templating/functions/fromYaml/functions.yaml index 1e35fc3..53ce153 100644 --- a/examples/function-go-templating/functions/fromYaml/functions.yaml +++ b/examples/function-go-templating/functions/fromYaml/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/function-go-templating/functions/fromYaml/render.sh b/examples/function-go-templating/functions/fromYaml/render.sh index 2fe6816..17aec4e 100755 --- a/examples/function-go-templating/functions/fromYaml/render.sh +++ b/examples/function-go-templating/functions/fromYaml/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render xr.yaml composition.yaml functions.yaml +#exec crossplane render xr.yaml composition.yaml functions.yaml +exec function-pythonic render xr.yaml composition.yaml diff --git a/examples/function-go-templating/functions/getComposedResource/composition.yaml b/examples/function-go-templating/functions/getComposedResource/composition.yaml index 13006a4..66536fe 100644 --- a/examples/function-go-templating/functions/getComposedResource/composition.yaml +++ b/examples/function-go-templating/functions/getComposedResource/composition.yaml @@ -13,7 +13,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | apiVersion = 'dbforpostgresql.azure.upbound.io/v1beta1' diff --git a/examples/function-go-templating/functions/getComposedResource/functions.yaml b/examples/function-go-templating/functions/getComposedResource/functions.yaml index 1e35fc3..53ce153 100644 --- a/examples/function-go-templating/functions/getComposedResource/functions.yaml +++ b/examples/function-go-templating/functions/getComposedResource/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/function-go-templating/functions/getComposedResource/render.sh b/examples/function-go-templating/functions/getComposedResource/render.sh index cb62c9b..934bf9e 100755 --- a/examples/function-go-templating/functions/getComposedResource/render.sh +++ b/examples/function-go-templating/functions/getComposedResource/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render --observed-resources=observed.yaml xr.yaml composition.yaml functions.yaml +#exec crossplane render --observed-resources=observed.yaml xr.yaml composition.yaml functions.yaml +exec function-pythonic render --observed-resources=observed.yaml xr.yaml composition.yaml diff --git a/examples/function-go-templating/functions/getCompositeResource/composition.yaml b/examples/function-go-templating/functions/getCompositeResource/composition.yaml index a0e320b..71b94f4 100644 --- a/examples/function-go-templating/functions/getCompositeResource/composition.yaml +++ b/examples/function-go-templating/functions/getCompositeResource/composition.yaml @@ -13,7 +13,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): diff --git a/examples/function-go-templating/functions/getCompositeResource/functions.yaml b/examples/function-go-templating/functions/getCompositeResource/functions.yaml index 1e35fc3..53ce153 100644 --- a/examples/function-go-templating/functions/getCompositeResource/functions.yaml +++ b/examples/function-go-templating/functions/getCompositeResource/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/function-go-templating/functions/getCompositeResource/render.sh b/examples/function-go-templating/functions/getCompositeResource/render.sh index 2fe6816..17aec4e 100755 --- a/examples/function-go-templating/functions/getCompositeResource/render.sh +++ b/examples/function-go-templating/functions/getCompositeResource/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render xr.yaml composition.yaml functions.yaml +#exec crossplane render xr.yaml composition.yaml functions.yaml +exec function-pythonic render xr.yaml composition.yaml diff --git a/examples/function-go-templating/functions/getCredentialData/composition.yaml b/examples/function-go-templating/functions/getCredentialData/composition.yaml new file mode 100644 index 0000000..87b717f --- /dev/null +++ b/examples/function-go-templating/functions/getCredentialData/composition.yaml @@ -0,0 +1,27 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: example-function-get-credential-data +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1beta1 + kind: XR + mode: Pipeline + pipeline: + - step: render-templates + functionRef: + name: function-pythonic + credentials: + - name: foo-creds + secretRef: + name: foo-creds + namespace: default + source: Secret + input: + apiVersion: pythonic.fn.crossplane.io/v1alpha1 + kind: Composite + composite: | + class Composite(BaseComposite): + def compose(self): + self.context.username = self.credentials['foo-creds'].username + self.context.password = self.credentials['foo-creds'].password diff --git a/examples/function-go-templating/functions/getCredentialData/credentials.yaml b/examples/function-go-templating/functions/getCredentialData/credentials.yaml new file mode 100644 index 0000000..2b4f04e --- /dev/null +++ b/examples/function-go-templating/functions/getCredentialData/credentials.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: foo-creds + namespace: default +type: Opaque +data: + username: Zm9v # foo + password: YmFy # bar diff --git a/examples/function-go-templating/functions/getCredentialData/functions.yaml b/examples/function-go-templating/functions/getCredentialData/functions.yaml new file mode 100644 index 0000000..53ce153 --- /dev/null +++ b/examples/function-go-templating/functions/getCredentialData/functions.yaml @@ -0,0 +1,9 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-pythonic + annotations: + # This tells crossplane beta render to connect to the function locally. + render.crossplane.io/runtime: Development +spec: + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/function-go-templating/functions/getCredentialData/render.sh b/examples/function-go-templating/functions/getCredentialData/render.sh new file mode 100755 index 0000000..8508fbd --- /dev/null +++ b/examples/function-go-templating/functions/getCredentialData/render.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +cd $(dirname $(realpath $0)) +#exec crossplane render --function-credentials=credentials.yaml --include-context xr.yaml composition.yaml functions.yaml +exec function-pythonic render --function-credentials=credentials.yaml --include-context xr.yaml composition.yaml diff --git a/examples/function-go-templating/functions/getCredentialData/xr.yaml b/examples/function-go-templating/functions/getCredentialData/xr.yaml new file mode 100644 index 0000000..2221440 --- /dev/null +++ b/examples/function-go-templating/functions/getCredentialData/xr.yaml @@ -0,0 +1,5 @@ +apiVersion: example.crossplane.io/v1beta1 +kind: XR +metadata: + name: example +spec: {} diff --git a/examples/function-go-templating/functions/getResourceCondition/composition.yaml b/examples/function-go-templating/functions/getResourceCondition/composition.yaml index 602680d..ee70966 100644 --- a/examples/function-go-templating/functions/getResourceCondition/composition.yaml +++ b/examples/function-go-templating/functions/getResourceCondition/composition.yaml @@ -13,7 +13,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): diff --git a/examples/function-go-templating/functions/getResourceCondition/functions.yaml b/examples/function-go-templating/functions/getResourceCondition/functions.yaml index 1e35fc3..53ce153 100644 --- a/examples/function-go-templating/functions/getResourceCondition/functions.yaml +++ b/examples/function-go-templating/functions/getResourceCondition/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/function-go-templating/functions/getResourceCondition/render.sh b/examples/function-go-templating/functions/getResourceCondition/render.sh index cb62c9b..303643b 100755 --- a/examples/function-go-templating/functions/getResourceCondition/render.sh +++ b/examples/function-go-templating/functions/getResourceCondition/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render --observed-resources=observed.yaml xr.yaml composition.yaml functions.yaml +#exec crossplane render --observed-resources=observed.yaml xr.yaml composition.yaml functions.yaml +exec function-pythonic render --observed-resources=observed.yaml xr.yaml composition.yaml diff --git a/examples/function-go-templating/functions/include/composition.yaml b/examples/function-go-templating/functions/include/composition.yaml index 2700f54..161fa0c 100644 --- a/examples/function-go-templating/functions/include/composition.yaml +++ b/examples/function-go-templating/functions/include/composition.yaml @@ -12,7 +12,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): diff --git a/examples/function-go-templating/functions/include/functions.yaml b/examples/function-go-templating/functions/include/functions.yaml index 1e35fc3..53ce153 100644 --- a/examples/function-go-templating/functions/include/functions.yaml +++ b/examples/function-go-templating/functions/include/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/function-go-templating/functions/include/render.sh b/examples/function-go-templating/functions/include/render.sh index 2fe6816..17aec4e 100755 --- a/examples/function-go-templating/functions/include/render.sh +++ b/examples/function-go-templating/functions/include/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render xr.yaml composition.yaml functions.yaml +#exec crossplane render xr.yaml composition.yaml functions.yaml +exec function-pythonic render xr.yaml composition.yaml diff --git a/examples/function-go-templating/functions/toYaml/composition.yaml b/examples/function-go-templating/functions/toYaml/composition.yaml index b5efb4d..5571a80 100644 --- a/examples/function-go-templating/functions/toYaml/composition.yaml +++ b/examples/function-go-templating/functions/toYaml/composition.yaml @@ -13,7 +13,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): diff --git a/examples/function-go-templating/functions/toYaml/functions.yaml b/examples/function-go-templating/functions/toYaml/functions.yaml index 1e35fc3..53ce153 100644 --- a/examples/function-go-templating/functions/toYaml/functions.yaml +++ b/examples/function-go-templating/functions/toYaml/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/function-go-templating/functions/toYaml/render.sh b/examples/function-go-templating/functions/toYaml/render.sh index 2fe6816..17aec4e 100755 --- a/examples/function-go-templating/functions/toYaml/render.sh +++ b/examples/function-go-templating/functions/toYaml/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render xr.yaml composition.yaml functions.yaml +#exec crossplane render xr.yaml composition.yaml functions.yaml +exec function-pythonic render xr.yaml composition.yaml diff --git a/examples/function-go-templating/inline/composition.yaml b/examples/function-go-templating/inline/composition.yaml index 7e61fc9..7ea89f7 100644 --- a/examples/function-go-templating/inline/composition.yaml +++ b/examples/function-go-templating/inline/composition.yaml @@ -13,7 +13,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | import random diff --git a/examples/function-go-templating/inline/functions.yaml b/examples/function-go-templating/inline/functions.yaml index 1e35fc3..53ce153 100644 --- a/examples/function-go-templating/inline/functions.yaml +++ b/examples/function-go-templating/inline/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/function-go-templating/inline/render.sh b/examples/function-go-templating/inline/render.sh index 2fe6816..17aec4e 100755 --- a/examples/function-go-templating/inline/render.sh +++ b/examples/function-go-templating/inline/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render xr.yaml composition.yaml functions.yaml +#exec crossplane render xr.yaml composition.yaml functions.yaml +exec function-pythonic render xr.yaml composition.yaml diff --git a/examples/function-go-templating/recursive/composition-real.yaml b/examples/function-go-templating/recursive/composition-real.yaml index 88a77af..e51bdee 100644 --- a/examples/function-go-templating/recursive/composition-real.yaml +++ b/examples/function-go-templating/recursive/composition-real.yaml @@ -12,7 +12,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): diff --git a/examples/function-go-templating/recursive/composition-wrapper.yaml b/examples/function-go-templating/recursive/composition-wrapper.yaml index 1f95803..0ecce94 100644 --- a/examples/function-go-templating/recursive/composition-wrapper.yaml +++ b/examples/function-go-templating/recursive/composition-wrapper.yaml @@ -12,7 +12,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): diff --git a/examples/function-go-templating/recursive/functions.yaml b/examples/function-go-templating/recursive/functions.yaml index 1e35fc3..53ce153 100644 --- a/examples/function-go-templating/recursive/functions.yaml +++ b/examples/function-go-templating/recursive/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/function-go-templating/recursive/render.sh b/examples/function-go-templating/recursive/render.sh index 579d68c..e00d274 100755 --- a/examples/function-go-templating/recursive/render.sh +++ b/examples/function-go-templating/recursive/render.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -crossplane render xr.yaml composition-wrapper.yaml functions.yaml +#crossplane render xr.yaml composition-wrapper.yaml functions.yaml #crossplane render xr.yaml composition-real.yaml functions.yaml + +function-pythonic render xr.yaml composition-wrapper.yaml +#function-pythonic render xr.yaml composition-real.yaml diff --git a/examples/get-started-app/composition.yaml b/examples/get-started-app/composition.yaml index c29f4dd..cf0bd0e 100644 --- a/examples/get-started-app/composition.yaml +++ b/examples/get-started-app/composition.yaml @@ -12,7 +12,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class Composite(BaseComposite): diff --git a/examples/get-started-app/functions.yaml b/examples/get-started-app/functions.yaml index f8f4c05..b4f4a12 100644 --- a/examples/get-started-app/functions.yaml +++ b/examples/get-started-app/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/get-started-app/render.sh b/examples/get-started-app/render.sh index 2fe6816..17aec4e 100755 --- a/examples/get-started-app/render.sh +++ b/examples/get-started-app/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render xr.yaml composition.yaml functions.yaml +#exec crossplane render xr.yaml composition.yaml functions.yaml +exec function-pythonic render xr.yaml composition.yaml diff --git a/examples/helm-copy-secret/composition.yaml b/examples/helm-copy-secret/composition.yaml index fdad6e3..6e259a4 100644 --- a/examples/helm-copy-secret/composition.yaml +++ b/examples/helm-copy-secret/composition.yaml @@ -12,6 +12,6 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: vcluster.VClusterStatusComposite diff --git a/examples/helm-copy-secret/functions.yaml b/examples/helm-copy-secret/functions.yaml index f8f4c05..b4f4a12 100644 --- a/examples/helm-copy-secret/functions.yaml +++ b/examples/helm-copy-secret/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/helm-copy-secret/render.sh b/examples/helm-copy-secret/render.sh index 7c5c08c..7b8a6dc 100755 --- a/examples/helm-copy-secret/render.sh +++ b/examples/helm-copy-secret/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render --include-full-xr --include-function-results xr.yaml composition.yaml functions.yaml +#exec crossplane render --include-full-xr --include-function-results xr.yaml composition.yaml functions.yaml +exec function-pythonic render --python-path=. --render-unknowns --include-full-xr --include-function-results xr.yaml composition.yaml diff --git a/examples/helm-copy-secret/vcluster.py b/examples/helm-copy-secret/vcluster.py index d204eae..8187837 100644 --- a/examples/helm-copy-secret/vcluster.py +++ b/examples/helm-copy-secret/vcluster.py @@ -59,5 +59,5 @@ def compose(self): self.conditions.VClusterReady(reason, message, status) if not status: - self.events.info(reason, message) + self.results.info(reason, message) self.ready = False diff --git a/examples/import-existing-vpc/composition.yaml b/examples/import-existing-vpc/composition.yaml index 8afb9e2..afefba1 100644 --- a/examples/import-existing-vpc/composition.yaml +++ b/examples/import-existing-vpc/composition.yaml @@ -13,7 +13,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | from crossplane.pythonic import BaseComposite @@ -47,7 +47,7 @@ spec: if len(vpcs) == 1: vpc.externalName = vpcs[0]['VpcId'] else: - self.events.fatal('MultipleResources', f"More than one vpc found for: {self.metadata.name}") + self.results.fatal('MultipleResources', f"More than one vpc found for: {self.metadata.name}") self.status.vpcId = vpc.status.atProvider.id return vpc diff --git a/examples/import-existing-vpc/functions.yaml b/examples/import-existing-vpc/functions.yaml index 651ccc9..fa1ec3d 100644 --- a/examples/import-existing-vpc/functions.yaml +++ b/examples/import-existing-vpc/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/import-existing-vpc/render.sh b/examples/import-existing-vpc/render.sh index 65a573e..d0dbb1b 100755 --- a/examples/import-existing-vpc/render.sh +++ b/examples/import-existing-vpc/render.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) #exec crossplane render xr.yaml composition.yaml functions.yaml -exec crossplane render --observed-resources=observed.yaml xr.yaml composition.yaml functions.yaml +#exec crossplane render --observed-resources=observed.yaml xr.yaml composition.yaml functions.yaml + +exec function-pythonic render xr.yaml composition.yaml +#exec function-pythonic render --observed-resources=observed.yaml xr.yaml composition.yaml diff --git a/examples/import-existing-vpc/xr.yaml b/examples/import-existing-vpc/xr.yaml index 07d021f..6a9b921 100644 --- a/examples/import-existing-vpc/xr.yaml +++ b/examples/import-existing-vpc/xr.yaml @@ -7,4 +7,4 @@ spec: region: us-east-1 cidr: 10.0.0.0/16 tags: - 'fortra:cloudops': 'owned' + 'crossplane-contrib:cloudops': 'owned' diff --git a/examples/single-purpose/functions.yaml b/examples/single-purpose/functions.yaml index f8f4c05..b4f4a12 100644 --- a/examples/single-purpose/functions.yaml +++ b/examples/single-purpose/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/single-purpose/render.sh b/examples/single-purpose/render.sh index dcc81e5..6cf4f23 100755 --- a/examples/single-purpose/render.sh +++ b/examples/single-purpose/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render xr.yaml ../../package/composite-composition.yaml functions.yaml +#exec crossplane render xr.yaml ../../package/composite-composition.yaml functions.yaml +exec function-pythonic render xr.yaml diff --git a/examples/single-purpose/xr.yaml b/examples/single-purpose/xr.yaml index dce0317..3ed0c65 100644 --- a/examples/single-purpose/xr.yaml +++ b/examples/single-purpose/xr.yaml @@ -1,4 +1,4 @@ -apiVersion: pythonic.fortra.com/v1alpha1 +apiVersion: pythonic.crossplane.io/v1alpha1 kind: Composite metadata: name: composite-example diff --git a/examples/usages-extra/composition.yaml b/examples/usages-extra/composition.yaml index b2d0760..38dd4d3 100644 --- a/examples/usages-extra/composition.yaml +++ b/examples/usages-extra/composition.yaml @@ -12,7 +12,7 @@ spec: functionRef: name: function-pythonic input: - apiVersion: pythonic.fn.fortra.com/v1alpha1 + apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite composite: | class UsagesComposite(BaseComposite): diff --git a/examples/usages-extra/functions.yaml b/examples/usages-extra/functions.yaml index 1e35fc3..53ce153 100644 --- a/examples/usages-extra/functions.yaml +++ b/examples/usages-extra/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 diff --git a/examples/usages-extra/render.sh b/examples/usages-extra/render.sh index 10eb286..4e97d15 100755 --- a/examples/usages-extra/render.sh +++ b/examples/usages-extra/render.sh @@ -1,3 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -exec crossplane render --extra-resources extraResources.yaml --observed-resources=observedResources.yaml xr.yaml composition.yaml functions.yaml +#exec crossplane render --extra-resources extraResources.yaml --observed-resources=observedResources.yaml xr.yaml composition.yaml functions.yaml +exec function-pythonic render --extra-resources=extraResources.yaml --observed-resources=observedResources.yaml xr.yaml composition.yaml diff --git a/package/composite-composition.yaml b/package/composite-composition.yaml index 7fb739c..1ffc768 100644 --- a/package/composite-composition.yaml +++ b/package/composite-composition.yaml @@ -1,3 +1,23 @@ +--- +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: composites.pythonic.crossplane.io +spec: + compositeTypeRef: + apiVersion: pythonic.crossplane.io/v1alpha1 + kind: Composite + mode: Pipeline + pipeline: + - step: pythonic + functionRef: + name: function-pythonic + input: + apiVersion: pythonic.fn.crossplane.io/v1alpha1 + kind: Composite + # functin-pythonic gets the python from the composite itself + composite: '' +--- apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: diff --git a/package/composite-definition.yaml b/package/composite-definition.yaml index dd08d16..efdb7aa 100644 --- a/package/composite-definition.yaml +++ b/package/composite-definition.yaml @@ -1,3 +1,40 @@ +--- +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: composites.pythonic.crossplane.io +spec: + group: pythonic.crossplane.io + names: + kind: Composite + plural: composites + defaultCompositionRef: + name: composites.pythonic.crossplane.io + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + parameters: + type: object + x-kubernetes-preserve-unknown-fields: true + composite: + type: string + description: 'A Python module that defines a class with the signature: class Composite(BaseComposite)' + required: + - composite + status: + type: object + properties: + composite: + x-kubernetes-preserve-unknown-fields: true +--- apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: diff --git a/package/crossplane.yaml b/package/crossplane.yaml index 054be9a..f35ee62 100644 --- a/package/crossplane.yaml +++ b/package/crossplane.yaml @@ -2,4 +2,13 @@ apiVersion: meta.pkg.crossplane.io/v1 kind: Function metadata: name: function-pythonic + annotations: + meta.crossplane.io/source: github.com/crossplane-contrib/function-pythonic + meta.crossplane.io/license: Apache-2.0 + meta.crossplane.io/description: Python based Crossplane Function providing a clean and elegant syntax for writing Crossplane Compositions + meta.crossplane.io/readme: | + A Crossplane composition function that lets you compose Composites using + a set of python classes enabling an elegant and terse syntax. See the + [README](https://github.com/crossplane-contrib/function-pythonic) + for examples and documentation. spec: {} diff --git a/package/input-definition.yaml b/package/input-definition.yaml index 3f42901..647d7b5 100644 --- a/package/input-definition.yaml +++ b/package/input-definition.yaml @@ -1,6 +1,47 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: composites.pythonic.fn.crossplane.io +spec: + group: pythonic.fn.crossplane.io + names: + categories: + - crossplane + kind: Composite + listKind: CompositeList + plural: composites + singular: composite + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + type: object + description: A Python script for composing Crossplane Composites + properties: + apiVersion: + type: string + description: Crossplane Composition Pipeline Function Input Api Version + kind: + type: string + description: Crossplane Composition Pipeline Function Input Kind + step: + type: string + description: Optional step name used in logging + parameters: + type: object + x-kubernetes-preserve-unknown-fields: true + composite: + type: string + description: 'A Python module that defines a class with the signature: class Composite(BaseComposite)' + required: + - composite + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: composites.pythonic.fn.fortra.com spec: diff --git a/pyproject.toml b/pyproject.toml index a3a5522..234615e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,9 +26,9 @@ packages = ["kopf==1.39.1"] pip-install = ["pip==25.3"] [project.urls] -Documentation = "https://github.com/fortra/function-pythonic#readme" -Issues = "https://github.com/fortra/function-pythonic/issues" -Source = "https://github.com/fortra/function-pythonic" +Documentation = "https://github.com/crossplane-contrib/function-pythonic#readme" +Issues = "https://github.com/crossplane-contrib/function-pythonic/issues" +Source = "https://github.com/crossplane-contrib/function-pythonic" [project.scripts] function-pythonic = "crossplane.pythonic.main:main" @@ -38,7 +38,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.version] -path = "crossplane/pythonic/__version__.py" +path = "crossplane/pythonic/__about__.py" validate-bump = false [tool.hatch.build.targets.wheel] @@ -57,10 +57,10 @@ path = ".venv-default" dependencies = ["ipython==9.1.0"] packages = ["crossplane"] [tool.hatch.envs.default.scripts] -production = "python -m crossplane.pythonic.main --insecure" -development = "python -m crossplane.pythonic.main --insecure --debug --render-unknowns" -#development = "python -m crossplane.pythonic.main --insecure --debug" -packages = "python -m crossplane.pythonic.main --insecure --debug --render-unknowns --packages --packages-secrets" +production = "python -m crossplane.pythonic.main grpc --insecure" +development = "python -m crossplane.pythonic.main grpc --insecure --debug --render-unknowns" +#development = "python -m crossplane.pythonic.main grpc --insecure --debug" +packages = "python -m crossplane.pythonic.main grpc --insecure --debug --render-unknowns --packages --packages-secrets" [tool.hatch.envs.lint] type = "virtual" @@ -82,7 +82,7 @@ dependencies = [ ] packages = ["crossplane"] [tool.hatch.envs.test.scripts] -all = "python -m pytest tests/ -x --verbose --verbose --cov --cov-report=term --cov-report=html:reports" +all = "python -m pytest tests -x --verbose --verbose --cov --cov-report=term --cov-report=html:reports" protobuf = "python -m pytest tests/test_protobuf_*.py -x --verbose --verbose --cov --cov-report=term --cov-report=html:reports" ci = "python -m pytest tests --verbose --verbose --junitxml=reports/pytest-junit.xml --cov --cov-report=term --cov-report=xml:reports/pytest-coverage.xml" diff --git a/scripts/setup-local.sh b/scripts/setup-local.sh index 200ee3c..a1f1cda 100755 --- a/scripts/setup-local.sh +++ b/scripts/setup-local.sh @@ -196,9 +196,8 @@ spec: name: function-pythonic EOF -# package: ghcr.io/iciclespider/function-pythonic:v0.0.0-20251204014512-08165a015bbf -# package: ghcr.io/fortra/function-pythonic:v0.0.0-20250819201108-49cfb066579f -# package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.1.5 +# package: ghcr.io/crossplane-contrib/function-pythonic:v0.0.0-20250819201108-49cfb066579f +# package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 kubectl apply -f - <