crossplane-function-pythonic 0.5.0__py3-none-any.whl → 0.6.1__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 +157 -3
- crossplane/pythonic/function.py +21 -6
- crossplane/pythonic/grpc.py +26 -4
- crossplane/pythonic/packages.py +128 -96
- crossplane/pythonic/proto/v1/run_function.proto +436 -0
- crossplane/pythonic/proto/v1/run_function_pb2.py +129 -0
- crossplane/pythonic/proto/v1/run_function_pb2.pyi +306 -0
- crossplane/pythonic/proto/v1/run_function_pb2_grpc.py +101 -0
- crossplane/pythonic/protobuf.py +28 -0
- crossplane/pythonic/render.py +107 -10
- {crossplane_function_pythonic-0.5.0.dist-info → crossplane_function_pythonic-0.6.1.dist-info}/METADATA +115 -18
- crossplane_function_pythonic-0.6.1.dist-info/RECORD +22 -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.1.dist-info}/WHEEL +0 -0
- {crossplane_function_pythonic-0.5.0.dist-info → crossplane_function_pythonic-0.6.1.dist-info}/entry_points.txt +0 -0
- {crossplane_function_pythonic-0.5.0.dist-info → crossplane_function_pythonic-0.6.1.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.1"
|
crossplane/pythonic/composite.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
|
|
2
2
|
import datetime
|
|
3
3
|
from google.protobuf.duration_pb2 import Duration
|
|
4
|
-
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
|
|
5
4
|
|
|
6
|
-
from . import
|
|
7
|
-
|
|
5
|
+
from . import (
|
|
6
|
+
auto_ready,
|
|
7
|
+
protobuf,
|
|
8
|
+
)
|
|
9
|
+
from .proto.v1 import run_function_pb2 as fnv1
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
_notset = object()
|
|
@@ -104,11 +106,13 @@ class BaseComposite:
|
|
|
104
106
|
)
|
|
105
107
|
self.response = protobuf.Message(None, 'response', response.DESCRIPTOR, response)
|
|
106
108
|
self.logger = logger
|
|
109
|
+
self.capabilities = Capabilities(self.request.meta.capabilities)
|
|
107
110
|
self.parameters = self.request.input.parameters
|
|
108
111
|
self.credentials = Credentials(self.request)
|
|
109
112
|
self.context = self.response.context
|
|
110
113
|
self.environment = self.context['apiextensions.crossplane.io/environment']
|
|
111
114
|
self.requireds = Requireds(self)
|
|
115
|
+
self.schemas = Schemas(self)
|
|
112
116
|
self.resources = Resources(self)
|
|
113
117
|
self.autoReady = True
|
|
114
118
|
self.usages = False
|
|
@@ -123,6 +127,7 @@ class BaseComposite:
|
|
|
123
127
|
self.metadata = self.observed.metadata
|
|
124
128
|
self.spec = self.observed.spec
|
|
125
129
|
self.status = self.desired.status
|
|
130
|
+
self.output = self.response.output
|
|
126
131
|
self.conditions = Conditions(observed, self.response)
|
|
127
132
|
self.results = Results(self.response)
|
|
128
133
|
self.events = Results(self.response) # Deprecated, use self.results
|
|
@@ -136,6 +141,30 @@ class BaseComposite:
|
|
|
136
141
|
raise NotImplementedError()
|
|
137
142
|
|
|
138
143
|
|
|
144
|
+
class Capabilities:
|
|
145
|
+
def __init__(self, capabilities):
|
|
146
|
+
self._capabilities = capabilities
|
|
147
|
+
|
|
148
|
+
def __bool__(self):
|
|
149
|
+
return fnv1.CAPABILITY_CAPABILITIES in self._capabilities
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def requireds(self):
|
|
153
|
+
return fnv1.CAPABILITY_REQUIRED_RESOURCES in self._capabilities if self else None
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def credentials(self):
|
|
157
|
+
return fnv1.CAPABILITY_CREDENTIALS in self._capabilities if self else None
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def conditions(self):
|
|
161
|
+
return fnv1.CAPABILITY_CONDITIONS in self._capabilities if self else None
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def schemas(self):
|
|
165
|
+
return fnv1.CAPABILITY_REQUIRED_SCHEMAS in self._capabilities if self else None
|
|
166
|
+
|
|
167
|
+
|
|
139
168
|
class Credentials:
|
|
140
169
|
def __init__(self, request):
|
|
141
170
|
self.__dict__['_request'] = request
|
|
@@ -558,6 +587,131 @@ class RequiredResource:
|
|
|
558
587
|
return bool(self.observed)
|
|
559
588
|
|
|
560
589
|
|
|
590
|
+
class Schemas:
|
|
591
|
+
def __init__(self, composite):
|
|
592
|
+
self._composite = composite
|
|
593
|
+
self._cache = {}
|
|
594
|
+
|
|
595
|
+
def __getattr__(self, key):
|
|
596
|
+
return self[key]
|
|
597
|
+
|
|
598
|
+
def __getitem__(self, key):
|
|
599
|
+
schema = self._cache.get(key)
|
|
600
|
+
if not schema:
|
|
601
|
+
schema = Schema(self._composite, key)
|
|
602
|
+
self._cache[key] = schema
|
|
603
|
+
return schema
|
|
604
|
+
|
|
605
|
+
def __bool__(self):
|
|
606
|
+
return bool(len(self))
|
|
607
|
+
|
|
608
|
+
def __len__(self):
|
|
609
|
+
names = set()
|
|
610
|
+
for name, schema in self._composite.request.required_schemas:
|
|
611
|
+
names.add(name)
|
|
612
|
+
for name, selector in self._composite.response.requirements.schemas:
|
|
613
|
+
names.add(name)
|
|
614
|
+
return len(names)
|
|
615
|
+
|
|
616
|
+
def __contains__(self, key):
|
|
617
|
+
if key in self._composite.request.required_schemas:
|
|
618
|
+
return True
|
|
619
|
+
if key in self._composite.response.requirements.schemas:
|
|
620
|
+
return True
|
|
621
|
+
return False
|
|
622
|
+
|
|
623
|
+
def __iter__(self):
|
|
624
|
+
names = set()
|
|
625
|
+
for name, schema in self._composite.request.required_schemas:
|
|
626
|
+
names.add(name)
|
|
627
|
+
for name, selector in self._composite.response.requirements.schemas:
|
|
628
|
+
names.add(name)
|
|
629
|
+
for name in sorted(names):
|
|
630
|
+
yield name, self[name]
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
class Schema:
|
|
634
|
+
def __init__(self, composite, name):
|
|
635
|
+
self.name = name
|
|
636
|
+
self._selector = composite.response.requirements.schemas[name]
|
|
637
|
+
self._schema = composite.request.required_schemas[name].openapi_v3
|
|
638
|
+
|
|
639
|
+
def __call__(self, kind=_notset, apiVersion=_notset):
|
|
640
|
+
self._selector()
|
|
641
|
+
if kind != _notset:
|
|
642
|
+
# Allow for apiVersion in the first arg and kind in the second arg
|
|
643
|
+
if '/' in kind or kind == 'v1':
|
|
644
|
+
if apiVersion != _notset:
|
|
645
|
+
self.kind = apiVersion
|
|
646
|
+
apiVersion = kind
|
|
647
|
+
else:
|
|
648
|
+
self.kind = kind
|
|
649
|
+
if apiVersion != _notset:
|
|
650
|
+
self.apiVersion = apiVersion
|
|
651
|
+
return self
|
|
652
|
+
|
|
653
|
+
@property
|
|
654
|
+
def apiVersion(self):
|
|
655
|
+
return self._selector.api_version
|
|
656
|
+
|
|
657
|
+
@apiVersion.setter
|
|
658
|
+
def apiVersion(self, apiVersion):
|
|
659
|
+
self._selector.api_version = apiVersion
|
|
660
|
+
|
|
661
|
+
@property
|
|
662
|
+
def kind(self):
|
|
663
|
+
return self._selector.kind
|
|
664
|
+
|
|
665
|
+
@kind.setter
|
|
666
|
+
def kind(self, kind):
|
|
667
|
+
self._selector.kind = kind
|
|
668
|
+
|
|
669
|
+
def __enter__(self):
|
|
670
|
+
return self
|
|
671
|
+
|
|
672
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
673
|
+
pass
|
|
674
|
+
|
|
675
|
+
def __aenter__(self):
|
|
676
|
+
return self
|
|
677
|
+
|
|
678
|
+
def __aexit__(self, exc_type, exc_value, traceback):
|
|
679
|
+
pass
|
|
680
|
+
|
|
681
|
+
def __getattr__(self, key):
|
|
682
|
+
return self[key]
|
|
683
|
+
|
|
684
|
+
def __getitem__(self, key):
|
|
685
|
+
return self._schema[key]
|
|
686
|
+
|
|
687
|
+
def __bool__(self):
|
|
688
|
+
return bool(self._schema)
|
|
689
|
+
|
|
690
|
+
def __len__(self):
|
|
691
|
+
return len(self._schema)
|
|
692
|
+
|
|
693
|
+
def __contains__(self, item):
|
|
694
|
+
return item in self._schema
|
|
695
|
+
|
|
696
|
+
def __iter__(self):
|
|
697
|
+
for key, value in self._schema:
|
|
698
|
+
yield key, value
|
|
699
|
+
|
|
700
|
+
def __hash__(self):
|
|
701
|
+
return hash(self._schema)
|
|
702
|
+
|
|
703
|
+
def __eq__(self, other):
|
|
704
|
+
if instance(other, Schema):
|
|
705
|
+
other = other._schema
|
|
706
|
+
return self._schema == other
|
|
707
|
+
|
|
708
|
+
def __str__(self):
|
|
709
|
+
return str(self._schema)
|
|
710
|
+
|
|
711
|
+
def __format__(self, spec='yaml'):
|
|
712
|
+
return format(self,_schema, spec)
|
|
713
|
+
|
|
714
|
+
|
|
561
715
|
class Conditions:
|
|
562
716
|
def __init__(self, observed, response=None):
|
|
563
717
|
self._observed = observed
|
crossplane/pythonic/function.py
CHANGED
|
@@ -7,8 +7,8 @@ import logging
|
|
|
7
7
|
import sys
|
|
8
8
|
|
|
9
9
|
import grpc
|
|
10
|
-
from
|
|
11
|
-
from
|
|
10
|
+
from .proto.v1 import run_function_pb2 as fnv1
|
|
11
|
+
from .proto.v1 import run_function_pb2_grpc as grpcv1
|
|
12
12
|
from .. import pythonic
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
@@ -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
|
@@ -7,7 +7,6 @@ import shlex
|
|
|
7
7
|
import signal
|
|
8
8
|
import sys
|
|
9
9
|
|
|
10
|
-
import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1
|
|
11
10
|
import grpc
|
|
12
11
|
|
|
13
12
|
from . import (
|
|
@@ -15,6 +14,8 @@ from . import (
|
|
|
15
14
|
command,
|
|
16
15
|
function,
|
|
17
16
|
)
|
|
17
|
+
from .proto.v1 import run_function_pb2_grpc as grpcv1
|
|
18
|
+
|
|
18
19
|
|
|
19
20
|
logger = logging.getLogger(__name__)
|
|
20
21
|
|
|
@@ -45,6 +46,12 @@ class Command(command.Command):
|
|
|
45
46
|
parser.add_argument(
|
|
46
47
|
'--packages',
|
|
47
48
|
action='store_true',
|
|
49
|
+
dest='packages_configmaps',
|
|
50
|
+
help='Discover python packages from function-pythonic ConfigMaps, deprecated use --packages-configmaps'
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
'--packages-configmaps',
|
|
54
|
+
action='store_true',
|
|
48
55
|
help='Discover python packages from function-pythonic ConfigMaps.'
|
|
49
56
|
)
|
|
50
57
|
parser.add_argument(
|
|
@@ -57,7 +64,17 @@ class Command(command.Command):
|
|
|
57
64
|
action='append',
|
|
58
65
|
default=[],
|
|
59
66
|
metavar='NAMESPACE',
|
|
60
|
-
help='Namespaces to discover function-pythonic ConfigMaps in, default is cluster wide.',
|
|
67
|
+
help='Namespaces to discover function-pythonic ConfigMaps and Secrets in, default is cluster wide.',
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
'--packages-environmentconfigs',
|
|
71
|
+
action='store_true',
|
|
72
|
+
help='Also Discover python packages from function-pythonic EnvironmentConfigs.'
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
'--packages-compositions',
|
|
76
|
+
action='store_true',
|
|
77
|
+
help='Also Discover python packages from function-pythonic Compositions.'
|
|
61
78
|
)
|
|
62
79
|
parser.add_argument(
|
|
63
80
|
'--packages-dir',
|
|
@@ -75,7 +92,9 @@ class Command(command.Command):
|
|
|
75
92
|
if not self.args.tls_certs_dir and not self.args.insecure:
|
|
76
93
|
print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr)
|
|
77
94
|
sys.exit(1)
|
|
78
|
-
|
|
95
|
+
if (self.args.packages_environmentconfigs or self.args.packages_compositions) and self.args.packages_namespace:
|
|
96
|
+
print('--packages-namespace cannot be used with --packages-environment-configs or --packages-compositions', file=sys.stderr)
|
|
97
|
+
sys.exit(1)
|
|
79
98
|
if self.args.pip_install:
|
|
80
99
|
import pip._internal.cli.main
|
|
81
100
|
pip._internal.cli.main.main(['install', '--user', *shlex.split(self.args.pip_install)])
|
|
@@ -108,15 +127,18 @@ class Command(command.Command):
|
|
|
108
127
|
)
|
|
109
128
|
await grpc_server.start()
|
|
110
129
|
|
|
111
|
-
if self.args.
|
|
130
|
+
if self.args.packages_configmaps or self.args.packages_secrets or self.args.packages_environmentconfigs or self.args.packages_compositions:
|
|
112
131
|
from . import packages
|
|
113
132
|
async with asyncio.TaskGroup() as tasks:
|
|
114
133
|
tasks.create_task(grpc_server.wait_for_termination())
|
|
115
134
|
tasks.create_task(packages.operator(
|
|
116
135
|
grpc_server,
|
|
117
136
|
grpc_runner,
|
|
137
|
+
self.args.packages_configmaps,
|
|
118
138
|
self.args.packages_secrets,
|
|
119
139
|
self.args.packages_namespace,
|
|
140
|
+
self.args.packages_environmentconfigs,
|
|
141
|
+
self.args.packages_compositions,
|
|
120
142
|
self.args.packages_dir,
|
|
121
143
|
))
|
|
122
144
|
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
|