crossplane-function-pythonic 0.5.0__py3-none-any.whl → 0.6.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/composite.py +152 -0
- crossplane/pythonic/function.py +19 -4
- crossplane/pythonic/grpc.py +24 -3
- crossplane/pythonic/packages.py +128 -96
- crossplane/pythonic/protobuf.py +2 -0
- crossplane/pythonic/render.py +105 -9
- {crossplane_function_pythonic-0.5.0.dist-info → crossplane_function_pythonic-0.6.0.dist-info}/METADATA +112 -16
- crossplane_function_pythonic-0.6.0.dist-info/RECORD +18 -0
- crossplane_function_pythonic-0.5.0.dist-info/RECORD +0 -18
- {crossplane_function_pythonic-0.5.0.dist-info → crossplane_function_pythonic-0.6.0.dist-info}/WHEEL +0 -0
- {crossplane_function_pythonic-0.5.0.dist-info → crossplane_function_pythonic-0.6.0.dist-info}/entry_points.txt +0 -0
- {crossplane_function_pythonic-0.5.0.dist-info → crossplane_function_pythonic-0.6.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.6.0"
|
crossplane/pythonic/composite.py
CHANGED
|
@@ -104,11 +104,13 @@ class BaseComposite:
|
|
|
104
104
|
)
|
|
105
105
|
self.response = protobuf.Message(None, 'response', response.DESCRIPTOR, response)
|
|
106
106
|
self.logger = logger
|
|
107
|
+
self.capabilities = Capabilities(self.request.meta.capabilities)
|
|
107
108
|
self.parameters = self.request.input.parameters
|
|
108
109
|
self.credentials = Credentials(self.request)
|
|
109
110
|
self.context = self.response.context
|
|
110
111
|
self.environment = self.context['apiextensions.crossplane.io/environment']
|
|
111
112
|
self.requireds = Requireds(self)
|
|
113
|
+
self.schemas = Schemas(self)
|
|
112
114
|
self.resources = Resources(self)
|
|
113
115
|
self.autoReady = True
|
|
114
116
|
self.usages = False
|
|
@@ -123,6 +125,7 @@ class BaseComposite:
|
|
|
123
125
|
self.metadata = self.observed.metadata
|
|
124
126
|
self.spec = self.observed.spec
|
|
125
127
|
self.status = self.desired.status
|
|
128
|
+
self.output = self.response.output
|
|
126
129
|
self.conditions = Conditions(observed, self.response)
|
|
127
130
|
self.results = Results(self.response)
|
|
128
131
|
self.events = Results(self.response) # Deprecated, use self.results
|
|
@@ -136,6 +139,30 @@ class BaseComposite:
|
|
|
136
139
|
raise NotImplementedError()
|
|
137
140
|
|
|
138
141
|
|
|
142
|
+
class Capabilities:
|
|
143
|
+
def __init__(self, capabilities):
|
|
144
|
+
self._capabilities = capabilities
|
|
145
|
+
|
|
146
|
+
def __bool__(self):
|
|
147
|
+
return fnv1.CAPABILITY_CAPABILITIES in self._capabilities
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def requireds(self):
|
|
151
|
+
return fnv1.CAPABILITY_REQUIRED_RESOURCES in self._capabilities if self else None
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def credentials(self):
|
|
155
|
+
return fnv1.CAPABILITY_CREDENTIALS in self._capabilities if self else None
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def conditions(self):
|
|
159
|
+
return fnv1.CAPABILITY_CONDITIONS in self._capabilities if self else None
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def schemas(self):
|
|
163
|
+
return fnv1.CAPABILITY_REQUIRED_SCHEMAS in self._capabilities if self else None
|
|
164
|
+
|
|
165
|
+
|
|
139
166
|
class Credentials:
|
|
140
167
|
def __init__(self, request):
|
|
141
168
|
self.__dict__['_request'] = request
|
|
@@ -558,6 +585,131 @@ class RequiredResource:
|
|
|
558
585
|
return bool(self.observed)
|
|
559
586
|
|
|
560
587
|
|
|
588
|
+
class Schemas:
|
|
589
|
+
def __init__(self, composite):
|
|
590
|
+
self._composite = composite
|
|
591
|
+
self._cache = {}
|
|
592
|
+
|
|
593
|
+
def __getattr__(self, key):
|
|
594
|
+
return self[key]
|
|
595
|
+
|
|
596
|
+
def __getitem__(self, key):
|
|
597
|
+
schema = self._cache.get(key)
|
|
598
|
+
if not schema:
|
|
599
|
+
schema = Schema(self._composite, key)
|
|
600
|
+
self._cache[key] = schema
|
|
601
|
+
return schema
|
|
602
|
+
|
|
603
|
+
def __bool__(self):
|
|
604
|
+
return bool(len(self))
|
|
605
|
+
|
|
606
|
+
def __len__(self):
|
|
607
|
+
names = set()
|
|
608
|
+
for name, schema in self._composite.request.required_schemas:
|
|
609
|
+
names.add(name)
|
|
610
|
+
for name, selector in self._composite.response.requirements.schemas:
|
|
611
|
+
names.add(name)
|
|
612
|
+
return len(names)
|
|
613
|
+
|
|
614
|
+
def __contains__(self, key):
|
|
615
|
+
if key in self._composite.request.required_schemas:
|
|
616
|
+
return True
|
|
617
|
+
if key in self._composite.response.requirements.schemas:
|
|
618
|
+
return True
|
|
619
|
+
return False
|
|
620
|
+
|
|
621
|
+
def __iter__(self):
|
|
622
|
+
names = set()
|
|
623
|
+
for name, schema in self._composite.request.required_schemas:
|
|
624
|
+
names.add(name)
|
|
625
|
+
for name, selector in self._composite.response.requirements.schemas:
|
|
626
|
+
names.add(name)
|
|
627
|
+
for name in sorted(names):
|
|
628
|
+
yield name, self[name]
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class Schema:
|
|
632
|
+
def __init__(self, composite, name):
|
|
633
|
+
self.name = name
|
|
634
|
+
self._selector = composite.response.requirements.schemas[name]
|
|
635
|
+
self._schema = composite.request.required_schemas[name].openapi_v3
|
|
636
|
+
|
|
637
|
+
def __call__(self, kind=_notset, apiVersion=_notset):
|
|
638
|
+
self._selector()
|
|
639
|
+
if kind != _notset:
|
|
640
|
+
# Allow for apiVersion in the first arg and kind in the second arg
|
|
641
|
+
if '/' in kind or kind == 'v1':
|
|
642
|
+
if apiVersion != _notset:
|
|
643
|
+
self.kind = apiVersion
|
|
644
|
+
apiVersion = kind
|
|
645
|
+
else:
|
|
646
|
+
self.kind = kind
|
|
647
|
+
if apiVersion != _notset:
|
|
648
|
+
self.apiVersion = apiVersion
|
|
649
|
+
return self
|
|
650
|
+
|
|
651
|
+
@property
|
|
652
|
+
def apiVersion(self):
|
|
653
|
+
return self._selector.api_version
|
|
654
|
+
|
|
655
|
+
@apiVersion.setter
|
|
656
|
+
def apiVersion(self, apiVersion):
|
|
657
|
+
self._selector.api_version = apiVersion
|
|
658
|
+
|
|
659
|
+
@property
|
|
660
|
+
def kind(self):
|
|
661
|
+
return self._selector.kind
|
|
662
|
+
|
|
663
|
+
@kind.setter
|
|
664
|
+
def kind(self, kind):
|
|
665
|
+
self._selector.kind = kind
|
|
666
|
+
|
|
667
|
+
def __enter__(self):
|
|
668
|
+
return self
|
|
669
|
+
|
|
670
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
671
|
+
pass
|
|
672
|
+
|
|
673
|
+
def __aenter__(self):
|
|
674
|
+
return self
|
|
675
|
+
|
|
676
|
+
def __aexit__(self, exc_type, exc_value, traceback):
|
|
677
|
+
pass
|
|
678
|
+
|
|
679
|
+
def __getattr__(self, key):
|
|
680
|
+
return self[key]
|
|
681
|
+
|
|
682
|
+
def __getitem__(self, key):
|
|
683
|
+
return self._schema[key]
|
|
684
|
+
|
|
685
|
+
def __bool__(self):
|
|
686
|
+
return bool(self._schema)
|
|
687
|
+
|
|
688
|
+
def __len__(self):
|
|
689
|
+
return len(self._schema)
|
|
690
|
+
|
|
691
|
+
def __contains__(self, item):
|
|
692
|
+
return item in self._schema
|
|
693
|
+
|
|
694
|
+
def __iter__(self):
|
|
695
|
+
for key, value in self._schema:
|
|
696
|
+
yield key, value
|
|
697
|
+
|
|
698
|
+
def __hash__(self):
|
|
699
|
+
return hash(self._schema)
|
|
700
|
+
|
|
701
|
+
def __eq__(self, other):
|
|
702
|
+
if instance(other, Schema):
|
|
703
|
+
other = other._schema
|
|
704
|
+
return self._schema == other
|
|
705
|
+
|
|
706
|
+
def __str__(self):
|
|
707
|
+
return str(self._schema)
|
|
708
|
+
|
|
709
|
+
def __format__(self, spec='yaml'):
|
|
710
|
+
return format(self,_schema, spec)
|
|
711
|
+
|
|
712
|
+
|
|
561
713
|
class Conditions:
|
|
562
714
|
def __init__(self, observed, response=None):
|
|
563
715
|
self._observed = observed
|
crossplane/pythonic/function.py
CHANGED
|
@@ -121,8 +121,13 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
|
121
121
|
except Exception as e:
|
|
122
122
|
return self.fatal(request, logger, 'Compose', e)
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
schemas = self.get_schemas(step, composite)
|
|
125
|
+
requireds = self.get_requireds(step, composite)
|
|
126
|
+
if schemas or requireds:
|
|
127
|
+
if schemas:
|
|
128
|
+
logger.debug(f"Required schemas: {','.join(schemas)}")
|
|
129
|
+
if requireds:
|
|
130
|
+
logger.debug(f"Required resources: {','.join(requireds)}")
|
|
126
131
|
else:
|
|
127
132
|
self.process_usages(composite)
|
|
128
133
|
self.process_unknowns(composite)
|
|
@@ -155,11 +160,21 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
|
|
|
155
160
|
]
|
|
156
161
|
)
|
|
157
162
|
|
|
163
|
+
def get_schemas(self, step, composite):
|
|
164
|
+
schemas = []
|
|
165
|
+
for name, schema in composite.schemas:
|
|
166
|
+
if len(schema.kind) and len(schema.apiVersion):
|
|
167
|
+
s = pythonic.Map(kind=schema.kind, apiVersion=schema.apiVersion)
|
|
168
|
+
if s != step.schemas[name]:
|
|
169
|
+
step.schemas[name] = s
|
|
170
|
+
schemas.append(name)
|
|
171
|
+
return schemas
|
|
172
|
+
|
|
158
173
|
def get_requireds(self, step, composite):
|
|
159
174
|
requireds = []
|
|
160
175
|
for name, required in composite.requireds:
|
|
161
|
-
if len(required.
|
|
162
|
-
r = pythonic.Map(
|
|
176
|
+
if len(required.kind) and len(required.apiVersion):
|
|
177
|
+
r = pythonic.Map(kind=required.kind, apiVersion=required.apiVersion)
|
|
163
178
|
if len(required.namespace):
|
|
164
179
|
r.namespace = required.namespace
|
|
165
180
|
if len(required.matchName):
|
crossplane/pythonic/grpc.py
CHANGED
|
@@ -45,6 +45,12 @@ class Command(command.Command):
|
|
|
45
45
|
parser.add_argument(
|
|
46
46
|
'--packages',
|
|
47
47
|
action='store_true',
|
|
48
|
+
dest='packages_configmaps',
|
|
49
|
+
help='Discover python packages from function-pythonic ConfigMaps, deprecated use --packages-configmaps'
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
'--packages-configmaps',
|
|
53
|
+
action='store_true',
|
|
48
54
|
help='Discover python packages from function-pythonic ConfigMaps.'
|
|
49
55
|
)
|
|
50
56
|
parser.add_argument(
|
|
@@ -57,7 +63,17 @@ class Command(command.Command):
|
|
|
57
63
|
action='append',
|
|
58
64
|
default=[],
|
|
59
65
|
metavar='NAMESPACE',
|
|
60
|
-
help='Namespaces to discover function-pythonic ConfigMaps in, default is cluster wide.',
|
|
66
|
+
help='Namespaces to discover function-pythonic ConfigMaps and Secrets in, default is cluster wide.',
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
'--packages-environmentconfigs',
|
|
70
|
+
action='store_true',
|
|
71
|
+
help='Also Discover python packages from function-pythonic EnvironmentConfigs.'
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
'--packages-compositions',
|
|
75
|
+
action='store_true',
|
|
76
|
+
help='Also Discover python packages from function-pythonic Compositions.'
|
|
61
77
|
)
|
|
62
78
|
parser.add_argument(
|
|
63
79
|
'--packages-dir',
|
|
@@ -75,7 +91,9 @@ class Command(command.Command):
|
|
|
75
91
|
if not self.args.tls_certs_dir and not self.args.insecure:
|
|
76
92
|
print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr)
|
|
77
93
|
sys.exit(1)
|
|
78
|
-
|
|
94
|
+
if (self.args.packages_environmentconfigs or self.args.packages_compositions) and self.args.packages_namespace:
|
|
95
|
+
print('--packages-namespace cannot be used with --packages-environment-configs or --packages-compositions', file=sys.stderr)
|
|
96
|
+
sys.exit(1)
|
|
79
97
|
if self.args.pip_install:
|
|
80
98
|
import pip._internal.cli.main
|
|
81
99
|
pip._internal.cli.main.main(['install', '--user', *shlex.split(self.args.pip_install)])
|
|
@@ -108,15 +126,18 @@ class Command(command.Command):
|
|
|
108
126
|
)
|
|
109
127
|
await grpc_server.start()
|
|
110
128
|
|
|
111
|
-
if self.args.
|
|
129
|
+
if self.args.packages_configmaps or self.args.packages_secrets or self.args.packages_environmentconfigs or self.args.packages_compositions:
|
|
112
130
|
from . import packages
|
|
113
131
|
async with asyncio.TaskGroup() as tasks:
|
|
114
132
|
tasks.create_task(grpc_server.wait_for_termination())
|
|
115
133
|
tasks.create_task(packages.operator(
|
|
116
134
|
grpc_server,
|
|
117
135
|
grpc_runner,
|
|
136
|
+
self.args.packages_configmaps,
|
|
118
137
|
self.args.packages_secrets,
|
|
119
138
|
self.args.packages_namespace,
|
|
139
|
+
self.args.packages_environmentconfigs,
|
|
140
|
+
self.args.packages_compositions,
|
|
120
141
|
self.args.packages_dir,
|
|
121
142
|
))
|
|
122
143
|
else:
|
crossplane/pythonic/packages.py
CHANGED
|
@@ -10,27 +10,38 @@ import kopf
|
|
|
10
10
|
GRPC_SERVER = None
|
|
11
11
|
GRPC_RUNNER = None
|
|
12
12
|
PACKAGES_DIR = None
|
|
13
|
-
PACKAGE_LABEL =
|
|
13
|
+
PACKAGE_LABEL = 'function-pythonic.package'
|
|
14
|
+
PACKAGE_LABELS = {PACKAGE_LABEL: kopf.PRESENT}
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
def operator(grpc_server, grpc_runner, packages_secrets, packages_namespaces, packages_dir):
|
|
17
|
+
def operator(grpc_server, grpc_runner, packages_configmaps, packages_secrets, packages_namespaces, packages_environmentconfigs, packages_compositions, packages_dir):
|
|
17
18
|
logging.getLogger('kopf.objects').setLevel(logging.INFO)
|
|
18
19
|
global GRPC_SERVER, GRPC_RUNNER, PACKAGES_DIR
|
|
19
20
|
GRPC_SERVER = grpc_server
|
|
20
21
|
GRPC_RUNNER = grpc_runner
|
|
21
22
|
PACKAGES_DIR = pathlib.Path(packages_dir).expanduser().resolve()
|
|
22
23
|
sys.path.insert(0, str(PACKAGES_DIR))
|
|
24
|
+
if packages_configmaps:
|
|
25
|
+
on_resource('', 'v1', 'configmaps')
|
|
23
26
|
if packages_secrets:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
on_resource('', 'v1', 'secrets')
|
|
28
|
+
if not packages_namespaces:
|
|
29
|
+
if packages_environmentconfigs:
|
|
30
|
+
on_resource('apiextensions.crossplane.io', 'v1beta1', 'environmentconfigs')
|
|
31
|
+
if packages_compositions:
|
|
32
|
+
on_resource('apiextensions.crossplane.io', 'v1', 'compositions')
|
|
28
33
|
return kopf.operator(
|
|
29
34
|
standalone=True,
|
|
30
35
|
clusterwide=not packages_namespaces,
|
|
31
36
|
namespaces=packages_namespaces,
|
|
32
37
|
)
|
|
33
38
|
|
|
39
|
+
def on_resource(group, version, plural):
|
|
40
|
+
kopf.on.create(group, version, plural, labels=PACKAGE_LABELS)(create)
|
|
41
|
+
kopf.on.resume(group, version, plural, labels=PACKAGE_LABELS)(create)
|
|
42
|
+
kopf.on.update(group, version, plural, labels=PACKAGE_LABELS)(update)
|
|
43
|
+
kopf.on.delete(group, version, plural, labels=PACKAGE_LABELS)(delete)
|
|
44
|
+
|
|
34
45
|
|
|
35
46
|
@kopf.on.startup()
|
|
36
47
|
async def startup(settings, **_):
|
|
@@ -42,107 +53,128 @@ async def cleanup(**_):
|
|
|
42
53
|
await GRPC_SERVER.stop(5)
|
|
43
54
|
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if
|
|
68
|
-
action
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def get_package_dir(body, logger=None):
|
|
88
|
-
package = body.get('metadata', {}).get('labels', {}).get('function-pythonic.package', None)
|
|
56
|
+
async def create(resource, labels, body, logger, **_):
|
|
57
|
+
resource_create(resource, labels, 'Created', body, logger)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def update(resource, labels, body, old, logger, **_):
|
|
61
|
+
resource_delete(resource, labels, 'Removed', old, logger)
|
|
62
|
+
resource_create(resource, labels, 'Added', body, logger)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def delete(resource, labels, body, logger, **_):
|
|
66
|
+
resource_delete(resource, labels, 'Deleted', body, logger)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def resource_create(resource, labels, action, body, logger):
|
|
70
|
+
package_dir = resource_package_dir(resource, labels, logger)
|
|
71
|
+
if not package_dir:
|
|
72
|
+
return
|
|
73
|
+
if resource.plural in ('configmaps', 'secrets', 'environmentconfigs'):
|
|
74
|
+
package_create(resource, action, package_dir, body.get('data', {}), logger)
|
|
75
|
+
elif resource.plural == 'compositions':
|
|
76
|
+
for step in body.get('spec', {}).get('pipeline', []):
|
|
77
|
+
input = step.get('input')
|
|
78
|
+
if input and input.get('apiVersion') == 'pythonic.fn.crossplane.io/v1alpha1':
|
|
79
|
+
package_create(resource, action, package_dir, input.get('packages', {}), logger)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def resource_delete(resource, labels, action, body, logger):
|
|
83
|
+
package_dir = resource_package_dir(resource, labels, logger)
|
|
84
|
+
if not package_dir:
|
|
85
|
+
return
|
|
86
|
+
if resource.plural in ('configmaps', 'secrets', 'environmentconfigs'):
|
|
87
|
+
package_delete(action, package_dir, body.get('data', {}), logger)
|
|
88
|
+
elif resource.plural == 'compositions':
|
|
89
|
+
for step in body.get('spec', {}).get('pipeline', []):
|
|
90
|
+
input = step.get('input')
|
|
91
|
+
if input and input.get('apiVersion') == 'pythonic.fn.crossplane.io/v1alpha1':
|
|
92
|
+
package_delete(action, package_dir, step.get('input', {}).get('packages', {}), logger)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def resource_package_dir(resource, labels, logger):
|
|
96
|
+
package = labels.get(PACKAGE_LABEL)
|
|
89
97
|
if package is None:
|
|
90
98
|
if logger:
|
|
91
|
-
logger.error(
|
|
99
|
+
logger.error(f"{PACKAGE_LABEL} label is missing")
|
|
92
100
|
return None
|
|
93
101
|
package_dir = PACKAGES_DIR
|
|
94
|
-
if package:
|
|
102
|
+
if resource.plural in ('configmaps', 'secrets') and package:
|
|
95
103
|
for segment in package.split('.'):
|
|
96
104
|
if not segment.isidentifier():
|
|
97
|
-
|
|
98
|
-
logger.error('Package has invalid package name: %s', package)
|
|
105
|
+
logger.error('Package has invalid package name: %s', package)
|
|
99
106
|
return None
|
|
100
107
|
package_dir = package_dir / segment
|
|
101
108
|
return package_dir
|
|
102
109
|
|
|
103
110
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
111
|
+
def package_create(resource, action, package_dir, package, logger):
|
|
112
|
+
for name, value in package.items():
|
|
113
|
+
if validate_entry(name, value, logger):
|
|
114
|
+
package_name = package_dir / name
|
|
115
|
+
if isinstance(value, str):
|
|
116
|
+
package_name.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
if resource.plural == 'secrets':
|
|
118
|
+
package_name.write_bytes(base64.b64decode(value.encode('utf-8')))
|
|
119
|
+
else:
|
|
120
|
+
package_name.write_text(value)
|
|
121
|
+
module, name = package_file_name(package_name)
|
|
122
|
+
if module:
|
|
123
|
+
GRPC_RUNNER.invalidate_module(name)
|
|
124
|
+
logger.info(f"{action} module: {name}")
|
|
125
|
+
else:
|
|
126
|
+
logger.info(f"{action} file: {name}")
|
|
127
|
+
elif isinstance(value, dict):
|
|
128
|
+
package_create(resource, action, package_name, value, logger)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def package_delete(action, package_dir, package, logger):
|
|
132
|
+
for name, value in package.items():
|
|
133
|
+
if validate_entry(name, value, logger):
|
|
134
|
+
package_name = package_dir / name
|
|
135
|
+
if isinstance(value, str):
|
|
136
|
+
package_name.unlink(missing_ok=True)
|
|
137
|
+
module, name = package_file_name(package_name)
|
|
138
|
+
if module:
|
|
139
|
+
GRPC_RUNNER.invalidate_module(name)
|
|
140
|
+
logger.info(f"{action} module: {name}")
|
|
141
|
+
else:
|
|
142
|
+
logger.info(f"{action} file: {name}")
|
|
143
|
+
parent = package_name.parent
|
|
144
|
+
while (
|
|
145
|
+
parent.is_relative_to(PACKAGES_DIR)
|
|
146
|
+
and parent.is_dir()
|
|
147
|
+
and not list(parent.iterdir())
|
|
148
|
+
):
|
|
149
|
+
parent.rmdir()
|
|
150
|
+
module = str(parent.relative_to(PACKAGES_DIR)).replace('/', '.')
|
|
151
|
+
if module != '.':
|
|
152
|
+
GRPC_RUNNER.invalidate_module(module)
|
|
153
|
+
logger.info(f"{action} package: {module}")
|
|
154
|
+
parent = parent.parent
|
|
155
|
+
elif isinstance(value, dict):
|
|
156
|
+
package_delete(action, package_name, value, logger)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def validate_entry(name, value, logger):
|
|
160
|
+
if isinstance(value, str):
|
|
161
|
+
if not name.endswith('.py'):
|
|
162
|
+
if '.' in name or '/' in name:
|
|
163
|
+
logger.error(f"Python package file name is not valid: {name}")
|
|
164
|
+
return False
|
|
165
|
+
return True
|
|
166
|
+
name = name[:-3]
|
|
167
|
+
elif not isinstance(value, dict):
|
|
168
|
+
logger.error(f"Python package \"{name}\" value is not a valid type: {value.__class__}")
|
|
169
|
+
return False
|
|
170
|
+
if name.isidentifier():
|
|
171
|
+
return True
|
|
172
|
+
logger.error(f"Python package name is not an identifier: {name}")
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def package_file_name(package_name):
|
|
177
|
+
name = str(package_name.relative_to(PACKAGES_DIR))
|
|
146
178
|
if name.endswith('.py'):
|
|
147
179
|
return True, name[:-3].replace('/', '.')
|
|
148
180
|
return False, name
|
crossplane/pythonic/protobuf.py
CHANGED
crossplane/pythonic/render.py
CHANGED
|
@@ -3,6 +3,7 @@ import asyncio
|
|
|
3
3
|
import importlib
|
|
4
4
|
import inflect
|
|
5
5
|
import inspect
|
|
6
|
+
import json
|
|
6
7
|
import kr8s
|
|
7
8
|
import logging
|
|
8
9
|
import pathlib
|
|
@@ -12,10 +13,10 @@ from crossplane.function.proto.v1 import run_function_pb2 as fnv1
|
|
|
12
13
|
|
|
13
14
|
from . import (
|
|
14
15
|
command,
|
|
15
|
-
composite,
|
|
16
16
|
function,
|
|
17
17
|
protobuf,
|
|
18
18
|
)
|
|
19
|
+
from .composite import BaseComposite
|
|
19
20
|
|
|
20
21
|
INFLECT = inflect.engine()
|
|
21
22
|
INFLECT.classical(all=False)
|
|
@@ -58,7 +59,7 @@ class Command(command.Command):
|
|
|
58
59
|
action='append',
|
|
59
60
|
default=[],
|
|
60
61
|
metavar='KEY=VALUE',
|
|
61
|
-
help='Context key-value pairs to pass to the Function pipeline. Values must be
|
|
62
|
+
help='Context key-value pairs to pass to the Function pipeline. Values must be YAML/JSON. Keys take precedence over --context-files.',
|
|
62
63
|
)
|
|
63
64
|
parser.add_argument(
|
|
64
65
|
'--observed-resources', '-o',
|
|
@@ -77,12 +78,12 @@ class Command(command.Command):
|
|
|
77
78
|
help='A YAML file or directory of YAML files specifying required resources to pass to the Function pipeline.',
|
|
78
79
|
)
|
|
79
80
|
parser.add_argument(
|
|
80
|
-
'--
|
|
81
|
+
'--required-schemas', '-s',
|
|
81
82
|
action='append',
|
|
82
83
|
type=pathlib.Path,
|
|
83
84
|
default=[],
|
|
84
85
|
metavar='PATH',
|
|
85
|
-
help='A
|
|
86
|
+
help='A JSON file or directory of JSON files specifying required schemas to pass to the Function pipeline.',
|
|
86
87
|
)
|
|
87
88
|
parser.add_argument(
|
|
88
89
|
'--include-full-xr', '-x',
|
|
@@ -119,11 +120,10 @@ class Command(command.Command):
|
|
|
119
120
|
observed = self.collect_resources(self.args.observed_resources)
|
|
120
121
|
composition = await self.setup_composition(composite, api)
|
|
121
122
|
resources = self.collect_resources(self.args.required_resources)
|
|
122
|
-
|
|
123
|
-
resources.sort(key=lambda resource: str(resource.metadata.name))
|
|
123
|
+
schemas = self.collect_schemas(self.args.required_schemas)
|
|
124
124
|
context = self.setup_context()
|
|
125
125
|
|
|
126
|
-
render = await self.render(composite, observed, composition, resources, context, api, self.args.render_unknowns, self.args.crossplane_v1)
|
|
126
|
+
render = await self.render(composite, observed, composition, resources, schemas, context, api, self.args.render_unknowns, self.args.crossplane_v1)
|
|
127
127
|
if not render:
|
|
128
128
|
sys.exit(1)
|
|
129
129
|
|
|
@@ -212,7 +212,7 @@ class Command(command.Command):
|
|
|
212
212
|
if not inspect.isclass(clazz):
|
|
213
213
|
print(f"Composition class {self.args.composition} is not a class", file=sys.stderr)
|
|
214
214
|
sys.exit(1)
|
|
215
|
-
if not issubclass(clazz,
|
|
215
|
+
if not issubclass(clazz, BaseComposite):
|
|
216
216
|
print(f"Composition class {self.args.composition} is not a subclass of BaseComposite", file=sys.stderr)
|
|
217
217
|
sys.exit(1)
|
|
218
218
|
return self.create_composition(composite, self.args.composition)
|
|
@@ -246,8 +246,25 @@ class Command(command.Command):
|
|
|
246
246
|
else:
|
|
247
247
|
print(f"Specified resource is not a file or a directory: {entry}", file=sys.stderr)
|
|
248
248
|
sys.exit(1)
|
|
249
|
+
resources.sort(key=lambda resource: str(resource.metadata.name))
|
|
249
250
|
return resources
|
|
250
251
|
|
|
252
|
+
def collect_schemas(self, entries):
|
|
253
|
+
schemas = []
|
|
254
|
+
for entry in entries:
|
|
255
|
+
if entry.is_file():
|
|
256
|
+
document = json.loads(entry.read_text())
|
|
257
|
+
schemas.append(protobuf.Value(None, None, document))
|
|
258
|
+
elif entry.is_dir():
|
|
259
|
+
for file in entry.iterdir():
|
|
260
|
+
if file.suffix == '.json':
|
|
261
|
+
document = json.loads(file.read_text())
|
|
262
|
+
schemas.append(protobuf.Value(None, None, document))
|
|
263
|
+
else:
|
|
264
|
+
print(f"Specified resource is not a file or a directory: {entry}", file=sys.stderr)
|
|
265
|
+
sys.exit(1)
|
|
266
|
+
return schemas
|
|
267
|
+
|
|
251
268
|
def setup_context(self):
|
|
252
269
|
# Load the request context with any specified command line options.
|
|
253
270
|
context = protobuf.Map()
|
|
@@ -269,7 +286,7 @@ class Command(command.Command):
|
|
|
269
286
|
context[key_value[0]] = protobuf.Yaml(key_value[1])
|
|
270
287
|
return context
|
|
271
288
|
|
|
272
|
-
async def render(self, composite, observed=[], composition=None, resources=[], context=None, api=None, render_unknowns=False, crossplane_v1=False, composite_observeds=True):
|
|
289
|
+
async def render(self, composite, observed=[], composition=None, resources=[], schemas=[], context=None, api=None, render_unknowns=False, crossplane_v1=False, composite_observeds=True):
|
|
273
290
|
# Create the request used when running Composition steps.
|
|
274
291
|
request = protobuf.Message(None, 'request', fnv1.RunFunctionRequest.DESCRIPTOR, fnv1.RunFunctionRequest())
|
|
275
292
|
if context is not None:
|
|
@@ -362,6 +379,10 @@ class Command(command.Command):
|
|
|
362
379
|
request.extra_resources()
|
|
363
380
|
for name, selector in requirements.extra_resources:
|
|
364
381
|
await self.set_required(name, selector, request.extra_resources, resources, api)
|
|
382
|
+
# Fetch the schemas requested.
|
|
383
|
+
request.required_schemas()
|
|
384
|
+
for name, selector in requirements.schemas:
|
|
385
|
+
await self.set_schema(name, selector, request.required_schemas, schemas, api)
|
|
365
386
|
# Run the step using the function-pythonic function runner.
|
|
366
387
|
response = protobuf.Message(
|
|
367
388
|
None,
|
|
@@ -546,6 +567,81 @@ class Command(command.Command):
|
|
|
546
567
|
for key, value in connection.data:
|
|
547
568
|
destination.connection_details[key] = protobuf.B64Decode(value)
|
|
548
569
|
|
|
570
|
+
async def set_schema(self, name, selector, schemas, documents=[], api=None):
|
|
571
|
+
if not name:
|
|
572
|
+
return
|
|
573
|
+
name = str(name)
|
|
574
|
+
schema = schemas[name].openapi_v3
|
|
575
|
+
schema() # Force this to get created
|
|
576
|
+
gvk = protobuf.Map(kind=selector.kind)
|
|
577
|
+
version = str(selector.api_version)
|
|
578
|
+
if '/' in version:
|
|
579
|
+
gvk.group, gvk.version = version.split('/', 1)
|
|
580
|
+
else:
|
|
581
|
+
gvk.group = ''
|
|
582
|
+
gvk.version = version
|
|
583
|
+
for document in documents:
|
|
584
|
+
if self.find_schema(gvk, document, schema):
|
|
585
|
+
return
|
|
586
|
+
if api:
|
|
587
|
+
if gvk.group == '':
|
|
588
|
+
url = f"api/{gvk.version}"
|
|
589
|
+
else:
|
|
590
|
+
url = f"apis/{gvk.group}/{gvk.version}"
|
|
591
|
+
try:
|
|
592
|
+
async with api.call_api(base='/openapi/v3', version='', url=url) as response:
|
|
593
|
+
document = protobuf.Value(None, None, response.json())
|
|
594
|
+
except kr8s.NotFoundError:
|
|
595
|
+
return
|
|
596
|
+
self.find_schema(gvk, document, schema)
|
|
597
|
+
|
|
598
|
+
def find_schema(self, gvk, document, schema):
|
|
599
|
+
for name, s in document.components.schemas:
|
|
600
|
+
gvks = s['x-kubernetes-group-version-kind']
|
|
601
|
+
if len(gvks) == 1 and gvks[0] == gvk:
|
|
602
|
+
self.resolve_ref(document, set(), f"#/components/schemas/{name}", schema)
|
|
603
|
+
return True
|
|
604
|
+
return False
|
|
605
|
+
|
|
606
|
+
def resolve_ref(self, document, visiting, ref, schema):
|
|
607
|
+
if not ref:
|
|
608
|
+
return
|
|
609
|
+
ref = str(ref)
|
|
610
|
+
if ref in visiting:
|
|
611
|
+
return
|
|
612
|
+
d = None
|
|
613
|
+
for segment in ref.split('/'):
|
|
614
|
+
if segment == '#':
|
|
615
|
+
d = document
|
|
616
|
+
else:
|
|
617
|
+
d = d[segment]
|
|
618
|
+
if not d:
|
|
619
|
+
return
|
|
620
|
+
visiting.add(ref)
|
|
621
|
+
try:
|
|
622
|
+
for name, value in d:
|
|
623
|
+
self.copy_schema(document, visiting, name, value, schema)
|
|
624
|
+
finally:
|
|
625
|
+
visiting.remove(ref)
|
|
626
|
+
|
|
627
|
+
def copy_schema(self, document, visiting, key, value, schema):
|
|
628
|
+
if key == '$ref':
|
|
629
|
+
self.resolve_ref(document, visiting, value, schema)
|
|
630
|
+
elif key == 'allOf':
|
|
631
|
+
if value._isList and len(value) == 1:
|
|
632
|
+
self.resolve_ref(document, visiting, value[0]['$ref'], schema)
|
|
633
|
+
else:
|
|
634
|
+
if value._isMap:
|
|
635
|
+
s = schema[key]
|
|
636
|
+
for n, v in value:
|
|
637
|
+
self.copy_schema(document, visiting, n, v, s)
|
|
638
|
+
elif value._isList:
|
|
639
|
+
s = schema[key]
|
|
640
|
+
for ix, v in enumerate(value):
|
|
641
|
+
self.copy_schema(document, visiting, ix, v, s)
|
|
642
|
+
else:
|
|
643
|
+
schema[key] = value
|
|
644
|
+
|
|
549
645
|
def copy_resource(self, source, destination):
|
|
550
646
|
destination.resource = source.resource
|
|
551
647
|
destination.connection_details()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crossplane-function-pythonic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -19,7 +19,7 @@ Requires-Dist: inflect==7.5.0
|
|
|
19
19
|
Requires-Dist: kr8s==0.20.15
|
|
20
20
|
Requires-Dist: pyyaml==6.0.3
|
|
21
21
|
Provides-Extra: packages
|
|
22
|
-
Requires-Dist: kopf==1.
|
|
22
|
+
Requires-Dist: kopf==1.44.4; extra == 'packages'
|
|
23
23
|
Provides-Extra: pip-install
|
|
24
24
|
Requires-Dist: pip==26.0.1; extra == 'pip-install'
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
@@ -83,7 +83,7 @@ kind: Function
|
|
|
83
83
|
metadata:
|
|
84
84
|
name: function-pythonic
|
|
85
85
|
spec:
|
|
86
|
-
package: xpkg.
|
|
86
|
+
package: xpkg.crossplane.io/crossplane-contrib/function-pythonic:v0.6.0
|
|
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.
|
|
98
|
+
package: xpkg.crossplane.io/crossplane-contrib/function-pythonic:v0.6.0
|
|
99
99
|
runtimeConfigRef:
|
|
100
100
|
name: function-pythonic
|
|
101
101
|
--
|
|
@@ -292,6 +292,7 @@ The BaseComposite class provides the following fields for manipulating the Compo
|
|
|
292
292
|
| self.metadata | Map | The composite observed metadata |
|
|
293
293
|
| self.spec | Map | The composite observed spec |
|
|
294
294
|
| self.status | Map | The composite desired and observed status, read from observed if not in desired |
|
|
295
|
+
| self.output | Map | The step output, only used during Operations |
|
|
295
296
|
| self.conditions | Conditions | The composite desired and observed conditions, read from observed if not in desired |
|
|
296
297
|
| self.results | Results | Returned results applied to the Composite and optionally on the Claim |
|
|
297
298
|
| self.connectionSecret | Map | The name, namespace, and resourceName to use when generating the connection secret in Crossplane v2 |
|
|
@@ -306,17 +307,31 @@ The BaseComposite also provides access to the following Crossplane Function leve
|
|
|
306
307
|
| self.request | Message | Low level direct access to the RunFunctionRequest message |
|
|
307
308
|
| self.response | Message | Low level direct access to the RunFunctionResponse message |
|
|
308
309
|
| self.logger | Logger | Python logger to log messages to the running function stdout |
|
|
310
|
+
| self.capabilities | Capabilities | This Crossplane version's Capabilities |
|
|
309
311
|
| self.parameters | Map | The configured step parameters |
|
|
310
312
|
| self.ttl | Integer | Get or set the response TTL, in seconds |
|
|
311
313
|
| self.credentials | Credentials | The request credentials |
|
|
312
314
|
| self.context | Map | The response context, initialized from the request context |
|
|
313
315
|
| self.environment | Map | The response environment, initialized from the request context environment |
|
|
314
316
|
| self.requireds | Requireds | Request and read additional local Kubernetes resources |
|
|
317
|
+
| self.schemas | Schemas | Request and read CustomResourceDefinition schemas |
|
|
315
318
|
| self.resources | Resources | Define and process composed resources |
|
|
316
319
|
| self.usages| Boolean | Generate Crossplane Usages for resource dependencies, default False |
|
|
317
320
|
| self.autoReady | Boolean | Perform auto ready processing on all composed resources, default True |
|
|
318
321
|
| self.unknownsFatal | Boolean | Terminate the composition if already created resources are assigned unknown values, default False |
|
|
319
322
|
|
|
323
|
+
### Capabiities
|
|
324
|
+
|
|
325
|
+
The Capabilities of the Crossplane version calling function-pythonic.
|
|
326
|
+
|
|
327
|
+
| Field | Type | Description |
|
|
328
|
+
| ----- | ---- | ----------- |
|
|
329
|
+
| bool(Capabilities) | Boolean | Whether or not the Crossplane version supports Capabilities |
|
|
330
|
+
| Capabiities.requireds | Boolean | Functions can return required resources and Crossplane will fetch the required resources |
|
|
331
|
+
| Capabiities.credentials | Boolean | Functions can receive credentials from secrets specified in the Composition |
|
|
332
|
+
| Capabiities.conditions | Boolean | Functions can return status conditions to be applied to the XR and optionally its claim |
|
|
333
|
+
| Capabiities.schemas | Boolean | Functions can request OpenAPI schemas and Crossplane will return them |
|
|
334
|
+
|
|
320
335
|
### Composed Resources
|
|
321
336
|
|
|
322
337
|
Creating and accessing composed resources is performed using the `BaseComposite.resources` field.
|
|
@@ -350,7 +365,7 @@ Resource class:
|
|
|
350
365
|
|
|
351
366
|
Creating and accessing required resources is performed using the `BaseComposite.requireds` field.
|
|
352
367
|
`BaseComposite.requireds` is a dictionary of the required resources whose key is the required
|
|
353
|
-
|
|
368
|
+
schema name. The value returned when getting a required resource from BaseComposite is the
|
|
354
369
|
following RequiredResources class:
|
|
355
370
|
|
|
356
371
|
| Field | Type | Description |
|
|
@@ -363,9 +378,6 @@ following RequiredResources class:
|
|
|
363
378
|
| RequiredResources.matchName | String | The names to match when returning the required resources |
|
|
364
379
|
| RequiredResources.matchLabels | Map | The labels to match when returning the required resources |
|
|
365
380
|
|
|
366
|
-
The current version of crossplane-sdk-python used by function-pythonic does not support namespace
|
|
367
|
-
selection. For now, use matchLabels and filter the results if required.
|
|
368
|
-
|
|
369
381
|
RequiredResources acts like a Python list to provide access to the found required resources.
|
|
370
382
|
Each resource in the list is the following RequiredResource class:
|
|
371
383
|
|
|
@@ -382,6 +394,22 @@ Each resource in the list is the following RequiredResource class:
|
|
|
382
394
|
| RequiredResource.conditions | Map | The required resource conditions |
|
|
383
395
|
| RequiredResource.connection | Map | The required resource connection details |
|
|
384
396
|
|
|
397
|
+
### Required Schemas
|
|
398
|
+
|
|
399
|
+
Creating and accessing required schemas is performed using the `BaseComposite.schemas` field.
|
|
400
|
+
`BaseComposite.schemas` is a dictionary of the required schema whose key is the required
|
|
401
|
+
resource name. The value returned when getting a required resource from BaseComposite is the
|
|
402
|
+
following Schema class:
|
|
403
|
+
|
|
404
|
+
| Field | Type | Description |
|
|
405
|
+
| ----- | ---- | ----------- |
|
|
406
|
+
| Schema(apiVersion,kind) | Schema | Reset the required schema and set the optional parameters |
|
|
407
|
+
| Schema.name | String | The required schema name |
|
|
408
|
+
| Schema.apiVersion | String | The required schema selector apiVersion |
|
|
409
|
+
| Schema.kind | String | The required schema selector kind |
|
|
410
|
+
| Schema.\_\_getitem\_\_ | Map | The required schema openAPIV3Schema |
|
|
411
|
+
| Schema.\_\_getattr\_\_ | Map | The required schema openAPIV3Schema |
|
|
412
|
+
|
|
385
413
|
### Conditions
|
|
386
414
|
|
|
387
415
|
The `BaseComposite.conditions`, `Resource.conditions`, and `RequiredResource.conditions` fields
|
|
@@ -500,7 +528,7 @@ $ function-pythonic render --help
|
|
|
500
528
|
usage: Crossplane Function Pythonic render [-h] [--debug] [--log-name-width WIDTH] [--logger-level LOGGER=LEVEL] [--python-path DIRECTORY]
|
|
501
529
|
[--render-unknowns] [--allow-oversize-protos] [--crossplane-v1] [--kube-context CONTEXT]
|
|
502
530
|
[--context-files KEY=PATH] [--context-values KEY=VALUE] [--observed-resources PATH]
|
|
503
|
-
[--required-resources PATH] [--
|
|
531
|
+
[--required-resources PATH] [--required-schemas PATH] [--include-full-xr] [--include-connection-xr]
|
|
504
532
|
[--include-function-results] [--include-context]
|
|
505
533
|
COMPOSITE [COMPOSITION]
|
|
506
534
|
|
|
@@ -532,8 +560,8 @@ options:
|
|
|
532
560
|
A YAML file or directory of YAML files specifying the observed state of composed resources.
|
|
533
561
|
--required-resources, -e PATH
|
|
534
562
|
A YAML file or directory of YAML files specifying required resources to pass to the Function pipeline.
|
|
535
|
-
--
|
|
536
|
-
A
|
|
563
|
+
--required-schemas, -s PATH
|
|
564
|
+
A JSON file or directory of JSON files specifying required schemas to pass to the Function pipeline.
|
|
537
565
|
--include-full-xr, -x
|
|
538
566
|
Include a direct copy of the input XR's spedc and metadata fields in the rendered output.
|
|
539
567
|
--include-connection-xr
|
|
@@ -602,9 +630,15 @@ status:
|
|
|
602
630
|
Most of the examples contain a `render.sh` command which uses `function-pythonic render` to
|
|
603
631
|
render the example.
|
|
604
632
|
|
|
605
|
-
##
|
|
633
|
+
## Shared Python Packages
|
|
634
|
+
|
|
635
|
+
Python packages and modules can be added to the function-pythonic runtime
|
|
636
|
+
by including the python code in any of the following resources: ConfigMap,
|
|
637
|
+
Secret, EnvironmentConfig, or Composition
|
|
638
|
+
|
|
639
|
+
### ConfigMap Packages
|
|
606
640
|
|
|
607
|
-
ConfigMap based python packages are enable using the `--packages` and
|
|
641
|
+
ConfigMap based python packages are enable using the `--packages-configmaps` and
|
|
608
642
|
`--packages-namespace` command line options. ConfigMaps with the label
|
|
609
643
|
`function-pythonic.package` will be incorporated in the python path at
|
|
610
644
|
the location configured in the label value. For example, the following
|
|
@@ -666,7 +700,7 @@ data:
|
|
|
666
700
|
composite: example.pythonic.features.FeatureOneComposite
|
|
667
701
|
...
|
|
668
702
|
```
|
|
669
|
-
This requires enabling the the packages support using the `--packages` command
|
|
703
|
+
This requires enabling the the packages support using the `--packages-configmaps` command
|
|
670
704
|
line option in the DeploymentRuntimeConfig and configuring the required
|
|
671
705
|
Kubernetes RBAC permissions. For example:
|
|
672
706
|
```yaml
|
|
@@ -675,7 +709,7 @@ kind: Function
|
|
|
675
709
|
metadata:
|
|
676
710
|
name: function-pythonic
|
|
677
711
|
spec:
|
|
678
|
-
package: xpkg.
|
|
712
|
+
package: xpkg.crossplane.io/crossplane-contrib/function-pythonic:v0.6.0
|
|
679
713
|
runtimeConfigRef:
|
|
680
714
|
name: function-pythonic
|
|
681
715
|
---
|
|
@@ -737,9 +771,71 @@ ClusterRole permissions. The `--packages-namespace` command line option will res
|
|
|
737
771
|
to only using the supplied namespace. This option can be invoked multiple times.
|
|
738
772
|
The above RBAC permission can then be per namespace RBAC Role permissions.
|
|
739
773
|
|
|
774
|
+
### Secret Packages
|
|
775
|
+
|
|
740
776
|
Secrets can also be used in an identical manner as ConfigMaps by enabling the
|
|
741
777
|
`--packages-secrets` command line option. Secrets permissions need to be
|
|
742
|
-
added to the above RBAC configuration.
|
|
778
|
+
added to the above RBAC configuration. Secret based python packages also enable
|
|
779
|
+
provisioning files with binary data.
|
|
780
|
+
|
|
781
|
+
### EnvironmentConfig Packages
|
|
782
|
+
|
|
783
|
+
EnvironmentConfig based provisioning enable an entire package and module
|
|
784
|
+
directory structure. Use the `--packages-environmentconfigs` command line option
|
|
785
|
+
and configure the ClusterRole RBAC access.
|
|
786
|
+
```yaml
|
|
787
|
+
apiVersion: apiextensions.crossplane.io/v1beta1
|
|
788
|
+
kind: EnvironmentConfig
|
|
789
|
+
metadata:
|
|
790
|
+
name: test
|
|
791
|
+
labels:
|
|
792
|
+
function-pythonic.package: 'true'
|
|
793
|
+
data:
|
|
794
|
+
arootpackage:
|
|
795
|
+
asubpackage:
|
|
796
|
+
bmodule.py: |
|
|
797
|
+
def hello(where):
|
|
798
|
+
return f"Hello, {where}!"
|
|
799
|
+
amodule.py: |
|
|
800
|
+
def goodby(where):
|
|
801
|
+
return f"Goodby, {where}!"
|
|
802
|
+
```
|
|
803
|
+
### Composition Packages
|
|
804
|
+
|
|
805
|
+
Composition based provisioning works just like EnvironmentConfig where a
|
|
806
|
+
directory structure is created. Use the `--packages-compositions` command line option
|
|
807
|
+
and configure the ClusterRole RBAC access. The main reason to use Composition
|
|
808
|
+
based provision is because Compositions can be included in a Crossplane
|
|
809
|
+
Configuration Package.
|
|
810
|
+
```yaml
|
|
811
|
+
apiVersion: apiextensions.crossplane.io/v1
|
|
812
|
+
kind: Composition
|
|
813
|
+
metadata:
|
|
814
|
+
labels:
|
|
815
|
+
function-pythonic.package: 'true'
|
|
816
|
+
name: test
|
|
817
|
+
spec:
|
|
818
|
+
compositeTypeRef:
|
|
819
|
+
apiVersion: code.pythoni.com/v1alpha1
|
|
820
|
+
kind: Code
|
|
821
|
+
mode: Pipeline
|
|
822
|
+
pipeline:
|
|
823
|
+
- step: render
|
|
824
|
+
functionRef:
|
|
825
|
+
name: function-pythonic
|
|
826
|
+
input:
|
|
827
|
+
apiVersion: pythonic.fn.crossplane.io/v1alpha1
|
|
828
|
+
kind: Composite
|
|
829
|
+
packages:
|
|
830
|
+
arootpackage:
|
|
831
|
+
asubpackage:
|
|
832
|
+
bmodule.py: |
|
|
833
|
+
def hello(where):
|
|
834
|
+
return f"Hello, {where}!"
|
|
835
|
+
amodule.py: |
|
|
836
|
+
def goodby(where):
|
|
837
|
+
return f"Goodby, {where}!"
|
|
838
|
+
```
|
|
743
839
|
|
|
744
840
|
## Step Parameters
|
|
745
841
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
crossplane/pythonic/__about__.py,sha256=v3R2QlL6bK_t2oKW7b96iHro1p7Iag8djZQbp6xQLPE,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=gQ7DzY0kn8J2lUWH9hYVLZB6NYDBo0IJJwUDBooTNsc,34459
|
|
7
|
+
crossplane/pythonic/function.py,sha256=2f8-J5sgDrtn3z6Fxu2JODnr6VsjBESIVSJ5Hd4LssY,18573
|
|
8
|
+
crossplane/pythonic/grpc.py,sha256=WFyxkIh-nNtOswQhtN1V3e-8Qr79fDcNht8OpvihHYM,5694
|
|
9
|
+
crossplane/pythonic/main.py,sha256=ujUa_FYElQSGqnhZ-0NJrD3kSyYjfRbIp79FV2Yl7hs,599
|
|
10
|
+
crossplane/pythonic/packages.py,sha256=cOlF-wo2CzknwFPAfuwgCiwdT5vDE4n7oBMfQErVzbA,7023
|
|
11
|
+
crossplane/pythonic/protobuf.py,sha256=-HFHlGV1zMi5DmTOxe9qcBMJ0bZKH2hEkJH1CF7nvp4,53128
|
|
12
|
+
crossplane/pythonic/render.py,sha256=qIcUsE6iCka6VwUlhpN1BZhHvbl3GL6X3dPepmdlDV8,33822
|
|
13
|
+
crossplane/pythonic/version.py,sha256=-RiB0p146ayqJj0SXfYxTNv49u9Fx9pPgm59Ji2blhc,214
|
|
14
|
+
crossplane_function_pythonic-0.6.0.dist-info/METADATA,sha256=kaKnXE14jueu5X-kf8k2H904NIg9qV-o4WGOJOaUqqs,36678
|
|
15
|
+
crossplane_function_pythonic-0.6.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
16
|
+
crossplane_function_pythonic-0.6.0.dist-info/entry_points.txt,sha256=jJ4baywFDviB9WyAhyhNYF2VOCb6XtbRSjKf7bnBwhg,68
|
|
17
|
+
crossplane_function_pythonic-0.6.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
18
|
+
crossplane_function_pythonic-0.6.0.dist-info/RECORD,,
|
|
@@ -1,18 +0,0 @@
|
|
|
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,,
|
{crossplane_function_pythonic-0.5.0.dist-info → crossplane_function_pythonic-0.6.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|