crossplane-function-pythonic 0.4.0__py3-none-any.whl → 0.4.2__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/__about__.py +1 -1
- crossplane/pythonic/__init__.py +2 -1
- crossplane/pythonic/command.py +7 -1
- crossplane/pythonic/composite.py +6 -6
- crossplane/pythonic/function.py +5 -5
- crossplane/pythonic/grpc.py +1 -1
- crossplane/pythonic/protobuf.py +25 -4
- crossplane/pythonic/render.py +341 -331
- {crossplane_function_pythonic-0.4.0.dist-info → crossplane_function_pythonic-0.4.2.dist-info}/METADATA +6 -5
- crossplane_function_pythonic-0.4.2.dist-info/RECORD +18 -0
- crossplane_function_pythonic-0.4.0.dist-info/RECORD +0 -18
- {crossplane_function_pythonic-0.4.0.dist-info → crossplane_function_pythonic-0.4.2.dist-info}/WHEEL +0 -0
- {crossplane_function_pythonic-0.4.0.dist-info → crossplane_function_pythonic-0.4.2.dist-info}/entry_points.txt +0 -0
- {crossplane_function_pythonic-0.4.0.dist-info → crossplane_function_pythonic-0.4.2.dist-info}/licenses/LICENSE +0 -0
crossplane/pythonic/__about__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# This is set at build time, using "hatch version"
|
|
2
|
-
__version__ = "0.4.
|
|
2
|
+
__version__ = "0.4.2"
|
crossplane/pythonic/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
3
|
from .composite import BaseComposite
|
|
4
|
-
from .protobuf import append, Map, List, Unknown, Yaml, Json, B64Encode, B64Decode
|
|
4
|
+
from .protobuf import append, Map, List, Unknown, Yaml, YamlAll, Json, B64Encode, B64Decode
|
|
5
5
|
|
|
6
6
|
__all__ = [
|
|
7
7
|
'BaseComposite',
|
|
@@ -10,6 +10,7 @@ __all__ = [
|
|
|
10
10
|
'List',
|
|
11
11
|
'Unknown',
|
|
12
12
|
'Yaml',
|
|
13
|
+
'YamlAll',
|
|
13
14
|
'Json',
|
|
14
15
|
'B64Encode',
|
|
15
16
|
'B64Decode',
|
crossplane/pythonic/command.py
CHANGED
|
@@ -63,7 +63,7 @@ class Command:
|
|
|
63
63
|
help='Enable Crossplane V1 compatibility mode',
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
-
def __init__(self, args):
|
|
66
|
+
def __init__(self, args=None):
|
|
67
67
|
self.args = args
|
|
68
68
|
self.initialize()
|
|
69
69
|
|
|
@@ -77,6 +77,12 @@ class Command:
|
|
|
77
77
|
logger = logging.getLogger()
|
|
78
78
|
logger.handlers = [handler]
|
|
79
79
|
logger.setLevel(logging.DEBUG if self.args.debug else logging.INFO)
|
|
80
|
+
# Suppress noisy libraries, these can be overriden using --logger-level
|
|
81
|
+
logging.getLogger('asyncio').setLevel(logging.INFO)
|
|
82
|
+
logging.getLogger('grpc').setLevel(logging.INFO)
|
|
83
|
+
logging.getLogger('httpcore').setLevel(logging.INFO)
|
|
84
|
+
logging.getLogger('httpx').setLevel(logging.WARNING)
|
|
85
|
+
logging.getLogger('kr8s').setLevel(logging.INFO)
|
|
80
86
|
for logger_level in self.args.logger_level:
|
|
81
87
|
for logger_level in logger_level.split(','):
|
|
82
88
|
logger_level = logger_level.split('=')
|
crossplane/pythonic/composite.py
CHANGED
|
@@ -43,7 +43,7 @@ class Connection:
|
|
|
43
43
|
|
|
44
44
|
def __set__(self, composite, values):
|
|
45
45
|
connection = self.__get__(composite)
|
|
46
|
-
|
|
46
|
+
connection()
|
|
47
47
|
for key, value in values:
|
|
48
48
|
connection[key] = value
|
|
49
49
|
|
|
@@ -384,7 +384,7 @@ class Resource:
|
|
|
384
384
|
field = resource.observed.metadata.name
|
|
385
385
|
else:
|
|
386
386
|
field = resource.status.notReady
|
|
387
|
-
self.metadata.annotations[f"
|
|
387
|
+
self.metadata.annotations[f"pythonic.dependency/{resource.name}"] = field
|
|
388
388
|
|
|
389
389
|
class Requireds:
|
|
390
390
|
def __init__(self, composite):
|
|
@@ -757,7 +757,7 @@ class Results:
|
|
|
757
757
|
return len(self) > 0
|
|
758
758
|
|
|
759
759
|
def __len__(self):
|
|
760
|
-
len(self._results)
|
|
760
|
+
return len(self._results)
|
|
761
761
|
|
|
762
762
|
def __getitem__(self, key):
|
|
763
763
|
if key >= len(self._results):
|
|
@@ -832,7 +832,7 @@ class Result:
|
|
|
832
832
|
|
|
833
833
|
@property
|
|
834
834
|
def claim(self):
|
|
835
|
-
return bool(self) and self._result == fnv1.Target.TARGET_COMPOSITE_AND_CLAIM
|
|
835
|
+
return bool(self) and self._result.target == fnv1.Target.TARGET_COMPOSITE_AND_CLAIM
|
|
836
836
|
|
|
837
837
|
@claim.setter
|
|
838
838
|
def claim(self, claim):
|
|
@@ -887,10 +887,10 @@ class _Connection:
|
|
|
887
887
|
|
|
888
888
|
def __call__(self, **kwargs):
|
|
889
889
|
self._composite.response.desired.composite.connection_details(**kwargs)
|
|
890
|
-
if self.
|
|
890
|
+
if self._composite.crossplane_v1:
|
|
891
891
|
return
|
|
892
892
|
del self._composite.resources[self._resource_name]
|
|
893
|
-
for key, value in kwargs:
|
|
893
|
+
for key, value in kwargs.items():
|
|
894
894
|
self[key] = value
|
|
895
895
|
|
|
896
896
|
def __setattr__(self, key, value):
|
crossplane/pythonic/function.py
CHANGED
|
@@ -17,9 +17,8 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
18
18
|
"""A FunctionRunner handles gRPC RunFunctionRequests."""
|
|
19
19
|
|
|
20
|
-
def __init__(self,
|
|
20
|
+
def __init__(self, renderUnknowns=False, crossplane_v1=False):
|
|
21
21
|
"""Create a new FunctionRunner."""
|
|
22
|
-
self.debug = debug
|
|
23
22
|
self.renderUnknowns = renderUnknowns
|
|
24
23
|
self.crossplane_v1 = crossplane_v1
|
|
25
24
|
self.clazzes = {}
|
|
@@ -182,7 +181,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
|
182
181
|
for _, resource in sorted(entry for entry in composite.resources):
|
|
183
182
|
dependencies = resource.desired._getDependencies
|
|
184
183
|
if dependencies:
|
|
185
|
-
if
|
|
184
|
+
if composite.logger.isEnabledFor(logging.DEBUG):
|
|
186
185
|
for destination, source in sorted(dependencies.items()):
|
|
187
186
|
destination = self.trimFullName(destination)
|
|
188
187
|
source = self.trimFullName(source)
|
|
@@ -278,7 +277,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
|
278
277
|
if resource.unknownsFatal or (resource.unknownsFatal is None and composite.unknownsFatal):
|
|
279
278
|
fatalResources.append(name)
|
|
280
279
|
fatal = True
|
|
281
|
-
if
|
|
280
|
+
if composite.logger.isEnabledFor(logging.DEBUG):
|
|
282
281
|
for destination, source in sorted(unknowns.items()):
|
|
283
282
|
destination = self.trimFullName(destination)
|
|
284
283
|
source = self.trimFullName(source)
|
|
@@ -319,7 +318,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
|
319
318
|
message = 'All resources are composed'
|
|
320
319
|
status = True
|
|
321
320
|
result = None
|
|
322
|
-
if not
|
|
321
|
+
if not composite.logger.isEnabledFor(logging.DEBUG) and level:
|
|
323
322
|
level(message)
|
|
324
323
|
composite.conditions.ResourcesComposed(reason, message, status)
|
|
325
324
|
if result:
|
|
@@ -378,6 +377,7 @@ class Module:
|
|
|
378
377
|
self.List = pythonic.List
|
|
379
378
|
self.Unknown = pythonic.Unknown
|
|
380
379
|
self.Yaml = pythonic.Yaml
|
|
380
|
+
self.YamlAll = pythonic.YamlAll
|
|
381
381
|
self.Json = pythonic.Json
|
|
382
382
|
self.B64Encode = pythonic.B64Encode
|
|
383
383
|
self.B64Decode = pythonic.B64Decode
|
crossplane/pythonic/grpc.py
CHANGED
|
@@ -88,7 +88,7 @@ class Command(command.Command):
|
|
|
88
88
|
|
|
89
89
|
async def run(self):
|
|
90
90
|
grpc.aio.init_grpc_aio()
|
|
91
|
-
grpc_runner = function.FunctionRunner(self.args.
|
|
91
|
+
grpc_runner = function.FunctionRunner(self.args.render_unknowns, self.args.crossplane_v1)
|
|
92
92
|
grpc_server = grpc.aio.server()
|
|
93
93
|
grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
|
|
94
94
|
if self.args.insecure:
|
crossplane/pythonic/protobuf.py
CHANGED
|
@@ -42,6 +42,13 @@ def Yaml(string, readOnly=None):
|
|
|
42
42
|
string = str(string)
|
|
43
43
|
return Value(None, None, yaml.safe_load(string), readOnly)
|
|
44
44
|
|
|
45
|
+
def YamlAll(string, readOnly=None):
|
|
46
|
+
if isinstance(string, (FieldMessage, Value)):
|
|
47
|
+
if not string:
|
|
48
|
+
return string
|
|
49
|
+
string = str(string)
|
|
50
|
+
return Value(None, None, [document for document in yaml.safe_load_all(string)], readOnly)
|
|
51
|
+
|
|
45
52
|
def Json(string, readOnly=None):
|
|
46
53
|
if isinstance(string, (FieldMessage, Value)):
|
|
47
54
|
if not string:
|
|
@@ -406,12 +413,26 @@ class RepeatedMessage:
|
|
|
406
413
|
|
|
407
414
|
def __getitem__(self, key):
|
|
408
415
|
key = self._validate_key(key)
|
|
409
|
-
if key
|
|
410
|
-
|
|
411
|
-
|
|
416
|
+
if key == append:
|
|
417
|
+
if self._messages is _Unknown:
|
|
418
|
+
key = 0
|
|
419
|
+
else:
|
|
420
|
+
key = len(self._messages)
|
|
412
421
|
value = _Unknown
|
|
413
422
|
else:
|
|
414
|
-
|
|
423
|
+
if key < 0:
|
|
424
|
+
if self._messages is _Unknown:
|
|
425
|
+
key = 0
|
|
426
|
+
else:
|
|
427
|
+
key = len(self._messages) + key
|
|
428
|
+
if key < 0:
|
|
429
|
+
key = 0
|
|
430
|
+
if key in self._cache:
|
|
431
|
+
return self._cache[key]
|
|
432
|
+
if self._messages is _Unknown or key >= len(self._messages):
|
|
433
|
+
value = _Unknown
|
|
434
|
+
else:
|
|
435
|
+
value = self._messages[key]
|
|
415
436
|
if value is None and self._field.has_default_value:
|
|
416
437
|
value = self._field.default_value
|
|
417
438
|
if self._field.type == self._field.TYPE_MESSAGE:
|
crossplane/pythonic/render.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
|
|
2
2
|
import asyncio
|
|
3
|
-
import kr8s.asyncio
|
|
4
3
|
import importlib
|
|
5
4
|
import inflect
|
|
6
5
|
import inspect
|
|
@@ -103,80 +102,268 @@ class Command(command.Command):
|
|
|
103
102
|
)
|
|
104
103
|
|
|
105
104
|
def initialize(self):
|
|
106
|
-
self.
|
|
105
|
+
if self.args:
|
|
106
|
+
self.initialize_function()
|
|
107
107
|
self.logger = logging.getLogger(__name__)
|
|
108
|
-
self.inflect = inflect.engine()
|
|
109
|
-
self.inflect.classical(all=False)
|
|
110
108
|
|
|
111
109
|
async def run(self):
|
|
112
110
|
if self.args.kube_context:
|
|
113
|
-
|
|
111
|
+
kapi = Kr8sApi(self.args.kube_context, self.logger)
|
|
112
|
+
else:
|
|
113
|
+
kapi = None
|
|
114
|
+
composite = await self.setup_composite(kapi)
|
|
115
|
+
observed = self.collect_resources(self.args.observed_resources)
|
|
116
|
+
composition = await self.setup_composition(composite, kapi)
|
|
117
|
+
resources = self.collect_resources(self.args.required_resources)
|
|
118
|
+
resources += self.collect_resources(self.args.secret_store)
|
|
119
|
+
resources.sort(key=lambda resource: str(resource.metadata.name))
|
|
120
|
+
context = self.setup_context()
|
|
121
|
+
|
|
122
|
+
render = await self.render(composite, observed, composition, resources, context, kapi, self.args.render_unknowns, self.args.crossplane_v1)
|
|
123
|
+
if not render:
|
|
124
|
+
sys.exit(1)
|
|
125
|
+
|
|
126
|
+
if self.args.include_full_xr:
|
|
127
|
+
render.composite.metadata = composite.metadata
|
|
128
|
+
del render.composite.metadata.managedFields
|
|
129
|
+
if composite.spec:
|
|
130
|
+
render.composite.spec = composite.spec
|
|
131
|
+
else:
|
|
132
|
+
if composite.metadata.namespace:
|
|
133
|
+
render.composite.metadata.namespace = composite.metadata.namespace
|
|
134
|
+
render.composite.metadata.name = composite.metadata.name
|
|
135
|
+
|
|
136
|
+
# Print the composite.
|
|
137
|
+
print('---')
|
|
138
|
+
print(str(render.composite), end='')
|
|
139
|
+
# Print Composite connection if requested.
|
|
140
|
+
if self.args.include_connection_xr:
|
|
141
|
+
print('---')
|
|
142
|
+
print(str(render.connection), end='')
|
|
143
|
+
# Print the composed resources.
|
|
144
|
+
for resource in sorted(render.resources, key=lambda resource: str(resource.metadata.annotations['crossplane.io/composition-resource-name'])):
|
|
145
|
+
print('---')
|
|
146
|
+
print(str(resource), end='')
|
|
147
|
+
# Print the results (AKA events) if requested.
|
|
148
|
+
if self.args.include_function_results:
|
|
149
|
+
for result in render.results:
|
|
150
|
+
print('---')
|
|
151
|
+
print(str(result), end='')
|
|
152
|
+
# Print the final context if requested.
|
|
153
|
+
if self.args.include_context:
|
|
154
|
+
print('---')
|
|
155
|
+
print(str(render.context), end='')
|
|
156
|
+
|
|
157
|
+
async def setup_composite(self, kapi=None):
|
|
158
|
+
# Obtain the Composite to render.
|
|
159
|
+
if self.args.composite.is_file():
|
|
160
|
+
return protobuf.Yaml(self.args.composite.read_text())
|
|
161
|
+
if not kapi:
|
|
162
|
+
print(f"Composite \"{self.args.composite}\" is not a file", file=sys.stderr)
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
composite = str(self.args.composite).split(':')
|
|
165
|
+
if len(composite) == 3:
|
|
166
|
+
namespace = None
|
|
167
|
+
elif len(composite) == 4:
|
|
168
|
+
if len(composite[2]):
|
|
169
|
+
namespace = composite[2]
|
|
170
|
+
else:
|
|
171
|
+
namespace = None
|
|
114
172
|
else:
|
|
115
|
-
self.
|
|
173
|
+
print(f"Composite \"{self.args.composite}\" is not kind:apiVersion:namespace:name", file=sys.stderr)
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
composite = await kapi.get(composite[0], composite[1], namespace, composite[-1])
|
|
176
|
+
if not composite:
|
|
177
|
+
print(f"Composite \"{self.args.composite}\" not found", file=sys.stderr)
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
return composite
|
|
180
|
+
|
|
181
|
+
async def setup_composition(self, composite, kapi=None):
|
|
182
|
+
# Obtain the Composition that will be used to render the Composite.
|
|
183
|
+
if not self.args.composition:
|
|
184
|
+
return None
|
|
185
|
+
if self.args.composition.is_file():
|
|
186
|
+
composition = self.args.composition.read_text()
|
|
187
|
+
if self.args.composition.suffix == '.py':
|
|
188
|
+
return self.create_composition(compsite, composition)
|
|
189
|
+
composition = protobuf.Yaml(composition)
|
|
190
|
+
if not len(composition.spec.pipeline):
|
|
191
|
+
print(f"Composition file does not contain any pipeline steps: {self.args.composition}", file=sys.stderr)
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
return composition
|
|
194
|
+
composition = str(self.args.composition).rsplit('.', 1)
|
|
195
|
+
if len(composition) == 1:
|
|
196
|
+
print(f"Composition class name does not include module: {self.args.composition}", file=sys.stderr)
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
try:
|
|
199
|
+
module = importlib.import_module(composition[0])
|
|
200
|
+
except Exception as e:
|
|
201
|
+
print(e)
|
|
202
|
+
print(f"Unable to import composition module: {composition[0]}", file=sys.stderr)
|
|
203
|
+
sys.exit(1)
|
|
204
|
+
clazz = getattr(module, composition[1], None)
|
|
205
|
+
if not clazz:
|
|
206
|
+
print(f"Composition class {composition[0]} does not define: {composition[1]}", file=sys.stderr)
|
|
207
|
+
sys.exit(1)
|
|
208
|
+
if not inspect.isclass(clazz):
|
|
209
|
+
print(f"Composition class {self.args.composition} is not a class", file=sys.stderr)
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
if not issubclass(clazz, composite.BaseComposite):
|
|
212
|
+
print(f"Composition class {self.args.composition} is not a subclass of BaseComposite", file=sys.stderr)
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
return self.create_composition(composite, self.args.composition)
|
|
215
|
+
|
|
216
|
+
def create_composition(self, composite, module=''):
|
|
217
|
+
composition = protobuf.Map()
|
|
218
|
+
composition.apiVersion = 'apiextensions.crossplane.io/v1'
|
|
219
|
+
composition.kind = 'Composition'
|
|
220
|
+
composition.metadata.name = 'function-pythonic-render'
|
|
221
|
+
composition.spec.compositeTypeRef.apiVersion = composite.apiVersion
|
|
222
|
+
composition.spec.compositeTypeRef.kind = composite.kind
|
|
223
|
+
composition.spec.mode = 'Pipeline'
|
|
224
|
+
composition.spec.pipeline[0].step = 'function-pythonic-render'
|
|
225
|
+
composition.spec.pipeline[0].functionRef.name = 'function-pythonic'
|
|
226
|
+
composition.spec.pipeline[0].input.apiVersion = 'pythonic.fn.crossplane.io/v1alpha1'
|
|
227
|
+
composition.spec.pipeline[0].input.kind = 'Composite'
|
|
228
|
+
composition.spec.pipeline[0].input.composite = str(module)
|
|
229
|
+
return composition
|
|
230
|
+
|
|
231
|
+
def collect_resources(self, entries):
|
|
232
|
+
resources = []
|
|
233
|
+
for entry in entries:
|
|
234
|
+
if entry.is_file():
|
|
235
|
+
for document in yaml.safe_load_all(entry.read_text()):
|
|
236
|
+
resources.append(protobuf.Value(None, None, document))
|
|
237
|
+
elif entry.is_dir():
|
|
238
|
+
for file in entry.iterdir():
|
|
239
|
+
if file.suffix in ('.yaml', '.yml'):
|
|
240
|
+
for document in yaml.safe_load_all(file.read_text()):
|
|
241
|
+
resources.append(protobuf.Value(None, None, document))
|
|
242
|
+
else:
|
|
243
|
+
print(f"Specified resource is not a file or a directory: {entry}", file=sys.stderr)
|
|
244
|
+
sys.exit(1)
|
|
245
|
+
return resources
|
|
116
246
|
|
|
117
|
-
|
|
118
|
-
|
|
247
|
+
def setup_context(self):
|
|
248
|
+
# Load the request context with any specified command line options.
|
|
249
|
+
context = protobuf.Map()
|
|
250
|
+
for entry in self.args.context_files:
|
|
251
|
+
key_path = entry.split('=', 1)
|
|
252
|
+
if len(key_path) != 2:
|
|
253
|
+
print(f"Invalid --context-files: {entry}", file=sys.stderr)
|
|
254
|
+
sys.exit(1)
|
|
255
|
+
path = pathlib.Path(key_path[1])
|
|
256
|
+
if not path.is_file():
|
|
257
|
+
print(f"Invalid --context-files {path} is not a file", file=sys.stderr)
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
context[key_path[0]] = protobuf.Yaml(path.read_text())
|
|
260
|
+
for entry in self.args.context_values:
|
|
261
|
+
key_value = entry.split('=', 1)
|
|
262
|
+
if len(key_value) != 2:
|
|
263
|
+
print(f"Invalid --context-values: {entry}", file=sys.stderr)
|
|
264
|
+
sys.exit(1)
|
|
265
|
+
context[key_value[0]] = protobuf.Yaml(key_value[1])
|
|
266
|
+
return context
|
|
267
|
+
|
|
268
|
+
async def render(self, composite, observed=[], composition=None, resources=[], context=None, kapi=None, render_unknowns=False, crossplane_v1=False):
|
|
269
|
+
# Create the request used when running Composition steps.
|
|
270
|
+
request = protobuf.Message(None, 'request', fnv1.RunFunctionRequest.DESCRIPTOR, fnv1.RunFunctionRequest())
|
|
271
|
+
if context is not None:
|
|
272
|
+
request.context = context
|
|
119
273
|
|
|
120
|
-
#
|
|
121
|
-
self.
|
|
122
|
-
|
|
123
|
-
|
|
274
|
+
# Establish the request observed composite.
|
|
275
|
+
await self.set_resource(composite, request.observed.composite, resources, kapi)
|
|
276
|
+
# Establish the manually configured observed resources.
|
|
277
|
+
if observed:
|
|
278
|
+
async with asyncio.TaskGroup() as group:
|
|
279
|
+
for resource in observed:
|
|
280
|
+
name = resource.metadata.annotations['crossplane.io/composition-resource-name']
|
|
281
|
+
if name:
|
|
282
|
+
group.create_task(self.set_resource(resource, request.observed.resources[name], resources, kapi))
|
|
283
|
+
if kapi:
|
|
284
|
+
refs = composite.spec.crossplane.resourceRefs
|
|
285
|
+
if not refs:
|
|
286
|
+
refs = composite.spec.resourceRefs
|
|
287
|
+
if refs:
|
|
288
|
+
async with asyncio.TaskGroup() as group:
|
|
289
|
+
for ref in refs:
|
|
290
|
+
group.create_task(self.get_composite_ref(composite, ref, request, resources, kapi))
|
|
291
|
+
|
|
292
|
+
if not composition:
|
|
293
|
+
if composite.apiVersion in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and composite.kind == 'Composite':
|
|
294
|
+
composition = self.create_composition(composite)
|
|
295
|
+
else:
|
|
296
|
+
if not kapi:
|
|
297
|
+
print('"composition" required', file=sys.stderr)
|
|
298
|
+
return None
|
|
299
|
+
revision = composite.spec.crossplane.compositionRevisionRef
|
|
300
|
+
if not revision.name:
|
|
301
|
+
# Crossplane V1 location
|
|
302
|
+
revision = composite.spec.compositionRevisionRef
|
|
303
|
+
if not revision.name:
|
|
304
|
+
print('Composite does not contain a CompositionRevision name', file=sys.stderr)
|
|
305
|
+
return None
|
|
306
|
+
composition = await kapi.get('CompositionRevision', 'apiextensions.crossplane.io/v1', None, revision.name)
|
|
307
|
+
if not composition:
|
|
308
|
+
print(f"Compositioin \"{revision.name}\" not found", file=sys.stderr)
|
|
309
|
+
return None
|
|
124
310
|
|
|
125
311
|
# These will hold the response conditions and results.
|
|
126
312
|
conditions = protobuf.List()
|
|
127
313
|
results = protobuf.List()
|
|
128
314
|
|
|
129
315
|
# Create a function-pythonic function runner used to run pipeline steps.
|
|
130
|
-
runner = function.FunctionRunner(
|
|
316
|
+
runner = function.FunctionRunner(render_unknowns, crossplane_v1)
|
|
131
317
|
fatal = False
|
|
132
318
|
|
|
133
319
|
# Process the composition pipeline steps.
|
|
134
|
-
for step in
|
|
320
|
+
for step in composition.spec.pipeline:
|
|
135
321
|
if step.functionRef.name != 'function-pythonic':
|
|
136
322
|
print(f"Only function-pythonic functions can be run: {step.functionRef.name}", file=sys.stderr)
|
|
137
|
-
|
|
323
|
+
return None
|
|
138
324
|
if not step.input.step:
|
|
139
325
|
step.input.step = step.step
|
|
140
|
-
|
|
326
|
+
request.input = step.input
|
|
141
327
|
|
|
142
328
|
# Supply step requested credentials.
|
|
143
|
-
|
|
329
|
+
request.credentials()
|
|
144
330
|
for credential in step.credentials:
|
|
145
331
|
if credential.source == 'Secret' and credential.secretRef:
|
|
146
332
|
namespace = credential.secretRef.namespace
|
|
147
333
|
name = credential.secretRef.name
|
|
148
334
|
if namespace and name:
|
|
149
|
-
for
|
|
150
|
-
if
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
335
|
+
for resource in resources:
|
|
336
|
+
if resource.kind == 'Secret' and resource.apiVersion == 'v1':
|
|
337
|
+
if resource.metadata.namespace == namespace and resource.metadata.name == name:
|
|
338
|
+
data = request.credentials[credential.name].credential_data.data
|
|
339
|
+
data()
|
|
340
|
+
for key, value in resource.data:
|
|
341
|
+
data[key] = protobuf.B64Decode(value)
|
|
342
|
+
break
|
|
156
343
|
else:
|
|
157
344
|
print(f"Step \"{step.step}\" secret not found: {namespace}/{name}", file=sys.stderr)
|
|
158
|
-
|
|
345
|
+
return None
|
|
159
346
|
|
|
160
347
|
# Track what extra/required resources have been processed.
|
|
161
348
|
requirements = protobuf.Message(None, 'requirements', fnv1.Requirements.DESCRIPTOR, fnv1.Requirements())
|
|
162
349
|
for _ in range(5):
|
|
163
350
|
# Fetch the step bootstrap resources specified.
|
|
164
|
-
|
|
351
|
+
request.required_resources()
|
|
165
352
|
for requirement in step.requirements.requiredResources:
|
|
166
|
-
await self.
|
|
353
|
+
await self.set_required(requirement.requirementName, requirement, request.required_resources, resources, kapi)
|
|
167
354
|
# Fetch the required resources requested.
|
|
168
355
|
for name, selector in requirements.resources:
|
|
169
|
-
await self.
|
|
356
|
+
await self.set_required(name, selector, request.required_resources, resources, kapi)
|
|
170
357
|
# Fetch the now deprecated extra resources requested.
|
|
171
|
-
|
|
358
|
+
request.extra_resources()
|
|
172
359
|
for name, selector in requirements.extra_resources:
|
|
173
|
-
await self.
|
|
360
|
+
await self.set_required(name, selector, request.extra_resources, resources, kapi)
|
|
174
361
|
# Run the step using the function-pythonic function runner.
|
|
175
362
|
response = protobuf.Message(
|
|
176
363
|
None,
|
|
177
364
|
'response',
|
|
178
365
|
fnv1.RunFunctionResponse.DESCRIPTOR,
|
|
179
|
-
await runner.RunFunction(
|
|
366
|
+
await runner.RunFunction(request._message, None),
|
|
180
367
|
)
|
|
181
368
|
# All done if there is a fatal result.
|
|
182
369
|
for result in response.results:
|
|
@@ -184,7 +371,7 @@ class Command(command.Command):
|
|
|
184
371
|
fatal = True
|
|
185
372
|
break
|
|
186
373
|
# Copy the response context to the request context to use in subsequent steps.
|
|
187
|
-
|
|
374
|
+
request.context = response.context
|
|
188
375
|
# Exit this loop if the function has not requested additional extra/required resources.
|
|
189
376
|
if response.requirements == requirements:
|
|
190
377
|
break
|
|
@@ -192,10 +379,10 @@ class Command(command.Command):
|
|
|
192
379
|
requirements = response.requirements
|
|
193
380
|
|
|
194
381
|
# Copy the response desired state to the request desired state to use in subsequent steps.
|
|
195
|
-
|
|
196
|
-
self.copy_resource(response.desired.composite,
|
|
382
|
+
request.desired.resources()
|
|
383
|
+
self.copy_resource(response.desired.composite, request.desired.composite)
|
|
197
384
|
for name, resource in response.desired.resources:
|
|
198
|
-
self.copy_resource(resource,
|
|
385
|
+
self.copy_resource(resource, request.desired.resources[name])
|
|
199
386
|
|
|
200
387
|
# Collect the step's returned conditions.
|
|
201
388
|
for condition in response.conditions:
|
|
@@ -220,10 +407,10 @@ class Command(command.Command):
|
|
|
220
407
|
# Collect and format all the returned desired composed resources.
|
|
221
408
|
resources = protobuf.List()
|
|
222
409
|
unready = protobuf.List()
|
|
223
|
-
prefix =
|
|
410
|
+
prefix = composite.metadata.labels['crossplane.io/composite']
|
|
224
411
|
if not prefix:
|
|
225
|
-
prefix =
|
|
226
|
-
for name, resource in
|
|
412
|
+
prefix = composite.metadata.name
|
|
413
|
+
for name, resource in request.desired.resources:
|
|
227
414
|
if resource.ready == fnv1.Ready.READY_TRUE:
|
|
228
415
|
ready = True
|
|
229
416
|
elif resource.ready == fnv1.Ready.READY_FALSE:
|
|
@@ -233,51 +420,42 @@ class Command(command.Command):
|
|
|
233
420
|
if not ready:
|
|
234
421
|
unready[protobuf.append] = name
|
|
235
422
|
resource = resource.resource
|
|
236
|
-
observed =
|
|
423
|
+
observed = request.observed.resources[name].resource
|
|
237
424
|
if observed:
|
|
238
425
|
for key in ('namespace', 'generateName', 'name'):
|
|
239
426
|
if observed.metadata[key]:
|
|
240
427
|
resource.metadata[key] = observed.metadata[key]
|
|
241
428
|
if not resource.metadata.name and not resource.metadata.generateName:
|
|
242
429
|
resource.metadata.generateName = f"{prefix}-"
|
|
243
|
-
if
|
|
244
|
-
resource.metadata.namespace =
|
|
430
|
+
if composite.metadata.namespace:
|
|
431
|
+
resource.metadata.namespace = composite.metadata.namespace
|
|
245
432
|
resource.metadata.annotations['crossplane.io/composition-resource-name'] = name
|
|
246
433
|
resource.metadata.labels['crossplane.io/composite'] = prefix
|
|
247
|
-
if
|
|
248
|
-
resource.metadata.labels['crossplane.io/claim-namespace'] =
|
|
249
|
-
resource.metadata.labels['crossplane.io/claim-name'] =
|
|
250
|
-
elif
|
|
251
|
-
resource.metadata.labels['crossplane.io/claim-namespace'] =
|
|
252
|
-
resource.metadata.labels['crossplane.io/claim-name'] =
|
|
434
|
+
if composite.metadata.labels['crossplane.io/claim-name'] and composite.metadata.labels['crossplane.io/claim-namespace']:
|
|
435
|
+
resource.metadata.labels['crossplane.io/claim-namespace'] = composite.metadata.labels['crossplane.io/claim-namespace']
|
|
436
|
+
resource.metadata.labels['crossplane.io/claim-name'] = composite.metadata.labels['crossplane.io/claim-name']
|
|
437
|
+
elif composite.spec.claimRef.namespace and composite.spec.claimRef.name:
|
|
438
|
+
resource.metadata.labels['crossplane.io/claim-namespace'] = composite.spec.claimRef.namespace
|
|
439
|
+
resource.metadata.labels['crossplane.io/claim-name'] = composite.spec.claimRef.name
|
|
253
440
|
resource.metadata.ownerReferences[0].controller = True
|
|
254
441
|
resource.metadata.ownerReferences[0].blockOwnerDeletion = True
|
|
255
|
-
resource.metadata.ownerReferences[0].apiVersion =
|
|
256
|
-
resource.metadata.ownerReferences[0].kind =
|
|
257
|
-
resource.metadata.ownerReferences[0].name =
|
|
442
|
+
resource.metadata.ownerReferences[0].apiVersion = composite.apiVersion
|
|
443
|
+
resource.metadata.ownerReferences[0].kind = composite.kind
|
|
444
|
+
resource.metadata.ownerReferences[0].name = composite.metadata.name
|
|
258
445
|
resource.metadata.ownerReferences[0].uid = ''
|
|
259
446
|
resource.ready = ready
|
|
260
447
|
resources[protobuf.append] = resource
|
|
261
448
|
|
|
262
449
|
# Format the returned desired composite
|
|
263
450
|
composite = protobuf.Map()
|
|
264
|
-
for name, value in
|
|
451
|
+
for name, value in request.desired.composite.resource:
|
|
265
452
|
composite[name] = value
|
|
266
|
-
composite.apiVersion =
|
|
267
|
-
composite.kind =
|
|
268
|
-
if self.args.include_full_xr:
|
|
269
|
-
composite.metadata = self.request.observed.composite.resource.metadata
|
|
270
|
-
del composite.metadata.managedFields
|
|
271
|
-
if self.request.observed.composite.resource.spec:
|
|
272
|
-
composite.spec = self.request.observed.composite.resource.spec
|
|
273
|
-
else:
|
|
274
|
-
if self.request.observed.composite.resource.metadata.namespace:
|
|
275
|
-
composite.metadata.namespace = self.request.observed.composite.resource.metadata.namespace
|
|
276
|
-
composite.metadata.name = self.request.observed.composite.resource.metadata.name
|
|
453
|
+
composite.apiVersion = request.observed.composite.resource.apiVersion
|
|
454
|
+
composite.kind = request.observed.composite.resource.kind
|
|
277
455
|
# Add in the composite's status.conditions.
|
|
278
|
-
if
|
|
456
|
+
if request.desired.composite.ready == fnv1.Ready.READY_FALSE:
|
|
279
457
|
condition = self.create_condition('Ready', False, 'Creating')
|
|
280
|
-
elif
|
|
458
|
+
elif request.desired.composite.ready == fnv1.Ready.READY_UNSPECIFIED and len(unready):
|
|
281
459
|
condition = self.create_condition('Ready', False, 'Creating', f"Unready resources: {','.join(str(name) for name in unready)}")
|
|
282
460
|
else:
|
|
283
461
|
condition = self.create_condition('Ready', True, 'Available')
|
|
@@ -285,264 +463,82 @@ class Command(command.Command):
|
|
|
285
463
|
for condition in conditions:
|
|
286
464
|
composite.status.conditions[protobuf.append] = condition
|
|
287
465
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
# Print the composed resources.
|
|
304
|
-
for resource in sorted(resources, key=lambda resource: str(resource.metadata.annotations['crossplane.io/composition-resource-name'])):
|
|
305
|
-
print('---')
|
|
306
|
-
print(str(resource), end='')
|
|
307
|
-
|
|
308
|
-
# Print the results (AKA events) if requested.
|
|
309
|
-
if self.args.include_function_results:
|
|
310
|
-
for result in results:
|
|
311
|
-
print('---')
|
|
312
|
-
print(str(result), end='')
|
|
313
|
-
|
|
314
|
-
# Print the final context if requested.
|
|
315
|
-
if self.args.include_context:
|
|
316
|
-
print('---')
|
|
317
|
-
print(
|
|
318
|
-
str(protobuf.Map(
|
|
319
|
-
apiVersion = 'render.crossplane.io/v1beta1',
|
|
320
|
-
kind = 'Context',
|
|
321
|
-
values = self.request.context,
|
|
322
|
-
)),
|
|
323
|
-
end='',
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
async def setup_composite(self):
|
|
327
|
-
# Obtain the Composite to render.
|
|
328
|
-
if self.args.composite.is_file():
|
|
329
|
-
self.composite = protobuf.Yaml(self.args.composite.read_text())
|
|
330
|
-
return
|
|
331
|
-
if not self.kube_context:
|
|
332
|
-
print(f"Composite \"{self.args.composite}\" is not a file", file=sys.stderr)
|
|
333
|
-
sys.exit(1)
|
|
334
|
-
composite = str(self.args.composite).split(':')
|
|
335
|
-
if len(composite) == 3:
|
|
336
|
-
namespace = None
|
|
337
|
-
elif len(composite) == 4:
|
|
338
|
-
if len(composite[2]):
|
|
339
|
-
namespace = composite[2]
|
|
340
|
-
else:
|
|
341
|
-
namespace = None
|
|
342
|
-
else:
|
|
343
|
-
print(f"Composite \"{self.args.composite}\" is not kind:apiVersion:namespace:name", file=sys.stderr)
|
|
344
|
-
sys.exit(1)
|
|
345
|
-
self.composite = await self.kube_get(composite[0], composite[1], namespace, composite[-1])
|
|
346
|
-
|
|
347
|
-
async def setup_composition(self):
|
|
348
|
-
# Obtain the Composition that will be used to render the Composite.
|
|
349
|
-
if self.composite.apiVersion in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and self.composite.kind == 'Composite':
|
|
350
|
-
if self.args.composition:
|
|
351
|
-
print('Composite type of "composite.pythonic.crossplane.io" does not use "composition" argument', file=sys.stderr)
|
|
352
|
-
sys.exit(1)
|
|
353
|
-
self.create_composition()
|
|
354
|
-
return
|
|
355
|
-
if not self.args.composition:
|
|
356
|
-
if not self.kube_context:
|
|
357
|
-
print('"composition" argument required', file=sys.stderr)
|
|
358
|
-
sys.exit(1)
|
|
359
|
-
if self.args.crossplane_v1:
|
|
360
|
-
revision = self.composite.spec.compositionRevisionRef
|
|
361
|
-
else:
|
|
362
|
-
revision = self.composite.spec.crossplane.compositionRevisionRef
|
|
363
|
-
if not revision.name:
|
|
364
|
-
print('Composite does not contain a CompositionRevision name', file=sys.stderr)
|
|
365
|
-
sys.exit(1)
|
|
366
|
-
self.composition = await self.kube_get('CompositionRevision', 'apiextensions.crossplane.io/v1', None, str(revision.name))
|
|
367
|
-
return
|
|
368
|
-
if self.args.composition.is_file():
|
|
369
|
-
composition = self.args.composition.read_text()
|
|
370
|
-
if self.args.composition.suffix == '.py':
|
|
371
|
-
self.create_composition(composition)
|
|
372
|
-
else:
|
|
373
|
-
self.composition = protobuf.Yaml(composition)
|
|
374
|
-
if not len(self.composition.spec.pipeline):
|
|
375
|
-
print(f"Composition file does not contain any pipeline steps: {self.args.composition}", file=sys.stderr)
|
|
376
|
-
sys.exit(1)
|
|
377
|
-
return
|
|
378
|
-
composition = str(self.args.composition).rsplit('.', 1)
|
|
379
|
-
if len(composition) == 1:
|
|
380
|
-
print(f"Composition class name does not include module: {self.args.composition}", file=sys.stderr)
|
|
381
|
-
sys.exit(1)
|
|
382
|
-
try:
|
|
383
|
-
module = importlib.import_module(composition[0])
|
|
384
|
-
except Exception as e:
|
|
385
|
-
print(e)
|
|
386
|
-
print(f"Unable to import composition module: {composition[0]}", file=sys.stderr)
|
|
387
|
-
sys.exit(1)
|
|
388
|
-
clazz = getattr(module, composition[1], None)
|
|
389
|
-
if not clazz:
|
|
390
|
-
print(f"Composition class {composition[0]} does not define: {composition[1]}", file=sys.stderr)
|
|
391
|
-
sys.exit(1)
|
|
392
|
-
if not inspect.isclass(clazz):
|
|
393
|
-
print(f"Composition class {self.args.composition} is not a class", file=sys.stderr)
|
|
394
|
-
sys.exit(1)
|
|
395
|
-
if not issubclass(clazz, composite.BaseComposite):
|
|
396
|
-
print(f"Composition class {self.args.composition} is not a subclass of BaseComposite", file=sys.stderr)
|
|
397
|
-
sys.exit(1)
|
|
398
|
-
self.create_composition(self.args.composition)
|
|
399
|
-
|
|
400
|
-
def setup_local_resources(self):
|
|
401
|
-
# Load the request context with any specified command line options.
|
|
402
|
-
for entry in self.args.context_files:
|
|
403
|
-
key_path = entry.split('=', 1)
|
|
404
|
-
if len(key_path) != 2:
|
|
405
|
-
print(f"Invalid --context-files: {entry}", file=sys.stderr)
|
|
406
|
-
sys.exit(1)
|
|
407
|
-
path = pathlib.Path(key_path[1])
|
|
408
|
-
if not path.is_file():
|
|
409
|
-
print(f"Invalid --context-files {path} is not a file", file=sys.stderr)
|
|
410
|
-
sys.exit(1)
|
|
411
|
-
self.request.context[key_path[0]] = protobuf.Yaml(path.read_text())
|
|
412
|
-
for entry in self.args.context_values:
|
|
413
|
-
key_value = entry.split('=', 1)
|
|
414
|
-
if len(key_value) != 2:
|
|
415
|
-
print(f"Invalid --context-values: {entry}", file=sys.stderr)
|
|
416
|
-
sys.exit(1)
|
|
417
|
-
self.request.context[key_value[0]] = protobuf.Yaml(key_value[1])
|
|
418
|
-
# Collect specified required/extra resources. Sort for stable order when processed.
|
|
419
|
-
self.requireds = sorted(
|
|
420
|
-
self.collect_resources(self.args.required_resources),
|
|
421
|
-
key=lambda required: str(required.metadata.name),
|
|
466
|
+
return protobuf.Map(
|
|
467
|
+
composite=composite,
|
|
468
|
+
connection=protobuf.Map(
|
|
469
|
+
apiVersion='render.crossplane.io/v1beta1',
|
|
470
|
+
kind='Connection',
|
|
471
|
+
values={key: value for key, value in request.desired.composite.connection_details}
|
|
472
|
+
),
|
|
473
|
+
resources=resources,
|
|
474
|
+
results=results,
|
|
475
|
+
context=protobuf.Map(
|
|
476
|
+
apiVersion='render.crossplane.io/v1beta1',
|
|
477
|
+
kind='Context',
|
|
478
|
+
values=request.context,
|
|
479
|
+
),
|
|
422
480
|
)
|
|
423
|
-
# Collect specified connection and credential secrets.
|
|
424
|
-
self.secrets = [
|
|
425
|
-
secret
|
|
426
|
-
for secret in self.collect_resources(self.args.secret_store)
|
|
427
|
-
if secret.apiVersion == 'v1' and secret.kind == 'Secret'
|
|
428
|
-
]
|
|
429
|
-
|
|
430
|
-
async def setup_observed_resources(self):
|
|
431
|
-
# Establish the request observed composite.
|
|
432
|
-
await self.setup_resource(self.composite, self.request.observed.composite)
|
|
433
481
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
for ref in refs:
|
|
442
|
-
group.create_task(self.setup_observed_resource(ref))
|
|
443
|
-
|
|
444
|
-
# Establish the manually configured observed resources.
|
|
445
|
-
for resource in self.collect_resources(self.args.observed_resources):
|
|
446
|
-
name = resource.metadata.annotations['crossplane.io/composition-resource-name']
|
|
447
|
-
if name:
|
|
448
|
-
await self.setup_resource(resource, self.request.observed.resources[name])
|
|
449
|
-
|
|
450
|
-
async def setup_observed_resource(self, ref):
|
|
451
|
-
if ref.namespace:
|
|
452
|
-
namespace = str(ref.namespace)
|
|
453
|
-
elif self.composite.metadata.namespace:
|
|
454
|
-
namespace = str(self.composite.metadata.namespace)
|
|
455
|
-
else:
|
|
456
|
-
namespace = None
|
|
457
|
-
source = await self.kube_get(
|
|
458
|
-
str(ref.kind),
|
|
459
|
-
str(ref.apiVersion),
|
|
460
|
-
namespace,
|
|
461
|
-
str(ref.name),
|
|
462
|
-
False,
|
|
463
|
-
)
|
|
482
|
+
async def get_composite_ref(self, composite, ref, request, resources, kapi):
|
|
483
|
+
namespace = ref.namespace
|
|
484
|
+
if not namespace:
|
|
485
|
+
namespace = composite.metadata.namespace
|
|
486
|
+
if not namespace:
|
|
487
|
+
namespace = None
|
|
488
|
+
source = await kapi.get(ref.kind, ref.apiVersion, namespace, ref.name)
|
|
464
489
|
if source:
|
|
465
490
|
name = source.metadata.annotations['crossplane.io/composition-resource-name']
|
|
466
491
|
if name:
|
|
467
|
-
|
|
468
|
-
if not
|
|
469
|
-
await self.
|
|
470
|
-
|
|
471
|
-
def create_composition(self, module=''):
|
|
472
|
-
self.composition = protobuf.Map()
|
|
473
|
-
self.composition.apiVersion = 'apiextensions.crossplane.io/v1'
|
|
474
|
-
self.composition.kind = 'Composition'
|
|
475
|
-
self.composition.metadata.name = 'function-pythonic-render'
|
|
476
|
-
self.composition.spec.compositeTypeRef.apiVersion = self.composite.apiVersion
|
|
477
|
-
self.composition.spec.compositeTypeRef.kind = self.composite.kind
|
|
478
|
-
self.composition.spec.mode = 'Pipeline'
|
|
479
|
-
self.composition.spec.pipeline[0].step = 'function-pythonic-render'
|
|
480
|
-
self.composition.spec.pipeline[0].functionRef.name = 'function-pythonic'
|
|
481
|
-
self.composition.spec.pipeline[0].input.apiVersion = 'pythonic.fn.crossplane.io/v1alpha1'
|
|
482
|
-
self.composition.spec.pipeline[0].input.kind = 'Composite'
|
|
483
|
-
self.composition.spec.pipeline[0].input.composite = str(module)
|
|
484
|
-
|
|
485
|
-
def collect_resources(self, resources):
|
|
486
|
-
files = []
|
|
487
|
-
for resource in resources:
|
|
488
|
-
if resource.is_file():
|
|
489
|
-
files.append(resource)
|
|
490
|
-
elif resource.is_dir():
|
|
491
|
-
for file in resource.iterdir():
|
|
492
|
-
if file.suffix in ('.yaml', '.yml'):
|
|
493
|
-
files.append(file)
|
|
494
|
-
else:
|
|
495
|
-
print(f"Specified resource is not a file or a directory: {resource}", file=sys.stderr)
|
|
496
|
-
sys.exit(1)
|
|
497
|
-
for file in files:
|
|
498
|
-
for document in yaml.safe_load_all(file.read_text()):
|
|
499
|
-
yield protobuf.Value(None, None, document)
|
|
500
|
-
|
|
501
|
-
async def setup_resource(self, source, resource):
|
|
502
|
-
resource.resource = source
|
|
503
|
-
namespace = source.spec.writeConnectionSecretToRef.namespace or source.metadata.namespace
|
|
504
|
-
name = source.spec.writeConnectionSecretToRef.name
|
|
505
|
-
if namespace and name:
|
|
506
|
-
connection = None
|
|
507
|
-
for secret in self.secrets:
|
|
508
|
-
if secret.metadata.namespace == namespace and secret.metadata.name == name:
|
|
509
|
-
connection = secret
|
|
510
|
-
break
|
|
511
|
-
else:
|
|
512
|
-
if self.kube_context:
|
|
513
|
-
connection = await self.kube_get('Secret', 'v1', namespace, name, False)
|
|
514
|
-
if connection:
|
|
515
|
-
resource.connection_details()
|
|
516
|
-
for key, value in connection.data:
|
|
517
|
-
resource.connection_details[key] = protobuf.B64Decode(value)
|
|
492
|
+
destination = request.observed.resources[name]
|
|
493
|
+
if not destination: # Do not override manual observed
|
|
494
|
+
await self.set_resource(source, destination, resources, kapi)
|
|
518
495
|
|
|
519
|
-
async def
|
|
496
|
+
async def set_required(self, name, selector, requireds, resources=[], kapi=None):
|
|
520
497
|
if not name:
|
|
521
498
|
return
|
|
522
499
|
name = str(name)
|
|
523
|
-
items =
|
|
500
|
+
items = requireds[name].items
|
|
524
501
|
items() # Force this to get created
|
|
525
|
-
for
|
|
526
|
-
if selector.api_version ==
|
|
527
|
-
if ((not selector.namespace and not
|
|
528
|
-
or (selector.namespace ==
|
|
502
|
+
for resource in resources:
|
|
503
|
+
if selector.api_version == resource.apiVersion and selector.kind == resource.kind:
|
|
504
|
+
if ((not len(selector.namespace) and not len(resource.metadata.namespace))
|
|
505
|
+
or (selector.namespace == resource.metadata.namespace)
|
|
529
506
|
):
|
|
530
|
-
if selector.match_name ==
|
|
531
|
-
await self.
|
|
507
|
+
if selector.match_name == resource.metadata.name:
|
|
508
|
+
await self.set_resource(resource, items[protobuf.append], resources, kapi)
|
|
532
509
|
elif selector.match_labels.labels:
|
|
533
510
|
for key, value in selector.match_labels.labels:
|
|
534
|
-
if value !=
|
|
511
|
+
if value != resource.metadata.labels[key]:
|
|
535
512
|
break
|
|
536
513
|
else:
|
|
537
|
-
await self.
|
|
538
|
-
if not len(items) and
|
|
539
|
-
if selector.match_name:
|
|
540
|
-
|
|
541
|
-
if
|
|
542
|
-
await self.
|
|
543
|
-
elif selector.match_labels.labels:
|
|
544
|
-
for
|
|
545
|
-
await self.
|
|
514
|
+
await self.set_resource(resource, items[protobuf.append], resources, kapi)
|
|
515
|
+
if not len(items) and kapi:
|
|
516
|
+
if len(selector.match_name):
|
|
517
|
+
resource = await kapi.get(selector.kind, selector.api_version, selector.namespace, selector.match_name)
|
|
518
|
+
if resource:
|
|
519
|
+
await self.set_resource(resource, items[protobuf.append], resources, kapi)
|
|
520
|
+
elif len(selector.match_labels.labels):
|
|
521
|
+
for resource in await kapi.list(selector.kind, selector.api_version, selector.namespace, selector.match_labels.labels):
|
|
522
|
+
await self.set_resource(resource, items[protobuf.append], resources, kapi)
|
|
523
|
+
|
|
524
|
+
async def set_resource(self, source, destination, resources=[], kapi=None):
|
|
525
|
+
destination.resource = source
|
|
526
|
+
namespace = source.spec.writeConnectionSecretToRef.namespace or source.metadata.namespace
|
|
527
|
+
name = source.spec.writeConnectionSecretToRef.name
|
|
528
|
+
if namespace and name:
|
|
529
|
+
connection = None
|
|
530
|
+
for resource in resources:
|
|
531
|
+
if resource.kind == 'Secret' and resource.apiVersion == 'v1':
|
|
532
|
+
if resource.metadata.namespace == namespace and resource.metadata.name == name:
|
|
533
|
+
connection = resource
|
|
534
|
+
break
|
|
535
|
+
else:
|
|
536
|
+
if kapi:
|
|
537
|
+
connection = await kapi.get('Secret', 'v1', namespace, name)
|
|
538
|
+
if connection:
|
|
539
|
+
destination.connection_details()
|
|
540
|
+
for key, value in connection.data:
|
|
541
|
+
destination.connection_details[key] = protobuf.B64Decode(value)
|
|
546
542
|
|
|
547
543
|
def copy_resource(self, source, destination):
|
|
548
544
|
destination.resource = source.resource
|
|
@@ -576,45 +572,50 @@ class Command(command.Command):
|
|
|
576
572
|
condition['message'] = message
|
|
577
573
|
return condition
|
|
578
574
|
|
|
579
|
-
def kube_clazz(self, kind, apiVersion, namespaced):
|
|
580
|
-
kind = str(kind)
|
|
581
|
-
apiVersion = str(apiVersion)
|
|
582
|
-
try:
|
|
583
|
-
return kr8s.asyncio.objects.get_class(kind, apiVersion, True)
|
|
584
|
-
except KeyError:
|
|
585
|
-
pass
|
|
586
|
-
return kr8s.asyncio.objects.new_class(kind, apiVersion, True, bool(namespaced) and len(namespaced), plural=self.inflect.plural_noun(kind))
|
|
587
575
|
|
|
588
|
-
|
|
589
|
-
|
|
576
|
+
class Kr8sApi:
|
|
577
|
+
def __init__(self, context=None, logger=None):
|
|
578
|
+
self.kr8s = importlib.import_module('kr8s')
|
|
579
|
+
self.inflect = inflect.engine()
|
|
580
|
+
self.inflect.classical(all=False)
|
|
581
|
+
self.context = context
|
|
582
|
+
self.logger = logger
|
|
583
|
+
self._api = None
|
|
584
|
+
|
|
585
|
+
async def api(self):
|
|
586
|
+
if not self._api:
|
|
587
|
+
self._api = await self.kr8s.asyncio.api(context=self.context)
|
|
588
|
+
return self._api
|
|
589
|
+
|
|
590
|
+
async def get(self, kind, apiVersion, namespace, name):
|
|
591
|
+
clazz = self._get_clazz(kind, apiVersion, namespace)
|
|
590
592
|
try:
|
|
591
593
|
fullName = [str(kind), str(apiVersion), str(name)]
|
|
592
594
|
if namespace and len(namespace):
|
|
593
595
|
fullName.insert(-1, str(namespace))
|
|
594
|
-
resource = await clazz.get(str(name), namespace=str(namespace), api=self.
|
|
596
|
+
resource = await clazz.get(str(name), namespace=str(namespace), api=await self.api())
|
|
595
597
|
else:
|
|
596
|
-
resource = await clazz.get(str(name), api=self.
|
|
598
|
+
resource = await clazz.get(str(name), api=await self.api())
|
|
597
599
|
resource = protobuf.Value(None, None, resource.raw)
|
|
598
600
|
result = 'found'
|
|
599
|
-
except kr8s.NotFoundError:
|
|
600
|
-
if required:
|
|
601
|
-
print(f"Resource not found: {':'.join(fullName)}", file=sys.stderr)
|
|
602
|
-
sys.exit(1)
|
|
601
|
+
except self.kr8s.NotFoundError:
|
|
603
602
|
resource = None
|
|
604
603
|
result = 'missing'
|
|
605
|
-
self.logger
|
|
604
|
+
if self.logger:
|
|
605
|
+
self.logger.debug(f"Resource {result}: {':'.join(fullName)}")
|
|
606
606
|
return resource
|
|
607
607
|
|
|
608
|
-
async def
|
|
609
|
-
clazz = self.
|
|
608
|
+
async def list(self, kind, apiVersion, namespace, labelSelector):
|
|
609
|
+
clazz = self._get_clazz(kind, apiVersion, namespace)
|
|
610
610
|
resources = [
|
|
611
611
|
protobuf.Value(None, None, resource.raw)
|
|
612
612
|
async for resource in clazz.list(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
613
|
+
namespace=str(namespace) if namespace and len(namespace) else None,
|
|
614
|
+
label_selector={
|
|
615
|
+
label: str(value)
|
|
616
|
+
for label, value in labelSelector
|
|
617
|
+
},
|
|
618
|
+
api=await self.api()
|
|
618
619
|
)
|
|
619
620
|
]
|
|
620
621
|
if self.logger.isEnabledFor(logging.DEBUG):
|
|
@@ -628,3 +629,12 @@ class Command(command.Command):
|
|
|
628
629
|
result = 'missing'
|
|
629
630
|
self.logger.debug(f"Resources {result}: {':'.join(fullName)}")
|
|
630
631
|
return resources
|
|
632
|
+
|
|
633
|
+
def _get_clazz(self, kind, apiVersion, namespaced):
|
|
634
|
+
kind = str(kind)
|
|
635
|
+
apiVersion = str(apiVersion)
|
|
636
|
+
try:
|
|
637
|
+
return self.kr8s.asyncio.objects.get_class(kind, apiVersion, True)
|
|
638
|
+
except KeyError:
|
|
639
|
+
pass
|
|
640
|
+
return self.kr8s.asyncio.objects.new_class(kind, apiVersion, True, bool(namespaced) and len(namespaced), plural=self.inflect.plural_noun(kind))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crossplane-function-pythonic
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: A Python centric Crossplane Function
|
|
5
5
|
Project-URL: Documentation, https://github.com/crossplane-contrib/function-pythonic#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/crossplane-contrib/function-pythonic/issues
|
|
@@ -19,7 +19,7 @@ Requires-Dist: inflect==7.5.0
|
|
|
19
19
|
Requires-Dist: kr8s==0.20.15
|
|
20
20
|
Requires-Dist: pyyaml==6.0.3
|
|
21
21
|
Provides-Extra: packages
|
|
22
|
-
Requires-Dist: kopf==1.
|
|
22
|
+
Requires-Dist: kopf==1.43.0; extra == 'packages'
|
|
23
23
|
Provides-Extra: pip-install
|
|
24
24
|
Requires-Dist: pip==26.0.1; extra == 'pip-install'
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
@@ -83,7 +83,7 @@ kind: Function
|
|
|
83
83
|
metadata:
|
|
84
84
|
name: function-pythonic
|
|
85
85
|
spec:
|
|
86
|
-
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.
|
|
86
|
+
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.2
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
### Crossplane V1
|
|
@@ -95,7 +95,7 @@ kind: Function
|
|
|
95
95
|
metadata:
|
|
96
96
|
name: function-pythonic
|
|
97
97
|
spec:
|
|
98
|
-
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.
|
|
98
|
+
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.2
|
|
99
99
|
runtimeConfigRef:
|
|
100
100
|
name: function-pythonic
|
|
101
101
|
--
|
|
@@ -241,6 +241,7 @@ The following functions are provided to create Protobuf structures:
|
|
|
241
241
|
| List | Create a new Protobuf list |
|
|
242
242
|
| Unknown | Create a new Protobuf unknown placeholder |
|
|
243
243
|
| Yaml | Create a new Protobuf structure from a yaml string |
|
|
244
|
+
| YamlAll | Create a new Protobuf list from a yaml string |
|
|
244
245
|
| Json | Create a new Protobuf structure from a json string |
|
|
245
246
|
| B64Encode | Encode a string into base 64 |
|
|
246
247
|
| B64Decode | Decode a string from base 64 |
|
|
@@ -616,7 +617,7 @@ kind: Function
|
|
|
616
617
|
metadata:
|
|
617
618
|
name: function-pythonic
|
|
618
619
|
spec:
|
|
619
|
-
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.
|
|
620
|
+
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.2
|
|
620
621
|
runtimeConfigRef:
|
|
621
622
|
name: function-pythonic
|
|
622
623
|
---
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
crossplane/pythonic/__about__.py,sha256=6nxRspkiSsLkBLqHh-X--3FLozkQy1xfr-6ZG0LORbs,73
|
|
2
|
+
crossplane/pythonic/__init__.py,sha256=cRk12kc18RLhc9s5dW6sQqZSCEp80fooRO5zxXqc1oA,292
|
|
3
|
+
crossplane/pythonic/__main__.py,sha256=6vYRlYDJtqFgLyiTamnl3htiNOtz8QlDl5WlIP98I8o,31
|
|
4
|
+
crossplane/pythonic/auto_ready.py,sha256=sPetUuJRhwZbg9muaDmbdqmtTIIUDmY4qoadoJA0EtQ,7201
|
|
5
|
+
crossplane/pythonic/command.py,sha256=aT58WBrhU_scaOGeqmsBfofIDnXyW1CQOpCktVGBj5s,4211
|
|
6
|
+
crossplane/pythonic/composite.py,sha256=f9SYjtXVqRx7vse7nQ06Ic6EnnfCYVtpoOmBhR9Jv6g,30451
|
|
7
|
+
crossplane/pythonic/function.py,sha256=uILb3XICcWi2JA5W1xuq37nFQA3_TqbdJefo0-gUUNI,18063
|
|
8
|
+
crossplane/pythonic/grpc.py,sha256=9ZQceboDju37NB6AhcUSWpBx_hZQ5W7uo7CZF6ynhfI,4451
|
|
9
|
+
crossplane/pythonic/main.py,sha256=ujUa_FYElQSGqnhZ-0NJrD3kSyYjfRbIp79FV2Yl7hs,599
|
|
10
|
+
crossplane/pythonic/packages.py,sha256=4TxyT6V79R0m4tJbC8R1gwU_vgHGLXKSBzeTTKd8xGo,5120
|
|
11
|
+
crossplane/pythonic/protobuf.py,sha256=YcKqH0Y4Vj63m6pUb1ptL4gIKcdNLU-WR8TaQ5eGW-M,52987
|
|
12
|
+
crossplane/pythonic/render.py,sha256=e_WQF5TlemfsZim2oGkTPAWuU6BDJztexJ9iBbuFQbo,30417
|
|
13
|
+
crossplane/pythonic/version.py,sha256=-RiB0p146ayqJj0SXfYxTNv49u9Fx9pPgm59Ji2blhc,214
|
|
14
|
+
crossplane_function_pythonic-0.4.2.dist-info/METADATA,sha256=KemrVxbVM4-B6I1P-Gq3hDdqE67sPMCRyfu9gjBY4_w,31561
|
|
15
|
+
crossplane_function_pythonic-0.4.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
crossplane_function_pythonic-0.4.2.dist-info/entry_points.txt,sha256=jJ4baywFDviB9WyAhyhNYF2VOCb6XtbRSjKf7bnBwhg,68
|
|
17
|
+
crossplane_function_pythonic-0.4.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
18
|
+
crossplane_function_pythonic-0.4.2.dist-info/RECORD,,
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
crossplane/pythonic/__about__.py,sha256=lDX3x8k47nyHPBKNc91376Yj6P16n0uGqBZGVhqsHLY,73
|
|
2
|
-
crossplane/pythonic/__init__.py,sha256=A9U4-azc4DjSsOnOnjQxCkoTzsZMRBb_AvqzR_Bd95A,268
|
|
3
|
-
crossplane/pythonic/__main__.py,sha256=6vYRlYDJtqFgLyiTamnl3htiNOtz8QlDl5WlIP98I8o,31
|
|
4
|
-
crossplane/pythonic/auto_ready.py,sha256=sPetUuJRhwZbg9muaDmbdqmtTIIUDmY4qoadoJA0EtQ,7201
|
|
5
|
-
crossplane/pythonic/command.py,sha256=KZxklRR3Z5iiAPxqUbQgaPgoXOIN5T8bcRev4ZUmrrY,3830
|
|
6
|
-
crossplane/pythonic/composite.py,sha256=ERwcyAkbX2AJi3cB1muyXH0zMrDreZcJBGEqmmsweIE,30408
|
|
7
|
-
crossplane/pythonic/function.py,sha256=BSQmgjGmKM9-2ryu1gLApV0Xf3_VwjmbuAQtjIK-gB0,17961
|
|
8
|
-
crossplane/pythonic/grpc.py,sha256=8hQZZsNrcbiCGfEdPWRrG9SYfTrmuPgMmAIHwWhCdt8,4468
|
|
9
|
-
crossplane/pythonic/main.py,sha256=ujUa_FYElQSGqnhZ-0NJrD3kSyYjfRbIp79FV2Yl7hs,599
|
|
10
|
-
crossplane/pythonic/packages.py,sha256=4TxyT6V79R0m4tJbC8R1gwU_vgHGLXKSBzeTTKd8xGo,5120
|
|
11
|
-
crossplane/pythonic/protobuf.py,sha256=qL3RMsOsuyUaRMHKYOlZEE_5E8BM9wl8K574-cR0agg,52271
|
|
12
|
-
crossplane/pythonic/render.py,sha256=K8pbiFrw76zsJVkwN3lgHcA9P8Mfsmj3jih2LlUinrc,29824
|
|
13
|
-
crossplane/pythonic/version.py,sha256=-RiB0p146ayqJj0SXfYxTNv49u9Fx9pPgm59Ji2blhc,214
|
|
14
|
-
crossplane_function_pythonic-0.4.0.dist-info/METADATA,sha256=I64Dq_5k4brXlGjOtjkswzuNeSlxP2H5Xx_1yn729WE,31501
|
|
15
|
-
crossplane_function_pythonic-0.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
-
crossplane_function_pythonic-0.4.0.dist-info/entry_points.txt,sha256=jJ4baywFDviB9WyAhyhNYF2VOCb6XtbRSjKf7bnBwhg,68
|
|
17
|
-
crossplane_function_pythonic-0.4.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
18
|
-
crossplane_function_pythonic-0.4.0.dist-info/RECORD,,
|
{crossplane_function_pythonic-0.4.0.dist-info → crossplane_function_pythonic-0.4.2.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|