crossplane-function-pythonic 0.0.10__tar.gz → 0.0.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/.gitignore +1 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/PKG-INFO +2 -2
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/README.md +1 -1
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/crossplane/pythonic/composite.py +53 -6
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/crossplane/pythonic/function.py +80 -75
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/crossplane/pythonic/main.py +6 -1
- crossplane_function_pythonic-0.0.11/crossplane/pythonic/packages.py +148 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/crossplane/pythonic/protobuf.py +67 -43
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/README.md +32 -0
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/aks/kubernetescluster.py +29 -0
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/aks/resourcegroup.py +14 -0
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/cluster-function-pythonic.yaml +63 -0
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/composition.yaml +24 -0
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/definition.yaml +43 -0
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/functions.yaml +10 -0
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/install.sh +13 -0
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/kustomization.yaml +20 -0
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/providers.yaml +14 -0
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/render.sh +9 -0
- crossplane_function_pythonic-0.0.11/examples/aks-cluster/xr.yaml +15 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/get-started-app/composition.yaml +2 -2
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/pyproject.toml +4 -2
- crossplane_function_pythonic-0.0.10/crossplane/pythonic/packages.py +0 -157
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/LICENSE +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/crossplane/pythonic/__init__.py +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/.dev/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/composition-v2.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/definition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/render-v2.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/filing-system/README.md +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/filing-system/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/filing-system/definition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/filing-system/function.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/filing-system/kustomization.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/filing-system/runtime.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/filing-system/vcluster.py +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/filing-system/vcluster.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/conditions/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/conditions/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/conditions/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/conditions/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/conditions/xrd.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/environmentConfigs.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/xrd.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/extra-resources/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/extra-resources/extraResources.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/extra-resources/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/extra-resources/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/extra-resources/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/fromYaml/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/fromYaml/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/fromYaml/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/fromYaml/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getComposedResource/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getComposedResource/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getComposedResource/observed.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getComposedResource/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getComposedResource/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getCompositeResource/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getCompositeResource/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getCompositeResource/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getCompositeResource/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getResourceCondition/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getResourceCondition/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getResourceCondition/observed.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getResourceCondition/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getResourceCondition/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/include/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/include/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/include/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/include/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/toYaml/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/toYaml/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/toYaml/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/toYaml/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/inline/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/inline/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/inline/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/inline/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/recursive/composition-real.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/recursive/composition-wrapper.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/recursive/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/recursive/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/recursive/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/get-started-app/definition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/get-started-app/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/get-started-app/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/get-started-app/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/composition.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/kustomization.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/run-function.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/vcluster.py +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/xr.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/xrd.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/single-purpose/functions.yaml +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/single-purpose/render.sh +0 -0
- {crossplane_function_pythonic-0.0.10 → crossplane_function_pythonic-0.0.11}/examples/single-purpose/xr.yaml +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: crossplane-function-pythonic
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.11
|
4
4
|
Summary: A Python centric Crossplane Function
|
5
5
|
Project-URL: Documentation, https://github.com/fortra/function-pythonic#readme
|
6
6
|
Project-URL: Issues, https://github.com/fortra/function-pythonic/issues
|
@@ -390,7 +390,7 @@ spec:
|
|
390
390
|
```
|
391
391
|
In one terminal session, run function-pythonic:
|
392
392
|
```shell
|
393
|
-
$ function-pythonic --insecure --debug
|
393
|
+
$ function-pythonic --insecure --debug --render-unknowns
|
394
394
|
[2025-08-21 15:32:37.966] grpc._cython.cygrpc [DEBUG ] Using AsyncIOEngine.POLLER as I/O engine
|
395
395
|
```
|
396
396
|
In another terminal session, render the Composite:
|
@@ -366,7 +366,7 @@ spec:
|
|
366
366
|
```
|
367
367
|
In one terminal session, run function-pythonic:
|
368
368
|
```shell
|
369
|
-
$ function-pythonic --insecure --debug
|
369
|
+
$ function-pythonic --insecure --debug --render-unknowns
|
370
370
|
[2025-08-21 15:32:37.966] grpc._cython.cygrpc [DEBUG ] Using AsyncIOEngine.POLLER as I/O engine
|
371
371
|
```
|
372
372
|
In another terminal session, render the Composite:
|
@@ -1,5 +1,6 @@
|
|
1
1
|
|
2
2
|
import datetime
|
3
|
+
from google.protobuf.duration_pb2 import Duration
|
3
4
|
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
|
4
5
|
|
5
6
|
from . import protobuf
|
@@ -9,8 +10,18 @@ _notset = object()
|
|
9
10
|
|
10
11
|
|
11
12
|
class BaseComposite:
|
12
|
-
def __init__(self, request,
|
13
|
+
def __init__(self, request, logger):
|
13
14
|
self.request = protobuf.Message(None, 'request', request.DESCRIPTOR, request, 'Function Request')
|
15
|
+
response = fnv1.RunFunctionResponse(
|
16
|
+
meta=fnv1.ResponseMeta(
|
17
|
+
tag=request.meta.tag,
|
18
|
+
ttl=Duration(
|
19
|
+
seconds=60,
|
20
|
+
),
|
21
|
+
),
|
22
|
+
desired=request.desired,
|
23
|
+
context=request.context,
|
24
|
+
)
|
14
25
|
self.response = protobuf.Message(None, 'response', response.DESCRIPTOR, response)
|
15
26
|
self.logger = logger
|
16
27
|
self.credentials = Credentials(self.request)
|
@@ -36,11 +47,23 @@ class BaseComposite:
|
|
36
47
|
|
37
48
|
@property
|
38
49
|
def ttl(self):
|
50
|
+
if self.response.meta.ttl.nanos:
|
51
|
+
return float(self.response.meta.ttl.seconds) + (float(self.response.meta.ttl.nanos) / 1000000000.0)
|
39
52
|
return self.response.meta.ttl.seconds
|
40
53
|
|
41
54
|
@ttl.setter
|
42
55
|
def ttl(self, ttl):
|
43
|
-
|
56
|
+
if isinstance(ttl, int):
|
57
|
+
self.response.meta.ttl.seconds = ttl
|
58
|
+
self.response.meta.ttl.nanos = 0
|
59
|
+
elif isinstance(ttl, float):
|
60
|
+
self.response.meta.ttl.seconds = int(ttl)
|
61
|
+
if ttl.is_integer():
|
62
|
+
self.response.meta.ttl.nanos = 0
|
63
|
+
else:
|
64
|
+
self.response.meta.ttl.nanos = int((ttl - self.response.meta.ttl.seconds) * 1000000000)
|
65
|
+
else:
|
66
|
+
raise ValueError('ttl must be an int or float')
|
44
67
|
|
45
68
|
@property
|
46
69
|
def ready(self):
|
@@ -73,22 +96,46 @@ class Credentials:
|
|
73
96
|
return self[key]
|
74
97
|
|
75
98
|
def __getitem__(self, key):
|
76
|
-
return self._request.credentials[key]
|
99
|
+
return Credential(self._request.credentials[key])
|
77
100
|
|
78
101
|
def __bool__(self):
|
79
|
-
return bool(_request.credentials)
|
102
|
+
return bool(self._request.credentials)
|
80
103
|
|
81
104
|
def __len__(self):
|
82
105
|
return len(self._request.credentials)
|
83
106
|
|
84
107
|
def __contains__(self, key):
|
85
|
-
return key in _request.credentials
|
108
|
+
return key in self._request.credentials
|
86
109
|
|
87
110
|
def __iter__(self):
|
88
111
|
for key, resource in self._request.credentials:
|
89
112
|
yield key, self[key]
|
90
113
|
|
91
114
|
|
115
|
+
class Credential:
|
116
|
+
def __init__(self, credential):
|
117
|
+
self.__dict__['_credential'] = credential
|
118
|
+
|
119
|
+
def __getattr__(self, key):
|
120
|
+
return self[key]
|
121
|
+
|
122
|
+
def __getitem__(self, key):
|
123
|
+
return self._credential.credential_data.data[key]
|
124
|
+
|
125
|
+
def __bool__(self):
|
126
|
+
return bool(self._credential.credential_data.data)
|
127
|
+
|
128
|
+
def __len__(self):
|
129
|
+
return len(self._credential.credential_data.data)
|
130
|
+
|
131
|
+
def __contains__(self, key):
|
132
|
+
return key in self._credential.credential_data.data
|
133
|
+
|
134
|
+
def __iter__(self):
|
135
|
+
for key, resource in self._credential.credential_data.data:
|
136
|
+
yield key, self[key]
|
137
|
+
|
138
|
+
|
92
139
|
class Resources:
|
93
140
|
def __init__(self, composite):
|
94
141
|
self.__dict__['_composite'] = composite
|
@@ -587,7 +634,7 @@ class Events:
|
|
587
634
|
def __getitem__(self, key):
|
588
635
|
if key >= len(self._results):
|
589
636
|
return Event()
|
590
|
-
return Event(self._results[
|
637
|
+
return Event(self._results[key])
|
591
638
|
|
592
639
|
def __iter__(self):
|
593
640
|
for ix in range(len(self._results)):
|
@@ -1,38 +1,26 @@
|
|
1
1
|
"""A Crossplane composition function."""
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
import base64
|
5
|
-
import builtins
|
6
4
|
import importlib
|
7
5
|
import inspect
|
8
6
|
import logging
|
9
7
|
import sys
|
10
8
|
|
11
9
|
import grpc
|
12
|
-
import crossplane.function.response
|
13
10
|
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
|
14
11
|
from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1
|
15
12
|
from .. import pythonic
|
16
13
|
|
17
|
-
builtins.BaseComposite = pythonic.BaseComposite
|
18
|
-
builtins.append = pythonic.append
|
19
|
-
builtins.Map = pythonic.Map
|
20
|
-
builtins.List = pythonic.List
|
21
|
-
builtins.Unknown = pythonic.Unknown
|
22
|
-
builtins.Yaml = pythonic.Yaml
|
23
|
-
builtins.Json = pythonic.Json
|
24
|
-
builtins.B64Encode = pythonic.B64Encode
|
25
|
-
builtins.B64Decode = pythonic.B64Decode
|
26
|
-
|
27
14
|
logger = logging.getLogger(__name__)
|
28
15
|
|
29
16
|
|
30
17
|
class FunctionRunner(grpcv1.FunctionRunnerService):
|
31
18
|
"""A FunctionRunner handles gRPC RunFunctionRequests."""
|
32
19
|
|
33
|
-
def __init__(self, debug=False):
|
20
|
+
def __init__(self, debug=False, renderUnknowns=False):
|
34
21
|
"""Create a new FunctionRunner."""
|
35
22
|
self.debug = debug
|
23
|
+
self.renderUnknowns = renderUnknowns
|
36
24
|
self.clazzes = {}
|
37
25
|
|
38
26
|
def invalidate_module(self, module):
|
@@ -46,9 +34,8 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
46
34
|
) -> fnv1.RunFunctionResponse:
|
47
35
|
try:
|
48
36
|
return await self.run_function(request)
|
49
|
-
except:
|
50
|
-
|
51
|
-
raise
|
37
|
+
except Exception as e:
|
38
|
+
return self.fatal(request, logger, 'RunFunction', e)
|
52
39
|
|
53
40
|
async def run_function(self, request):
|
54
41
|
composite = request.observed.composite.resource
|
@@ -56,27 +43,22 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
56
43
|
name.append(composite['kind'])
|
57
44
|
name.append(composite['metadata']['name'])
|
58
45
|
logger = logging.getLogger('.'.join(name))
|
59
|
-
if 'iteration' in request.context:
|
60
|
-
request.context['iteration'] = request.context['iteration'] + 1
|
61
|
-
else:
|
62
|
-
request.context['iteration'] = 1
|
63
|
-
logger.debug(f"Starting compose, {ordinal(request.context['iteration'])} pass")
|
64
|
-
|
65
|
-
response = crossplane.function.response.to(request)
|
66
46
|
|
67
47
|
if composite['apiVersion'] == 'pythonic.fortra.com/v1alpha1' and composite['kind'] == 'Composite':
|
68
|
-
if 'composite' not in composite['spec']:
|
69
|
-
|
70
|
-
crossplane.function.response.fatal(response, 'Missing spec "composite"')
|
71
|
-
return response
|
48
|
+
if 'spec' not in composite or 'composite' not in composite['spec']:
|
49
|
+
return self.fatal(request, logger, 'Missing spec "composite"')
|
72
50
|
composite = composite['spec']['composite']
|
73
51
|
else:
|
74
52
|
if 'composite' not in request.input:
|
75
|
-
|
76
|
-
crossplane.function.response.fatal(response, 'Missing input "composite"')
|
77
|
-
return response
|
53
|
+
return self.fatal(request, logger, 'Missing input "composite"')
|
78
54
|
composite = request.input['composite']
|
79
55
|
|
56
|
+
# Ideally this is something the Function API provides
|
57
|
+
if 'step' in request.input:
|
58
|
+
step = request.input['step']
|
59
|
+
else:
|
60
|
+
step = str(hash(composite))
|
61
|
+
|
80
62
|
clazz = self.clazzes.get(composite)
|
81
63
|
if not clazz:
|
82
64
|
if '\n' in composite:
|
@@ -84,81 +66,67 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
84
66
|
try:
|
85
67
|
exec(composite, module.__dict__)
|
86
68
|
except Exception as e:
|
87
|
-
|
88
|
-
crossplane.function.response.fatal(response, f"Exec exception: {e}")
|
89
|
-
return response
|
69
|
+
return self.fatal(request, logger, 'Exec', e)
|
90
70
|
for field in dir(module):
|
91
71
|
value = getattr(module, field)
|
92
|
-
if inspect.isclass(value) and issubclass(value, BaseComposite) and value != BaseComposite:
|
72
|
+
if inspect.isclass(value) and issubclass(value, pythonic.BaseComposite) and value != pythonic.BaseComposite:
|
93
73
|
if clazz:
|
94
|
-
|
95
|
-
crossplane.function.response.fatal(response, 'Composite script has multiple BaseComposite classes')
|
96
|
-
return response
|
74
|
+
return self.fatal(request, logger, 'Composite script has multiple BaseComposite classes')
|
97
75
|
clazz = value
|
98
76
|
if not clazz:
|
99
|
-
|
100
|
-
crossplane.function.response.fatal(response, 'Composite script does have have a BaseComposite class')
|
101
|
-
return response
|
77
|
+
return self.fatal(request, logger, 'Composite script does not have a BaseComposite class')
|
102
78
|
else:
|
103
79
|
composite = composite.rsplit('.', 1)
|
104
80
|
if len(composite) == 1:
|
105
|
-
|
106
|
-
crossplane.function.response.fatal(response, f"Composite class name does not include module: {composite[0]}")
|
107
|
-
return response
|
81
|
+
return self.fatal(request, logger, f"Composite class name does not include module: {composite[0]}")
|
108
82
|
try:
|
109
83
|
module = importlib.import_module(composite[0])
|
110
84
|
except Exception as e:
|
111
|
-
|
112
|
-
crossplane.function.response.fatal(response, f"Import module exception: {e}")
|
113
|
-
return response
|
85
|
+
return self.fatal(request, logger, 'Import module', e)
|
114
86
|
clazz = getattr(module, composite[1], None)
|
115
87
|
if not clazz:
|
116
|
-
|
117
|
-
crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
|
118
|
-
return response
|
88
|
+
return self.fatal(request, logger, f"{composite[0]} does not define: {composite[1]}")
|
119
89
|
composite = '.'.join(composite)
|
120
90
|
if not inspect.isclass(clazz):
|
121
|
-
|
122
|
-
|
123
|
-
return
|
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
|
91
|
+
return self.fatal(request, logger, f"{composite} is not a class")
|
92
|
+
if not issubclass(clazz, pythonic.BaseComposite):
|
93
|
+
return self.fatal(request, logger, f"{composite} is not a subclass of BaseComposite")
|
128
94
|
self.clazzes[composite] = clazz
|
129
95
|
|
130
96
|
try:
|
131
|
-
composite = clazz(request,
|
97
|
+
composite = clazz(request, logger)
|
132
98
|
except Exception as e:
|
133
|
-
|
134
|
-
|
135
|
-
|
99
|
+
return self.fatal(request, logger, 'Instantiate', e)
|
100
|
+
|
101
|
+
step = composite.context._pythonic[step]
|
102
|
+
iteration = (step.iteration or 0) + 1
|
103
|
+
step.iteration = iteration
|
104
|
+
composite.context.iteration = iteration
|
105
|
+
logger.debug(f"Starting compose, {ordinal(len(composite.context._pythonic))} step, {ordinal(iteration)} pass")
|
136
106
|
|
137
107
|
try:
|
138
108
|
result = composite.compose()
|
139
109
|
if asyncio.iscoroutine(result):
|
140
110
|
await result
|
141
111
|
except Exception as e:
|
142
|
-
|
143
|
-
crossplane.function.response.fatal(response, f"Compose exception: {e}")
|
144
|
-
return response
|
112
|
+
return self.fatal(request, logger, 'Compose', e)
|
145
113
|
|
146
114
|
requested = []
|
147
115
|
for name, required in composite.requireds:
|
148
116
|
if required.apiVersion and required.kind:
|
149
|
-
r = Map(apiVersion=required.apiVersion, kind=required.kind)
|
117
|
+
r = pythonic.Map(apiVersion=required.apiVersion, kind=required.kind)
|
150
118
|
if required.namespace:
|
151
119
|
r.namespace = required.namespace
|
152
120
|
if required.matchName:
|
153
121
|
r.matchName = required.matchName
|
154
122
|
for key, value in required.matchLabels:
|
155
123
|
r.matchLabels[key] = value
|
156
|
-
if r !=
|
157
|
-
|
124
|
+
if r != step.requireds[name]:
|
125
|
+
step.requireds[name] = r
|
158
126
|
requested.append(name)
|
159
127
|
if requested:
|
160
128
|
logger.info(f"Requireds requested: {','.join(requested)}")
|
161
|
-
return response
|
129
|
+
return composite.response._message
|
162
130
|
|
163
131
|
unknownResources = []
|
164
132
|
warningResources = []
|
@@ -187,6 +155,8 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
187
155
|
logger.debug(f'Desired unknown: {destination} = {source}')
|
188
156
|
if resource.observed:
|
189
157
|
resource.desired._patchUnknowns(resource.observed)
|
158
|
+
elif self.renderUnknowns:
|
159
|
+
resource.desired._renderUnknowns(self.trimFullName)
|
190
160
|
else:
|
191
161
|
del composite.resources[name]
|
192
162
|
|
@@ -227,7 +197,29 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
227
197
|
resource.ready = True
|
228
198
|
|
229
199
|
logger.info('Completed compose')
|
230
|
-
return response
|
200
|
+
return composite.response._message
|
201
|
+
|
202
|
+
def fatal(self, request, logger, message, exception=None):
|
203
|
+
if exception:
|
204
|
+
message += ' exceptiion'
|
205
|
+
logger.exception(message)
|
206
|
+
m = str(exception)
|
207
|
+
if not m:
|
208
|
+
m = exception.__class__.__name__
|
209
|
+
message += ': ' + m
|
210
|
+
else:
|
211
|
+
logger.error(message)
|
212
|
+
return fnv1.RunFunctionResponse(
|
213
|
+
meta=fnv1.ResponseMeta(
|
214
|
+
tag=request.meta.tag,
|
215
|
+
),
|
216
|
+
results=[
|
217
|
+
fnv1.Result(
|
218
|
+
severity=fnv1.SEVERITY_FATAL,
|
219
|
+
message=message,
|
220
|
+
)
|
221
|
+
]
|
222
|
+
)
|
231
223
|
|
232
224
|
def trimFullName(self, name):
|
233
225
|
name = name.split('.')
|
@@ -236,10 +228,15 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
236
228
|
('request', 'extra_resources', None, 'items', 'resource'),
|
237
229
|
('response', 'desired', 'resources', None, 'resource'),
|
238
230
|
):
|
239
|
-
if len(values)
|
240
|
-
|
241
|
-
|
242
|
-
|
231
|
+
if len(values) < len(name):
|
232
|
+
ix = 0
|
233
|
+
for iv, value in enumerate(values):
|
234
|
+
if value:
|
235
|
+
if value != name[ix]:
|
236
|
+
if not name[ix].startswith(f"{values[iv]}[") or iv+1 >= len(values) or values[iv+1]:
|
237
|
+
break
|
238
|
+
continue
|
239
|
+
ix += 1
|
243
240
|
else:
|
244
241
|
ix = 0
|
245
242
|
for value in values:
|
@@ -251,7 +248,6 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
251
248
|
del name[ix]
|
252
249
|
else:
|
253
250
|
name[ix] = name[ix][len(value):]
|
254
|
-
ix += 1
|
255
251
|
else:
|
256
252
|
ix += 1
|
257
253
|
break
|
@@ -268,4 +264,13 @@ def ordinal(ix):
|
|
268
264
|
|
269
265
|
|
270
266
|
class Module:
|
271
|
-
|
267
|
+
def __init__(self):
|
268
|
+
self.BaseComposite = pythonic.BaseComposite
|
269
|
+
self.append = pythonic.append
|
270
|
+
self.Map = pythonic.Map
|
271
|
+
self.List = pythonic.List
|
272
|
+
self.Unknown = pythonic.Unknown
|
273
|
+
self.Yaml = pythonic.Yaml
|
274
|
+
self.Json = pythonic.Json
|
275
|
+
self.B64Encode = pythonic.B64Encode
|
276
|
+
self.B64Decode = pythonic.B64Decode
|
@@ -92,6 +92,11 @@ class Main:
|
|
92
92
|
action='store_true',
|
93
93
|
help='Allow oversized protobuf messages'
|
94
94
|
)
|
95
|
+
parser.add_argument(
|
96
|
+
'--render-unknowns',
|
97
|
+
action='store_true',
|
98
|
+
help='Render resources with unknowns, useful during local develomment'
|
99
|
+
)
|
95
100
|
args = parser.parse_args()
|
96
101
|
if not args.tls_certs_dir and not args.insecure:
|
97
102
|
print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr)
|
@@ -117,7 +122,7 @@ class Main:
|
|
117
122
|
api_implementation._c_module.SetAllowOversizeProtos(True)
|
118
123
|
|
119
124
|
grpc.aio.init_grpc_aio()
|
120
|
-
grpc_runner = function.FunctionRunner(args.debug)
|
125
|
+
grpc_runner = function.FunctionRunner(args.debug, args.render_unknowns)
|
121
126
|
grpc_server = grpc.aio.server()
|
122
127
|
grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
|
123
128
|
if args.insecure:
|
@@ -0,0 +1,148 @@
|
|
1
|
+
|
2
|
+
import base64
|
3
|
+
import logging
|
4
|
+
import pathlib
|
5
|
+
import sys
|
6
|
+
|
7
|
+
import kopf
|
8
|
+
|
9
|
+
|
10
|
+
GRPC_SERVER = None
|
11
|
+
GRPC_RUNNER = None
|
12
|
+
PACKAGES_DIR = None
|
13
|
+
PACKAGE_LABEL = {'function-pythonic.package': kopf.PRESENT}
|
14
|
+
|
15
|
+
|
16
|
+
def operator(grpc_server, grpc_runner, packages_secrets, packages_namespaces, packages_dir):
|
17
|
+
logging.getLogger('kopf.objects').setLevel(logging.INFO)
|
18
|
+
global GRPC_SERVER, GRPC_RUNNER, PACKAGES_DIR
|
19
|
+
GRPC_SERVER = grpc_server
|
20
|
+
GRPC_RUNNER = grpc_runner
|
21
|
+
PACKAGES_DIR = pathlib.Path(packages_dir).expanduser().resolve()
|
22
|
+
sys.path.insert(0, str(PACKAGES_DIR))
|
23
|
+
if packages_secrets:
|
24
|
+
kopf.on.create('', 'v1', 'secrets', labels=PACKAGE_LABEL)(create)
|
25
|
+
kopf.on.resume('', 'v1', 'secrets', labels=PACKAGE_LABEL)(create)
|
26
|
+
kopf.on.update('', 'v1', 'secrets', labels=PACKAGE_LABEL)(update)
|
27
|
+
kopf.on.delete('', 'v1', 'secrets', labels=PACKAGE_LABEL)(delete)
|
28
|
+
return kopf.operator(
|
29
|
+
standalone=True,
|
30
|
+
clusterwide=not packages_namespaces,
|
31
|
+
namespaces=packages_namespaces,
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
@kopf.on.startup()
|
36
|
+
async def startup(settings, **_):
|
37
|
+
settings.scanning.disabled = True
|
38
|
+
|
39
|
+
|
40
|
+
@kopf.on.cleanup()
|
41
|
+
async def cleanup(**_):
|
42
|
+
await GRPC_SERVER.stop(5)
|
43
|
+
|
44
|
+
|
45
|
+
@kopf.on.create('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
|
46
|
+
@kopf.on.resume('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
|
47
|
+
async def create(body, logger, **_):
|
48
|
+
package_dir = get_package_dir(body, logger)
|
49
|
+
if package_dir:
|
50
|
+
secret = body['kind'] == 'Secret'
|
51
|
+
for name, text in body.get('data', {}).items():
|
52
|
+
package_file_write(package_dir, name, secret, text, 'Created', logger)
|
53
|
+
|
54
|
+
|
55
|
+
@kopf.on.update('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
|
56
|
+
async def update(body, old, logger, **_):
|
57
|
+
old_package_dir = get_package_dir(old)
|
58
|
+
if old_package_dir:
|
59
|
+
old_data = old.get('data', {})
|
60
|
+
else:
|
61
|
+
old_data = {}
|
62
|
+
old_names = set(old_data.keys())
|
63
|
+
package_dir = get_package_dir(body, logger)
|
64
|
+
if package_dir:
|
65
|
+
secret = body['kind'] == 'Secret'
|
66
|
+
for name, text in body.get('data', {}).items():
|
67
|
+
if package_dir == old_package_dir and text == old_data.get(name, None):
|
68
|
+
action = 'Unchanged'
|
69
|
+
else:
|
70
|
+
action = 'Updated' if package_dir == old_package_dir and name in old_names else 'Created'
|
71
|
+
package_file_write(package_dir, name, secret, text, action, logger)
|
72
|
+
if package_dir == old_package_dir:
|
73
|
+
old_names.discard(name)
|
74
|
+
if old_package_dir:
|
75
|
+
for name in old_names:
|
76
|
+
package_file_unlink(old_package_dir, name, 'Removed', logger)
|
77
|
+
|
78
|
+
|
79
|
+
@kopf.on.delete('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
|
80
|
+
async def delete(old, logger, **_):
|
81
|
+
package_dir = get_package_dir(old)
|
82
|
+
if package_dir:
|
83
|
+
for name in old.get('data', {}).keys():
|
84
|
+
package_file_unlink(package_dir, name, 'Deleted', logger)
|
85
|
+
|
86
|
+
|
87
|
+
def get_package_dir(body, logger=None):
|
88
|
+
package = body.get('metadata', {}).get('labels', {}).get('function-pythonic.package', None)
|
89
|
+
if package is None:
|
90
|
+
if logger:
|
91
|
+
logger.error('function-pythonic.package label is missing')
|
92
|
+
return None
|
93
|
+
package_dir = PACKAGES_DIR
|
94
|
+
if package:
|
95
|
+
for segment in package.split('.'):
|
96
|
+
if not segment.isidentifier():
|
97
|
+
if logger:
|
98
|
+
logger.error('Package has invalid package name: %s', package)
|
99
|
+
return None
|
100
|
+
package_dir = package_dir / segment
|
101
|
+
return package_dir
|
102
|
+
|
103
|
+
|
104
|
+
def package_file_write(package_dir, name, secret, text, action, logger):
|
105
|
+
package_file = package_dir / name
|
106
|
+
if action != 'Unchanged':
|
107
|
+
package_file.parent.mkdir(parents=True, exist_ok=True)
|
108
|
+
if secret:
|
109
|
+
package_file.write_bytes(base64.b64decode(text.encode('utf-8')))
|
110
|
+
else:
|
111
|
+
package_file.write_text(text)
|
112
|
+
module, name = package_file_name(package_file)
|
113
|
+
if module:
|
114
|
+
if action != 'Unchanged':
|
115
|
+
GRPC_RUNNER.invalidate_module(name)
|
116
|
+
logger.info(f"{action} module: {name}")
|
117
|
+
else:
|
118
|
+
logger.info(f"{action} file: {name}")
|
119
|
+
|
120
|
+
|
121
|
+
def package_file_unlink(package_dir, name, action, logger):
|
122
|
+
package_file = package_dir / name
|
123
|
+
package_file.unlink(missing_ok=True)
|
124
|
+
module, name = package_file_name(package_file)
|
125
|
+
if module:
|
126
|
+
GRPC_RUNNER.invalidate_module(name)
|
127
|
+
logger.info(f"{action} module: {name}")
|
128
|
+
else:
|
129
|
+
logger.info(f"{action} file: {name}")
|
130
|
+
package_dir = package_file.parent
|
131
|
+
while (
|
132
|
+
package_dir.is_relative_to(PACKAGES_DIR)
|
133
|
+
and package_dir.is_dir()
|
134
|
+
and not list(package_dir.iterdir())
|
135
|
+
):
|
136
|
+
package_dir.rmdir()
|
137
|
+
module = str(package_dir.relative_to(PACKAGES_DIR)).replace('/', '.')
|
138
|
+
if module != '.':
|
139
|
+
GRPC_RUNNER.invalidate_module(module)
|
140
|
+
logger.info(f"{action} package: {module}")
|
141
|
+
package_dir = package_dir.parent
|
142
|
+
|
143
|
+
|
144
|
+
def package_file_name(package_file):
|
145
|
+
name = str(package_file.relative_to(PACKAGES_DIR))
|
146
|
+
if name.endswith('.py'):
|
147
|
+
return True, name[:-3].replace('/', '.')
|
148
|
+
return False, name
|