crossplane-function-pythonic 0.6.1__py3-none-any.whl → 0.6.3__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.
@@ -1,13 +1,13 @@
1
-
2
1
  import asyncio
3
2
  import importlib
4
- import inflect
5
3
  import inspect
6
4
  import json
7
- import kr8s
8
5
  import logging
9
6
  import pathlib
10
7
  import sys
8
+
9
+ import inflect
10
+ import kr8s
11
11
  import yaml
12
12
 
13
13
  from . import (
@@ -18,93 +18,99 @@ from . import (
18
18
  from .composite import BaseComposite
19
19
  from .proto.v1 import run_function_pb2 as fnv1
20
20
 
21
-
22
21
  INFLECT = inflect.engine()
23
22
  INFLECT.classical(all=False)
24
23
 
25
24
 
26
25
  class Command(command.Command):
27
- name = 'render'
28
- help = 'Render a function-pythonic Composition'
26
+ name = "render"
27
+ help = "Render a function-pythonic Composition"
29
28
 
30
29
  @classmethod
31
30
  def add_parser_arguments(cls, parser):
32
31
  cls.add_function_arguments(parser)
33
32
  parser.add_argument(
34
- 'composite',
33
+ "composite",
35
34
  type=pathlib.Path,
36
- metavar='COMPOSITE',
37
- help='A YAML file containing the Composite resource to render, or kind:apiVersion:namespace:name of cluster Composite.',
35
+ metavar="COMPOSITE",
36
+ help="A YAML file containing the Composite resource to render, or kind:apiVersion:namespace:name of cluster Composite.",
38
37
  )
39
38
  parser.add_argument(
40
- 'composition',
39
+ "composition",
41
40
  type=pathlib.Path,
42
- nargs='?',
43
- metavar='COMPOSITION',
44
- help='A YAML file containing the Composition resource, or the complete path of a function-pythonic BaseComposite subclass.',
41
+ nargs="?",
42
+ metavar="COMPOSITION",
43
+ help="A YAML file containing the Composition resource, or the complete path of a function-pythonic BaseComposite subclass.",
45
44
  )
46
45
  parser.add_argument(
47
- '--kube-context', '-k',
48
- metavar='CONTEXT',
49
- help='The kubectl context to use to obtain external resources from, such as required resources, connections, etc.'
46
+ "--kube-context",
47
+ "-k",
48
+ metavar="CONTEXT",
49
+ help="The kubectl context to use to obtain external resources from, such as required resources, connections, etc.",
50
50
  )
51
51
  parser.add_argument(
52
- '--context-files',
53
- action='append',
52
+ "--context-files",
53
+ action="append",
54
54
  default=[],
55
- metavar='KEY=PATH',
56
- help='Context key-value pairs to pass to the Function pipeline. Values must be files containing YAML/JSON.',
55
+ metavar="KEY=PATH",
56
+ help="Context key-value pairs to pass to the Function pipeline. Values must be files containing YAML/JSON.",
57
57
  )
58
58
  parser.add_argument(
59
- '--context-values',
60
- action='append',
59
+ "--context-values",
60
+ action="append",
61
61
  default=[],
62
- metavar='KEY=VALUE',
63
- help='Context key-value pairs to pass to the Function pipeline. Values must be YAML/JSON. Keys take precedence over --context-files.',
62
+ metavar="KEY=VALUE",
63
+ help="Context key-value pairs to pass to the Function pipeline. Values must be YAML/JSON. Keys take precedence over --context-files.",
64
64
  )
65
65
  parser.add_argument(
66
- '--observed-resources', '-o',
67
- action='append',
66
+ "--observed-resources",
67
+ "-o",
68
+ action="append",
68
69
  type=pathlib.Path,
69
70
  default=[],
70
- metavar='PATH',
71
- help='A YAML file or directory of YAML files specifying the observed state of composed resources.'
71
+ metavar="PATH",
72
+ help="A YAML file or directory of YAML files specifying the observed state of composed resources.",
72
73
  )
73
74
  parser.add_argument(
74
- '--required-resources', '-e',
75
- action='append',
75
+ "--required-resources",
76
+ "-e",
77
+ action="append",
76
78
  type=pathlib.Path,
77
79
  default=[],
78
- metavar='PATH',
79
- help='A YAML file or directory of YAML files specifying required resources to pass to the Function pipeline.',
80
+ metavar="PATH",
81
+ help="A YAML file or directory of YAML files specifying required resources to pass to the Function pipeline.",
80
82
  )
81
83
  parser.add_argument(
82
- '--required-schemas', '-s',
83
- action='append',
84
+ "--required-schemas",
85
+ "-s",
86
+ action="append",
84
87
  type=pathlib.Path,
85
88
  default=[],
86
- metavar='PATH',
87
- help='A JSON file or directory of JSON files specifying required schemas to pass to the Function pipeline.',
89
+ metavar="PATH",
90
+ help="A JSON file or directory of JSON files specifying required schemas to pass to the Function pipeline.",
88
91
  )
89
92
  parser.add_argument(
90
- '--include-full-xr', '-x',
91
- action='store_true',
93
+ "--include-full-xr",
94
+ "-x",
95
+ action="store_true",
92
96
  help="Include a direct copy of the input XR's spedc and metadata fields in the rendered output.",
93
97
  )
94
98
  parser.add_argument(
95
- '--include-connection-xr',
96
- action='store_true',
99
+ "--include-connection-xr",
100
+ action="store_true",
97
101
  help="Include the Composite connection values in the rendered output as a resource of kind: Connection.",
98
102
  )
99
103
  parser.add_argument(
100
- '--include-function-results', '-r',
101
- action='store_true',
102
- help='Include informational and warning messages from Functions in the rendered output as resources of kind: Result.',
104
+ "--include-function-results",
105
+ "-r",
106
+ action="store_true",
107
+ help="Include informational and warning messages from Functions in the rendered output as resources of kind: Result.",
103
108
  )
104
109
  parser.add_argument(
105
- '--include-context', '-c',
106
- action='store_true',
107
- help='Include the context in the rendered output as a resource of kind: Context.',
110
+ "--include-context",
111
+ "-c",
112
+ action="store_true",
113
+ help="Include the context in the rendered output as a resource of kind: Context.",
108
114
  )
109
115
 
110
116
  def initialize(self):
@@ -124,7 +130,17 @@ class Command(command.Command):
124
130
  schemas = self.collect_schemas(self.args.required_schemas)
125
131
  context = self.setup_context()
126
132
 
127
- render = await self.render(composite, observed, composition, resources, schemas, context, api, self.args.render_unknowns, self.args.crossplane_v1)
133
+ render = await self.render(
134
+ composite,
135
+ observed,
136
+ composition,
137
+ resources,
138
+ schemas,
139
+ context,
140
+ api,
141
+ self.args.render_unknowns,
142
+ self.args.crossplane_v1,
143
+ )
128
144
  if not render:
129
145
  sys.exit(1)
130
146
 
@@ -139,34 +155,39 @@ class Command(command.Command):
139
155
  render.composite.metadata.name = composite.metadata.name
140
156
 
141
157
  # Print the composite.
142
- print('---')
143
- print(str(render.composite), end='')
158
+ print("---")
159
+ print(str(render.composite), end="")
144
160
  # Print Composite connection if requested.
145
161
  if self.args.include_connection_xr:
146
- print('---')
147
- print(str(render.connection), end='')
162
+ print("---")
163
+ print(str(render.connection), end="")
148
164
  # Print the composed resources.
149
- for resource in sorted(render.resources, key=lambda resource: str(resource.metadata.annotations['crossplane.io/composition-resource-name'])):
150
- print('---')
151
- print(str(resource), end='')
165
+ for resource in sorted(
166
+ render.resources,
167
+ key=lambda resource: str(
168
+ resource.metadata.annotations["crossplane.io/composition-resource-name"]
169
+ ),
170
+ ):
171
+ print("---")
172
+ print(str(resource), end="")
152
173
  # Print the results (AKA events) if requested.
153
174
  if self.args.include_function_results:
154
175
  for result in render.results:
155
- print('---')
156
- print(str(result), end='')
176
+ print("---")
177
+ print(str(result), end="")
157
178
  # Print the final context if requested.
158
179
  if self.args.include_context:
159
- print('---')
160
- print(str(render.context), end='')
180
+ print("---")
181
+ print(str(render.context), end="")
161
182
 
162
183
  async def setup_composite(self, api=None):
163
184
  # Obtain the Composite to render.
164
185
  if self.args.composite.is_file():
165
186
  return protobuf.Yaml(self.args.composite.read_text())
166
187
  if not api:
167
- print(f"Composite \"{self.args.composite}\" is not a file", file=sys.stderr)
188
+ print(f'Composite "{self.args.composite}" is not a file', file=sys.stderr)
168
189
  sys.exit(1)
169
- composite = str(self.args.composite).split(':')
190
+ composite = str(self.args.composite).split(":")
170
191
  if len(composite) == 3:
171
192
  namespace = None
172
193
  elif len(composite) == 4:
@@ -175,11 +196,16 @@ class Command(command.Command):
175
196
  else:
176
197
  namespace = None
177
198
  else:
178
- print(f"Composite \"{self.args.composite}\" is not kind:apiVersion:namespace:name", file=sys.stderr)
199
+ print(
200
+ f'Composite "{self.args.composite}" is not kind:apiVersion:namespace:name',
201
+ file=sys.stderr,
202
+ )
179
203
  sys.exit(1)
180
- composite = await self.kr8s_get(api, composite[0], composite[1], namespace, composite[-1])
204
+ composite = await self.kr8s_get(
205
+ api, composite[0], composite[1], namespace, composite[-1]
206
+ )
181
207
  if not composite:
182
- print(f"Composite \"{self.args.composite}\" not found", file=sys.stderr)
208
+ print(f'Composite "{self.args.composite}" not found', file=sys.stderr)
183
209
  sys.exit(1)
184
210
  return composite
185
211
 
@@ -189,47 +215,67 @@ class Command(command.Command):
189
215
  return None
190
216
  if self.args.composition.is_file():
191
217
  composition = self.args.composition.read_text()
192
- if self.args.composition.suffix == '.py':
193
- return self.create_composition(compsite, composition)
218
+ if self.args.composition.suffix == ".py":
219
+ return self.create_composition(composite, composition)
194
220
  composition = protobuf.Yaml(composition)
195
221
  if not len(composition.spec.pipeline):
196
- print(f"Composition file does not contain any pipeline steps: {self.args.composition}", file=sys.stderr)
222
+ print(
223
+ f"Composition file does not contain any pipeline steps: {self.args.composition}",
224
+ file=sys.stderr,
225
+ )
197
226
  sys.exit(1)
198
227
  return composition
199
- composition = str(self.args.composition).rsplit('.', 1)
228
+ composition = str(self.args.composition).rsplit(".", 1)
200
229
  if len(composition) == 1:
201
- print(f"Composition class name does not include module: {self.args.composition}", file=sys.stderr)
230
+ print(
231
+ f"Composition class name does not include module: {self.args.composition}",
232
+ file=sys.stderr,
233
+ )
202
234
  sys.exit(1)
203
235
  try:
204
236
  module = importlib.import_module(composition[0])
205
- except Exception as e:
237
+ except Exception as e: # noqa: BLE001
206
238
  print(e)
207
- print(f"Unable to import composition module: {composition[0]}", file=sys.stderr)
239
+ print(
240
+ f"Unable to import composition module: {composition[0]}",
241
+ file=sys.stderr,
242
+ )
208
243
  sys.exit(1)
209
244
  clazz = getattr(module, composition[1], None)
210
245
  if not clazz:
211
- print(f"Composition class {composition[0]} does not define: {composition[1]}", file=sys.stderr)
246
+ print(
247
+ f"Composition class {composition[0]} does not define: {composition[1]}",
248
+ file=sys.stderr,
249
+ )
212
250
  sys.exit(1)
213
251
  if not inspect.isclass(clazz):
214
- print(f"Composition class {self.args.composition} is not a class", file=sys.stderr)
252
+ print(
253
+ f"Composition class {self.args.composition} is not a class",
254
+ file=sys.stderr,
255
+ )
215
256
  sys.exit(1)
216
257
  if not issubclass(clazz, BaseComposite):
217
- print(f"Composition class {self.args.composition} is not a subclass of BaseComposite", file=sys.stderr)
258
+ print(
259
+ f"Composition class {self.args.composition} is not a subclass of BaseComposite",
260
+ file=sys.stderr,
261
+ )
218
262
  sys.exit(1)
219
263
  return self.create_composition(composite, self.args.composition)
220
264
 
221
- def create_composition(self, composite, module=''):
265
+ def create_composition(self, composite, module=""):
222
266
  composition = protobuf.Map()
223
- composition.apiVersion = 'apiextensions.crossplane.io/v1'
224
- composition.kind = 'Composition'
225
- composition.metadata.name = 'function-pythonic-render'
267
+ composition.apiVersion = "apiextensions.crossplane.io/v1"
268
+ composition.kind = "Composition"
269
+ composition.metadata.name = "function-pythonic-render"
226
270
  composition.spec.compositeTypeRef.apiVersion = composite.apiVersion
227
271
  composition.spec.compositeTypeRef.kind = composite.kind
228
- composition.spec.mode = 'Pipeline'
229
- composition.spec.pipeline[0].step = 'function-pythonic-render'
230
- composition.spec.pipeline[0].functionRef.name = 'function-pythonic'
231
- composition.spec.pipeline[0].input.apiVersion = 'pythonic.fn.crossplane.io/v1alpha1'
232
- composition.spec.pipeline[0].input.kind = 'Composite'
272
+ composition.spec.mode = "Pipeline"
273
+ composition.spec.pipeline[0].step = "function-pythonic-render"
274
+ composition.spec.pipeline[0].functionRef.name = "function-pythonic"
275
+ composition.spec.pipeline[
276
+ 0
277
+ ].input.apiVersion = "pythonic.fn.crossplane.io/v1alpha1"
278
+ composition.spec.pipeline[0].input.kind = "Composite"
233
279
  composition.spec.pipeline[0].input.composite = str(module)
234
280
  return composition
235
281
 
@@ -241,11 +287,14 @@ class Command(command.Command):
241
287
  resources.append(protobuf.Value(None, None, document))
242
288
  elif entry.is_dir():
243
289
  for file in entry.iterdir():
244
- if file.suffix in ('.yaml', '.yml'):
290
+ if file.suffix in (".yaml", ".yml"):
245
291
  for document in yaml.safe_load_all(file.read_text()):
246
292
  resources.append(protobuf.Value(None, None, document))
247
293
  else:
248
- print(f"Specified resource is not a file or a directory: {entry}", file=sys.stderr)
294
+ print(
295
+ f"Specified resource is not a file or a directory: {entry}",
296
+ file=sys.stderr,
297
+ )
249
298
  sys.exit(1)
250
299
  resources.sort(key=lambda resource: str(resource.metadata.name))
251
300
  return resources
@@ -258,11 +307,14 @@ class Command(command.Command):
258
307
  schemas.append(protobuf.Value(None, None, document))
259
308
  elif entry.is_dir():
260
309
  for file in entry.iterdir():
261
- if file.suffix == '.json':
310
+ if file.suffix == ".json":
262
311
  document = json.loads(file.read_text())
263
312
  schemas.append(protobuf.Value(None, None, document))
264
313
  else:
265
- print(f"Specified resource is not a file or a directory: {entry}", file=sys.stderr)
314
+ print(
315
+ f"Specified resource is not a file or a directory: {entry}",
316
+ file=sys.stderr,
317
+ )
266
318
  sys.exit(1)
267
319
  return schemas
268
320
 
@@ -270,7 +322,7 @@ class Command(command.Command):
270
322
  # Load the request context with any specified command line options.
271
323
  context = protobuf.Map()
272
324
  for entry in self.args.context_files:
273
- key_path = entry.split('=', 1)
325
+ key_path = entry.split("=", 1)
274
326
  if len(key_path) != 2:
275
327
  print(f"Invalid --context-files: {entry}", file=sys.stderr)
276
328
  sys.exit(1)
@@ -280,16 +332,39 @@ class Command(command.Command):
280
332
  sys.exit(1)
281
333
  context[key_path[0]] = protobuf.Yaml(path.read_text())
282
334
  for entry in self.args.context_values:
283
- key_value = entry.split('=', 1)
335
+ key_value = entry.split("=", 1)
284
336
  if len(key_value) != 2:
285
337
  print(f"Invalid --context-values: {entry}", file=sys.stderr)
286
338
  sys.exit(1)
287
339
  context[key_value[0]] = protobuf.Yaml(key_value[1])
288
340
  return context
289
341
 
290
- async def render(self, composite, observed=[], composition=None, resources=[], schemas=[], context=None, api=None, render_unknowns=False, crossplane_v1=False, composite_observeds=True):
342
+ async def render(
343
+ self,
344
+ composite,
345
+ observed=None,
346
+ composition=None,
347
+ resources=None,
348
+ schemas=None,
349
+ context=None,
350
+ api=None,
351
+ render_unknowns=False,
352
+ crossplane_v1=False,
353
+ composite_observeds=False,
354
+ ):
291
355
  # Create the request used when running Composition steps.
292
- request = protobuf.Message(None, 'request', fnv1.RunFunctionRequest.DESCRIPTOR, fnv1.RunFunctionRequest())
356
+ if schemas is None:
357
+ schemas = []
358
+ if resources is None:
359
+ resources = []
360
+ if observed is None:
361
+ observed = []
362
+ request = protobuf.Message(
363
+ None,
364
+ "request",
365
+ fnv1.RunFunctionRequest.DESCRIPTOR,
366
+ fnv1.RunFunctionRequest(),
367
+ )
293
368
  if context is not None:
294
369
  request.context = context
295
370
 
@@ -299,9 +374,18 @@ class Command(command.Command):
299
374
  if observed:
300
375
  async with asyncio.TaskGroup() as group:
301
376
  for resource in observed:
302
- name = resource.metadata.annotations['crossplane.io/composition-resource-name']
377
+ name = resource.metadata.annotations[
378
+ "crossplane.io/composition-resource-name"
379
+ ]
303
380
  if name:
304
- group.create_task(self.set_resource(resource, request.observed.resources[name], resources, api))
381
+ group.create_task(
382
+ self.set_resource(
383
+ resource,
384
+ request.observed.resources[name],
385
+ resources,
386
+ api,
387
+ )
388
+ )
305
389
  if api and composite_observeds:
306
390
  refs = composite.spec.crossplane.resourceRefs
307
391
  if not refs:
@@ -309,10 +393,18 @@ class Command(command.Command):
309
393
  if refs:
310
394
  async with asyncio.TaskGroup() as group:
311
395
  for ref in refs:
312
- group.create_task(self.get_composite_ref(composite, ref, request, resources, api))
396
+ group.create_task(
397
+ self.get_composite_ref(
398
+ composite, ref, request, resources, api
399
+ )
400
+ )
313
401
 
314
402
  if not composition:
315
- if composite.apiVersion in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and composite.kind == 'Composite':
403
+ if (
404
+ composite.apiVersion
405
+ in ("pythonic.crossplane.io/v1alpha1", "pythonic.fortra.com/v1alpha1")
406
+ and composite.kind == "Composite"
407
+ ):
316
408
  composition = self.create_composition(composite)
317
409
  else:
318
410
  if not api:
@@ -323,11 +415,20 @@ class Command(command.Command):
323
415
  # Crossplane V1 location
324
416
  revision = composite.spec.compositionRevisionRef
325
417
  if not revision.name:
326
- print('Composite does not contain a CompositionRevision name', file=sys.stderr)
418
+ print(
419
+ "Composite does not contain a CompositionRevision name",
420
+ file=sys.stderr,
421
+ )
327
422
  return None
328
- composition = await self.kr8s_get(api, 'CompositionRevision', 'apiextensions.crossplane.io/v1', None, revision.name)
423
+ composition = await self.kr8s_get(
424
+ api,
425
+ "CompositionRevision",
426
+ "apiextensions.crossplane.io/v1",
427
+ None,
428
+ revision.name,
429
+ )
329
430
  if not composition:
330
- print(f"Compositioin \"{revision.name}\" not found", file=sys.stderr)
431
+ print(f'Compositioin "{revision.name}" not found', file=sys.stderr)
331
432
  return None
332
433
 
333
434
  # These will hold the response conditions and results.
@@ -340,8 +441,11 @@ class Command(command.Command):
340
441
 
341
442
  # Process the composition pipeline steps.
342
443
  for step in composition.spec.pipeline:
343
- if step.functionRef.name != 'function-pythonic':
344
- print(f"Only function-pythonic functions can be run: {step.functionRef.name}", file=sys.stderr)
444
+ if step.functionRef.name != "function-pythonic":
445
+ print(
446
+ f"Only function-pythonic functions can be run: {step.functionRef.name}",
447
+ file=sys.stderr,
448
+ )
345
449
  return None
346
450
  if not step.input.step:
347
451
  step.input.step = step.step
@@ -350,44 +454,67 @@ class Command(command.Command):
350
454
  # Supply step requested credentials.
351
455
  request.credentials()
352
456
  for credential in step.credentials:
353
- if credential.source == 'Secret' and credential.secretRef:
457
+ if credential.source == "Secret" and credential.secretRef:
354
458
  namespace = credential.secretRef.namespace
355
459
  name = credential.secretRef.name
356
460
  if namespace and name:
357
461
  for resource in resources:
358
- if resource.kind == 'Secret' and resource.apiVersion == 'v1':
359
- if resource.metadata.namespace == namespace and resource.metadata.name == name:
360
- data = request.credentials[credential.name].credential_data.data
361
- data()
362
- for key, value in resource.data:
363
- data[key] = protobuf.B64Decode(value)
364
- break
462
+ if (
463
+ resource.kind == "Secret"
464
+ and resource.apiVersion == "v1"
465
+ and resource.metadata.namespace == namespace
466
+ and resource.metadata.name == name
467
+ ):
468
+ data = request.credentials[
469
+ credential.name
470
+ ].credential_data.data
471
+ data()
472
+ for key, value in resource.data:
473
+ data[key] = protobuf.B64Decode(value)
474
+ break
365
475
  else:
366
- print(f"Step \"{step.step}\" secret not found: {namespace}/{name}", file=sys.stderr)
476
+ print(
477
+ f'Step "{step.step}" secret not found: {namespace}/{name}',
478
+ file=sys.stderr,
479
+ )
367
480
  return None
368
481
 
369
482
  # Track what extra/required resources have been processed.
370
- requirements = protobuf.Message(None, 'requirements', fnv1.Requirements.DESCRIPTOR, fnv1.Requirements())
483
+ requirements = protobuf.Message(
484
+ None, "requirements", fnv1.Requirements.DESCRIPTOR, fnv1.Requirements()
485
+ )
371
486
  for _ in range(5):
372
487
  # Fetch the step bootstrap resources specified.
373
488
  request.required_resources()
374
489
  for requirement in step.requirements.requiredResources:
375
- await self.set_required(requirement.requirementName, requirement, request.required_resources, resources, api)
490
+ await self.set_required(
491
+ requirement.requirementName,
492
+ requirement,
493
+ request.required_resources,
494
+ resources,
495
+ api,
496
+ )
376
497
  # Fetch the required resources requested.
377
498
  for name, selector in requirements.resources:
378
- await self.set_required(name, selector, request.required_resources, resources, api)
499
+ await self.set_required(
500
+ name, selector, request.required_resources, resources, api
501
+ )
379
502
  # Fetch the now deprecated extra resources requested.
380
503
  request.extra_resources()
381
504
  for name, selector in requirements.extra_resources:
382
- await self.set_required(name, selector, request.extra_resources, resources, api)
505
+ await self.set_required(
506
+ name, selector, request.extra_resources, resources, api
507
+ )
383
508
  # Fetch the schemas requested.
384
509
  request.required_schemas()
385
510
  for name, selector in requirements.schemas:
386
- await self.set_schema(name, selector, request.required_schemas, schemas, api)
511
+ await self.set_schema(
512
+ name, selector, request.required_schemas, schemas, api
513
+ )
387
514
  # Run the step using the function-pythonic function runner.
388
515
  response = protobuf.Message(
389
516
  None,
390
- 'response',
517
+ "response",
391
518
  fnv1.RunFunctionResponse.DESCRIPTOR,
392
519
  await runner.RunFunction(request._message, None),
393
520
  )
@@ -414,13 +541,18 @@ class Command(command.Command):
414
541
 
415
542
  # Collect the step's returned conditions.
416
543
  for condition in response.conditions:
417
- if condition.type not in ('Ready', 'Synced', 'Healthy'):
418
- conditions[protobuf.append] = self.create_condition(condition.type, condition.status, condition.reason, condition.message)
544
+ if condition.type not in ("Ready", "Synced", "Healthy"):
545
+ conditions[protobuf.append] = self.create_condition(
546
+ condition.type,
547
+ condition.status,
548
+ condition.reason,
549
+ condition.message,
550
+ )
419
551
  # Collect the step's returned results.
420
552
  for result in response.results:
421
553
  ix = len(results)
422
- results[ix].apiVersion = 'render.crossplane.io/v1beta1'
423
- results[ix].kind = 'Result'
554
+ results[ix].apiVersion = "render.crossplane.io/v1beta1"
555
+ results[ix].kind = "Result"
424
556
  results[ix].step = step.step
425
557
  results[ix].severity = fnv1.Severity.Name(result.severity._value)
426
558
  if result.reason:
@@ -435,7 +567,7 @@ class Command(command.Command):
435
567
  # Collect and format all the returned desired composed resources.
436
568
  resources = protobuf.List()
437
569
  unready = protobuf.List()
438
- prefix = composite.metadata.labels['crossplane.io/composite']
570
+ prefix = composite.metadata.labels["crossplane.io/composite"]
439
571
  if not prefix:
440
572
  prefix = composite.metadata.name
441
573
  for name, resource in request.desired.resources:
@@ -450,27 +582,40 @@ class Command(command.Command):
450
582
  resource = resource.resource
451
583
  observed = request.observed.resources[name].resource
452
584
  if observed:
453
- for key in ('namespace', 'generateName', 'name'):
585
+ for key in ("namespace", "generateName", "name"):
454
586
  if observed.metadata[key]:
455
587
  resource.metadata[key] = observed.metadata[key]
456
588
  if not resource.metadata.name and not resource.metadata.generateName:
457
589
  resource.metadata.generateName = f"{prefix}-"
458
590
  if composite.metadata.namespace:
459
591
  resource.metadata.namespace = composite.metadata.namespace
460
- resource.metadata.annotations['crossplane.io/composition-resource-name'] = name
461
- resource.metadata.labels['crossplane.io/composite'] = prefix
462
- if composite.metadata.labels['crossplane.io/claim-name'] and composite.metadata.labels['crossplane.io/claim-namespace']:
463
- resource.metadata.labels['crossplane.io/claim-namespace'] = composite.metadata.labels['crossplane.io/claim-namespace']
464
- resource.metadata.labels['crossplane.io/claim-name'] = composite.metadata.labels['crossplane.io/claim-name']
592
+ resource.metadata.annotations["crossplane.io/composition-resource-name"] = (
593
+ name
594
+ )
595
+ resource.metadata.labels["crossplane.io/composite"] = prefix
596
+ if (
597
+ composite.metadata.labels["crossplane.io/claim-name"]
598
+ and composite.metadata.labels["crossplane.io/claim-namespace"]
599
+ ):
600
+ resource.metadata.labels["crossplane.io/claim-namespace"] = (
601
+ composite.metadata.labels["crossplane.io/claim-namespace"]
602
+ )
603
+ resource.metadata.labels["crossplane.io/claim-name"] = (
604
+ composite.metadata.labels["crossplane.io/claim-name"]
605
+ )
465
606
  elif composite.spec.claimRef.namespace and composite.spec.claimRef.name:
466
- resource.metadata.labels['crossplane.io/claim-namespace'] = composite.spec.claimRef.namespace
467
- resource.metadata.labels['crossplane.io/claim-name'] = composite.spec.claimRef.name
607
+ resource.metadata.labels["crossplane.io/claim-namespace"] = (
608
+ composite.spec.claimRef.namespace
609
+ )
610
+ resource.metadata.labels["crossplane.io/claim-name"] = (
611
+ composite.spec.claimRef.name
612
+ )
468
613
  resource.metadata.ownerReferences[0].controller = True
469
614
  resource.metadata.ownerReferences[0].blockOwnerDeletion = True
470
615
  resource.metadata.ownerReferences[0].apiVersion = composite.apiVersion
471
616
  resource.metadata.ownerReferences[0].kind = composite.kind
472
617
  resource.metadata.ownerReferences[0].name = composite.metadata.name
473
- resource.metadata.ownerReferences[0].uid = ''
618
+ resource.metadata.ownerReferences[0].uid = ""
474
619
  resource.ready = ready
475
620
  resources[protobuf.append] = resource
476
621
 
@@ -482,30 +627,44 @@ class Command(command.Command):
482
627
  composite.kind = request.observed.composite.resource.kind
483
628
  # Add in the composite's status.conditions.
484
629
  if request.desired.composite.ready == fnv1.Ready.READY_FALSE:
485
- condition = self.create_condition('Ready', False, 'Creating')
486
- elif request.desired.composite.ready == fnv1.Ready.READY_UNSPECIFIED and len(unready):
487
- condition = self.create_condition('Ready', False, 'Creating', f"Unready resources: {','.join(str(name) for name in unready)}")
630
+ condition = self.create_condition("Ready", False, "Creating")
631
+ elif request.desired.composite.ready == fnv1.Ready.READY_UNSPECIFIED and len(
632
+ unready
633
+ ):
634
+ condition = self.create_condition(
635
+ "Ready",
636
+ False,
637
+ "Creating",
638
+ f"Unready resources: {','.join(str(name) for name in unready)}",
639
+ )
488
640
  else:
489
- condition = self.create_condition('Ready', True, 'Available')
641
+ condition = self.create_condition("Ready", True, "Available")
490
642
  composite.status.conditions[protobuf.append] = condition
491
643
  for condition in conditions:
492
644
  composite.status.conditions[protobuf.append] = condition
493
645
 
494
- return protobuf.Map(
646
+ response = protobuf.Map(
495
647
  composite=composite,
496
648
  connection=protobuf.Map(
497
- apiVersion='render.crossplane.io/v1beta1',
498
- kind='Connection',
499
- values={key: value for key, value in request.desired.composite.connection_details}
649
+ apiVersion="render.crossplane.io/v1beta1",
650
+ kind="Connection",
651
+ values={
652
+ key: value
653
+ for key, value in request.desired.composite.connection_details
654
+ },
500
655
  ),
501
656
  resources=resources,
502
657
  results=results,
503
658
  context=protobuf.Map(
504
- apiVersion='render.crossplane.io/v1beta1',
505
- kind='Context',
659
+ apiVersion="render.crossplane.io/v1beta1",
660
+ kind="Context",
506
661
  values=request.context,
507
662
  ),
508
663
  )
664
+ if composite_observeds:
665
+ for name, resource in request.observed.resources:
666
+ response.observed[name] = resource.resource
667
+ return response
509
668
 
510
669
  async def get_composite_ref(self, composite, ref, request, resources, api):
511
670
  namespace = ref.namespace
@@ -515,82 +674,128 @@ class Command(command.Command):
515
674
  namespace = None
516
675
  source = await self.kr8s_get(api, ref.kind, ref.apiVersion, namespace, ref.name)
517
676
  if source:
518
- name = source.metadata.annotations['crossplane.io/composition-resource-name']
677
+ name = source.metadata.annotations[
678
+ "crossplane.io/composition-resource-name"
679
+ ]
519
680
  if name:
520
681
  destination = request.observed.resources[name]
521
- if not destination: # Do not override manual observed
682
+ if not destination: # Do not override manual observed
522
683
  await self.set_resource(source, destination, resources, api)
523
684
 
524
- async def set_required(self, name, selector, requireds, resources=[], api=None):
685
+ async def set_required(self, name, selector, requireds, resources=None, api=None):
686
+ if resources is None:
687
+ resources = []
525
688
  if not name:
526
689
  return
527
690
  name = str(name)
528
691
  items = requireds[name].items
529
- items() # Force this to get created
692
+ items() # Force this to get created
530
693
  for resource in resources:
531
- if selector.api_version == resource.apiVersion and selector.kind == resource.kind:
532
- if ((not len(selector.namespace) and not len(resource.metadata.namespace))
694
+ if (
695
+ selector.api_version == resource.apiVersion
696
+ and selector.kind == resource.kind
697
+ and (
698
+ (
699
+ not len(selector.namespace)
700
+ and not len(resource.metadata.namespace)
701
+ )
533
702
  or (selector.namespace == resource.metadata.namespace)
534
- ):
535
- if selector.match_name == resource.metadata.name:
536
- await self.set_resource(resource, items[protobuf.append], resources, api)
537
- elif selector.match_labels.labels:
538
- for key, value in selector.match_labels.labels:
539
- if value != resource.metadata.labels[key]:
540
- break
541
- else:
542
- await self.set_resource(resource, items[protobuf.append], resources, api)
703
+ )
704
+ ):
705
+ if selector.match_name == resource.metadata.name:
706
+ await self.set_resource(
707
+ resource, items[protobuf.append], resources, api
708
+ )
709
+ elif selector.match_labels.labels:
710
+ for key, value in selector.match_labels.labels:
711
+ if value != resource.metadata.labels[key]:
712
+ break
713
+ else:
714
+ await self.set_resource(
715
+ resource, items[protobuf.append], resources, api
716
+ )
543
717
  if not len(items) and api:
544
718
  if len(selector.match_name):
545
- resource = await self.kr8s_get(api, selector.kind, selector.api_version, selector.namespace, selector.match_name)
719
+ resource = await self.kr8s_get(
720
+ api,
721
+ selector.kind,
722
+ selector.api_version,
723
+ selector.namespace,
724
+ selector.match_name,
725
+ )
546
726
  if resource:
547
- await self.set_resource(resource, items[protobuf.append], resources, api)
727
+ await self.set_resource(
728
+ resource, items[protobuf.append], resources, api
729
+ )
548
730
  elif len(selector.match_labels.labels):
549
- for resource in await self.kr8s_list(api, selector.kind, selector.api_version, selector.namespace, selector.match_labels.labels):
550
- await self.set_resource(resource, items[protobuf.append], resources, api)
551
-
552
- async def set_resource(self, source, destination, resources=[], api=None):
731
+ for resource in await self.kr8s_list(
732
+ api,
733
+ selector.kind,
734
+ selector.api_version,
735
+ selector.namespace,
736
+ selector.match_labels.labels,
737
+ ):
738
+ await self.set_resource(
739
+ resource, items[protobuf.append], resources, api
740
+ )
741
+
742
+ async def set_resource(self, source, destination, resources=None, api=None):
743
+ if resources is None:
744
+ resources = []
553
745
  destination.resource = source
554
- namespace = source.spec.writeConnectionSecretToRef.namespace or source.metadata.namespace
746
+ namespace = (
747
+ source.spec.writeConnectionSecretToRef.namespace
748
+ or source.metadata.namespace
749
+ )
555
750
  name = source.spec.writeConnectionSecretToRef.name
556
751
  if namespace and name:
557
752
  connection = None
558
753
  for resource in resources:
559
- if resource.kind == 'Secret' and resource.apiVersion == 'v1':
560
- if resource.metadata.namespace == namespace and resource.metadata.name == name:
561
- connection = resource
562
- break
754
+ if (
755
+ resource.kind == "Secret"
756
+ and resource.apiVersion == "v1"
757
+ and resource.metadata.namespace == namespace
758
+ and resource.metadata.name == name
759
+ ):
760
+ connection = resource
761
+ break
563
762
  else:
564
763
  if api:
565
- connection = await self.kr8s_get(api, 'Secret', 'v1', namespace, name)
764
+ connection = await self.kr8s_get(
765
+ api, "Secret", "v1", namespace, name
766
+ )
566
767
  if connection:
567
768
  destination.connection_details()
568
769
  for key, value in connection.data:
569
770
  destination.connection_details[key] = protobuf.B64Decode(value)
570
771
 
571
- async def set_schema(self, name, selector, schemas, documents=[], api=None):
772
+ async def set_schema(self, name, selector, schemas, documents=None, api=None):
773
+ if documents is None:
774
+ documents = []
572
775
  if not name:
573
776
  return
574
777
  name = str(name)
575
778
  schema = schemas[name].openapi_v3
576
- schema() # Force this to get created
779
+ schema() # Force this to get created
577
780
  gvk = protobuf.Map(kind=selector.kind)
578
781
  version = str(selector.api_version)
579
- if '/' in version:
580
- gvk.group, gvk.version = version.split('/', 1)
782
+ if "/" in version:
783
+ gvk.group, gvk.version = version.split("/", 1)
581
784
  else:
582
- gvk.group = ''
785
+ gvk.group = ""
583
786
  gvk.version = version
584
787
  for document in documents:
585
788
  if self.find_schema(gvk, document, schema):
586
789
  return
587
790
  if api:
588
- if gvk.group == '':
791
+ if gvk.group == "":
589
792
  url = f"api/{gvk.version}"
590
793
  else:
591
794
  url = f"apis/{gvk.group}/{gvk.version}"
592
795
  try:
593
- async with api.call_api(base='/openapi/v3', version='', url=url) as response:
796
+ async with api.call_api(
797
+ base="/openapi/v3", version="", url=url
798
+ ) as response:
594
799
  document = protobuf.Value(None, None, response.json())
595
800
  except kr8s.NotFoundError:
596
801
  return
@@ -598,9 +803,11 @@ class Command(command.Command):
598
803
 
599
804
  def find_schema(self, gvk, document, schema):
600
805
  for name, s in document.components.schemas:
601
- gvks = s['x-kubernetes-group-version-kind']
806
+ gvks = s["x-kubernetes-group-version-kind"]
602
807
  if len(gvks) == 1 and gvks[0] == gvk:
603
- self.resolve_ref(document, set(), f"#/components/schemas/{name}", schema)
808
+ self.resolve_ref(
809
+ document, set(), f"#/components/schemas/{name}", schema
810
+ )
604
811
  return True
605
812
  return False
606
813
 
@@ -611,8 +818,8 @@ class Command(command.Command):
611
818
  if ref in visiting:
612
819
  return
613
820
  d = None
614
- for segment in ref.split('/'):
615
- if segment == '#':
821
+ for segment in ref.split("/"):
822
+ if segment == "#":
616
823
  d = document
617
824
  else:
618
825
  d = d[segment]
@@ -626,11 +833,11 @@ class Command(command.Command):
626
833
  visiting.remove(ref)
627
834
 
628
835
  def copy_schema(self, document, visiting, key, value, schema):
629
- if key == '$ref':
836
+ if key == "$ref":
630
837
  self.resolve_ref(document, visiting, value, schema)
631
- elif key == 'allOf':
838
+ elif key == "allOf":
632
839
  if value._isList and len(value) == 1:
633
- self.resolve_ref(document, visiting, value[0]['$ref'], schema)
840
+ self.resolve_ref(document, visiting, value[0]["$ref"], schema)
634
841
  else:
635
842
  if value._isMap:
636
843
  s = schema[key]
@@ -653,26 +860,26 @@ class Command(command.Command):
653
860
  def create_condition(self, type, status, reason, message=None):
654
861
  if isinstance(status, protobuf.FieldMessage):
655
862
  if status._value == fnv1.Status.STATUS_CONDITION_TRUE:
656
- status = 'True'
863
+ status = "True"
657
864
  elif status._value == fnv1.Status.STATUS_CONDITION_FALSE:
658
- status = 'False'
865
+ status = "False"
659
866
  else:
660
- status = 'Unknown'
867
+ status = "Unknown"
661
868
  elif isinstance(status, bool):
662
869
  if status:
663
- status = 'True'
870
+ status = "True"
664
871
  else:
665
- status = 'False'
872
+ status = "False"
666
873
  elif status is None:
667
- status = 'Unknown'
874
+ status = "Unknown"
668
875
  condition = {
669
- 'type': type,
670
- 'status': status,
671
- 'reason': reason,
672
- 'lastTransitionTime': '2026-01-01T00:00:00Z'
876
+ "type": type,
877
+ "status": status,
878
+ "reason": reason,
879
+ "lastTransitionTime": "2026-01-01T00:00:00Z",
673
880
  }
674
881
  if message:
675
- condition['message'] = message
882
+ condition["message"] = message
676
883
  return condition
677
884
 
678
885
  async def kr8s_get(self, api, kind, apiVersion, namespace, name):
@@ -686,10 +893,10 @@ class Command(command.Command):
686
893
  else:
687
894
  resource = await clazz.get(str(name), api=api)
688
895
  resource = protobuf.Value(None, None, resource.raw)
689
- result = 'found'
896
+ result = "found"
690
897
  except kr8s.NotFoundError:
691
898
  resource = None
692
- result = 'missing'
899
+ result = "missing"
693
900
  self.logger.debug(f"Resource {result}: {':'.join(fullName)}")
694
901
  return resource
695
902
 
@@ -699,23 +906,22 @@ class Command(command.Command):
699
906
  resources = [
700
907
  protobuf.Value(None, None, resource.raw)
701
908
  async for resource in clazz.list(
702
- namespace=str(namespace) if namespaced else None,
703
- label_selector={
704
- label: str(value)
705
- for label, value in labelSelector
706
- },
707
- api=api,
909
+ namespace=str(namespace) if namespaced else None,
910
+ label_selector={label: str(value) for label, value in labelSelector},
911
+ api=api,
708
912
  )
709
913
  ]
710
914
  if self.logger.isEnabledFor(logging.DEBUG):
711
915
  fullName = [str(kind), str(apiVersion)]
712
916
  if namespaced:
713
917
  fullName.append(str(namespace))
714
- fullName.append('&'.join(f"{label}={value}" for label, value in labelSelector))
918
+ fullName.append(
919
+ "&".join(f"{label}={value}" for label, value in labelSelector)
920
+ )
715
921
  if resources:
716
922
  result = f"found {INFLECT.number_to_words(len(resources))}"
717
923
  else:
718
- result = 'missing'
924
+ result = "missing"
719
925
  self.logger.debug(f"Resources {result}: {':'.join(fullName)}")
720
926
  return resources
721
927
 
@@ -724,4 +930,10 @@ class Command(command.Command):
724
930
  return kr8s.asyncio.objects.get_class(str(kind), str(apiVersion), True)
725
931
  except KeyError:
726
932
  pass
727
- return kr8s.asyncio.objects.new_class(str(kind), str(apiVersion), True, namespaced, plural=INFLECT.plural_noun(str(kind)).lower())
933
+ return kr8s.asyncio.objects.new_class(
934
+ str(kind),
935
+ str(apiVersion),
936
+ True,
937
+ namespaced,
938
+ plural=INFLECT.plural_noun(str(kind)).lower(),
939
+ )