crossplane-function-pythonic 0.0.10__py3-none-any.whl → 0.0.11__py3-none-any.whl
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/pythonic/composite.py +53 -6
- crossplane/pythonic/function.py +80 -75
- crossplane/pythonic/main.py +6 -1
- crossplane/pythonic/packages.py +61 -70
- crossplane/pythonic/protobuf.py +67 -43
- {crossplane_function_pythonic-0.0.10.dist-info → crossplane_function_pythonic-0.0.11.dist-info}/METADATA +2 -2
- crossplane_function_pythonic-0.0.11.dist-info/RECORD +11 -0
- crossplane_function_pythonic-0.0.10.dist-info/RECORD +0 -11
- {crossplane_function_pythonic-0.0.10.dist-info → crossplane_function_pythonic-0.0.11.dist-info}/WHEEL +0 -0
- {crossplane_function_pythonic-0.0.10.dist-info → crossplane_function_pythonic-0.0.11.dist-info}/entry_points.txt +0 -0
- {crossplane_function_pythonic-0.0.10.dist-info → crossplane_function_pythonic-0.0.11.dist-info}/licenses/LICENSE +0 -0
crossplane/pythonic/composite.py
CHANGED
@@ -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)):
|
crossplane/pythonic/function.py
CHANGED
@@ -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
|
crossplane/pythonic/main.py
CHANGED
@@ -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:
|
crossplane/pythonic/packages.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
|
2
2
|
import base64
|
3
|
-
import importlib
|
4
3
|
import logging
|
5
4
|
import pathlib
|
6
5
|
import sys
|
@@ -10,8 +9,8 @@ import kopf
|
|
10
9
|
|
11
10
|
GRPC_SERVER = None
|
12
11
|
GRPC_RUNNER = None
|
13
|
-
PACKAGE_LABEL = {'function-pythonic.package': kopf.PRESENT}
|
14
12
|
PACKAGES_DIR = None
|
13
|
+
PACKAGE_LABEL = {'function-pythonic.package': kopf.PRESENT}
|
15
14
|
|
16
15
|
|
17
16
|
def operator(grpc_server, grpc_runner, packages_secrets, packages_namespaces, packages_dir):
|
@@ -46,95 +45,43 @@ async def cleanup(**_):
|
|
46
45
|
@kopf.on.create('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
|
47
46
|
@kopf.on.resume('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
|
48
47
|
async def create(body, logger, **_):
|
49
|
-
package_dir
|
48
|
+
package_dir = get_package_dir(body, logger)
|
50
49
|
if package_dir:
|
51
|
-
package_dir.mkdir(parents=True, exist_ok=True)
|
52
50
|
secret = body['kind'] == 'Secret'
|
53
|
-
invalidate = False
|
54
51
|
for name, text in body.get('data', {}).items():
|
55
|
-
|
56
|
-
if secret:
|
57
|
-
package_file.write_bytes(base64.b64decode(text.encode('utf-8')))
|
58
|
-
else:
|
59
|
-
package_file.write_text(text)
|
60
|
-
if package_file.suffixes == ['.py']:
|
61
|
-
module = '.'.join(package + [package_file.stem])
|
62
|
-
GRPC_RUNNER.invalidate_module(module)
|
63
|
-
logger.info(f"Created module: {module}")
|
64
|
-
else:
|
65
|
-
logger.info(f"Created file: {'/'.join(package + [name])}")
|
52
|
+
package_file_write(package_dir, name, secret, text, 'Created', logger)
|
66
53
|
|
67
54
|
|
68
55
|
@kopf.on.update('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
|
69
56
|
async def update(body, old, logger, **_):
|
70
|
-
old_package_dir
|
57
|
+
old_package_dir = get_package_dir(old)
|
71
58
|
if old_package_dir:
|
72
59
|
old_data = old.get('data', {})
|
73
60
|
else:
|
74
61
|
old_data = {}
|
75
62
|
old_names = set(old_data.keys())
|
76
|
-
package_dir
|
63
|
+
package_dir = get_package_dir(body, logger)
|
77
64
|
if package_dir:
|
78
|
-
package_dir.mkdir(parents=True, exist_ok=True)
|
79
65
|
secret = body['kind'] == 'Secret'
|
80
66
|
for name, text in body.get('data', {}).items():
|
81
|
-
package_file = package_dir / name
|
82
67
|
if package_dir == old_package_dir and text == old_data.get(name, None):
|
83
68
|
action = 'Unchanged'
|
84
69
|
else:
|
85
|
-
if secret:
|
86
|
-
package_file.write_bytes(base64.b64decode(text.encode('utf-8')))
|
87
|
-
else:
|
88
|
-
package_file.write_text(text)
|
89
70
|
action = 'Updated' if package_dir == old_package_dir and name in old_names else 'Created'
|
90
|
-
|
91
|
-
module = '.'.join(package + [package_file.stem])
|
92
|
-
if action != 'Unchanged':
|
93
|
-
GRPC_RUNNER.invalidate_module(module)
|
94
|
-
logger.info(f"{action} module: {module}")
|
95
|
-
else:
|
96
|
-
logger.info(f"{action} file: {'/'.join(package + [name])}")
|
71
|
+
package_file_write(package_dir, name, secret, text, action, logger)
|
97
72
|
if package_dir == old_package_dir:
|
98
73
|
old_names.discard(name)
|
99
74
|
if old_package_dir:
|
100
75
|
for name in old_names:
|
101
|
-
|
102
|
-
package_file.unlink(missing_ok=True)
|
103
|
-
if package_file.suffixes == ['.py']:
|
104
|
-
module = '.'.join(old_package + [package_file.stem])
|
105
|
-
GRPC_RUNNER.invalidate_module(module)
|
106
|
-
logger.info(f"Removed module: {module}")
|
107
|
-
else:
|
108
|
-
logger.info(f"Removed file: {'/'.join(old_package + [name])}")
|
109
|
-
while old_package and old_package_dir.is_dir() and not list(old_package_dir.iterdir()):
|
110
|
-
old_package_dir.rmdir()
|
111
|
-
module = '.'.join(old_package)
|
112
|
-
GRPC_RUNNER.invalidate_module(module)
|
113
|
-
logger.info(f"Removed package: {module}")
|
114
|
-
old_package_dir = old_package_dir.parent
|
115
|
-
old_package.pop()
|
76
|
+
package_file_unlink(old_package_dir, name, 'Removed', logger)
|
116
77
|
|
117
78
|
|
118
79
|
@kopf.on.delete('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
|
119
80
|
async def delete(old, logger, **_):
|
120
|
-
package_dir
|
81
|
+
package_dir = get_package_dir(old)
|
121
82
|
if package_dir:
|
122
83
|
for name in old.get('data', {}).keys():
|
123
|
-
|
124
|
-
package_file.unlink(missing_ok=True)
|
125
|
-
if package_file.suffixes == ['.py']:
|
126
|
-
module = '.'.join(package + [package_file.stem])
|
127
|
-
GRPC_RUNNER.invalidate_module(module)
|
128
|
-
logger.info(f"Deleted module: {module}")
|
129
|
-
else:
|
130
|
-
logger.info(f"Deleted file: {'/'.join(package + [name])}")
|
131
|
-
while package and package_dir.is_dir() and not list(package_dir.iterdir()):
|
132
|
-
package_dir.rmdir()
|
133
|
-
module = '.'.join(package)
|
134
|
-
GRPC_RUNNER.invalidate_module(module)
|
135
|
-
logger.info(f"Deleted package: {module}")
|
136
|
-
package_dir = package_dir.parent
|
137
|
-
package.pop()
|
84
|
+
package_file_unlink(package_dir, name, 'Deleted', logger)
|
138
85
|
|
139
86
|
|
140
87
|
def get_package_dir(body, logger=None):
|
@@ -142,16 +89,60 @@ def get_package_dir(body, logger=None):
|
|
142
89
|
if package is None:
|
143
90
|
if logger:
|
144
91
|
logger.error('function-pythonic.package label is missing')
|
145
|
-
return None
|
92
|
+
return None
|
146
93
|
package_dir = PACKAGES_DIR
|
147
|
-
if package
|
148
|
-
|
149
|
-
else:
|
150
|
-
package = package.split('.')
|
151
|
-
for segment in package:
|
94
|
+
if package:
|
95
|
+
for segment in package.split('.'):
|
152
96
|
if not segment.isidentifier():
|
153
97
|
if logger:
|
154
98
|
logger.error('Package has invalid package name: %s', package)
|
155
|
-
return None
|
99
|
+
return None
|
156
100
|
package_dir = package_dir / segment
|
157
|
-
return package_dir
|
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
|
crossplane/pythonic/protobuf.py
CHANGED
@@ -83,16 +83,16 @@ class Message:
|
|
83
83
|
value = None
|
84
84
|
if value is None and field.has_default_value:
|
85
85
|
value = field.default_value
|
86
|
-
if field.
|
86
|
+
if field.label == field.LABEL_REPEATED:
|
87
|
+
if field.type == field.TYPE_MESSAGE and field.message_type.GetOptions().map_entry:
|
88
|
+
value = MapMessage(self, key, field.message_type.fields_by_name['value'], value, self._readOnly)
|
89
|
+
else:
|
90
|
+
value = RepeatedMessage(self, key, field, value, self._readOnly)
|
91
|
+
elif field.type == field.TYPE_MESSAGE:
|
87
92
|
if field.message_type.name == 'Struct':
|
88
93
|
value = Values(self, key, value, Values.Type.MAP, self._readOnly)
|
89
94
|
elif field.message_type.name == 'ListValue':
|
90
95
|
value = Values(self, key, value, Values.Type.LIST, self._readOnly)
|
91
|
-
elif field.label == field.LABEL_REPEATED:
|
92
|
-
if field.message_type.GetOptions().map_entry:
|
93
|
-
value = MapMessage(self, key, field.message_type, value, self._readOnly)
|
94
|
-
else:
|
95
|
-
value = RepeatedMessage(self, key, field.message_type, value, self._readOnly)
|
96
96
|
else:
|
97
97
|
value = Message(self, key, field.message_type, value, self._readOnly)
|
98
98
|
self._cache[key] = value
|
@@ -147,10 +147,10 @@ class Message:
|
|
147
147
|
else:
|
148
148
|
name = str(self._key)
|
149
149
|
if key is not None:
|
150
|
-
if key
|
151
|
-
name += f"
|
150
|
+
if '.' in key:
|
151
|
+
name += f"[{key}]"
|
152
152
|
else:
|
153
|
-
name += f"
|
153
|
+
name += f".{key}"
|
154
154
|
return name
|
155
155
|
if key is not None:
|
156
156
|
return str(key)
|
@@ -202,15 +202,15 @@ class Message:
|
|
202
202
|
if key not in self._descriptor.fields_by_name:
|
203
203
|
raise AttributeError(obj=self, name=key)
|
204
204
|
if self._message is not None:
|
205
|
-
|
205
|
+
self._message.ClearField(key)
|
206
206
|
self._cache.pop(key, None)
|
207
207
|
|
208
208
|
|
209
209
|
class MapMessage:
|
210
|
-
def __init__(self, parent, key,
|
210
|
+
def __init__(self, parent, key, field, messages, readOnly=False):
|
211
211
|
self.__dict__['_parent'] = parent
|
212
212
|
self.__dict__['_key'] = key
|
213
|
-
self.__dict__['_field'] =
|
213
|
+
self.__dict__['_field'] = field
|
214
214
|
self.__dict__['_messages'] = messages
|
215
215
|
self.__dict__['_readOnly'] = readOnly
|
216
216
|
self.__dict__['_cache'] = {}
|
@@ -232,11 +232,6 @@ class MapMessage:
|
|
232
232
|
value = Values(self, key, value, Values.Type.MAP, self._readOnly)
|
233
233
|
elif self._field.message_type.name == 'ListValue':
|
234
234
|
value = Values(self, key, value, Values.Type.LIST, self._readOnly)
|
235
|
-
elif self._field.label == self._field.LABEL_REPEATED:
|
236
|
-
if self._field.message_type.GetOptions().map_entry:
|
237
|
-
value = MapMessage(self, key, self._field.message_type, value, self._readOnly)
|
238
|
-
else:
|
239
|
-
value = RepeatedMessage(self, key, self._field.message_type, value, self._readOnly)
|
240
235
|
else:
|
241
236
|
value = Message(self, key, self._field.message_type, value, self._readOnly)
|
242
237
|
elif self._field.type == self._field.TYPE_BYTES and isinstance(value, bytes):
|
@@ -266,8 +261,11 @@ class MapMessage:
|
|
266
261
|
def __eq__(self, other):
|
267
262
|
if not isinstance(other, MapMessage):
|
268
263
|
return False
|
269
|
-
if self.
|
264
|
+
if self._field.type != other._field.type:
|
270
265
|
return False
|
266
|
+
if self._field.type == self._field.TYPE_MESSAGE:
|
267
|
+
if self._field.message_type.full_name != other._field.message_type.full_name:
|
268
|
+
return False
|
271
269
|
if self._messages is None:
|
272
270
|
return other._messages is None
|
273
271
|
elif other._messages is None:
|
@@ -294,10 +292,10 @@ class MapMessage:
|
|
294
292
|
else:
|
295
293
|
name = str(self._key)
|
296
294
|
if key is not None:
|
297
|
-
if key
|
298
|
-
name += f"
|
295
|
+
if '.' in key:
|
296
|
+
name += f"[{key}]"
|
299
297
|
else:
|
300
|
-
name += f"
|
298
|
+
name += f".{key}"
|
301
299
|
return name
|
302
300
|
if key is not None:
|
303
301
|
return str(key)
|
@@ -349,10 +347,10 @@ class MapMessage:
|
|
349
347
|
|
350
348
|
|
351
349
|
class RepeatedMessage:
|
352
|
-
def __init__(self, parent, key,
|
350
|
+
def __init__(self, parent, key, field, messages, readOnly=False):
|
353
351
|
self._parent = parent
|
354
352
|
self._key = key
|
355
|
-
self.
|
353
|
+
self._field = field
|
356
354
|
self._messages = messages
|
357
355
|
self._readOnly = readOnly
|
358
356
|
self._cache = {}
|
@@ -361,10 +359,20 @@ class RepeatedMessage:
|
|
361
359
|
if key in self._cache:
|
362
360
|
return self._cache[key]
|
363
361
|
if self._messages is None or key >= len(self._messages):
|
364
|
-
|
362
|
+
value = None
|
365
363
|
else:
|
366
|
-
|
367
|
-
value
|
364
|
+
value = self._messages[key]
|
365
|
+
if value is None and self._field.has_default_value:
|
366
|
+
value = self._field.default_value
|
367
|
+
if self._field.type == self._field.TYPE_MESSAGE:
|
368
|
+
if self._field.message_type.name == 'Struct':
|
369
|
+
value = Values(self, key, value, Values.Type.MAP, self._readOnly)
|
370
|
+
elif self._field.message_type.name == 'ListValue':
|
371
|
+
value = Values(self, key, value, Values.Type.LIST, self._readOnly)
|
372
|
+
else:
|
373
|
+
value = Message(self, key, self._field.message_type, value, self._readOnly)
|
374
|
+
elif self._field.type == self._field.TYPE_BYTES and isinstance(value, bytes):
|
375
|
+
value = value.decode('utf-8')
|
368
376
|
self._cache[key] = value
|
369
377
|
return value
|
370
378
|
|
@@ -394,8 +402,11 @@ class RepeatedMessage:
|
|
394
402
|
def __eq__(self, other):
|
395
403
|
if not isinstance(other, RepeatedMessage):
|
396
404
|
return False
|
397
|
-
if self.
|
405
|
+
if self._field.type != other._field.type:
|
398
406
|
return False
|
407
|
+
if self._field.type == self._field.TYPE_MESSAGE:
|
408
|
+
if self._field.message_type.full_name != other._field.message_type.full_name:
|
409
|
+
return False
|
399
410
|
if self._messages is None:
|
400
411
|
return other._messages is None
|
401
412
|
elif other._messages is None:
|
@@ -440,7 +451,7 @@ class RepeatedMessage:
|
|
440
451
|
raise ValueError(f"{self._readOnly} is read only")
|
441
452
|
if self._messages is None:
|
442
453
|
self.__dict__['_messages'] = self._parent._create_child(self._key)
|
443
|
-
self._messages.
|
454
|
+
self._messages.clear()
|
444
455
|
self._cache.clear()
|
445
456
|
for arg in args:
|
446
457
|
self.append(arg)
|
@@ -451,22 +462,23 @@ class RepeatedMessage:
|
|
451
462
|
raise ValueError(f"{self._readOnly} is read only")
|
452
463
|
if self._messages is None:
|
453
464
|
self._messages = self._parent._create_child(self._key)
|
454
|
-
if key
|
455
|
-
key = len(self._messages)
|
456
|
-
elif key < 0:
|
465
|
+
if key < 0:
|
457
466
|
key = len(self._messages) + key
|
458
|
-
while key >= len(self._messages):
|
459
|
-
self._messages.add()
|
460
467
|
if isinstance(message, Message):
|
461
468
|
message = message._message
|
462
|
-
self.
|
469
|
+
if self._field.type == self._field.TYPE_BYTES and isinstance(message, str):
|
470
|
+
message = message.encode('utf-8')
|
471
|
+
if key >= len(self._messages):
|
472
|
+
self._messages.append(message)
|
473
|
+
else:
|
474
|
+
self._messages[key] = message
|
463
475
|
self._cache.pop(key, None)
|
464
476
|
|
465
477
|
def __delitem__(self, key):
|
466
478
|
if self._readOnly:
|
467
479
|
raise ValueError(f"{self._readOnly} is read only")
|
468
|
-
if self.
|
469
|
-
del self.
|
480
|
+
if self._messages is not None:
|
481
|
+
del self._messages[key]
|
470
482
|
self._cache.pop(key, None)
|
471
483
|
|
472
484
|
def append(self, message=None):
|
@@ -513,7 +525,7 @@ class Values:
|
|
513
525
|
if isinstance(key, str):
|
514
526
|
if not self._isMap:
|
515
527
|
if not self._isUnknown:
|
516
|
-
raise ValueError(f"Invalid key, must be a
|
528
|
+
raise ValueError(f"Invalid key, must be a int for lists: {key}")
|
517
529
|
self.__dict__['_type'] = self.Type.MAP
|
518
530
|
if self._values is None or key not in self._values:
|
519
531
|
struct_value = None
|
@@ -522,7 +534,7 @@ class Values:
|
|
522
534
|
elif isinstance(key, int):
|
523
535
|
if not self._isList:
|
524
536
|
if not self._isUnknown:
|
525
|
-
raise ValueError(f"Invalid key, must be
|
537
|
+
raise ValueError(f"Invalid key, must be a str for maps: {key}")
|
526
538
|
self.__dict__['_type'] = self.Type.LIST
|
527
539
|
if self._values is None or key >= len(self._values):
|
528
540
|
struct_value = None
|
@@ -638,10 +650,10 @@ class Values:
|
|
638
650
|
if isinstance(key, int):
|
639
651
|
name += f"[{key}]"
|
640
652
|
else:
|
641
|
-
if key
|
642
|
-
name += f"
|
653
|
+
if '.' in key:
|
654
|
+
name += f"[{key}]"
|
643
655
|
else:
|
644
|
-
name += f"
|
656
|
+
name += f".{key}"
|
645
657
|
return name
|
646
658
|
if key is not None:
|
647
659
|
return str(key)
|
@@ -849,7 +861,7 @@ class Values:
|
|
849
861
|
return unknowns
|
850
862
|
|
851
863
|
def _patchUnknowns(self, patches):
|
852
|
-
for key in
|
864
|
+
for key in list(self._unknowns.keys()):
|
853
865
|
self[key] = patches[key]
|
854
866
|
if self._isMap:
|
855
867
|
for key, value in self:
|
@@ -864,6 +876,18 @@ class Values:
|
|
864
876
|
if isinstance(patch, Values) and patch._type == value._type and len(patch):
|
865
877
|
value._patchUnknowns(patch)
|
866
878
|
|
879
|
+
def _renderUnknowns(self, trimFullName):
|
880
|
+
for key, unknown in list(self._unknowns.items()):
|
881
|
+
self[key] = f"UNKNOWN:{trimFullName(unknown._fullName())}"
|
882
|
+
if self._isMap:
|
883
|
+
for key, value in self:
|
884
|
+
if isinstance(value, Values) and len(value):
|
885
|
+
value._renderUnknowns(trimFullName)
|
886
|
+
elif self._isList:
|
887
|
+
for ix, value in enumerate(self):
|
888
|
+
if isinstance(value, Values) and len(value):
|
889
|
+
value._renderUnknowns(trimFullName)
|
890
|
+
|
867
891
|
|
868
892
|
def _formatObject(object, spec):
|
869
893
|
if spec == 'json':
|
@@ -901,7 +925,7 @@ class _JSONEncoder(json.JSONEncoder):
|
|
901
925
|
return '<<UNEXPECTED>>'
|
902
926
|
if isinstance(object, datetime.datetime):
|
903
927
|
return object.isoformat()
|
904
|
-
return super(
|
928
|
+
return super(_JSONEncoder, self).default(object)
|
905
929
|
|
906
930
|
|
907
931
|
class _Dumper(yaml.SafeDumper):
|
@@ -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:
|
@@ -0,0 +1,11 @@
|
|
1
|
+
crossplane/pythonic/__init__.py,sha256=9Oz3mvFO-8GXb75iEfybSHgVr3p8INdqA201tCusuSo,408
|
2
|
+
crossplane/pythonic/composite.py,sha256=a_TVaz4ABRvsyhEEnMpZKFtSrH1mYWmqAn8IXGUtgrc,21519
|
3
|
+
crossplane/pythonic/function.py,sha256=8B-chQmuBSSbX3ptOIuE2Tz5KAZ7M5U4-q6wf0Qlwf0,11139
|
4
|
+
crossplane/pythonic/main.py,sha256=HILfW6WP-QvOiyfLLu41bAN_2hbkxuw-3DD8rEUMTPQ,6893
|
5
|
+
crossplane/pythonic/packages.py,sha256=4TxyT6V79R0m4tJbC8R1gwU_vgHGLXKSBzeTTKd8xGo,5120
|
6
|
+
crossplane/pythonic/protobuf.py,sha256=qkzhrXWsaqWHnciKiPavJNcq-qSLcFEoKw-tEJj2Ou4,35037
|
7
|
+
crossplane_function_pythonic-0.0.11.dist-info/METADATA,sha256=OKe1qRnvorxI0qVHWxmIJxnEHTPMxW9tK7mZ7H8Z6DA,23468
|
8
|
+
crossplane_function_pythonic-0.0.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
9
|
+
crossplane_function_pythonic-0.0.11.dist-info/entry_points.txt,sha256=jJ4baywFDviB9WyAhyhNYF2VOCb6XtbRSjKf7bnBwhg,68
|
10
|
+
crossplane_function_pythonic-0.0.11.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
11
|
+
crossplane_function_pythonic-0.0.11.dist-info/RECORD,,
|
@@ -1,11 +0,0 @@
|
|
1
|
-
crossplane/pythonic/__init__.py,sha256=9Oz3mvFO-8GXb75iEfybSHgVr3p8INdqA201tCusuSo,408
|
2
|
-
crossplane/pythonic/composite.py,sha256=TxloK31jx3xDmLnalntPXCHNnxuudevdeqPzKcSBO_I,19937
|
3
|
-
crossplane/pythonic/function.py,sha256=jTOqoOri8ulpnCa-ndjP4bKxGPW4J1_a9EjWRoAmQPU,11363
|
4
|
-
crossplane/pythonic/main.py,sha256=kcpoR4F84IhxLzaPSWWdIoaXmrUyjXofvwQuenVPHSE,6683
|
5
|
-
crossplane/pythonic/packages.py,sha256=quxAkmioIGJr9g4uRHsqPwhzyu2f2_UyNHHQmZjSJ8A,6108
|
6
|
-
crossplane/pythonic/protobuf.py,sha256=ULcaqeyeqCaz0SSSZXNpeUPh1EQLdAV09Dwj3ltIx7k,33899
|
7
|
-
crossplane_function_pythonic-0.0.10.dist-info/METADATA,sha256=P1pD57y-60DtKcKiOMMll8Y8ZX6hzoTKBvc960Tl4fg,23450
|
8
|
-
crossplane_function_pythonic-0.0.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
9
|
-
crossplane_function_pythonic-0.0.10.dist-info/entry_points.txt,sha256=jJ4baywFDviB9WyAhyhNYF2VOCb6XtbRSjKf7bnBwhg,68
|
10
|
-
crossplane_function_pythonic-0.0.10.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
11
|
-
crossplane_function_pythonic-0.0.10.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|