crossplane-function-pythonic 0.4.1__py3-none-any.whl → 0.5.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,2 +1,2 @@
1
1
  # This is set at build time, using "hatch version"
2
- __version__ = "0.4.1"
2
+ __version__ = "0.5.0"
@@ -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
 
@@ -79,6 +79,7 @@ class Command:
79
79
  logger.setLevel(logging.DEBUG if self.args.debug else logging.INFO)
80
80
  # Suppress noisy libraries, these can be overriden using --logger-level
81
81
  logging.getLogger('asyncio').setLevel(logging.INFO)
82
+ logging.getLogger('grpc').setLevel(logging.INFO)
82
83
  logging.getLogger('httpcore').setLevel(logging.INFO)
83
84
  logging.getLogger('httpx').setLevel(logging.WARNING)
84
85
  logging.getLogger('kr8s').setLevel(logging.INFO)
@@ -89,7 +89,7 @@ class Ready:
89
89
 
90
90
 
91
91
  class BaseComposite:
92
- def __init__(self, crossplane_v1, request, single_use, logger):
92
+ def __init__(self, crossplane_v1, request, logger):
93
93
  self.crossplane_v1 = crossplane_v1
94
94
  self.request = protobuf.Message(None, 'request', request.DESCRIPTOR, request, 'Function Request')
95
95
  response = fnv1.RunFunctionResponse(
@@ -104,10 +104,7 @@ class BaseComposite:
104
104
  )
105
105
  self.response = protobuf.Message(None, 'response', response.DESCRIPTOR, response)
106
106
  self.logger = logger
107
- if single_use:
108
- self.parameters = self.request.observed.composite.resource.spec.parameters
109
- else:
110
- self.parameters = self.request.input.parameters
107
+ self.parameters = self.request.input.parameters
111
108
  self.credentials = Credentials(self.request)
112
109
  self.context = self.response.context
113
110
  self.environment = self.context['apiextensions.crossplane.io/environment']
@@ -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 = {}
@@ -49,15 +48,13 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
49
48
  name.append(composite['metadata']['name'])
50
49
  logger = logging.getLogger('.'.join(name))
51
50
 
52
- if composite['apiVersion'] in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and composite['kind'] == 'Composite':
53
- if 'spec' not in composite or 'composite' not in composite['spec']:
54
- return self.fatal(request, logger, 'Missing spec "composite"')
55
- single_use = True
56
- composite = composite['spec']['composite']
51
+ if 'inlined' in request.input and request.input['inlined']:
52
+ if 'spec' not in composite or request.input['inlined'] not in composite['spec']:
53
+ return self.fatal(request, logger, f"Missing inlined spec.{request.input['inlined']}")
54
+ composite = composite['spec'][request.input['inlined']]
57
55
  else:
58
56
  if 'composite' not in request.input:
59
57
  return self.fatal(request, logger, 'Missing input "composite"')
60
- single_use = False
61
58
  composite = request.input['composite']
62
59
 
63
60
  # Ideally this is something the Function API provides
@@ -101,7 +98,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
101
98
  self.clazzes[composite] = clazz
102
99
 
103
100
  try:
104
- composite = clazz(self.crossplane_v1, request, single_use, logger)
101
+ composite = clazz(self.crossplane_v1, request, logger)
105
102
  except Exception as e:
106
103
  return self.fatal(request, logger, 'Instantiate', e)
107
104
 
@@ -182,7 +179,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
182
179
  for _, resource in sorted(entry for entry in composite.resources):
183
180
  dependencies = resource.desired._getDependencies
184
181
  if dependencies:
185
- if self.debug:
182
+ if composite.logger.isEnabledFor(logging.DEBUG):
186
183
  for destination, source in sorted(dependencies.items()):
187
184
  destination = self.trimFullName(destination)
188
185
  source = self.trimFullName(source)
@@ -278,7 +275,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
278
275
  if resource.unknownsFatal or (resource.unknownsFatal is None and composite.unknownsFatal):
279
276
  fatalResources.append(name)
280
277
  fatal = True
281
- if self.debug:
278
+ if composite.logger.isEnabledFor(logging.DEBUG):
282
279
  for destination, source in sorted(unknowns.items()):
283
280
  destination = self.trimFullName(destination)
284
281
  source = self.trimFullName(source)
@@ -319,7 +316,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
319
316
  message = 'All resources are composed'
320
317
  status = True
321
318
  result = None
322
- if not self.debug and level:
319
+ if not composite.logger.isEnabledFor(logging.DEBUG) and level:
323
320
  level(message)
324
321
  composite.conditions.ResourcesComposed(reason, message, status)
325
322
  if result:
@@ -378,6 +375,7 @@ class Module:
378
375
  self.List = pythonic.List
379
376
  self.Unknown = pythonic.Unknown
380
377
  self.Yaml = pythonic.Yaml
378
+ self.YamlAll = pythonic.YamlAll
381
379
  self.Json = pythonic.Json
382
380
  self.B64Encode = pythonic.B64Encode
383
381
  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:
@@ -789,8 +796,10 @@ class Value:
789
796
  def __contains__(self, item):
790
797
  match self._kind:
791
798
  case 'struct_value':
799
+ item = self._validate_key(item)
792
800
  return item in self._value.struct_value.fields or item in self._unknowns
793
801
  case 'Struct':
802
+ item = self._validate_key(item)
794
803
  return item in self._value.fields or item in self._unknowns
795
804
  case 'list_value' | 'ListValue':
796
805
  for value in self:
@@ -1,9 +1,9 @@
1
1
 
2
2
  import asyncio
3
- import kr8s.asyncio
4
3
  import importlib
5
4
  import inflect
6
5
  import inspect
6
+ import kr8s
7
7
  import logging
8
8
  import pathlib
9
9
  import sys
@@ -17,6 +17,9 @@ from . import (
17
17
  protobuf,
18
18
  )
19
19
 
20
+ INFLECT = inflect.engine()
21
+ INFLECT.classical(all=False)
22
+
20
23
 
21
24
  class Command(command.Command):
22
25
  name = 'render'
@@ -55,7 +58,7 @@ class Command(command.Command):
55
58
  action='append',
56
59
  default=[],
57
60
  metavar='KEY=VALUE',
58
- help='Context key-value pairs to pass to the Function pipeline. Values must be YAML/JSON. Keys take precedence over --context-files.',
61
+ help='Context key-value pairs to pass to the Function pipeline. Values must be sYAML/JSON. Keys take precedence over --context-files.',
59
62
  )
60
63
  parser.add_argument(
61
64
  '--observed-resources', '-o',
@@ -103,88 +106,278 @@ class Command(command.Command):
103
106
  )
104
107
 
105
108
  def initialize(self):
106
- self.initialize_function()
109
+ if self.args:
110
+ self.initialize_function()
107
111
  self.logger = logging.getLogger(__name__)
108
- self.inflect = inflect.engine()
109
- self.inflect.classical(all=False)
110
112
 
111
113
  async def run(self):
112
114
  if self.args.kube_context:
113
- self.kube_context = await kr8s.asyncio.api(context=self.args.kube_context)
115
+ api = await kr8s.asyncio.api(context=self.args.kube_context)
116
+ else:
117
+ api = None
118
+ composite = await self.setup_composite(api)
119
+ observed = self.collect_resources(self.args.observed_resources)
120
+ composition = await self.setup_composition(composite, api)
121
+ resources = self.collect_resources(self.args.required_resources)
122
+ resources += self.collect_resources(self.args.secret_store)
123
+ resources.sort(key=lambda resource: str(resource.metadata.name))
124
+ context = self.setup_context()
125
+
126
+ render = await self.render(composite, observed, composition, resources, context, api, self.args.render_unknowns, self.args.crossplane_v1)
127
+ if not render:
128
+ sys.exit(1)
129
+
130
+ if self.args.include_full_xr:
131
+ render.composite.metadata = composite.metadata
132
+ del render.composite.metadata.managedFields
133
+ if composite.spec:
134
+ render.composite.spec = composite.spec
114
135
  else:
115
- self.kube_context = None
136
+ if composite.metadata.namespace:
137
+ render.composite.metadata.namespace = composite.metadata.namespace
138
+ render.composite.metadata.name = composite.metadata.name
139
+
140
+ # Print the composite.
141
+ print('---')
142
+ print(str(render.composite), end='')
143
+ # Print Composite connection if requested.
144
+ if self.args.include_connection_xr:
145
+ print('---')
146
+ print(str(render.connection), end='')
147
+ # Print the composed resources.
148
+ for resource in sorted(render.resources, key=lambda resource: str(resource.metadata.annotations['crossplane.io/composition-resource-name'])):
149
+ print('---')
150
+ print(str(resource), end='')
151
+ # Print the results (AKA events) if requested.
152
+ if self.args.include_function_results:
153
+ for result in render.results:
154
+ print('---')
155
+ print(str(result), end='')
156
+ # Print the final context if requested.
157
+ if self.args.include_context:
158
+ print('---')
159
+ print(str(render.context), end='')
160
+
161
+ async def setup_composite(self, api=None):
162
+ # Obtain the Composite to render.
163
+ if self.args.composite.is_file():
164
+ return protobuf.Yaml(self.args.composite.read_text())
165
+ if not api:
166
+ print(f"Composite \"{self.args.composite}\" is not a file", file=sys.stderr)
167
+ sys.exit(1)
168
+ composite = str(self.args.composite).split(':')
169
+ if len(composite) == 3:
170
+ namespace = None
171
+ elif len(composite) == 4:
172
+ if len(composite[2]):
173
+ namespace = composite[2]
174
+ else:
175
+ namespace = None
176
+ else:
177
+ print(f"Composite \"{self.args.composite}\" is not kind:apiVersion:namespace:name", file=sys.stderr)
178
+ sys.exit(1)
179
+ composite = await self.kr8s_get(api, composite[0], composite[1], namespace, composite[-1])
180
+ if not composite:
181
+ print(f"Composite \"{self.args.composite}\" not found", file=sys.stderr)
182
+ sys.exit(1)
183
+ return composite
184
+
185
+ async def setup_composition(self, composite, api=None):
186
+ # Obtain the Composition that will be used to render the Composite.
187
+ if not self.args.composition:
188
+ return None
189
+ if self.args.composition.is_file():
190
+ composition = self.args.composition.read_text()
191
+ if self.args.composition.suffix == '.py':
192
+ return self.create_composition(compsite, composition)
193
+ composition = protobuf.Yaml(composition)
194
+ if not len(composition.spec.pipeline):
195
+ print(f"Composition file does not contain any pipeline steps: {self.args.composition}", file=sys.stderr)
196
+ sys.exit(1)
197
+ return composition
198
+ composition = str(self.args.composition).rsplit('.', 1)
199
+ if len(composition) == 1:
200
+ print(f"Composition class name does not include module: {self.args.composition}", file=sys.stderr)
201
+ sys.exit(1)
202
+ try:
203
+ module = importlib.import_module(composition[0])
204
+ except Exception as e:
205
+ print(e)
206
+ print(f"Unable to import composition module: {composition[0]}", file=sys.stderr)
207
+ sys.exit(1)
208
+ clazz = getattr(module, composition[1], None)
209
+ if not clazz:
210
+ print(f"Composition class {composition[0]} does not define: {composition[1]}", file=sys.stderr)
211
+ sys.exit(1)
212
+ if not inspect.isclass(clazz):
213
+ print(f"Composition class {self.args.composition} is not a class", file=sys.stderr)
214
+ sys.exit(1)
215
+ if not issubclass(clazz, composite.BaseComposite):
216
+ print(f"Composition class {self.args.composition} is not a subclass of BaseComposite", file=sys.stderr)
217
+ sys.exit(1)
218
+ return self.create_composition(composite, self.args.composition)
219
+
220
+ def create_composition(self, composite, module=''):
221
+ composition = protobuf.Map()
222
+ composition.apiVersion = 'apiextensions.crossplane.io/v1'
223
+ composition.kind = 'Composition'
224
+ composition.metadata.name = 'function-pythonic-render'
225
+ composition.spec.compositeTypeRef.apiVersion = composite.apiVersion
226
+ composition.spec.compositeTypeRef.kind = composite.kind
227
+ composition.spec.mode = 'Pipeline'
228
+ composition.spec.pipeline[0].step = 'function-pythonic-render'
229
+ composition.spec.pipeline[0].functionRef.name = 'function-pythonic'
230
+ composition.spec.pipeline[0].input.apiVersion = 'pythonic.fn.crossplane.io/v1alpha1'
231
+ composition.spec.pipeline[0].input.kind = 'Composite'
232
+ composition.spec.pipeline[0].input.composite = str(module)
233
+ return composition
234
+
235
+ def collect_resources(self, entries):
236
+ resources = []
237
+ for entry in entries:
238
+ if entry.is_file():
239
+ for document in yaml.safe_load_all(entry.read_text()):
240
+ resources.append(protobuf.Value(None, None, document))
241
+ elif entry.is_dir():
242
+ for file in entry.iterdir():
243
+ if file.suffix in ('.yaml', '.yml'):
244
+ for document in yaml.safe_load_all(file.read_text()):
245
+ resources.append(protobuf.Value(None, None, document))
246
+ else:
247
+ print(f"Specified resource is not a file or a directory: {entry}", file=sys.stderr)
248
+ sys.exit(1)
249
+ return resources
116
250
 
117
- await self.setup_composite()
118
- await self.setup_composition()
251
+ def setup_context(self):
252
+ # Load the request context with any specified command line options.
253
+ context = protobuf.Map()
254
+ for entry in self.args.context_files:
255
+ key_path = entry.split('=', 1)
256
+ if len(key_path) != 2:
257
+ print(f"Invalid --context-files: {entry}", file=sys.stderr)
258
+ sys.exit(1)
259
+ path = pathlib.Path(key_path[1])
260
+ if not path.is_file():
261
+ print(f"Invalid --context-files {path} is not a file", file=sys.stderr)
262
+ sys.exit(1)
263
+ context[key_path[0]] = protobuf.Yaml(path.read_text())
264
+ for entry in self.args.context_values:
265
+ key_value = entry.split('=', 1)
266
+ if len(key_value) != 2:
267
+ print(f"Invalid --context-values: {entry}", file=sys.stderr)
268
+ sys.exit(1)
269
+ context[key_value[0]] = protobuf.Yaml(key_value[1])
270
+ return context
119
271
 
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()
272
+ async def render(self, composite, observed=[], composition=None, resources=[], context=None, api=None, render_unknowns=False, crossplane_v1=False, composite_observeds=True):
273
+ # Create the request used when running Composition steps.
274
+ request = protobuf.Message(None, 'request', fnv1.RunFunctionRequest.DESCRIPTOR, fnv1.RunFunctionRequest())
275
+ if context is not None:
276
+ request.context = context
277
+
278
+ # Establish the request observed composite.
279
+ await self.set_resource(composite, request.observed.composite, resources, api)
280
+ # Establish the manually configured observed resources.
281
+ if observed:
282
+ async with asyncio.TaskGroup() as group:
283
+ for resource in observed:
284
+ name = resource.metadata.annotations['crossplane.io/composition-resource-name']
285
+ if name:
286
+ group.create_task(self.set_resource(resource, request.observed.resources[name], resources, api))
287
+ if api and composite_observeds:
288
+ refs = composite.spec.crossplane.resourceRefs
289
+ if not refs:
290
+ refs = composite.spec.resourceRefs
291
+ if refs:
292
+ async with asyncio.TaskGroup() as group:
293
+ for ref in refs:
294
+ group.create_task(self.get_composite_ref(composite, ref, request, resources, api))
295
+
296
+ if not composition:
297
+ if composite.apiVersion in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and composite.kind == 'Composite':
298
+ composition = self.create_composition(composite)
299
+ else:
300
+ if not api:
301
+ print('"composition" required', file=sys.stderr)
302
+ return None
303
+ revision = composite.spec.crossplane.compositionRevisionRef
304
+ if not revision.name:
305
+ # Crossplane V1 location
306
+ revision = composite.spec.compositionRevisionRef
307
+ if not revision.name:
308
+ print('Composite does not contain a CompositionRevision name', file=sys.stderr)
309
+ return None
310
+ composition = await self.kr8s_get(api, 'CompositionRevision', 'apiextensions.crossplane.io/v1', None, revision.name)
311
+ if not composition:
312
+ print(f"Compositioin \"{revision.name}\" not found", file=sys.stderr)
313
+ return None
124
314
 
125
315
  # These will hold the response conditions and results.
126
316
  conditions = protobuf.List()
127
317
  results = protobuf.List()
128
318
 
129
319
  # 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)
320
+ runner = function.FunctionRunner(render_unknowns, crossplane_v1)
131
321
  fatal = False
132
322
 
133
323
  # Process the composition pipeline steps.
134
- for step in self.composition.spec.pipeline:
324
+ for step in composition.spec.pipeline:
135
325
  if step.functionRef.name != 'function-pythonic':
136
326
  print(f"Only function-pythonic functions can be run: {step.functionRef.name}", file=sys.stderr)
137
- sys.exit(1)
327
+ return None
138
328
  if not step.input.step:
139
329
  step.input.step = step.step
140
- self.request.input = step.input
330
+ request.input = step.input
141
331
 
142
332
  # Supply step requested credentials.
143
- self.request.credentials()
333
+ request.credentials()
144
334
  for credential in step.credentials:
145
335
  if credential.source == 'Secret' and credential.secretRef:
146
336
  namespace = credential.secretRef.namespace
147
337
  name = credential.secretRef.name
148
338
  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
339
+ for resource in resources:
340
+ if resource.kind == 'Secret' and resource.apiVersion == 'v1':
341
+ if resource.metadata.namespace == namespace and resource.metadata.name == name:
342
+ data = request.credentials[credential.name].credential_data.data
343
+ data()
344
+ for key, value in resource.data:
345
+ data[key] = protobuf.B64Decode(value)
346
+ break
156
347
  else:
157
348
  print(f"Step \"{step.step}\" secret not found: {namespace}/{name}", file=sys.stderr)
158
- sys.exit(1)
349
+ return None
159
350
 
160
351
  # Track what extra/required resources have been processed.
161
352
  requirements = protobuf.Message(None, 'requirements', fnv1.Requirements.DESCRIPTOR, fnv1.Requirements())
162
353
  for _ in range(5):
163
354
  # Fetch the step bootstrap resources specified.
164
- self.request.required_resources()
355
+ request.required_resources()
165
356
  for requirement in step.requirements.requiredResources:
166
- await self.fetch_requireds(requirement.requirementName, requirement, self.request.required_resources)
357
+ await self.set_required(requirement.requirementName, requirement, request.required_resources, resources, api)
167
358
  # Fetch the required resources requested.
168
359
  for name, selector in requirements.resources:
169
- await self.fetch_requireds(name, selector, self.request.required_resources)
360
+ await self.set_required(name, selector, request.required_resources, resources, api)
170
361
  # Fetch the now deprecated extra resources requested.
171
- self.request.extra_resources()
362
+ request.extra_resources()
172
363
  for name, selector in requirements.extra_resources:
173
- await self.fetch_requireds(name, selector, self.request.extra_resources)
364
+ await self.set_required(name, selector, request.extra_resources, resources, api)
174
365
  # Run the step using the function-pythonic function runner.
175
366
  response = protobuf.Message(
176
367
  None,
177
368
  'response',
178
369
  fnv1.RunFunctionResponse.DESCRIPTOR,
179
- await runner.RunFunction(self.request._message, None),
370
+ await runner.RunFunction(request._message, None),
180
371
  )
372
+ # Copy the response context to the request context to use in subsequent steps.
373
+ request.context = response.context
181
374
  # All done if there is a fatal result.
182
375
  for result in response.results:
183
376
  if result.severity == fnv1.Severity.SEVERITY_FATAL:
184
377
  fatal = True
185
378
  break
186
- # Copy the response context to the request context to use in subsequent steps.
187
- self.request.context = response.context
379
+ if fatal:
380
+ break
188
381
  # Exit this loop if the function has not requested additional extra/required resources.
189
382
  if response.requirements == requirements:
190
383
  break
@@ -192,10 +385,10 @@ class Command(command.Command):
192
385
  requirements = response.requirements
193
386
 
194
387
  # 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)
388
+ request.desired.resources()
389
+ self.copy_resource(response.desired.composite, request.desired.composite)
197
390
  for name, resource in response.desired.resources:
198
- self.copy_resource(resource, self.request.desired.resources[name])
391
+ self.copy_resource(resource, request.desired.resources[name])
199
392
 
200
393
  # Collect the step's returned conditions.
201
394
  for condition in response.conditions:
@@ -220,10 +413,10 @@ class Command(command.Command):
220
413
  # Collect and format all the returned desired composed resources.
221
414
  resources = protobuf.List()
222
415
  unready = protobuf.List()
223
- prefix = self.composite.metadata.labels['crossplane.io/composite']
416
+ prefix = composite.metadata.labels['crossplane.io/composite']
224
417
  if not prefix:
225
- prefix = self.composite.metadata.name
226
- for name, resource in self.request.desired.resources:
418
+ prefix = composite.metadata.name
419
+ for name, resource in request.desired.resources:
227
420
  if resource.ready == fnv1.Ready.READY_TRUE:
228
421
  ready = True
229
422
  elif resource.ready == fnv1.Ready.READY_FALSE:
@@ -233,51 +426,42 @@ class Command(command.Command):
233
426
  if not ready:
234
427
  unready[protobuf.append] = name
235
428
  resource = resource.resource
236
- observed = self.request.observed.resources[name].resource
429
+ observed = request.observed.resources[name].resource
237
430
  if observed:
238
431
  for key in ('namespace', 'generateName', 'name'):
239
432
  if observed.metadata[key]:
240
433
  resource.metadata[key] = observed.metadata[key]
241
434
  if not resource.metadata.name and not resource.metadata.generateName:
242
435
  resource.metadata.generateName = f"{prefix}-"
243
- if self.composite.metadata.namespace:
244
- resource.metadata.namespace = self.composite.metadata.namespace
436
+ if composite.metadata.namespace:
437
+ resource.metadata.namespace = composite.metadata.namespace
245
438
  resource.metadata.annotations['crossplane.io/composition-resource-name'] = name
246
439
  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
440
+ if composite.metadata.labels['crossplane.io/claim-name'] and composite.metadata.labels['crossplane.io/claim-namespace']:
441
+ resource.metadata.labels['crossplane.io/claim-namespace'] = composite.metadata.labels['crossplane.io/claim-namespace']
442
+ resource.metadata.labels['crossplane.io/claim-name'] = composite.metadata.labels['crossplane.io/claim-name']
443
+ elif composite.spec.claimRef.namespace and composite.spec.claimRef.name:
444
+ resource.metadata.labels['crossplane.io/claim-namespace'] = composite.spec.claimRef.namespace
445
+ resource.metadata.labels['crossplane.io/claim-name'] = composite.spec.claimRef.name
253
446
  resource.metadata.ownerReferences[0].controller = True
254
447
  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
448
+ resource.metadata.ownerReferences[0].apiVersion = composite.apiVersion
449
+ resource.metadata.ownerReferences[0].kind = composite.kind
450
+ resource.metadata.ownerReferences[0].name = composite.metadata.name
258
451
  resource.metadata.ownerReferences[0].uid = ''
259
452
  resource.ready = ready
260
453
  resources[protobuf.append] = resource
261
454
 
262
455
  # Format the returned desired composite
263
456
  composite = protobuf.Map()
264
- for name, value in self.request.desired.composite.resource:
457
+ for name, value in request.desired.composite.resource:
265
458
  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
459
+ composite.apiVersion = request.observed.composite.resource.apiVersion
460
+ composite.kind = request.observed.composite.resource.kind
277
461
  # Add in the composite's status.conditions.
278
- if self.request.desired.composite.ready == fnv1.Ready.READY_FALSE:
462
+ if request.desired.composite.ready == fnv1.Ready.READY_FALSE:
279
463
  condition = self.create_condition('Ready', False, 'Creating')
280
- elif self.request.desired.composite.ready == fnv1.Ready.READY_UNSPECIFIED and len(unready):
464
+ elif request.desired.composite.ready == fnv1.Ready.READY_UNSPECIFIED and len(unready):
281
465
  condition = self.create_condition('Ready', False, 'Creating', f"Unready resources: {','.join(str(name) for name in unready)}")
282
466
  else:
283
467
  condition = self.create_condition('Ready', True, 'Available')
@@ -285,263 +469,82 @@ class Command(command.Command):
285
469
  for condition in conditions:
286
470
  composite.status.conditions[protobuf.append] = condition
287
471
 
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
- revision = self.composite.spec.crossplane.compositionRevisionRef
360
- if not revision.name:
361
- # Crossplane V1 location
362
- revision = self.composite.spec.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),
472
+ return protobuf.Map(
473
+ composite=composite,
474
+ connection=protobuf.Map(
475
+ apiVersion='render.crossplane.io/v1beta1',
476
+ kind='Connection',
477
+ values={key: value for key, value in request.desired.composite.connection_details}
478
+ ),
479
+ resources=resources,
480
+ results=results,
481
+ context=protobuf.Map(
482
+ apiVersion='render.crossplane.io/v1beta1',
483
+ kind='Context',
484
+ values=request.context,
485
+ ),
422
486
  )
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
487
 
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
- refs = self.composite.spec.crossplane.resourceRefs
438
- if not refs:
439
- refs = self.composite.spec.resourceRefs
440
- for ref in refs:
441
- group.create_task(self.setup_observed_resource(ref))
442
-
443
- # Establish the manually configured observed resources.
444
- for resource in self.collect_resources(self.args.observed_resources):
445
- name = resource.metadata.annotations['crossplane.io/composition-resource-name']
446
- if name:
447
- await self.setup_resource(resource, self.request.observed.resources[name])
448
-
449
- async def setup_observed_resource(self, ref):
450
- if ref.namespace:
451
- namespace = str(ref.namespace)
452
- elif self.composite.metadata.namespace:
453
- namespace = str(self.composite.metadata.namespace)
454
- else:
455
- namespace = None
456
- source = await self.kube_get(
457
- str(ref.kind),
458
- str(ref.apiVersion),
459
- namespace,
460
- str(ref.name),
461
- False,
462
- )
488
+ async def get_composite_ref(self, composite, ref, request, resources, api):
489
+ namespace = ref.namespace
490
+ if not namespace:
491
+ namespace = composite.metadata.namespace
492
+ if not namespace:
493
+ namespace = None
494
+ source = await self.kr8s_get(api, ref.kind, ref.apiVersion, namespace, ref.name)
463
495
  if source:
464
496
  name = source.metadata.annotations['crossplane.io/composition-resource-name']
465
497
  if name:
466
- resource = self.request.observed.resources[name]
467
- if not resource:
468
- await self.setup_resource(source, resource)
469
-
470
- def create_composition(self, module=''):
471
- self.composition = protobuf.Map()
472
- self.composition.apiVersion = 'apiextensions.crossplane.io/v1'
473
- self.composition.kind = 'Composition'
474
- self.composition.metadata.name = 'function-pythonic-render'
475
- self.composition.spec.compositeTypeRef.apiVersion = self.composite.apiVersion
476
- self.composition.spec.compositeTypeRef.kind = self.composite.kind
477
- self.composition.spec.mode = 'Pipeline'
478
- self.composition.spec.pipeline[0].step = 'function-pythonic-render'
479
- self.composition.spec.pipeline[0].functionRef.name = 'function-pythonic'
480
- self.composition.spec.pipeline[0].input.apiVersion = 'pythonic.fn.crossplane.io/v1alpha1'
481
- self.composition.spec.pipeline[0].input.kind = 'Composite'
482
- self.composition.spec.pipeline[0].input.composite = str(module)
483
-
484
- def collect_resources(self, resources):
485
- files = []
486
- for resource in resources:
487
- if resource.is_file():
488
- files.append(resource)
489
- elif resource.is_dir():
490
- for file in resource.iterdir():
491
- if file.suffix in ('.yaml', '.yml'):
492
- files.append(file)
493
- else:
494
- print(f"Specified resource is not a file or a directory: {resource}", file=sys.stderr)
495
- sys.exit(1)
496
- for file in files:
497
- for document in yaml.safe_load_all(file.read_text()):
498
- yield protobuf.Value(None, None, document)
498
+ destination = request.observed.resources[name]
499
+ if not destination: # Do not override manual observed
500
+ await self.set_resource(source, destination, resources, api)
499
501
 
500
- async def setup_resource(self, source, resource):
501
- resource.resource = source
502
- namespace = source.spec.writeConnectionSecretToRef.namespace or source.metadata.namespace
503
- name = source.spec.writeConnectionSecretToRef.name
504
- if namespace and name:
505
- connection = None
506
- for secret in self.secrets:
507
- if secret.metadata.namespace == namespace and secret.metadata.name == name:
508
- connection = secret
509
- break
510
- else:
511
- if self.kube_context:
512
- connection = await self.kube_get('Secret', 'v1', namespace, name, False)
513
- if connection:
514
- resource.connection_details()
515
- for key, value in connection.data:
516
- resource.connection_details[key] = protobuf.B64Decode(value)
517
-
518
- async def fetch_requireds(self, name, selector, resources):
502
+ async def set_required(self, name, selector, requireds, resources=[], api=None):
519
503
  if not name:
520
504
  return
521
505
  name = str(name)
522
- items = resources[name].items
506
+ items = requireds[name].items
523
507
  items() # Force this to get created
524
- for required in self.requireds:
525
- if selector.api_version == required.apiVersion and selector.kind == required.kind:
526
- if ((not selector.namespace and not required.metadata.namespace)
527
- or (selector.namespace == required.metadata.namespace)
508
+ for resource in resources:
509
+ if selector.api_version == resource.apiVersion and selector.kind == resource.kind:
510
+ if ((not len(selector.namespace) and not len(resource.metadata.namespace))
511
+ or (selector.namespace == resource.metadata.namespace)
528
512
  ):
529
- if selector.match_name == required.metadata.name:
530
- await self.setup_resource(required, items[protobuf.append])
513
+ if selector.match_name == resource.metadata.name:
514
+ await self.set_resource(resource, items[protobuf.append], resources, api)
531
515
  elif selector.match_labels.labels:
532
516
  for key, value in selector.match_labels.labels:
533
- if value != required.metadata.labels[key]:
517
+ if value != resource.metadata.labels[key]:
534
518
  break
535
519
  else:
536
- await self.setup_resource(required, items[protobuf.append])
537
- if not len(items) and self.kube_context:
520
+ await self.set_resource(resource, items[protobuf.append], resources, api)
521
+ if not len(items) and api:
538
522
  if len(selector.match_name):
539
- required = await self.kube_get(selector.kind, selector.api_version, selector.namespace, selector.match_name, False)
540
- if required:
541
- await self.setup_resource(required, items[protobuf.append])
523
+ resource = await self.kr8s_get(api, selector.kind, selector.api_version, selector.namespace, selector.match_name)
524
+ if resource:
525
+ await self.set_resource(resource, items[protobuf.append], resources, api)
542
526
  elif len(selector.match_labels.labels):
543
- for required in await self.kube_list(selector.kind, selector.api_version, selector.namespace, selector.match_labels.labels):
544
- await self.setup_resource(required, items[protobuf.append])
527
+ for resource in await self.kr8s_list(api, selector.kind, selector.api_version, selector.namespace, selector.match_labels.labels):
528
+ await self.set_resource(resource, items[protobuf.append], resources, api)
529
+
530
+ async def set_resource(self, source, destination, resources=[], api=None):
531
+ destination.resource = source
532
+ namespace = source.spec.writeConnectionSecretToRef.namespace or source.metadata.namespace
533
+ name = source.spec.writeConnectionSecretToRef.name
534
+ if namespace and name:
535
+ connection = None
536
+ for resource in resources:
537
+ if resource.kind == 'Secret' and resource.apiVersion == 'v1':
538
+ if resource.metadata.namespace == namespace and resource.metadata.name == name:
539
+ connection = resource
540
+ break
541
+ else:
542
+ if api:
543
+ connection = await self.kr8s_get(api, 'Secret', 'v1', namespace, name)
544
+ if connection:
545
+ destination.connection_details()
546
+ for key, value in connection.data:
547
+ destination.connection_details[key] = protobuf.B64Decode(value)
545
548
 
546
549
  def copy_resource(self, source, destination):
547
550
  destination.resource = source.resource
@@ -575,56 +578,53 @@ class Command(command.Command):
575
578
  condition['message'] = message
576
579
  return condition
577
580
 
578
- def kube_clazz(self, kind, apiVersion, namespaced):
579
- kind = str(kind)
580
- apiVersion = str(apiVersion)
581
- try:
582
- return kr8s.asyncio.objects.get_class(kind, apiVersion, True)
583
- except KeyError:
584
- pass
585
- return kr8s.asyncio.objects.new_class(kind, apiVersion, True, bool(namespaced) and len(namespaced), plural=self.inflect.plural_noun(kind))
586
-
587
- async def kube_get(self, kind, apiVersion, namespace, name, required=True):
588
- clazz = self.kube_clazz(kind, apiVersion, namespace)
581
+ async def kr8s_get(self, api, kind, apiVersion, namespace, name):
582
+ namespaced = namespace and len(namespace)
583
+ clazz = self.kr8s_class(kind, apiVersion, namespaced)
589
584
  try:
590
585
  fullName = [str(kind), str(apiVersion), str(name)]
591
- if namespace and len(namespace):
586
+ if namespaced:
592
587
  fullName.insert(-1, str(namespace))
593
- resource = await clazz.get(str(name), namespace=str(namespace), api=self.kube_context)
588
+ resource = await clazz.get(str(name), namespace=str(namespace), api=api)
594
589
  else:
595
- resource = await clazz.get(str(name), api=self.kube_context)
590
+ resource = await clazz.get(str(name), api=api)
596
591
  resource = protobuf.Value(None, None, resource.raw)
597
592
  result = 'found'
598
593
  except kr8s.NotFoundError:
599
- if required:
600
- print(f"Resource not found: {':'.join(fullName)}", file=sys.stderr)
601
- sys.exit(1)
602
594
  resource = None
603
595
  result = 'missing'
604
596
  self.logger.debug(f"Resource {result}: {':'.join(fullName)}")
605
597
  return resource
606
598
 
607
- async def kube_list(self, kind, apiVersion, namespace, labelSelector):
608
- clazz = self.kube_clazz(kind, apiVersion, namespace)
599
+ async def kr8s_list(self, api, kind, apiVersion, namespace, labelSelector):
600
+ namespaced = namespace and len(namespace)
601
+ clazz = self.kr8s_class(kind, apiVersion, namespaced)
609
602
  resources = [
610
603
  protobuf.Value(None, None, resource.raw)
611
604
  async for resource in clazz.list(
612
- namespace=str(namespace) if namespace and len(namespace) else None,
613
- label_selector={
614
- label: str(value)
615
- for label, value in labelSelector
616
- },
617
- api=self.kube_context,
605
+ namespace=str(namespace) if namespaced else None,
606
+ label_selector={
607
+ label: str(value)
608
+ for label, value in labelSelector
609
+ },
610
+ api=api,
618
611
  )
619
612
  ]
620
613
  if self.logger.isEnabledFor(logging.DEBUG):
621
614
  fullName = [str(kind), str(apiVersion)]
622
- if namespace and len(namespace):
615
+ if namespaced:
623
616
  fullName.append(str(namespace))
624
617
  fullName.append('&'.join(f"{label}={value}" for label, value in labelSelector))
625
618
  if resources:
626
- result = f"found {self.inflect.number_to_words(len(resources))}"
619
+ result = f"found {INFLECT.number_to_words(len(resources))}"
627
620
  else:
628
621
  result = 'missing'
629
622
  self.logger.debug(f"Resources {result}: {':'.join(fullName)}")
630
623
  return resources
624
+
625
+ def kr8s_class(self, kind, apiVersion, namespaced):
626
+ try:
627
+ return kr8s.asyncio.objects.get_class(str(kind), str(apiVersion), True)
628
+ except KeyError:
629
+ pass
630
+ return kr8s.asyncio.objects.new_class(str(kind), str(apiVersion), True, namespaced, plural=INFLECT.plural_noun(str(kind)).lower())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossplane-function-pythonic
3
- Version: 0.4.1
3
+ Version: 0.5.0
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
@@ -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.1
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.1
98
+ package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.2
99
99
  runtimeConfigRef:
100
100
  name: function-pythonic
101
101
  --
@@ -199,6 +199,11 @@ subnet.spec.forProvider.cidrBlock = '10.0.0.0/20'
199
199
  ```
200
200
  Will generate the appropriate Crossplane Usage resource.
201
201
 
202
+ ## API Documentation
203
+
204
+ - [Composite API (`composite.py`)](https://github.com/crossplane-contrib/function-pythonic/blob/main/docs/composite.md)
205
+ - [Protobuf Wrapper API (`protobuf.py`)](https://github.com/crossplane-contrib/function-pythonic/blob/main/docs/protobuf.md)
206
+
202
207
  ## Pythonic access of Protobuf Messages
203
208
 
204
209
  All Protobuf messages are wrapped by a set of python classes which enable using
@@ -241,6 +246,7 @@ The following functions are provided to create Protobuf structures:
241
246
  | List | Create a new Protobuf list |
242
247
  | Unknown | Create a new Protobuf unknown placeholder |
243
248
  | Yaml | Create a new Protobuf structure from a yaml string |
249
+ | YamlAll | Create a new Protobuf list from a yaml string |
244
250
  | Json | Create a new Protobuf structure from a json string |
245
251
  | B64Encode | Encode a string into base 64 |
246
252
  | B64Decode | Decode a string from base 64 |
@@ -405,25 +411,78 @@ optionally to the Claim.
405
411
  | Result.message | String | Human-readable details about the result |
406
412
  | Result.claim | Boolean | Also apply the result to the claim |
407
413
 
408
- ## Single use Composites
414
+ ## Inlined Composites
409
415
 
410
416
  Tired of creating a CompositeResourceDefinition, a Composition, and a Composite
411
- just to run that Composition once in a single use or initialize task?
417
+ just to run that Composition once in a setup or initialize task?
412
418
 
413
- function-pythonic installs a `Composite` CompositeResourceDefinition that enables
414
- creating such tasks using a single Composite resource:
419
+ function-pythonic supports "inlined" Compositions, where the python module
420
+ is obtained from a field in the Composite's spec.
415
421
  ```yaml
416
- apiVersion: pythonic.fn.crossplane.io/v1alpha1
417
- kind: Composite
422
+ apiVersion: inlined.example.org/v1alpha1
423
+ kind: Step
418
424
  metadata:
419
- name: composite-example
425
+ name: inlined-example
420
426
  spec:
421
427
  composite: |
422
428
  class HelloComposite(BaseComposite):
423
429
  def compose(self):
424
- self.status.composite = 'Hello, World!'
430
+ self.status.step = 'Hello, World!'
431
+ ```
432
+ The CompositeResourceDefinition and Composition to support the above example:
433
+ ```yaml
434
+ apiVersion: apiextensions.crossplane.io/v1
435
+ kind: CompositeResourceDefinition
436
+ metadata:
437
+ name: inlined.example.org/v1alpha1
438
+ spec:
439
+ group: inlined.example.org
440
+ names:
441
+ kind: Step
442
+ plural: steps
443
+ defaultCompositionRef:
444
+ name: steps.inlined.example.org
445
+ versions:
446
+ - name: v1alpha1
447
+ served: true
448
+ referenceable: true
449
+ schema:
450
+ openAPIV3Schema:
451
+ type: object
452
+ properties:
453
+ spec:
454
+ type: object
455
+ properties:
456
+ composite:
457
+ type: string
458
+ description: 'A Python module that defines a class with the signature: class Composite(BaseComposite)'
459
+ required:
460
+ - composite
461
+ status:
462
+ type: object
463
+ properties:
464
+ composite:
465
+ x-kubernetes-preserve-unknown-fields: true
466
+ ```
467
+ ```yaml
468
+ apiVersion: apiextensions.crossplane.io/v1
469
+ kind: Composition
470
+ metadata:
471
+ name: steps.inlined.example.org
472
+ spec:
473
+ compositeTypeRef:
474
+ apiVersion: inlined.example.org/v1alpha1
475
+ kind: Step
476
+ mode: Pipeline
477
+ pipeline:
478
+ - step: inlined
479
+ functionRef:
480
+ name: function-pythonic
481
+ input:
482
+ apiVersion: pythonic.fn.crossplane.io/v1alpha1
483
+ kind: Composite
484
+ inlined: composite
425
485
  ```
426
-
427
486
  ## Quick Start Development
428
487
 
429
488
  function-pythonic includes a pure python implementation of the `crossplane render ...`
@@ -616,7 +675,7 @@ kind: Function
616
675
  metadata:
617
676
  name: function-pythonic
618
677
  spec:
619
- package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.1
678
+ package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.2
620
679
  runtimeConfigRef:
621
680
  name: function-pythonic
622
681
  ---
@@ -0,0 +1,18 @@
1
+ crossplane/pythonic/__about__.py,sha256=S3lIpABQ5LwPMuO5TLfDrT_eJOQtFzsph4TXcfnx4OY,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=niq-JSVZ8NB53Q7khkMqH9vQTJPb6yB-13O-wa2Is1U,30311
7
+ crossplane/pythonic/function.py,sha256=CL2j_Br0eYWbn_8r8md9O9ErfHizz78H1KR8l2oV1IA,17964
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=nmVf-Xn_-ER8BEfEbqd8uQo2gdhmNYyQh9QlhcaYebs,53083
12
+ crossplane/pythonic/render.py,sha256=Y1-fdjQxvPBSkGjfJnNwCOOg6I-bX7Ys9X4jXkqIZp4,30140
13
+ crossplane/pythonic/version.py,sha256=-RiB0p146ayqJj0SXfYxTNv49u9Fx9pPgm59Ji2blhc,214
14
+ crossplane_function_pythonic-0.5.0.dist-info/METADATA,sha256=yMVu_bg-pjIrhuHDjeTAmKEa082V2pq5NroKIeo9poI,33133
15
+ crossplane_function_pythonic-0.5.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
16
+ crossplane_function_pythonic-0.5.0.dist-info/entry_points.txt,sha256=jJ4baywFDviB9WyAhyhNYF2VOCb6XtbRSjKf7bnBwhg,68
17
+ crossplane_function_pythonic-0.5.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
18
+ crossplane_function_pythonic-0.5.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.28.0
2
+ Generator: hatchling 1.29.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,18 +0,0 @@
1
- crossplane/pythonic/__about__.py,sha256=dmVecpNqyy8QqMHx8kQerbsS1bVy_KSbwEjreR9uSZg,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=IHWS3Rb4xarCz_4S6thds5R-5O-JCDm0JAwpbLxyK9Y,4149
6
- crossplane/pythonic/composite.py,sha256=f9SYjtXVqRx7vse7nQ06Ic6EnnfCYVtpoOmBhR9Jv6g,30451
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=D_DJQQmuOH-Ij1wWwPDLA1V_evuPzDh9mGSrDomq2XQ,52728
12
- crossplane/pythonic/render.py,sha256=mXFbFFhovoTWzqsqNmMyiww4mtWvUz5uSzcws7u6LlQ,29842
13
- crossplane/pythonic/version.py,sha256=-RiB0p146ayqJj0SXfYxTNv49u9Fx9pPgm59Ji2blhc,214
14
- crossplane_function_pythonic-0.4.1.dist-info/METADATA,sha256=zRTEgofZ2Aw5LAS_1_PDNwIuRpzyPmnFhx-i2Ep-wfY,31501
15
- crossplane_function_pythonic-0.4.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
- crossplane_function_pythonic-0.4.1.dist-info/entry_points.txt,sha256=jJ4baywFDviB9WyAhyhNYF2VOCb6XtbRSjKf7bnBwhg,68
17
- crossplane_function_pythonic-0.4.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
18
- crossplane_function_pythonic-0.4.1.dist-info/RECORD,,