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