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 )
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.
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.
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.
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.
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.
146@dataclasses.dataclass 147class Credentials: 148 """Credentials.""" 149 150 type: str 151 data: dict
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.