Skip to content

Commit eba1d8b

Browse files
committed
Implement obtain python packages from EnvironmentConfigs and from Compositions
Add support for Capabilities and Required Schemas
1 parent 497cc17 commit eba1d8b

10 files changed

Lines changed: 845 additions & 112 deletions

File tree

crossplane/pythonic/composite.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,13 @@ def __init__(self, crossplane_v1, request, logger):
104104
)
105105
self.response = protobuf.Message(None, 'response', response.DESCRIPTOR, response)
106106
self.logger = logger
107+
self.capabilities = Capabilities(self.request.meta.capabilities)
107108
self.parameters = self.request.input.parameters
108109
self.credentials = Credentials(self.request)
109110
self.context = self.response.context
110111
self.environment = self.context['apiextensions.crossplane.io/environment']
111112
self.requireds = Requireds(self)
113+
self.schemas = Schemas(self)
112114
self.resources = Resources(self)
113115
self.autoReady = True
114116
self.usages = False
@@ -123,6 +125,7 @@ def __init__(self, crossplane_v1, request, logger):
123125
self.metadata = self.observed.metadata
124126
self.spec = self.observed.spec
125127
self.status = self.desired.status
128+
self.output = self.response.output
126129
self.conditions = Conditions(observed, self.response)
127130
self.results = Results(self.response)
128131
self.events = Results(self.response) # Deprecated, use self.results
@@ -136,6 +139,30 @@ async def compose(self):
136139
raise NotImplementedError()
137140

138141

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 schemas(self):
155+
return fnv1.CAPABILITY_REQUIRED_SCHEMAS in self._capabilities if self else None
156+
157+
@property
158+
def credentials(self):
159+
return fnv1.CAPABILITY_CREDENTIALS in self._capabilities if self else None
160+
161+
@property
162+
def conditions(self):
163+
return fnv1.CAPABILITY_CONDITIONS in self._capabilities if self else None
164+
165+
139166
class Credentials:
140167
def __init__(self, request):
141168
self.__dict__['_request'] = request
@@ -558,6 +585,150 @@ def __bool__(self):
558585
return bool(self.observed)
559586

560587

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+
713+
class RequiredResource:
714+
def __init__(self, name, ix, resource):
715+
self.name = name
716+
self.ix = ix
717+
self.observed = resource.resource
718+
self.apiVersion = self.observed.apiVersion
719+
self.kind = self.observed.kind
720+
self.metadata = self.observed.metadata
721+
self.spec = self.observed.spec
722+
self.type = self.observed.type
723+
self.data = self.observed.data
724+
self.status = self.observed.status
725+
self.conditions = Conditions(resource)
726+
self.connection = self.observed.connection_details
727+
728+
def __bool__(self):
729+
return bool(self.observed)
730+
731+
561732
class Conditions:
562733
def __init__(self, observed, response=None):
563734
self._observed = observed

crossplane/pythonic/function.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,13 @@ async def run_function(self, request):
121121
except Exception as e:
122122
return self.fatal(request, logger, 'Compose', e)
123123

124-
if requireds := self.get_requireds(step, composite):
125-
logger.debug(f"Requireds requested: {','.join(requireds)}")
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)}")
126131
else:
127132
self.process_usages(composite)
128133
self.process_unknowns(composite)
@@ -155,11 +160,21 @@ def fatal(self, request, logger, message, exception=None):
155160
]
156161
)
157162

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+
158173
def get_requireds(self, step, composite):
159174
requireds = []
160175
for name, required in composite.requireds:
161-
if len(required.apiVersion) and len(required.kind):
162-
r = pythonic.Map(apiVersion=required.apiVersion, kind=required.kind)
176+
if len(required.kind) and len(required.apiVersion):
177+
r = pythonic.Map(kind=required.kind, apiVersion=required.apiVersion)
163178
if len(required.namespace):
164179
r.namespace = required.namespace
165180
if len(required.matchName):

crossplane/pythonic/grpc.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ def add_parser_arguments(cls, parser):
4545
parser.add_argument(
4646
'--packages',
4747
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',
4854
help='Discover python packages from function-pythonic ConfigMaps.'
4955
)
5056
parser.add_argument(
@@ -57,7 +63,17 @@ def add_parser_arguments(cls, parser):
5763
action='append',
5864
default=[],
5965
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.'
6177
)
6278
parser.add_argument(
6379
'--packages-dir',
@@ -75,7 +91,9 @@ def initialize(self):
7591
if not self.args.tls_certs_dir and not self.args.insecure:
7692
print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr)
7793
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)
7997
if self.args.pip_install:
8098
import pip._internal.cli.main
8199
pip._internal.cli.main.main(['install', '--user', *shlex.split(self.args.pip_install)])
@@ -108,15 +126,18 @@ async def run(self):
108126
)
109127
await grpc_server.start()
110128

111-
if self.args.packages:
129+
if self.args.packages_configmaps or self.args.packages_secrets or self.args.packages_environmentconfigs or self.args.packages_compositions:
112130
from . import packages
113131
async with asyncio.TaskGroup() as tasks:
114132
tasks.create_task(grpc_server.wait_for_termination())
115133
tasks.create_task(packages.operator(
116134
grpc_server,
117135
grpc_runner,
136+
self.args.packages_configmaps,
118137
self.args.packages_secrets,
119138
self.args.packages_namespace,
139+
self.args.packages_environmentconfigs,
140+
self.args.packages_compositions,
120141
self.args.packages_dir,
121142
))
122143
else:

0 commit comments

Comments
 (0)