Skip to content

Commit aabed6e

Browse files
renovate[bot]iciclespider
authored andcommitted
Integrate render command with an active cluster
1 parent 4f53ce9 commit aabed6e

File tree

4 files changed

+543
-180
lines changed

4 files changed

+543
-180
lines changed

crossplane/pythonic/composite.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,12 +242,18 @@ def __init__(self, composite, name):
242242
self.autoReady = None
243243
self.usages = None
244244

245-
def __call__(self, apiVersion=_notset, kind=_notset, namespace=_notset, name=_notset):
245+
def __call__(self, kind=_notset, apiVersion=_notset, namespace=_notset, name=_notset):
246246
self.desired()
247+
if kind != _notset:
248+
# Allow for apiVersion in the first arg and kind in the second arg
249+
if '/' in kind or kind == 'v1':
250+
if apiVersion != _notset:
251+
self.kind = apiVersion
252+
apiVersion = kind
253+
else:
254+
self.kind = kind
247255
if apiVersion != _notset:
248256
self.apiVersion = apiVersion
249-
if kind != _notset:
250-
self.kind = kind
251257
if namespace != _notset:
252258
self.metadata.namespace = namespace
253259
if name != _notset:
@@ -408,12 +414,18 @@ def __init__(self, composite, name):
408414
self._resources = composite.request.required_resources[name]
409415
self._cache = {}
410416

411-
def __call__(self, apiVersion=_notset, kind=_notset, namespace=_notset, name=_notset, labels=_notset):
417+
def __call__(self, kind=_notset, apiVersion=_notset, namespace=_notset, name=_notset, labels=_notset):
412418
self._selector()
419+
if kind != _notset:
420+
# Allow for apiVersion in the first arg and kind in the second arg
421+
if '/' in kind or kind == 'v1':
422+
if apiVersion != _notset:
423+
self.kind = apiVersion
424+
apiVersion = kind
425+
else:
426+
self.kind = kind
413427
if apiVersion != _notset:
414428
self.apiVersion = apiVersion
415-
if kind != _notset:
416-
self.kind = kind
417429
if namespace != _notset:
418430
self.namespace = namespace
419431
if name != _notset:

crossplane/pythonic/main.py

Lines changed: 186 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,197 @@
1-
"""The function-pythonic's main CLI."""
1+
"""The composition function's main CLI."""
22

33
import argparse
44
import asyncio
5+
import logging
6+
import os
7+
import pathlib
8+
import shlex
9+
import signal
510
import sys
11+
import traceback
612

7-
from . import (
8-
grpc,
9-
render,
10-
version,
11-
)
13+
import crossplane.function.logging
14+
import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1
15+
import grpc
16+
17+
from . import function
1218

1319

1420
def main():
15-
parser = argparse.ArgumentParser('Crossplane Function Pythonic')
16-
subparsers = parser.add_subparsers(title='Command', metavar='')
17-
grpc.Command.create(subparsers)
18-
render.Command.create(subparsers)
19-
version.Command.create(subparsers)
20-
args = parser.parse_args()
21-
if not hasattr(args, 'command'):
22-
parser.print_help()
23-
sys.exit(1)
24-
asyncio.run(args.command(args).run())
21+
asyncio.run(Main().main())
22+
23+
24+
class Main:
25+
async def main(self):
26+
parser = argparse.ArgumentParser('Crossplane Function Pythonic')
27+
parser.add_argument(
28+
'--debug', '-d',
29+
action='store_true',
30+
help='Emit debug logs.',
31+
)
32+
parser.add_argument(
33+
'--log-name-width',
34+
type=int,
35+
default=40,
36+
metavar='WIDTH',
37+
help='Width of the logger name in the log output, default 40',
38+
)
39+
parser.add_argument(
40+
'--address',
41+
default='0.0.0.0:9443',
42+
help='Address to listen on for gRPC connections, default: 0.0.0.0:9443',
43+
)
44+
parser.add_argument(
45+
'--tls-certs-dir',
46+
default=os.getenv('TLS_SERVER_CERTS_DIR'),
47+
metavar='DIRECTORY',
48+
help='Serve using TLS certificates.',
49+
)
50+
parser.add_argument(
51+
'--insecure',
52+
action='store_true',
53+
help='Run without mTLS credentials, --tls-certs-dir will be ignored.',
54+
)
55+
parser.add_argument(
56+
'--packages',
57+
action='store_true',
58+
help='Discover python packages from function-pythonic ConfigMaps.'
59+
)
60+
parser.add_argument(
61+
'--packages-secrets',
62+
action='store_true',
63+
help='Also Discover python packages from function-pythonic Secrets.'
64+
)
65+
parser.add_argument(
66+
'--packages-namespace',
67+
action='append',
68+
default=[],
69+
metavar='NAMESPACE',
70+
help='Namespaces to discover function-pythonic ConfigMaps in, default is cluster wide.',
71+
)
72+
parser.add_argument(
73+
'--packages-dir',
74+
default='./pythonic-packages',
75+
metavar='DIRECTORY',
76+
help='Directory to store discovered function-pythonic ConfigMaps to, defaults "<cwd>/pythonic-packages"'
77+
)
78+
parser.add_argument(
79+
'--pip-install',
80+
metavar='COMMAND',
81+
help='Pip install command to install additional Python packages.'
82+
)
83+
parser.add_argument(
84+
'--python-path',
85+
action='append',
86+
default=[],
87+
metavar='DIRECTORY',
88+
help='Filing system directories to add to the python path',
89+
)
90+
parser.add_argument(
91+
'--allow-oversize-protos',
92+
action='store_true',
93+
help='Allow oversized protobuf messages'
94+
)
95+
parser.add_argument(
96+
'--render-unknowns',
97+
action='store_true',
98+
help='Render resources with unknowns, useful during local develomment'
99+
)
100+
args = parser.parse_args()
101+
if not args.tls_certs_dir and not args.insecure:
102+
print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr)
103+
sys.exit(1)
104+
105+
if args.pip_install:
106+
import pip._internal.cli.main
107+
pip._internal.cli.main.main(['install', '--user', *shlex.split(args.pip_install)])
108+
109+
for path in reversed(args.python_path):
110+
sys.path.insert(0, str(pathlib.Path(path).expanduser().resolve()))
111+
112+
self.configure_logging(args)
113+
# enables read only volumes or mismatched uid volumes
114+
sys.dont_write_bytecode = True
115+
await self.run(args)
116+
117+
# Allow for independent running of function-pythonic
118+
async def run(self, args):
119+
if args.allow_oversize_protos:
120+
from google.protobuf.internal import api_implementation
121+
if api_implementation._c_module:
122+
api_implementation._c_module.SetAllowOversizeProtos(True)
123+
124+
grpc.aio.init_grpc_aio()
125+
grpc_runner = function.FunctionRunner(args.debug, args.render_unknowns)
126+
grpc_server = grpc.aio.server()
127+
grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
128+
if args.insecure:
129+
grpc_server.add_insecure_port(args.address)
130+
else:
131+
certs = pathlib.Path(args.tls_certs_dir).expanduser().resolve()
132+
grpc_server.add_secure_port(
133+
args.address,
134+
grpc.ssl_server_credentials(
135+
private_key_certificate_chain_pairs=[(
136+
(certs / 'tls.key').read_bytes(),
137+
(certs / 'tls.crt').read_bytes(),
138+
)],
139+
root_certificates=(certs / 'ca.crt').read_bytes(),
140+
require_client_auth=True,
141+
),
142+
)
143+
await grpc_server.start()
144+
145+
if args.packages:
146+
from . import packages
147+
async with asyncio.TaskGroup() as tasks:
148+
tasks.create_task(grpc_server.wait_for_termination())
149+
tasks.create_task(packages.operator(
150+
grpc_server,
151+
grpc_runner,
152+
args.packages_secrets,
153+
args.packages_namespace,
154+
args.packages_dir,
155+
))
156+
else:
157+
def stop():
158+
asyncio.ensure_future(grpc_server.stop(5))
159+
loop = asyncio.get_event_loop()
160+
loop.add_signal_handler(signal.SIGINT, stop)
161+
loop.add_signal_handler(signal.SIGTERM, stop)
162+
await grpc_server.wait_for_termination()
163+
164+
def configure_logging(self, args):
165+
formatter = Formatter(args.log_name_width)
166+
handler = logging.StreamHandler()
167+
handler.setFormatter(formatter)
168+
logger = logging.getLogger()
169+
logger.handlers = [handler]
170+
logger.setLevel(logging.DEBUG if args.debug else logging.INFO)
171+
172+
173+
class Formatter(logging.Formatter):
174+
def __init__(self, name_width):
175+
super(Formatter, self).__init__(
176+
f"[{{asctime}}.{{msecs:03.0f}}] {{sname:{name_width}.{name_width}}} [{{levelname:8.8}}] {{message}}",
177+
'%Y-%m-%d %H:%M:%S',
178+
'{',
179+
)
180+
self.name_width = name_width
181+
182+
def format(self, record):
183+
record.sname = record.name
184+
extra = len(record.sname) - self.name_width
185+
if extra > 0:
186+
names = record.sname.split('.')
187+
for ix, name in enumerate(names):
188+
if len(name) > extra:
189+
names[ix] = name[extra:]
190+
break
191+
names[ix] = name[:1]
192+
extra -= len(name) - 1
193+
record.sname = '.'.join(names)
194+
return super(Formatter, self).format(record)
25195

26196

27197
if __name__ == '__main__':

crossplane/pythonic/protobuf.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,29 @@ def Unknown():
3737

3838
def Yaml(string, readOnly=None):
3939
if isinstance(string, (FieldMessage, Value)):
40+
if not string:
41+
return string
4042
string = str(string)
4143
return Value(None, None, yaml.safe_load(string), readOnly)
4244

4345
def Json(string, readOnly=None):
4446
if isinstance(string, (FieldMessage, Value)):
47+
if not string:
48+
return string
4549
string = str(string)
4650
return Value(None, None, json.loads(string), readOnly)
4751

4852
def B64Encode(string):
4953
if isinstance(string, (FieldMessage, Value)):
54+
if not string:
55+
return string
5056
string = str(string)
5157
return base64.b64encode(string.encode('utf-8')).decode('utf-8')
5258

5359
def B64Decode(string):
5460
if isinstance(string, (FieldMessage, Value)):
61+
if not string:
62+
return string
5563
string = str(string)
5664
return base64.b64decode(string.encode('utf-8')).decode('utf-8')
5765

@@ -684,10 +692,11 @@ def __getattr__(self, key):
684692

685693
def __getitem__(self, key):
686694
key = self._validate_key(key)
687-
if key in self._cache:
688-
return self._cache[key]
689-
if key in self._unknowns:
690-
return self._unknowns[key]
695+
if key != append:
696+
if key in self._cache:
697+
return self._cache[key]
698+
if key in self._unknowns:
699+
return self._unknowns[key]
691700
if isinstance(key, str):
692701
match self._kind:
693702
case 'struct_value':
@@ -701,14 +710,26 @@ def __getitem__(self, key):
701710
elif isinstance(key, int):
702711
match self._kind:
703712
case 'list_value':
713+
if key < 0:
714+
key = len(self._value.list_value.values) + key
715+
if key < 0:
716+
key = 0
704717
if key < len(self._value.list_value.values):
705718
value = self._value.list_value.values[key]
706719
else:
720+
if key == append:
721+
key = len(self._value.list_value.values)
707722
value = _Unknown
708723
case 'ListValue':
724+
if key < 0:
725+
key = len(self._value.values) + key
726+
if key < 0:
727+
key = 0
709728
if key < len(self._value.values):
710729
value = self._value.values[key]
711730
else:
731+
if key == append:
732+
key = len(self._value.values)
712733
value = _Unknown
713734
case 'Unknown':
714735
value = _Unknown

0 commit comments

Comments
 (0)