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