diff --git a/README.md b/README.md index c934492..33049ae 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ spec: composite: | class VpcComposite(BaseComposite): def compose(self): - vpc = self.resources.vpc('ec2.aws.crossplane.io/v1beta1', 'VPC') + vpc = self.resources.vpc('VPC', 'ec2.aws.crossplane.io/v1beta1') vpc.spec.forProvider.region = self.spec.region vpc.spec.forProvider.cidrBlock = self.spec.cidr self.status.vpcId = vpc.status.atProvider.vpcId @@ -57,7 +57,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.0 ``` ### Crossplane V1 @@ -69,7 +69,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.0 runtimeConfigRef: name: function-pythonic -- @@ -104,12 +104,12 @@ condition, the composition will be terminated or the observed value for that fie be used, depending on the `unknownsFatal` settings. Take the following example: -```yaml -vpc = self.resources.VPC('ec2.aws.crossplane.io/v1beta1', 'VPC') +```python +vpc = self.resources.VPC('VPC', 'ec2.aws.crossplane.io/v1beta1') vpc.spec.forProvider.region = 'us-east-1 vpc.spec.forProvider.cidrBlock = '10.0.0.0/16' -subnet = self.resources.SubnetA('ec2.aws.crossplane.io/v1beta1', 'Subnet') +subnet = self.resources.SubnetA('Subnet', 'ec2.aws.crossplane.io/v1beta1') subnet.spec.forProvider.region = 'us-east-1' subnet.spec.forProvider.vpcId = vpc.status.atProvider.vpcId subnet.spec.forProvider.availabilityZone = 'us-east-1a' @@ -126,19 +126,46 @@ overridden for all composed resource by setting the Composite `self.unknownsFata to False, or at the individual composed resource level by setting the `Resource.unknownsFatal` field to False. +## Explicit Dependencies + +At times, the above implicit dependency handling does not account for all cases. +Explicit dependencies can be configured using the resource `addDependency` method. +The dependency's "ready" is used to determine when that dependency is available +for use. The dependency's ready state can either be explictly set, or will be +defaulted to it's auto-ready calculation. + +Here is an example of specifying an explicit dependency: +```yaml +crd = self.resources.KarpenterCrdRelease('Release', 'helm.crossplane.io/v1beta1') +crd.spec.deletionPolicy = 'Orphan' +crd.spec.forProvider.chart.repository = 'oci://public.ecr.aws/karpenter' +crd.spec.forProvider.chart.name = 'karpenter-crd' +crd.spec.forProvider.chart.version = '1.8.6' +crd.spec.forProvider.namespace = 'karpenter' +crd.externalName = 'karpenter-crd' +karpenter = self.resources.KarpenterRelease('Release', 'helm.crossplane.io/v1beta1') +karpenter.addDependency(crd) +karpenter.spec.deletionPolicy = 'Orphan' +karpenter.spec.forProvider.chart.repository = 'oci://public.ecr.aws/karpenter' +karpenter.spec.forProvider.chart.name = 'karpenter' +karpenter.spec.forProvider.chart.version = '1.8.6' +karpenter.spec.forProvider.namespace = 'karpenter' +karpenter.externalName = 'karpenter' +``` + ## Usage Dependencies function-pythonic can be configured to automatically create [Crossplane Usages](https://docs.crossplane.io/latest/managed-resources/usages/) dependencies between resources. Modifying the above VPC example with: -```yaml +```python self.usages = True -vpc = self.resources.VPC('ec2.aws.crossplane.io/v1beta1', 'VPC') +vpc = self.resources.VPC('VPC', 'ec2.aws.crossplane.io/v1beta1') vpc.spec.forProvider.region = 'us-east-1 vpc.spec.forProvider.cidrBlock = '10.0.0.0/16' -subnet = self.resources.SubnetA('ec2.aws.crossplane.io/v1beta1', 'Subnet') +subnet = self.resources.SubnetA('Subnet', 'ec2.aws.crossplane.io/v1beta1') subnet.spec.forProvider.region = 'us-east-1' subnet.spec.forProvider.vpcId = vpc.status.atProvider.vpcId subnet.spec.forProvider.availabilityZone = 'us-east-1a' @@ -179,7 +206,7 @@ Calling a message or map will clear it and will set any provided key word arguments. For example, this will either create or clear the resource and then set its apiVersion and kind: ```python -response.desired.resources.vpc.resource(apiVersion='ec2.aws.crossplane.io/v1beta1', kind='VPC') +response.desired.resources.vpc.resource(kind='VPC', apiVersion='ec2.aws.crossplane.io/v1beta1') ``` The following functions are provided to create Protobuf structures: | Function | Description | @@ -236,8 +263,8 @@ The BaseComposite class provides the following fields for manipulating the Compo | 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.connection | Map | The composite desired connection details | +| self.connection.observed | Map | The composite observed connection details | | self.ready | Boolean | The composite desired ready state | The BaseComposite also provides access to the following Crossplane Function level features: @@ -254,9 +281,9 @@ The BaseComposite also provides access to the following Crossplane Function leve | self.environment | Map | The response environment, initialized from the request context environment | | self.requireds | Requireds | Request and read additional local Kubernetes resources | | self.resources | Resources | Define and process composed resources | -| self.unknownsFatal | Boolean | Terminate the composition if already created resources are assigned unknown values, default True | | self.usages| Boolean | Generate Crossplane Usages for resource dependencies, default False | | self.autoReady | Boolean | Perform auto ready processing on all composed resources, default True | +| self.unknownsFatal | Boolean | Terminate the composition if already created resources are assigned unknown values, default False | ### Composed Resources @@ -281,9 +308,11 @@ Resource class: | Resource.conditions | Conditions | The resource conditions | | Resource.connection | Map | The resource observed connection details | | Resource.ready | Boolean | The resource ready state | -| Resource.unknownsFatal | Boolean | Terminate the composition if this resource has been created and is assigned unknown values, default is Composite.unknownsFatal | +| Resource.addDependency | Method | Add another composed resource as a dependency | +| Resource.setReadyCondition | Method | Set Resource.ready to the Ready Condition status | | 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 | +| Resource.unknownsFatal | Boolean | Terminate the composition if this resource has been created and is assigned unknown values, default is Composite.unknownsFatal | ### Required Resources @@ -382,28 +411,34 @@ $ pip install crossplane-function-pythonic Then to render function-pythonic Compositions, use the `function-pythonic render ...` command. ```shell -$ function-pythonic render -h -usage: Crossplane Function Pythonic render [-h] [--debug] [--log-name-width WIDTH] [--python-path DIRECTORY] [--render-unknowns] - [--allow-oversize-protos] [--context-files KEY=PATH] [--context-values KEY=VALUE] - [--observed-resources PATH] [--required-resources PATH] [--secret-store PATH] [--include-full-xr] - [--include-connection-xr] [--include-function-results] [--include-context] - PATH [PATH/CLASS] +$ function-pythonic render --help +usage: Crossplane Function Pythonic render [-h] [--debug] [--log-name-width WIDTH] [--logger-level LOGGER=LEVEL] [--python-path DIRECTORY] + [--render-unknowns] [--allow-oversize-protos] [--crossplane-v1] [--kube-context CONTEXT] + [--context-files KEY=PATH] [--context-values KEY=VALUE] [--observed-resources PATH] + [--required-resources PATH] [--secret-store PATH] [--include-full-xr] [--include-connection-xr] + [--include-function-results] [--include-context] + COMPOSITE [COMPOSITION] positional arguments: - PATH A YAML file containing the Composite resource to render. - PATH/CLASS A YAML file containing the Composition resource or the complete path of a function=-pythonic BaseComposite subclass. + COMPOSITE A YAML file containing the Composite resource to render, or kind:apiVersion:namespace:name of cluster Composite. + COMPOSITION A YAML file containing the Composition resource, or the complete path of a function-pythonic BaseComposite subclass. options: -h, --help show this help message and exit --debug, -d Emit debug logs. --log-name-width WIDTH Width of the logger name in the log output, default 40. + --logger-level LOGGER=LEVEL + Logger level, for example: botocore.hooks=INFO --python-path DIRECTORY Filing system directories to add to the python path. --render-unknowns, -u Render resources with unknowns, useful during local development. --allow-oversize-protos Allow oversized protobuf messages + --crossplane-v1 Enable Crossplane V1 compatibility mode + --kube-context, -k CONTEXT + The kubectl context to use to obtain external resources from, such as required resources, connections, etc. --context-files KEY=PATH Context key-value pairs to pass to the Function pipeline. Values must be files containing YAML/JSON. --context-values KEY=VALUE @@ -419,7 +454,7 @@ options: --include-connection-xr Include the Composite connection values in the rendered output as a resource of kind: Connection. --include-function-results, -r - Include informational and warning messages from Functions in the rendered output as resources of kind: Result.. + Include informational and warning messages from Functions in the rendered output as resources of kind: Result. --include-context, -c Include the context in the rendered output as a resource of kind: Context. ``` @@ -555,7 +590,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.0 runtimeConfigRef: name: function-pythonic --- @@ -639,7 +674,7 @@ data: from crossplane.pythonic import BaseComposite class GreetingComposite(BaseComposite): def compose(self): - cm = self.resources.ConfigMap('v1', 'ConfigMap') + cm = self.resources.ConfigMap('ConfigMap', 'v1') cm.data.greeting = f"Hello, {self.parameters.who}!" ``` ```yaml diff --git a/crossplane/pythonic/auto_ready.py b/crossplane/pythonic/auto_ready.py index 5efda6e..7e519d5 100644 --- a/crossplane/pythonic/auto_ready.py +++ b/crossplane/pythonic/auto_ready.py @@ -1,20 +1,23 @@ +def resource_ready(resource): + if not resource.observed: + return None + return _checks.get((resource.observed.apiVersion, resource.observed.kind), _check_default).ready(resource) -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: +class ReadyCondition: def ready(self, resource): - return bool(resource.conditions.Ready.status) + ready = resource.conditions.Ready + if not ready._find_condition(): + return None + if ready.status: + return resource.observed.metadata.name + if ready.reason: + return resource.status.notReadyCondition[ready.reason] + return resource.status.notReadyCondition _checks = {} -_check_default = ConditionReady() +_check_default = ReadyCondition() class Check: @classmethod @@ -28,7 +31,7 @@ def ready(self, resource): class AlwaysReady(Check): def ready(self, resource): - return True + return resource.observed.metadata.name class ClusterRole(AlwaysReady): @@ -44,57 +47,82 @@ class CronJob(Check): apiVersion = 'batch/v1' def ready(self, resource): if resource.observed.spec.suspend and len(resource.observed.spec.suspend): - return True + return resource.observed.metadata.name if not resource.status.lastScheduleTime: - return False + return resource.status.lastScheduleTime if resource.status.active: - return True + return resource.observed.metadata.name if not resource.status.lastSuccessfulTime: - return False - return str(resource.status.lastSuccessfulTime) >= str(resource.status.lastScheduleTime) + return resource.status.lastSuccessfulTime + if str(resource.status.lastSuccessfulTime) < str(resource.status.lastScheduleTime): + return resource.status.successfulBeforeSchedule + return resource.observed.metadata.name 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 - ) + if not scheduled: + return scheduled + for field in ('numberReady', 'updatedNumberScheduled', 'numberAvailable'): + value = resource.status[field] + if not value: + return value + if scheduled != value: + return resource.status[f"{field}NotScheduled"] + return resource.observed.metadata.name 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) + for field in ('updatedReplicas', 'availableReplicas'): + value = resource.status[field] + if not value: + return value + if replicas != value: + return resource.status[F"{field}NotReplicas"] + available = resource.conditions.Available + if not available: + return resource.status.notAvailable + if not available.status: + if available.reason: + return resource.status.notAvailable[available.reason] + return resource.status.notAvailable + return resource.observed.metadata.name class HorizontalPodAutoscaler(Check): apiVersion = 'autoscaling/v2' def ready(self, resource): for type in ('FailedGetScale', 'FailedUpdateScale', 'FailedGetResourceMetric', 'InvalidSelector'): if resource.conditions[type].status: - return False + return resource.status[f"is{type}"] for type in ('ScalingActive', 'ScalingLimited'): if resource.conditions[type].status: - return True - return False + return resource.observed.metadata.name + return resource.status.notScalingActiveOrLimiited class Ingress(Check): apiVersion = 'networking.k8s.io/v1' def ready(self, resource): - return len(resource.status.loadBalancer.ingress) > 0 + if not len(resource.status.loadBalancer.ingress): + return resource.status.noLoadBalanceIngresses + return resource.observed.metadata.name 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) + return resource.status[f"is{type}"] + complete = resource.conditions.Complete + if not complete: + return resource.status.notComplete + if not complete.status: + if complete.reason: + return resource.status.notComplete[complete.reason] + return resource.status.notComplete + return resource.observed.metadata.name class Namespace(AlwaysReady): apiVersion = 'v1' @@ -102,27 +130,33 @@ class Namespace(AlwaysReady): class PersistentVolumeClaim(Check): apiVersion = 'v1' def ready(self, resource): - return resource.status.phase == 'Bound' + if resource.status.phase != 'Bound': + return resource.status.phaseNotBound + return resource.observed.metadata.name class Pod(Check): apiVersion = 'v1' def ready(self, resource): if resource.status.phase == 'Succeeded': - return True + return resource.observed.metadata.name if resource.status.phase == 'Running': if resource.observed.spec.restartPolicy == 'Always': if resource.conditions.Ready.status: - return True - return False + return resource.observed.metadata.name + return resource.status.notSucceededOrRunning class ReplicaSet(Check): apiVersion = 'v1' def ready(self, resource): if int(resource.status.observedGeneration) < int(resource.observed.metadata.generation): - return False + return resource.status.priorObservedGeneration if resource.conditions.ReplicaFailure.status: - return False - return int(resource.status.availableReplicas) >= int(resource.observed.spec.replicas or 1) + if resource.conditions.ReplicaFailure.reason: + return resource.status.isReplicaFailure[resource.conditions.ReplicaFailure.reason] + return resource.status.isReplicaFailure + if int(resource.status.availableReplicas) < int(resource.observed.spec.replicas or 1): + return resource.status.tooFewavailableReplicas + return resource.observed.metadata.name class Role(AlwaysReady): apiVersion = 'rbac.authorization.k8s.io/v1' @@ -137,8 +171,10 @@ class Service(Check): apiVersion = 'v1' def ready(self, resource): if resource.observed.spec.type != 'LoadBalancer': - return True - return len(resource.status.loadBalancer.ingress) > 0 + return resource.observed.metadata.name + if not len(resource.status.loadBalancer.ingress): + return resource.status.noLoadBalancerIngresses + return resource.observed.metadata.name class ServiceAccount(AlwaysReady): apiVersion = 'v1' @@ -147,7 +183,12 @@ 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 - ) + for field in ('readyReplicas', 'currentReplicas'): + value = resource.status[field] + if not value: + return value + if replicas != value: + return resource.status[F"{field}NotReplicas"] + if resource.status.currentRevision != resource.status.updateRevision: + return resource.status.currentRevisionNotUpdateReivsion + return resource.observed.metadata.name diff --git a/crossplane/pythonic/command.py b/crossplane/pythonic/command.py index aa012b8..d2ae861 100644 --- a/crossplane/pythonic/command.py +++ b/crossplane/pythonic/command.py @@ -33,6 +33,13 @@ def add_function_arguments(cls, parser): metavar='WIDTH', help='Width of the logger name in the log output, default 40.', ) + parser.add_argument( + '--logger-level', + action='append', + default=[], + metavar='LOGGER=LEVEL', + help='Logger level, for example: botocore.hooks=INFO', + ) parser.add_argument( '--python-path', action='append', @@ -70,6 +77,11 @@ def initialize_function(self): logger = logging.getLogger() logger.handlers = [handler] logger.setLevel(logging.DEBUG if self.args.debug else logging.INFO) + for logger_level in self.args.logger_level: + for logger_level in logger_level.split(','): + logger_level = logger_level.split('=') + if len(logger_level) == 2: + logging.getLogger(logger_level[0]).setLevel(logger_level[1].upper()) for path in reversed(self.args.python_path): sys.path.insert(0, str(pathlib.Path(path).expanduser().resolve())) diff --git a/crossplane/pythonic/composite.py b/crossplane/pythonic/composite.py index ef26b18..b9cd7cd 100644 --- a/crossplane/pythonic/composite.py +++ b/crossplane/pythonic/composite.py @@ -3,6 +3,7 @@ from google.protobuf.duration_pb2 import Duration from crossplane.function.proto.v1 import run_function_pb2 as fnv1 +from . import auto_ready from . import protobuf @@ -69,21 +70,22 @@ def __set__(self, composite, ttl): class Ready: def __get__(self, composite, objtype=None): - ready = composite.desired._parent.ready - if ready == fnv1.Ready.READY_TRUE: + if hasattr(composite, '_ready'): + return composite._ready + if composite.desired._parent.ready == fnv1.Ready.READY_TRUE: return True - if ready == fnv1.Ready.READY_FALSE: + if composite.desired._parent.ready == fnv1.Ready.READY_FALSE: return False return None def __set__(self, composite, ready): + composite._ready = ready if ready: - ready = fnv1.Ready.READY_TRUE - elif ready == None or (isinstance(ready, protobuf.Value) and ready._isUnknown): - ready = fnv1.Ready.READY_UNSPECIFIED + composite.desired._parent.ready = fnv1.Ready.READY_TRUE + elif ready is None: + composite.desired._parent.ready = fnv1.Ready.READY_UNSPECIFIED else: - ready = fnv1.Ready.READY_FALSE - composite.desired._parent.ready = ready + composite.desired._parent.ready = fnv1.Ready.READY_FALSE class BaseComposite: @@ -111,9 +113,9 @@ def __init__(self, crossplane_v1, request, single_use, logger): self.environment = self.context['apiextensions.crossplane.io/environment'] self.requireds = Requireds(self) self.resources = Resources(self) - self.unknownsFatal = True self.autoReady = True self.usages = False + self.unknownsFatal = False observed = self.request.observed.composite desired = self.response.desired.composite @@ -231,6 +233,7 @@ def __delitem__(self, key): class Resource: def __init__(self, composite, name): + self._composite = composite self.name = name observed = composite.request.observed.resources[name] desired = composite.response.desired.resources[name] @@ -238,16 +241,22 @@ def __init__(self, composite, name): self.desired = desired.resource self.conditions = Conditions(observed) self.connection = observed.connection_details - self.unknownsFatal = None self.autoReady = None self.usages = None + self.unknownsFatal = None - def __call__(self, apiVersion=_notset, kind=_notset, namespace=_notset, name=_notset): + def __call__(self, kind=_notset, apiVersion=_notset, namespace=_notset, name=_notset): self.desired() + if kind != _notset: + # Allow for apiVersion in the first arg and kind in the second arg + if '/' in kind or kind == 'v1': + if apiVersion != _notset: + self.kind = apiVersion + apiVersion = kind + else: + self.kind = kind if apiVersion != _notset: self.apiVersion = apiVersion - if kind != _notset: - self.kind = kind if namespace != _notset: self.metadata.namespace = namespace if name != _notset: @@ -272,9 +281,12 @@ def kind(self, kind): @property def externalName(self): - if 'crossplane.io/external-name' in self.metadata.annotations: - return self.metadata.annotations['crossplane.io/external-name'] - return self.observed.metadata.annotations['crossplane.io/external-name'] + name = self.metadata.annotations['crossplane.io/external-name'] + if not name: + name = self.observed.metadata.annotations['crossplane.io/external-name'] + if name: + self.externalName = name + return name @externalName.setter def externalName(self, name): @@ -296,6 +308,7 @@ def spec(self): def spec(self, spec): self.desired.spec = spec + # Used by Secret:v1 @property def type(self): return self.desired.type @@ -318,23 +331,60 @@ def status(self): @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 + if not hasattr(self, '_ready'): + if self.desired._parent.ready == fnv1.Ready.READY_TRUE: + self._ready = True + elif self.desired._parent.ready == fnv1.Ready.READY_FALSE: + self._ready = False + else: + self._ready = None + if self.autoReady or (self.autoReady is None and self._composite.autoReady): + ready = auto_ready.resource_ready(self) + if ready: + self._ready = ready + self.desired._parent.ready = fnv1.Ready.READY_TRUE + return self._ready @ready.setter def ready(self, ready): + self._ready = ready if ready: - ready = fnv1.Ready.READY_TRUE - elif ready == None or (isinstance(ready, protobuf.Value) and ready._isUnknown): - ready = fnv1.Ready.READY_UNSPECIFIED + self.desired._parent.ready = fnv1.Ready.READY_TRUE + elif ready is None: + self.desired._parent.ready = fnv1.Ready.READY_UNSPECIFIED else: - ready = fnv1.Ready.READY_FALSE - self.desired._parent.ready = ready + self.desired._parent.ready = fnv1.Ready.READY_FALSE + def setReadyCondition(self, type='Ready'): + condition = self.conditions[type] + if condition.status: + self.ready = self.observed.metadata.name + else: + error = f"not{type}Condition" + if condition.reason: + self.ready = self.status[error][condition.reason] + else: + self.ready = self.status[error] + + def addDependency(self, resource, field=_notset): + if field is _notset: + field = resource.ready + if field is None: + field = auto_ready.resource_ready(resource) + if field is None: + field = False + resource.ready = field + elif field is None: + field = True + if not isinstance(field, (protobuf.FieldMessage, protobuf.Value)): + if field: + field = resource.observed.metadata.name + else: + if not resource.observed.metadata.name: + field = resource.observed.metadata.name + else: + field = resource.status.notReady + self.metadata.annotations[f"Dependency{resource.name}"] = field class Requireds: def __init__(self, composite): @@ -408,12 +458,18 @@ def __init__(self, composite, name): self._resources = composite.request.required_resources[name] self._cache = {} - def __call__(self, apiVersion=_notset, kind=_notset, namespace=_notset, name=_notset, labels=_notset): + def __call__(self, kind=_notset, apiVersion=_notset, namespace=_notset, name=_notset, labels=_notset): self._selector() + if kind != _notset: + # Allow for apiVersion in the first arg and kind in the second arg + if '/' in kind or kind == 'v1': + if apiVersion != _notset: + self.kind = apiVersion + apiVersion = kind + else: + self.kind = kind if apiVersion != _notset: self.apiVersion = apiVersion - if kind != _notset: - self.kind = kind if namespace != _notset: self.namespace = namespace if name != _notset: @@ -802,60 +858,36 @@ def _resource_name(self): @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 + return self._composite.response.observed.composite.connection_details 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 + return self._composite.response.desired.composite.connection_details[key] 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) + return bool(self._composite.response.desired.composite.connection_details) 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) + return len(self._composite.response.desired.composite.connection_details) def __contains__(self, key): - if self._composite.crossplane_v1: - return key in self._composite.response.desired.composite.connection_details + 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) + for key, value in self._composite.response.desired.composite.connection_details: + yield key, 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) + return format(self._composite.response.desired.composite.connection_details, spec) def __call__(self, **kwargs): + self._composite.response.desired.composite.connection_details(**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: @@ -872,16 +904,13 @@ def __setitem__(self, key, value): if not value: return value = str(value) - if self._composite.crossplane_v1: - self._composite.response.desired.composite.connection_details[key] = value + self._composite.response.desired.composite.connection_details[key] = value + if self._composite.crossplane_v1 or not self._composite.connectionSecret.name: 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: @@ -896,8 +925,8 @@ def __delattr__(self, key): del self[key] def __delitem__(self, key): + del self._composite.response.desired.composite.connection_details[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] diff --git a/crossplane/pythonic/function.py b/crossplane/pythonic/function.py index 845f36f..fae20f4 100644 --- a/crossplane/pythonic/function.py +++ b/crossplane/pythonic/function.py @@ -9,7 +9,6 @@ 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__) @@ -110,7 +109,13 @@ async def run_function(self, request): iteration = int(step.iteration) + 1 step.iteration = iteration composite.context.iteration = iteration - logger.debug(f"Starting compose, {ordinal(len(composite.context._pythonic))} step, {ordinal(iteration)} pass") + if iteration == 1 and not logger.isEnabledFor(logging.DEBUG): + if len(composite.context._pythonic) == 1: + logger.info('Starting compose') + else: + logger.info(f"Starting compose, {ordinal(len(composite.context._pythonic))} step") + else: + logger.debug(f"Starting compose, {ordinal(len(composite.context._pythonic))} step, {ordinal(iteration)} pass") try: result = composite.compose() @@ -120,11 +125,13 @@ async def run_function(self, request): return self.fatal(request, logger, 'Compose', e) if requireds := self.get_requireds(step, composite): - logger.info(f"Requireds requested: {','.join(requireds)}") + logger.debug(f"Requireds requested: {','.join(requireds)}") else: self.process_usages(composite) self.process_unknowns(composite) - auto_ready.process(composite) + # Perform auto ready on all resources. + for name, resource in composite.resources: + resource.ready logger.info('Completed compose') return composite.response._message @@ -322,9 +329,11 @@ def trimFullName(self, name): name = name.split('.') for values in ( ('request', 'observed', 'composite', 'resource'), + ('response', 'desired', 'composite', 'resource'), ('request', 'observed', 'resources', None, 'resource'), - ('request', 'extra_resources', None, 'items', None, 'resource'), ('response', 'desired', 'resources', None, 'resource'), + ('request', 'required_resources', None, 'items', None, 'resource'), + ('request', 'extra_resources', None, 'items', None, 'resource'), ): if len(values) < len(name): ix = 0 diff --git a/crossplane/pythonic/protobuf.py b/crossplane/pythonic/protobuf.py index 70f3a4a..5fdfc52 100644 --- a/crossplane/pythonic/protobuf.py +++ b/crossplane/pythonic/protobuf.py @@ -37,21 +37,29 @@ def Unknown(): def Yaml(string, readOnly=None): if isinstance(string, (FieldMessage, Value)): + if not string: + return string string = str(string) return Value(None, None, yaml.safe_load(string), readOnly) def Json(string, readOnly=None): if isinstance(string, (FieldMessage, Value)): + if not string: + return string string = str(string) return Value(None, None, json.loads(string), readOnly) def B64Encode(string): if isinstance(string, (FieldMessage, Value)): + if not string: + return string string = str(string) return base64.b64encode(string.encode('utf-8')).decode('utf-8') def B64Decode(string): if isinstance(string, (FieldMessage, Value)): + if not string: + return string string = str(string) return base64.b64decode(string.encode('utf-8')).decode('utf-8') @@ -679,15 +687,28 @@ def __init__(self, parent, key, value=_Unknown, readOnly=None): def _set_attribute(self, key, value): self.__dict__[key] = value + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def __aenter__(self): + return self + + def __aexit__(self, exc_type, exc_value, traceback): + pass + def __getattr__(self, key): return self[key] def __getitem__(self, key): key = self._validate_key(key) - if key in self._cache: - return self._cache[key] - if key in self._unknowns: - return self._unknowns[key] + if key != append: + if key in self._cache: + return self._cache[key] + if key in self._unknowns: + return self._unknowns[key] if isinstance(key, str): match self._kind: case 'struct_value': @@ -701,14 +722,26 @@ def __getitem__(self, key): elif isinstance(key, int): match self._kind: case 'list_value': + if key < 0: + key = len(self._value.list_value.values) + key + if key < 0: + key = 0 if key < len(self._value.list_value.values): value = self._value.list_value.values[key] else: + if key == append: + key = len(self._value.list_value.values) value = _Unknown case 'ListValue': + if key < 0: + key = len(self._value.values) + key + if key < 0: + key = 0 if key < len(self._value.values): value = self._value.values[key] else: + if key == append: + key = len(self._value.values) value = _Unknown case 'Unknown': value = _Unknown @@ -1246,7 +1279,6 @@ def _patchUnknowns(self, patches): for key, value in self: if isinstance(value, Value) and len(value): patch = patches[key] - print(patch.__class__, str(patch)) if isinstance(patch, Value) and patch._kind == value._kind and len(patch): value._patchUnknowns(patch) elif self._isList: diff --git a/crossplane/pythonic/render.py b/crossplane/pythonic/render.py index 5c8eb87..6956b3a 100644 --- a/crossplane/pythonic/render.py +++ b/crossplane/pythonic/render.py @@ -1,4 +1,10 @@ +import asyncio +import kr8s.asyncio +import importlib +import inflect +import inspect +import logging import pathlib import sys import yaml @@ -6,6 +12,7 @@ from . import ( command, + composite, function, protobuf, ) @@ -21,15 +28,20 @@ def add_parser_arguments(cls, parser): parser.add_argument( 'composite', type=pathlib.Path, - metavar='PATH', - help='A YAML file containing the Composite resource to render.', + metavar='COMPOSITE', + help='A YAML file containing the Composite resource to render, or kind:apiVersion:namespace:name of cluster Composite.', ) parser.add_argument( 'composition', type=pathlib.Path, nargs='?', - metavar='PATH/CLASS', - help='A YAML file containing the Composition resource or the complete path of a function=-pythonic BaseComposite subclass.', + metavar='COMPOSITION', + help='A YAML file containing the Composition resource, or the complete path of a function-pythonic BaseComposite subclass.', + ) + parser.add_argument( + '--kube-context', '-k', + metavar='CONTEXT', + help='The kubectl context to use to obtain external resources from, such as required resources, connections, etc.' ) parser.add_argument( '--context-files', @@ -82,7 +94,7 @@ def add_parser_arguments(cls, parser): parser.add_argument( '--include-function-results', '-r', action='store_true', - help='Include informational and warning messages from Functions in the rendered output as resources of kind: Result..', + help='Include informational and warning messages from Functions in the rendered output as resources of kind: Result.', ) parser.add_argument( '--include-context', '-c', @@ -92,89 +104,23 @@ def add_parser_arguments(cls, parser): def initialize(self): self.initialize_function() + self.logger = logging.getLogger(__name__) + self.inflect = inflect.engine() + self.inflect.classical(all=False) async def run(self): - # Obtain the Composite to render. - if not self.args.composite.is_file(): - print(f"Composite \"{self.args.composite}\" is not a file", file=sys.stderr) - sys.exit(1) - composite = protobuf.Yaml(self.args.composite.read_text()) - - # Obtain the Composition that will be used to render the Composite. - if composite.apiVersion in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and composite.kind == 'Composite': - if self.args.composition: - print('Composite type of "composite.pythonic.crossplane.io" does not use "composition" argument', file=sys.stderr) - sys.exit(1) - composition = self.create_composition(composite, '') + if self.args.kube_context: + self.kube_context = await kr8s.asyncio.api(context=self.args.kube_context) else: - if not self.args.composition: - print('"composition" argument required', file=sys.stderr) - sys.exit(1) - if self.args.composition.is_file(): - composition = protobuf.Yaml(self.args.composition.read_text()) - else: - composite = self.args.composition.rsplit('.', 1) - if len(composite) == 1: - print(f"Composition class name does not include module: {self.args.composition}", file=sys.stderr) - sys.exit(1) - try: - module = importlib.import_module(composite[0]) - except Exception as e: - print(f"Unable to import composition class: {composite[0]}", file=sys.stderr) - sys.exit(1) - clazz = getattr(module, composite[1], None) - if not clazz: - print(f"Composition class {composite[0]} does not define: {composite[1]}", file=sys.stderr) - sys.exit(1) - if not inspect.isclass(clazz): - print(f"Composition class {self.args.composition} is not a class", file=sys.stderr) - sys.exit(1) - if not issubclass(clazz, pythonic.BaseComposite): - print(f"Composition class {self.args.composition} is not a subclass of BaseComposite", file=sys.stderr) - sys.exit(1) - composition = self.create_composition(composite, str(self.args.composition)) - - # Build up the RunFunctionRequest protobuf message used to call function-pythonic. - request = protobuf.Message(None, 'request', fnv1.RunFunctionRequest.DESCRIPTOR, fnv1.RunFunctionRequest()) - - # Load the request context with any specified command line options. - for entry in self.args.context_files: - key_path = entry.split('=', 1) - if len(key_path) != 2: - print(f"Invalid --context-files: {entry}", file=sys.stderr) - sys.exit(1) - path = pathlib.Path(key_path[1]) - if not path.is_file(): - print(f"Invalid --context-files {path} is not a file", file=sys.stderr) - sys.exit(1) - request.context[key_path[0]] = protobuf.Yaml(path.read_text()) - for entry in self.args.context_values: - key_value = entry.split('=', 1) - if len(key_value) != 2: - print(f"Invalid --context-values: {entry}", file=sys.stderr) - sys.exit(1) - request.context[key_value[0]] = protobuf.Yaml(key_value[1]) + self.kube_context = None - # Collect specified required/extra resources. Sort for stable order when processed. - requireds = sorted( - self.collect_resources(self.args.required_resources), - key=lambda required: str(required.metadata.name), - ) + await self.setup_composite() + await self.setup_composition() - # Collect specified connection and credential secrets. - secrets = [] - for secret in self.collect_resources(self.args.secret_store): - if secret.apiVersion == 'v1' and secret.kind == 'Secret': - secrets.append(secret) - - # Establish the request observed composite. - self.setup_resource(composite, secrets, request.observed.composite) - - # Establish the configured observed resources. - for resource in self.collect_resources(self.args.observed_resources): - name = resource.metadata.annotations['crossplane.io/composition-resource-name'] - if name: - self.setup_resource(resource, secrets, request.observed.resources[name]) + # Build up the RunFunctionRequest protobuf message used to call function-pythonic. + self.request = protobuf.Message(None, 'request', fnv1.RunFunctionRequest.DESCRIPTOR, fnv1.RunFunctionRequest()) + self.setup_local_resources() + await self.setup_observed_resources() # These will hold the response conditions and results. conditions = protobuf.List() @@ -185,24 +131,24 @@ async def run(self): fatal = False # Process the composition pipeline steps. - for step in composition.spec.pipeline: + for step in self.composition.spec.pipeline: if step.functionRef.name != 'function-pythonic': print(f"Only function-pythonic functions can be run: {step.functionRef.name}", file=sys.stderr) sys.exit(1) if not step.input.step: step.input.step = step.step - request.input = step.input + self.request.input = step.input # Supply step requested credentials. - request.credentials() + self.request.credentials() for credential in step.credentials: if credential.source == 'Secret' and credential.secretRef: namespace = credential.secretRef.namespace name = credential.secretRef.name if namespace and name: - for secret in secrets: + for secret in self.secrets: if secret.metadata.namespace == namespace and secret.metadata.name == name: - data = request.credentials[credential.name].credential_data.data + data = self.request.credentials[credential.name].credential_data.data data() for key, value in secret.data: data[key] = protobuf.B64Decode(value) @@ -215,22 +161,22 @@ async def run(self): requirements = protobuf.Message(None, 'requirements', fnv1.Requirements.DESCRIPTOR, fnv1.Requirements()) for _ in range(5): # Fetch the step bootstrap resources specified. - request.required_resources() - for requirement in step.requirements: - self.fetch_requireds(requireds, secrets, requirement.requirementName, requirement, request.required_resources) + self.request.required_resources() + for requirement in step.requirements.requiredResources: + await self.fetch_requireds(requirement.requirementName, requirement, self.request.required_resources) # Fetch the required resources requested. for name, selector in requirements.resources: - self.fetch_requireds(requireds, secrets, name, selector, request.required_resources) + await self.fetch_requireds(name, selector, self.request.required_resources) # Fetch the now deprecated extra resources requested. - request.extra_resources() + self.request.extra_resources() for name, selector in requirements.extra_resources: - self.fetch_requireds(requireds, secrets, name, selector, request.extra_resources) + await self.fetch_requireds(name, selector, self.request.extra_resources) # Run the step using the function-pythonic function runner. response = protobuf.Message( None, 'response', fnv1.RunFunctionResponse.DESCRIPTOR, - await runner.RunFunction(request._message, None), + await runner.RunFunction(self.request._message, None), ) # All done if there is a fatal result. for result in response.results: @@ -238,7 +184,7 @@ async def run(self): fatal = True break # Copy the response context to the request context to use in subsequent steps. - request.context = response.context + self.request.context = response.context # Exit this loop if the function has not requested additional extra/required resources. if response.requirements == requirements: break @@ -246,10 +192,10 @@ async def run(self): requirements = response.requirements # Copy the response desired state to the request desired state to use in subsequent steps. - request.desired.resources() - self.copy_resource(response.desired.composite, request.desired.composite) + self.request.desired.resources() + self.copy_resource(response.desired.composite, self.request.desired.composite) for name, resource in response.desired.resources: - self.copy_resource(resource, request.desired.resources[name]) + self.copy_resource(resource, self.request.desired.resources[name]) # Collect the step's returned conditions. for condition in response.conditions: @@ -274,57 +220,65 @@ async def run(self): # Collect and format all the returned desired composed resources. resources = protobuf.List() unready = protobuf.List() - prefix = composite.metadata.labels['crossplane.io/composite'] + prefix = self.composite.metadata.labels['crossplane.io/composite'] if not prefix: - prefix = composite.metadata.name - for name, resource in request.desired.resources: - if resource.ready != fnv1.Ready.READY_TRUE: + prefix = self.composite.metadata.name + for name, resource in self.request.desired.resources: + if resource.ready == fnv1.Ready.READY_TRUE: + ready = True + elif resource.ready == fnv1.Ready.READY_FALSE: + ready = False + else: + ready = None + if not ready: unready[protobuf.append] = name resource = resource.resource - observed = request.observed.resources[name].resource + observed = self.request.observed.resources[name].resource if observed: for key in ('namespace', 'generateName', 'name'): if observed.metadata[key]: resource.metadata[key] = observed.metadata[key] if not resource.metadata.name and not resource.metadata.generateName: resource.metadata.generateName = f"{prefix}-" - if composite.metadata.namespace: - resource.metadata.namespace = composite.metadata.namespace + if self.composite.metadata.namespace: + resource.metadata.namespace = self.composite.metadata.namespace resource.metadata.annotations['crossplane.io/composition-resource-name'] = name resource.metadata.labels['crossplane.io/composite'] = prefix - if composite.metadata.labels['crossplane.io/claim-name'] and composite.metadata.labels['crossplane.io/claim-namespace']: - resource.metadata.labels['crossplane.io/claim-namespace'] = composite.metadata.labels['crossplane.io/claim-namespace'] - resource.metadata.labels['crossplane.io/claim-name'] = composite.metadata.labels['crossplane.io/claim-name'] - elif composite.spec.claimRef.namespace and composite.spec.claimRef.name: - resource.metadata.labels['crossplane.io/claim-namespace'] = composite.spec.claimRef.namespace - resource.metadata.labels['crossplane.io/claim-name'] = composite.spec.claimRef.name + if self.composite.metadata.labels['crossplane.io/claim-name'] and self.composite.metadata.labels['crossplane.io/claim-namespace']: + resource.metadata.labels['crossplane.io/claim-namespace'] = self.composite.metadata.labels['crossplane.io/claim-namespace'] + resource.metadata.labels['crossplane.io/claim-name'] = self.composite.metadata.labels['crossplane.io/claim-name'] + elif self.composite.spec.claimRef.namespace and self.composite.spec.claimRef.name: + resource.metadata.labels['crossplane.io/claim-namespace'] = self.composite.spec.claimRef.namespace + resource.metadata.labels['crossplane.io/claim-name'] = self.composite.spec.claimRef.name resource.metadata.ownerReferences[0].controller = True resource.metadata.ownerReferences[0].blockOwnerDeletion = True - resource.metadata.ownerReferences[0].apiVersion = composite.apiVersion - resource.metadata.ownerReferences[0].kind = composite.kind - resource.metadata.ownerReferences[0].name = composite.metadata.name + resource.metadata.ownerReferences[0].apiVersion = self.composite.apiVersion + resource.metadata.ownerReferences[0].kind = self.composite.kind + resource.metadata.ownerReferences[0].name = self.composite.metadata.name resource.metadata.ownerReferences[0].uid = '' + resource.ready = ready resources[protobuf.append] = resource # Format the returned desired composite composite = protobuf.Map() - for name, value in request.desired.composite.resource: + for name, value in self.request.desired.composite.resource: composite[name] = value - composite.apiVersion = request.observed.composite.resource.apiVersion - composite.kind = request.observed.composite.resource.kind + composite.apiVersion = self.request.observed.composite.resource.apiVersion + composite.kind = self.request.observed.composite.resource.kind if self.args.include_full_xr: - composite.metadata = request.observed.composite.resource.metadata - if request.observed.composite.resource.spec: - composite.spec = request.observed.composite.resource.spec + composite.metadata = self.request.observed.composite.resource.metadata + del composite.metadata.managedFields + if self.request.observed.composite.resource.spec: + composite.spec = self.request.observed.composite.resource.spec else: - if request.observed.composite.resource.metadata.namespace: - composite.metadata.namespace = request.observed.composite.resource.metadata.namespace - composite.metadata.name = request.observed.composite.resource.metadata.name + if self.request.observed.composite.resource.metadata.namespace: + composite.metadata.namespace = self.request.observed.composite.resource.metadata.namespace + composite.metadata.name = self.request.observed.composite.resource.metadata.name # Add in the composite's status.conditions. - if request.desired.composite.ready == fnv1.Ready.READY_FALSE: + if self.request.desired.composite.ready == fnv1.Ready.READY_FALSE: condition = self.create_condition('Ready', False, 'Creating') - elif request.desired.composite.ready == fnv1.Ready.READY_UNSPECIFIED and len(unready): - condition = self.create_condition('Ready', False, 'Creating', f"Unready resources: {', '.join(str(name) for name in unready)}") + elif self.request.desired.composite.ready == fnv1.Ready.READY_UNSPECIFIED and len(unready): + condition = self.create_condition('Ready', False, 'Creating', f"Unready resources: {','.join(str(name) for name in unready)}") else: condition = self.create_condition('Ready', True, 'Available') composite.status.conditions[protobuf.append] = condition @@ -341,7 +295,7 @@ async def run(self): apiVersion = 'render.crossplane.io/v1beta1', kind = 'Connection', ) - for key, value in request.desired.composite.connection_details: + for key, value in self.request.desired.composite.connection_details: connection.values[key] = value print('---') print(str(connection), end='') @@ -364,25 +318,169 @@ async def run(self): str(protobuf.Map( apiVersion = 'render.crossplane.io/v1beta1', kind = 'Context', - values = request.context, + values = self.request.context, )), end='', ) - def create_composition(self, composite, module): - composition = protobuf.Map() - composition.apiVersion = 'apiextensions.crossplane.io/v1' - composition.kind = 'Composition' - composition.metadata.name = 'function-pythonic-render' - composition.spec.compositeTypeRef.apiVersion = composite.apiVersion - composition.spec.compositeTypeRef.kind = composite.kind - composition.spec.mode = 'Pipeline' - composition.spec.pipeline[0].step = 'function-pythonic-render' - composition.spec.pipeline[0].functionRef.name = 'function-pythonic' - composition.spec.pipeline[0].input.apiVersion = 'pythonic.fn.crossplane.io/v1alpha1' - composition.spec.pipeline[0].input.kind = 'Composite' - composition.spec.pipeline[0].input.composite = module - return composition + async def setup_composite(self): + # Obtain the Composite to render. + if self.args.composite.is_file(): + self.composite = protobuf.Yaml(self.args.composite.read_text()) + return + if not self.kube_context: + print(f"Composite \"{self.args.composite}\" is not a file", file=sys.stderr) + sys.exit(1) + composite = str(self.args.composite).split(':') + if len(composite) == 3: + namespace = None + elif len(composite) == 4: + if len(composite[2]): + namespace = composite[2] + else: + namespace = None + else: + print(f"Composite \"{self.args.composite}\" is not kind:apiVersion:namespace:name", file=sys.stderr) + sys.exit(1) + self.composite = await self.kube_get(composite[0], composite[1], namespace, composite[-1]) + + async def setup_composition(self): + # Obtain the Composition that will be used to render the Composite. + if self.composite.apiVersion in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and self.composite.kind == 'Composite': + if self.args.composition: + print('Composite type of "composite.pythonic.crossplane.io" does not use "composition" argument', file=sys.stderr) + sys.exit(1) + self.create_composition() + return + if not self.args.composition: + if not self.kube_context: + print('"composition" argument required', file=sys.stderr) + sys.exit(1) + if self.args.crossplane_v1: + revision = self.composite.spec.compositionRevisionRef + else: + revision = self.composite.spec.crossplane.compositionRevisionRef + if not revision.name: + print('Composite does not contain a CompositionRevision name', file=sys.stderr) + sys.exit(1) + self.composition = await self.kube_get('CompositionRevision', 'apiextensions.crossplane.io/v1', None, str(revision.name)) + return + if self.args.composition.is_file(): + composition = self.args.composition.read_text() + if self.args.composition.suffix == '.py': + self.create_composition(composition) + else: + self.composition = protobuf.Yaml(composition) + if not len(self.composition.spec.pipeline): + print(f"Composition file does not contain any pipeline steps: {self.args.composition}", file=sys.stderr) + sys.exit(1) + return + composition = str(self.args.composition).rsplit('.', 1) + if len(composition) == 1: + print(f"Composition class name does not include module: {self.args.composition}", file=sys.stderr) + sys.exit(1) + try: + module = importlib.import_module(composition[0]) + except Exception as e: + print(e) + print(f"Unable to import composition module: {composition[0]}", file=sys.stderr) + sys.exit(1) + clazz = getattr(module, composition[1], None) + if not clazz: + print(f"Composition class {composition[0]} does not define: {composition[1]}", file=sys.stderr) + sys.exit(1) + if not inspect.isclass(clazz): + print(f"Composition class {self.args.composition} is not a class", file=sys.stderr) + sys.exit(1) + if not issubclass(clazz, composite.BaseComposite): + print(f"Composition class {self.args.composition} is not a subclass of BaseComposite", file=sys.stderr) + sys.exit(1) + self.create_composition(self.args.composition) + + def setup_local_resources(self): + # Load the request context with any specified command line options. + for entry in self.args.context_files: + key_path = entry.split('=', 1) + if len(key_path) != 2: + print(f"Invalid --context-files: {entry}", file=sys.stderr) + sys.exit(1) + path = pathlib.Path(key_path[1]) + if not path.is_file(): + print(f"Invalid --context-files {path} is not a file", file=sys.stderr) + sys.exit(1) + self.request.context[key_path[0]] = protobuf.Yaml(path.read_text()) + for entry in self.args.context_values: + key_value = entry.split('=', 1) + if len(key_value) != 2: + print(f"Invalid --context-values: {entry}", file=sys.stderr) + sys.exit(1) + self.request.context[key_value[0]] = protobuf.Yaml(key_value[1]) + # Collect specified required/extra resources. Sort for stable order when processed. + self.requireds = sorted( + self.collect_resources(self.args.required_resources), + key=lambda required: str(required.metadata.name), + ) + # Collect specified connection and credential secrets. + self.secrets = [ + secret + for secret in self.collect_resources(self.args.secret_store) + if secret.apiVersion == 'v1' and secret.kind == 'Secret' + ] + + async def setup_observed_resources(self): + # Establish the request observed composite. + await self.setup_resource(self.composite, self.request.observed.composite) + + # Obtain observed resources if using external cluster + if self.kube_context: + async with asyncio.TaskGroup() as group: + if self.args.crossplane_v1: + refs = self.composite.spec.resourceRefs + else: + refs = self.composite.spec.crossplane.resourceRefs + for ref in refs: + group.create_task(self.setup_observed_resource(ref)) + + # Establish the manually configured observed resources. + for resource in self.collect_resources(self.args.observed_resources): + name = resource.metadata.annotations['crossplane.io/composition-resource-name'] + if name: + await self.setup_resource(resource, self.request.observed.resources[name]) + + async def setup_observed_resource(self, ref): + if ref.namespace: + namespace = str(ref.namespace) + elif self.composite.metadata.namespace: + namespace = str(self.composite.metadata.namespace) + else: + namespace = None + source = await self.kube_get( + str(ref.kind), + str(ref.apiVersion), + namespace, + str(ref.name), + False, + ) + if source: + name = source.metadata.annotations['crossplane.io/composition-resource-name'] + if name: + resource = self.request.observed.resources[name] + if not resource: + await self.setup_resource(source, resource) + + def create_composition(self, module=''): + self.composition = protobuf.Map() + self.composition.apiVersion = 'apiextensions.crossplane.io/v1' + self.composition.kind = 'Composition' + self.composition.metadata.name = 'function-pythonic-render' + self.composition.spec.compositeTypeRef.apiVersion = self.composite.apiVersion + self.composition.spec.compositeTypeRef.kind = self.composite.kind + self.composition.spec.mode = 'Pipeline' + self.composition.spec.pipeline[0].step = 'function-pythonic-render' + self.composition.spec.pipeline[0].functionRef.name = 'function-pythonic' + self.composition.spec.pipeline[0].input.apiVersion = 'pythonic.fn.crossplane.io/v1alpha1' + self.composition.spec.pipeline[0].input.kind = 'Composite' + self.composition.spec.pipeline[0].input.composite = str(module) def collect_resources(self, resources): files = [] @@ -400,34 +498,51 @@ def collect_resources(self, resources): for document in yaml.safe_load_all(file.read_text()): yield protobuf.Value(None, None, document) - def setup_resource(self, source, secrets, resource): + async def setup_resource(self, source, resource): resource.resource = source namespace = source.spec.writeConnectionSecretToRef.namespace or source.metadata.namespace name = source.spec.writeConnectionSecretToRef.name if namespace and name: - for secret in secrets: + connection = None + for secret in self.secrets: if secret.metadata.namespace == namespace and secret.metadata.name == name: - resource.connection_details() - for key, value in secret.data: - resource.connection_details[key] = protobuf.B64Decode(value) + connection = secret break - - def fetch_requireds(self, requireds, secrets, name, selector, resources): + else: + if self.kube_context: + connection = await self.kube_get('Secret', 'v1', namespace, name, False) + if connection: + resource.connection_details() + for key, value in connection.data: + resource.connection_details[key] = protobuf.B64Decode(value) + + async def fetch_requireds(self, name, selector, resources): if not name: return name = str(name) items = resources[name].items items() # Force this to get created - for required in requireds: + for required in self.requireds: if selector.api_version == required.apiVersion and selector.kind == required.kind: - if selector.match_name == required.metadata.name: - self.setup_resource(required, secrets, items[protobuf.append]) - elif selector.match_labels.labels: - for key, value in selector.match_labels.labels: - if value != required.metadata.labels[key]: - break - else: - self.setup_resource(required, secrets, items[protobuf.append]) + if ((not selector.namespace and not required.metadata.namespace) + or (selector.namespace == required.metadata.namespace) + ): + if selector.match_name == required.metadata.name: + await self.setup_resource(required, items[protobuf.append]) + elif selector.match_labels.labels: + for key, value in selector.match_labels.labels: + if value != required.metadata.labels[key]: + break + else: + await self.setup_resource(required, items[protobuf.append]) + if not len(items) and self.kube_context: + if selector.match_name: + required = await self.kube_get(selector.kind, selector.api_version, selector.namespace, selector.match_name, False) + if required: + await self.setup_resource(required, items[protobuf.append]) + elif selector.match_labels.labels: + for requiest in await kube_list(selector.kind, selector.api_version, selector.namespace, selector.match_labels.labels): + await self.setup_resource(required, items[protobuf.append]) def copy_resource(self, source, destination): destination.resource = source.resource @@ -460,3 +575,56 @@ def create_condition(self, type, status, reason, message=None): if message: condition['message'] = message return condition + + def kube_clazz(self, kind, apiVersion, namespaced): + kind = str(kind) + apiVersion = str(apiVersion) + try: + return kr8s.asyncio.objects.get_class(kind, apiVersion, True) + except KeyError: + pass + return kr8s.asyncio.objects.new_class(kind, apiVersion, True, bool(namespaced) and len(namespaced), plural=self.inflect.plural_noun(kind)) + + async def kube_get(self, kind, apiVersion, namespace, name, required=True): + clazz = self.kube_clazz(kind, apiVersion, namespace) + try: + fullName = [str(kind), str(apiVersion), str(name)] + if namespace and len(namespace): + fullName.insert(-1, str(namespace)) + resource = await clazz.get(str(name), namespace=str(namespace), api=self.kube_context) + else: + resource = await clazz.get(str(name), api=self.kube_context) + resource = protobuf.Value(None, None, resource.raw) + result = 'found' + except kr8s.NotFoundError: + if required: + print(f"Resource not found: {':'.join(fullName)}", file=sys.stderr) + sys.exit(1) + resource = None + result = 'missing' + self.logger.debug(f"Resource {result}: {':'.join(fullName)}") + return resource + + async def kube_list(self, kind, apiVersion, namespace, labelSelector): + clazz = self.kube_clazz(kind, apiVersion, namespace) + resources = [ + protobuf.Value(None, None, resource.raw) + async for resource in clazz.list( + namespace=str(namespace) if namespace and len(namespace) else None, + label_selector={ + label: str(value) + for label, value in labelSelector + }, + ) + ] + if self.logger.isEnabledFor(logging.DEBUG): + fullName = [str(kind), str(apiVersion)] + if namespace and len(namespace): + fullName.append(str(namespace)) + fullName.append('&'.join(f"{label}={value}" for label, value in labelSelector)) + if resources: + result = f"found {self.inflect.number_to_words(len(resources))}" + else: + result = 'missing' + self.logger.debug(f"Resources {result}: {':'.join(fullName)}") + return resources diff --git a/examples/.dev/functions.yaml b/examples/.dev/functions.yaml deleted file mode 100644 index fd5a6f2..0000000 --- a/examples/.dev/functions.yaml +++ /dev/null @@ -1,13 +0,0 @@ ---- -# Use this function yaml when developing and executing the function directly instead of in a container -# E.g. `go run . --insecure --debug --address=":9443"` - -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - render.crossplane.io/runtime: Development - render.crossplane.io/runtime-development-target: localhost:9443 -spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.1 diff --git a/examples/aks-cluster/aks/kubernetescluster.py b/examples/aks-cluster/aks/kubernetescluster.py index 14789be..5a1d184 100644 --- a/examples/aks-cluster/aks/kubernetescluster.py +++ b/examples/aks-cluster/aks/kubernetescluster.py @@ -6,7 +6,7 @@ def compose(self): self.logger.info(f"Composing AKS cluster {self.metadata.name}") aks = self.resources.KubernetesCluster( - 'containerservice.azure.upbound.io/v1beta2', 'KubernetesCluster' + 'KubernetesCluster', 'containerservice.azure.upbound.io/v1beta2' ) aks.metadata.name = self.metadata.name aks.metadata.labels = labels diff --git a/examples/aks-cluster/aks/resourcegroup.py b/examples/aks-cluster/aks/resourcegroup.py index 91664ae..faf587c 100644 --- a/examples/aks-cluster/aks/resourcegroup.py +++ b/examples/aks-cluster/aks/resourcegroup.py @@ -5,9 +5,7 @@ def compose(self): labels = {'example.crossplane.io/AzureKubernetesCluster': self.metadata.name} self.logger.info(f"Composing Azure ResourceGroup {self.spec.resourceGroupName}") - rg = self.resources.ResourceGroup( - 'azure.upbound.io/v1beta1', 'ResourceGroup' - ) + rg = self.resources.ResourceGroup('ResourceGroup', 'azure.upbound.io/v1beta1') rg.metadata.name = self.spec.resourceGroupName rg.metadata.labels = labels diff --git a/examples/aks-cluster/cluster-function-pythonic.yaml b/examples/aks-cluster/cluster-function-pythonic.yaml index 462bc3a..2cc3230 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.3.1 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.0 runtimeConfigRef: name: function-pythonic --- diff --git a/examples/aks-cluster/functions.yaml b/examples/aks-cluster/functions.yaml deleted file mode 100644 index 241a3c6..0000000 --- a/examples/aks-cluster/functions.yaml +++ /dev/null @@ -1,10 +0,0 @@ -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.1 - runtimeConfigRef: - name: function-pythonic diff --git a/examples/aks-cluster/render.sh b/examples/aks-cluster/render.sh index 93a521a..cfc7086 100755 --- a/examples/aks-cluster/render.sh +++ b/examples/aks-cluster/render.sh @@ -1,10 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) - -# In one terminal -# PYTHONPATH=. function-pythonic grpc --insecure --debug -# In another terminal: -#exec crossplane render xr.yaml composition.yaml functions.yaml - exec function-pythonic render --python-path=. xr.yaml composition.yaml diff --git a/examples/connection-details-composition/composition.yaml b/examples/connection-details-composition/composition.yaml index 5a59f8d..91b59ff 100644 --- a/examples/connection-details-composition/composition.yaml +++ b/examples/connection-details-composition/composition.yaml @@ -19,11 +19,11 @@ spec: def compose(self): self.connectionSecret = self.spec.writeConnectionSecretToRef - user = self.resources.user('iam.aws.m.upbound.io/v1beta1', 'User') + user = self.resources.user('User', 'iam.aws.m.upbound.io/v1beta1') user.spec.forProvider = {} for ix in range(2): - key = self.resources[f"access-key-{ix}"]('iam.aws.m.upbound.io/v1beta1', 'AccessKey') + key = self.resources[f"access-key-{ix}"]('AccessKey', 'iam.aws.m.upbound.io/v1beta1') 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 diff --git a/examples/connection-details-composition/functions.yaml b/examples/connection-details-composition/functions.yaml deleted file mode 100644 index caa6091..0000000 --- a/examples/connection-details-composition/functions.yaml +++ /dev/null @@ -1,8 +0,0 @@ -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.1 diff --git a/examples/connection-details-composition/render.sh b/examples/connection-details-composition/render.sh index f1dec18..e8c6502 100755 --- a/examples/connection-details-composition/render.sh +++ b/examples/connection-details-composition/render.sh @@ -1,7 +1,5 @@ #!/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 diff --git a/examples/connections/composition.yaml b/examples/connections/composition.yaml index d8a07dc..04eaa92 100644 --- a/examples/connections/composition.yaml +++ b/examples/connections/composition.yaml @@ -23,7 +23,7 @@ spec: composite: | class Composite(BaseComposite): def compose(self): - rds = self.resources.rds('rds.aws.upbound.io/v1beta1', 'Instance') + rds = self.resources.rds('Instance', 'rds.aws.upbound.io/v1beta1') rds.spec.forProvider.name = 'pythonic-example' rds.spec.writeConnectionSecretToRef.namespace = 'default' rds.spec.writeConnectionSecretToRef.name = 'rds-connection' diff --git a/examples/eks-cluster/composition-v2.yaml b/examples/eks-cluster/composition-v2.yaml index 3889681..d9ed2a6 100644 --- a/examples/eks-cluster/composition-v2.yaml +++ b/examples/eks-cluster/composition-v2.yaml @@ -18,7 +18,7 @@ spec: composite: | class Composite(BaseComposite): def compose(self): - v = self.resources.VPC('ec2.aws.crossplane.io/v1beta1', 'VPC') + v = self.resources.VPC('VPC', 'ec2.aws.crossplane.io/v1beta1') v.spec.forProvider( region = self.spec.cluster.region, enableDnsHostNames = True, @@ -30,12 +30,12 @@ spec: publicTableId, publicSubnetIds = self.compose_network( True, '10.0.0.0/20', '10.0.16.0/20', '10.0.32.0/20', ) - g = self.resources.InternetGateway('ec2.aws.crossplane.io/v1beta1', 'InternetGateway') + g = self.resources.InternetGateway('InternetGateway', 'ec2.aws.crossplane.io/v1beta1') g.spec.forProvider( region = self.spec.cluster.region, vpcId = self.status.vpcId, tags = self.tags(Name=self.spec.cluster.name)) - self.resources.RoutePublic('ec2.aws.crossplane.io/v1alpha1', 'Route').spec.forProvider( + self.resources.RoutePublic('Route', 'ec2.aws.crossplane.io/v1alpha1').spec.forProvider( region = self.spec.cluster.region, routeTableId = publicTableId, destinationCIDRBlock = '0.0.0.0/0', @@ -44,23 +44,23 @@ spec: privateTableId, privateSubnetIds = self.compose_network( False, '10.0.64.0/18', '10.0.128.0/18', '10.0.192.0/18', ) - a = self.resources.Address('ec2.aws.crossplane.io/v1beta1', 'Address') + a = self.resources.Address('Address', 'ec2.aws.crossplane.io/v1beta1') a.spec.forProvider( region = self.spec.cluster.region, tags = self.tags(Name=self.spec.cluster.name)) - g = self.resources.NATGateway('ec2.aws.crossplane.io/v1beta1', 'NATGateway') + g = self.resources.NATGateway('NATGateway', 'ec2.aws.crossplane.io/v1beta1') g.spec.forProvider( region = self.spec.cluster.region, allocationId = a.status.atProvider.allocationId, subnetId = publicSubnetIds[0], tags = self.tags(Name=self.spec.cluster.name)) - self.resources.RoutePrivate('ec2.aws.crossplane.io/v1alpha1', 'Route').spec.forProvider( + self.resources.RoutePrivate('Route', 'ec2.aws.crossplane.io/v1alpha1').spec.forProvider( region = self.spec.cluster.region, routeTableId = privateTableId, destinationCIDRBlock = '0.0.0.0/0', natGatewayId = g.status.atProvider.natGatewayId) - c = self.resources.Cluster('eks.aws.crossplane.io/v1beta1', 'Cluster') + c = self.resources.Cluster('Cluster', 'eks.aws.crossplane.io/v1beta1') c.externalName = self.spec.cluster.name c.spec.forProvider( region = self.spec.cluster.region, @@ -78,7 +78,7 @@ spec: for ix, subnet in enumerate(subnets): zone = chr(ord('a') + ix) s = self.resources[f"Subnet{access.capitalize()}{zone.capitalize()}"]( - 'ec2.aws.crossplane.io/v1beta1', 'Subnet' + 'Subnet', 'ec2.aws.crossplane.io/v1beta1', ) s.spec.forProvider( region = self.spec.cluster.region, @@ -95,7 +95,7 @@ spec: subnetIds[ix] = s.status.atProvider.subnetId associations[ix].subnetId = s.status.atProvider.subnetId t = self.resources[f"RouteTable{access.capitalize()}"]( - 'ec2.aws.crossplane.io/v1beta1', 'RouteTable' + 'RouteTable', 'ec2.aws.crossplane.io/v1beta1', ) t.spec.forProvider( region = self.spec.cluster.region, @@ -112,13 +112,13 @@ spec: s.Effect = 'Allow' s.Principal.Service = 'eks.amazonaws.com' s.Action = 'sts:AssumeRole' - r = self.resources.RoleCluster('iam.aws.crossplane.io/v1beta1', 'Role') + r = self.resources.RoleCluster('Role', 'iam.aws.crossplane.io/v1beta1') r.externalName = name r.spec.forProvider.assumeRolePolicyDocument = format(d, 'json') r.spec.forProvider.tags = self.tags() for policy in ('ClusterPolicy', 'VPCResourceController'): self.resources[f"Rpa{policy}"]( - 'iam.aws.crossplane.io/v1beta1', 'RolePolicyAttachment' + 'RolePolicyAttachment', 'iam.aws.crossplane.io/v1beta1', ).spec.forProvider( roleName = r.status.atProvider.roleID and name, policyArn = f"arn:aws:iam::aws:policy/AmazonEKS{policy}") diff --git a/examples/eks-cluster/composition.yaml b/examples/eks-cluster/composition.yaml index fd8355d..595bf6e 100644 --- a/examples/eks-cluster/composition.yaml +++ b/examples/eks-cluster/composition.yaml @@ -20,7 +20,7 @@ spec: def compose(self): self.usages = True - v = self.resources.VPC('ec2.aws.crossplane.io/v1beta1', 'VPC') + v = self.resources.VPC('VPC', 'ec2.aws.crossplane.io/v1beta1') v.spec.forProvider.region = self.spec.cluster.region v.spec.forProvider.enableDnsHostNames = True v.spec.forProvider.enableDnsSupport = True @@ -31,11 +31,11 @@ spec: publicTableId, publicSubnetIds = self.compose_network( True, '10.0.0.0/20', '10.0.16.0/20', '10.0.32.0/20', ) - g = self.resources.InternetGateway('ec2.aws.crossplane.io/v1beta1', 'InternetGateway') + g = self.resources.InternetGateway('InternetGateway', 'ec2.aws.crossplane.io/v1beta1') g.spec.forProvider.region = self.spec.cluster.region g.spec.forProvider.vpcId = self.status.vpcId g.spec.forProvider.tags = self.tags(Name=self.spec.cluster.name) - r = self.resources.RoutePublic('ec2.aws.crossplane.io/v1alpha1', 'Route').spec.forProvider + r = self.resources.RoutePublic('Route', 'ec2.aws.crossplane.io/v1alpha1').spec.forProvider r.region = self.spec.cluster.region r.routeTableId = publicTableId r.destinationCIDRBlock = '0.0.0.0/0' @@ -44,21 +44,21 @@ spec: privateTableId, privateSubnetIds = self.compose_network( False, '10.0.64.0/18', '10.0.128.0/18', '10.0.192.0/18', ) - a = self.resources.Address('ec2.aws.crossplane.io/v1beta1', 'Address') + a = self.resources.Address('Address', 'ec2.aws.crossplane.io/v1beta1') a.spec.forProvider.region = self.spec.cluster.region a.spec.forProvider.tags = self.tags(Name=self.spec.cluster.name) - g = self.resources.NATGateway('ec2.aws.crossplane.io/v1beta1', 'NATGateway') + g = self.resources.NATGateway('NATGateway', 'ec2.aws.crossplane.io/v1beta1') g.spec.forProvider.region = self.spec.cluster.region g.spec.forProvider.allocationId = a.status.atProvider.allocationId g.spec.forProvider.subnetId = publicSubnetIds[0] g.spec.forProvider.tags = self.tags(Name=self.spec.cluster.name) - r = self.resources.RoutePrivate('ec2.aws.crossplane.io/v1alpha1', 'Route').spec.forProvider + r = self.resources.RoutePrivate('Route', 'ec2.aws.crossplane.io/v1alpha1').spec.forProvider r.region = self.spec.cluster.region r.routeTableId = privateTableId r.destinationCIDRBlock = '0.0.0.0/0' r.natGatewayId = g.status.atProvider.natGatewayId - c = self.resources.Cluster('eks.aws.crossplane.io/v1beta1', 'Cluster') + c = self.resources.Cluster('Cluster', 'eks.aws.crossplane.io/v1beta1') c.externalName = self.spec.cluster.name c.spec.forProvider.region = self.spec.cluster.region c.spec.forProvider.roleArn = self.compose_cluster_role() @@ -72,7 +72,7 @@ spec: for ix, subnet in enumerate(subnets): zone = chr(ord('a') + ix) s = self.resources[f"Subnet{access.capitalize()}{zone.capitalize()}"]( - 'ec2.aws.crossplane.io/v1beta1', 'Subnet' + 'Subnet', 'ec2.aws.crossplane.io/v1beta1', ) s.spec.forProvider.region = self.spec.cluster.region s.spec.forProvider.vpcId = self.status.vpcId @@ -87,7 +87,7 @@ spec: ) subnetIds.append(s.status.atProvider.subnetId) t = self.resources[f"RouteTable{access.capitalize()}"]( - 'ec2.aws.crossplane.io/v1beta1', 'RouteTable' + 'RouteTable', 'ec2.aws.crossplane.io/v1beta1', ) t.spec.forProvider.region = self.spec.cluster.region t.spec.forProvider.vpcId = self.status.vpcId @@ -102,12 +102,12 @@ spec: d.Statement[0].Effect = 'Allow' d.Statement[0].Principal.Service = 'eks.amazonaws.com' d.Statement[0].Action = 'sts:AssumeRole' - r = self.resources.RoleCluster('iam.aws.crossplane.io/v1beta1', 'Role') + r = self.resources.RoleCluster('Role', 'iam.aws.crossplane.io/v1beta1') r.externalName = name r.spec.forProvider.assumeRolePolicyDocument = format(d, 'json') r.spec.forProvider.tags = self.tags() for policy in ('ClusterPolicy', 'VPCResourceController'): - a = self.resources[f"Rpa{policy}"]('iam.aws.crossplane.io/v1beta1', 'RolePolicyAttachment').spec.forProvider + a = self.resources[f"Rpa{policy}"]('RolePolicyAttachment', 'iam.aws.crossplane.io/v1beta1').spec.forProvider a.roleName = r.status.atProvider.roleID and name a.policyArn = f"arn:aws:iam::aws:policy/AmazonEKS{policy}" return r.status.atProvider.arn diff --git a/examples/eks-cluster/functions.yaml b/examples/eks-cluster/functions.yaml deleted file mode 100644 index 2febaea..0000000 --- a/examples/eks-cluster/functions.yaml +++ /dev/null @@ -1,8 +0,0 @@ -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.1 diff --git a/examples/eks-cluster/render-v2.sh b/examples/eks-cluster/render-v2.sh index 69df3f6..ca0386d 100755 --- a/examples/eks-cluster/render-v2.sh +++ b/examples/eks-cluster/render-v2.sh @@ -1,4 +1,3 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -#exec crossplane render xr.yaml composition-v2.yaml functions.yaml exec function-pythonic render xr.yaml composition-v2.yaml diff --git a/examples/eks-cluster/render.sh b/examples/eks-cluster/render.sh index 17aec4e..119b278 100755 --- a/examples/eks-cluster/render.sh +++ b/examples/eks-cluster/render.sh @@ -1,4 +1,3 @@ #!/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 diff --git a/examples/filing-system/function.yaml b/examples/filing-system/function.yaml deleted file mode 100644 index 96e5b5e..0000000 --- a/examples/filing-system/function.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic -spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.1 - runtimeConfigRef: - apiVersion: pkg.crossplane.io/v1beta1 - kind: DeploymentRuntimeConfig - name: function-pythonic diff --git a/examples/filing-system/vcluster.py b/examples/filing-system/vcluster.py index c1b770e..2ea3ec2 100644 --- a/examples/filing-system/vcluster.py +++ b/examples/filing-system/vcluster.py @@ -6,7 +6,7 @@ def compose(self): name = self.metadata.name namespace = name - release = self.resources.release('helm.crossplane.io/v1beta1', 'Release', name=name) + release = self.resources.release('Release', 'helm.crossplane.io/v1beta1', name=name) release.spec.rollbackLimit = 1 release.spec.forProvider.chart.repository = 'https://charts.loft.sh' release.spec.forProvider.chart.name = 'vcluster' @@ -15,9 +15,9 @@ def compose(self): release.spec.forProvider.values.controlPlane.proxy.extraSANs[0] = f'{name}.{namespace}' secret_name = f'vc-{name}' - vcluster_secret = self.requireds.vcluster_secret('v1', 'Secret', namespace, secret_name)[0] + vcluster_secret = self.requireds.vcluster_secret('Secret', 'v1', namespace, secret_name)[0] if vcluster_secret: - argocd_secret = self.resources.argocd_secret('v1', 'Secret', 'default', secret_name) + argocd_secret = self.resources.argocd_secret('Secret', 'v1', 'default', secret_name) argocd_secret.metadata.labels['argocd.argoproj.io/secret-type'] = 'cluster' argocd_secret.type = 'Opaque' argocd_secret.data.name = B64Encode(name) diff --git a/examples/function-go-templating/conditions/functions.yaml b/examples/function-go-templating/conditions/functions.yaml deleted file mode 100644 index 34d17c4..0000000 --- a/examples/function-go-templating/conditions/functions.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/conditions/render.sh b/examples/function-go-templating/conditions/render.sh index 17aec4e..119b278 100755 --- a/examples/function-go-templating/conditions/render.sh +++ b/examples/function-go-templating/conditions/render.sh @@ -1,4 +1,3 @@ #!/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 diff --git a/examples/function-go-templating/context/functions.yaml b/examples/function-go-templating/context/functions.yaml deleted file mode 100644 index 34d17c4..0000000 --- a/examples/function-go-templating/context/functions.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/context/render.sh b/examples/function-go-templating/context/render.sh index 382ae5d..009bea5 100755 --- a/examples/function-go-templating/context/render.sh +++ b/examples/function-go-templating/context/render.sh @@ -1,4 +1,3 @@ #!/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 --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 deleted file mode 100644 index e153d1b..0000000 --- a/examples/function-go-templating/functions/fromYaml/functions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/functions/fromYaml/render.sh b/examples/function-go-templating/functions/fromYaml/render.sh index 17aec4e..119b278 100755 --- a/examples/function-go-templating/functions/fromYaml/render.sh +++ b/examples/function-go-templating/functions/fromYaml/render.sh @@ -1,4 +1,3 @@ #!/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 diff --git a/examples/function-go-templating/functions/getComposedResource/composition.yaml b/examples/function-go-templating/functions/getComposedResource/composition.yaml index 66536fe..4da1039 100644 --- a/examples/function-go-templating/functions/getComposedResource/composition.yaml +++ b/examples/function-go-templating/functions/getComposedResource/composition.yaml @@ -20,9 +20,9 @@ spec: class Composite(BaseComposite): def compose(self): self.ttl = (5 * 60) + 30 - s = self.resources.flexServer(apiVersion, 'FlexibleServer') + s = self.resources.flexServer('FlexibleServer', apiVersion) s.spec.providerConfigRef.name = 'my-provider-cfg' s.spec.forProvider.storageMb = 32768 - c = self.resources.flexServerConfig(apiVersion, 'FlexibleServerConfiguration') + c = self.resources.flexServerConfig('FlexibleServerConfiguration', apiVersion) c.spec.providerConfigRef.name = 'my-provider-cfg' c.spec.forProvider.serverId = s.status.atProvider.id diff --git a/examples/function-go-templating/functions/getComposedResource/functions.yaml b/examples/function-go-templating/functions/getComposedResource/functions.yaml deleted file mode 100644 index e153d1b..0000000 --- a/examples/function-go-templating/functions/getComposedResource/functions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/functions/getComposedResource/render.sh b/examples/function-go-templating/functions/getComposedResource/render.sh index 934bf9e..653b760 100755 --- a/examples/function-go-templating/functions/getComposedResource/render.sh +++ b/examples/function-go-templating/functions/getComposedResource/render.sh @@ -1,4 +1,3 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -#exec crossplane render --observed-resources=observed.yaml xr.yaml composition.yaml functions.yaml exec function-pythonic render --observed-resources=observed.yaml xr.yaml composition.yaml diff --git a/examples/function-go-templating/functions/getCompositeResource/composition.yaml b/examples/function-go-templating/functions/getCompositeResource/composition.yaml index 71b94f4..939c808 100644 --- a/examples/function-go-templating/functions/getCompositeResource/composition.yaml +++ b/examples/function-go-templating/functions/getCompositeResource/composition.yaml @@ -18,6 +18,6 @@ spec: composite: | class Composite(BaseComposite): def compose(self): - r = self.resources.flexserver('dbforpostgresql.azure.upbound.io/v1beta1', 'FlexibleServer') + r = self.resources.flexserver('FlexibleServer', 'dbforpostgresql.azure.upbound.io/v1beta1') r.spec.forProvider.adminLogin = self.spec.adminLogin r.spec.forProvider.location = self.spec.location diff --git a/examples/function-go-templating/functions/getCompositeResource/functions.yaml b/examples/function-go-templating/functions/getCompositeResource/functions.yaml deleted file mode 100644 index e153d1b..0000000 --- a/examples/function-go-templating/functions/getCompositeResource/functions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/functions/getCompositeResource/render.sh b/examples/function-go-templating/functions/getCompositeResource/render.sh index 17aec4e..119b278 100755 --- a/examples/function-go-templating/functions/getCompositeResource/render.sh +++ b/examples/function-go-templating/functions/getCompositeResource/render.sh @@ -1,4 +1,3 @@ #!/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 diff --git a/examples/function-go-templating/functions/getCredentialData/functions.yaml b/examples/function-go-templating/functions/getCredentialData/functions.yaml deleted file mode 100644 index e153d1b..0000000 --- a/examples/function-go-templating/functions/getCredentialData/functions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/functions/getCredentialData/render.sh b/examples/function-go-templating/functions/getCredentialData/render.sh index 7227e1d..e648bb6 100755 --- a/examples/function-go-templating/functions/getCredentialData/render.sh +++ b/examples/function-go-templating/functions/getCredentialData/render.sh @@ -1,4 +1,3 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -#exec crossplane render --function-credentials=credentials.yaml --include-context xr.yaml composition.yaml functions.yaml exec function-pythonic render --secret-store=credentials.yaml --include-context xr.yaml composition.yaml diff --git a/examples/function-go-templating/functions/getResourceCondition/functions.yaml b/examples/function-go-templating/functions/getResourceCondition/functions.yaml deleted file mode 100644 index e153d1b..0000000 --- a/examples/function-go-templating/functions/getResourceCondition/functions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/functions/getResourceCondition/render.sh b/examples/function-go-templating/functions/getResourceCondition/render.sh index 303643b..79eee72 100755 --- a/examples/function-go-templating/functions/getResourceCondition/render.sh +++ b/examples/function-go-templating/functions/getResourceCondition/render.sh @@ -1,4 +1,3 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -#exec crossplane render --observed-resources=observed.yaml xr.yaml composition.yaml functions.yaml exec function-pythonic render --observed-resources=observed.yaml xr.yaml composition.yaml diff --git a/examples/function-go-templating/functions/include/composition.yaml b/examples/function-go-templating/functions/include/composition.yaml index 161fa0c..4a4a9db 100644 --- a/examples/function-go-templating/functions/include/composition.yaml +++ b/examples/function-go-templating/functions/include/composition.yaml @@ -21,7 +21,7 @@ spec: 'some-text': self.spec.val1, 'other-text': self.spec.val2, } - r = self.resources.test1('apps/v1', 'Deployment') + r = self.resources.test1('Deployment', 'apps/v1', 'Deployment') r.metadata.labels = labels r.spec.replicas = 3 r.spec.selector.matchLabels = labels diff --git a/examples/function-go-templating/functions/include/functions.yaml b/examples/function-go-templating/functions/include/functions.yaml deleted file mode 100644 index e153d1b..0000000 --- a/examples/function-go-templating/functions/include/functions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/functions/include/render.sh b/examples/function-go-templating/functions/include/render.sh index 17aec4e..119b278 100755 --- a/examples/function-go-templating/functions/include/render.sh +++ b/examples/function-go-templating/functions/include/render.sh @@ -1,4 +1,3 @@ #!/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 diff --git a/examples/function-go-templating/functions/toYaml/functions.yaml b/examples/function-go-templating/functions/toYaml/functions.yaml deleted file mode 100644 index e153d1b..0000000 --- a/examples/function-go-templating/functions/toYaml/functions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/functions/toYaml/render.sh b/examples/function-go-templating/functions/toYaml/render.sh index 17aec4e..119b278 100755 --- a/examples/function-go-templating/functions/toYaml/render.sh +++ b/examples/function-go-templating/functions/toYaml/render.sh @@ -1,4 +1,3 @@ #!/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 diff --git a/examples/function-go-templating/inline/composition.yaml b/examples/function-go-templating/inline/composition.yaml index 7ea89f7..84a3c6b 100644 --- a/examples/function-go-templating/inline/composition.yaml +++ b/examples/function-go-templating/inline/composition.yaml @@ -21,10 +21,10 @@ spec: def compose(self): for ix in range(int(self.spec.count)): user = f"test-user-{ix}" - r = self.resources[user]('iam.aws.upbound.io/v1beta1', 'User') + r = self.resources[user]('User', 'iam.aws.upbound.io/v1beta1') r.metadata.labels['testing.upbound.io/example-name'] = user r.metadata.labels.dummy = r.observed.metadata.labels.dummy or random.choice(['foo', 'bar', 'baz']) - r = self.resources[f"sample-access-key-{ix}"]('iam.aws.upbound.io/v1beta1', 'AccessKey') + r = self.resources[f"sample-access-key-{ix}"]('AccessKey', 'iam.aws.upbound.io/v1beta1') r.spec.forProvider.userSelector.matchLabels['testing.upbound.io/example-name'] = user r.spec.writeConnectionSecretToRef.namespace = 'crossplane.system' r.spec.writeConnectionSecretToRef.name = f"sample-access-key-secret-{ix}" diff --git a/examples/function-go-templating/inline/functions.yaml b/examples/function-go-templating/inline/functions.yaml deleted file mode 100644 index e153d1b..0000000 --- a/examples/function-go-templating/inline/functions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/inline/render.sh b/examples/function-go-templating/inline/render.sh index 17aec4e..119b278 100755 --- a/examples/function-go-templating/inline/render.sh +++ b/examples/function-go-templating/inline/render.sh @@ -1,4 +1,3 @@ #!/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 diff --git a/examples/function-go-templating/recursive/composition-real.yaml b/examples/function-go-templating/recursive/composition-real.yaml index e51bdee..1536baa 100644 --- a/examples/function-go-templating/recursive/composition-real.yaml +++ b/examples/function-go-templating/recursive/composition-real.yaml @@ -17,5 +17,5 @@ spec: composite: | class Composite(BaseComposite): def compose(self): - r = self.resources.bucket('s3.aws.upbound.io/v1beta1', 'Bucket') + r = self.resources.bucket('Bucket', 's3.aws.upbound.io/v1beta1') r.spec.forProvider.region = self.spec.region diff --git a/examples/function-go-templating/recursive/composition-wrapper.yaml b/examples/function-go-templating/recursive/composition-wrapper.yaml index 0ecce94..5556cc4 100644 --- a/examples/function-go-templating/recursive/composition-wrapper.yaml +++ b/examples/function-go-templating/recursive/composition-wrapper.yaml @@ -18,6 +18,6 @@ spec: class Composite(BaseComposite): def compose(self): for ix in range(int(self.spec.count)): - r = self.resources[f"test-xr-{ix}"]('example.crossplane.io/v1beta1', 'XR') + r = self.resources[f"test-xr-{ix}"]('XR', 'example.crossplane.io/v1beta1') r.spec.compositionRef.name = 'example-recursive-real' r.spec.region = f"us-west-{ix+1}" diff --git a/examples/function-go-templating/recursive/functions.yaml b/examples/function-go-templating/recursive/functions.yaml deleted file mode 100644 index e153d1b..0000000 --- a/examples/function-go-templating/recursive/functions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/recursive/render.sh b/examples/function-go-templating/recursive/render.sh index e00d274..a1edb77 100755 --- a/examples/function-go-templating/recursive/render.sh +++ b/examples/function-go-templating/recursive/render.sh @@ -1,7 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -#crossplane render xr.yaml composition-wrapper.yaml functions.yaml -#crossplane render xr.yaml composition-real.yaml functions.yaml - function-pythonic render xr.yaml composition-wrapper.yaml #function-pythonic render xr.yaml composition-real.yaml diff --git a/examples/function-go-templating/required-resources/composition.yaml b/examples/function-go-templating/required-resources/composition.yaml index 97211e5..3e0ca8a 100644 --- a/examples/function-go-templating/required-resources/composition.yaml +++ b/examples/function-go-templating/required-resources/composition.yaml @@ -17,9 +17,9 @@ spec: composite: | class Composite(BaseComposite): def compose(self): - buckets = self.requireds.bucket('s3.aws.upbound.io/v1beta1', 'Bucket', name=f"my-awesome-{self.spec.environment}-bucket") + buckets = self.requireds.bucket('Bucket', 's3.aws.upbound.io/v1beta1', name=f"my-awesome-{self.spec.environment}-bucket") for ix, bucket in enumerate(buckets): - r = self.resources[f"bucket-configmap-{ix}"]('kubernetes.crossplane.io/v1alpha1', 'Object') + r = self.resources[f"bucket-configmap-{ix}"]('Object', 'kubernetes.crossplane.io/v1alpha1') r.spec.providerConfigRef.name = 'kubernetes' manifest = r.spec.forProvider.manifest manifest.apiVersion = 'v1' diff --git a/examples/function-go-templating/required-resources/functions.yaml b/examples/function-go-templating/required-resources/functions.yaml deleted file mode 100644 index e153d1b..0000000 --- a/examples/function-go-templating/required-resources/functions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/function-go-templating/required-resources/render.sh b/examples/function-go-templating/required-resources/render.sh index 131ca01..1c18917 100755 --- a/examples/function-go-templating/required-resources/render.sh +++ b/examples/function-go-templating/required-resources/render.sh @@ -1,4 +1,3 @@ #!/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 --required-resources required-resources.yaml xr.yaml composition.yaml diff --git a/examples/function-sequencer/composition.yaml b/examples/function-sequencer/composition.yaml index c0aac2b..a221f2e 100644 --- a/examples/function-sequencer/composition.yaml +++ b/examples/function-sequencer/composition.yaml @@ -24,7 +24,7 @@ spec: self.create_resource('third', (5, False), (10, True)) def create_resource(self, name, *conditions): - resource = self.resources[f"{name}-resource"]('nop.crossplane.io/v1alpha1', 'NopResource') + resource = self.resources[f"{name}-resource"]('NopResource', 'nop.crossplane.io/v1alpha1') for ix, (seconds, status) in enumerate(conditions): resource.spec.forProvider.conditionAfter[ix]( time=f"{seconds}s", diff --git a/examples/function-sequencer/functions.yaml b/examples/function-sequencer/functions.yaml deleted file mode 100644 index caa6091..0000000 --- a/examples/function-sequencer/functions.yaml +++ /dev/null @@ -1,8 +0,0 @@ -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.1 diff --git a/examples/function-sequencer/render.sh b/examples/function-sequencer/render.sh index 9551eea..42f7578 100755 --- a/examples/function-sequencer/render.sh +++ b/examples/function-sequencer/render.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -#exec crossplane render -r xr.yaml composition.yaml functions.yaml exec function-pythonic render \ xr.yaml composition.yaml \ diff --git a/examples/get-started-app/composition.yaml b/examples/get-started-app/composition.yaml index 9853a10..6a2e62c 100644 --- a/examples/get-started-app/composition.yaml +++ b/examples/get-started-app/composition.yaml @@ -19,7 +19,7 @@ spec: def compose(self): labels = {'example.crossplane.io/app': self.metadata.name} - d = self.resources.deployment('apps/v1', 'Deployment') + d = self.resources.deployment('Deployment', 'apps/v1') d.metadata.labels = labels d.spec.replicas = 2 d.spec.selector.matchLabels = labels @@ -28,7 +28,7 @@ spec: d.spec.template.spec.containers[0].image = self.spec.image d.spec.template.spec.containers[0].ports[0].containerPort = 80 - s = self.resources.service('v1', 'Service') + s = self.resources.service('Service', 'v1') s.metadata.labels = labels s.spec.selector = labels s.spec.ports[0].protocol = 'TCP' diff --git a/examples/get-started-app/functions.yaml b/examples/get-started-app/functions.yaml deleted file mode 100644 index caa6091..0000000 --- a/examples/get-started-app/functions.yaml +++ /dev/null @@ -1,8 +0,0 @@ -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.1 diff --git a/examples/get-started-app/observed.yaml b/examples/get-started-app/observed.yaml new file mode 100644 index 0000000..ab3efd8 --- /dev/null +++ b/examples/get-started-app/observed.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + crossplane.io/composition-resource-name: deployment + generateName: my-app- + labels: + crossplane.io/composite: my-app + example.crossplane.io/app: my-app + namespace: default + name: my-app-01234 +spec: + replicas: 2 + selector: + matchLabels: + example.crossplane.io/app: my-app + template: + metadata: + labels: + example.crossplane.io/app: my-app + spec: + containers: + - image: nginx + name: app + ports: + - containerPort: 80 +status: + availableReplicas: 2 + updatedReplicas: 2 + conditions: + - type: Available + status: 'True' +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + crossplane.io/composition-resource-name: service + generateName: my-app- + labels: + crossplane.io/composite: my-app + example.crossplane.io/app: my-app + namespace: default + name: my-app-12345 +spec: + clusterIP: 1.2.3.4 + ports: + - port: 8080 + protocol: TCP + targetPort: 80 + selector: + example.crossplane.io/app: my-app diff --git a/examples/get-started-app/render.sh b/examples/get-started-app/render.sh index f5215b3..5e1f74e 100755 --- a/examples/get-started-app/render.sh +++ b/examples/get-started-app/render.sh @@ -1,4 +1,3 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -#exec crossplane render xr.yaml composition.yaml functions.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 deleted file mode 100644 index caa6091..0000000 --- a/examples/helm-copy-secret/functions.yaml +++ /dev/null @@ -1,8 +0,0 @@ -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.1 diff --git a/examples/helm-copy-secret/render.sh b/examples/helm-copy-secret/render.sh index 7b8a6dc..93012f8 100755 --- a/examples/helm-copy-secret/render.sh +++ b/examples/helm-copy-secret/render.sh @@ -1,4 +1,3 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -#exec crossplane render --include-full-xr --include-function-results xr.yaml composition.yaml functions.yaml exec function-pythonic render --python-path=. --render-unknowns --include-full-xr --include-function-results xr.yaml composition.yaml diff --git a/examples/helm-copy-secret/run-function.sh b/examples/helm-copy-secret/run-function.sh deleted file mode 100755 index e16d311..0000000 --- a/examples/helm-copy-secret/run-function.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -cd $(dirname $(realpath $0)) -PYTHONPATH=$PWD exec hatch run development diff --git a/examples/helm-copy-secret/vcluster.py b/examples/helm-copy-secret/vcluster.py index 8187837..689295c 100644 --- a/examples/helm-copy-secret/vcluster.py +++ b/examples/helm-copy-secret/vcluster.py @@ -7,15 +7,15 @@ def compose(self): namespace = name secret_name = f'vc-{name}' - release = self.resources.release('helm.crossplane.io/v1beta1', 'Release', name=name) + release = self.resources.release('Release', 'helm.crossplane.io/v1beta1', name=name) release.spec.rollbackLimit = 1 release.spec.forProvider.chart.repository = 'https://charts.loft.sh' release.spec.forProvider.chart.name = 'vcluster' release.spec.forProvider.chart.version = '0.26.0' release.spec.forProvider.namespace = namespace release.spec.forProvider.values.controlPlane.proxy.extraSANs[0] = f'{name}.{namespace}' - vcluster_secret = self.requireds.vcluster_secret('v1', 'Secret', namespace, secret_name)[0] - argocd_secret = self.resources.argocd_secret('v1', 'Secret', 'argocd', secret_name) + vcluster_secret = self.requireds.vcluster_secret('Secret', 'v1', namespace, secret_name)[0] + argocd_secret = self.resources.argocd_secret('Secret', 'v1', 'argocd', secret_name) argocd_secret.metadata.labels['argocd.argoproj.io/secret-type'] = 'cluster' argocd_secret.type = 'Opaque' argocd_secret.data.name = B64Encode(name) diff --git a/examples/import-existing-vpc/composition.yaml b/examples/import-existing-vpc/composition.yaml index afefba1..3136e86 100644 --- a/examples/import-existing-vpc/composition.yaml +++ b/examples/import-existing-vpc/composition.yaml @@ -27,7 +27,7 @@ spec: vpc = await self.compose_vpc() async def compose_vpc(self): - vpc = self.resources.VPC('ec2.aws.m.upbound.io/v1beta1', 'VPC') + vpc = self.resources.VPC('VPC', 'ec2.aws.m.upbound.io/v1beta1') vpc.spec.forProvider( region = self.spec.region, cidrBlock = self.spec.cidr, diff --git a/examples/import-existing-vpc/functions.yaml b/examples/import-existing-vpc/functions.yaml deleted file mode 100644 index 2febaea..0000000 --- a/examples/import-existing-vpc/functions.yaml +++ /dev/null @@ -1,8 +0,0 @@ -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.1 diff --git a/examples/import-existing-vpc/render.sh b/examples/import-existing-vpc/render.sh index d0dbb1b..7f93d0c 100755 --- a/examples/import-existing-vpc/render.sh +++ b/examples/import-existing-vpc/render.sh @@ -1,7 +1,4 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -#exec crossplane render xr.yaml composition.yaml functions.yaml -#exec crossplane render --observed-resources=observed.yaml 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/run-all.sh b/examples/run-all.sh new file mode 100755 index 0000000..05770c8 --- /dev/null +++ b/examples/run-all.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +cd $(dirname $(realpath $0)) +find . -name render.sh -exec bash -c 'echo "======================= {} ===========================" && {}' \; diff --git a/examples/single-purpose/functions.yaml b/examples/single-purpose/functions.yaml deleted file mode 100644 index caa6091..0000000 --- a/examples/single-purpose/functions.yaml +++ /dev/null @@ -1,8 +0,0 @@ -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.1 diff --git a/examples/single-purpose/render.sh b/examples/single-purpose/render.sh index 6cf4f23..2c56016 100755 --- a/examples/single-purpose/render.sh +++ b/examples/single-purpose/render.sh @@ -1,4 +1,3 @@ #!/usr/bin/env bash cd $(dirname $(realpath $0)) -#exec crossplane render xr.yaml ../../package/composite-composition.yaml functions.yaml exec function-pythonic render xr.yaml diff --git a/examples/usages-extra/composition.yaml b/examples/usages-extra/composition.yaml index 38dd4d3..aeb8a36 100644 --- a/examples/usages-extra/composition.yaml +++ b/examples/usages-extra/composition.yaml @@ -18,8 +18,8 @@ spec: class UsagesComposite(BaseComposite): def compose(self): self.usages = True - vpc = self.requireds.VPC('ec2.aws.crossplane.io/v1beta1', 'VPC', name='usages-vpc')[0] - subnet = self.resources.SubnetA('ec2.aws.crossplane.io/v1beta1', 'Subnet') + vpc = self.requireds.VPC('VPC', 'ec2.aws.crossplane.io/v1beta1', name='usages-vpc')[0] + subnet = self.resources.SubnetA('Subnet', 'ec2.aws.crossplane.io/v1beta1') subnet.spec.forProvider.region = 'us-east-1' subnet.spec.forProvider.vpcId = vpc.status.atProvider.vpcId subnet.spec.forProvider.availabilityZone = 'us-east-1a' diff --git a/examples/usages-extra/functions.yaml b/examples/usages-extra/functions.yaml deleted file mode 100644 index e153d1b..0000000 --- a/examples/usages-extra/functions.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: pkg.crossplane.io/v1 -kind: Function -metadata: - name: function-pythonic - annotations: - # 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.3.1 diff --git a/examples/usages-extra/render.sh b/examples/usages-extra/render.sh index 00e1388..b1714a4 100755 --- a/examples/usages-extra/render.sh +++ b/examples/usages-extra/render.sh @@ -1,4 +1,3 @@ #!/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 --required-resources=extraResources.yaml --observed-resources=observedResources.yaml xr.yaml composition.yaml diff --git a/pyproject.toml b/pyproject.toml index ed41ef9..fa8b496 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,8 @@ dynamic = ["version"] dependencies = [ "crossplane-function-sdk-python==0.11.0", + "inflect==7.5.0", + "kr8s==0.20.14", "pyyaml==6.0.3", ] diff --git a/scripts/setup-local.sh b/scripts/setup-local.sh index feeaf0f..6371717 100755 --- a/scripts/setup-local.sh +++ b/scripts/setup-local.sh @@ -199,7 +199,7 @@ spec: EOF # package: ghcr.io/iciclespider/function-pythonic:v0.0.0-20260115045752-3c0cf4ebffd2 -# package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.1 +# package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.4.0 kubectl apply -f - <