crossplane-function-pythonic 0.3.0__py3-none-any.whl → 0.4.0__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,4 +1,10 @@
1
1
 
2
+ import asyncio
3
+ import kr8s.asyncio
4
+ import importlib
5
+ import inflect
6
+ import inspect
7
+ import logging
2
8
  import pathlib
3
9
  import sys
4
10
  import yaml
@@ -6,6 +12,7 @@ from crossplane.function.proto.v1 import run_function_pb2 as fnv1
6
12
 
7
13
  from . import (
8
14
  command,
15
+ composite,
9
16
  function,
10
17
  protobuf,
11
18
  )
@@ -21,15 +28,20 @@ class Command(command.Command):
21
28
  parser.add_argument(
22
29
  'composite',
23
30
  type=pathlib.Path,
24
- metavar='PATH',
25
- help='A YAML file containing the Composite resource to render.',
31
+ metavar='COMPOSITE',
32
+ help='A YAML file containing the Composite resource to render, or kind:apiVersion:namespace:name of cluster Composite.',
26
33
  )
27
34
  parser.add_argument(
28
35
  'composition',
29
36
  type=pathlib.Path,
30
37
  nargs='?',
31
- metavar='PATH/CLASS',
32
- help='A YAML file containing the Composition resource or the complete path of a function=-pythonic BaseComposite subclass.',
38
+ metavar='COMPOSITION',
39
+ help='A YAML file containing the Composition resource, or the complete path of a function-pythonic BaseComposite subclass.',
40
+ )
41
+ parser.add_argument(
42
+ '--kube-context', '-k',
43
+ metavar='CONTEXT',
44
+ help='The kubectl context to use to obtain external resources from, such as required resources, connections, etc.'
33
45
  )
34
46
  parser.add_argument(
35
47
  '--context-files',
@@ -82,7 +94,7 @@ class Command(command.Command):
82
94
  parser.add_argument(
83
95
  '--include-function-results', '-r',
84
96
  action='store_true',
85
- help='Include informational and warning messages from Functions in the rendered output as resources of kind: Result..',
97
+ help='Include informational and warning messages from Functions in the rendered output as resources of kind: Result.',
86
98
  )
87
99
  parser.add_argument(
88
100
  '--include-context', '-c',
@@ -92,89 +104,23 @@ class Command(command.Command):
92
104
 
93
105
  def initialize(self):
94
106
  self.initialize_function()
107
+ self.logger = logging.getLogger(__name__)
108
+ self.inflect = inflect.engine()
109
+ self.inflect.classical(all=False)
95
110
 
96
111
  async def run(self):
97
- # Obtain the Composite to render.
98
- if not self.args.composite.is_file():
99
- print(f"Composite \"{self.args.composite}\" is not a file", file=sys.stderr)
100
- sys.exit(1)
101
- composite = protobuf.Yaml(self.args.composite.read_text())
102
-
103
- # Obtain the Composition that will be used to render the Composite.
104
- if composite.apiVersion in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and composite.kind == 'Composite':
105
- if self.args.composition:
106
- print('Composite type of "composite.pythonic.crossplane.io" does not use "composition" argument', file=sys.stderr)
107
- sys.exit(1)
108
- composition = self.create_composition(composite, '')
112
+ if self.args.kube_context:
113
+ self.kube_context = await kr8s.asyncio.api(context=self.args.kube_context)
109
114
  else:
110
- if not self.args.composition:
111
- print('"composition" argument required', file=sys.stderr)
112
- sys.exit(1)
113
- if self.args.composition.is_file():
114
- composition = protobuf.Yaml(self.args.composition.read_text())
115
- else:
116
- composite = self.args.composition.rsplit('.', 1)
117
- if len(composite) == 1:
118
- print(f"Composition class name does not include module: {self.args.composition}", file=sys.stderr)
119
- sys.exit(1)
120
- try:
121
- module = importlib.import_module(composite[0])
122
- except Exception as e:
123
- print(f"Unable to import composition class: {composite[0]}", file=sys.stderr)
124
- sys.exit(1)
125
- clazz = getattr(module, composite[1], None)
126
- if not clazz:
127
- print(f"Composition class {composite[0]} does not define: {composite[1]}", file=sys.stderr)
128
- sys.exit(1)
129
- if not inspect.isclass(clazz):
130
- print(f"Composition class {self.args.composition} is not a class", file=sys.stderr)
131
- sys.exit(1)
132
- if not issubclass(clazz, pythonic.BaseComposite):
133
- print(f"Composition class {self.args.composition} is not a subclass of BaseComposite", file=sys.stderr)
134
- sys.exit(1)
135
- composition = self.create_composition(composite, str(self.args.composition))
136
-
137
- # Build up the RunFunctionRequest protobuf message used to call function-pythonic.
138
- request = protobuf.Message(None, 'request', fnv1.RunFunctionRequest.DESCRIPTOR, fnv1.RunFunctionRequest())
139
-
140
- # Load the request context with any specified command line options.
141
- for entry in self.args.context_files:
142
- key_path = entry.split('=', 1)
143
- if len(key_path) != 2:
144
- print(f"Invalid --context-files: {entry}", file=sys.stderr)
145
- sys.exit(1)
146
- path = pathlib.Path(key_path[1])
147
- if not path.is_file():
148
- print(f"Invalid --context-files {path} is not a file", file=sys.stderr)
149
- sys.exit(1)
150
- request.context[key_path[0]] = protobuf.Yaml(path.read_text())
151
- for entry in self.args.context_values:
152
- key_value = entry.split('=', 1)
153
- if len(key_value) != 2:
154
- print(f"Invalid --context-values: {entry}", file=sys.stderr)
155
- sys.exit(1)
156
- request.context[key_value[0]] = protobuf.Yaml(key_value[1])
115
+ self.kube_context = None
157
116
 
158
- # Collect specified required/extra resources. Sort for stable order when processed.
159
- requireds = sorted(
160
- self.collect_resources(self.args.required_resources),
161
- key=lambda required: str(required.metadata.name),
162
- )
117
+ await self.setup_composite()
118
+ await self.setup_composition()
163
119
 
164
- # Collect specified connection and credential secrets.
165
- secrets = []
166
- for secret in self.collect_resources(self.args.secret_store):
167
- if secret.apiVersion == 'v1' and secret.kind == 'Secret':
168
- secrets.append(secret)
169
-
170
- # Establish the request observed composite.
171
- self.setup_resource(composite, secrets, request.observed.composite)
172
-
173
- # Establish the configured observed resources.
174
- for resource in self.collect_resources(self.args.observed_resources):
175
- name = resource.metadata.annotations['crossplane.io/composition-resource-name']
176
- if name:
177
- self.setup_resource(resource, secrets, request.observed.resources[name])
120
+ # Build up the RunFunctionRequest protobuf message used to call function-pythonic.
121
+ self.request = protobuf.Message(None, 'request', fnv1.RunFunctionRequest.DESCRIPTOR, fnv1.RunFunctionRequest())
122
+ self.setup_local_resources()
123
+ await self.setup_observed_resources()
178
124
 
179
125
  # These will hold the response conditions and results.
180
126
  conditions = protobuf.List()
@@ -185,24 +131,24 @@ class Command(command.Command):
185
131
  fatal = False
186
132
 
187
133
  # Process the composition pipeline steps.
188
- for step in composition.spec.pipeline:
134
+ for step in self.composition.spec.pipeline:
189
135
  if step.functionRef.name != 'function-pythonic':
190
136
  print(f"Only function-pythonic functions can be run: {step.functionRef.name}", file=sys.stderr)
191
137
  sys.exit(1)
192
138
  if not step.input.step:
193
139
  step.input.step = step.step
194
- request.input = step.input
140
+ self.request.input = step.input
195
141
 
196
142
  # Supply step requested credentials.
197
- request.credentials()
143
+ self.request.credentials()
198
144
  for credential in step.credentials:
199
145
  if credential.source == 'Secret' and credential.secretRef:
200
146
  namespace = credential.secretRef.namespace
201
147
  name = credential.secretRef.name
202
148
  if namespace and name:
203
- for secret in secrets:
149
+ for secret in self.secrets:
204
150
  if secret.metadata.namespace == namespace and secret.metadata.name == name:
205
- data = request.credentials[credential.name].credential_data.data
151
+ data = self.request.credentials[credential.name].credential_data.data
206
152
  data()
207
153
  for key, value in secret.data:
208
154
  data[key] = protobuf.B64Decode(value)
@@ -215,22 +161,22 @@ class Command(command.Command):
215
161
  requirements = protobuf.Message(None, 'requirements', fnv1.Requirements.DESCRIPTOR, fnv1.Requirements())
216
162
  for _ in range(5):
217
163
  # Fetch the step bootstrap resources specified.
218
- request.required_resources()
219
- for requirement in step.requirements:
220
- self.fetch_requireds(requireds, secrets, requirement.requirementName, requirement, request.required_resources)
164
+ self.request.required_resources()
165
+ for requirement in step.requirements.requiredResources:
166
+ await self.fetch_requireds(requirement.requirementName, requirement, self.request.required_resources)
221
167
  # Fetch the required resources requested.
222
168
  for name, selector in requirements.resources:
223
- self.fetch_requireds(requireds, secrets, name, selector, request.required_resources)
169
+ await self.fetch_requireds(name, selector, self.request.required_resources)
224
170
  # Fetch the now deprecated extra resources requested.
225
- request.extra_resources()
171
+ self.request.extra_resources()
226
172
  for name, selector in requirements.extra_resources:
227
- self.fetch_requireds(requireds, secrets, name, selector, request.extra_resources)
173
+ await self.fetch_requireds(name, selector, self.request.extra_resources)
228
174
  # Run the step using the function-pythonic function runner.
229
175
  response = protobuf.Message(
230
176
  None,
231
177
  'response',
232
178
  fnv1.RunFunctionResponse.DESCRIPTOR,
233
- await runner.RunFunction(request._message, None),
179
+ await runner.RunFunction(self.request._message, None),
234
180
  )
235
181
  # All done if there is a fatal result.
236
182
  for result in response.results:
@@ -238,7 +184,7 @@ class Command(command.Command):
238
184
  fatal = True
239
185
  break
240
186
  # Copy the response context to the request context to use in subsequent steps.
241
- request.context = response.context
187
+ self.request.context = response.context
242
188
  # Exit this loop if the function has not requested additional extra/required resources.
243
189
  if response.requirements == requirements:
244
190
  break
@@ -246,10 +192,10 @@ class Command(command.Command):
246
192
  requirements = response.requirements
247
193
 
248
194
  # Copy the response desired state to the request desired state to use in subsequent steps.
249
- request.desired.resources()
250
- self.copy_resource(response.desired.composite, request.desired.composite)
195
+ self.request.desired.resources()
196
+ self.copy_resource(response.desired.composite, self.request.desired.composite)
251
197
  for name, resource in response.desired.resources:
252
- self.copy_resource(resource, request.desired.resources[name])
198
+ self.copy_resource(resource, self.request.desired.resources[name])
253
199
 
254
200
  # Collect the step's returned conditions.
255
201
  for condition in response.conditions:
@@ -274,57 +220,65 @@ class Command(command.Command):
274
220
  # Collect and format all the returned desired composed resources.
275
221
  resources = protobuf.List()
276
222
  unready = protobuf.List()
277
- prefix = composite.metadata.labels['crossplane.io/composite']
223
+ prefix = self.composite.metadata.labels['crossplane.io/composite']
278
224
  if not prefix:
279
- prefix = composite.metadata.name
280
- for name, resource in request.desired.resources:
281
- if resource.ready != fnv1.Ready.READY_TRUE:
225
+ prefix = self.composite.metadata.name
226
+ for name, resource in self.request.desired.resources:
227
+ if resource.ready == fnv1.Ready.READY_TRUE:
228
+ ready = True
229
+ elif resource.ready == fnv1.Ready.READY_FALSE:
230
+ ready = False
231
+ else:
232
+ ready = None
233
+ if not ready:
282
234
  unready[protobuf.append] = name
283
235
  resource = resource.resource
284
- observed = request.observed.resources[name].resource
236
+ observed = self.request.observed.resources[name].resource
285
237
  if observed:
286
238
  for key in ('namespace', 'generateName', 'name'):
287
239
  if observed.metadata[key]:
288
240
  resource.metadata[key] = observed.metadata[key]
289
241
  if not resource.metadata.name and not resource.metadata.generateName:
290
242
  resource.metadata.generateName = f"{prefix}-"
291
- if composite.metadata.namespace:
292
- resource.metadata.namespace = composite.metadata.namespace
243
+ if self.composite.metadata.namespace:
244
+ resource.metadata.namespace = self.composite.metadata.namespace
293
245
  resource.metadata.annotations['crossplane.io/composition-resource-name'] = name
294
246
  resource.metadata.labels['crossplane.io/composite'] = prefix
295
- if composite.metadata.labels['crossplane.io/claim-name'] and composite.metadata.labels['crossplane.io/claim-namespace']:
296
- resource.metadata.labels['crossplane.io/claim-namespace'] = composite.metadata.labels['crossplane.io/claim-namespace']
297
- resource.metadata.labels['crossplane.io/claim-name'] = composite.metadata.labels['crossplane.io/claim-name']
298
- elif composite.spec.claimRef.namespace and composite.spec.claimRef.name:
299
- resource.metadata.labels['crossplane.io/claim-namespace'] = composite.spec.claimRef.namespace
300
- resource.metadata.labels['crossplane.io/claim-name'] = composite.spec.claimRef.name
247
+ if self.composite.metadata.labels['crossplane.io/claim-name'] and self.composite.metadata.labels['crossplane.io/claim-namespace']:
248
+ resource.metadata.labels['crossplane.io/claim-namespace'] = self.composite.metadata.labels['crossplane.io/claim-namespace']
249
+ resource.metadata.labels['crossplane.io/claim-name'] = self.composite.metadata.labels['crossplane.io/claim-name']
250
+ elif self.composite.spec.claimRef.namespace and self.composite.spec.claimRef.name:
251
+ resource.metadata.labels['crossplane.io/claim-namespace'] = self.composite.spec.claimRef.namespace
252
+ resource.metadata.labels['crossplane.io/claim-name'] = self.composite.spec.claimRef.name
301
253
  resource.metadata.ownerReferences[0].controller = True
302
254
  resource.metadata.ownerReferences[0].blockOwnerDeletion = True
303
- resource.metadata.ownerReferences[0].apiVersion = composite.apiVersion
304
- resource.metadata.ownerReferences[0].kind = composite.kind
305
- resource.metadata.ownerReferences[0].name = composite.metadata.name
255
+ resource.metadata.ownerReferences[0].apiVersion = self.composite.apiVersion
256
+ resource.metadata.ownerReferences[0].kind = self.composite.kind
257
+ resource.metadata.ownerReferences[0].name = self.composite.metadata.name
306
258
  resource.metadata.ownerReferences[0].uid = ''
259
+ resource.ready = ready
307
260
  resources[protobuf.append] = resource
308
261
 
309
262
  # Format the returned desired composite
310
263
  composite = protobuf.Map()
311
- for name, value in request.desired.composite.resource:
264
+ for name, value in self.request.desired.composite.resource:
312
265
  composite[name] = value
313
- composite.apiVersion = request.observed.composite.resource.apiVersion
314
- composite.kind = request.observed.composite.resource.kind
266
+ composite.apiVersion = self.request.observed.composite.resource.apiVersion
267
+ composite.kind = self.request.observed.composite.resource.kind
315
268
  if self.args.include_full_xr:
316
- composite.metadata = request.observed.composite.resource.metadata
317
- if request.observed.composite.resource.spec:
318
- composite.spec = request.observed.composite.resource.spec
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
319
273
  else:
320
- if request.observed.composite.resource.metadata.namespace:
321
- composite.metadata.namespace = request.observed.composite.resource.metadata.namespace
322
- composite.metadata.name = request.observed.composite.resource.metadata.name
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
323
277
  # Add in the composite's status.conditions.
324
- if request.desired.composite.ready == fnv1.Ready.READY_FALSE:
278
+ if self.request.desired.composite.ready == fnv1.Ready.READY_FALSE:
325
279
  condition = self.create_condition('Ready', False, 'Creating')
326
- elif request.desired.composite.ready == fnv1.Ready.READY_UNSPECIFIED and len(unready):
327
- condition = self.create_condition('Ready', False, 'Creating', f"Unready resources: {', '.join(str(name) for name in unready)}")
280
+ elif self.request.desired.composite.ready == fnv1.Ready.READY_UNSPECIFIED and len(unready):
281
+ condition = self.create_condition('Ready', False, 'Creating', f"Unready resources: {','.join(str(name) for name in unready)}")
328
282
  else:
329
283
  condition = self.create_condition('Ready', True, 'Available')
330
284
  composite.status.conditions[protobuf.append] = condition
@@ -341,7 +295,7 @@ class Command(command.Command):
341
295
  apiVersion = 'render.crossplane.io/v1beta1',
342
296
  kind = 'Connection',
343
297
  )
344
- for key, value in request.desired.composite.connection_details:
298
+ for key, value in self.request.desired.composite.connection_details:
345
299
  connection.values[key] = value
346
300
  print('---')
347
301
  print(str(connection), end='')
@@ -364,25 +318,169 @@ class Command(command.Command):
364
318
  str(protobuf.Map(
365
319
  apiVersion = 'render.crossplane.io/v1beta1',
366
320
  kind = 'Context',
367
- values = request.context,
321
+ values = self.request.context,
368
322
  )),
369
323
  end='',
370
324
  )
371
325
 
372
- def create_composition(self, composite, module):
373
- composition = protobuf.Map()
374
- composition.apiVersion = 'apiextensions.crossplane.io/v1'
375
- composition.kind = 'Composition'
376
- composition.metadata.name = 'function-pythonic-render'
377
- composition.spec.compositeTypeRef.apiVersion = composite.apiVersion
378
- composition.spec.compositeTypeRef.kind = composite.kind
379
- composition.spec.mode = 'Pipeline'
380
- composition.spec.pipeline[0].step = 'function-pythonic-render'
381
- composition.spec.pipeline[0].functionRef.name = 'function-pythonic'
382
- composition.spec.pipeline[0].input.apiVersion = 'pythonic.fn.crossplane.io/v1alpha1'
383
- composition.spec.pipeline[0].input.kind = 'Composite'
384
- composition.spec.pipeline[0].input.composite = module
385
- return composition
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),
422
+ )
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
+
434
+ # Obtain observed resources if using external cluster
435
+ if self.kube_context:
436
+ async with asyncio.TaskGroup() as group:
437
+ if self.args.crossplane_v1:
438
+ refs = self.composite.spec.resourceRefs
439
+ else:
440
+ refs = self.composite.spec.crossplane.resourceRefs
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
+ )
464
+ if source:
465
+ name = source.metadata.annotations['crossplane.io/composition-resource-name']
466
+ if name:
467
+ resource = self.request.observed.resources[name]
468
+ if not resource:
469
+ await self.setup_resource(source, resource)
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)
386
484
 
387
485
  def collect_resources(self, resources):
388
486
  files = []
@@ -400,34 +498,51 @@ class Command(command.Command):
400
498
  for document in yaml.safe_load_all(file.read_text()):
401
499
  yield protobuf.Value(None, None, document)
402
500
 
403
- def setup_resource(self, source, secrets, resource):
501
+ async def setup_resource(self, source, resource):
404
502
  resource.resource = source
405
503
  namespace = source.spec.writeConnectionSecretToRef.namespace or source.metadata.namespace
406
504
  name = source.spec.writeConnectionSecretToRef.name
407
505
  if namespace and name:
408
- for secret in secrets:
506
+ connection = None
507
+ for secret in self.secrets:
409
508
  if secret.metadata.namespace == namespace and secret.metadata.name == name:
410
- resource.connection_details()
411
- for key, value in secret.data:
412
- resource.connection_details[key] = protobuf.B64Decode(value)
509
+ connection = secret
413
510
  break
414
-
415
- def fetch_requireds(self, requireds, secrets, name, selector, resources):
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)
518
+
519
+ async def fetch_requireds(self, name, selector, resources):
416
520
  if not name:
417
521
  return
418
522
  name = str(name)
419
523
  items = resources[name].items
420
524
  items() # Force this to get created
421
- for required in requireds:
525
+ for required in self.requireds:
422
526
  if selector.api_version == required.apiVersion and selector.kind == required.kind:
423
- if selector.match_name == required.metadata.name:
424
- self.setup_resource(required, secrets, items[protobuf.append])
425
- elif selector.match_labels.labels:
426
- for key, value in selector.match_labels.labels:
427
- if value != required.metadata.labels[key]:
428
- break
429
- else:
430
- self.setup_resource(required, secrets, items[protobuf.append])
527
+ if ((not selector.namespace and not required.metadata.namespace)
528
+ or (selector.namespace == required.metadata.namespace)
529
+ ):
530
+ if selector.match_name == required.metadata.name:
531
+ await self.setup_resource(required, items[protobuf.append])
532
+ elif selector.match_labels.labels:
533
+ for key, value in selector.match_labels.labels:
534
+ if value != required.metadata.labels[key]:
535
+ break
536
+ else:
537
+ await self.setup_resource(required, items[protobuf.append])
538
+ if not len(items) and self.kube_context:
539
+ if selector.match_name:
540
+ required = await self.kube_get(selector.kind, selector.api_version, selector.namespace, selector.match_name, False)
541
+ if required:
542
+ await self.setup_resource(required, items[protobuf.append])
543
+ elif selector.match_labels.labels:
544
+ for requiest in await kube_list(selector.kind, selector.api_version, selector.namespace, selector.match_labels.labels):
545
+ await self.setup_resource(required, items[protobuf.append])
431
546
 
432
547
  def copy_resource(self, source, destination):
433
548
  destination.resource = source.resource
@@ -460,3 +575,56 @@ class Command(command.Command):
460
575
  if message:
461
576
  condition['message'] = message
462
577
  return condition
578
+
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
+
588
+ async def kube_get(self, kind, apiVersion, namespace, name, required=True):
589
+ clazz = self.kube_clazz(kind, apiVersion, namespace)
590
+ try:
591
+ fullName = [str(kind), str(apiVersion), str(name)]
592
+ if namespace and len(namespace):
593
+ fullName.insert(-1, str(namespace))
594
+ resource = await clazz.get(str(name), namespace=str(namespace), api=self.kube_context)
595
+ else:
596
+ resource = await clazz.get(str(name), api=self.kube_context)
597
+ resource = protobuf.Value(None, None, resource.raw)
598
+ result = 'found'
599
+ except kr8s.NotFoundError:
600
+ if required:
601
+ print(f"Resource not found: {':'.join(fullName)}", file=sys.stderr)
602
+ sys.exit(1)
603
+ resource = None
604
+ result = 'missing'
605
+ self.logger.debug(f"Resource {result}: {':'.join(fullName)}")
606
+ return resource
607
+
608
+ async def kube_list(self, kind, apiVersion, namespace, labelSelector):
609
+ clazz = self.kube_clazz(kind, apiVersion, namespace)
610
+ resources = [
611
+ protobuf.Value(None, None, resource.raw)
612
+ async for resource in clazz.list(
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
+ )
619
+ ]
620
+ if self.logger.isEnabledFor(logging.DEBUG):
621
+ fullName = [str(kind), str(apiVersion)]
622
+ if namespace and len(namespace):
623
+ fullName.append(str(namespace))
624
+ fullName.append('&'.join(f"{label}={value}" for label, value in labelSelector))
625
+ if resources:
626
+ result = f"found {self.inflect.number_to_words(len(resources))}"
627
+ else:
628
+ result = 'missing'
629
+ self.logger.debug(f"Resources {result}: {':'.join(fullName)}")
630
+ return resources