function.response

Utilities for working with RunFunctionResponses.

  1# Copyright 2023 The Crossplane Authors.
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""Utilities for working with RunFunctionResponses."""
 16
 17import datetime
 18
 19from google.protobuf import duration_pb2 as durationpb
 20from google.protobuf import struct_pb2 as structpb
 21
 22import crossplane.function.proto.v1.run_function_pb2 as fnv1
 23from crossplane.function import resource
 24
 25"""The default TTL for which a RunFunctionResponse may be cached."""
 26DEFAULT_TTL = datetime.timedelta(minutes=1)
 27
 28_default_ttl = DEFAULT_TTL
 29
 30
 31def get_default_ttl() -> datetime.timedelta:
 32    """Return the process-wide default TTL for RunFunctionResponses."""
 33    return _default_ttl
 34
 35
 36def set_default_ttl(ttl: datetime.timedelta) -> None:
 37    """Set the process-wide default TTL for RunFunctionResponses.
 38
 39    Args:
 40        ttl: How long Crossplane may optionally cache responses when no explicit
 41            TTL is passed to :func:`to`.
 42    """
 43    global _default_ttl  # noqa: PLW0603
 44    _default_ttl = ttl
 45
 46
 47def to(
 48    req: fnv1.RunFunctionRequest,
 49    ttl: datetime.timedelta | None = None,
 50) -> fnv1.RunFunctionResponse:
 51    """Create a response to the supplied request.
 52
 53    Args:
 54        req: The request to respond to.
 55        ttl: How long Crossplane may optionally cache the response. Defaults to
 56            the process-wide default TTL set by :func:`set_default_ttl`.
 57
 58    Returns:
 59        A response to the supplied request.
 60
 61    The request's tag, desired resources, and context is automatically copied to
 62    the response. Using response.to is a good pattern to ensure
 63    """
 64    if ttl is None:
 65        ttl = get_default_ttl()
 66
 67    dttl = durationpb.Duration()
 68    dttl.FromTimedelta(ttl)
 69    return fnv1.RunFunctionResponse(
 70        meta=fnv1.ResponseMeta(tag=req.meta.tag, ttl=dttl),
 71        desired=req.desired,
 72        context=req.context,
 73    )
 74
 75
 76def normal(rsp: fnv1.RunFunctionResponse, message: str) -> None:
 77    """Add a normal result to the response."""
 78    rsp.results.append(
 79        fnv1.Result(
 80            severity=fnv1.SEVERITY_NORMAL,
 81            message=message,
 82        )
 83    )
 84
 85
 86def warning(rsp: fnv1.RunFunctionResponse, message: str) -> None:
 87    """Add a warning result to the response."""
 88    rsp.results.append(
 89        fnv1.Result(
 90            severity=fnv1.SEVERITY_WARNING,
 91            message=message,
 92        )
 93    )
 94
 95
 96def fatal(rsp: fnv1.RunFunctionResponse, message: str) -> None:
 97    """Add a fatal result to the response."""
 98    rsp.results.append(
 99        fnv1.Result(
100            severity=fnv1.SEVERITY_FATAL,
101            message=message,
102        )
103    )
104
105
106_STATUS_MAP = {
107    "True": fnv1.STATUS_CONDITION_TRUE,
108    "False": fnv1.STATUS_CONDITION_FALSE,
109    "Unknown": fnv1.STATUS_CONDITION_UNKNOWN,
110}
111
112
113def set_conditions(
114    rsp: fnv1.RunFunctionResponse,
115    *conditions: resource.Condition,
116) -> None:
117    """Set one or more conditions on the composite resource (XR).
118
119    Args:
120        rsp: The RunFunctionResponse to update.
121        *conditions: The conditions to set.
122
123    Each condition is appended to ``rsp.conditions``. Crossplane uses the
124    conditions returned by a function to set custom status conditions on
125    the composite resource.
126
127    The ``last_transition_time`` field of each condition is ignored.
128    Crossplane sets the transition time itself.
129
130    Do not set the ``Ready`` condition type. Crossplane manages it based
131    on resource readiness.
132    """
133    for condition in conditions:
134        c = fnv1.Condition(
135            type=condition.typ,
136            status=_STATUS_MAP.get(condition.status, fnv1.STATUS_CONDITION_UNKNOWN),
137            reason=condition.reason or "",
138        )
139        if condition.message:
140            c.message = condition.message
141        rsp.conditions.append(c)
142
143
144def set_output(rsp: fnv1.RunFunctionResponse, output: dict | structpb.Struct) -> None:
145    """Set the output field in a RunFunctionResponse for operation functions.
146
147    Args:
148        rsp: The RunFunctionResponse to update.
149        output: The output data as a dictionary or protobuf Struct.
150
151    Operation functions can return arbitrary output data that will be written
152    to the Operation's status.pipeline field. This function sets that output
153    on the response.
154    """
155    match output:
156        case dict():
157            rsp.output.CopyFrom(resource.dict_to_struct(output))
158        case structpb.Struct():
159            rsp.output.CopyFrom(output)
160        case _:
161            t = type(output)
162            msg = f"Unsupported output type: {t}"
163            raise TypeError(msg)
164
165
166def require_resources(  # noqa: PLR0913
167    rsp: fnv1.RunFunctionResponse,
168    name: str,
169    api_version: str,
170    kind: str,
171    *,
172    match_name: str | None = None,
173    match_labels: dict[str, str] | None = None,
174    namespace: str | None = None,
175) -> None:
176    """Add a resource requirement to the response.
177
178    Args:
179        rsp: The RunFunctionResponse to update.
180        name: The name to use for this requirement.
181        api_version: The API version of resources to require.
182        kind: The kind of resources to require.
183        match_name: Match a resource by name (mutually exclusive with match_labels).
184        match_labels: Match resources by labels (mutually exclusive with match_name).
185        namespace: The namespace to search in (optional).
186
187    Raises:
188        ValueError: If both match_name and match_labels are provided.
189
190    This tells Crossplane to fetch the specified resources and include them
191    in the next call to the function in req.required_resources[name].
192
193    If neither match_name nor match_labels is provided, all resources of the
194    given api_version and kind are matched.
195    """
196    if match_name is not None and match_labels is not None:
197        msg = "match_name and match_labels are mutually exclusive"
198        raise ValueError(msg)
199
200    selector = fnv1.ResourceSelector(
201        api_version=api_version,
202        kind=kind,
203    )
204
205    if match_name is not None:
206        selector.match_name = match_name
207
208    if match_labels is not None:
209        selector.match_labels.labels.update(match_labels)
210
211    if namespace is not None:
212        selector.namespace = namespace
213
214    rsp.requirements.resources[name].CopyFrom(selector)
215
216
217def require_schema(
218    rsp: fnv1.RunFunctionResponse,
219    name: str,
220    api_version: str,
221    kind: str,
222) -> None:
223    """Add a schema requirement to the response.
224
225    Args:
226        rsp: The RunFunctionResponse to update.
227        name: The name to use for this requirement.
228        api_version: The API version of the resource kind, e.g. "example.org/v1".
229        kind: The kind of resource, e.g. "MyResource".
230
231    This tells Crossplane to fetch the OpenAPI schema for the specified resource
232    kind and include it in the next call to the function in
233    req.required_schemas[name]. Use request.get_required_schema to retrieve it.
234
235    For CRDs, Crossplane returns the spec.versions[].schema.openAPIV3Schema field.
236    If Crossplane cannot find a schema for the requested kind, the schema will be
237    empty (get_required_schema will return None).
238    """
239    selector = fnv1.SchemaSelector(
240        api_version=api_version,
241        kind=kind,
242    )
243    rsp.requirements.schemas[name].CopyFrom(selector)
DEFAULT_TTL = datetime.timedelta(seconds=60)
def get_default_ttl() -> datetime.timedelta:
32def get_default_ttl() -> datetime.timedelta:
33    """Return the process-wide default TTL for RunFunctionResponses."""
34    return _default_ttl

Return the process-wide default TTL for RunFunctionResponses.

def set_default_ttl(ttl: datetime.timedelta) -> None:
37def set_default_ttl(ttl: datetime.timedelta) -> None:
38    """Set the process-wide default TTL for RunFunctionResponses.
39
40    Args:
41        ttl: How long Crossplane may optionally cache responses when no explicit
42            TTL is passed to :func:`to`.
43    """
44    global _default_ttl  # noqa: PLW0603
45    _default_ttl = ttl

Set the process-wide default TTL for RunFunctionResponses.

Arguments:
  • ttl: How long Crossplane may optionally cache responses when no explicit TTL is passed to to().
def to( req: crossplane.function.proto.v1.run_function_pb2.RunFunctionRequest, ttl: datetime.timedelta | None = None) -> crossplane.function.proto.v1.run_function_pb2.RunFunctionResponse:
48def to(
49    req: fnv1.RunFunctionRequest,
50    ttl: datetime.timedelta | None = None,
51) -> fnv1.RunFunctionResponse:
52    """Create a response to the supplied request.
53
54    Args:
55        req: The request to respond to.
56        ttl: How long Crossplane may optionally cache the response. Defaults to
57            the process-wide default TTL set by :func:`set_default_ttl`.
58
59    Returns:
60        A response to the supplied request.
61
62    The request's tag, desired resources, and context is automatically copied to
63    the response. Using response.to is a good pattern to ensure
64    """
65    if ttl is None:
66        ttl = get_default_ttl()
67
68    dttl = durationpb.Duration()
69    dttl.FromTimedelta(ttl)
70    return fnv1.RunFunctionResponse(
71        meta=fnv1.ResponseMeta(tag=req.meta.tag, ttl=dttl),
72        desired=req.desired,
73        context=req.context,
74    )

Create a response to the supplied request.

Arguments:
  • req: The request to respond to.
  • ttl: How long Crossplane may optionally cache the response. Defaults to the process-wide default TTL set by set_default_ttl().
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

def normal( rsp: crossplane.function.proto.v1.run_function_pb2.RunFunctionResponse, message: str) -> None:
77def normal(rsp: fnv1.RunFunctionResponse, message: str) -> None:
78    """Add a normal result to the response."""
79    rsp.results.append(
80        fnv1.Result(
81            severity=fnv1.SEVERITY_NORMAL,
82            message=message,
83        )
84    )

Add a normal result to the response.

def warning( rsp: crossplane.function.proto.v1.run_function_pb2.RunFunctionResponse, message: str) -> None:
87def warning(rsp: fnv1.RunFunctionResponse, message: str) -> None:
88    """Add a warning result to the response."""
89    rsp.results.append(
90        fnv1.Result(
91            severity=fnv1.SEVERITY_WARNING,
92            message=message,
93        )
94    )

Add a warning result to the response.

def fatal( rsp: crossplane.function.proto.v1.run_function_pb2.RunFunctionResponse, message: str) -> None:
 97def fatal(rsp: fnv1.RunFunctionResponse, message: str) -> None:
 98    """Add a fatal result to the response."""
 99    rsp.results.append(
100        fnv1.Result(
101            severity=fnv1.SEVERITY_FATAL,
102            message=message,
103        )
104    )

Add a fatal result to the response.

def set_conditions( rsp: crossplane.function.proto.v1.run_function_pb2.RunFunctionResponse, *conditions: crossplane.function.resource.Condition) -> None:
114def set_conditions(
115    rsp: fnv1.RunFunctionResponse,
116    *conditions: resource.Condition,
117) -> None:
118    """Set one or more conditions on the composite resource (XR).
119
120    Args:
121        rsp: The RunFunctionResponse to update.
122        *conditions: The conditions to set.
123
124    Each condition is appended to ``rsp.conditions``. Crossplane uses the
125    conditions returned by a function to set custom status conditions on
126    the composite resource.
127
128    The ``last_transition_time`` field of each condition is ignored.
129    Crossplane sets the transition time itself.
130
131    Do not set the ``Ready`` condition type. Crossplane manages it based
132    on resource readiness.
133    """
134    for condition in conditions:
135        c = fnv1.Condition(
136            type=condition.typ,
137            status=_STATUS_MAP.get(condition.status, fnv1.STATUS_CONDITION_UNKNOWN),
138            reason=condition.reason or "",
139        )
140        if condition.message:
141            c.message = condition.message
142        rsp.conditions.append(c)

Set one or more conditions on the composite resource (XR).

Arguments:
  • rsp: The RunFunctionResponse to update.
  • *conditions: The conditions to set.

Each condition is appended to rsp.conditions. Crossplane uses the conditions returned by a function to set custom status conditions on the composite resource.

The last_transition_time field of each condition is ignored. Crossplane sets the transition time itself.

Do not set the Ready condition type. Crossplane manages it based on resource readiness.

def set_output( rsp: crossplane.function.proto.v1.run_function_pb2.RunFunctionResponse, output: dict | google.protobuf.struct_pb2.Struct) -> None:
145def set_output(rsp: fnv1.RunFunctionResponse, output: dict | structpb.Struct) -> None:
146    """Set the output field in a RunFunctionResponse for operation functions.
147
148    Args:
149        rsp: The RunFunctionResponse to update.
150        output: The output data as a dictionary or protobuf Struct.
151
152    Operation functions can return arbitrary output data that will be written
153    to the Operation's status.pipeline field. This function sets that output
154    on the response.
155    """
156    match output:
157        case dict():
158            rsp.output.CopyFrom(resource.dict_to_struct(output))
159        case structpb.Struct():
160            rsp.output.CopyFrom(output)
161        case _:
162            t = type(output)
163            msg = f"Unsupported output type: {t}"
164            raise TypeError(msg)

Set the output field in a RunFunctionResponse for operation functions.

Arguments:
  • rsp: The RunFunctionResponse to update.
  • output: The output data as a dictionary or protobuf Struct.

Operation functions can return arbitrary output data that will be written to the Operation's status.pipeline field. This function sets that output on the response.

def require_resources( rsp: crossplane.function.proto.v1.run_function_pb2.RunFunctionResponse, name: str, api_version: str, kind: str, *, match_name: str | None = None, match_labels: dict[str, str] | None = None, namespace: str | None = None) -> None:
167def require_resources(  # noqa: PLR0913
168    rsp: fnv1.RunFunctionResponse,
169    name: str,
170    api_version: str,
171    kind: str,
172    *,
173    match_name: str | None = None,
174    match_labels: dict[str, str] | None = None,
175    namespace: str | None = None,
176) -> None:
177    """Add a resource requirement to the response.
178
179    Args:
180        rsp: The RunFunctionResponse to update.
181        name: The name to use for this requirement.
182        api_version: The API version of resources to require.
183        kind: The kind of resources to require.
184        match_name: Match a resource by name (mutually exclusive with match_labels).
185        match_labels: Match resources by labels (mutually exclusive with match_name).
186        namespace: The namespace to search in (optional).
187
188    Raises:
189        ValueError: If both match_name and match_labels are provided.
190
191    This tells Crossplane to fetch the specified resources and include them
192    in the next call to the function in req.required_resources[name].
193
194    If neither match_name nor match_labels is provided, all resources of the
195    given api_version and kind are matched.
196    """
197    if match_name is not None and match_labels is not None:
198        msg = "match_name and match_labels are mutually exclusive"
199        raise ValueError(msg)
200
201    selector = fnv1.ResourceSelector(
202        api_version=api_version,
203        kind=kind,
204    )
205
206    if match_name is not None:
207        selector.match_name = match_name
208
209    if match_labels is not None:
210        selector.match_labels.labels.update(match_labels)
211
212    if namespace is not None:
213        selector.namespace = namespace
214
215    rsp.requirements.resources[name].CopyFrom(selector)

Add a resource requirement to the response.

Arguments:
  • rsp: The RunFunctionResponse to update.
  • name: The name to use for this requirement.
  • api_version: The API version of resources to require.
  • kind: The kind of resources to require.
  • match_name: Match a resource by name (mutually exclusive with match_labels).
  • match_labels: Match resources by labels (mutually exclusive with match_name).
  • namespace: The namespace to search in (optional).
Raises:
  • ValueError: If both match_name and match_labels are provided.

This tells Crossplane to fetch the specified resources and include them in the next call to the function in req.required_resources[name].

If neither match_name nor match_labels is provided, all resources of the given api_version and kind are matched.

def require_schema( rsp: crossplane.function.proto.v1.run_function_pb2.RunFunctionResponse, name: str, api_version: str, kind: str) -> None:
218def require_schema(
219    rsp: fnv1.RunFunctionResponse,
220    name: str,
221    api_version: str,
222    kind: str,
223) -> None:
224    """Add a schema requirement to the response.
225
226    Args:
227        rsp: The RunFunctionResponse to update.
228        name: The name to use for this requirement.
229        api_version: The API version of the resource kind, e.g. "example.org/v1".
230        kind: The kind of resource, e.g. "MyResource".
231
232    This tells Crossplane to fetch the OpenAPI schema for the specified resource
233    kind and include it in the next call to the function in
234    req.required_schemas[name]. Use request.get_required_schema to retrieve it.
235
236    For CRDs, Crossplane returns the spec.versions[].schema.openAPIV3Schema field.
237    If Crossplane cannot find a schema for the requested kind, the schema will be
238    empty (get_required_schema will return None).
239    """
240    selector = fnv1.SchemaSelector(
241        api_version=api_version,
242        kind=kind,
243    )
244    rsp.requirements.schemas[name].CopyFrom(selector)

Add a schema requirement to the response.

Arguments:
  • rsp: The RunFunctionResponse to update.
  • name: The name to use for this requirement.
  • api_version: The API version of the resource kind, e.g. "example.org/v1".
  • kind: The kind of resource, e.g. "MyResource".

This tells Crossplane to fetch the OpenAPI schema for the specified resource kind and include it in the next call to the function in req.required_schemas[name]. Use request.get_required_schema to retrieve it.

For CRDs, Crossplane returns the spec.versions[].schema.openAPIV3Schema field. If Crossplane cannot find a schema for the requested kind, the schema will be empty (get_required_schema will return None).