diff --git a/README.md b/README.md index 27b014e..e462a7e 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 ``` ## Composed Resource Dependencies @@ -204,8 +204,9 @@ The BaseComposite class provides the following fields for manipulating the Compo | self.status | Map | The composite desired and observed status, read from observed if not in desired | | self.conditions | Conditions | The composite desired and observed conditions, read from observed if not in desired | | self.results | Results | Returned results applied to the Composite and optionally on the Claim | -| self.connection | Connection | The composite desired and observed connection detials, read from observed if not in desired | +| self.connection | Map | The composite desired connection detials | | self.ready | Boolean | The composite desired ready state | +| self.observed.connection | Map | The composite observed connection detials | The BaseComposite also provides access to the following Crossplane Function level features: @@ -246,7 +247,7 @@ Resource class: | Resource.data | Map | The resource data | | Resource.status | Map | The resource status | | Resource.conditions | Conditions | The resource conditions | -| Resource.connection | Connection | The resource connection details | +| 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.usages | Boolean | Generate Crossplane Usages for this resource, default is Composite.autoReady | @@ -286,6 +287,7 @@ Each resource in the list is the following RequiredResource class: | RequiredResource.data | Map | The required resource data | | RequiredResource.status | Map | The required resource status | | RequiredResource.conditions | Map | The required resource conditions | +| RequiredResource.connection | Map | The required resource connection details | ### Conditions @@ -348,11 +350,11 @@ $ pip install crossplane-function-pythonic Then to render function-pythonic Compositions, use the `function-pythonic render ...` command. ```shell -$ function-pythonic render --help +$ 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] [--extra-resources PATH] [--required-resources PATH] - [--function-credentials PATH] [--include-full-xr] [--include-function-results] [--include-context] + [--observed-resources PATH] [--required-resources PATH] [--secret-store PATH] [--include-full-xr] + [--include-connection-xr] [--include-function-results] [--include-context] PATH [PATH/CLASS] positional arguments: @@ -376,14 +378,14 @@ options: Context key-value pairs to pass to the Function pipeline. Values must be YAML/JSON. Keys take precedence over --context-files. --observed-resources, -o PATH A YAML file or directory of YAML files specifying the observed state of composed resources. - --extra-resources PATH - A YAML file or directory of YAML files specifying required resources (deprecated, use --required-resources). --required-resources, -e PATH A YAML file or directory of YAML files specifying required resources to pass to the Function pipeline. - --function-credentials PATH - A YAML file or directory of YAML files specifying credentials to use for Functions to render the XR. + --secret-store, -s PATH + A YAML file or directory of YAML files specifying Secrets to use to resolve connections and credentials. --include-full-xr, -x Include a direct copy of the input XR's spedc and metadata fields in the rendered output. + --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-context, -c @@ -521,7 +523,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 runtimeConfigRef: name: function-pythonic --- diff --git a/crossplane/pythonic/composite.py b/crossplane/pythonic/composite.py index 6b7dcd5..7a56b16 100644 --- a/crossplane/pythonic/composite.py +++ b/crossplane/pythonic/composite.py @@ -40,6 +40,7 @@ def __init__(self, request, single_use, logger): observed = self.request.observed.composite desired = self.response.desired.composite self.observed = observed.resource + self.observed._set_attribute('connection', self.request.observed.composite.connection_details) self.desired = desired.resource self.apiVersion = self.observed.apiVersion self.kind = self.observed.kind @@ -434,6 +435,7 @@ def __init__(self, name, ix, resource): self.data = self.observed.data self.status = self.observed.status self.conditions = Conditions(resource) + self.connection = self.observed.connection_details def __bool__(self): return bool(self.observed) diff --git a/crossplane/pythonic/protobuf.py b/crossplane/pythonic/protobuf.py index 19c2292..bc0b869 100644 --- a/crossplane/pythonic/protobuf.py +++ b/crossplane/pythonic/protobuf.py @@ -58,17 +58,21 @@ def B64Decode(string): class Message: def __init__(self, parent, key, descriptor, message=_Unknown, readOnly=False): - self.__dict__['_parent'] = parent - self.__dict__['_key'] = key - self.__dict__['_descriptor'] = descriptor - self.__dict__['_message'] = message - self.__dict__['_readOnly'] = readOnly - self.__dict__['_cache'] = {} + self._set_attribute('_parent', parent) + self._set_attribute('_key', key) + self._set_attribute('_descriptor', descriptor) + self._set_attribute('_message', message) + self._set_attribute('_readOnly', readOnly) + self._set_attribute('_cache', {}) + + def _set_attribute(self, key, value): + self.__dict__[key] = value 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] field = self._descriptor.fields_by_name.get(key) @@ -156,6 +160,7 @@ def _fullName(self, key=None): def _create_child(self, key): if self._readOnly: raise ValueError(f"{self._readOnly} is read only") + key = self._validate_key(key) if self._message is _Unknown: self.__dict__['_message'] = self._parent._create_child(self._key) return getattr(self._message, key) @@ -177,6 +182,7 @@ def __setattr__(self, key, value): def __setitem__(self, key, value): if self._readOnly: raise ValueError(f"{self._readOnly} is read only") + key = self._validate_key(key) if key not in self._descriptor.fields_by_name: raise AttributeError(obj=self, name=key) field = self._descriptor.fields_by_name[key] @@ -201,26 +207,40 @@ def __delattr__(self, key): def __delitem__(self, key): if self._readOnly: raise ValueError(f"{self._readOnly} is read only") + key = self._validate_key(key) if key not in self._descriptor.fields_by_name: raise AttributeError(obj=self, name=key) if self._message is not _Unknown: self._message.ClearField(key) self._cache.pop(key, None) + def _validate_key(self, key): + if isinstance(key, FieldMessage): + key = key._value + elif isinstance(key, Value): + key = key._raw + if not isinstance(key, str): + raise TypeError(f"Unexpected key type: {key.__class__}") + return key + class MapMessage: def __init__(self, parent, key, field, messages=_Unknown, readOnly=False): - self.__dict__['_parent'] = parent - self.__dict__['_key'] = key - self.__dict__['_field'] = field - self.__dict__['_messages'] = messages - self.__dict__['_readOnly'] = readOnly - self.__dict__['_cache'] = {} + self._set_attribute('_parent', parent) + self._set_attribute('_key', key) + self._set_attribute('_field', field) + self._set_attribute('_messages', messages) + self._set_attribute('_readOnly', readOnly) + self._set_attribute('_cache', {}) + + def _set_attribute(self, key, value): + self.__dict__[key] = value 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 self._messages is _Unknown or key not in self._messages: @@ -304,6 +324,7 @@ def _fullName(self, key=None): def _create_child(self, key): if self._readOnly: raise ValueError(f"{self._readOnly} is read only") + key = self._validate_key(key) if self._messages is _Unknown: self.__dict__['_messages'] = self._parent._create_child(self._key) return self._messages[key] @@ -325,6 +346,7 @@ def __setattr__(self, key, message): def __setitem__(self, key, message): if self._readOnly: raise ValueError(f"{self._readOnly} is read only") + key = self._validate_key(key) if self._messages is _Unknown: self._messages = self._parent._create_child(self._key) if isinstance(message, Message): @@ -349,11 +371,21 @@ def __delattr__(self, key): def __delitem__(self, key): if self._readOnly: raise ValueError(f"{self._readOnly} is read only") + key = self._validate_key(key) if self._messages is not _Unknown: if key in self._messages: del self._messages[key] self._cache.pop(key, None) + def _validate_key(self, key): + if isinstance(key, FieldMessage): + key = key._value + elif isinstance(key, Value): + key = key._raw + if not isinstance(key, str): + raise TypeError(f"Unexpected key type: {key.__class__}") + return key + class RepeatedMessage: def __init__(self, parent, key, field, messages=_Unknown, readOnly=False): @@ -365,6 +397,7 @@ def __init__(self, parent, key, field, messages=_Unknown, readOnly=False): self._cache = {} def __getitem__(self, key): + key = self._validate_key(key) if key in self._cache: return self._cache[key] if self._messages is _Unknown or key >= len(self._messages): @@ -447,6 +480,7 @@ def _fullName(self, key=None): def _create_child(self, key): if self._readOnly: raise ValueError(f"{self._readOnly} is read only") + key = self._validate_key(key) if self._messages is _Unknown: self.__dict__['_messages'] = self._parent._create_child(self._key) if key == append: @@ -471,6 +505,7 @@ def __call__(self, *args): def __setitem__(self, key, message): if self._readOnly: raise ValueError(f"{self._readOnly} is read only") + key = self._validate_key(key) if self._messages is _Unknown: self._messages = self._parent._create_child(self._key) if key < 0: @@ -499,6 +534,7 @@ def __setitem__(self, key, message): def __delitem__(self, key): if self._readOnly: raise ValueError(f"{self._readOnly} is read only") + key = self._validate_key(key) if self._messages is not _Unknown: del self._messages[key] self._cache.pop(key, None) @@ -514,13 +550,22 @@ def append(self, message=_Unknown): message = self._messages.append(message) return self[len(self._messages) - 1] + def _validate_key(self, key): + if isinstance(key, FieldMessage): + key = key._value + elif isinstance(key, Value): + key = key._raw + if not isinstance(key, int): + raise TypeError(f"Unexpected key type: {key.__class__}") + return key + class FieldMessage: def __init__(self, parent, key, kind, value): - self.__dict__['_parent'] = parent - self.__dict__['_key'] = key - self.__dict__['_kind'] = kind - self.__dict__['_value'] = value + self._parent = parent + self._key = key + self._kind = kind + self._value = value def __bool__(self): return bool(self._value) @@ -539,7 +584,14 @@ def __eq__(self, other): return self._value == other._value return self._value == other + def __bytes__(self): + if isinstance(self._value, str): + return self._value.encode('utf-8') + return bytes(self._value) + def __str__(self): + if isinstance(self._value, bytes): + return self._value.decode('utf-8') return str(self._value) def __format__(self, spec=''): @@ -576,16 +628,16 @@ def _protobuf_value(self): class Value: def __init__(self, parent, key, value=_Unknown, readOnly=None): - self.__dict__['_parent'] = parent - self.__dict__['_key'] = key - self.__dict__['_dependencies'] = {} - self.__dict__['_unknowns'] = {} - self.__dict__['_cache'] = {} - self.__dict__['_readOnly'] = None + self._set_attribute('_parent', parent) + self._set_attribute('_key', key) + self._set_attribute('_dependencies', {}) + self._set_attribute('_unknowns', {}) + self._set_attribute('_cache', {}) + self._set_attribute('_readOnly', None) if isinstance(value, (google.protobuf.struct_pb2.Value, google.protobuf.struct_pb2.Struct, google.protobuf.struct_pb2.ListValue)) or value is _Unknown: - self.__dict__['_value'] = value + self._set_attribute('_value', value) else: - self.__dict__['_value'] = google.protobuf.struct_pb2.Value() + self._set_attribute('_value', google.protobuf.struct_pb2.Value()) if value is None: self._value.null_value = 0 elif isinstance(value, dict): @@ -604,12 +656,16 @@ def __init__(self, parent, key, value=_Unknown, readOnly=None): self._value.string_value = value else: raise ValueError(f"Unexpected Value type: {value.__class__}") - self.__dict__['_readOnly'] = readOnly + self._set_attribute('_readOnly', readOnly) + + def _set_attribute(self, key, value): + self.__dict__[key] = value 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: @@ -641,7 +697,7 @@ def __getitem__(self, key): case _: raise ValueError(f"Invalid key \"{key}\" for kind: {self._kind}") else: - raise ValueError(f"Unexpected key type: {key.__class__}") + raise NotImplementedError() value = Value(self, key, value, self._readOnly) self._cache[key] = value return value @@ -860,6 +916,7 @@ def __setattr__(self, key, value): def __setitem__(self, key, value): if self._readOnly: raise ValueError(f"{self._readOnly} is read only") + key = self._validate_key(key) if isinstance(key, str): if self._ensure_map() == 'struct_value': values = self._value.struct_value.fields @@ -877,7 +934,7 @@ def __setitem__(self, key, value): while key >= len(values): values.add() else: - raise ValueError('Unexpected key type') + raise NotImplementedError() self._cache.pop(key, None) self._dependencies.pop(key, None) self._unknowns.pop(key, None) @@ -887,6 +944,8 @@ def __setitem__(self, key, value): values[key].null_value = 0 elif isinstance(value, bool): # Must be before int check values[key].bool_value = value + elif isinstance(value, bytes): + values[key].string_value = value._value.decode('utf-8') elif isinstance(value, str): values[key].string_value = value elif isinstance(value, (int, float)): @@ -995,6 +1054,7 @@ def __delitem__(self, key): kind = self._kind if kind == 'Unknown': return + key = self._validate_key(key) if isinstance(key, str): match kind: case 'struct_value': @@ -1036,11 +1096,12 @@ def __delitem__(self, key): break del values[ix] else: - raise ValueError('Unexpected key type') + raise NotImplementedError() def _create_child(self, key): if self._readOnly: raise ValueError(f"{self._readOnly} is read only") + key = self._validate_key(key) if isinstance(key, str): if self._ensure_map() == 'struct_value': fields = self._value.struct_value.fields @@ -1061,7 +1122,16 @@ def _create_child(self, key): values.add() values[key].Clear() return values[key] - raise ValueError('Unexpected key type') + raise NotImplementedError() + + def _validate_key(self, key): + if isinstance(key, FieldMessage): + key = key._value + elif isinstance(key, Value): + key = key._raw + if not isinstance(key, (str, int)): + raise TypeError(f"Unexpected key type: {key.__class__}") + return key def _ensure_map(self): kind = self._kind diff --git a/crossplane/pythonic/render.py b/crossplane/pythonic/render.py index 9669690..76e02e3 100644 --- a/crossplane/pythonic/render.py +++ b/crossplane/pythonic/render.py @@ -53,14 +53,6 @@ def add_parser_arguments(cls, parser): metavar='PATH', help='A YAML file or directory of YAML files specifying the observed state of composed resources.' ) - parser.add_argument( - '--extra-resources', - action='append', - type=pathlib.Path, - default=[], - metavar='PATH', - help='A YAML file or directory of YAML files specifying required resources (deprecated, use --required-resources).', - ) parser.add_argument( '--required-resources', '-e', action='append', @@ -70,18 +62,23 @@ def add_parser_arguments(cls, parser): help='A YAML file or directory of YAML files specifying required resources to pass to the Function pipeline.', ) parser.add_argument( - '--function-credentials', + '--secret-store', '-s', action='append', type=pathlib.Path, default=[], metavar='PATH', - help='A YAML file or directory of YAML files specifying credentials to use for Functions to render the XR.', + help='A YAML file or directory of YAML files specifying Secrets to use to resolve connections and credentials.', ) parser.add_argument( '--include-full-xr', '-x', action='store_true', help="Include a direct copy of the input XR's spedc and metadata fields in the rendered output.", ) + parser.add_argument( + '--include-connection-xr', + action='store_true', + help="Include the Composite connection values in the rendered output as a resource of kind: Connection.", + ) parser.add_argument( '--include-function-results', '-r', action='store_true', @@ -158,22 +155,26 @@ async def run(self): sys.exit(1) request.context[key_value[0]] = protobuf.Yaml(key_value[1]) - # Establish the request observed composite and specifed observed resources. - request.observed.composite.resource = composite + # Collect specified required/extra resources. Sort for stable order when processed. + requireds = sorted( + self.collect_resources(self.args.required_resources), + key=lambda required: str(resource.metadata.name), + ) + + # 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: - request.observed.resources[str(name)].resource = resource - - # Collect specified required/extra resources. - requireds = [resource for resource in self.collect_resources(self.args.required_resources)] - requireds += [resource for resource in self.collect_resources(self.args.extra_resources)] - - # Collect specified credential secrets. - credentials = [] - for credential in self.collect_resources(self.args.function_credentials): - if credential.apiVersion == 'v1' and credential.kind == 'Secret': - credentials.append(credential) + self.setup_resource(resource, secrets, request.observed.resources[name]) # These will hold the response conditions and results. conditions = protobuf.List() @@ -194,18 +195,21 @@ async def run(self): # Supply step requested credentials. request.credentials() - for fn_credential in step.credentials: - if fn_credential.source == 'Secret' and fn_credential.secretRef: - for credential in credentials: - if credential.metadata.namespace == fn_credential.secretRef.namespace and credential.metadata.name == fn_credential.secretRef.name: - data = request.credentials[str(fn_credential.name)].credential_data.data - data() - for key, value in credential.data: - data[key] = protobuf.B64Decode(value) - break - else: - print(f"Step \"{step.step}\" secret not found: {fn_credential.secretRef.namespace} {fn_credential.secretRef.name}", file=sys.stderr) - sys.exit(1) + 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: + if secret.metadata.namespace == namespace and secret.metadata.name == name: + data = request.credentials[credential.name].credential_data.data + data() + for key, value in secret.data: + data[key] = protobuf.B64Decode(value) + break + else: + print(f"Step \"{step.step}\" secret not found: {namespace}/{name}", file=sys.stderr) + sys.exit(1) # Track what extra/required resources have been processed. requirements = protobuf.Message(None, 'requirements', fnv1.Requirements.DESCRIPTOR, fnv1.Requirements()) @@ -213,14 +217,14 @@ async def run(self): # Fetch the step bootstrap resources specified. request.required_resources() for requirement in step.requirements: - self.fetch_requireds(requireds, requirement.requirementName, requirement, request.required_resources) + self.fetch_requireds(requireds, secrets, requirement.requirementName, requirement, request.required_resources) # Fetch the required resources requested. for name, selector in requirements.resources: - self.fetch_requireds(requireds, name, selector, request.required_resources) + self.fetch_requireds(requireds, secrets, name, selector, request.required_resources) # Fetch the now deprecated extra resources requested. request.extra_resources() for name, selector in requirements.extra_resources: - self.fetch_requireds(requireds, name, selector, request.extra_resources) + self.fetch_requireds(requireds, secrets, name, selector, request.extra_resources) # Run the step using the function-pythonic function runner. response = protobuf.Message( None, @@ -330,15 +334,29 @@ async def run(self): # Print the composite. print('---') print(str(composite), end='') + + # Print Composite connection if requested. + if self.args.include_connection_xr: + connection = protobuf.Map( + apiVersion = 'render.crossplane.io/v1beta1', + kind = 'Connection', + ) + for key, value in request.desired.composite.connection_details: + connection.values[key] = value + print('---') + print(str(connection), end='') + # Print the composed resources. for resource in sorted(resources, key=lambda resource: str(resource.metadata.annotations['crossplane.io/composition-resource-name'])): print('---') print(str(resource), end='') + # Print the results (AKA events) if requested. if self.args.include_function_results: for result in results: print('---') print(str(result), end='') + # Print the final context if requested. if self.args.include_context: print('---') @@ -346,7 +364,7 @@ async def run(self): str(protobuf.Map( apiVersion = 'render.crossplane.io/v1beta1', kind = 'Context', - fields = request.context, + values = request.context, )), end='', ) @@ -382,7 +400,19 @@ def collect_resources(self, resources): for document in yaml.safe_load_all(file.read_text()): yield protobuf.Value(None, None, document) - def fetch_requireds(self, requireds, name, selector, resources): + def setup_resource(self, source, secrets, 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: + 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) + break + + def fetch_requireds(self, requireds, secrets, name, selector, resources): if not name: return name = str(name) @@ -391,13 +421,13 @@ def fetch_requireds(self, requireds, name, selector, resources): for required in requireds: if selector.api_version == required.apiVersion and selector.kind == required.kind: if selector.match_name == required.metadata.name: - items[protobuf.append].resource = required + 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: - items[protobuf.append].resource = required + self.setup_resource(required, secrets, items[protobuf.append]) def copy_resource(self, source, destination): destination.resource = source.resource diff --git a/examples/.dev/functions.yaml b/examples/.dev/functions.yaml index d161629..805adc2 100644 --- a/examples/.dev/functions.yaml +++ b/examples/.dev/functions.yaml @@ -10,4 +10,4 @@ metadata: render.crossplane.io/runtime: Development render.crossplane.io/runtime-development-target: localhost:9443 spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/aks-cluster/cluster-function-pythonic.yaml b/examples/aks-cluster/cluster-function-pythonic.yaml index 679ae1c..13b8574 100644 --- a/examples/aks-cluster/cluster-function-pythonic.yaml +++ b/examples/aks-cluster/cluster-function-pythonic.yaml @@ -3,7 +3,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 runtimeConfigRef: name: function-pythonic --- diff --git a/examples/aks-cluster/functions.yaml b/examples/aks-cluster/functions.yaml index e7492f7..e1aef3b 100644 --- a/examples/aks-cluster/functions.yaml +++ b/examples/aks-cluster/functions.yaml @@ -5,6 +5,6 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 runtimeConfigRef: name: function-pythonic diff --git a/examples/connections/composite-connection.yaml b/examples/connections/composite-connection.yaml new file mode 100644 index 0000000..e44e2ec --- /dev/null +++ b/examples/connections/composite-connection.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + namespace: default + name: composite-connection +data: + username: Y29tcG9zaXRlLXVzZXJuYW1l # composite-username + password: Y29tcG9zaXRlLXBhc3N3b3Jk # composite-password diff --git a/examples/connections/composition.yaml b/examples/connections/composition.yaml new file mode 100644 index 0000000..d8a07dc --- /dev/null +++ b/examples/connections/composition.yaml @@ -0,0 +1,39 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: pythonic-example-connection +spec: + compositeTypeRef: + apiVersion: example.pythonic.crossplane.io/v1alpha1 + kind: Connection + mode: Pipeline + pipeline: + - step: connections + functionRef: + name: function-pythonic + credentials: + - name: step + source: Secret + secretRef: + namespace: default + name: step-credential + input: + apiVersion: pythonic.fn.crossplane.io/v1alpha1 + kind: Composite + composite: | + class Composite(BaseComposite): + def compose(self): + rds = self.resources.rds('rds.aws.upbound.io/v1beta1', 'Instance') + rds.spec.forProvider.name = 'pythonic-example' + rds.spec.writeConnectionSecretToRef.namespace = 'default' + rds.spec.writeConnectionSecretToRef.name = 'rds-connection' + + self.connection['self-username'] = 'self-username' + self.connection['self-password'] = 'self-password' + for name, credential in self.credentials: + for key, value in credential: + self.connection[f"{name}-{key}"] = value + for key, value in self.observed.connection: + self.connection[f"composite-{key}"] = value + for key, value in rds.connection: + self.connection[f"rds-{key}"] = value diff --git a/examples/connections/rds-connection.yaml b/examples/connections/rds-connection.yaml new file mode 100644 index 0000000..ffa43d3 --- /dev/null +++ b/examples/connections/rds-connection.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + namespace: default + name: rds-connection +data: + username: cmRzLXVzZXJuYW1l # rds-username + password: cmRzLXBhc3N3b3Jk # rds-password diff --git a/examples/connections/rds-observed.yaml b/examples/connections/rds-observed.yaml new file mode 100644 index 0000000..b4e2fcd --- /dev/null +++ b/examples/connections/rds-observed.yaml @@ -0,0 +1,21 @@ +apiVersion: rds.aws.upbound.io/v1beta1 +kind: Instance +metadata: + annotations: + crossplane.io/composition-resource-name: rds + generateName: composite-example- + labels: + crossplane.io/composite: composite-example + ownerReferences: + - apiVersion: pythonic.crossplane.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Composite + name: composite-example + uid: '' +spec: + forProvider: + name: pythonic-example + writeConnectionSecretToRef: + namespace: default + name: rds-connection diff --git a/examples/connections/render.sh b/examples/connections/render.sh new file mode 100755 index 0000000..9ae8188 --- /dev/null +++ b/examples/connections/render.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +cd $(dirname $(realpath $0)) +exec function-pythonic render xr.yaml composition.yaml \ + --debug \ + --observed-resources rds-observed.yaml \ + --secret-store step-credential.yaml \ + --secret-store composite-connection.yaml \ + --secret-store rds-connection.yaml \ + --include-connection-xr diff --git a/examples/connections/step-credential.yaml b/examples/connections/step-credential.yaml new file mode 100644 index 0000000..0e46ed3 --- /dev/null +++ b/examples/connections/step-credential.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + namespace: default + name: step-credential +data: + username: c3RlcC11c2VybmFtZQ== # step-username + password: c3RlcC1wYXNzd29yZA== # step-password diff --git a/examples/connections/xr.yaml b/examples/connections/xr.yaml new file mode 100644 index 0000000..1083c99 --- /dev/null +++ b/examples/connections/xr.yaml @@ -0,0 +1,8 @@ +apiVersion: example.pythonic.crossplane.io/v1alpha1 +kind: Connection +metadata: + name: example-connection +spec: + writeConnectionSecretToRef: + namespace: default + name: composite-connection diff --git a/examples/eks-cluster/functions.yaml b/examples/eks-cluster/functions.yaml index fa1ec3d..2f532ec 100644 --- a/examples/eks-cluster/functions.yaml +++ b/examples/eks-cluster/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/filing-system/function.yaml b/examples/filing-system/function.yaml index 56cf80d..2e4f1c6 100644 --- a/examples/filing-system/function.yaml +++ b/examples/filing-system/function.yaml @@ -3,7 +3,7 @@ kind: Function metadata: name: function-pythonic spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 runtimeConfigRef: apiVersion: pkg.crossplane.io/v1beta1 kind: DeploymentRuntimeConfig diff --git a/examples/function-go-templating/conditions/functions.yaml b/examples/function-go-templating/conditions/functions.yaml index 4dedf69..7b312d9 100644 --- a/examples/function-go-templating/conditions/functions.yaml +++ b/examples/function-go-templating/conditions/functions.yaml @@ -7,4 +7,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-go-templating/context/functions.yaml b/examples/function-go-templating/context/functions.yaml index 4dedf69..7b312d9 100644 --- a/examples/function-go-templating/context/functions.yaml +++ b/examples/function-go-templating/context/functions.yaml @@ -7,4 +7,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-go-templating/functions/fromYaml/functions.yaml b/examples/function-go-templating/functions/fromYaml/functions.yaml index 53ce153..bb9f4c2 100644 --- a/examples/function-go-templating/functions/fromYaml/functions.yaml +++ b/examples/function-go-templating/functions/fromYaml/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-go-templating/functions/getComposedResource/functions.yaml b/examples/function-go-templating/functions/getComposedResource/functions.yaml index 53ce153..bb9f4c2 100644 --- a/examples/function-go-templating/functions/getComposedResource/functions.yaml +++ b/examples/function-go-templating/functions/getComposedResource/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-go-templating/functions/getCompositeResource/functions.yaml b/examples/function-go-templating/functions/getCompositeResource/functions.yaml index 53ce153..bb9f4c2 100644 --- a/examples/function-go-templating/functions/getCompositeResource/functions.yaml +++ b/examples/function-go-templating/functions/getCompositeResource/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-go-templating/functions/getCredentialData/composition.yaml b/examples/function-go-templating/functions/getCredentialData/composition.yaml index 87b717f..b969f5f 100644 --- a/examples/function-go-templating/functions/getCredentialData/composition.yaml +++ b/examples/function-go-templating/functions/getCredentialData/composition.yaml @@ -12,11 +12,11 @@ spec: functionRef: name: function-pythonic credentials: - - name: foo-creds - secretRef: - name: foo-creds - namespace: default - source: Secret + - name: foo-creds + source: Secret + secretRef: + namespace: default + name: foo-creds input: apiVersion: pythonic.fn.crossplane.io/v1alpha1 kind: Composite diff --git a/examples/function-go-templating/functions/getCredentialData/functions.yaml b/examples/function-go-templating/functions/getCredentialData/functions.yaml index 53ce153..bb9f4c2 100644 --- a/examples/function-go-templating/functions/getCredentialData/functions.yaml +++ b/examples/function-go-templating/functions/getCredentialData/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-go-templating/functions/getCredentialData/render.sh b/examples/function-go-templating/functions/getCredentialData/render.sh index 8508fbd..7227e1d 100755 --- a/examples/function-go-templating/functions/getCredentialData/render.sh +++ b/examples/function-go-templating/functions/getCredentialData/render.sh @@ -1,4 +1,4 @@ #!/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 --function-credentials=credentials.yaml --include-context xr.yaml composition.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 index 53ce153..bb9f4c2 100644 --- a/examples/function-go-templating/functions/getResourceCondition/functions.yaml +++ b/examples/function-go-templating/functions/getResourceCondition/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-go-templating/functions/include/functions.yaml b/examples/function-go-templating/functions/include/functions.yaml index 53ce153..bb9f4c2 100644 --- a/examples/function-go-templating/functions/include/functions.yaml +++ b/examples/function-go-templating/functions/include/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-go-templating/functions/toYaml/functions.yaml b/examples/function-go-templating/functions/toYaml/functions.yaml index 53ce153..bb9f4c2 100644 --- a/examples/function-go-templating/functions/toYaml/functions.yaml +++ b/examples/function-go-templating/functions/toYaml/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-go-templating/inline/functions.yaml b/examples/function-go-templating/inline/functions.yaml index 53ce153..bb9f4c2 100644 --- a/examples/function-go-templating/inline/functions.yaml +++ b/examples/function-go-templating/inline/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-go-templating/recursive/functions.yaml b/examples/function-go-templating/recursive/functions.yaml index 53ce153..bb9f4c2 100644 --- a/examples/function-go-templating/recursive/functions.yaml +++ b/examples/function-go-templating/recursive/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-sequencer/composition.yaml b/examples/function-sequencer/composition.yaml new file mode 100644 index 0000000..c0aac2b --- /dev/null +++ b/examples/function-sequencer/composition.yaml @@ -0,0 +1,48 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: function-sequencer +spec: + compositeTypeRef: + apiVersion: example.crossplane.io/v1 + kind: XR + mode: Pipeline + pipeline: + + - step: create-resources + functionRef: + name: function-pythonic + input: + apiVersion: pythonic.fn.crossplane.io/v1alpha1 + kind: Composite + composite: | + class Composite(BaseComposite): + + def compose(self): + self.create_resource('first', (5, False), (10, True), (30, False), (90, True)) + self.create_resource('second', (5, False), (10, True)) + 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') + for ix, (seconds, status) in enumerate(conditions): + resource.spec.forProvider.conditionAfter[ix]( + time=f"{seconds}s", + conditionType='Ready', + conditionStatus=str(status), + ) + + - step: sequencer + functionRef: + name: function-pythonic + input: + apiVersion: pythonic.fn.crossplane.io/v1alpha1 + kind: Composite + composite: | + class Composite(BaseComposite): + def compose(self): + if not self.resources['first-resource'].ready: + for resource in ('second-resource', 'third-resource'): + if not self.resources[resource].observed: + del self.resources[resource] + self.results.info(message=f"Delaying creation of resource matching \"{resource}\" because \"first-resource\" is not fully ready") diff --git a/examples/function-sequencer/functions.yaml b/examples/function-sequencer/functions.yaml new file mode 100644 index 0000000..5aba4f5 --- /dev/null +++ b/examples/function-sequencer/functions.yaml @@ -0,0 +1,8 @@ +apiVersion: pkg.crossplane.io/v1 +kind: Function +metadata: + name: function-pythonic + annotations: + render.crossplane.io/runtime: Development +spec: + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/function-sequencer/observed.yaml b/examples/function-sequencer/observed.yaml new file mode 100644 index 0000000..ceb476e --- /dev/null +++ b/examples/function-sequencer/observed.yaml @@ -0,0 +1,27 @@ +apiVersion: nop.crossplane.io/v1alpha1 +kind: NopResource +metadata: + annotations: + crossplane.io/external-name: foo + crossplane.io/composition-resource-name: first-resource + name: first +spec: + forProvider: + conditionAfter: + - conditionStatus: "True" + conditionType: Ready + time: 1s + - conditionStatus: "False" + conditionType: Ready + time: 0s +status: + atProvider: {} + conditions: + - lastTransitionTime: "2024-02-17T11:56:27Z" + reason: ReconcileSuccess + status: "True" + type: Synced + - lastTransitionTime: "2024-02-17T11:56:28Z" + reason: "" + status: "True" + type: Ready diff --git a/examples/function-sequencer/render.sh b/examples/function-sequencer/render.sh new file mode 100755 index 0000000..9551eea --- /dev/null +++ b/examples/function-sequencer/render.sh @@ -0,0 +1,12 @@ +#!/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 \ + --include-function-results + +#exec function-pythonic render \ +# xr.yaml composition.yaml \ +# --observed-resources observed.yaml \ +# --include-function-results diff --git a/examples/function-sequencer/sequencer.py b/examples/function-sequencer/sequencer.py new file mode 100644 index 0000000..4866a71 --- /dev/null +++ b/examples/function-sequencer/sequencer.py @@ -0,0 +1,12 @@ +from crossplane.pythonic import BaseComposite + +class Composite(BaseComposite): + def compose(self): + for sequence in self.parameters: + for ix, resource in enueration(sequence): + if ix == 0: + continue + if self.resources[resource].observed: + continue + for before in sequence[:ix]: + diff --git a/examples/function-sequencer/xr.yaml b/examples/function-sequencer/xr.yaml new file mode 100644 index 0000000..25472b9 --- /dev/null +++ b/examples/function-sequencer/xr.yaml @@ -0,0 +1,6 @@ +# Replace this with your XR! +apiVersion: example.crossplane.io/v1 +kind: XR +metadata: + name: example-xr +spec: {} diff --git a/examples/get-started-app/functions.yaml b/examples/get-started-app/functions.yaml index b4f4a12..5aba4f5 100644 --- a/examples/get-started-app/functions.yaml +++ b/examples/get-started-app/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/helm-copy-secret/functions.yaml b/examples/helm-copy-secret/functions.yaml index b4f4a12..5aba4f5 100644 --- a/examples/helm-copy-secret/functions.yaml +++ b/examples/helm-copy-secret/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/import-existing-vpc/functions.yaml b/examples/import-existing-vpc/functions.yaml index fa1ec3d..2f532ec 100644 --- a/examples/import-existing-vpc/functions.yaml +++ b/examples/import-existing-vpc/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/single-purpose/functions.yaml b/examples/single-purpose/functions.yaml index b4f4a12..5aba4f5 100644 --- a/examples/single-purpose/functions.yaml +++ b/examples/single-purpose/functions.yaml @@ -5,4 +5,4 @@ metadata: annotations: render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/examples/usages-extra/functions.yaml b/examples/usages-extra/functions.yaml index 53ce153..bb9f4c2 100644 --- a/examples/usages-extra/functions.yaml +++ b/examples/usages-extra/functions.yaml @@ -6,4 +6,4 @@ metadata: # This tells crossplane beta render to connect to the function locally. render.crossplane.io/runtime: Development spec: - package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 + package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 diff --git a/scripts/setup-local.sh b/scripts/setup-local.sh index a1f1cda..41896c2 100755 --- a/scripts/setup-local.sh +++ b/scripts/setup-local.sh @@ -197,7 +197,7 @@ spec: EOF # package: ghcr.io/crossplane-contrib/function-pythonic:v0.0.0-20250819201108-49cfb066579f -# package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.0 +# package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1 kubectl apply -f - <