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