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.
@@ -1,2 +1,2 @@
1
1
  # This is set at build time, using "hatch version"
2
- __version__ = "0.4.0"
2
+ __version__ = "0.4.2"
@@ -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',
@@ -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('=')
@@ -43,7 +43,7 @@ class Connection:
43
43
 
44
44
  def __set__(self, composite, values):
45
45
  connection = self.__get__(composite)
46
- coneection()
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"Dependency{resource.name}"] = field
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._composite_v1:
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):
@@ -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, debug=False, renderUnknowns=False, crossplane_v1=False):
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 self.debug:
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 self.debug:
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 self.debug and level:
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
@@ -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.debug, self.args.render_unknowns, self.args.crossplane_v1)
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:
@@ -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 in self._cache:
410
- return self._cache[key]
411
- if self._messages is _Unknown or key >= len(self._messages):
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
- value = self._messages[key]
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:
@@ -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.initialize_function()
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
- self.kube_context = await kr8s.asyncio.api(context=self.args.kube_context)
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.kube_context = None
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
- await self.setup_composite()
118
- await self.setup_composition()
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
- # 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()
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(self.args.debug, self.args.render_unknowns, self.args.crossplane_v1)
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 self.composition.spec.pipeline:
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
- sys.exit(1)
323
+ return None
138
324
  if not step.input.step:
139
325
  step.input.step = step.step
140
- self.request.input = step.input
326
+ request.input = step.input
141
327
 
142
328
  # Supply step requested credentials.
143
- self.request.credentials()
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 secret in self.secrets:
150
- if secret.metadata.namespace == namespace and secret.metadata.name == name:
151
- data = self.request.credentials[credential.name].credential_data.data
152
- data()
153
- for key, value in secret.data:
154
- data[key] = protobuf.B64Decode(value)
155
- break
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
- sys.exit(1)
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
- self.request.required_resources()
351
+ request.required_resources()
165
352
  for requirement in step.requirements.requiredResources:
166
- await self.fetch_requireds(requirement.requirementName, requirement, self.request.required_resources)
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.fetch_requireds(name, selector, self.request.required_resources)
356
+ await self.set_required(name, selector, request.required_resources, resources, kapi)
170
357
  # Fetch the now deprecated extra resources requested.
171
- self.request.extra_resources()
358
+ request.extra_resources()
172
359
  for name, selector in requirements.extra_resources:
173
- await self.fetch_requireds(name, selector, self.request.extra_resources)
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(self.request._message, None),
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
- self.request.context = response.context
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
- self.request.desired.resources()
196
- self.copy_resource(response.desired.composite, self.request.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, self.request.desired.resources[name])
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 = self.composite.metadata.labels['crossplane.io/composite']
410
+ prefix = composite.metadata.labels['crossplane.io/composite']
224
411
  if not prefix:
225
- prefix = self.composite.metadata.name
226
- for name, resource in self.request.desired.resources:
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 = self.request.observed.resources[name].resource
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 self.composite.metadata.namespace:
244
- resource.metadata.namespace = self.composite.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 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
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 = self.composite.apiVersion
256
- resource.metadata.ownerReferences[0].kind = self.composite.kind
257
- resource.metadata.ownerReferences[0].name = self.composite.metadata.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 self.request.desired.composite.resource:
451
+ for name, value in request.desired.composite.resource:
265
452
  composite[name] = value
266
- composite.apiVersion = self.request.observed.composite.resource.apiVersion
267
- composite.kind = self.request.observed.composite.resource.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 self.request.desired.composite.ready == fnv1.Ready.READY_FALSE:
456
+ if request.desired.composite.ready == fnv1.Ready.READY_FALSE:
279
457
  condition = self.create_condition('Ready', False, 'Creating')
280
- elif self.request.desired.composite.ready == fnv1.Ready.READY_UNSPECIFIED and len(unready):
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
- # Print the composite.
289
- print('---')
290
- print(str(composite), end='')
291
-
292
- # Print Composite connection if requested.
293
- if self.args.include_connection_xr:
294
- connection = protobuf.Map(
295
- apiVersion = 'render.crossplane.io/v1beta1',
296
- kind = 'Connection',
297
- )
298
- for key, value in self.request.desired.composite.connection_details:
299
- connection.values[key] = value
300
- print('---')
301
- print(str(connection), end='')
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
- # 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
- )
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
- 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)
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 fetch_requireds(self, name, selector, resources):
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 = resources[name].items
500
+ items = requireds[name].items
524
501
  items() # Force this to get created
525
- for required in self.requireds:
526
- if selector.api_version == required.apiVersion and selector.kind == required.kind:
527
- if ((not selector.namespace and not required.metadata.namespace)
528
- or (selector.namespace == required.metadata.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 == required.metadata.name:
531
- await self.setup_resource(required, items[protobuf.append])
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 != required.metadata.labels[key]:
511
+ if value != resource.metadata.labels[key]:
535
512
  break
536
513
  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])
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
- async def kube_get(self, kind, apiVersion, namespace, name, required=True):
589
- clazz = self.kube_clazz(kind, apiVersion, namespace)
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.kube_context)
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.kube_context)
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.debug(f"Resource {result}: {':'.join(fullName)}")
604
+ if self.logger:
605
+ self.logger.debug(f"Resource {result}: {':'.join(fullName)}")
606
606
  return resource
607
607
 
608
- async def kube_list(self, kind, apiVersion, namespace, labelSelector):
609
- clazz = self.kube_clazz(kind, apiVersion, namespace)
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
- namespace=str(namespace) if namespace and len(namespace) else None,
614
- label_selector={
615
- label: str(value)
616
- for label, value in labelSelector
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.0
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.42.5; extra == 'packages'
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.0
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.0
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.0
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,,