diff --git a/.gitignore b/.gitignore index c558fb4..c5ab72d 100644 --- a/.gitignore +++ b/.gitignore @@ -215,3 +215,4 @@ crossplane/pythonic/__version__.py pocs/ pythonic-packages/ tests/protobuf/pytest_pb2* +scripts/.aws-credentials diff --git a/README.md b/README.md index e462a7e..5194f21 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,39 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 ``` + +### Crossplane V1 +When running function-pythonic in Crossplane V1, the `--crossplane-v1` command line +option should be specified. This requires using a Crossplane DeploymentRuntimeConfig. +```yaml +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-pythonic +spec: + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 + runtimeConfigRef: + name: function-pythonic +-- +apiVersion: pkg.crossplane.io/v1beta1 +kind: DeploymentRuntimeConfig +metadata: + name: function-pythonic +spec: + deploymentTemplate: + spec: + selector: {} + template: + spec: + containers: + - name: package-runtime + args: + - --debug + - --crossplane-v1 +``` + ## Composed Resource Dependencies function-pythonic automatically handles dependencies between composed resources. @@ -204,9 +235,10 @@ The BaseComposite class provides the following fields for manipulating the Compo | self.status | Map | The composite desired and observed status, read from observed if not in desired | | self.conditions | Conditions | The composite desired and observed conditions, read from observed if not in desired | | self.results | Results | Returned results applied to the Composite and optionally on the Claim | +| self.connectionSecret | Map | The name, namespace, and resourceName to use when generating the connection secret in Crossplane v2 | | self.connection | Map | The composite desired connection detials | +| self.connection.observed | Map | The composite observed connection detials | | self.ready | Boolean | The composite desired ready state | -| self.observed.connection | Map | The composite observed connection detials | The BaseComposite also provides access to the following Crossplane Function level features: @@ -253,7 +285,7 @@ Resource class: | Resource.usages | Boolean | Generate Crossplane Usages for this resource, default is Composite.autoReady | | Resource.autoReady | Boolean | Perform auto ready processing on this resource, default is Composite.autoReady | -### Required Resources (AKA Extra Resources) +### Required Resources Creating and accessing required resources is performed using the `BaseComposite.requireds` field. `BaseComposite.requireds` is a dictionary of the required resources whose key is the required @@ -523,7 +555,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 runtimeConfigRef: name: function-pythonic --- diff --git a/crossplane/pythonic/auto_ready.py b/crossplane/pythonic/auto_ready.py new file mode 100644 index 0000000..5efda6e --- /dev/null +++ b/crossplane/pythonic/auto_ready.py @@ -0,0 +1,153 @@ + + +def process(composite): + for name, resource in composite.resources: + if resource.observed: + if resource.autoReady or (resource.autoReady is None and composite.autoReady): + if resource.ready is None: + if _checks.get((resource.apiVersion, resource.kind), _check_default).ready(resource): + resource.ready = True + + +class ConditionReady: + def ready(self, resource): + return bool(resource.conditions.Ready.status) + +_checks = {} +_check_default = ConditionReady() + +class Check: + @classmethod + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + if hasattr(cls, 'apiVersion'): + _checks[(cls.apiVersion, cls.__name__)] = cls() + + def ready(self, resource): + raise NotImplementedError() + +class AlwaysReady(Check): + def ready(self, resource): + return True + + +class ClusterRole(AlwaysReady): + apiVersion = 'rbac.authorization.k8s.io/v1' + +class ClusterRoleBinding(AlwaysReady): + apiVersion = 'rbac.authorization.k8s.io/v1' + +class ConfigMap(AlwaysReady): + apiVersion = 'v1' + +class CronJob(Check): + apiVersion = 'batch/v1' + def ready(self, resource): + if resource.observed.spec.suspend and len(resource.observed.spec.suspend): + return True + if not resource.status.lastScheduleTime: + return False + if resource.status.active: + return True + if not resource.status.lastSuccessfulTime: + return False + return str(resource.status.lastSuccessfulTime) >= str(resource.status.lastScheduleTime) + +class DaemonSet(Check): + apiVersion = 'apps/v1' + def ready(self, resource): + if not resource.status.desiredNumberScheduled: + return False + scheduled = resource.status.desiredNumberScheduled + return (scheduled == resource.status.numberReady and + scheduled == resource.status.updatedNumberScheduled and + scheduled == resource.status.numberAvailable + ) + +class Deployment(Check): + apiVersion = 'apps/v1' + def ready(self, resource): + replicas = resource.observed.spec.replicas or 1 + if resource.status.updatedReplicas != replicas or resource.status.availableReplicas != replicas: + return False + return bool(resource.conditions.Available.status) + +class HorizontalPodAutoscaler(Check): + apiVersion = 'autoscaling/v2' + def ready(self, resource): + for type in ('FailedGetScale', 'FailedUpdateScale', 'FailedGetResourceMetric', 'InvalidSelector'): + if resource.conditions[type].status: + return False + for type in ('ScalingActive', 'ScalingLimited'): + if resource.conditions[type].status: + return True + return False + +class Ingress(Check): + apiVersion = 'networking.k8s.io/v1' + def ready(self, resource): + return len(resource.status.loadBalancer.ingress) > 0 + +class Job(Check): + apiVersion = 'batch/v1' + def ready(self, resource): + for type in ('Failed', 'Suspended'): + if resource.conditions[type].status: + return False + return bool(resource.conditions.Complete.status) + +class Namespace(AlwaysReady): + apiVersion = 'v1' + +class PersistentVolumeClaim(Check): + apiVersion = 'v1' + def ready(self, resource): + return resource.status.phase == 'Bound' + +class Pod(Check): + apiVersion = 'v1' + def ready(self, resource): + if resource.status.phase == 'Succeeded': + return True + if resource.status.phase == 'Running': + if resource.observed.spec.restartPolicy == 'Always': + if resource.conditions.Ready.status: + return True + return False + +class ReplicaSet(Check): + apiVersion = 'v1' + def ready(self, resource): + if int(resource.status.observedGeneration) < int(resource.observed.metadata.generation): + return False + if resource.conditions.ReplicaFailure.status: + return False + return int(resource.status.availableReplicas) >= int(resource.observed.spec.replicas or 1) + +class Role(AlwaysReady): + apiVersion = 'rbac.authorization.k8s.io/v1' + +class RoleBinding(AlwaysReady): + apiVersion = 'rbac.authorization.k8s.io/v1' + +class Secret(AlwaysReady): + apiVersion = 'v1' + +class Service(Check): + apiVersion = 'v1' + def ready(self, resource): + if resource.observed.spec.type != 'LoadBalancer': + return True + return len(resource.status.loadBalancer.ingress) > 0 + +class ServiceAccount(AlwaysReady): + apiVersion = 'v1' + +class StatefulSet(Check): + apiVersion = 'apps/v1' + def ready(self, resource): + replicas = resource.observed.spec.replicas or 1 + return (resource.status.readyReplicas == replicas and + resource.status.currentReplicas == replicas and + resource.status.currentRevision == resource.status.updateRevision + ) diff --git a/crossplane/pythonic/command.py b/crossplane/pythonic/command.py index 287f8bc..aa012b8 100644 --- a/crossplane/pythonic/command.py +++ b/crossplane/pythonic/command.py @@ -50,6 +50,11 @@ def add_function_arguments(cls, parser): action='store_true', help='Allow oversized protobuf messages', ) + parser.add_argument( + '--crossplane-v1', + action='store_true', + help='Enable Crossplane V1 compatibility mode', + ) def __init__(self, args): self.args = args diff --git a/crossplane/pythonic/composite.py b/crossplane/pythonic/composite.py index 7a56b16..ef26b18 100644 --- a/crossplane/pythonic/composite.py +++ b/crossplane/pythonic/composite.py @@ -9,8 +9,86 @@ _notset = object() +class ConnectionSecret: + def __get__(self, composite, objtype=None): + if composite.crossplane_v1: + return composite.spec.writeConnectionSecretToRef + secret = getattr(composite, '_connectionSecret', None) + if not secret: + secret = protobuf.Map() + for key, value in composite.request.input.writeConnectionSecretToRef: + secret[key] = value + composite._connectionSecret = secret + return secret + + def __set__(self, composite, values): + if composite.crossplane_v1: + if values != composite.spec.writeConnectionSecretToRef: + raise NotImplementedError('Connection Secret cannot be set in Crossplane V1') + return + secret = protobuf.Map() + for key, value in values: + secret[key] = value + composite._connectionSecret = secret + + +class Connection: + def __get__(self, composite, objtype=None): + connection = getattr(composite, '_connection', None) + if not connection: + connection = _Connection(composite) + composite._connection = connection + return connection + + def __set__(self, composite, values): + connection = self.__get__(composite) + coneection() + for key, value in values: + connection[key] = value + + +class TTL: + def __get__(self, composite, objtype=None): + if composite.response.meta.ttl.nanos: + return float(composite.response.meta.ttl.seconds) + (float(composite.response.meta.ttl.nanos) / 1000000000.0) + return int(composite.response.meta.ttl.seconds) + + def __set__(self, composite, ttl): + if isinstance(ttl, int): + composite.response.meta.ttl.seconds = ttl + composite.response.meta.ttl.nanos = 0 + elif isinstance(ttl, float): + composite.response.meta.ttl.seconds = int(ttl) + if ttl.is_integer(): + composite.response.meta.ttl.nanos = 0 + else: + composite.response.meta.ttl.nanos = int((ttl - int(composite.response.meta.ttl.seconds)) * 1000000000) + else: + raise ValueError('ttl must be an int or float') + + +class Ready: + def __get__(self, composite, objtype=None): + ready = composite.desired._parent.ready + if ready == fnv1.Ready.READY_TRUE: + return True + if ready == fnv1.Ready.READY_FALSE: + return False + return None + + def __set__(self, composite, ready): + if ready: + ready = fnv1.Ready.READY_TRUE + elif ready == None or (isinstance(ready, protobuf.Value) and ready._isUnknown): + ready = fnv1.Ready.READY_UNSPECIFIED + else: + ready = fnv1.Ready.READY_FALSE + composite.desired._parent.ready = ready + + class BaseComposite: - def __init__(self, request, single_use, logger): + def __init__(self, crossplane_v1, request, single_use, logger): + self.crossplane_v1 = crossplane_v1 self.request = protobuf.Message(None, 'request', request.DESCRIPTOR, request, 'Function Request') response = fnv1.RunFunctionResponse( meta=fnv1.ResponseMeta( @@ -40,7 +118,6 @@ def __init__(self, request, single_use, logger): observed = self.request.observed.composite desired = self.response.desired.composite self.observed = observed.resource - self.observed._set_attribute('connection', self.request.observed.composite.connection_details) self.desired = desired.resource self.apiVersion = self.observed.apiVersion self.kind = self.observed.kind @@ -51,54 +128,10 @@ def __init__(self, request, single_use, logger): self.results = Results(self.response) self.events = Results(self.response) # Deprecated, use self.results - @property - def ttl(self): - if self.response.meta.ttl.nanos: - return float(self.response.meta.ttl.seconds) + (float(self.response.meta.ttl.nanos) / 1000000000.0) - return int(self.response.meta.ttl.seconds) - - @ttl.setter - def ttl(self, ttl): - if isinstance(ttl, int): - self.response.meta.ttl.seconds = ttl - self.response.meta.ttl.nanos = 0 - elif isinstance(ttl, float): - self.response.meta.ttl.seconds = int(ttl) - if ttl.is_integer(): - self.response.meta.ttl.nanos = 0 - else: - self.response.meta.ttl.nanos = int((ttl - int(self.response.meta.ttl.seconds)) * 1000000000) - else: - raise ValueError('ttl must be an int or float') - - @property - def connection(self): - return self.response.desired.composite.connection_details - - @connection.setter - def connection(self, connection): - self.response.desired.composite.connection_details() - for key, value in connection: - self.response.desired.composite.connection_details[key] = value - - @property - def ready(self): - ready = self.desired._parent.ready - if ready == fnv1.Ready.READY_TRUE: - return True - if ready == fnv1.Ready.READY_FALSE: - return False - return None - - @ready.setter - def ready(self, ready): - if ready: - ready = fnv1.Ready.READY_TRUE - elif ready == None or (isinstance(ready, protobuf.Value) and ready._isUnknown): - ready = fnv1.Ready.READY_UNSPECIFIED - else: - ready = fnv1.Ready.READY_FALSE - self.desired._parent.ready = ready + ttl = TTL() + connectionSecret = ConnectionSecret() + connection = Connection() + ready = Ready() async def compose(self): raise NotImplementedError() @@ -263,6 +296,14 @@ def spec(self): def spec(self, spec): self.desired.spec = spec + @property + def type(self): + return self.desired.type + + @type.setter + def type(self, type): + self.desired.type = type + @property def data(self): return self.desired.data @@ -315,25 +356,43 @@ def __bool__(self): def __len__(self): names = set() - for name, resource in self._composite.request.extra_resources: - names.add(name) - for name, resource in self._composite.response.requirements.extra_resources: - names.add(name) + if self._composite.crossplane_v1: + for name, resource in self._composite.request.extra_resources: + names.add(name) + for name, resource in self._composite.response.requirements.extra_resources: + names.add(name) + else: + for name, resource in self._composite.request.required_resources: + names.add(name) + for name, resource in self._composite.response.requirements.resources: + names.add(name) return len(names) def __contains__(self, key): - if key in self._composite.request.extra_resources: - return True - if key in self._composite.response.desired.resources: - return True + if self._composite.crossplane_v1: + if key in self._composite.request.extra_resources: + return True + if key in self._composite.response.requirements.extra_resources: + return True + else: + if key in self._composite.request.required_resources: + return True + if key in self._composite.response.requirements.resources: + return True return False def __iter__(self): names = set() - for name, resource in self._composite.request.extra_resources: - names.add(name) - for name, resource in self._composite.response.requirements.extra_resources: - names.add(name) + if self._composite.crossplane_v1: + for name, resource in self._composite.request.extra_resources: + names.add(name) + for name, resource in self._composite.response.requirements.extra_resources: + names.add(name) + else: + for name, resource in self._composite.request.required_resources: + names.add(name) + for name, resource in self._composite.response.requirements.resources: + names.add(name) for name in sorted(names): yield name, self[name] @@ -341,8 +400,12 @@ def __iter__(self): class RequiredResources: def __init__(self, composite, name): self.name = name - self._selector = composite.response.requirements.extra_resources[name] - self._resources = composite.request.extra_resources[name] + if composite.crossplane_v1: + self._selector = composite.response.requirements.extra_resources[name] + self._resources = composite.request.extra_resources[name] + else: + self._selector = composite.response.requirements.resources[name] + self._resources = composite.request.required_resources[name] self._cache = {} def __call__(self, apiVersion=_notset, kind=_notset, namespace=_notset, name=_notset, labels=_notset): @@ -432,6 +495,7 @@ def __init__(self, name, ix, resource): self.kind = self.observed.kind self.metadata = self.observed.metadata self.spec = self.observed.spec + self.type = self.observed.type self.data = self.observed.data self.status = self.observed.status self.conditions = Conditions(resource) @@ -723,3 +787,119 @@ def claim(self, claim): self._result.target = fnv1.Target.TARGET_UNSPECIFIED else: self._result.target = fnv1.Target.TARGET_COMPOSITE + + +class _Connection: + def __init__(self, composite): + self._set_attribute('_composite', composite) + + def _set_attribute(self, key, value): + self.__dict__[key] = value + + @property + def _resource_name(self): + return self._composite.connectionSecret.resourceName or 'connection-secret' + + @property + def observed(self): + if self._composite.crossplane_v1: + return self._composite.response.observed.composite.connection_details + data = protobuf.Map() + for key, value in self._composite.resources[self._resource_name].observed.data: + data[key] = protobuf.B64Decode(value) + return data + + def __getattr__(self, key): + return self[key] + + def __getitem__(self, key): + if self._composite.crossplane_v1: + return self._composite.response.desired.composite.connection_details[key] + value = self._composite.resources[self._resource_name].data[key] + if value: + value = protobuf.B64Decode(value) + return value + + def __bool__(self): + if self._composite.crossplane_v1: + return bool(self._composite.response.desired.composite.connection_details) + return bool(self._composite.resources[self._resource_name].data) + + def __len__(self): + if self._composite.crossplane_v1: + return len(self._composite.response.desired.composite.connection_details) + return len(self._composite.resources[self._resource_name].data) + + def __contains__(self, key): + if self._composite.crossplane_v1: + return key in self._composite.response.desired.composite.connection_details + + def __iter__(self): + keys = set() + if self._composite.crossplane_v1: + for key, value in self._composite.response.desired.composite.connection_details: + yield key, value + for key, value in self._composite.resources[self._resource_name].data: + yield key, protobuf.B64Decode(value) + + def __str__(self): + return format(self) + + def __format__(self, spec='yaml'): + if self._composite.crossplane_v1: + return format(self._composite.response.desired.composite.connection_details, spec) + data = protobuf.Map() + for key, value in self._composite.resources[self._resource_name].data: + data[key] = protobuf.B64Decode(value) + return format(data, spec) + + def __call__(self, **kwargs): + if self._composite_v1: + self._composite.response.desired.composite.connection_details(**kwargs) + return + del self._composite.resources[self._resource_name] + for key, value in kwargs: + self[key] = value + + def __setattr__(self, key, value): + self[key] = value + + def __setitem__(self, key, value): + if not isinstance(value, str): + if value is None: + return + if isinstance(value, (protobuf.FieldMessage, protobuf.Value)): + if not value: + return + value = str(value) + if self._composite.crossplane_v1: + self._composite.response.desired.composite.connection_details[key] = value + return + #if not self._composite.connectionSecret.name: + # return + if self._resource_name in self._composite.resources: + secret = self._composite.resources[self._resource_name] + else: + secret = self._composite.resources[self._resource_name]('v1', 'Secret') + print(bool(self._composite.connectionSecret.name), len(self._composite.connectionSecret.name)) + if self._composite.connectionSecret.name and len(self._composite.connectionSecret.name): + secret.metadata.name = self._composite.connectionSecret.name + if not self._composite.metadata.namespace: + if not self._composite.connectionSecret.namespace: + self._composite.results.fatal('ConnectionNoNamespace', 'Cluster scoped XR must specify connection secret namespace') + return + secret.metadata.namespace = self._composite.connectionSecret.namespace + secret.type = 'connection.crossplane.io/v1alpha1' + secret.data[key] = protobuf.B64Encode(value) + + def __delattr__(self, key): + del self[key] + + def __delitem__(self, key): + if self._composite.crossplane_v1: + del self._composite.response.desired.composite.connection_details[key] + return + if self._resource_name in self._composite.resources: + del self._composite.resources[self._resource_name].data[key] + if not len(self._composite.resources[self._resource_name].data): + del self._composite.resources[self._resource_name] diff --git a/crossplane/pythonic/function.py b/crossplane/pythonic/function.py index 11547dd..845f36f 100644 --- a/crossplane/pythonic/function.py +++ b/crossplane/pythonic/function.py @@ -9,6 +9,7 @@ import grpc from crossplane.function.proto.v1 import run_function_pb2 as fnv1 from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1 +from . import auto_ready from .. import pythonic logger = logging.getLogger(__name__) @@ -17,10 +18,11 @@ class FunctionRunner(grpcv1.FunctionRunnerService): """A FunctionRunner handles gRPC RunFunctionRequests.""" - def __init__(self, debug=False, renderUnknowns=False): + def __init__(self, debug=False, renderUnknowns=False, crossplane_v1=False): """Create a new FunctionRunner.""" self.debug = debug self.renderUnknowns = renderUnknowns + self.crossplane_v1 = crossplane_v1 self.clazzes = {} def invalidate_module(self, module): @@ -100,7 +102,7 @@ async def run_function(self, request): self.clazzes[composite] = clazz try: - composite = clazz(request, single_use, logger) + composite = clazz(self.crossplane_v1, request, single_use, logger) except Exception as e: return self.fatal(request, logger, 'Instantiate', e) @@ -122,7 +124,7 @@ async def run_function(self, request): else: self.process_usages(composite) self.process_unknowns(composite) - self.process_auto_readies(composite) + auto_ready.process(composite) logger.info('Completed compose') return composite.response._message @@ -152,11 +154,11 @@ def fatal(self, request, logger, message, exception=None): def get_requireds(self, step, composite): requireds = [] for name, required in composite.requireds: - if required.apiVersion and required.kind: + if len(required.apiVersion) and len(required.kind): r = pythonic.Map(apiVersion=required.apiVersion, kind=required.kind) - if required.namespace: + if len(required.namespace): r.namespace = required.namespace - if required.matchName: + if len(required.matchName): r.matchName = required.matchName for key, value in required.matchLabels: r.matchLabels[key] = value @@ -166,6 +168,10 @@ def get_requireds(self, step, composite): return requireds def process_usages(self, composite): + if self.crossplane_v1: + apiVersion = 'apiextensions.crossplane.io/v1beta1' + else: + apiVersion = 'protection.crossplane.io/v1beta1' for _, resource in sorted(entry for entry in composite.resources): dependencies = resource.desired._getDependencies if dependencies: @@ -175,7 +181,6 @@ def process_usages(self, composite): source = self.trimFullName(source) composite.logger.debug(f"Dependency: {destination} = {source}") if resource.usages or (resource.usages is None and composite.usages): - apiVersion = 'protection.crossplane.io/v1beta1' if composite.metadata.namespace else 'apiextensions.crossplane.io/v1beta1' resources = {} requireds = {} for destination, source in sorted(dependencies.items()): @@ -191,7 +196,7 @@ def process_usages(self, composite): resources[name[3]].append(f"{'.'.join(destination.split('.')[5:])} = {'.'.join(name[5:])}") elif (len(name) > 5 and name[0] == 'request' and - name[1] == 'extra_resources' and + name[1] in ('required_resources', 'extra_resources') and name[3].startswith('items[') and name[3][-1] == ']' and name[4] == 'resource' ): @@ -206,8 +211,6 @@ def process_usages(self, composite): name.append(str(source.metadata.namespace)) name.append(str(source.observed.metadata.name)) usage = composite.resources['_'.join(name)](apiVersion, 'Usage') - if resource.metadata.namespace: - usage.metadata.namespace = resource.metadata.namespace usage.spec.reason = '\n'.join(dependencies) usage.spec.replayDeletion = True usage.spec.by.apiVersion = resource.apiVersion @@ -215,9 +218,17 @@ def process_usages(self, composite): usage.spec.by.resourceRef.name = resource.observed.metadata.name usage.spec.of.apiVersion = source.apiVersion usage.spec.of.kind = source.kind - if source.metadata.namespace: - usage.spec.of.resourceRef.namespace = source.metadata.namespace usage.spec.of.resourceRef.name = source.observed.metadata.name + if not self.crossplane_v1: + if composite.metadata.namespace: + if source.metadata.namespace and source.metadata.namespace != composite.metadata.namespace: + usage.spec.of.resourceRef.namespace = source.metadata.namespace + elif resource.metadata.namespace: + usage.metadata.namespace = resource.metadata.namespace + if source.metadata.namespace and source.metadata.namespace != resource.metadata.namespace: + usage.spec.of.resourceRef.namespace = source.metadata.namespace + else: + usage.kind = 'ClusterUsage' for key, dependencies in requireds.items(): source = composite.requireds[key[0]][key[1]] name = [resource.name, str(source.kind)] @@ -225,8 +236,6 @@ def process_usages(self, composite): name.append(str(source.metadata.namespace)) name.append(str(source.metadata.name)) usage = composite.resources['_'.join(name)](apiVersion, 'Usage') - if resource.metadata.namespace: - usage.metadata.namespace = resource.metadata.namespace usage.spec.reason = '\n'.join(dependencies) usage.spec.replayDeletion = True usage.spec.by.apiVersion = resource.apiVersion @@ -234,9 +243,17 @@ def process_usages(self, composite): usage.spec.by.resourceRef.name = resource.observed.metadata.name usage.spec.of.apiVersion = source.apiVersion usage.spec.of.kind = source.kind - if source.metadata.namespace: - usage.spec.of.resourceRef.namespace = source.metadata.namespace usage.spec.of.resourceRef.name = source.observed.metadata.name + if not self.crossplane_v1: + if composite.metadata.namespace: + if source.metadata.namespace and source.metadata.namespace != composite.metadata.namespace: + usage.spec.of.resourceRef.namespace = source.metadata.namespace + elif resource.metadata.namespace: + usage.metadata.namespace = resource.metadata.namespace + if source.metadata.namespace and source.metadata.namespace != resource.metadata.namespace: + usage.spec.of.resourceRef.namespace = source.metadata.namespace + else: + usage.kind = 'ClusterUsage' def process_unknowns(self, composite): unknownResources = [] @@ -301,13 +318,6 @@ def process_unknowns(self, composite): if result: result(reason, message) - def process_auto_readies(self, composite): - for name, resource in composite.resources: - if resource.autoReady or (resource.autoReady is None and composite.autoReady): - if resource.ready is None: - if resource.conditions.Ready.status: - resource.ready = True - def trimFullName(self, name): name = name.split('.') for values in ( diff --git a/crossplane/pythonic/grpc.py b/crossplane/pythonic/grpc.py index 33e486b..404a6fb 100644 --- a/crossplane/pythonic/grpc.py +++ b/crossplane/pythonic/grpc.py @@ -88,7 +88,7 @@ def initialize(self): async def run(self): grpc.aio.init_grpc_aio() - grpc_runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns) + grpc_runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns, self.args.crossplane_v1) grpc_server = grpc.aio.server() grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server) if self.args.insecure: diff --git a/crossplane/pythonic/protobuf.py b/crossplane/pythonic/protobuf.py index bc0b869..70f3a4a 100644 --- a/crossplane/pythonic/protobuf.py +++ b/crossplane/pythonic/protobuf.py @@ -568,39 +568,57 @@ def __init__(self, parent, key, kind, value): self._value = value def __bool__(self): - return bool(self._value) + return self._value is not _Unknown def __len__(self): + if self._value is _Unknown: + return 0 return len(self._value) def __contains__(self, key): + if self._value is _Unknown: + return False return key in self._value def __hash__(self): + if self._value is _Unknown: + return 0 return hash(self._value) def __eq__(self, other): + if self._value is _Unknown: + return False if isinstance(other, FieldMessage): return self._value == other._value return self._value == other def __bytes__(self): + if self._value is _Unknown: + return None if isinstance(self._value, str): return self._value.encode('utf-8') return bytes(self._value) def __str__(self): + if self._value is _Unknown: + return None if isinstance(self._value, bytes): return self._value.decode('utf-8') return str(self._value) def __format__(self, spec=''): + if self._value is _Unknown: + return None return format(self._value, spec) def __int__(self): + if self._value is _Unknown: + return None return int(self._value) def __float__(self): + if self._value is _Unknown: + return None return float(self._value) def _fullName(self, key=None): @@ -715,6 +733,10 @@ def __len__(self): return len(self._value.list_value.values) + len(self._unknowns) case 'ListValue': return len(self._value.values) + len(self._unknowns) + case 'string_value': + return len(self._value.string_value) + case 'bool_value': + return 1 if self._value.bool_value else 0 return 0 def __contains__(self, item): @@ -1224,13 +1246,14 @@ def _patchUnknowns(self, patches): for key, value in self: if isinstance(value, Value) and len(value): patch = patches[key] - if isinstance(patch, Value) and patch._type == value._type and len(patch): + print(patch.__class__, str(patch)) + if isinstance(patch, Value) and patch._kind == value._kind and len(patch): value._patchUnknowns(patch) elif self._isList: for ix, value in enumerate(self): if isinstance(value, Value) and len(value): patch = patches[ix] - if isinstance(patch, Value) and patch._type == value._type and len(patch): + if isinstance(patch, Value) and patch._kind == value._kind and len(patch): value._patchUnknowns(patch) def _renderUnknowns(self, trimFullName): diff --git a/crossplane/pythonic/render.py b/crossplane/pythonic/render.py index 76e02e3..5c8eb87 100644 --- a/crossplane/pythonic/render.py +++ b/crossplane/pythonic/render.py @@ -158,7 +158,7 @@ async def run(self): # Collect specified required/extra resources. Sort for stable order when processed. requireds = sorted( self.collect_resources(self.args.required_resources), - key=lambda required: str(resource.metadata.name), + key=lambda required: str(required.metadata.name), ) # Collect specified connection and credential secrets. @@ -181,7 +181,7 @@ async def run(self): results = protobuf.List() # Create a function-pythonic function runner used to run pipeline steps. - runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns) + runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns, self.args.crossplane_v1) fatal = False # Process the composition pipeline steps. diff --git a/examples/.dev/functions.yaml b/examples/.dev/functions.yaml index 805adc2..97b3592 100644 --- a/examples/.dev/functions.yaml +++ b/examples/.dev/functions.yaml @@ -10,4 +10,4 @@ metadata: render.crossplane.io/runtime: Development render.crossplane.io/runtime-development-target: localhost:9443 spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/aks-cluster/cluster-function-pythonic.yaml b/examples/aks-cluster/cluster-function-pythonic.yaml index 13b8574..f9ae6c7 100644 --- a/examples/aks-cluster/cluster-function-pythonic.yaml +++ b/examples/aks-cluster/cluster-function-pythonic.yaml @@ -3,7 +3,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 runtimeConfigRef: name: function-pythonic --- diff --git a/examples/aks-cluster/functions.yaml b/examples/aks-cluster/functions.yaml index e1aef3b..2a7e708 100644 --- a/examples/aks-cluster/functions.yaml +++ b/examples/aks-cluster/functions.yaml @@ -5,6 +5,6 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 runtimeConfigRef: name: function-pythonic diff --git a/examples/connection-details-composition/access-keys.yaml b/examples/connection-details-composition/access-keys.yaml new file mode 100644 index 0000000..05f2f4c --- /dev/null +++ b/examples/connection-details-composition/access-keys.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: iam.aws.m.upbound.io/v1beta1 +kind: AccessKey +metadata: + annotations: + crossplane.io/composition-resource-name: access-key-0 + labels: + crossplane.io/composite: my-keys + namespace: default + name: my-keys-12345 +spec: + forProvider: + user: my-user + writeConnectionSecretToRef: + name: my-keys-accesskey-secret-0 +--- +apiVersion: iam.aws.m.upbound.io/v1beta1 +kind: AccessKey +metadata: + annotations: + crossplane.io/composition-resource-name: access-key-1 + labels: + crossplane.io/composite: my-keys + namespace: default + name: my-keys-67890 +spec: + forProvider: + user: my-user + writeConnectionSecretToRef: + name: my-keys-accesskey-secret-1 diff --git a/examples/connection-details-composition/composition.yaml b/examples/connection-details-composition/composition.yaml new file mode 100644 index 0000000..5a59f8d --- /dev/null +++ b/examples/connection-details-composition/composition.yaml @@ -0,0 +1,30 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: useraccesskeys-pythonic +spec: + compositeTypeRef: + apiVersion: example.org/v1alpha1 + kind: UserAccessKey + mode: Pipeline + pipeline: + - step: render-pythonic + functionRef: + name: function-pythonic + input: + apiVersion: pythonic.fn.crossplane.io/v1alpha1 + kind: Composite + composite: | + class Composite(BaseComposite): + def compose(self): + self.connectionSecret = self.spec.writeConnectionSecretToRef + + user = self.resources.user('iam.aws.m.upbound.io/v1beta1', 'User') + user.spec.forProvider = {} + + for ix in range(2): + key = self.resources[f"access-key-{ix}"]('iam.aws.m.upbound.io/v1beta1', 'AccessKey') + key.spec.forProvider.user = user.status.atProvider.id + key.spec.writeConnectionSecretToRef.name = f"{self.metadata.name}-accesskey-{ix}" + self.connection[f"user-{ix}"] = key.connection.username + self.connection[f"password-{ix}"] = key.connection.password diff --git a/examples/connection-details-composition/definition.yaml b/examples/connection-details-composition/definition.yaml new file mode 100644 index 0000000..a9ae5e2 --- /dev/null +++ b/examples/connection-details-composition/definition.yaml @@ -0,0 +1,26 @@ +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: useraccesskeys.example.org +spec: + group: example.org + names: + kind: UserAccessKey + plural: useraccesskeys + scope: Namespaced + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + writeConnectionSecretToRef: + type: object + properties: + name: + type: string diff --git a/examples/connection-details-composition/functions.yaml b/examples/connection-details-composition/functions.yaml new file mode 100644 index 0000000..175d875 --- /dev/null +++ b/examples/connection-details-composition/functions.yaml @@ -0,0 +1,8 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-pythonic + annotations: + render.crossplane.io/runtime: Development +spec: + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/connection-details-composition/render.sh b/examples/connection-details-composition/render.sh new file mode 100755 index 0000000..f1dec18 --- /dev/null +++ b/examples/connection-details-composition/render.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +cd $(dirname $(realpath $0)) +#exec crossplane render xr.yaml composition.yaml functions.yaml + +#function-pythonic render xr.yaml composition.yaml +#function-pythonic render --observed-resource user.yaml xr.yaml composition.yaml +function-pythonic render --observed-resource user.yaml --observed-resource access-keys.yaml --secret-store secrets.yaml xr.yaml composition.yaml +#function-pythonic render --crossplane-v1 --observed-resource user.yaml --observed-resource access-keys.yaml --secret-store secrets.yaml --include-connection-xr xr.yaml composition.yaml diff --git a/examples/connection-details-composition/secrets.yaml b/examples/connection-details-composition/secrets.yaml new file mode 100644 index 0000000..746f64a --- /dev/null +++ b/examples/connection-details-composition/secrets.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + namespace: default + name: my-keys-accesskey-secret-0 +data: + username: YWNjZXNza2V5LTAtdXNlcm5hbWU= + password: YWNjZXNza2V5LTAtcGFzc3dvcmQ= +--- +apiVersion: v1 +kind: Secret +metadata: + namespace: default + name: my-keys-accesskey-secret-1 +data: + username: YWNjZXNza2V5LTEtdXNlcm5hbWU= + password: YWNjZXNza2V5LTEtcGFzc3dvcmQ= diff --git a/examples/connection-details-composition/user.yaml b/examples/connection-details-composition/user.yaml new file mode 100644 index 0000000..da21435 --- /dev/null +++ b/examples/connection-details-composition/user.yaml @@ -0,0 +1,18 @@ +apiVersion: iam.aws.m.upbound.io/v1beta1 +kind: User +metadata: + annotations: + crossplane.io/composition-resource-name: user + labels: + crossplane.io/composite: my-keys + namespace: default + name: my-keys-12345 +spec: + forProvider: {} +status: + atProvider: + id: my-user + conditions: + - type: Ready + reason: Created + status: 'True' diff --git a/examples/connection-details-composition/xr.yaml b/examples/connection-details-composition/xr.yaml new file mode 100644 index 0000000..13191fc --- /dev/null +++ b/examples/connection-details-composition/xr.yaml @@ -0,0 +1,8 @@ +apiVersion: example.org/v1alpha1 +kind: UserAccessKey +metadata: + namespace: default + name: my-keys +spec: + writeConnectionSecretToRef: + name: my-keys-connection-details diff --git a/examples/eks-cluster/functions.yaml b/examples/eks-cluster/functions.yaml index 2f532ec..647d864 100644 --- a/examples/eks-cluster/functions.yaml +++ b/examples/eks-cluster/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/filing-system/function.yaml b/examples/filing-system/function.yaml index 2e4f1c6..e24d23a 100644 --- a/examples/filing-system/function.yaml +++ b/examples/filing-system/function.yaml @@ -3,7 +3,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 runtimeConfigRef: apiVersion: pkg.crossplane.io/v1beta1 kind: DeploymentRuntimeConfig diff --git a/examples/function-go-templating/conditions/functions.yaml b/examples/function-go-templating/conditions/functions.yaml index 7b312d9..ff3c45e 100644 --- a/examples/function-go-templating/conditions/functions.yaml +++ b/examples/function-go-templating/conditions/functions.yaml @@ -7,4 +7,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/context/functions.yaml b/examples/function-go-templating/context/functions.yaml index 7b312d9..ff3c45e 100644 --- a/examples/function-go-templating/context/functions.yaml +++ b/examples/function-go-templating/context/functions.yaml @@ -7,4 +7,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/context/render.sh b/examples/function-go-templating/context/render.sh index 8316b63..382ae5d 100755 --- a/examples/function-go-templating/context/render.sh +++ b/examples/function-go-templating/context/render.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) #exec crossplane render --extra-resources environmentConfigs.yaml --include-context xr.yaml composition.yaml functions.yaml -exec function-pythonic render --extra-resources=environmentConfigs.yaml --include-context xr.yaml composition.yaml +exec function-pythonic render --required-resources=environmentConfigs.yaml --include-context xr.yaml composition.yaml diff --git a/examples/function-go-templating/functions/fromYaml/functions.yaml b/examples/function-go-templating/functions/fromYaml/functions.yaml index bb9f4c2..493aa94 100644 --- a/examples/function-go-templating/functions/fromYaml/functions.yaml +++ b/examples/function-go-templating/functions/fromYaml/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/functions/getComposedResource/functions.yaml b/examples/function-go-templating/functions/getComposedResource/functions.yaml index bb9f4c2..493aa94 100644 --- a/examples/function-go-templating/functions/getComposedResource/functions.yaml +++ b/examples/function-go-templating/functions/getComposedResource/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/functions/getCompositeResource/functions.yaml b/examples/function-go-templating/functions/getCompositeResource/functions.yaml index bb9f4c2..493aa94 100644 --- a/examples/function-go-templating/functions/getCompositeResource/functions.yaml +++ b/examples/function-go-templating/functions/getCompositeResource/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/functions/getCredentialData/functions.yaml b/examples/function-go-templating/functions/getCredentialData/functions.yaml index bb9f4c2..493aa94 100644 --- a/examples/function-go-templating/functions/getCredentialData/functions.yaml +++ b/examples/function-go-templating/functions/getCredentialData/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/functions/getResourceCondition/functions.yaml b/examples/function-go-templating/functions/getResourceCondition/functions.yaml index bb9f4c2..493aa94 100644 --- a/examples/function-go-templating/functions/getResourceCondition/functions.yaml +++ b/examples/function-go-templating/functions/getResourceCondition/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/functions/include/functions.yaml b/examples/function-go-templating/functions/include/functions.yaml index bb9f4c2..493aa94 100644 --- a/examples/function-go-templating/functions/include/functions.yaml +++ b/examples/function-go-templating/functions/include/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/functions/toYaml/functions.yaml b/examples/function-go-templating/functions/toYaml/functions.yaml index bb9f4c2..493aa94 100644 --- a/examples/function-go-templating/functions/toYaml/functions.yaml +++ b/examples/function-go-templating/functions/toYaml/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/inline/functions.yaml b/examples/function-go-templating/inline/functions.yaml index bb9f4c2..493aa94 100644 --- a/examples/function-go-templating/inline/functions.yaml +++ b/examples/function-go-templating/inline/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/recursive/functions.yaml b/examples/function-go-templating/recursive/functions.yaml index bb9f4c2..493aa94 100644 --- a/examples/function-go-templating/recursive/functions.yaml +++ b/examples/function-go-templating/recursive/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/extra-resources/composition.yaml b/examples/function-go-templating/required-resources/composition.yaml similarity index 100% rename from examples/function-go-templating/extra-resources/composition.yaml rename to examples/function-go-templating/required-resources/composition.yaml diff --git a/examples/function-go-templating/extra-resources/functions.yaml b/examples/function-go-templating/required-resources/functions.yaml similarity index 97% rename from examples/function-go-templating/extra-resources/functions.yaml rename to examples/function-go-templating/required-resources/functions.yaml index 44113c0..493aa94 100644 --- a/examples/function-go-templating/extra-resources/functions.yaml +++ b/examples/function-go-templating/required-resources/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2,0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/function-go-templating/extra-resources/render.sh b/examples/function-go-templating/required-resources/render.sh similarity index 60% rename from examples/function-go-templating/extra-resources/render.sh rename to examples/function-go-templating/required-resources/render.sh index 08e8e1a..131ca01 100755 --- a/examples/function-go-templating/extra-resources/render.sh +++ b/examples/function-go-templating/required-resources/render.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) #exec crossplane render --extra-resources extraResources.yaml xr.yaml composition.yaml functions.yaml -exec function-pythonic render --extra-resources extraResources.yaml xr.yaml composition.yaml +exec function-pythonic render --required-resources required-resources.yaml xr.yaml composition.yaml diff --git a/examples/function-go-templating/extra-resources/extraResources.yaml b/examples/function-go-templating/required-resources/required-resources.yaml similarity index 100% rename from examples/function-go-templating/extra-resources/extraResources.yaml rename to examples/function-go-templating/required-resources/required-resources.yaml diff --git a/examples/function-go-templating/extra-resources/xr.yaml b/examples/function-go-templating/required-resources/xr.yaml similarity index 100% rename from examples/function-go-templating/extra-resources/xr.yaml rename to examples/function-go-templating/required-resources/xr.yaml diff --git a/examples/function-sequencer/functions.yaml b/examples/function-sequencer/functions.yaml index 5aba4f5..175d875 100644 --- a/examples/function-sequencer/functions.yaml +++ b/examples/function-sequencer/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/get-started-app/composition.yaml b/examples/get-started-app/composition.yaml index cf0bd0e..9853a10 100644 --- a/examples/get-started-app/composition.yaml +++ b/examples/get-started-app/composition.yaml @@ -27,7 +27,6 @@ spec: d.spec.template.spec.containers[0].name = 'app' d.spec.template.spec.containers[0].image = self.spec.image d.spec.template.spec.containers[0].ports[0].containerPort = 80 - d.ready = d.conditions.Available.status s = self.resources.service('v1', 'Service') s.metadata.labels = labels @@ -35,7 +34,6 @@ spec: s.spec.ports[0].protocol = 'TCP' s.spec.ports[0].port = 8080 s.spec.ports[0].targetPort = 80 - s.ready = s.observed.spec.clusterIP self.status.replicas = d.status.availableReplicas self.status.address = s.observed.spec.clusterIP diff --git a/examples/get-started-app/definition.yaml b/examples/get-started-app/definition.yaml index 3fff7ce..e2d3856 100644 --- a/examples/get-started-app/definition.yaml +++ b/examples/get-started-app/definition.yaml @@ -1,4 +1,4 @@ -apiVersion: apiextensions.crossplane.io/v1 +apiVersion: apiextensions.crossplane.io/v2 kind: CompositeResourceDefinition metadata: name: apps.example.crossplane.io diff --git a/examples/get-started-app/functions.yaml b/examples/get-started-app/functions.yaml index 5aba4f5..175d875 100644 --- a/examples/get-started-app/functions.yaml +++ b/examples/get-started-app/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/get-started-app/render.sh b/examples/get-started-app/render.sh index 17aec4e..f5215b3 100755 --- a/examples/get-started-app/render.sh +++ b/examples/get-started-app/render.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) #exec crossplane render xr.yaml composition.yaml functions.yaml -exec function-pythonic render xr.yaml composition.yaml +exec function-pythonic render --observed-resources observed.yaml xr.yaml composition.yaml diff --git a/examples/helm-copy-secret/functions.yaml b/examples/helm-copy-secret/functions.yaml index 5aba4f5..175d875 100644 --- a/examples/helm-copy-secret/functions.yaml +++ b/examples/helm-copy-secret/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/import-existing-vpc/functions.yaml b/examples/import-existing-vpc/functions.yaml index 2f532ec..647d864 100644 --- a/examples/import-existing-vpc/functions.yaml +++ b/examples/import-existing-vpc/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/single-purpose/functions.yaml b/examples/single-purpose/functions.yaml index 5aba4f5..175d875 100644 --- a/examples/single-purpose/functions.yaml +++ b/examples/single-purpose/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/usages-extra/functions.yaml b/examples/usages-extra/functions.yaml index bb9f4c2..493aa94 100644 --- a/examples/usages-extra/functions.yaml +++ b/examples/usages-extra/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0 diff --git a/examples/usages-extra/render.sh b/examples/usages-extra/render.sh index 4e97d15..00e1388 100755 --- a/examples/usages-extra/render.sh +++ b/examples/usages-extra/render.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) #exec crossplane render --extra-resources extraResources.yaml --observed-resources=observedResources.yaml xr.yaml composition.yaml functions.yaml -exec function-pythonic render --extra-resources=extraResources.yaml --observed-resources=observedResources.yaml xr.yaml composition.yaml +exec function-pythonic render --required-resources=extraResources.yaml --observed-resources=observedResources.yaml xr.yaml composition.yaml diff --git a/package/input-definition.yaml b/package/input-definition.yaml index 647d7b5..5434733 100644 --- a/package/input-definition.yaml +++ b/package/input-definition.yaml @@ -29,6 +29,21 @@ spec: step: type: string description: Optional step name used in logging + writeConnectionSecretToRef: + type: object + properties: + resourceName: + description: 'Composed resource name to use, default: pythonic-connection-secret' + type: string + name: + description: Name of the connection secret. If not specified, defaults to {xr-name}-connection. + type: string + namespace: + description: |- + Namespace of the connection secret. If not specified for namespaced XRs, + Crossplane will default it to the XR's namespace. For cluster-scoped XRs, + namespace must be explicitly provided. + type: string parameters: type: object x-kubernetes-preserve-unknown-fields: true @@ -70,6 +85,21 @@ spec: step: type: string description: Optional step name used in logging + writeConnectionSecretToRef: + type: object + properties: + resourceName: + description: 'Composed resource name to use, default: pythonic-connection-secret' + type: string + name: + description: Name of the connection secret. If not specified, defaults to {xr-name}-connection. + type: string + namespace: + description: |- + Namespace of the connection secret. If not specified for namespaced XRs, + Crossplane will default it to the XR's namespace. For cluster-scoped XRs, + namespace must be explicitly provided. + type: string parameters: type: object x-kubernetes-preserve-unknown-fields: true diff --git a/scripts/setup-local.sh b/scripts/setup-local.sh index 41896c2..1f0e6d4 100755 --- a/scripts/setup-local.sh +++ b/scripts/setup-local.sh @@ -1,9 +1,11 @@ #!/usr/bin/env bash +cd $(dirname $0) + helm upgrade --install crossplane \ --namespace crossplane-system \ --create-namespace crossplane-stable/crossplane \ - --version v2.0.2 \ + --version v2.1.3 \ --set 'args={--enable-realtime-compositions=false,--debug}' kubectl apply -f - <&1) + then + token=$(echo "$response" | jq -r .accessToken) + break + fi + if ! [[ "$response" =~ [(]AuthorizationPendingException[)] ]] + then + echo + echo "$response" + exit 1 + fi + echo -n '.' + sleep 1 + done + echo + credentials=$(aws --region=us-east-2 sso get-role-credentials --account-id=$awsAccount --role-name=$ssoRole --access-token=$token | jq .roleCredentials) + echo -n "$credentials" >.aws-credentials +fi + +kubectl apply -f - <