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