Skip to content

Commit 919fac4

Browse files
author
Patrick J. McNerthney
committed
Make the --pip-install and --packages dependencies optional,
allow any class name that subclasses BaseComposite.
1 parent b520959 commit 919fac4

24 files changed

+116
-108
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ COPY dist/*.whl /root
44
WORKDIR /
55
RUN \
66
set -eux && \
7-
pip install --root-user-action ignore --no-build-isolation /root/*.whl && \
7+
pip install --root-user-action ignore --no-build-isolation $(echo /root/*.whl)[packages,pip-install] && \
88
rm -rf /root/*.whl /root/.cache && \
99
groupadd --gid 2000 pythonic && \
1010
useradd --uid 2000 --gid pythonic --home-dir /opt/pythonic --create-home --shell /usr/sbin/nologin pythonic
1111

1212
USER pythonic:pythonic
1313
WORKDIR /opt/pythonic
1414
EXPOSE 9443
15-
ENTRYPOINT ["python", "-m", "crossplane.pythonic.main"]
15+
ENTRYPOINT ["function-pythonic"]

README.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ spec:
2828
apiVersion: pythonic.fn.crossplane.io/v1beta1
2929
kind: Composite
3030
composite: |
31-
class Composite(BaseComposite):
31+
class VpcComposite(BaseComposite):
3232
def compose(self):
3333
vpc = self.resources.vpc('ec2.aws.crossplane.io/v1beta1', 'VPC')
3434
vpc.spec.forProvider.region = self.spec.region
@@ -57,7 +57,7 @@ kind: Function
5757
metadata:
5858
name: function-pythonic
5959
spec:
60-
package: ghcr.io/fortra/function-pythonic:v0.0.7
60+
package: ghcr.io/fortra/function-pythonic:v0.0.9
6161
```
6262
## Composed Resource Dependencies
6363
@@ -158,13 +158,13 @@ proto = format(request, 'protobuf') # get the request as a protobuf string
158158
Composite composition is performed from a Composite orientation. A `BaseComposite` class
159159
is subclassed and the `compose` method is implemented.
160160
```python
161-
class Composite(BaseComposite):
161+
class MyComposite(BaseComposite):
162162
def compose(self):
163163
# Compose the Composite
164164
```
165165
The compose method can also declare itself as performing async io:
166166
```python
167-
class Composite(BaseComposite):
167+
class MyAsyncComposite(BaseComposite):
168168
async def compose(self):
169169
# Compose the Composite using async io when needed
170170
```
@@ -266,7 +266,7 @@ Each resource in the list is the following RequiredResource class:
266266

267267
### Conditions
268268

269-
The `BaseCompsite.conditions`, `Resource.conditions`, and `RequiredResource.conditions` fields
269+
The `BaseComposite.conditions`, `Resource.conditions`, and `RequiredResource.conditions` fields
270270
are maps of that entity's status conditions array, with the map key being the condition type.
271271
The fields are read only for `Resource.conditions` and `RequiredResource.conditions`.
272272

@@ -307,7 +307,7 @@ metadata:
307307
name: composite-example
308308
spec:
309309
composite: |
310-
class Composite(BaseComposite):
310+
class HelloComposite(BaseComposite):
311311
def compose(self):
312312
self.status.composite = 'Hello, World!'
313313
```
@@ -349,7 +349,7 @@ spec:
349349
apiVersion: pythonic.fn.fortra.com/v1alpha1
350350
kind: Composite
351351
composite: |
352-
class Composite(BaseComposite):
352+
class GreetingComposite(BaseComposite):
353353
def compose(self):
354354
self.status.greeting = f"Hello, {self.spec.who}!"
355355
```
@@ -362,7 +362,7 @@ metadata:
362362
annotations:
363363
render.crossplane.io/runtime: Development
364364
spec:
365-
package: ghcr.io/fortra/function-pythonic:v0.0.7
365+
package: ghcr.io/fortra/function-pythonic:v0.0.9
366366
```
367367
In one terminal session, run function-pythonic:
368368
```shell
@@ -422,7 +422,7 @@ Then, in your Composition:
422422
kind: Composite
423423
composite: |
424424
from example.pythonic import features
425-
class Composite(BaseComposite):
425+
class FetureComposite(BaseComposite):
426426
def compose(self):
427427
anything = features.anything()
428428
...
@@ -464,7 +464,7 @@ kind: Function
464464
metadata:
465465
name: function-pythonic
466466
spec:
467-
package: ghcr.io/fortra/function-pythonic:v0.0.7
467+
package: ghcr.io/fortra/function-pythonic:v0.0.9
468468
runtimeConfigRef:
469469
name: function-pythonic
470470
---
@@ -493,12 +493,6 @@ kind: ClusterRole
493493
metadata:
494494
name: function-pythonic
495495
rules:
496-
- apiGroups:
497-
- ''
498-
resources:
499-
- events
500-
verbs:
501-
- create
502496
- apiGroups:
503497
- ''
504498
resources:
@@ -507,6 +501,12 @@ rules:
507501
- list
508502
- watch
509503
- patch
504+
- apiGroups:
505+
- ''
506+
resources:
507+
- events
508+
verbs:
509+
- create
510510
---
511511
apiVersion: rbac.authorization.k8s.io/v1
512512
kind: ClusterRoleBinding

crossplane/pythonic/composite.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -637,15 +637,6 @@ def fatal(self, fatal):
637637
else:
638638
self._result.severity = fnv1.Severity.SEVERITY_NORMAL
639639

640-
@property
641-
def message(self):
642-
return self._result.message if bool(self) else None
643-
644-
@message.setter
645-
def message(self, message):
646-
if bool(self):
647-
self._result.message = message
648-
649640
@property
650641
def reason(self):
651642
return self._result.reason if bool(self) else None
@@ -655,6 +646,15 @@ def reason(self, reason):
655646
if bool(self):
656647
self._result.reason = reason
657648

649+
@property
650+
def message(self):
651+
return self._result.message if bool(self) else None
652+
653+
@message.setter
654+
def message(self, message):
655+
if bool(self):
656+
self._result.message = message
657+
658658
@property
659659
def claim(self):
660660
return bool(self) and self._result == fnv1.Target.TARGET_COMPOSITE_AND_CLAIM

crossplane/pythonic/function.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,18 @@ async def run_function(self, request):
8787
logger.exception('Exec exception')
8888
crossplane.function.response.fatal(response, f"Exec exception: {e}")
8989
return response
90-
composite = ['<script>', 'Composite']
90+
for field in dir(module):
91+
value = getattr(module, field)
92+
if inspect.isclass(value) and issubclass(value, BaseComposite):
93+
if clazz:
94+
logger.error('Composite script has multiple BaseComposite classes')
95+
crossplane.function.response.fatal(response, 'Composite script has multiple BaseComposite classes')
96+
return response
97+
clazz = value
98+
if not clazz:
99+
logger.error('Composite script does not have have a BaseComposite class')
100+
crossplane.function.response.fatal(response, 'Composite script does have have a BaseComposite class')
101+
return response
91102
else:
92103
composite = composite.rsplit('.', 1)
93104
if len(composite) == 1:
@@ -100,20 +111,20 @@ async def run_function(self, request):
100111
logger.error(str(e))
101112
crossplane.function.response.fatal(response, f"Import module exception: {e}")
102113
return response
103-
clazz = getattr(module, composite[1], None)
104-
if not clazz:
105-
logger.error(f"{composite[0]} did not define: {composite[1]}")
106-
crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
107-
return response
108-
composite = '.'.join(composite)
109-
if not inspect.isclass(clazz):
110-
logger.error(f"{composite} is not a class")
111-
crossplane.function.response.fatal(response, f"{composite} is not a class")
112-
return response
113-
if not issubclass(clazz, BaseComposite):
114-
logger.error(f"{composite} is not a subclass of BaseComposite")
115-
crossplane.function.response.fatal(response, f"{composite} is not a subclass of BaseComposite")
116-
return response
114+
clazz = getattr(module, composite[1], None)
115+
if not clazz:
116+
logger.error(f"{composite[0]} did not define: {composite[1]}")
117+
crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
118+
return response
119+
composite = '.'.join(composite)
120+
if not inspect.isclass(clazz):
121+
logger.error(f"{composite} is not a class")
122+
crossplane.function.response.fatal(response, f"{composite} is not a class")
123+
return response
124+
if not issubclass(clazz, BaseComposite):
125+
logger.error(f"{composite} is not a subclass of BaseComposite")
126+
crossplane.function.response.fatal(response, f"{composite} is not a subclass of BaseComposite")
127+
return response
117128
self.clazzes[composite] = clazz
118129

119130
try:

crossplane/pythonic/main.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import crossplane.function.logging
1414
import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1
1515
import grpc
16-
import pip._internal.cli.main
1716

1817
from . import function
1918

@@ -24,7 +23,7 @@ def main():
2423

2524
class Main:
2625
async def main(self):
27-
parser = argparse.ArgumentParser('Forta Crossplane Function')
26+
parser = argparse.ArgumentParser('Crossplane Function Pythonic')
2827
parser.add_argument(
2928
'--debug', '-d',
3029
action='store_true',
@@ -40,18 +39,18 @@ async def main(self):
4039
parser.add_argument(
4140
'--address',
4241
default='0.0.0.0:9443',
43-
help='Address at which to listen for gRPC connections, default: 0.0.0.0:9443',
42+
help='Address to listen on for gRPC connections, default: 0.0.0.0:9443',
4443
)
4544
parser.add_argument(
4645
'--tls-certs-dir',
4746
default=os.getenv('TLS_SERVER_CERTS_DIR'),
4847
metavar='DIRECTORY',
49-
help='Serve using mTLS certificates.',
48+
help='Serve using TLS certificates.',
5049
)
5150
parser.add_argument(
5251
'--insecure',
5352
action='store_true',
54-
help='Run without mTLS credentials. If you supply this flag --tls-certs-dir will be ignored.',
53+
help='Run without mTLS credentials, --tls-certs-dir will be ignored.',
5554
)
5655
parser.add_argument(
5756
'--packages',
@@ -94,15 +93,23 @@ async def main(self):
9493
help='Allow oversized protobuf messages'
9594
)
9695
args = parser.parse_args()
96+
if not args.tls_certs_dir and not args.insecure:
97+
print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr)
98+
sys.exit(1)
99+
97100
self.configure_logging(args)
101+
# enables read only volumes or mismatched uid volumes
102+
sys.dont_write_bytecode = True
103+
await self.run(args)
98104

105+
# Allow for independent running of function-pythonic
106+
async def run(self, args):
99107
if args.pip_install:
100-
pip._internal.cli.main.main(['install', *shlex.split(args.pip_install)])
108+
import pip._internal.cli.main
109+
pip._internal.cli.main.main(['install', '--user', *shlex.split(args.pip_install)])
101110

102-
# enables read only volumes or mismatched uid volumes
103-
sys.dont_write_bytecode = True
104111
for path in reversed(args.python_path):
105-
sys.path.insert(0, str(pathlib.Path(path).resolve()))
112+
sys.path.insert(0, str(pathlib.Path(path).expanduser().resolve()))
106113

107114
if args.allow_oversize_protos:
108115
from google.protobuf.internal import api_implementation
@@ -113,8 +120,10 @@ async def main(self):
113120
grpc_runner = function.FunctionRunner(args.debug)
114121
grpc_server = grpc.aio.server()
115122
grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
116-
if args.tls_certs_dir:
117-
certs = pathlib.Path(args.tls_certs_dir)
123+
if args.insecure:
124+
grpc_server.add_insecure_port(args.address)
125+
else:
126+
certs = pathlib.Path(args.tls_certs_dir).expanduser().resolve()
118127
grpc_server.add_secure_port(
119128
args.address,
120129
grpc.ssl_server_credentials(
@@ -126,10 +135,6 @@ async def main(self):
126135
require_client_auth=True,
127136
),
128137
)
129-
else:
130-
if not args.insecure:
131-
raise ValueError('Either --tls-certs-dir or --insecure must be specified')
132-
grpc_server.add_insecure_port(args.address)
133138
await grpc_server.start()
134139

135140
if args.packages:

crossplane/pythonic/packages.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
PACKAGES_DIR = None
1515

1616

17-
def operator(grpc_server, grpc_runner, packages_secrets, packages_namespace, packages_dir):
17+
def operator(grpc_server, grpc_runner, packages_secrets, packages_namespaces, packages_dir):
1818
logging.getLogger('kopf.objects').setLevel(logging.INFO)
1919
global GRPC_SERVER, GRPC_RUNNER, PACKAGES_DIR
2020
GRPC_SERVER = grpc_server
@@ -28,8 +28,8 @@ def operator(grpc_server, grpc_runner, packages_secrets, packages_namespace, pac
2828
kopf.on.delete('', 'v1', 'secrets', labels=PACKAGE_LABEL)(delete)
2929
return kopf.operator(
3030
standalone=True,
31-
clusterwide=not packages_namespace,
32-
namespaces=packages_namespace,
31+
clusterwide=not packages_namespaces,
32+
namespaces=packages_namespaces,
3333
)
3434

3535

0 commit comments

Comments
 (0)