diff --git a/crossplane/function/dependency.py b/crossplane/function/dependency.py new file mode 100644 index 0000000..b575fe8 --- /dev/null +++ b/crossplane/function/dependency.py @@ -0,0 +1,629 @@ +# Copyright 2026 The Crossplane Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Dependencies declared by referring to another resource's fields. + +Providers resolve cross-resource references themselves, through fields like +vpcIdRef and vpcIdSelector that the provider turns into a value at reconcile +time. That works, but Crossplane never sees the relationship: it can't order +anything, and a provider that can't resolve a reference yet retries against +the cloud API until it can. + +Reading the value through this module instead makes the relationship +visible. The function says where a value comes from, the SDK records the +dependency, and Crossplane waits rather than letting the provider discover +the problem: + + from crossplane.function import dependency + + vpc = dependency.named("vpc", VPC) + + with dependency.composing(req, rsp, "subnet") as c: + c.update( + Subnet( + spec={ + "forProvider": { + "region": "us-east-1", + "vpcId": c.external_name(vpc), # instead of vpcIdSelector + "cidrBlock": c.ref(vpc.spec.forProvider.cidrBlock), + } + } + ) + ) + +composing scopes the work to one composed resource. Within it, c.ref and +c.external_name return the value itself, or None while it isn't available, +and the scope records that the resource depends on where each value came +from when the block exits. + +Dependencies order resources only on a Crossplane that advertises +CAPABILITY_DEPENDENCIES. On any other, a resource whose value isn't available +yet is held back by leaving it out of desired state, but nothing waits for a +source to be ready, and nothing orders deletion. +""" + +import contextlib +import types +import typing + +import pydantic +from google.protobuf import struct_pb2 as structpb + +from crossplane.function import request, resource, response +from crossplane.function.proto.v1 import run_function_pb2 as fnv1 + +_EXTERNAL_NAME = "@externalName" +_EXTERNAL_NAME_ANNOTATION = "crossplane.io/external-name" + +CONDITION_TYPE = "DependencyValuesAvailable" +"""The XR condition composing reports whether any resource was kept. + +False while a resource was kept at its current spec because a value it's +built from isn't available, naming each one. True otherwise. +""" + +_COMPOSED = "composed" +_REQUIRED = "required" + +M = typing.TypeVar("M", bound=pydantic.BaseModel) +V = typing.TypeVar("V") + + +class _Source(typing.NamedTuple): + """The resource a reference points at.""" + + kind: str # _COMPOSED or _REQUIRED. + name: str # A composed resource name, or a requirement name. + resource_name: str | None = None # Required only: one of the matches. + namespace: str | None = None # Required only: namespace of resource_name. + + def describe(self) -> str: + if self.kind == _COMPOSED: + return self.name + s = f"requirement {self.name}" + if self.resource_name: + ns = f"{self.namespace}/" if self.namespace else "" + s += f" ({ns}{self.resource_name})" + return s + + +class _Field: + """A stand-in for a resource, or a field of one, that records its path. + + Reading an attribute or an item returns another stand-in one step deeper. + Given the resource's model it follows the model's own fields, so a typo + fails here rather than producing a reference to nothing, and a field whose + JSON name isn't a valid Python name (from, class) is recorded under its + JSON name. + + It deliberately has no value. Anything that would need one - a truth + test, str(), iteration - raises, because code that branches on a field + that doesn't exist yet would otherwise take the wrong branch silently. + """ + + __slots__ = ("_model", "_path", "_source") + + def __init__(self, source: _Source, path: tuple, model: typing.Any = None): + object.__setattr__(self, "_source", source) + object.__setattr__(self, "_path", path) + object.__setattr__(self, "_model", model) + + def __getattr__(self, attr: str) -> "_Field": + # Dunder lookups come from copy, pickle, pydantic and friends probing + # for protocols. Answering them would make a stand-in look like it + # supports things it doesn't. + if attr.startswith("__"): + raise AttributeError(attr) + + key, model = _field_of(self._model, attr, self._describe()) + return _Field(self._source, (*self._path, key), model) + + def __getitem__(self, key: str | int) -> "_Field": + return _Field(self._source, (*self._path, key), _item_of(self._model)) + + def __setattr__(self, attr: str, value: typing.Any) -> None: + msg = f"cannot set {attr} on {self._describe()}: references are read-only" + raise AttributeError(msg) + + def __bool__(self) -> bool: + msg = ( + f"{self._describe()} has no value yet. Read it with c.ref() inside " + "dependency.composing(), and branch on what that returns." + ) + raise TypeError(msg) + + def __iter__(self) -> typing.NoReturn: + msg = f"cannot iterate {self._describe()}; index it instead, e.g. [0]" + raise TypeError(msg) + + def __str__(self) -> str: + msg = f"{self._describe()} has no value yet. Read it with c.ref()." + raise TypeError(msg) + + def __repr__(self) -> str: + return f"" + + def _describe(self) -> str: + path = "".join(f"[{p}]" if isinstance(p, int) else f".{p}" for p in self._path) + return f"{self._source.describe()}{path}" + + +def _unwrap(annotation: typing.Any) -> typing.Any: + """Strip Optional, unions with None, and Annotated from a field's type.""" + while True: + origin = typing.get_origin(annotation) + if origin is typing.Annotated: + annotation = typing.get_args(annotation)[0] + continue + if origin in (typing.Union, types.UnionType): + args = [a for a in typing.get_args(annotation) if a is not type(None)] + if len(args) == 1: + annotation = args[0] + continue + return annotation + + +def _is_model(t: typing.Any) -> bool: + return isinstance(t, type) and issubclass(t, pydantic.BaseModel) + + +def _field_of(model: typing.Any, attr: str, where: str) -> tuple[str, typing.Any]: + """Return the JSON key for attr on model, and the type of that field. + + Without a model there is nothing to check against, so the attribute is the + key and the result is untyped. + """ + if not _is_model(model): + return attr, None + + fields = model.model_fields + if attr in fields: + info = fields[attr] + return info.alias or attr, _unwrap(info.annotation) + + # Accept the JSON name too, so from and from_ both work. + for info in fields.values(): + if info.alias == attr: + return attr, _unwrap(info.annotation) + + msg = f"{where} has no field {attr!r}; {model.__name__} has {', '.join(fields)}" + raise AttributeError(msg) + + +def _item_of(model: typing.Any) -> typing.Any: + """Return the element type of a list or dict type, if it has one.""" + origin, args = typing.get_origin(model), typing.get_args(model) + if origin is list and args: + return _unwrap(args[0]) + if origin is dict and len(args) == 2: # noqa: PLR2004 + return _unwrap(args[1]) + return None + + +@typing.overload +def named(name: str, model: type[M]) -> M: ... +@typing.overload +def named(name: str, model: None = None) -> typing.Any: ... +def named(name: str, model: type[M] | None = None) -> typing.Any: + """Name a composed resource so its fields can be referenced. + + Args: + name: The composed resource's name, the key into desired and observed + state. + model: The resource's generated model, if it has one. Fields are then + checked against it as they're read, and editors can complete them. + + Returns: + A stand-in that reads like the resource itself: + + vpc = dependency.named("vpc", VPC) + + with dependency.composing(req, rsp, "subnet") as c: + vpc_id = c.ref(vpc.status.atProvider.id) + """ + return _Field(_Source(_COMPOSED, name), (), model) + + +@typing.overload +def named_required( + requirement_name: str, + model: type[M], + *, + name: str | None = None, + namespace: str | None = None, +) -> M: ... +@typing.overload +def named_required( + requirement_name: str, + model: None = None, + *, + name: str | None = None, + namespace: str | None = None, +) -> typing.Any: ... +def named_required( + requirement_name: str, + model: type[M] | None = None, + *, + name: str | None = None, + namespace: str | None = None, +) -> typing.Any: + """Name a resource the function requires but doesn't compose. + + Args: + requirement_name: The requirement name, as passed to + response.require_resources. + model: The resource's generated model, if it has one. + name: Name of one resource the requirement matched. Needed when it + can match more than one; a reference declines to guess. + namespace: Namespace of name, for a namespaced resource. + + Returns: + A stand-in that reads like the resource itself. + + Crossplane never deletes a resource it didn't compose, so the dependency + this records orders only creation and updates. + """ + return _Field(_Source(_REQUIRED, requirement_name, name, namespace), (), model) + + +_MISSING = object() + + +def _value_at(obj: typing.Any, path: list) -> typing.Any: + for key in path: + if isinstance(key, int): + if not isinstance(obj, list) or not -len(obj) <= key < len(obj): + return _MISSING + obj = obj[key] + elif isinstance(obj, dict) and key in obj: + obj = obj[key] + else: + return _MISSING + return obj + + +def _lookup(req: fnv1.RunFunctionRequest, s: _Source) -> typing.Any: + """Return the resource a reference points at, or _MISSING.""" + if s.kind == _COMPOSED: + if s.name not in req.observed.resources: + return _MISSING + return resource.struct_to_dict(req.observed.resources[s.name].resource) + + items = request.get_required_resources(req, s.name) + if s.resource_name is None: + # With several matches and nothing to pick by there is no right + # answer, so decline rather than guess. + return items[0] if len(items) == 1 else _MISSING + for item in items: + meta = item.get("metadata", {}) + if meta.get("name") == s.resource_name and ( + s.namespace is None or meta.get("namespace") == s.namespace + ): + return item + return _MISSING + + +def _read(req: fnv1.RunFunctionRequest, s: _Source, path: list) -> typing.Any: + """Return the value a reference points at, or _MISSING if there isn't one.""" + src = _lookup(req, s) + if src is _MISSING: + return _MISSING + if path == [_EXTERNAL_NAME]: + v = _value_at(src, ["metadata", "annotations", _EXTERNAL_NAME_ANNOTATION]) + if v is _MISSING: + v = _value_at(src, ["metadata", "name"]) + else: + v = _value_at(src, path) + return _MISSING if v is None else v + + +def _record( + rsp: fnv1.RunFunctionResponse, name: str, sources: typing.Iterable[_Source] +) -> None: + """Add a dependency of name on each source, skipping any already declared.""" + declared = { + (d.resource, d.WhichOneof("depends_on"), _target(d)) + for d in rsp.dependencies.items + } + for s in sorted(sources, key=lambda s: tuple(p or "" for p in s)): + if s.kind == _COMPOSED: + key = (name, "composed_resource", s.name) + if s.name == name or key in declared: + continue + response.add_dependency(rsp, name, s.name) + else: + key = (name, "required_resource", (s.name, s.resource_name, s.namespace)) + if key in declared: + continue + response.add_required_resource_dependency( + rsp, name, s.name, name=s.resource_name, namespace=s.namespace + ) + declared.add(key) + + +def _target(d: fnv1.Dependency) -> typing.Any: + if d.WhichOneof("depends_on") == "required_resource": + r = d.required_resource + return ( + r.requirement_name, + r.name if r.HasField("name") else None, + r.namespace if r.HasField("namespace") else None, + ) + return d.composed_resource + + +class Scope: + """Resolves references for one composed resource as they're read. + + Get one from composing. The scope knows which resource is asking, so a + reference resolves to its value as soon as it's read, in a field of any + type, and the scope records the dependency. + """ + + def __init__( + self, req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse, name: str + ): + """Create a Scope. Use composing rather than calling this directly.""" + self.req = req + self.rsp = rsp + self.name = name + self._sources: set[_Source] = set() + self._unresolved: list[str] = [] + self._kept = False + + def ref(self, value: V) -> V: + """Read a field of a named resource, and depend on that resource. + + Args: + value: A field read through a stand-in from named or + named_required, for example vpc.status.atProvider.id. + + Returns: + The field's observed value, or None if it isn't available yet. + None leaves the field out when the model is written to desired + state, and the dependency means Crossplane doesn't create this + resource until the value exists. + + A resource named with its model returns the value as the model + types it: a nested object comes back as its model, and a list of + them as a list of models, rather than as the JSON observed state + stores. Without a model the value is the JSON. + + Raises: + TypeError: value isn't a field of a named resource. + ValueError: value is the whole resource rather than a field of + it, or the observed value doesn't fit the model's type for it. + """ + if not isinstance(value, _Field): + msg = ( + "ref() expects a field of a resource from named() or named_required(), " + "for example c.ref(vpc.status.atProvider.id), " + f"not {type(value).__name__}" + ) + raise TypeError(msg) + if not value._path: + msg = ( + f"ref() needs a field, not the whole {value._describe()} resource. " + "Use external_name() to reference its external name." + ) + raise ValueError(msg) + v = self._get(value._source, list(value._path), value._describe()) + if v is None or value._model is None: + return typing.cast(V, v) + try: + return typing.cast(V, pydantic.TypeAdapter(value._model).validate_python(v)) + except pydantic.ValidationError as e: + msg = f"{value._describe()} doesn't match its model: {e}" + raise ValueError(msg) from e + + def external_name(self, named_resource: typing.Any) -> str | None: + """Read a named resource's external name, and depend on that resource. + + Args: + named_resource: A stand-in from named or named_required. + + Returns: + The external name, or None if the resource doesn't exist yet. + + Raises: + TypeError: named_resource isn't a stand-in for a resource. + """ + if not isinstance(named_resource, _Field) or named_resource._path: + msg = "external_name() expects a resource from named() or named_required()" + raise TypeError(msg) + return self._get( + named_resource._source, + [_EXTERNAL_NAME], + f"{named_resource._describe()} external name", + ) + + def update(self, source: dict | structpb.Struct | pydantic.BaseModel) -> None: + """Write this scope's composed resource to desired state. + + Args: + source: The resource, as resource.update accepts it. + + Writing it with resource.update, or any helper that calls it, works + the same way. + """ + resource.update(self.rsp.desired.resources[self.name], source) + + def _get(self, s: _Source, path: list, what: str) -> typing.Any: + self._sources.add(s) + v = _read(self.req, s, path) + if v is _MISSING: + self._unresolved.append(what) + return None + return v + + def _close(self) -> None: + exists = self.name in self.req.observed.resources + + # In a scope None is what a reference returns for a value that isn't + # available yet, and sending it would ask the API server to clear the + # field. Strip it however the resource was written: through c.update, + # resource.update, or a helper that calls it. + if self.name in self.rsp.desired.resources: + r = self.rsp.desired.resources[self.name] + body = resource.struct_to_dict(r.resource) + cleaned = _without_none(body) + if cleaned != body: + r.resource.CopyFrom(resource.dict_to_struct(cleaned)) + + if self._unresolved: + if exists: + self._keep_current_spec() + elif not request.has_capability(self.req, fnv1.CAPABILITY_DEPENDENCIES): + # Crossplane won't create the resource until the dependency + # is ready. One that doesn't enforce dependencies would create + # it without the field, so hold it back by leaving it out. + self.rsp.desired.resources.pop(self.name, None) + + # Declare dependencies only for a resource that's composed or exists. + # A scope that composed nothing has nothing to order, and Crossplane + # would ignore the dependencies, with an event saying so. + if self.name in self.rsp.desired.resources or exists: + _record(self.rsp, self.name, self._sources) + + _report(self.rsp, self.name, self._unresolved if self._kept else []) + + def _keep_current_spec(self) -> None: + """Keep an existing resource's spec while a reference it needs is gone. + + The fields that refer to what's missing came back None and were left + out, and applying that would unset them. A function that didn't + compose the resource at all, because the value it needed wasn't + there, would have it deleted. Either way, keep what the resource + already has for anything this function doesn't set, and say so in + the CONDITION_TYPE condition. + """ + observed = resource.struct_to_dict( + self.req.observed.resources[self.name].resource + ) + if self.name in self.rsp.desired.resources: + r = self.rsp.desired.resources[self.name] + body = resource.struct_to_dict(r.resource) + if "spec" in observed: + body["spec"] = _overlay(observed["spec"], body.get("spec", {})) + r.resource.CopyFrom(resource.dict_to_struct(body)) + else: + meta = observed.get("metadata", {}) + body = { + "apiVersion": observed.get("apiVersion"), + "kind": observed.get("kind"), + "metadata": {k: meta[k] for k in ("name", "namespace") if k in meta}, + } + if "spec" in observed: + body["spec"] = observed["spec"] + resource.update(self.rsp.desired.resources[self.name], body) + + self._kept = True + + +def _report(rsp: fnv1.RunFunctionResponse, name: str, missing: list[str]) -> None: + """Report on CONDITION_TYPE for one scope. + + Every scope reports, so the condition is always returned: Crossplane + keeps a condition a function set earlier until the function sets it + again, so one returned only while something was kept would stay False + after it recovered. Scopes in one function share the condition, which + turns False on the first that kept its resource and names each. + + The message is built from resource names and field paths only, so it + holds still while the situation does. Crossplane then leaves the + condition alone, where a result would be an event every reconcile. + """ + c = next((c for c in rsp.conditions if c.type == CONDITION_TYPE), None) + if c is None: + c = rsp.conditions.add( + type=CONDITION_TYPE, status=fnv1.STATUS_CONDITION_TRUE, reason="Available" + ) + + if not missing: + return + + line = f"{name} kept its current spec: {', '.join(missing)} isn't available" + if c.status == fnv1.STATUS_CONDITION_TRUE: + c.status = fnv1.STATUS_CONDITION_FALSE + c.reason = "KeptCurrentSpec" + c.message = line + else: + c.message = f"{c.message}; {line}" + + +def _without_none(v: typing.Any) -> typing.Any: + """Return v with every None value left out, recursing into dicts and lists.""" + if isinstance(v, dict): + return {k: _without_none(x) for k, x in v.items() if x is not None} + if isinstance(v, list): + return [_without_none(x) for x in v if x is not None] + return v + + +def _overlay(base: dict, top: dict) -> dict: + """Return base with top written over it, recursing into nested dicts.""" + out = dict(base) + for k, v in top.items(): + out[k] = ( + _overlay(out[k], v) + if isinstance(v, dict) and isinstance(out.get(k), dict) + else v + ) + return out + + +@contextlib.contextmanager +def composing( + req: fnv1.RunFunctionRequest, rsp: fnv1.RunFunctionResponse, name: str +) -> typing.Iterator[Scope]: + """Compose one resource, depending on whatever it reads from others. + + Args: + req: The RunFunctionRequest, whose observed and required resources + references are read from. + rsp: The RunFunctionResponse to update. + name: The composed resource being built, a key into desired state. + + Yields: + A Scope whose ref and external_name return real values, and record + that this resource depends on the resources they came from: + + vpc = dependency.named("vpc", VPC) + + with dependency.composing(req, rsp, "subnet") as c: + c.update(Subnet(spec={"forProvider": { + "region": "us-east-1", + "vpcId": c.external_name(vpc), + "mapPublicIpOnLaunch": c.ref(vpc.spec.forProvider.enableDnsSupport), + }})) + + The dependencies are recorded when the block exits, so an exception + inside it records nothing. + + A value that isn't available yet comes back as None, and the field is left + out: when the block exits, None fields are removed from the resource + however it was written, because sending one would clear the field. If the + resource doesn't exist yet that's what ordering is for: Crossplane waits + for the dependency before creating it. If it does exist, it keeps its + current spec for the fields that were left out, and the XR's + DependencyValuesAvailable condition turns False, saying why. That holds + even if the block doesn't compose the resource at all because the value it + needed is missing: an existing resource is kept rather than deleted. + + Dependencies are declared only for a resource that's composed or already + exists. A block that composes nothing declares nothing. + """ + scope = Scope(req, rsp, name) + yield scope + scope._close() diff --git a/crossplane/function/proto/v1/run_function.proto b/crossplane/function/proto/v1/run_function.proto index e58a4b5..25bf397 100644 --- a/crossplane/function/proto/v1/run_function.proto +++ b/crossplane/function/proto/v1/run_function.proto @@ -97,6 +97,10 @@ message RunFunctionRequest { // sets the map key to an empty Schema message to indicate that it attempted // to satisfy the request. map required_schemas = 9; + + // Optional ordering constraints over composed resources, accumulated by the + // functions that ran before this one. + Dependencies dependencies = 10; } // Credentials that a function may use to communicate with an external system. @@ -118,6 +122,78 @@ message Resources { repeated Resource items = 1; } +// Dependencies is a set of ordering constraints over composed resources. +// +// This is a message wrapping a repeated field, rather than a bare repeated +// field, so that an unset value can be told apart from an empty one. A bare +// repeated field can't express that difference: proto3 has no presence for +// repeated fields, and an empty list and an unset field are both zero bytes on +// the wire. Crossplane relies on the difference, because an unset value means +// "this function has no opinion, carry my constraints forward" while an empty +// one means "this function wants no constraints at all." State wraps desired +// and observed resources for the same reason. +message Dependencies { + repeated Dependency items = 1; +} + +// A Dependency declares that one composed resource must be created after, and +// deleted before, another resource. It expresses ordering only. It doesn't move +// any data between resources. +message Dependency { + // Name of the composed resource that has the dependency. A key into the + // desired or observed State.resources map. + string resource = 1; + + // What the resource depends on. + oneof depends_on { + // Name of another composed resource. A key into the desired or observed + // State.resources map. + string composed_resource = 2; + + // A resource the pipeline required, rather than composed. Crossplane never + // deletes a resource it didn't compose, so these constrain only the order + // resources are created and updated. + RequiredResourceDependency required_resource = 4; + } + + // How this dependency constrains the order its endpoints are created and + // deleted. Defaults to symmetric ordering. + DependencyLifecycle lifecycle = 3; +} + +// A DependencyLifecycle is how a dependency constrains the order its endpoints +// are created and deleted. +enum DependencyLifecycle { + // Symmetric ordering: the resource is created only once what it depends on + // is ready, and what it depends on is deleted only once the resource is + // gone. This is the default, and the only valid value when depends_on is a + // required resource, because Crossplane never deletes a resource it didn't + // compose. + DEPENDENCY_LIFECYCLE_UNSPECIFIED = 0; + + // The resource may be created without waiting for what it depends on to be + // deleted. It must still exist and be ready before what it depends on is + // deleted. Use for a replacement that must exist before its predecessor is + // torn down. Only valid when depends_on is a composed resource. + DEPENDENCY_LIFECYCLE_CREATE_BEFORE_DESTROY = 1; +} + +// A RequiredResourceDependency identifies a resource the pipeline required, +// for the purpose of ordering. +message RequiredResourceDependency { + // The requirement name. A key into a RunFunctionRequest's required_resources + // map, and into a RunFunctionResponse's requirements.resources map. + string requirement_name = 1; + + // Optional name of a single resource within the set the requirement matched. + // If unset, every resource the requirement matched must be ready. + optional string name = 2; + + // Namespace of name for a namespaced resource. Leave unset for a + // cluster-scoped resource. name and namespace together identify one match. + optional string namespace = 3; +} + // A RunFunctionResponse contains the result of a function run. message RunFunctionResponse { // Metadata pertaining to this response. @@ -156,6 +232,14 @@ message RunFunctionResponse { // // Only Operations use function output. XRs will discard any function output. optional google.protobuf.Struct output = 7; + + // Optional ordering constraints over composed resources. A function that has + // an opinion about ordering must return the full set it wants going forward, + // including edges it received and still wants. Leaving this field unset means + // "no opinion" - Crossplane carries forward whatever it sent in the request. + // + // Dependencies are only used for composition. They're ignored by Operations. + Dependencies dependencies = 8; } // RequestMeta contains metadata pertaining to a RunFunctionRequest. @@ -201,6 +285,11 @@ enum Capability { // OpenAPI schemas and Crossplane will return them in required_schemas. Added // in Crossplane v2.2. CAPABILITY_REQUIRED_SCHEMAS = 5; + + // Crossplane supports the dependencies field. Functions can declare ordering + // constraints over composed resources, and Crossplane will sequence the + // resources it creates, updates and deletes accordingly. + CAPABILITY_DEPENDENCIES = 6; } // Requirements that must be satisfied for a function to run successfully. diff --git a/crossplane/function/proto/v1/run_function_pb2.py b/crossplane/function/proto/v1/run_function_pb2.py index 5fe00b0..278c288 100644 --- a/crossplane/function/proto/v1/run_function_pb2.py +++ b/crossplane/function/proto/v1/run_function_pb2.py @@ -26,7 +26,7 @@ from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/crossplane/function/proto/v1/run_function.proto\x12\x19\x61piextensions.fn.proto.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\"\x8c\x08\n\x12RunFunctionRequest\x12\x34\n\x04meta\x18\x01 \x01(\x0b\x32&.apiextensions.fn.proto.v1.RequestMeta\x12\x32\n\x08observed\x18\x02 \x01(\x0b\x32 .apiextensions.fn.proto.v1.State\x12\x31\n\x07\x64\x65sired\x18\x03 \x01(\x0b\x32 .apiextensions.fn.proto.v1.State\x12+\n\x05input\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x12-\n\x07\x63ontext\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructH\x01\x88\x01\x01\x12^\n\x0f\x65xtra_resources\x18\x06 \x03(\x0b\x32\x41.apiextensions.fn.proto.v1.RunFunctionRequest.ExtraResourcesEntryB\x02\x18\x01\x12S\n\x0b\x63redentials\x18\x07 \x03(\x0b\x32>.apiextensions.fn.proto.v1.RunFunctionRequest.CredentialsEntry\x12`\n\x12required_resources\x18\x08 \x03(\x0b\x32\x44.apiextensions.fn.proto.v1.RunFunctionRequest.RequiredResourcesEntry\x12\\\n\x10required_schemas\x18\t \x03(\x0b\x32\x42.apiextensions.fn.proto.v1.RunFunctionRequest.RequiredSchemasEntry\x1a[\n\x13\x45xtraResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.apiextensions.fn.proto.v1.Resources:\x02\x38\x01\x1aZ\n\x10\x43redentialsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32&.apiextensions.fn.proto.v1.Credentials:\x02\x38\x01\x1a^\n\x16RequiredResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.apiextensions.fn.proto.v1.Resources:\x02\x38\x01\x1aY\n\x14RequiredSchemasEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x30\n\x05value\x18\x02 \x01(\x0b\x32!.apiextensions.fn.proto.v1.Schema:\x02\x38\x01\x42\x08\n\x06_inputB\n\n\x08_context\"]\n\x0b\x43redentials\x12\x44\n\x0f\x63redential_data\x18\x01 \x01(\x0b\x32).apiextensions.fn.proto.v1.CredentialDataH\x00\x42\x08\n\x06source\"\x80\x01\n\x0e\x43redentialData\x12\x41\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x33.apiextensions.fn.proto.v1.CredentialData.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"?\n\tResources\x12\x32\n\x05items\x18\x01 \x03(\x0b\x32#.apiextensions.fn.proto.v1.Resource\"\xa0\x03\n\x13RunFunctionResponse\x12\x35\n\x04meta\x18\x01 \x01(\x0b\x32\'.apiextensions.fn.proto.v1.ResponseMeta\x12\x31\n\x07\x64\x65sired\x18\x02 \x01(\x0b\x32 .apiextensions.fn.proto.v1.State\x12\x32\n\x07results\x18\x03 \x03(\x0b\x32!.apiextensions.fn.proto.v1.Result\x12-\n\x07\x63ontext\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x12=\n\x0crequirements\x18\x05 \x01(\x0b\x32\'.apiextensions.fn.proto.v1.Requirements\x12\x38\n\nconditions\x18\x06 \x03(\x0b\x32$.apiextensions.fn.proto.v1.Condition\x12,\n\x06output\x18\x07 \x01(\x0b\x32\x17.google.protobuf.StructH\x01\x88\x01\x01\x42\n\n\x08_contextB\t\n\x07_output\"W\n\x0bRequestMeta\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12;\n\x0c\x63\x61pabilities\x18\x02 \x03(\x0e\x32%.apiextensions.fn.proto.v1.Capability\"\x98\x04\n\x0cRequirements\x12X\n\x0f\x65xtra_resources\x18\x01 \x03(\x0b\x32;.apiextensions.fn.proto.v1.Requirements.ExtraResourcesEntryB\x02\x18\x01\x12I\n\tresources\x18\x02 \x03(\x0b\x32\x36.apiextensions.fn.proto.v1.Requirements.ResourcesEntry\x12\x45\n\x07schemas\x18\x03 \x03(\x0b\x32\x34.apiextensions.fn.proto.v1.Requirements.SchemasEntry\x1a\x62\n\x13\x45xtraResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.apiextensions.fn.proto.v1.ResourceSelector:\x02\x38\x01\x1a]\n\x0eResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.apiextensions.fn.proto.v1.ResourceSelector:\x02\x38\x01\x1aY\n\x0cSchemasEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x01(\x0b\x32).apiextensions.fn.proto.v1.SchemaSelector:\x02\x38\x01\"3\n\x0eSchemaSelector\x12\x13\n\x0b\x61pi_version\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\"I\n\x06Schema\x12\x30\n\nopenapi_v3\x18\x01 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x42\r\n\x0b_openapi_v3\"\xba\x01\n\x10ResourceSelector\x12\x13\n\x0b\x61pi_version\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x14\n\nmatch_name\x18\x03 \x01(\tH\x00\x12>\n\x0cmatch_labels\x18\x04 \x01(\x0b\x32&.apiextensions.fn.proto.v1.MatchLabelsH\x00\x12\x16\n\tnamespace\x18\x05 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05matchB\x0c\n\n_namespace\"\x80\x01\n\x0bMatchLabels\x12\x42\n\x06labels\x18\x01 \x03(\x0b\x32\x32.apiextensions.fn.proto.v1.MatchLabels.LabelsEntry\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"P\n\x0cResponseMeta\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12+\n\x03ttl\x18\x02 \x01(\x0b\x32\x19.google.protobuf.DurationH\x00\x88\x01\x01\x42\x06\n\x04_ttl\"\xda\x01\n\x05State\x12\x36\n\tcomposite\x18\x01 \x01(\x0b\x32#.apiextensions.fn.proto.v1.Resource\x12\x42\n\tresources\x18\x02 \x03(\x0b\x32/.apiextensions.fn.proto.v1.State.ResourcesEntry\x1aU\n\x0eResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x32\n\x05value\x18\x02 \x01(\x0b\x32#.apiextensions.fn.proto.v1.Resource:\x02\x38\x01\"\xf8\x01\n\x08Resource\x12)\n\x08resource\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12V\n\x12\x63onnection_details\x18\x02 \x03(\x0b\x32:.apiextensions.fn.proto.v1.Resource.ConnectionDetailsEntry\x12/\n\x05ready\x18\x03 \x01(\x0e\x32 .apiextensions.fn.proto.v1.Ready\x1a\x38\n\x16\x43onnectionDetailsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"\xb3\x01\n\x06Result\x12\x35\n\x08severity\x18\x01 \x01(\x0e\x32#.apiextensions.fn.proto.v1.Severity\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x13\n\x06reason\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x36\n\x06target\x18\x04 \x01(\x0e\x32!.apiextensions.fn.proto.v1.TargetH\x01\x88\x01\x01\x42\t\n\x07_reasonB\t\n\x07_target\"\xc1\x01\n\tCondition\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x31\n\x06status\x18\x02 \x01(\x0e\x32!.apiextensions.fn.proto.v1.Status\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x14\n\x07message\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x36\n\x06target\x18\x05 \x01(\x0e\x32!.apiextensions.fn.proto.v1.TargetH\x01\x88\x01\x01\x42\n\n\x08_messageB\t\n\x07_target*\xc0\x01\n\nCapability\x12\x1a\n\x16\x43\x41PABILITY_UNSPECIFIED\x10\x00\x12\x1b\n\x17\x43\x41PABILITY_CAPABILITIES\x10\x01\x12!\n\x1d\x43\x41PABILITY_REQUIRED_RESOURCES\x10\x02\x12\x1a\n\x16\x43\x41PABILITY_CREDENTIALS\x10\x03\x12\x19\n\x15\x43\x41PABILITY_CONDITIONS\x10\x04\x12\x1f\n\x1b\x43\x41PABILITY_REQUIRED_SCHEMAS\x10\x05*?\n\x05Ready\x12\x15\n\x11READY_UNSPECIFIED\x10\x00\x12\x0e\n\nREADY_TRUE\x10\x01\x12\x0f\n\x0bREADY_FALSE\x10\x02*c\n\x08Severity\x12\x18\n\x14SEVERITY_UNSPECIFIED\x10\x00\x12\x12\n\x0eSEVERITY_FATAL\x10\x01\x12\x14\n\x10SEVERITY_WARNING\x10\x02\x12\x13\n\x0fSEVERITY_NORMAL\x10\x03*V\n\x06Target\x12\x16\n\x12TARGET_UNSPECIFIED\x10\x00\x12\x14\n\x10TARGET_COMPOSITE\x10\x01\x12\x1e\n\x1aTARGET_COMPOSITE_AND_CLAIM\x10\x02*\x7f\n\x06Status\x12 \n\x1cSTATUS_CONDITION_UNSPECIFIED\x10\x00\x12\x1c\n\x18STATUS_CONDITION_UNKNOWN\x10\x01\x12\x19\n\x15STATUS_CONDITION_TRUE\x10\x02\x12\x1a\n\x16STATUS_CONDITION_FALSE\x10\x03\x32\x87\x01\n\x15\x46unctionRunnerService\x12n\n\x0bRunFunction\x12-.apiextensions.fn.proto.v1.RunFunctionRequest\x1a..apiextensions.fn.proto.v1.RunFunctionResponse\"\x00\x42\x31Z/github.com/crossplane/crossplane/v2/proto/fn/v1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/crossplane/function/proto/v1/run_function.proto\x12\x19\x61piextensions.fn.proto.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\"\xcb\x08\n\x12RunFunctionRequest\x12\x34\n\x04meta\x18\x01 \x01(\x0b\x32&.apiextensions.fn.proto.v1.RequestMeta\x12\x32\n\x08observed\x18\x02 \x01(\x0b\x32 .apiextensions.fn.proto.v1.State\x12\x31\n\x07\x64\x65sired\x18\x03 \x01(\x0b\x32 .apiextensions.fn.proto.v1.State\x12+\n\x05input\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x12-\n\x07\x63ontext\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructH\x01\x88\x01\x01\x12^\n\x0f\x65xtra_resources\x18\x06 \x03(\x0b\x32\x41.apiextensions.fn.proto.v1.RunFunctionRequest.ExtraResourcesEntryB\x02\x18\x01\x12S\n\x0b\x63redentials\x18\x07 \x03(\x0b\x32>.apiextensions.fn.proto.v1.RunFunctionRequest.CredentialsEntry\x12`\n\x12required_resources\x18\x08 \x03(\x0b\x32\x44.apiextensions.fn.proto.v1.RunFunctionRequest.RequiredResourcesEntry\x12\\\n\x10required_schemas\x18\t \x03(\x0b\x32\x42.apiextensions.fn.proto.v1.RunFunctionRequest.RequiredSchemasEntry\x12=\n\x0c\x64\x65pendencies\x18\n \x01(\x0b\x32\'.apiextensions.fn.proto.v1.Dependencies\x1a[\n\x13\x45xtraResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.apiextensions.fn.proto.v1.Resources:\x02\x38\x01\x1aZ\n\x10\x43redentialsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32&.apiextensions.fn.proto.v1.Credentials:\x02\x38\x01\x1a^\n\x16RequiredResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.apiextensions.fn.proto.v1.Resources:\x02\x38\x01\x1aY\n\x14RequiredSchemasEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x30\n\x05value\x18\x02 \x01(\x0b\x32!.apiextensions.fn.proto.v1.Schema:\x02\x38\x01\x42\x08\n\x06_inputB\n\n\x08_context\"]\n\x0b\x43redentials\x12\x44\n\x0f\x63redential_data\x18\x01 \x01(\x0b\x32).apiextensions.fn.proto.v1.CredentialDataH\x00\x42\x08\n\x06source\"\x80\x01\n\x0e\x43redentialData\x12\x41\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x33.apiextensions.fn.proto.v1.CredentialData.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"?\n\tResources\x12\x32\n\x05items\x18\x01 \x03(\x0b\x32#.apiextensions.fn.proto.v1.Resource\"D\n\x0c\x44\x65pendencies\x12\x34\n\x05items\x18\x01 \x03(\x0b\x32%.apiextensions.fn.proto.v1.Dependency\"\xe0\x01\n\nDependency\x12\x10\n\x08resource\x18\x01 \x01(\t\x12\x1b\n\x11\x63omposed_resource\x18\x02 \x01(\tH\x00\x12R\n\x11required_resource\x18\x04 \x01(\x0b\x32\x35.apiextensions.fn.proto.v1.RequiredResourceDependencyH\x00\x12\x41\n\tlifecycle\x18\x03 \x01(\x0e\x32..apiextensions.fn.proto.v1.DependencyLifecycleB\x0c\n\ndepends_on\"x\n\x1aRequiredResourceDependency\x12\x18\n\x10requirement_name\x18\x01 \x01(\t\x12\x11\n\x04name\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x16\n\tnamespace\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05_nameB\x0c\n\n_namespace\"\xdf\x03\n\x13RunFunctionResponse\x12\x35\n\x04meta\x18\x01 \x01(\x0b\x32\'.apiextensions.fn.proto.v1.ResponseMeta\x12\x31\n\x07\x64\x65sired\x18\x02 \x01(\x0b\x32 .apiextensions.fn.proto.v1.State\x12\x32\n\x07results\x18\x03 \x03(\x0b\x32!.apiextensions.fn.proto.v1.Result\x12-\n\x07\x63ontext\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x12=\n\x0crequirements\x18\x05 \x01(\x0b\x32\'.apiextensions.fn.proto.v1.Requirements\x12\x38\n\nconditions\x18\x06 \x03(\x0b\x32$.apiextensions.fn.proto.v1.Condition\x12,\n\x06output\x18\x07 \x01(\x0b\x32\x17.google.protobuf.StructH\x01\x88\x01\x01\x12=\n\x0c\x64\x65pendencies\x18\x08 \x01(\x0b\x32\'.apiextensions.fn.proto.v1.DependenciesB\n\n\x08_contextB\t\n\x07_output\"W\n\x0bRequestMeta\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12;\n\x0c\x63\x61pabilities\x18\x02 \x03(\x0e\x32%.apiextensions.fn.proto.v1.Capability\"\x98\x04\n\x0cRequirements\x12X\n\x0f\x65xtra_resources\x18\x01 \x03(\x0b\x32;.apiextensions.fn.proto.v1.Requirements.ExtraResourcesEntryB\x02\x18\x01\x12I\n\tresources\x18\x02 \x03(\x0b\x32\x36.apiextensions.fn.proto.v1.Requirements.ResourcesEntry\x12\x45\n\x07schemas\x18\x03 \x03(\x0b\x32\x34.apiextensions.fn.proto.v1.Requirements.SchemasEntry\x1a\x62\n\x13\x45xtraResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.apiextensions.fn.proto.v1.ResourceSelector:\x02\x38\x01\x1a]\n\x0eResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.apiextensions.fn.proto.v1.ResourceSelector:\x02\x38\x01\x1aY\n\x0cSchemasEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x01(\x0b\x32).apiextensions.fn.proto.v1.SchemaSelector:\x02\x38\x01\"3\n\x0eSchemaSelector\x12\x13\n\x0b\x61pi_version\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\"I\n\x06Schema\x12\x30\n\nopenapi_v3\x18\x01 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x42\r\n\x0b_openapi_v3\"\xba\x01\n\x10ResourceSelector\x12\x13\n\x0b\x61pi_version\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x14\n\nmatch_name\x18\x03 \x01(\tH\x00\x12>\n\x0cmatch_labels\x18\x04 \x01(\x0b\x32&.apiextensions.fn.proto.v1.MatchLabelsH\x00\x12\x16\n\tnamespace\x18\x05 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05matchB\x0c\n\n_namespace\"\x80\x01\n\x0bMatchLabels\x12\x42\n\x06labels\x18\x01 \x03(\x0b\x32\x32.apiextensions.fn.proto.v1.MatchLabels.LabelsEntry\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"P\n\x0cResponseMeta\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12+\n\x03ttl\x18\x02 \x01(\x0b\x32\x19.google.protobuf.DurationH\x00\x88\x01\x01\x42\x06\n\x04_ttl\"\xda\x01\n\x05State\x12\x36\n\tcomposite\x18\x01 \x01(\x0b\x32#.apiextensions.fn.proto.v1.Resource\x12\x42\n\tresources\x18\x02 \x03(\x0b\x32/.apiextensions.fn.proto.v1.State.ResourcesEntry\x1aU\n\x0eResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x32\n\x05value\x18\x02 \x01(\x0b\x32#.apiextensions.fn.proto.v1.Resource:\x02\x38\x01\"\xf8\x01\n\x08Resource\x12)\n\x08resource\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12V\n\x12\x63onnection_details\x18\x02 \x03(\x0b\x32:.apiextensions.fn.proto.v1.Resource.ConnectionDetailsEntry\x12/\n\x05ready\x18\x03 \x01(\x0e\x32 .apiextensions.fn.proto.v1.Ready\x1a\x38\n\x16\x43onnectionDetailsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"\xb3\x01\n\x06Result\x12\x35\n\x08severity\x18\x01 \x01(\x0e\x32#.apiextensions.fn.proto.v1.Severity\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x13\n\x06reason\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x36\n\x06target\x18\x04 \x01(\x0e\x32!.apiextensions.fn.proto.v1.TargetH\x01\x88\x01\x01\x42\t\n\x07_reasonB\t\n\x07_target\"\xc1\x01\n\tCondition\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x31\n\x06status\x18\x02 \x01(\x0e\x32!.apiextensions.fn.proto.v1.Status\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x14\n\x07message\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x36\n\x06target\x18\x05 \x01(\x0e\x32!.apiextensions.fn.proto.v1.TargetH\x01\x88\x01\x01\x42\n\n\x08_messageB\t\n\x07_target*k\n\x13\x44\x65pendencyLifecycle\x12$\n DEPENDENCY_LIFECYCLE_UNSPECIFIED\x10\x00\x12.\n*DEPENDENCY_LIFECYCLE_CREATE_BEFORE_DESTROY\x10\x01*\xdd\x01\n\nCapability\x12\x1a\n\x16\x43\x41PABILITY_UNSPECIFIED\x10\x00\x12\x1b\n\x17\x43\x41PABILITY_CAPABILITIES\x10\x01\x12!\n\x1d\x43\x41PABILITY_REQUIRED_RESOURCES\x10\x02\x12\x1a\n\x16\x43\x41PABILITY_CREDENTIALS\x10\x03\x12\x19\n\x15\x43\x41PABILITY_CONDITIONS\x10\x04\x12\x1f\n\x1b\x43\x41PABILITY_REQUIRED_SCHEMAS\x10\x05\x12\x1b\n\x17\x43\x41PABILITY_DEPENDENCIES\x10\x06*?\n\x05Ready\x12\x15\n\x11READY_UNSPECIFIED\x10\x00\x12\x0e\n\nREADY_TRUE\x10\x01\x12\x0f\n\x0bREADY_FALSE\x10\x02*c\n\x08Severity\x12\x18\n\x14SEVERITY_UNSPECIFIED\x10\x00\x12\x12\n\x0eSEVERITY_FATAL\x10\x01\x12\x14\n\x10SEVERITY_WARNING\x10\x02\x12\x13\n\x0fSEVERITY_NORMAL\x10\x03*V\n\x06Target\x12\x16\n\x12TARGET_UNSPECIFIED\x10\x00\x12\x14\n\x10TARGET_COMPOSITE\x10\x01\x12\x1e\n\x1aTARGET_COMPOSITE_AND_CLAIM\x10\x02*\x7f\n\x06Status\x12 \n\x1cSTATUS_CONDITION_UNSPECIFIED\x10\x00\x12\x1c\n\x18STATUS_CONDITION_UNKNOWN\x10\x01\x12\x19\n\x15STATUS_CONDITION_TRUE\x10\x02\x12\x1a\n\x16STATUS_CONDITION_FALSE\x10\x03\x32\x87\x01\n\x15\x46unctionRunnerService\x12n\n\x0bRunFunction\x12-.apiextensions.fn.proto.v1.RunFunctionRequest\x1a..apiextensions.fn.proto.v1.RunFunctionResponse\"\x00\x42\x31Z/github.com/crossplane/crossplane/v2/proto/fn/v1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -60,70 +60,78 @@ _globals['_STATE_RESOURCESENTRY']._serialized_options = b'8\001' _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._loaded_options = None _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._serialized_options = b'8\001' - _globals['_CAPABILITY']._serialized_start=3898 - _globals['_CAPABILITY']._serialized_end=4090 - _globals['_READY']._serialized_start=4092 - _globals['_READY']._serialized_end=4155 - _globals['_SEVERITY']._serialized_start=4157 - _globals['_SEVERITY']._serialized_end=4256 - _globals['_TARGET']._serialized_start=4258 - _globals['_TARGET']._serialized_end=4344 - _globals['_STATUS']._serialized_start=4346 - _globals['_STATUS']._serialized_end=4473 + _globals['_DEPENDENCYLIFECYCLE']._serialized_start=4442 + _globals['_DEPENDENCYLIFECYCLE']._serialized_end=4549 + _globals['_CAPABILITY']._serialized_start=4552 + _globals['_CAPABILITY']._serialized_end=4773 + _globals['_READY']._serialized_start=4775 + _globals['_READY']._serialized_end=4838 + _globals['_SEVERITY']._serialized_start=4840 + _globals['_SEVERITY']._serialized_end=4939 + _globals['_TARGET']._serialized_start=4941 + _globals['_TARGET']._serialized_end=5027 + _globals['_STATUS']._serialized_start=5029 + _globals['_STATUS']._serialized_end=5156 _globals['_RUNFUNCTIONREQUEST']._serialized_start=141 - _globals['_RUNFUNCTIONREQUEST']._serialized_end=1177 - _globals['_RUNFUNCTIONREQUEST_EXTRARESOURCESENTRY']._serialized_start=785 - _globals['_RUNFUNCTIONREQUEST_EXTRARESOURCESENTRY']._serialized_end=876 - _globals['_RUNFUNCTIONREQUEST_CREDENTIALSENTRY']._serialized_start=878 - _globals['_RUNFUNCTIONREQUEST_CREDENTIALSENTRY']._serialized_end=968 - _globals['_RUNFUNCTIONREQUEST_REQUIREDRESOURCESENTRY']._serialized_start=970 - _globals['_RUNFUNCTIONREQUEST_REQUIREDRESOURCESENTRY']._serialized_end=1064 - _globals['_RUNFUNCTIONREQUEST_REQUIREDSCHEMASENTRY']._serialized_start=1066 - _globals['_RUNFUNCTIONREQUEST_REQUIREDSCHEMASENTRY']._serialized_end=1155 - _globals['_CREDENTIALS']._serialized_start=1179 - _globals['_CREDENTIALS']._serialized_end=1272 - _globals['_CREDENTIALDATA']._serialized_start=1275 - _globals['_CREDENTIALDATA']._serialized_end=1403 - _globals['_CREDENTIALDATA_DATAENTRY']._serialized_start=1360 - _globals['_CREDENTIALDATA_DATAENTRY']._serialized_end=1403 - _globals['_RESOURCES']._serialized_start=1405 - _globals['_RESOURCES']._serialized_end=1468 - _globals['_RUNFUNCTIONRESPONSE']._serialized_start=1471 - _globals['_RUNFUNCTIONRESPONSE']._serialized_end=1887 - _globals['_REQUESTMETA']._serialized_start=1889 - _globals['_REQUESTMETA']._serialized_end=1976 - _globals['_REQUIREMENTS']._serialized_start=1979 - _globals['_REQUIREMENTS']._serialized_end=2515 - _globals['_REQUIREMENTS_EXTRARESOURCESENTRY']._serialized_start=2231 - _globals['_REQUIREMENTS_EXTRARESOURCESENTRY']._serialized_end=2329 - _globals['_REQUIREMENTS_RESOURCESENTRY']._serialized_start=2331 - _globals['_REQUIREMENTS_RESOURCESENTRY']._serialized_end=2424 - _globals['_REQUIREMENTS_SCHEMASENTRY']._serialized_start=2426 - _globals['_REQUIREMENTS_SCHEMASENTRY']._serialized_end=2515 - _globals['_SCHEMASELECTOR']._serialized_start=2517 - _globals['_SCHEMASELECTOR']._serialized_end=2568 - _globals['_SCHEMA']._serialized_start=2570 - _globals['_SCHEMA']._serialized_end=2643 - _globals['_RESOURCESELECTOR']._serialized_start=2646 - _globals['_RESOURCESELECTOR']._serialized_end=2832 - _globals['_MATCHLABELS']._serialized_start=2835 - _globals['_MATCHLABELS']._serialized_end=2963 - _globals['_MATCHLABELS_LABELSENTRY']._serialized_start=2918 - _globals['_MATCHLABELS_LABELSENTRY']._serialized_end=2963 - _globals['_RESPONSEMETA']._serialized_start=2965 - _globals['_RESPONSEMETA']._serialized_end=3045 - _globals['_STATE']._serialized_start=3048 - _globals['_STATE']._serialized_end=3266 - _globals['_STATE_RESOURCESENTRY']._serialized_start=3181 - _globals['_STATE_RESOURCESENTRY']._serialized_end=3266 - _globals['_RESOURCE']._serialized_start=3269 - _globals['_RESOURCE']._serialized_end=3517 - _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._serialized_start=3461 - _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._serialized_end=3517 - _globals['_RESULT']._serialized_start=3520 - _globals['_RESULT']._serialized_end=3699 - _globals['_CONDITION']._serialized_start=3702 - _globals['_CONDITION']._serialized_end=3895 - _globals['_FUNCTIONRUNNERSERVICE']._serialized_start=4476 - _globals['_FUNCTIONRUNNERSERVICE']._serialized_end=4611 + _globals['_RUNFUNCTIONREQUEST']._serialized_end=1240 + _globals['_RUNFUNCTIONREQUEST_EXTRARESOURCESENTRY']._serialized_start=848 + _globals['_RUNFUNCTIONREQUEST_EXTRARESOURCESENTRY']._serialized_end=939 + _globals['_RUNFUNCTIONREQUEST_CREDENTIALSENTRY']._serialized_start=941 + _globals['_RUNFUNCTIONREQUEST_CREDENTIALSENTRY']._serialized_end=1031 + _globals['_RUNFUNCTIONREQUEST_REQUIREDRESOURCESENTRY']._serialized_start=1033 + _globals['_RUNFUNCTIONREQUEST_REQUIREDRESOURCESENTRY']._serialized_end=1127 + _globals['_RUNFUNCTIONREQUEST_REQUIREDSCHEMASENTRY']._serialized_start=1129 + _globals['_RUNFUNCTIONREQUEST_REQUIREDSCHEMASENTRY']._serialized_end=1218 + _globals['_CREDENTIALS']._serialized_start=1242 + _globals['_CREDENTIALS']._serialized_end=1335 + _globals['_CREDENTIALDATA']._serialized_start=1338 + _globals['_CREDENTIALDATA']._serialized_end=1466 + _globals['_CREDENTIALDATA_DATAENTRY']._serialized_start=1423 + _globals['_CREDENTIALDATA_DATAENTRY']._serialized_end=1466 + _globals['_RESOURCES']._serialized_start=1468 + _globals['_RESOURCES']._serialized_end=1531 + _globals['_DEPENDENCIES']._serialized_start=1533 + _globals['_DEPENDENCIES']._serialized_end=1601 + _globals['_DEPENDENCY']._serialized_start=1604 + _globals['_DEPENDENCY']._serialized_end=1828 + _globals['_REQUIREDRESOURCEDEPENDENCY']._serialized_start=1830 + _globals['_REQUIREDRESOURCEDEPENDENCY']._serialized_end=1950 + _globals['_RUNFUNCTIONRESPONSE']._serialized_start=1953 + _globals['_RUNFUNCTIONRESPONSE']._serialized_end=2432 + _globals['_REQUESTMETA']._serialized_start=2434 + _globals['_REQUESTMETA']._serialized_end=2521 + _globals['_REQUIREMENTS']._serialized_start=2524 + _globals['_REQUIREMENTS']._serialized_end=3060 + _globals['_REQUIREMENTS_EXTRARESOURCESENTRY']._serialized_start=2776 + _globals['_REQUIREMENTS_EXTRARESOURCESENTRY']._serialized_end=2874 + _globals['_REQUIREMENTS_RESOURCESENTRY']._serialized_start=2876 + _globals['_REQUIREMENTS_RESOURCESENTRY']._serialized_end=2969 + _globals['_REQUIREMENTS_SCHEMASENTRY']._serialized_start=2971 + _globals['_REQUIREMENTS_SCHEMASENTRY']._serialized_end=3060 + _globals['_SCHEMASELECTOR']._serialized_start=3062 + _globals['_SCHEMASELECTOR']._serialized_end=3113 + _globals['_SCHEMA']._serialized_start=3115 + _globals['_SCHEMA']._serialized_end=3188 + _globals['_RESOURCESELECTOR']._serialized_start=3191 + _globals['_RESOURCESELECTOR']._serialized_end=3377 + _globals['_MATCHLABELS']._serialized_start=3380 + _globals['_MATCHLABELS']._serialized_end=3508 + _globals['_MATCHLABELS_LABELSENTRY']._serialized_start=3463 + _globals['_MATCHLABELS_LABELSENTRY']._serialized_end=3508 + _globals['_RESPONSEMETA']._serialized_start=3510 + _globals['_RESPONSEMETA']._serialized_end=3590 + _globals['_STATE']._serialized_start=3593 + _globals['_STATE']._serialized_end=3811 + _globals['_STATE_RESOURCESENTRY']._serialized_start=3726 + _globals['_STATE_RESOURCESENTRY']._serialized_end=3811 + _globals['_RESOURCE']._serialized_start=3814 + _globals['_RESOURCE']._serialized_end=4062 + _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._serialized_start=4006 + _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._serialized_end=4062 + _globals['_RESULT']._serialized_start=4065 + _globals['_RESULT']._serialized_end=4244 + _globals['_CONDITION']._serialized_start=4247 + _globals['_CONDITION']._serialized_end=4440 + _globals['_FUNCTIONRUNNERSERVICE']._serialized_start=5159 + _globals['_FUNCTIONRUNNERSERVICE']._serialized_end=5294 # @@protoc_insertion_point(module_scope) diff --git a/crossplane/function/proto/v1/run_function_pb2.pyi b/crossplane/function/proto/v1/run_function_pb2.pyi index d0d29ca..6aeafc9 100644 --- a/crossplane/function/proto/v1/run_function_pb2.pyi +++ b/crossplane/function/proto/v1/run_function_pb2.pyi @@ -11,6 +11,11 @@ from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor +class DependencyLifecycle(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + DEPENDENCY_LIFECYCLE_UNSPECIFIED: _ClassVar[DependencyLifecycle] + DEPENDENCY_LIFECYCLE_CREATE_BEFORE_DESTROY: _ClassVar[DependencyLifecycle] + class Capability(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () CAPABILITY_UNSPECIFIED: _ClassVar[Capability] @@ -19,6 +24,7 @@ class Capability(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): CAPABILITY_CREDENTIALS: _ClassVar[Capability] CAPABILITY_CONDITIONS: _ClassVar[Capability] CAPABILITY_REQUIRED_SCHEMAS: _ClassVar[Capability] + CAPABILITY_DEPENDENCIES: _ClassVar[Capability] class Ready(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -45,12 +51,15 @@ class Status(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): STATUS_CONDITION_UNKNOWN: _ClassVar[Status] STATUS_CONDITION_TRUE: _ClassVar[Status] STATUS_CONDITION_FALSE: _ClassVar[Status] +DEPENDENCY_LIFECYCLE_UNSPECIFIED: DependencyLifecycle +DEPENDENCY_LIFECYCLE_CREATE_BEFORE_DESTROY: DependencyLifecycle CAPABILITY_UNSPECIFIED: Capability CAPABILITY_CAPABILITIES: Capability CAPABILITY_REQUIRED_RESOURCES: Capability CAPABILITY_CREDENTIALS: Capability CAPABILITY_CONDITIONS: Capability CAPABILITY_REQUIRED_SCHEMAS: Capability +CAPABILITY_DEPENDENCIES: Capability READY_UNSPECIFIED: Ready READY_TRUE: Ready READY_FALSE: Ready @@ -67,7 +76,7 @@ STATUS_CONDITION_TRUE: Status STATUS_CONDITION_FALSE: Status class RunFunctionRequest(_message.Message): - __slots__ = ("meta", "observed", "desired", "input", "context", "extra_resources", "credentials", "required_resources", "required_schemas") + __slots__ = ("meta", "observed", "desired", "input", "context", "extra_resources", "credentials", "required_resources", "required_schemas", "dependencies") class ExtraResourcesEntry(_message.Message): __slots__ = ("key", "value") KEY_FIELD_NUMBER: _ClassVar[int] @@ -105,6 +114,7 @@ class RunFunctionRequest(_message.Message): CREDENTIALS_FIELD_NUMBER: _ClassVar[int] REQUIRED_RESOURCES_FIELD_NUMBER: _ClassVar[int] REQUIRED_SCHEMAS_FIELD_NUMBER: _ClassVar[int] + DEPENDENCIES_FIELD_NUMBER: _ClassVar[int] meta: RequestMeta observed: State desired: State @@ -114,7 +124,8 @@ class RunFunctionRequest(_message.Message): credentials: _containers.MessageMap[str, Credentials] required_resources: _containers.MessageMap[str, Resources] required_schemas: _containers.MessageMap[str, Schema] - def __init__(self, meta: _Optional[_Union[RequestMeta, _Mapping]] = ..., observed: _Optional[_Union[State, _Mapping]] = ..., desired: _Optional[_Union[State, _Mapping]] = ..., input: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., context: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., extra_resources: _Optional[_Mapping[str, Resources]] = ..., credentials: _Optional[_Mapping[str, Credentials]] = ..., required_resources: _Optional[_Mapping[str, Resources]] = ..., required_schemas: _Optional[_Mapping[str, Schema]] = ...) -> None: ... + dependencies: Dependencies + def __init__(self, meta: _Optional[_Union[RequestMeta, _Mapping]] = ..., observed: _Optional[_Union[State, _Mapping]] = ..., desired: _Optional[_Union[State, _Mapping]] = ..., input: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., context: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., extra_resources: _Optional[_Mapping[str, Resources]] = ..., credentials: _Optional[_Mapping[str, Credentials]] = ..., required_resources: _Optional[_Mapping[str, Resources]] = ..., required_schemas: _Optional[_Mapping[str, Schema]] = ..., dependencies: _Optional[_Union[Dependencies, _Mapping]] = ...) -> None: ... class Credentials(_message.Message): __slots__ = ("credential_data",) @@ -141,8 +152,36 @@ class Resources(_message.Message): items: _containers.RepeatedCompositeFieldContainer[Resource] def __init__(self, items: _Optional[_Iterable[_Union[Resource, _Mapping]]] = ...) -> None: ... +class Dependencies(_message.Message): + __slots__ = ("items",) + ITEMS_FIELD_NUMBER: _ClassVar[int] + items: _containers.RepeatedCompositeFieldContainer[Dependency] + def __init__(self, items: _Optional[_Iterable[_Union[Dependency, _Mapping]]] = ...) -> None: ... + +class Dependency(_message.Message): + __slots__ = ("resource", "composed_resource", "required_resource", "lifecycle") + RESOURCE_FIELD_NUMBER: _ClassVar[int] + COMPOSED_RESOURCE_FIELD_NUMBER: _ClassVar[int] + REQUIRED_RESOURCE_FIELD_NUMBER: _ClassVar[int] + LIFECYCLE_FIELD_NUMBER: _ClassVar[int] + resource: str + composed_resource: str + required_resource: RequiredResourceDependency + lifecycle: DependencyLifecycle + def __init__(self, resource: _Optional[str] = ..., composed_resource: _Optional[str] = ..., required_resource: _Optional[_Union[RequiredResourceDependency, _Mapping]] = ..., lifecycle: _Optional[_Union[DependencyLifecycle, str]] = ...) -> None: ... + +class RequiredResourceDependency(_message.Message): + __slots__ = ("requirement_name", "name", "namespace") + REQUIREMENT_NAME_FIELD_NUMBER: _ClassVar[int] + NAME_FIELD_NUMBER: _ClassVar[int] + NAMESPACE_FIELD_NUMBER: _ClassVar[int] + requirement_name: str + name: str + namespace: str + def __init__(self, requirement_name: _Optional[str] = ..., name: _Optional[str] = ..., namespace: _Optional[str] = ...) -> None: ... + class RunFunctionResponse(_message.Message): - __slots__ = ("meta", "desired", "results", "context", "requirements", "conditions", "output") + __slots__ = ("meta", "desired", "results", "context", "requirements", "conditions", "output", "dependencies") META_FIELD_NUMBER: _ClassVar[int] DESIRED_FIELD_NUMBER: _ClassVar[int] RESULTS_FIELD_NUMBER: _ClassVar[int] @@ -150,6 +189,7 @@ class RunFunctionResponse(_message.Message): REQUIREMENTS_FIELD_NUMBER: _ClassVar[int] CONDITIONS_FIELD_NUMBER: _ClassVar[int] OUTPUT_FIELD_NUMBER: _ClassVar[int] + DEPENDENCIES_FIELD_NUMBER: _ClassVar[int] meta: ResponseMeta desired: State results: _containers.RepeatedCompositeFieldContainer[Result] @@ -157,7 +197,8 @@ class RunFunctionResponse(_message.Message): requirements: Requirements conditions: _containers.RepeatedCompositeFieldContainer[Condition] output: _struct_pb2.Struct - def __init__(self, meta: _Optional[_Union[ResponseMeta, _Mapping]] = ..., desired: _Optional[_Union[State, _Mapping]] = ..., results: _Optional[_Iterable[_Union[Result, _Mapping]]] = ..., context: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., requirements: _Optional[_Union[Requirements, _Mapping]] = ..., conditions: _Optional[_Iterable[_Union[Condition, _Mapping]]] = ..., output: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ...) -> None: ... + dependencies: Dependencies + def __init__(self, meta: _Optional[_Union[ResponseMeta, _Mapping]] = ..., desired: _Optional[_Union[State, _Mapping]] = ..., results: _Optional[_Iterable[_Union[Result, _Mapping]]] = ..., context: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., requirements: _Optional[_Union[Requirements, _Mapping]] = ..., conditions: _Optional[_Iterable[_Union[Condition, _Mapping]]] = ..., output: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., dependencies: _Optional[_Union[Dependencies, _Mapping]] = ...) -> None: ... class RequestMeta(_message.Message): __slots__ = ("tag", "capabilities") diff --git a/crossplane/function/proto/v1beta1/run_function.proto b/crossplane/function/proto/v1beta1/run_function.proto index 667c740..132a6c9 100644 --- a/crossplane/function/proto/v1beta1/run_function.proto +++ b/crossplane/function/proto/v1beta1/run_function.proto @@ -99,6 +99,10 @@ message RunFunctionRequest { // sets the map key to an empty Schema message to indicate that it attempted // to satisfy the request. map required_schemas = 9; + + // Optional ordering constraints over composed resources, accumulated by the + // functions that ran before this one. + Dependencies dependencies = 10; } // Credentials that a function may use to communicate with an external system. @@ -120,6 +124,78 @@ message Resources { repeated Resource items = 1; } +// Dependencies is a set of ordering constraints over composed resources. +// +// This is a message wrapping a repeated field, rather than a bare repeated +// field, so that an unset value can be told apart from an empty one. A bare +// repeated field can't express that difference: proto3 has no presence for +// repeated fields, and an empty list and an unset field are both zero bytes on +// the wire. Crossplane relies on the difference, because an unset value means +// "this function has no opinion, carry my constraints forward" while an empty +// one means "this function wants no constraints at all." State wraps desired +// and observed resources for the same reason. +message Dependencies { + repeated Dependency items = 1; +} + +// A Dependency declares that one composed resource must be created after, and +// deleted before, another resource. It expresses ordering only. It doesn't move +// any data between resources. +message Dependency { + // Name of the composed resource that has the dependency. A key into the + // desired or observed State.resources map. + string resource = 1; + + // What the resource depends on. + oneof depends_on { + // Name of another composed resource. A key into the desired or observed + // State.resources map. + string composed_resource = 2; + + // A resource the pipeline required, rather than composed. Crossplane never + // deletes a resource it didn't compose, so these constrain only the order + // resources are created and updated. + RequiredResourceDependency required_resource = 4; + } + + // How this dependency constrains the order its endpoints are created and + // deleted. Defaults to symmetric ordering. + DependencyLifecycle lifecycle = 3; +} + +// A DependencyLifecycle is how a dependency constrains the order its endpoints +// are created and deleted. +enum DependencyLifecycle { + // Symmetric ordering: the resource is created only once what it depends on + // is ready, and what it depends on is deleted only once the resource is + // gone. This is the default, and the only valid value when depends_on is a + // required resource, because Crossplane never deletes a resource it didn't + // compose. + DEPENDENCY_LIFECYCLE_UNSPECIFIED = 0; + + // The resource may be created without waiting for what it depends on to be + // deleted. It must still exist and be ready before what it depends on is + // deleted. Use for a replacement that must exist before its predecessor is + // torn down. Only valid when depends_on is a composed resource. + DEPENDENCY_LIFECYCLE_CREATE_BEFORE_DESTROY = 1; +} + +// A RequiredResourceDependency identifies a resource the pipeline required, +// for the purpose of ordering. +message RequiredResourceDependency { + // The requirement name. A key into a RunFunctionRequest's required_resources + // map, and into a RunFunctionResponse's requirements.resources map. + string requirement_name = 1; + + // Optional name of a single resource within the set the requirement matched. + // If unset, every resource the requirement matched must be ready. + optional string name = 2; + + // Namespace of name for a namespaced resource. Leave unset for a + // cluster-scoped resource. name and namespace together identify one match. + optional string namespace = 3; +} + // A RunFunctionResponse contains the result of a function run. message RunFunctionResponse { // Metadata pertaining to this response. @@ -158,6 +234,14 @@ message RunFunctionResponse { // // Only Operations use function output. XRs will discard any function output. optional google.protobuf.Struct output = 7; + + // Optional ordering constraints over composed resources. A function that has + // an opinion about ordering must return the full set it wants going forward, + // including edges it received and still wants. Leaving this field unset means + // "no opinion" - Crossplane carries forward whatever it sent in the request. + // + // Dependencies are only used for composition. They're ignored by Operations. + Dependencies dependencies = 8; } // RequestMeta contains metadata pertaining to a RunFunctionRequest. @@ -203,6 +287,11 @@ enum Capability { // OpenAPI schemas and Crossplane will return them in required_schemas. Added // in Crossplane v2.2. CAPABILITY_REQUIRED_SCHEMAS = 5; + + // Crossplane supports the dependencies field. Functions can declare ordering + // constraints over composed resources, and Crossplane will sequence the + // resources it creates, updates and deletes accordingly. + CAPABILITY_DEPENDENCIES = 6; } // Requirements that must be satisfied for a function to run successfully. diff --git a/crossplane/function/proto/v1beta1/run_function_pb2.py b/crossplane/function/proto/v1beta1/run_function_pb2.py index 3f3ebd4..93b6f9a 100644 --- a/crossplane/function/proto/v1beta1/run_function_pb2.py +++ b/crossplane/function/proto/v1beta1/run_function_pb2.py @@ -26,7 +26,7 @@ from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n4crossplane/function/proto/v1beta1/run_function.proto\x12\x1e\x61piextensions.fn.proto.v1beta1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\"\xc3\x08\n\x12RunFunctionRequest\x12\x39\n\x04meta\x18\x01 \x01(\x0b\x32+.apiextensions.fn.proto.v1beta1.RequestMeta\x12\x37\n\x08observed\x18\x02 \x01(\x0b\x32%.apiextensions.fn.proto.v1beta1.State\x12\x36\n\x07\x64\x65sired\x18\x03 \x01(\x0b\x32%.apiextensions.fn.proto.v1beta1.State\x12+\n\x05input\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x12-\n\x07\x63ontext\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructH\x01\x88\x01\x01\x12\x63\n\x0f\x65xtra_resources\x18\x06 \x03(\x0b\x32\x46.apiextensions.fn.proto.v1beta1.RunFunctionRequest.ExtraResourcesEntryB\x02\x18\x01\x12X\n\x0b\x63redentials\x18\x07 \x03(\x0b\x32\x43.apiextensions.fn.proto.v1beta1.RunFunctionRequest.CredentialsEntry\x12\x65\n\x12required_resources\x18\x08 \x03(\x0b\x32I.apiextensions.fn.proto.v1beta1.RunFunctionRequest.RequiredResourcesEntry\x12\x61\n\x10required_schemas\x18\t \x03(\x0b\x32G.apiextensions.fn.proto.v1beta1.RunFunctionRequest.RequiredSchemasEntry\x1a`\n\x13\x45xtraResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x01(\x0b\x32).apiextensions.fn.proto.v1beta1.Resources:\x02\x38\x01\x1a_\n\x10\x43redentialsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.apiextensions.fn.proto.v1beta1.Credentials:\x02\x38\x01\x1a\x63\n\x16RequiredResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x01(\x0b\x32).apiextensions.fn.proto.v1beta1.Resources:\x02\x38\x01\x1a^\n\x14RequiredSchemasEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32&.apiextensions.fn.proto.v1beta1.Schema:\x02\x38\x01\x42\x08\n\x06_inputB\n\n\x08_context\"b\n\x0b\x43redentials\x12I\n\x0f\x63redential_data\x18\x01 \x01(\x0b\x32..apiextensions.fn.proto.v1beta1.CredentialDataH\x00\x42\x08\n\x06source\"\x85\x01\n\x0e\x43redentialData\x12\x46\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x38.apiextensions.fn.proto.v1beta1.CredentialData.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"D\n\tResources\x12\x37\n\x05items\x18\x01 \x03(\x0b\x32(.apiextensions.fn.proto.v1beta1.Resource\"\xb9\x03\n\x13RunFunctionResponse\x12:\n\x04meta\x18\x01 \x01(\x0b\x32,.apiextensions.fn.proto.v1beta1.ResponseMeta\x12\x36\n\x07\x64\x65sired\x18\x02 \x01(\x0b\x32%.apiextensions.fn.proto.v1beta1.State\x12\x37\n\x07results\x18\x03 \x03(\x0b\x32&.apiextensions.fn.proto.v1beta1.Result\x12-\n\x07\x63ontext\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x12\x42\n\x0crequirements\x18\x05 \x01(\x0b\x32,.apiextensions.fn.proto.v1beta1.Requirements\x12=\n\nconditions\x18\x06 \x03(\x0b\x32).apiextensions.fn.proto.v1beta1.Condition\x12,\n\x06output\x18\x07 \x01(\x0b\x32\x17.google.protobuf.StructH\x01\x88\x01\x01\x42\n\n\x08_contextB\t\n\x07_output\"\\\n\x0bRequestMeta\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12@\n\x0c\x63\x61pabilities\x18\x02 \x03(\x0e\x32*.apiextensions.fn.proto.v1beta1.Capability\"\xb6\x04\n\x0cRequirements\x12]\n\x0f\x65xtra_resources\x18\x01 \x03(\x0b\x32@.apiextensions.fn.proto.v1beta1.Requirements.ExtraResourcesEntryB\x02\x18\x01\x12N\n\tresources\x18\x02 \x03(\x0b\x32;.apiextensions.fn.proto.v1beta1.Requirements.ResourcesEntry\x12J\n\x07schemas\x18\x03 \x03(\x0b\x32\x39.apiextensions.fn.proto.v1beta1.Requirements.SchemasEntry\x1ag\n\x13\x45xtraResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.apiextensions.fn.proto.v1beta1.ResourceSelector:\x02\x38\x01\x1a\x62\n\x0eResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.apiextensions.fn.proto.v1beta1.ResourceSelector:\x02\x38\x01\x1a^\n\x0cSchemasEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12=\n\x05value\x18\x02 \x01(\x0b\x32..apiextensions.fn.proto.v1beta1.SchemaSelector:\x02\x38\x01\"3\n\x0eSchemaSelector\x12\x13\n\x0b\x61pi_version\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\"I\n\x06Schema\x12\x30\n\nopenapi_v3\x18\x01 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x42\r\n\x0b_openapi_v3\"\xbf\x01\n\x10ResourceSelector\x12\x13\n\x0b\x61pi_version\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x14\n\nmatch_name\x18\x03 \x01(\tH\x00\x12\x43\n\x0cmatch_labels\x18\x04 \x01(\x0b\x32+.apiextensions.fn.proto.v1beta1.MatchLabelsH\x00\x12\x16\n\tnamespace\x18\x05 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05matchB\x0c\n\n_namespace\"\x85\x01\n\x0bMatchLabels\x12G\n\x06labels\x18\x01 \x03(\x0b\x32\x37.apiextensions.fn.proto.v1beta1.MatchLabels.LabelsEntry\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"P\n\x0cResponseMeta\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12+\n\x03ttl\x18\x02 \x01(\x0b\x32\x19.google.protobuf.DurationH\x00\x88\x01\x01\x42\x06\n\x04_ttl\"\xe9\x01\n\x05State\x12;\n\tcomposite\x18\x01 \x01(\x0b\x32(.apiextensions.fn.proto.v1beta1.Resource\x12G\n\tresources\x18\x02 \x03(\x0b\x32\x34.apiextensions.fn.proto.v1beta1.State.ResourcesEntry\x1aZ\n\x0eResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x37\n\x05value\x18\x02 \x01(\x0b\x32(.apiextensions.fn.proto.v1beta1.Resource:\x02\x38\x01\"\x82\x02\n\x08Resource\x12)\n\x08resource\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12[\n\x12\x63onnection_details\x18\x02 \x03(\x0b\x32?.apiextensions.fn.proto.v1beta1.Resource.ConnectionDetailsEntry\x12\x34\n\x05ready\x18\x03 \x01(\x0e\x32%.apiextensions.fn.proto.v1beta1.Ready\x1a\x38\n\x16\x43onnectionDetailsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"\xbd\x01\n\x06Result\x12:\n\x08severity\x18\x01 \x01(\x0e\x32(.apiextensions.fn.proto.v1beta1.Severity\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x13\n\x06reason\x18\x03 \x01(\tH\x00\x88\x01\x01\x12;\n\x06target\x18\x04 \x01(\x0e\x32&.apiextensions.fn.proto.v1beta1.TargetH\x01\x88\x01\x01\x42\t\n\x07_reasonB\t\n\x07_target\"\xcb\x01\n\tCondition\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x36\n\x06status\x18\x02 \x01(\x0e\x32&.apiextensions.fn.proto.v1beta1.Status\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x14\n\x07message\x18\x04 \x01(\tH\x00\x88\x01\x01\x12;\n\x06target\x18\x05 \x01(\x0e\x32&.apiextensions.fn.proto.v1beta1.TargetH\x01\x88\x01\x01\x42\n\n\x08_messageB\t\n\x07_target*\xc0\x01\n\nCapability\x12\x1a\n\x16\x43\x41PABILITY_UNSPECIFIED\x10\x00\x12\x1b\n\x17\x43\x41PABILITY_CAPABILITIES\x10\x01\x12!\n\x1d\x43\x41PABILITY_REQUIRED_RESOURCES\x10\x02\x12\x1a\n\x16\x43\x41PABILITY_CREDENTIALS\x10\x03\x12\x19\n\x15\x43\x41PABILITY_CONDITIONS\x10\x04\x12\x1f\n\x1b\x43\x41PABILITY_REQUIRED_SCHEMAS\x10\x05*?\n\x05Ready\x12\x15\n\x11READY_UNSPECIFIED\x10\x00\x12\x0e\n\nREADY_TRUE\x10\x01\x12\x0f\n\x0bREADY_FALSE\x10\x02*c\n\x08Severity\x12\x18\n\x14SEVERITY_UNSPECIFIED\x10\x00\x12\x12\n\x0eSEVERITY_FATAL\x10\x01\x12\x14\n\x10SEVERITY_WARNING\x10\x02\x12\x13\n\x0fSEVERITY_NORMAL\x10\x03*V\n\x06Target\x12\x16\n\x12TARGET_UNSPECIFIED\x10\x00\x12\x14\n\x10TARGET_COMPOSITE\x10\x01\x12\x1e\n\x1aTARGET_COMPOSITE_AND_CLAIM\x10\x02*\x7f\n\x06Status\x12 \n\x1cSTATUS_CONDITION_UNSPECIFIED\x10\x00\x12\x1c\n\x18STATUS_CONDITION_UNKNOWN\x10\x01\x12\x19\n\x15STATUS_CONDITION_TRUE\x10\x02\x12\x1a\n\x16STATUS_CONDITION_FALSE\x10\x03\x32\x91\x01\n\x15\x46unctionRunnerService\x12x\n\x0bRunFunction\x12\x32.apiextensions.fn.proto.v1beta1.RunFunctionRequest\x1a\x33.apiextensions.fn.proto.v1beta1.RunFunctionResponse\"\x00\x42\x36Z4github.com/crossplane/crossplane/v2/proto/fn/v1beta1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n4crossplane/function/proto/v1beta1/run_function.proto\x12\x1e\x61piextensions.fn.proto.v1beta1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1cgoogle/protobuf/struct.proto\"\x87\t\n\x12RunFunctionRequest\x12\x39\n\x04meta\x18\x01 \x01(\x0b\x32+.apiextensions.fn.proto.v1beta1.RequestMeta\x12\x37\n\x08observed\x18\x02 \x01(\x0b\x32%.apiextensions.fn.proto.v1beta1.State\x12\x36\n\x07\x64\x65sired\x18\x03 \x01(\x0b\x32%.apiextensions.fn.proto.v1beta1.State\x12+\n\x05input\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x12-\n\x07\x63ontext\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructH\x01\x88\x01\x01\x12\x63\n\x0f\x65xtra_resources\x18\x06 \x03(\x0b\x32\x46.apiextensions.fn.proto.v1beta1.RunFunctionRequest.ExtraResourcesEntryB\x02\x18\x01\x12X\n\x0b\x63redentials\x18\x07 \x03(\x0b\x32\x43.apiextensions.fn.proto.v1beta1.RunFunctionRequest.CredentialsEntry\x12\x65\n\x12required_resources\x18\x08 \x03(\x0b\x32I.apiextensions.fn.proto.v1beta1.RunFunctionRequest.RequiredResourcesEntry\x12\x61\n\x10required_schemas\x18\t \x03(\x0b\x32G.apiextensions.fn.proto.v1beta1.RunFunctionRequest.RequiredSchemasEntry\x12\x42\n\x0c\x64\x65pendencies\x18\n \x01(\x0b\x32,.apiextensions.fn.proto.v1beta1.Dependencies\x1a`\n\x13\x45xtraResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x01(\x0b\x32).apiextensions.fn.proto.v1beta1.Resources:\x02\x38\x01\x1a_\n\x10\x43redentialsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.apiextensions.fn.proto.v1beta1.Credentials:\x02\x38\x01\x1a\x63\n\x16RequiredResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x38\n\x05value\x18\x02 \x01(\x0b\x32).apiextensions.fn.proto.v1beta1.Resources:\x02\x38\x01\x1a^\n\x14RequiredSchemasEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32&.apiextensions.fn.proto.v1beta1.Schema:\x02\x38\x01\x42\x08\n\x06_inputB\n\n\x08_context\"b\n\x0b\x43redentials\x12I\n\x0f\x63redential_data\x18\x01 \x01(\x0b\x32..apiextensions.fn.proto.v1beta1.CredentialDataH\x00\x42\x08\n\x06source\"\x85\x01\n\x0e\x43redentialData\x12\x46\n\x04\x64\x61ta\x18\x01 \x03(\x0b\x32\x38.apiextensions.fn.proto.v1beta1.CredentialData.DataEntry\x1a+\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"D\n\tResources\x12\x37\n\x05items\x18\x01 \x03(\x0b\x32(.apiextensions.fn.proto.v1beta1.Resource\"I\n\x0c\x44\x65pendencies\x12\x39\n\x05items\x18\x01 \x03(\x0b\x32*.apiextensions.fn.proto.v1beta1.Dependency\"\xea\x01\n\nDependency\x12\x10\n\x08resource\x18\x01 \x01(\t\x12\x1b\n\x11\x63omposed_resource\x18\x02 \x01(\tH\x00\x12W\n\x11required_resource\x18\x04 \x01(\x0b\x32:.apiextensions.fn.proto.v1beta1.RequiredResourceDependencyH\x00\x12\x46\n\tlifecycle\x18\x03 \x01(\x0e\x32\x33.apiextensions.fn.proto.v1beta1.DependencyLifecycleB\x0c\n\ndepends_on\"x\n\x1aRequiredResourceDependency\x12\x18\n\x10requirement_name\x18\x01 \x01(\t\x12\x11\n\x04name\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x16\n\tnamespace\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05_nameB\x0c\n\n_namespace\"\xfd\x03\n\x13RunFunctionResponse\x12:\n\x04meta\x18\x01 \x01(\x0b\x32,.apiextensions.fn.proto.v1beta1.ResponseMeta\x12\x36\n\x07\x64\x65sired\x18\x02 \x01(\x0b\x32%.apiextensions.fn.proto.v1beta1.State\x12\x37\n\x07results\x18\x03 \x03(\x0b\x32&.apiextensions.fn.proto.v1beta1.Result\x12-\n\x07\x63ontext\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x12\x42\n\x0crequirements\x18\x05 \x01(\x0b\x32,.apiextensions.fn.proto.v1beta1.Requirements\x12=\n\nconditions\x18\x06 \x03(\x0b\x32).apiextensions.fn.proto.v1beta1.Condition\x12,\n\x06output\x18\x07 \x01(\x0b\x32\x17.google.protobuf.StructH\x01\x88\x01\x01\x12\x42\n\x0c\x64\x65pendencies\x18\x08 \x01(\x0b\x32,.apiextensions.fn.proto.v1beta1.DependenciesB\n\n\x08_contextB\t\n\x07_output\"\\\n\x0bRequestMeta\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12@\n\x0c\x63\x61pabilities\x18\x02 \x03(\x0e\x32*.apiextensions.fn.proto.v1beta1.Capability\"\xb6\x04\n\x0cRequirements\x12]\n\x0f\x65xtra_resources\x18\x01 \x03(\x0b\x32@.apiextensions.fn.proto.v1beta1.Requirements.ExtraResourcesEntryB\x02\x18\x01\x12N\n\tresources\x18\x02 \x03(\x0b\x32;.apiextensions.fn.proto.v1beta1.Requirements.ResourcesEntry\x12J\n\x07schemas\x18\x03 \x03(\x0b\x32\x39.apiextensions.fn.proto.v1beta1.Requirements.SchemasEntry\x1ag\n\x13\x45xtraResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.apiextensions.fn.proto.v1beta1.ResourceSelector:\x02\x38\x01\x1a\x62\n\x0eResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12?\n\x05value\x18\x02 \x01(\x0b\x32\x30.apiextensions.fn.proto.v1beta1.ResourceSelector:\x02\x38\x01\x1a^\n\x0cSchemasEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12=\n\x05value\x18\x02 \x01(\x0b\x32..apiextensions.fn.proto.v1beta1.SchemaSelector:\x02\x38\x01\"3\n\x0eSchemaSelector\x12\x13\n\x0b\x61pi_version\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\"I\n\x06Schema\x12\x30\n\nopenapi_v3\x18\x01 \x01(\x0b\x32\x17.google.protobuf.StructH\x00\x88\x01\x01\x42\r\n\x0b_openapi_v3\"\xbf\x01\n\x10ResourceSelector\x12\x13\n\x0b\x61pi_version\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x14\n\nmatch_name\x18\x03 \x01(\tH\x00\x12\x43\n\x0cmatch_labels\x18\x04 \x01(\x0b\x32+.apiextensions.fn.proto.v1beta1.MatchLabelsH\x00\x12\x16\n\tnamespace\x18\x05 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05matchB\x0c\n\n_namespace\"\x85\x01\n\x0bMatchLabels\x12G\n\x06labels\x18\x01 \x03(\x0b\x32\x37.apiextensions.fn.proto.v1beta1.MatchLabels.LabelsEntry\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"P\n\x0cResponseMeta\x12\x0b\n\x03tag\x18\x01 \x01(\t\x12+\n\x03ttl\x18\x02 \x01(\x0b\x32\x19.google.protobuf.DurationH\x00\x88\x01\x01\x42\x06\n\x04_ttl\"\xe9\x01\n\x05State\x12;\n\tcomposite\x18\x01 \x01(\x0b\x32(.apiextensions.fn.proto.v1beta1.Resource\x12G\n\tresources\x18\x02 \x03(\x0b\x32\x34.apiextensions.fn.proto.v1beta1.State.ResourcesEntry\x1aZ\n\x0eResourcesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x37\n\x05value\x18\x02 \x01(\x0b\x32(.apiextensions.fn.proto.v1beta1.Resource:\x02\x38\x01\"\x82\x02\n\x08Resource\x12)\n\x08resource\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12[\n\x12\x63onnection_details\x18\x02 \x03(\x0b\x32?.apiextensions.fn.proto.v1beta1.Resource.ConnectionDetailsEntry\x12\x34\n\x05ready\x18\x03 \x01(\x0e\x32%.apiextensions.fn.proto.v1beta1.Ready\x1a\x38\n\x16\x43onnectionDetailsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\"\xbd\x01\n\x06Result\x12:\n\x08severity\x18\x01 \x01(\x0e\x32(.apiextensions.fn.proto.v1beta1.Severity\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x13\n\x06reason\x18\x03 \x01(\tH\x00\x88\x01\x01\x12;\n\x06target\x18\x04 \x01(\x0e\x32&.apiextensions.fn.proto.v1beta1.TargetH\x01\x88\x01\x01\x42\t\n\x07_reasonB\t\n\x07_target\"\xcb\x01\n\tCondition\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x36\n\x06status\x18\x02 \x01(\x0e\x32&.apiextensions.fn.proto.v1beta1.Status\x12\x0e\n\x06reason\x18\x03 \x01(\t\x12\x14\n\x07message\x18\x04 \x01(\tH\x00\x88\x01\x01\x12;\n\x06target\x18\x05 \x01(\x0e\x32&.apiextensions.fn.proto.v1beta1.TargetH\x01\x88\x01\x01\x42\n\n\x08_messageB\t\n\x07_target*k\n\x13\x44\x65pendencyLifecycle\x12$\n DEPENDENCY_LIFECYCLE_UNSPECIFIED\x10\x00\x12.\n*DEPENDENCY_LIFECYCLE_CREATE_BEFORE_DESTROY\x10\x01*\xdd\x01\n\nCapability\x12\x1a\n\x16\x43\x41PABILITY_UNSPECIFIED\x10\x00\x12\x1b\n\x17\x43\x41PABILITY_CAPABILITIES\x10\x01\x12!\n\x1d\x43\x41PABILITY_REQUIRED_RESOURCES\x10\x02\x12\x1a\n\x16\x43\x41PABILITY_CREDENTIALS\x10\x03\x12\x19\n\x15\x43\x41PABILITY_CONDITIONS\x10\x04\x12\x1f\n\x1b\x43\x41PABILITY_REQUIRED_SCHEMAS\x10\x05\x12\x1b\n\x17\x43\x41PABILITY_DEPENDENCIES\x10\x06*?\n\x05Ready\x12\x15\n\x11READY_UNSPECIFIED\x10\x00\x12\x0e\n\nREADY_TRUE\x10\x01\x12\x0f\n\x0bREADY_FALSE\x10\x02*c\n\x08Severity\x12\x18\n\x14SEVERITY_UNSPECIFIED\x10\x00\x12\x12\n\x0eSEVERITY_FATAL\x10\x01\x12\x14\n\x10SEVERITY_WARNING\x10\x02\x12\x13\n\x0fSEVERITY_NORMAL\x10\x03*V\n\x06Target\x12\x16\n\x12TARGET_UNSPECIFIED\x10\x00\x12\x14\n\x10TARGET_COMPOSITE\x10\x01\x12\x1e\n\x1aTARGET_COMPOSITE_AND_CLAIM\x10\x02*\x7f\n\x06Status\x12 \n\x1cSTATUS_CONDITION_UNSPECIFIED\x10\x00\x12\x1c\n\x18STATUS_CONDITION_UNKNOWN\x10\x01\x12\x19\n\x15STATUS_CONDITION_TRUE\x10\x02\x12\x1a\n\x16STATUS_CONDITION_FALSE\x10\x03\x32\x91\x01\n\x15\x46unctionRunnerService\x12x\n\x0bRunFunction\x12\x32.apiextensions.fn.proto.v1beta1.RunFunctionRequest\x1a\x33.apiextensions.fn.proto.v1beta1.RunFunctionResponse\"\x00\x42\x36Z4github.com/crossplane/crossplane/v2/proto/fn/v1beta1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -60,70 +60,78 @@ _globals['_STATE_RESOURCESENTRY']._serialized_options = b'8\001' _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._loaded_options = None _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._serialized_options = b'8\001' - _globals['_CAPABILITY']._serialized_start=4093 - _globals['_CAPABILITY']._serialized_end=4285 - _globals['_READY']._serialized_start=4287 - _globals['_READY']._serialized_end=4350 - _globals['_SEVERITY']._serialized_start=4352 - _globals['_SEVERITY']._serialized_end=4451 - _globals['_TARGET']._serialized_start=4453 - _globals['_TARGET']._serialized_end=4539 - _globals['_STATUS']._serialized_start=4541 - _globals['_STATUS']._serialized_end=4668 + _globals['_DEPENDENCYLIFECYCLE']._serialized_start=4662 + _globals['_DEPENDENCYLIFECYCLE']._serialized_end=4769 + _globals['_CAPABILITY']._serialized_start=4772 + _globals['_CAPABILITY']._serialized_end=4993 + _globals['_READY']._serialized_start=4995 + _globals['_READY']._serialized_end=5058 + _globals['_SEVERITY']._serialized_start=5060 + _globals['_SEVERITY']._serialized_end=5159 + _globals['_TARGET']._serialized_start=5161 + _globals['_TARGET']._serialized_end=5247 + _globals['_STATUS']._serialized_start=5249 + _globals['_STATUS']._serialized_end=5376 _globals['_RUNFUNCTIONREQUEST']._serialized_start=151 - _globals['_RUNFUNCTIONREQUEST']._serialized_end=1242 - _globals['_RUNFUNCTIONREQUEST_EXTRARESOURCESENTRY']._serialized_start=830 - _globals['_RUNFUNCTIONREQUEST_EXTRARESOURCESENTRY']._serialized_end=926 - _globals['_RUNFUNCTIONREQUEST_CREDENTIALSENTRY']._serialized_start=928 - _globals['_RUNFUNCTIONREQUEST_CREDENTIALSENTRY']._serialized_end=1023 - _globals['_RUNFUNCTIONREQUEST_REQUIREDRESOURCESENTRY']._serialized_start=1025 - _globals['_RUNFUNCTIONREQUEST_REQUIREDRESOURCESENTRY']._serialized_end=1124 - _globals['_RUNFUNCTIONREQUEST_REQUIREDSCHEMASENTRY']._serialized_start=1126 - _globals['_RUNFUNCTIONREQUEST_REQUIREDSCHEMASENTRY']._serialized_end=1220 - _globals['_CREDENTIALS']._serialized_start=1244 - _globals['_CREDENTIALS']._serialized_end=1342 - _globals['_CREDENTIALDATA']._serialized_start=1345 - _globals['_CREDENTIALDATA']._serialized_end=1478 - _globals['_CREDENTIALDATA_DATAENTRY']._serialized_start=1435 - _globals['_CREDENTIALDATA_DATAENTRY']._serialized_end=1478 - _globals['_RESOURCES']._serialized_start=1480 - _globals['_RESOURCES']._serialized_end=1548 - _globals['_RUNFUNCTIONRESPONSE']._serialized_start=1551 - _globals['_RUNFUNCTIONRESPONSE']._serialized_end=1992 - _globals['_REQUESTMETA']._serialized_start=1994 - _globals['_REQUESTMETA']._serialized_end=2086 - _globals['_REQUIREMENTS']._serialized_start=2089 - _globals['_REQUIREMENTS']._serialized_end=2655 - _globals['_REQUIREMENTS_EXTRARESOURCESENTRY']._serialized_start=2356 - _globals['_REQUIREMENTS_EXTRARESOURCESENTRY']._serialized_end=2459 - _globals['_REQUIREMENTS_RESOURCESENTRY']._serialized_start=2461 - _globals['_REQUIREMENTS_RESOURCESENTRY']._serialized_end=2559 - _globals['_REQUIREMENTS_SCHEMASENTRY']._serialized_start=2561 - _globals['_REQUIREMENTS_SCHEMASENTRY']._serialized_end=2655 - _globals['_SCHEMASELECTOR']._serialized_start=2657 - _globals['_SCHEMASELECTOR']._serialized_end=2708 - _globals['_SCHEMA']._serialized_start=2710 - _globals['_SCHEMA']._serialized_end=2783 - _globals['_RESOURCESELECTOR']._serialized_start=2786 - _globals['_RESOURCESELECTOR']._serialized_end=2977 - _globals['_MATCHLABELS']._serialized_start=2980 - _globals['_MATCHLABELS']._serialized_end=3113 - _globals['_MATCHLABELS_LABELSENTRY']._serialized_start=3068 - _globals['_MATCHLABELS_LABELSENTRY']._serialized_end=3113 - _globals['_RESPONSEMETA']._serialized_start=3115 - _globals['_RESPONSEMETA']._serialized_end=3195 - _globals['_STATE']._serialized_start=3198 - _globals['_STATE']._serialized_end=3431 - _globals['_STATE_RESOURCESENTRY']._serialized_start=3341 - _globals['_STATE_RESOURCESENTRY']._serialized_end=3431 - _globals['_RESOURCE']._serialized_start=3434 - _globals['_RESOURCE']._serialized_end=3692 - _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._serialized_start=3636 - _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._serialized_end=3692 - _globals['_RESULT']._serialized_start=3695 - _globals['_RESULT']._serialized_end=3884 - _globals['_CONDITION']._serialized_start=3887 - _globals['_CONDITION']._serialized_end=4090 - _globals['_FUNCTIONRUNNERSERVICE']._serialized_start=4671 - _globals['_FUNCTIONRUNNERSERVICE']._serialized_end=4816 + _globals['_RUNFUNCTIONREQUEST']._serialized_end=1310 + _globals['_RUNFUNCTIONREQUEST_EXTRARESOURCESENTRY']._serialized_start=898 + _globals['_RUNFUNCTIONREQUEST_EXTRARESOURCESENTRY']._serialized_end=994 + _globals['_RUNFUNCTIONREQUEST_CREDENTIALSENTRY']._serialized_start=996 + _globals['_RUNFUNCTIONREQUEST_CREDENTIALSENTRY']._serialized_end=1091 + _globals['_RUNFUNCTIONREQUEST_REQUIREDRESOURCESENTRY']._serialized_start=1093 + _globals['_RUNFUNCTIONREQUEST_REQUIREDRESOURCESENTRY']._serialized_end=1192 + _globals['_RUNFUNCTIONREQUEST_REQUIREDSCHEMASENTRY']._serialized_start=1194 + _globals['_RUNFUNCTIONREQUEST_REQUIREDSCHEMASENTRY']._serialized_end=1288 + _globals['_CREDENTIALS']._serialized_start=1312 + _globals['_CREDENTIALS']._serialized_end=1410 + _globals['_CREDENTIALDATA']._serialized_start=1413 + _globals['_CREDENTIALDATA']._serialized_end=1546 + _globals['_CREDENTIALDATA_DATAENTRY']._serialized_start=1503 + _globals['_CREDENTIALDATA_DATAENTRY']._serialized_end=1546 + _globals['_RESOURCES']._serialized_start=1548 + _globals['_RESOURCES']._serialized_end=1616 + _globals['_DEPENDENCIES']._serialized_start=1618 + _globals['_DEPENDENCIES']._serialized_end=1691 + _globals['_DEPENDENCY']._serialized_start=1694 + _globals['_DEPENDENCY']._serialized_end=1928 + _globals['_REQUIREDRESOURCEDEPENDENCY']._serialized_start=1930 + _globals['_REQUIREDRESOURCEDEPENDENCY']._serialized_end=2050 + _globals['_RUNFUNCTIONRESPONSE']._serialized_start=2053 + _globals['_RUNFUNCTIONRESPONSE']._serialized_end=2562 + _globals['_REQUESTMETA']._serialized_start=2564 + _globals['_REQUESTMETA']._serialized_end=2656 + _globals['_REQUIREMENTS']._serialized_start=2659 + _globals['_REQUIREMENTS']._serialized_end=3225 + _globals['_REQUIREMENTS_EXTRARESOURCESENTRY']._serialized_start=2926 + _globals['_REQUIREMENTS_EXTRARESOURCESENTRY']._serialized_end=3029 + _globals['_REQUIREMENTS_RESOURCESENTRY']._serialized_start=3031 + _globals['_REQUIREMENTS_RESOURCESENTRY']._serialized_end=3129 + _globals['_REQUIREMENTS_SCHEMASENTRY']._serialized_start=3131 + _globals['_REQUIREMENTS_SCHEMASENTRY']._serialized_end=3225 + _globals['_SCHEMASELECTOR']._serialized_start=3227 + _globals['_SCHEMASELECTOR']._serialized_end=3278 + _globals['_SCHEMA']._serialized_start=3280 + _globals['_SCHEMA']._serialized_end=3353 + _globals['_RESOURCESELECTOR']._serialized_start=3356 + _globals['_RESOURCESELECTOR']._serialized_end=3547 + _globals['_MATCHLABELS']._serialized_start=3550 + _globals['_MATCHLABELS']._serialized_end=3683 + _globals['_MATCHLABELS_LABELSENTRY']._serialized_start=3638 + _globals['_MATCHLABELS_LABELSENTRY']._serialized_end=3683 + _globals['_RESPONSEMETA']._serialized_start=3685 + _globals['_RESPONSEMETA']._serialized_end=3765 + _globals['_STATE']._serialized_start=3768 + _globals['_STATE']._serialized_end=4001 + _globals['_STATE_RESOURCESENTRY']._serialized_start=3911 + _globals['_STATE_RESOURCESENTRY']._serialized_end=4001 + _globals['_RESOURCE']._serialized_start=4004 + _globals['_RESOURCE']._serialized_end=4262 + _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._serialized_start=4206 + _globals['_RESOURCE_CONNECTIONDETAILSENTRY']._serialized_end=4262 + _globals['_RESULT']._serialized_start=4265 + _globals['_RESULT']._serialized_end=4454 + _globals['_CONDITION']._serialized_start=4457 + _globals['_CONDITION']._serialized_end=4660 + _globals['_FUNCTIONRUNNERSERVICE']._serialized_start=5379 + _globals['_FUNCTIONRUNNERSERVICE']._serialized_end=5524 # @@protoc_insertion_point(module_scope) diff --git a/crossplane/function/proto/v1beta1/run_function_pb2.pyi b/crossplane/function/proto/v1beta1/run_function_pb2.pyi index d0d29ca..6aeafc9 100644 --- a/crossplane/function/proto/v1beta1/run_function_pb2.pyi +++ b/crossplane/function/proto/v1beta1/run_function_pb2.pyi @@ -11,6 +11,11 @@ from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor +class DependencyLifecycle(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + DEPENDENCY_LIFECYCLE_UNSPECIFIED: _ClassVar[DependencyLifecycle] + DEPENDENCY_LIFECYCLE_CREATE_BEFORE_DESTROY: _ClassVar[DependencyLifecycle] + class Capability(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () CAPABILITY_UNSPECIFIED: _ClassVar[Capability] @@ -19,6 +24,7 @@ class Capability(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): CAPABILITY_CREDENTIALS: _ClassVar[Capability] CAPABILITY_CONDITIONS: _ClassVar[Capability] CAPABILITY_REQUIRED_SCHEMAS: _ClassVar[Capability] + CAPABILITY_DEPENDENCIES: _ClassVar[Capability] class Ready(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () @@ -45,12 +51,15 @@ class Status(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): STATUS_CONDITION_UNKNOWN: _ClassVar[Status] STATUS_CONDITION_TRUE: _ClassVar[Status] STATUS_CONDITION_FALSE: _ClassVar[Status] +DEPENDENCY_LIFECYCLE_UNSPECIFIED: DependencyLifecycle +DEPENDENCY_LIFECYCLE_CREATE_BEFORE_DESTROY: DependencyLifecycle CAPABILITY_UNSPECIFIED: Capability CAPABILITY_CAPABILITIES: Capability CAPABILITY_REQUIRED_RESOURCES: Capability CAPABILITY_CREDENTIALS: Capability CAPABILITY_CONDITIONS: Capability CAPABILITY_REQUIRED_SCHEMAS: Capability +CAPABILITY_DEPENDENCIES: Capability READY_UNSPECIFIED: Ready READY_TRUE: Ready READY_FALSE: Ready @@ -67,7 +76,7 @@ STATUS_CONDITION_TRUE: Status STATUS_CONDITION_FALSE: Status class RunFunctionRequest(_message.Message): - __slots__ = ("meta", "observed", "desired", "input", "context", "extra_resources", "credentials", "required_resources", "required_schemas") + __slots__ = ("meta", "observed", "desired", "input", "context", "extra_resources", "credentials", "required_resources", "required_schemas", "dependencies") class ExtraResourcesEntry(_message.Message): __slots__ = ("key", "value") KEY_FIELD_NUMBER: _ClassVar[int] @@ -105,6 +114,7 @@ class RunFunctionRequest(_message.Message): CREDENTIALS_FIELD_NUMBER: _ClassVar[int] REQUIRED_RESOURCES_FIELD_NUMBER: _ClassVar[int] REQUIRED_SCHEMAS_FIELD_NUMBER: _ClassVar[int] + DEPENDENCIES_FIELD_NUMBER: _ClassVar[int] meta: RequestMeta observed: State desired: State @@ -114,7 +124,8 @@ class RunFunctionRequest(_message.Message): credentials: _containers.MessageMap[str, Credentials] required_resources: _containers.MessageMap[str, Resources] required_schemas: _containers.MessageMap[str, Schema] - def __init__(self, meta: _Optional[_Union[RequestMeta, _Mapping]] = ..., observed: _Optional[_Union[State, _Mapping]] = ..., desired: _Optional[_Union[State, _Mapping]] = ..., input: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., context: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., extra_resources: _Optional[_Mapping[str, Resources]] = ..., credentials: _Optional[_Mapping[str, Credentials]] = ..., required_resources: _Optional[_Mapping[str, Resources]] = ..., required_schemas: _Optional[_Mapping[str, Schema]] = ...) -> None: ... + dependencies: Dependencies + def __init__(self, meta: _Optional[_Union[RequestMeta, _Mapping]] = ..., observed: _Optional[_Union[State, _Mapping]] = ..., desired: _Optional[_Union[State, _Mapping]] = ..., input: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., context: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., extra_resources: _Optional[_Mapping[str, Resources]] = ..., credentials: _Optional[_Mapping[str, Credentials]] = ..., required_resources: _Optional[_Mapping[str, Resources]] = ..., required_schemas: _Optional[_Mapping[str, Schema]] = ..., dependencies: _Optional[_Union[Dependencies, _Mapping]] = ...) -> None: ... class Credentials(_message.Message): __slots__ = ("credential_data",) @@ -141,8 +152,36 @@ class Resources(_message.Message): items: _containers.RepeatedCompositeFieldContainer[Resource] def __init__(self, items: _Optional[_Iterable[_Union[Resource, _Mapping]]] = ...) -> None: ... +class Dependencies(_message.Message): + __slots__ = ("items",) + ITEMS_FIELD_NUMBER: _ClassVar[int] + items: _containers.RepeatedCompositeFieldContainer[Dependency] + def __init__(self, items: _Optional[_Iterable[_Union[Dependency, _Mapping]]] = ...) -> None: ... + +class Dependency(_message.Message): + __slots__ = ("resource", "composed_resource", "required_resource", "lifecycle") + RESOURCE_FIELD_NUMBER: _ClassVar[int] + COMPOSED_RESOURCE_FIELD_NUMBER: _ClassVar[int] + REQUIRED_RESOURCE_FIELD_NUMBER: _ClassVar[int] + LIFECYCLE_FIELD_NUMBER: _ClassVar[int] + resource: str + composed_resource: str + required_resource: RequiredResourceDependency + lifecycle: DependencyLifecycle + def __init__(self, resource: _Optional[str] = ..., composed_resource: _Optional[str] = ..., required_resource: _Optional[_Union[RequiredResourceDependency, _Mapping]] = ..., lifecycle: _Optional[_Union[DependencyLifecycle, str]] = ...) -> None: ... + +class RequiredResourceDependency(_message.Message): + __slots__ = ("requirement_name", "name", "namespace") + REQUIREMENT_NAME_FIELD_NUMBER: _ClassVar[int] + NAME_FIELD_NUMBER: _ClassVar[int] + NAMESPACE_FIELD_NUMBER: _ClassVar[int] + requirement_name: str + name: str + namespace: str + def __init__(self, requirement_name: _Optional[str] = ..., name: _Optional[str] = ..., namespace: _Optional[str] = ...) -> None: ... + class RunFunctionResponse(_message.Message): - __slots__ = ("meta", "desired", "results", "context", "requirements", "conditions", "output") + __slots__ = ("meta", "desired", "results", "context", "requirements", "conditions", "output", "dependencies") META_FIELD_NUMBER: _ClassVar[int] DESIRED_FIELD_NUMBER: _ClassVar[int] RESULTS_FIELD_NUMBER: _ClassVar[int] @@ -150,6 +189,7 @@ class RunFunctionResponse(_message.Message): REQUIREMENTS_FIELD_NUMBER: _ClassVar[int] CONDITIONS_FIELD_NUMBER: _ClassVar[int] OUTPUT_FIELD_NUMBER: _ClassVar[int] + DEPENDENCIES_FIELD_NUMBER: _ClassVar[int] meta: ResponseMeta desired: State results: _containers.RepeatedCompositeFieldContainer[Result] @@ -157,7 +197,8 @@ class RunFunctionResponse(_message.Message): requirements: Requirements conditions: _containers.RepeatedCompositeFieldContainer[Condition] output: _struct_pb2.Struct - def __init__(self, meta: _Optional[_Union[ResponseMeta, _Mapping]] = ..., desired: _Optional[_Union[State, _Mapping]] = ..., results: _Optional[_Iterable[_Union[Result, _Mapping]]] = ..., context: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., requirements: _Optional[_Union[Requirements, _Mapping]] = ..., conditions: _Optional[_Iterable[_Union[Condition, _Mapping]]] = ..., output: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ...) -> None: ... + dependencies: Dependencies + def __init__(self, meta: _Optional[_Union[ResponseMeta, _Mapping]] = ..., desired: _Optional[_Union[State, _Mapping]] = ..., results: _Optional[_Iterable[_Union[Result, _Mapping]]] = ..., context: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., requirements: _Optional[_Union[Requirements, _Mapping]] = ..., conditions: _Optional[_Iterable[_Union[Condition, _Mapping]]] = ..., output: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., dependencies: _Optional[_Union[Dependencies, _Mapping]] = ...) -> None: ... class RequestMeta(_message.Message): __slots__ = ("tag", "capabilities") diff --git a/crossplane/function/request.py b/crossplane/function/request.py index e059f16..839a340 100644 --- a/crossplane/function/request.py +++ b/crossplane/function/request.py @@ -224,3 +224,27 @@ def get_required_schema(req: fnv1.RunFunctionRequest, name: str) -> dict | None: return None return resource.struct_to_dict(schema.openapi_v3) + + +def get_dependencies(req: fnv1.RunFunctionRequest) -> list[fnv1.Dependency]: + """Get the ordering constraints accumulated so far in the pipeline. + + Args: + req: The RunFunctionRequest containing dependencies. + + Returns: + The dependencies returned by the functions that ran before this one. + + A dependency declares that one composed resource must be created after, + and deleted before, another resource. It expresses ordering only - it + doesn't move any data between resources. + + A function that has an opinion about ordering must return the full set it + wants going forward, including the edges it received here and still wants. + response.to copies them to the response for you, so most functions can + just add to them: + + rsp = response.to(req) + response.add_dependency(rsp, "database", "network") + """ + return list(req.dependencies.items) diff --git a/crossplane/function/response.py b/crossplane/function/response.py index d886725..4602dea 100644 --- a/crossplane/function/response.py +++ b/crossplane/function/response.py @@ -39,8 +39,15 @@ def to( Returns: A response to the supplied request. - The request's tag, desired resources, and context is automatically copied to - the response. Using response.to is a good pattern to ensure + The request's tag, desired resources, context, and dependencies are + automatically copied to the response. Using response.to is a good pattern + to ensure + + Dependencies are copied only if the request has them. An unset + Dependencies means "no opinion" and tells Crossplane to carry forward the + constraints it already has, while an empty one tells Crossplane to drop + them. Copying an unset field as an empty one would turn the former into + the latter. """ dttl = durationpb.Duration() dttl.FromTimedelta(ttl) @@ -48,6 +55,7 @@ def to( meta=fnv1.ResponseMeta(tag=req.meta.tag, ttl=dttl), desired=req.desired, context=req.context, + dependencies=req.dependencies if req.HasField("dependencies") else None, ) @@ -219,3 +227,108 @@ def require_schema( kind=kind, ) rsp.requirements.schemas[name].CopyFrom(selector) + + +def clear_dependencies(rsp: fnv1.RunFunctionResponse) -> None: + """Declare that no composed resources should be ordered. + + Args: + rsp: The RunFunctionResponse to update. + + This returns an empty set of dependencies, which tells Crossplane to drop + the constraints declared by the functions before this one. It's different + from leaving dependencies unset, which means "no opinion" and carries the + existing constraints forward. + + Note that response.to copies the request's dependencies to the response. + Call this on a response it created to drop them. + """ + rsp.dependencies.Clear() + rsp.dependencies.SetInParent() + + +def add_dependency( + rsp: fnv1.RunFunctionResponse, + resource_name: str, + depends_on: str, + *, + create_before_destroy: bool = False, +) -> None: + """Declare that one composed resource depends on another. + + Args: + rsp: The RunFunctionResponse to update. + resource_name: Name of the composed resource that has the dependency. A key + into the desired or observed state's resources. + depends_on: Name of the composed resource it depends on. Also a key + into the desired or observed state's resources. + create_before_destroy: Let the resource be created without waiting for + what it depends on to be deleted. Use for a replacement that must + exist before its predecessor is torn down. + + By default ordering is symmetric: the resource is created only once what + it depends on is ready, and what it depends on is deleted only once the + resource is gone. + + Dependencies express ordering only. They don't move any data between + resources. + + Remember that a function must return the full set of dependencies it + wants. response.to copies forward the ones the request carried, so build + on a response created by it rather than an empty one. + + Only Crossplane versions that advertise CAPABILITY_DEPENDENCIES honor + dependencies. Use request.has_capability to check before relying on them: + + if request.has_capability(req, fnv1.CAPABILITY_DEPENDENCIES): + response.add_dependency(rsp, "database", "network") + """ + lifecycle = ( + fnv1.DEPENDENCY_LIFECYCLE_CREATE_BEFORE_DESTROY + if create_before_destroy + else fnv1.DEPENDENCY_LIFECYCLE_UNSPECIFIED + ) + rsp.dependencies.items.append( + fnv1.Dependency( + resource=resource_name, + composed_resource=depends_on, + lifecycle=lifecycle, + ) + ) + + +def add_required_resource_dependency( + rsp: fnv1.RunFunctionResponse, + resource_name: str, + requirement_name: str, + *, + name: str | None = None, + namespace: str | None = None, +) -> None: + """Declare that a composed resource depends on a required resource. + + Args: + rsp: The RunFunctionResponse to update. + resource_name: Name of the composed resource that has the dependency. A key + into the desired or observed state's resources. + requirement_name: The requirement name, as passed to require_resources. + name: Name of a single resource within the set the requirement + matched. If unset, every matched resource must be ready. + namespace: Namespace of name, for a namespaced resource. Leave unset + for a cluster scoped resource. + + Crossplane never deletes a resource it didn't compose, so a dependency on + a required resource constrains only the order resources are created and + updated, never the order they're deleted. + """ + required = fnv1.RequiredResourceDependency(requirement_name=requirement_name) + + if name is not None: + required.name = name + + if namespace is not None: + required.namespace = namespace + + rsp.dependencies.items.append( + fnv1.Dependency(resource=resource_name, required_resource=required) + ) diff --git a/tests/test_dependency.py b/tests/test_dependency.py new file mode 100644 index 0000000..409a1f2 --- /dev/null +++ b/tests/test_dependency.py @@ -0,0 +1,669 @@ +# Copyright 2026 The Crossplane Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import typing +import unittest + +import pydantic +from google.protobuf import json_format + +from crossplane.function import dependency, logging, resource, response +from crossplane.function.proto.v1 import run_function_pb2 as fnv1 + +# Models shaped the way datamodel-codegen generates them from CRDs: JSON names +# as attribute names, and an alias only where the JSON name is a Python +# keyword. + + +class Metadata(pydantic.BaseModel): + name: str | None = None + annotations: dict[str, str] | None = None + + +class AtProvider(pydantic.BaseModel): + id: str | None = None + arn: str | None = None + + +class VPCStatus(pydantic.BaseModel): + atProvider: AtProvider | None = None # noqa: N815 # Generated models use JSON names. + + +class VPC(pydantic.BaseModel): + metadata: Metadata | None = None + status: VPCStatus | None = None + + +class Range(pydantic.BaseModel): + from_: str | None = pydantic.Field(None, alias="from") + + +class Pool(pydantic.BaseModel): + ranges: list[Range] | None = None + + +class SubnetForProvider(pydantic.BaseModel): + region: str | None = None + vpcId: str | None = None # noqa: N815 # Generated models use JSON names. + tags: list[str] | None = None + + +class SubnetSpec(pydantic.BaseModel): + forProvider: SubnetForProvider # noqa: N815 # Generated models use JSON names. + + +class Subnet(pydantic.BaseModel): + apiVersion: str = "ec2.aws.m.upbound.io/v1beta1" # noqa: N815 # Generated models use JSON names. + kind: str = "Subnet" + spec: SubnetSpec + + +def observed(**resources: dict) -> fnv1.State: + return fnv1.State( + resources={ + name: fnv1.Resource(resource=resource.dict_to_struct(body)) + for name, body in resources.items() + } + ) + + +VPC_OBSERVED = { + "apiVersion": "ec2.aws.m.upbound.io/v1beta1", + "kind": "VPC", + "metadata": { + "name": "xr-vpc-abc12", + "annotations": {"crossplane.io/external-name": "vpc-0123"}, + }, + "status": {"atProvider": {"id": "vpc-0123", "arn": "arn:aws:ec2:vpc/vpc-0123"}}, +} + +POOL_OBSERVED = {"ranges": [{"from": "10.0.0.0"}, {"from": "10.1.0.0"}]} + +ORDERED = fnv1.RequestMeta( + capabilities=[fnv1.CAPABILITY_CAPABILITIES, fnv1.CAPABILITY_DEPENDENCIES] +) +UNORDERED = fnv1.RequestMeta(capabilities=[fnv1.CAPABILITY_CAPABILITIES]) + + +def desired_subnet(rsp: fnv1.RunFunctionResponse, vpc_id: str) -> None: + subnet = Subnet( + spec=SubnetSpec(forProvider=SubnetForProvider(region="us-east-1", vpcId=vpc_id)) + ) + resource.update(rsp.desired.resources["subnet"], subnet) + + +def edges(rsp: fnv1.RunFunctionResponse) -> list[dict]: + return [ + json_format.MessageToDict(d, preserving_proto_field_name=True) + for d in rsp.dependencies.items + ] + + +def conditions(rsp: fnv1.RunFunctionResponse) -> list[dict]: + return [ + json_format.MessageToDict(c, preserving_proto_field_name=True) + for c in rsp.conditions + ] + + +def kept(name: str) -> str: + """The condition message for a resource kept while the VPC's id is missing.""" + return f"{name} kept its current spec: vpc.status.atProvider.id isn't available" + + +def body(rsp: fnv1.RunFunctionResponse, name: str) -> dict: + return resource.struct_to_dict(rsp.desired.resources[name].resource) + + +class TestNamed(unittest.TestCase): + def setUp(self) -> None: + logging.configure(level=logging.Level.DISABLED) + + def read( + self, + observed_state: fnv1.State, + fn: typing.Callable[[dependency.Scope], object], + ) -> object: + req = fnv1.RunFunctionRequest(meta=ORDERED, observed=observed_state) + with dependency.composing(req, response.to(req), "subnet") as c: + return fn(c) + + def test_follows_model_fields(self) -> None: + vpc = dependency.named("vpc", VPC) + self.assertEqual( + self.read( + observed(vpc=VPC_OBSERVED), lambda c: c.ref(vpc.status.atProvider.arn) + ), + "arn:aws:ec2:vpc/vpc-0123", + ) + + def test_typo_fails_where_it_is_written(self) -> None: + vpc = dependency.named("vpc", VPC) + with self.assertRaisesRegex( + AttributeError, "vpc.status has no field 'atprovider'" + ): + _ = vpc.status.atprovider + + def test_keyword_field_is_read_by_its_json_name(self) -> None: + pool = dependency.named("pool", Pool) + for field in ( + pool.ranges[0].from_, + pool.ranges[0]["from"], + getattr(pool.ranges[0], "from"), + ): + self.assertEqual( + self.read(observed(pool=POOL_OBSERVED), lambda c, f=field: c.ref(f)), + "10.0.0.0", + ) + + def test_untyped_reads_what_it_is_given(self) -> None: + vpc = dependency.named("vpc") + got = self.read( + observed(vpc=VPC_OBSERVED), + lambda c: c.ref(vpc.metadata.annotations["crossplane.io/external-name"]), + ) + self.assertEqual(got, "vpc-0123") + + def test_has_no_value(self) -> None: + vpc = dependency.named("vpc", VPC) + field = vpc.status.atProvider.id + with self.assertRaisesRegex(TypeError, "has no value yet"): + bool(field) + with self.assertRaisesRegex(TypeError, "has no value yet"): + str(field) + with self.assertRaises(TypeError): + iter(field) + with self.assertRaises(AttributeError): + field.id = "x" + + def test_ref_rejects_values(self) -> None: + with self.assertRaisesRegex(TypeError, "expects a field"): + self.read(fnv1.State(), lambda c: c.ref("vpc-0123")) + + def test_ref_rejects_whole_resource(self) -> None: + with self.assertRaisesRegex(ValueError, "external_name"): + self.read(fnv1.State(), lambda c: c.ref(dependency.named("vpc", VPC))) + + def test_external_name_rejects_fields(self) -> None: + vpc = dependency.named("vpc", VPC) + with self.assertRaises(TypeError): + self.read(fnv1.State(), lambda c: c.external_name(vpc.status)) + + def test_forgetting_ref_fails_validation(self) -> None: + vpc = dependency.named("vpc", VPC) + with self.assertRaises(pydantic.ValidationError): + SubnetForProvider(vpcId=vpc.status.atProvider.id) + + +class DnsForProvider(pydantic.BaseModel): + enableDnsSupport: bool | None = None # noqa: N815 # Generated models use JSON names. + + +class Dns(pydantic.BaseModel): + apiVersion: str = "example.org/v1" # noqa: N815 # Generated models use JSON names. + kind: str = "Dns" + spec: DnsForProvider + + +class TestComposing(unittest.TestCase): + def setUp(self) -> None: + logging.configure(level=logging.Level.DISABLED) + + def test_returns_values_and_records_edges(self) -> None: + req = fnv1.RunFunctionRequest(meta=ORDERED, observed=observed(vpc=VPC_OBSERVED)) + rsp = response.to(req) + vpc = dependency.named("vpc", VPC) + + with dependency.composing(req, rsp, "subnet") as c: + vpc_id = c.ref(vpc.status.atProvider.id) + name = c.external_name(vpc) + desired_subnet(rsp, vpc_id) + + self.assertEqual(vpc_id, "vpc-0123") + self.assertEqual(name, "vpc-0123") + self.assertEqual( + edges(rsp), [{"resource": "subnet", "composed_resource": "vpc"}] + ) + self.assertEqual(rsp.results, []) + + def test_typed_values_come_back_as_models(self) -> None: + req = fnv1.RunFunctionRequest( + meta=ORDERED, observed=observed(pool=POOL_OBSERVED) + ) + rsp = response.to(req) + pool = dependency.named("pool", Pool) + + with dependency.composing(req, rsp, "subnet") as c: + ranges = c.ref(pool.ranges) + first = c.ref(pool.ranges[0]) + start = c.ref(pool.ranges[0].from_) + + self.assertEqual( + ranges, [Range(**{"from": "10.0.0.0"}), Range(**{"from": "10.1.0.0"})] + ) + self.assertIsInstance(first, Range) + self.assertEqual(start, "10.0.0.0") + + def test_untyped_values_come_back_as_json(self) -> None: + req = fnv1.RunFunctionRequest( + meta=ORDERED, observed=observed(pool=POOL_OBSERVED) + ) + rsp = response.to(req) + pool = dependency.named("pool") + + with dependency.composing(req, rsp, "subnet") as c: + ranges = c.ref(pool.ranges) + + self.assertEqual(ranges, [{"from": "10.0.0.0"}, {"from": "10.1.0.0"}]) + + def test_values_that_dont_fit_the_model_fail(self) -> None: + req = fnv1.RunFunctionRequest( + meta=ORDERED, observed=observed(pool={"ranges": [{"from": 7}]}) + ) + rsp = response.to(req) + pool = dependency.named("pool", Pool) + + with ( + self.assertRaisesRegex(ValueError, "pool.ranges doesn't match its model"), + dependency.composing(req, rsp, "subnet") as c, + ): + c.ref(pool.ranges) + + def test_works_in_fields_that_are_not_strings(self) -> None: + vpc_body = {"spec": {"forProvider": {"enableDnsSupport": True}}} + req = fnv1.RunFunctionRequest(meta=ORDERED, observed=observed(vpc=vpc_body)) + rsp = response.to(req) + vpc = dependency.named("vpc") + + with dependency.composing(req, rsp, "subnet") as c: + c.update( + Dns( + spec=DnsForProvider( + enableDnsSupport=c.ref(vpc.spec.forProvider.enableDnsSupport) + ) + ) + ) + + self.assertEqual(body(rsp, "subnet")["spec"], {"enableDnsSupport": True}) + + def test_missing_source_returns_none_and_keeps_edge(self) -> None: + req = fnv1.RunFunctionRequest(meta=ORDERED) + rsp = response.to(req) + vpc = dependency.named("vpc", VPC) + + with dependency.composing(req, rsp, "subnet") as c: + vpc_id = c.ref(vpc.status.atProvider.id) + c.update( + Subnet( + spec=SubnetSpec( + forProvider=SubnetForProvider(region="us-east-1", vpcId=vpc_id) + ) + ) + ) + + self.assertIsNone(vpc_id) + self.assertEqual( + body(rsp, "subnet")["spec"]["forProvider"], {"region": "us-east-1"} + ) + self.assertEqual( + edges(rsp), [{"resource": "subnet", "composed_resource": "vpc"}] + ) + self.assertEqual(rsp.results, []) + + def test_existing_resource_keeps_its_spec(self) -> None: + subnet = {"spec": {"forProvider": {"region": "us-east-1", "vpcId": "vpc-0123"}}} + req = fnv1.RunFunctionRequest(meta=ORDERED, observed=observed(subnet=subnet)) + rsp = response.to(req) + vpc = dependency.named("vpc", VPC) + + with dependency.composing(req, rsp, "subnet") as c: + c.update( + Subnet( + spec=SubnetSpec( + forProvider=SubnetForProvider( + region="us-west-2", vpcId=c.ref(vpc.status.atProvider.id) + ) + ) + ) + ) + + # The function's own change wins; the field it couldn't fill is kept. + self.assertEqual( + body(rsp, "subnet")["spec"]["forProvider"], + {"region": "us-west-2", "vpcId": "vpc-0123"}, + ) + self.assertEqual(rsp.results, []) + self.assertEqual( + conditions(rsp), + [ + { + "type": "DependencyValuesAvailable", + "status": "STATUS_CONDITION_FALSE", + "reason": "KeptCurrentSpec", + "message": kept("subnet"), + } + ], + ) + + def test_condition_is_true_when_nothing_is_kept(self) -> None: + # Returned even so: Crossplane keeps a condition a function set + # earlier, so one only returned while False would never clear. + req = fnv1.RunFunctionRequest(meta=ORDERED) + rsp = response.to(req) + with dependency.composing(req, rsp, "subnet") as c: + c.ref(dependency.named("vpc", VPC).status.atProvider.id) + + self.assertEqual( + conditions(rsp), + [ + { + "type": "DependencyValuesAvailable", + "status": "STATUS_CONDITION_TRUE", + "reason": "Available", + } + ], + ) + + def test_condition_names_every_kept_resource(self) -> None: + existing = { + "spec": {"forProvider": {"region": "us-east-1", "vpcId": "vpc-0123"}} + } + req = fnv1.RunFunctionRequest( + meta=ORDERED, observed=observed(a=existing, b=existing, c=VPC_OBSERVED) + ) + rsp = response.to(req) + vpc = dependency.named("vpc", VPC) + + # A kept resource, one that's fine, then another kept one: the one + # that's fine mustn't turn the condition back to True. + for name in ("a", "c", "b"): + with dependency.composing(req, rsp, name) as c: + if name == "c": + continue + c.ref(vpc.status.atProvider.id) + + self.assertEqual( + conditions(rsp), + [ + { + "type": "DependencyValuesAvailable", + "status": "STATUS_CONDITION_FALSE", + "reason": "KeptCurrentSpec", + "message": f"{kept('a')}; {kept('b')}", + } + ], + ) + + def test_composing_nothing_declares_nothing(self) -> None: + req = fnv1.RunFunctionRequest(meta=ORDERED) + rsp = response.to(req) + vpc = dependency.named("vpc", VPC) + + with dependency.composing(req, rsp, "subnet") as c: + vpc_id = c.ref(vpc.status.atProvider.id) + if vpc_id: + desired_subnet(rsp, vpc_id) + + self.assertNotIn("subnet", rsp.desired.resources) + self.assertEqual(edges(rsp), []) + + def test_existing_resource_not_composed_is_kept(self) -> None: + subnet = { + "apiVersion": "ec2.aws.m.upbound.io/v1beta1", + "kind": "Subnet", + "metadata": {"name": "xr-subnet-1", "uid": "abc", "resourceVersion": "7"}, + "spec": {"forProvider": {"region": "us-east-1", "vpcId": "vpc-0123"}}, + "status": {"atProvider": {"id": "subnet-9"}}, + } + req = fnv1.RunFunctionRequest(meta=ORDERED, observed=observed(subnet=subnet)) + rsp = response.to(req) + vpc = dependency.named("vpc", VPC) + + with dependency.composing(req, rsp, "subnet") as c: + vpc_id = c.ref(vpc.status.atProvider.id) + if vpc_id: + desired_subnet(rsp, vpc_id) + + # Kept rather than deleted, as its spec, without what the API server + # owns: uid, resourceVersion and status. + self.assertEqual( + body(rsp, "subnet"), + { + "apiVersion": "ec2.aws.m.upbound.io/v1beta1", + "kind": "Subnet", + "metadata": {"name": "xr-subnet-1"}, + "spec": {"forProvider": {"region": "us-east-1", "vpcId": "vpc-0123"}}, + }, + ) + self.assertEqual( + edges(rsp), [{"resource": "subnet", "composed_resource": "vpc"}] + ) + self.assertEqual(conditions(rsp)[0]["reason"], "KeptCurrentSpec") + + def test_without_capability_holds_back_by_omission(self) -> None: + req = fnv1.RunFunctionRequest(meta=UNORDERED) + rsp = response.to(req) + vpc = dependency.named("vpc", VPC) + + with dependency.composing(req, rsp, "subnet") as c: + desired_subnet(rsp, c.ref(vpc.status.atProvider.id)) + + self.assertNotIn("subnet", rsp.desired.resources) + + def test_exception_records_nothing(self) -> None: + req = fnv1.RunFunctionRequest(meta=ORDERED, observed=observed(vpc=VPC_OBSERVED)) + rsp = response.to(req) + vpc = dependency.named("vpc", VPC) + + with ( + self.assertRaises(RuntimeError), + dependency.composing(req, rsp, "subnet") as c, + ): + c.ref(vpc.status.atProvider.id) + raise RuntimeError + + self.assertEqual(edges(rsp), []) + + def test_required(self) -> None: + req = fnv1.RunFunctionRequest( + meta=ORDERED, + required_resources={ + "dbs": fnv1.Resources( + items=[ + fnv1.Resource( + resource=resource.dict_to_struct( + { + "metadata": {"name": "a"}, + "status": {"address": "db.a"}, + } + ) + ) + ] + ) + }, + ) + rsp = response.to(req) + db = dependency.named_required("dbs") + + with dependency.composing(req, rsp, "app-config") as c: + host = c.ref(db.status.address) + c.update( + {"apiVersion": "v1", "kind": "ConfigMap", "data": {"DB_HOST": host}} + ) + + self.assertEqual(host, "db.a") + self.assertEqual( + edges(rsp), + [ + { + "resource": "app-config", + "required_resource": {"requirement_name": "dbs"}, + } + ], + ) + + def test_external_name(self) -> None: + req = fnv1.RunFunctionRequest(meta=ORDERED, observed=observed(vpc=VPC_OBSERVED)) + with dependency.composing(req, response.to(req), "subnet") as c: + self.assertEqual(c.external_name(dependency.named("vpc")), "vpc-0123") + + def test_external_name_falls_back_to_name(self) -> None: + req = fnv1.RunFunctionRequest( + meta=ORDERED, observed=observed(vpc={"metadata": {"name": "xr-vpc-abc12"}}) + ) + with dependency.composing(req, response.to(req), "subnet") as c: + self.assertEqual(c.external_name(dependency.named("vpc")), "xr-vpc-abc12") + + def test_missing_field_on_existing_source_is_none(self) -> None: + req = fnv1.RunFunctionRequest( + meta=ORDERED, observed=observed(vpc={"metadata": {"name": "xr-vpc-abc12"}}) + ) + rsp = response.to(req) + with dependency.composing(req, rsp, "subnet") as c: + vpc_id = c.ref(dependency.named("vpc", VPC).status.atProvider.id) + desired_subnet(rsp, vpc_id) + + self.assertIsNone(vpc_id) + self.assertNotIn("vpcId", body(rsp, "subnet")["spec"]["forProvider"]) + + def test_none_is_stripped_however_the_resource_is_written(self) -> None: + req = fnv1.RunFunctionRequest(meta=ORDERED, observed=observed(vpc=VPC_OBSERVED)) + rsp = response.to(req) + vpc = dependency.named("vpc", VPC) + gone = dependency.named("gone", VPC) + + # resource.update rather than c.update, as a helper would call it. A + # model field set to None is emitted as a null, because it was set. + with dependency.composing(req, rsp, "subnet") as c: + resource.update( + rsp.desired.resources["subnet"], + Subnet( + spec=SubnetSpec( + forProvider=SubnetForProvider( + region="us-east-1", vpcId=c.ref(gone.status.atProvider.id) + ) + ) + ), + ) + + # A dict can carry None nested, and in a list. + with dependency.composing(req, rsp, "tags") as c: + resource.update( + rsp.desired.resources["tags"], + { + "spec": { + "vpcId": c.ref(gone.status.atProvider.id), + "tags": [ + "static", + c.ref(vpc.status.atProvider.arn), + c.ref(gone.status.atProvider.arn), + ], + } + }, + ) + + self.assertEqual( + body(rsp, "subnet")["spec"]["forProvider"], {"region": "us-east-1"} + ) + self.assertEqual( + body(rsp, "tags")["spec"], {"tags": ["static", "arn:aws:ec2:vpc/vpc-0123"]} + ) + + def test_does_not_duplicate_declared_dependencies(self) -> None: + req = fnv1.RunFunctionRequest(meta=ORDERED, observed=observed(vpc=VPC_OBSERVED)) + rsp = response.to(req) + response.add_dependency(rsp, "subnet", "vpc") + vpc = dependency.named("vpc", VPC) + + with dependency.composing(req, rsp, "subnet") as c: + c.update( + { + "a": c.ref(vpc.status.atProvider.id), + "b": c.ref(vpc.status.atProvider.arn), + } + ) + + self.assertEqual( + edges(rsp), [{"resource": "subnet", "composed_resource": "vpc"}] + ) + + def test_self_reference_declares_nothing(self) -> None: + req = fnv1.RunFunctionRequest(meta=ORDERED, observed=observed(vpc=VPC_OBSERVED)) + rsp = response.to(req) + vpc = dependency.named("vpc", VPC) + + with dependency.composing(req, rsp, "vpc") as c: + c.update({"tag": c.ref(vpc.status.atProvider.id)}) + + self.assertEqual(edges(rsp), []) + + def required(self, *items: dict) -> fnv1.RunFunctionRequest: + return fnv1.RunFunctionRequest( + meta=ORDERED, + required_resources={ + "dbs": fnv1.Resources( + items=[ + fnv1.Resource(resource=resource.dict_to_struct(i)) + for i in items + ] + ) + }, + ) + + def test_required_named_match(self) -> None: + req = self.required( + { + "metadata": {"name": "a", "namespace": "x"}, + "status": {"address": "db.xa"}, + }, + { + "metadata": {"name": "a", "namespace": "y"}, + "status": {"address": "db.ya"}, + }, + ) + rsp = response.to(req) + db = dependency.named_required("dbs", name="a", namespace="y") + + with dependency.composing(req, rsp, "app-config") as c: + host = c.ref(db.status.address) + c.update( + {"apiVersion": "v1", "kind": "ConfigMap", "data": {"DB_HOST": host}} + ) + + self.assertEqual(host, "db.ya") + self.assertEqual( + edges(rsp)[0]["required_resource"], + {"requirement_name": "dbs", "name": "a", "namespace": "y"}, + ) + + def test_required_declines_to_guess_between_matches(self) -> None: + req = self.required( + {"metadata": {"name": "a"}, "status": {"address": "db.a"}}, + {"metadata": {"name": "b"}, "status": {"address": "db.b"}}, + ) + with dependency.composing(req, response.to(req), "app-config") as c: + self.assertIsNone(c.ref(dependency.named_required("dbs").status.address)) + + def test_rejects_values(self) -> None: + req = fnv1.RunFunctionRequest(meta=ORDERED) + rsp = response.to(req) + with ( + self.assertRaises(TypeError), + dependency.composing(req, rsp, "subnet") as c, + ): + c.ref("vpc-0123") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_request.py b/tests/test_request.py index f7fdf00..ebd1e2c 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -407,6 +407,52 @@ class TestCase: got = request.get_required_schema(case.req, case.name) self.assertEqual(case.want, got, case.reason) + def test_get_dependencies(self) -> None: + @dataclasses.dataclass + class TestCase: + reason: str + req: fnv1.RunFunctionRequest + want: list[fnv1.Dependency] + + cases = [ + TestCase( + reason="No dependencies should return an empty list.", + req=fnv1.RunFunctionRequest(), + want=[], + ), + TestCase( + reason="Should return the dependencies accumulated so far.", + req=fnv1.RunFunctionRequest( + dependencies=fnv1.Dependencies( + items=[ + fnv1.Dependency( + resource="database", composed_resource="network" + ), + fnv1.Dependency( + resource="database", + required_resource=fnv1.RequiredResourceDependency( + requirement_name="cluster" + ), + ), + ] + ) + ), + want=[ + fnv1.Dependency(resource="database", composed_resource="network"), + fnv1.Dependency( + resource="database", + required_resource=fnv1.RequiredResourceDependency( + requirement_name="cluster" + ), + ), + ], + ), + ] + + for case in cases: + got = request.get_dependencies(case.req) + self.assertEqual(case.want, got, case.reason) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_response.py b/tests/test_response.py index fa8edc9..999909b 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -61,10 +61,75 @@ class TestCase: context=resource.dict_to_struct({"cool-key": "cool-value"}), ), ), + TestCase( + reason="Dependencies should be copied.", + req=fnv1.RunFunctionRequest( + meta=fnv1.RequestMeta(tag="hi"), + dependencies=fnv1.Dependencies( + items=[ + fnv1.Dependency( + resource="database", composed_resource="network" + ), + ] + ), + ), + ttl=datetime.timedelta(minutes=10), + want=fnv1.RunFunctionResponse( + meta=fnv1.ResponseMeta( + tag="hi", ttl=durationpb.Duration(seconds=60 * 10) + ), + desired=fnv1.State(), + context=structpb.Struct(), + dependencies=fnv1.Dependencies( + items=[ + fnv1.Dependency( + resource="database", composed_resource="network" + ), + ] + ), + ), + ), + TestCase( + reason="Unset dependencies should stay unset, not become empty.", + req=fnv1.RunFunctionRequest(meta=fnv1.RequestMeta(tag="hi")), + ttl=datetime.timedelta(minutes=10), + want=fnv1.RunFunctionResponse( + meta=fnv1.ResponseMeta( + tag="hi", ttl=durationpb.Duration(seconds=60 * 10) + ), + desired=fnv1.State(), + context=structpb.Struct(), + ), + ), + TestCase( + reason="Empty dependencies should stay empty, not become unset.", + req=fnv1.RunFunctionRequest( + meta=fnv1.RequestMeta(tag="hi"), + dependencies=fnv1.Dependencies(), + ), + ttl=datetime.timedelta(minutes=10), + want=fnv1.RunFunctionResponse( + meta=fnv1.ResponseMeta( + tag="hi", ttl=durationpb.Duration(seconds=60 * 10) + ), + desired=fnv1.State(), + context=structpb.Struct(), + dependencies=fnv1.Dependencies(), + ), + ), ] for case in cases: got = response.to(case.req, case.ttl) + + # An unset Dependencies means "no opinion". An empty one means + # "no constraints at all". They must not be confused. + self.assertEqual( + case.want.HasField("dependencies"), + got.HasField("dependencies"), + case.reason, + ) + self.assertEqual( json_format.MessageToJson(case.want, sort_keys=True), json_format.MessageToJson(got, sort_keys=True), @@ -338,6 +403,153 @@ class TestCase: case.reason, ) + def test_dependencies(self) -> None: + @dataclasses.dataclass + class TestCase: + reason: str + rsp: fnv1.RunFunctionResponse + mutate: object + want: fnv1.RunFunctionResponse + + def no_opinion(_rsp) -> None: + pass + + def add_composed(rsp) -> None: + response.add_dependency(rsp, "database", "network") + + def add_replacement(rsp) -> None: + response.add_dependency( + rsp, "database-v2", "database", create_before_destroy=True + ) + + def add_required(rsp) -> None: + response.add_required_resource_dependency( + rsp, "database", "cluster", name="prod", namespace="default" + ) + + def add_required_set(rsp) -> None: + response.add_required_resource_dependency(rsp, "database", "cluster") + + def add_to_existing(rsp) -> None: + response.add_dependency(rsp, "cache", "database") + + def clear(rsp) -> None: + response.clear_dependencies(rsp) + + existing = fnv1.Dependency(resource="database", composed_resource="network") + + cases = [ + TestCase( + reason="A response should have no opinion about ordering by default.", + rsp=fnv1.RunFunctionResponse(), + mutate=no_opinion, + want=fnv1.RunFunctionResponse(), + ), + TestCase( + reason="Should add a dependency on another composed resource.", + rsp=fnv1.RunFunctionResponse(), + mutate=add_composed, + want=fnv1.RunFunctionResponse( + dependencies=fnv1.Dependencies(items=[existing]) + ), + ), + TestCase( + reason="Should add a create before destroy dependency.", + rsp=fnv1.RunFunctionResponse(), + mutate=add_replacement, + want=fnv1.RunFunctionResponse( + dependencies=fnv1.Dependencies( + items=[ + fnv1.Dependency( + resource="database-v2", + composed_resource="database", + lifecycle=fnv1.DEPENDENCY_LIFECYCLE_CREATE_BEFORE_DESTROY, + ) + ] + ) + ), + ), + TestCase( + reason="Should add a dependency on one required resource.", + rsp=fnv1.RunFunctionResponse(), + mutate=add_required, + want=fnv1.RunFunctionResponse( + dependencies=fnv1.Dependencies( + items=[ + fnv1.Dependency( + resource="database", + required_resource=fnv1.RequiredResourceDependency( + requirement_name="cluster", + name="prod", + namespace="default", + ), + ) + ] + ) + ), + ), + TestCase( + reason="Should add a dependency on every matched required resource.", + rsp=fnv1.RunFunctionResponse(), + mutate=add_required_set, + want=fnv1.RunFunctionResponse( + dependencies=fnv1.Dependencies( + items=[ + fnv1.Dependency( + resource="database", + required_resource=fnv1.RequiredResourceDependency( + requirement_name="cluster" + ), + ) + ] + ) + ), + ), + TestCase( + reason="Should add to the dependencies already on the response.", + rsp=fnv1.RunFunctionResponse( + dependencies=fnv1.Dependencies(items=[existing]) + ), + mutate=add_to_existing, + want=fnv1.RunFunctionResponse( + dependencies=fnv1.Dependencies( + items=[ + existing, + fnv1.Dependency( + resource="cache", composed_resource="database" + ), + ] + ) + ), + ), + TestCase( + reason="Clearing should return an empty set, not an unset one.", + rsp=fnv1.RunFunctionResponse( + dependencies=fnv1.Dependencies(items=[existing]) + ), + mutate=clear, + want=fnv1.RunFunctionResponse(dependencies=fnv1.Dependencies()), + ), + ] + + for case in cases: + got = case.rsp + case.mutate(got) + + self.assertEqual( + json_format.MessageToJson(case.want, sort_keys=True), + json_format.MessageToJson(got, sort_keys=True), + case.reason, + ) + + # An unset Dependencies means "no opinion". An empty one means + # "no constraints at all". They must not be confused. + self.assertEqual( + case.want.HasField("dependencies"), + got.HasField("dependencies"), + case.reason, + ) + if __name__ == "__main__": unittest.main()