function.resource

A composition function SDK.

  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"""A composition function SDK."""
 16
 17import dataclasses
 18import datetime
 19
 20import pydantic
 21from google.protobuf import json_format
 22from google.protobuf import struct_pb2 as structpb
 23
 24import crossplane.function.proto.v1.run_function_pb2 as fnv1
 25
 26# TODO(negz): Do we really need dict_to_struct and struct_to_dict? They don't do
 27# much, but are perhaps useful for discoverability/"documentation" purposes.
 28
 29
 30def update(r: fnv1.Resource, source: dict | structpb.Struct | pydantic.BaseModel):
 31    """Update a composite or composed resource.
 32
 33    Use update to add or update the supplied resource. If the resource doesn't
 34    exist, it'll be added. If the resource does exist, it'll be updated. The
 35    update method semantics are the same as a dictionary's update method. Fields
 36    that don't exist will be added. Fields that exist will be overwritten.
 37
 38    The source can be a dictionary, a protobuf Struct, or a Pydantic model.
 39    """
 40    match source:
 41        case pydantic.BaseModel():
 42            data = source.model_dump(exclude_defaults=True, warnings=False)
 43            # In Pydantic, exclude_defaults=True in model_dump excludes fields
 44            # that have their value equal to the default. If a field like
 45            # apiVersion is set to its default value 's3.aws.upbound.io/v1beta2'
 46            # (and not explicitly provided during initialization), it will be
 47            # excluded from the serialized output.
 48            data['apiVersion'] = source.apiVersion
 49            data['kind'] = source.kind
 50            r.resource.update(data)
 51        case structpb.Struct():
 52            # TODO(negz): Use struct_to_dict and update to match other semantics?
 53            r.resource.MergeFrom(source)
 54        case dict():
 55            r.resource.update(source)
 56        case _:
 57            t = type(source)
 58            msg = f"Unsupported type: {t}"
 59            raise TypeError(msg)
 60
 61
 62def dict_to_struct(d: dict) -> structpb.Struct:
 63    """Create a Struct well-known type from the supplied dict.
 64
 65    Functions must return desired resources encoded as a protobuf struct. This
 66    function makes it possible to work with a Python dict, then convert it to a
 67    struct in a RunFunctionResponse.
 68    """
 69    return json_format.ParseDict(d, structpb.Struct())
 70
 71
 72def struct_to_dict(s: structpb.Struct) -> dict:
 73    """Create a dict from the supplied Struct well-known type.
 74
 75    Crossplane sends observed and desired resources to a function encoded as a
 76    protobuf struct. This function makes it possible to convert resources to a
 77    dictionary.
 78    """
 79    return json_format.MessageToDict(s, preserving_proto_field_name=True)
 80
 81
 82@dataclasses.dataclass
 83class Condition:
 84    """A status condition."""
 85
 86    """Type of the condition - e.g. Ready."""
 87    typ: str
 88
 89    """Status of the condition - True, False, or Unknown."""
 90    status: str
 91
 92    """Reason for the condition status - typically CamelCase."""
 93    reason: str | None = None
 94
 95    """Optional message."""
 96    message: str | None = None
 97
 98    """The last time the status transitioned to this status."""
 99    last_transition_time: datetime.time | None = None
100
101
102def get_condition(resource: structpb.Struct, typ: str) -> Condition:
103    """Get the supplied status condition of the supplied resource.
104
105    Args:
106        resource: A Crossplane resource.
107        typ: The type of status condition to get (e.g. Ready).
108
109    Returns:
110        The requested status condition.
111
112    A status condition is always returned. If the status condition isn't present
113    in the supplied resource, a condition with status "Unknown" is returned.
114    """
115    unknown = Condition(typ=typ, status="Unknown")
116
117    if not resource or "status" not in resource:
118        return unknown
119
120    if not resource["status"] or "conditions" not in resource["status"]:
121        return unknown
122
123    for c in resource["status"]["conditions"]:
124        if c["type"] != typ:
125            continue
126
127        condition = Condition(
128            typ=c["type"],
129            status=c["status"],
130        )
131        if "message" in c:
132            condition.message = c["message"]
133        if "reason" in c:
134            condition.reason = c["reason"]
135        if "lastTransitionTime" in c:
136            condition.last_transition_time = datetime.datetime.fromisoformat(
137                c["lastTransitionTime"]
138            )
139
140        return condition
141
142    return unknown
143
144
145@dataclasses.dataclass
146class Credentials:
147    """Credentials."""
148
149    type: str
150    data: dict
151
152
153def get_credentials(req: structpb.Struct, name: str) -> Credentials:
154    """Get the supplied credentials."""
155    empty = Credentials(type="data", data={})
156    if not req or "credentials" not in req:
157        return empty
158    if not req["credentials"] or name not in req["credentials"]:
159        return empty
160    return Credentials(
161        type=req["credentials"][name]["type"],
162        data=struct_to_dict(req["credentials"][name]["data"]),
163    )
def update( r: crossplane.function.proto.v1.run_function_pb2.Resource, source: dict | google.protobuf.struct_pb2.Struct | pydantic.main.BaseModel):
31def update(r: fnv1.Resource, source: dict | structpb.Struct | pydantic.BaseModel):
32    """Update a composite or composed resource.
33
34    Use update to add or update the supplied resource. If the resource doesn't
35    exist, it'll be added. If the resource does exist, it'll be updated. The
36    update method semantics are the same as a dictionary's update method. Fields
37    that don't exist will be added. Fields that exist will be overwritten.
38
39    The source can be a dictionary, a protobuf Struct, or a Pydantic model.
40    """
41    match source:
42        case pydantic.BaseModel():
43            data = source.model_dump(exclude_defaults=True, warnings=False)
44            # In Pydantic, exclude_defaults=True in model_dump excludes fields
45            # that have their value equal to the default. If a field like
46            # apiVersion is set to its default value 's3.aws.upbound.io/v1beta2'
47            # (and not explicitly provided during initialization), it will be
48            # excluded from the serialized output.
49            data['apiVersion'] = source.apiVersion
50            data['kind'] = source.kind
51            r.resource.update(data)
52        case structpb.Struct():
53            # TODO(negz): Use struct_to_dict and update to match other semantics?
54            r.resource.MergeFrom(source)
55        case dict():
56            r.resource.update(source)
57        case _:
58            t = type(source)
59            msg = f"Unsupported type: {t}"
60            raise TypeError(msg)

Update a composite or composed resource.

Use update to add or update the supplied resource. If the resource doesn't exist, it'll be added. If the resource does exist, it'll be updated. The update method semantics are the same as a dictionary's update method. Fields that don't exist will be added. Fields that exist will be overwritten.

The source can be a dictionary, a protobuf Struct, or a Pydantic model.

def dict_to_struct(d: dict) -> google.protobuf.struct_pb2.Struct:
63def dict_to_struct(d: dict) -> structpb.Struct:
64    """Create a Struct well-known type from the supplied dict.
65
66    Functions must return desired resources encoded as a protobuf struct. This
67    function makes it possible to work with a Python dict, then convert it to a
68    struct in a RunFunctionResponse.
69    """
70    return json_format.ParseDict(d, structpb.Struct())

Create a Struct well-known type from the supplied dict.

Functions must return desired resources encoded as a protobuf struct. This function makes it possible to work with a Python dict, then convert it to a struct in a RunFunctionResponse.

def struct_to_dict(s: google.protobuf.struct_pb2.Struct) -> dict:
73def struct_to_dict(s: structpb.Struct) -> dict:
74    """Create a dict from the supplied Struct well-known type.
75
76    Crossplane sends observed and desired resources to a function encoded as a
77    protobuf struct. This function makes it possible to convert resources to a
78    dictionary.
79    """
80    return json_format.MessageToDict(s, preserving_proto_field_name=True)

Create a dict from the supplied Struct well-known type.

Crossplane sends observed and desired resources to a function encoded as a protobuf struct. This function makes it possible to convert resources to a dictionary.

@dataclasses.dataclass
class Condition:
 83@dataclasses.dataclass
 84class Condition:
 85    """A status condition."""
 86
 87    """Type of the condition - e.g. Ready."""
 88    typ: str
 89
 90    """Status of the condition - True, False, or Unknown."""
 91    status: str
 92
 93    """Reason for the condition status - typically CamelCase."""
 94    reason: str | None = None
 95
 96    """Optional message."""
 97    message: str | None = None
 98
 99    """The last time the status transitioned to this status."""
100    last_transition_time: datetime.time | None = None

A status condition.

Condition( typ: str, status: str, reason: str | None = None, message: str | None = None, last_transition_time: datetime.time | None = None)
typ: str

Status of the condition - True, False, or Unknown.

status: str

Reason for the condition status - typically CamelCase.

reason: str | None = None

Optional message.

message: str | None = None

The last time the status transitioned to this status.

last_transition_time: datetime.time | None = None
def get_condition( resource: google.protobuf.struct_pb2.Struct, typ: str) -> Condition:
103def get_condition(resource: structpb.Struct, typ: str) -> Condition:
104    """Get the supplied status condition of the supplied resource.
105
106    Args:
107        resource: A Crossplane resource.
108        typ: The type of status condition to get (e.g. Ready).
109
110    Returns:
111        The requested status condition.
112
113    A status condition is always returned. If the status condition isn't present
114    in the supplied resource, a condition with status "Unknown" is returned.
115    """
116    unknown = Condition(typ=typ, status="Unknown")
117
118    if not resource or "status" not in resource:
119        return unknown
120
121    if not resource["status"] or "conditions" not in resource["status"]:
122        return unknown
123
124    for c in resource["status"]["conditions"]:
125        if c["type"] != typ:
126            continue
127
128        condition = Condition(
129            typ=c["type"],
130            status=c["status"],
131        )
132        if "message" in c:
133            condition.message = c["message"]
134        if "reason" in c:
135            condition.reason = c["reason"]
136        if "lastTransitionTime" in c:
137            condition.last_transition_time = datetime.datetime.fromisoformat(
138                c["lastTransitionTime"]
139            )
140
141        return condition
142
143    return unknown

Get the supplied status condition of the supplied resource.

Arguments:
  • resource: A Crossplane resource.
  • typ: The type of status condition to get (e.g. Ready).
Returns:

The requested status condition.

A status condition is always returned. If the status condition isn't present in the supplied resource, a condition with status "Unknown" is returned.

@dataclasses.dataclass
class Credentials:
146@dataclasses.dataclass
147class Credentials:
148    """Credentials."""
149
150    type: str
151    data: dict

Credentials.

Credentials(type: str, data: dict)
type: str
data: dict
def get_credentials( req: google.protobuf.struct_pb2.Struct, name: str) -> Credentials:
154def get_credentials(req: structpb.Struct, name: str) -> Credentials:
155    """Get the supplied credentials."""
156    empty = Credentials(type="data", data={})
157    if not req or "credentials" not in req:
158        return empty
159    if not req["credentials"] or name not in req["credentials"]:
160        return empty
161    return Credentials(
162        type=req["credentials"][name]["type"],
163        data=struct_to_dict(req["credentials"][name]["data"]),
164    )

Get the supplied credentials.