Promote composition functions to v1

Signed-off-by: Nic Cope <nicc@rk0n.org>
This commit is contained in:
Nic Cope 2024-08-15 18:18:20 -07:00
parent 7837143425
commit 0f7d85f43d
12 changed files with 1025 additions and 82 deletions

View File

@ -0,0 +1,326 @@
/*
Copyright 2022 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.
*/
syntax = "proto3";
import "google/protobuf/struct.proto";
import "google/protobuf/duration.proto";
package apiextensions.fn.proto.v1;
option go_package = "github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1";
// A FunctionRunnerService is a Composition Function.
service FunctionRunnerService {
// RunFunction runs the Composition Function.
rpc RunFunction(RunFunctionRequest) returns (RunFunctionResponse) {}
}
// A RunFunctionRequest requests that the Composition Function be run.
message RunFunctionRequest {
// Metadata pertaining to this request.
RequestMeta meta = 1;
// The observed state prior to invocation of a Function pipeline. State passed
// to each Function is fresh as of the time the pipeline was invoked, not as
// of the time each Function was invoked.
State observed = 2;
// Desired state according to a Function pipeline. The state passed to a
// particular Function may have been accumulated by previous Functions in the
// pipeline.
//
// Note that the desired state must be a partial object with only the fields
// that this function (and its predecessors in the pipeline) wants to have
// set in the object. Copying a non-partial observed state to desired is most
// likely not what you want to do. Leaving out fields that had been returned
// as desired before will result in them being deleted from the objects in the
// cluster.
State desired = 3;
// Optional input specific to this Function invocation. A JSON representation
// of the 'input' block of the relevant entry in a Composition's pipeline.
optional google.protobuf.Struct input = 4;
// Optional context. Crossplane may pass arbitary contextual information to a
// Function. A Function may also return context in its RunFunctionResponse,
// and that context will be passed to subsequent Functions. Crossplane
// discards all context returned by the last Function in the pipeline.
optional google.protobuf.Struct context = 5;
// Optional extra resources that the Function required.
// Note that extra resources is a map to Resources, plural.
// The map key corresponds to the key in a RunFunctionResponse's
// extra_resources field. If a Function requested extra resources that
// did not exist, Crossplane sets the map key to an empty Resources message to
// indicate that it attempted to satisfy the request.
map<string, Resources> extra_resources = 6;
// Optional credentials that this Function may use to communicate with an
// external system.
map <string, Credentials> credentials = 7;
}
// Credentials that a Function may use to communicate with an external system.
message Credentials {
// Source of the credentials.
oneof source {
// Credential data loaded by Crossplane, for example from a Secret.
CredentialData credential_data = 1;
}
}
// CredentialData loaded by Crossplane, for example from a Secret.
message CredentialData {
map<string, bytes> data = 1;
}
// Resources represents the state of several Crossplane resources.
message Resources {
repeated Resource items = 1;
}
// A RunFunctionResponse contains the result of a Composition Function run.
message RunFunctionResponse {
// Metadata pertaining to this response.
ResponseMeta meta = 1;
// Desired state according to a Function pipeline. Functions may add desired
// state, and may mutate or delete any part of the desired state they are
// concerned with. A Function must pass through any part of the desired state
// that it is not concerned with.
//
//
// Note that the desired state must be a partial object with only the fields
// that this function (and its predecessors in the pipeline) wants to have
// set in the object. Copying a non-partial observed state to desired is most
// likely not what you want to do. Leaving out fields that had been returned
// as desired before will result in them being deleted from the objects in the
// cluster.
State desired = 2;
// Results of the Function run. Results are used for observability purposes.
repeated Result results = 3;
// Optional context to be passed to the next Function in the pipeline as part
// of the RunFunctionRequest. Dropped on the last function in the pipeline.
optional google.protobuf.Struct context = 4;
// Requirements that must be satisfied for this Function to run successfully.
Requirements requirements = 5;
// Status conditions to be applied to the composite resource. Conditions may also
// optionally be applied to the composite resource's associated claim.
repeated Condition conditions = 6;
}
// RequestMeta contains metadata pertaining to a RunFunctionRequest.
message RequestMeta {
// An opaque string identifying the content of the request. Two identical
// requests should have the same tag.
string tag = 1;
}
// Requirements that must be satisfied for a Function to run successfully.
message Requirements {
// Extra resources that this Function requires.
// The map key uniquely identifies the group of resources.
map<string, ResourceSelector> extra_resources = 1;
}
// ResourceSelector selects a group of resources, either by name or by label.
message ResourceSelector {
// API version of resources to select.
string api_version = 1;
// Kind of resources to select.
string kind = 2;
// Resources to match.
oneof match {
// Match the resource with this name.
string match_name = 3;
// Match all resources with these labels.
MatchLabels match_labels = 4;
}
}
// MatchLabels defines a set of labels to match resources against.
message MatchLabels {
map<string, string> labels = 1;
}
// ResponseMeta contains metadata pertaining to a RunFunctionResponse.
message ResponseMeta {
// An opaque string identifying the content of the request. Must match the
// meta.tag of the corresponding RunFunctionRequest.
string tag = 1;
// Time-to-live of this response. Deterministic Functions with no side-effects
// (e.g. simple templating Functions) may specify a TTL. Crossplane may choose
// to cache responses until the TTL expires.
optional google.protobuf.Duration ttl = 2;
}
// State of the composite resource (XR) and any composed resources.
message State {
// The state of the composite resource (XR).
Resource composite = 1;
// The state of any composed resources.
map<string, Resource> resources = 2;
}
// A Resource represents the state of a composite or composed resource.
message Resource {
// The JSON representation of the resource.
//
// * Crossplane will set this field in a RunFunctionRequest to the entire
// observed state of a resource - including its metadata, spec, and status.
//
// * A Function should set this field in a RunFunctionRequest to communicate
// the desired state of a composite or composed resource.
//
// * A Function may only specify the desired status of a composite resource -
// not its metadata or spec. A Function should not return desired metadata
// or spec for a composite resource. This will be ignored.
//
// * A Function may not specify the desired status of a composed resource -
// only its metadata and spec. A Function should not return desired status
// for a composed resource. This will be ignored.
google.protobuf.Struct resource = 1;
// The resource's connection details.
//
// * Crossplane will set this field in a RunFunctionRequest to communicate the
// the observed connection details of a composite or composed resource.
//
// * A Function should set this field in a RunFunctionResponse to indicate the
// desired connection details of the composite resource.
//
// * A Function should not set this field in a RunFunctionResponse to indicate
// the desired connection details of a composed resource. This will be
// ignored.
map<string, bytes> connection_details = 2;
// Ready indicates whether the resource should be considered ready.
//
// * Crossplane will never set this field in a RunFunctionRequest.
//
// * A Function should set this field to READY_TRUE in a RunFunctionResponse
// to indicate that a desired composed resource is ready.
//
// * A Function should not set this field in a RunFunctionResponse to indicate
// that the desired composite resource is ready. This will be ignored.
Ready ready = 3;
}
// Ready indicates whether a composed resource should be considered ready.
enum Ready {
READY_UNSPECIFIED = 0;
// True means the composed resource has been observed to be ready.
READY_TRUE = 1;
// False means the composed resource has not been observed to be ready.
READY_FALSE = 2;
}
// A Result of running a Function.
message Result {
// Severity of this result.
Severity severity = 1;
// Human-readable details about the result.
string message = 2;
// Optional PascalCase, machine-readable reason for this result. If omitted,
// the value will be ComposeResources.
optional string reason = 3;
// The resources this result targets.
optional Target target = 4;
}
// Severity of Function results.
enum Severity {
SEVERITY_UNSPECIFIED = 0;
// Fatal results are fatal; subsequent Composition Functions may run, but
// the Composition Function pipeline run will be considered a failure and
// the first fatal result will be returned as an error.
SEVERITY_FATAL = 1;
// Warning results are non-fatal; the entire Composition will run to
// completion but warning events and debug logs associated with the
// composite resource will be emitted.
SEVERITY_WARNING = 2;
// Normal results are emitted as normal events and debug logs associated
// with the composite resource.
SEVERITY_NORMAL = 3;
}
// Target of Function results and conditions.
enum Target {
// If the target is unspecified, the result targets the composite resource.
TARGET_UNSPECIFIED = 0;
// Target the composite resource. Results that target the composite resource
// should include detailed, advanced information.
TARGET_COMPOSITE = 1;
// Target the composite and the claim. Results that target the composite and
// the claim should include only end-user friendly information.
TARGET_COMPOSITE_AND_CLAIM = 2;
}
// Status condition to be applied to the composite resource. Condition may also
// optionally be applied to the composite resource's associated claim. For
// detailed information on proper usage of status conditions, please see
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties.
message Condition {
// Type of condition in PascalCase.
string type = 1;
// Status of the condition.
Status status = 2;
// Reason contains a programmatic identifier indicating the reason for the
// condition's last transition. Producers of specific condition types may
// define expected values and meanings for this field, and whether the values
// are considered a guaranteed API. The value should be a PascalCase string.
// This field may not be empty.
string reason = 3;
// Message is a human readable message indicating details about the
// transition. This may be an empty string.
optional string message = 4;
// The resources this condition targets.
optional Target target = 5;
}
enum Status {
STATUS_CONDITION_UNSPECIFIED = 0;
STATUS_CONDITION_UNKNOWN = 1;
STATUS_CONDITION_TRUE = 2;
STATUS_CONDITION_FALSE = 3;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,232 @@
from google.protobuf import struct_pb2 as _struct_pb2
from google.protobuf import duration_pb2 as _duration_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class Ready(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
READY_UNSPECIFIED: _ClassVar[Ready]
READY_TRUE: _ClassVar[Ready]
READY_FALSE: _ClassVar[Ready]
class Severity(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
SEVERITY_UNSPECIFIED: _ClassVar[Severity]
SEVERITY_FATAL: _ClassVar[Severity]
SEVERITY_WARNING: _ClassVar[Severity]
SEVERITY_NORMAL: _ClassVar[Severity]
class Target(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
TARGET_UNSPECIFIED: _ClassVar[Target]
TARGET_COMPOSITE: _ClassVar[Target]
TARGET_COMPOSITE_AND_CLAIM: _ClassVar[Target]
class Status(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
STATUS_CONDITION_UNSPECIFIED: _ClassVar[Status]
STATUS_CONDITION_UNKNOWN: _ClassVar[Status]
STATUS_CONDITION_TRUE: _ClassVar[Status]
STATUS_CONDITION_FALSE: _ClassVar[Status]
READY_UNSPECIFIED: Ready
READY_TRUE: Ready
READY_FALSE: Ready
SEVERITY_UNSPECIFIED: Severity
SEVERITY_FATAL: Severity
SEVERITY_WARNING: Severity
SEVERITY_NORMAL: Severity
TARGET_UNSPECIFIED: Target
TARGET_COMPOSITE: Target
TARGET_COMPOSITE_AND_CLAIM: Target
STATUS_CONDITION_UNSPECIFIED: Status
STATUS_CONDITION_UNKNOWN: Status
STATUS_CONDITION_TRUE: Status
STATUS_CONDITION_FALSE: Status
class RunFunctionRequest(_message.Message):
__slots__ = ("meta", "observed", "desired", "input", "context", "extra_resources", "credentials")
class ExtraResourcesEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: Resources
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[Resources, _Mapping]] = ...) -> None: ...
class CredentialsEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: Credentials
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[Credentials, _Mapping]] = ...) -> None: ...
META_FIELD_NUMBER: _ClassVar[int]
OBSERVED_FIELD_NUMBER: _ClassVar[int]
DESIRED_FIELD_NUMBER: _ClassVar[int]
INPUT_FIELD_NUMBER: _ClassVar[int]
CONTEXT_FIELD_NUMBER: _ClassVar[int]
EXTRA_RESOURCES_FIELD_NUMBER: _ClassVar[int]
CREDENTIALS_FIELD_NUMBER: _ClassVar[int]
meta: RequestMeta
observed: State
desired: State
input: _struct_pb2.Struct
context: _struct_pb2.Struct
extra_resources: _containers.MessageMap[str, Resources]
credentials: _containers.MessageMap[str, Credentials]
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]] = ...) -> None: ...
class Credentials(_message.Message):
__slots__ = ("credential_data",)
CREDENTIAL_DATA_FIELD_NUMBER: _ClassVar[int]
credential_data: CredentialData
def __init__(self, credential_data: _Optional[_Union[CredentialData, _Mapping]] = ...) -> None: ...
class CredentialData(_message.Message):
__slots__ = ("data",)
class DataEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: bytes
def __init__(self, key: _Optional[str] = ..., value: _Optional[bytes] = ...) -> None: ...
DATA_FIELD_NUMBER: _ClassVar[int]
data: _containers.ScalarMap[str, bytes]
def __init__(self, data: _Optional[_Mapping[str, bytes]] = ...) -> None: ...
class Resources(_message.Message):
__slots__ = ("items",)
ITEMS_FIELD_NUMBER: _ClassVar[int]
items: _containers.RepeatedCompositeFieldContainer[Resource]
def __init__(self, items: _Optional[_Iterable[_Union[Resource, _Mapping]]] = ...) -> None: ...
class RunFunctionResponse(_message.Message):
__slots__ = ("meta", "desired", "results", "context", "requirements", "conditions")
META_FIELD_NUMBER: _ClassVar[int]
DESIRED_FIELD_NUMBER: _ClassVar[int]
RESULTS_FIELD_NUMBER: _ClassVar[int]
CONTEXT_FIELD_NUMBER: _ClassVar[int]
REQUIREMENTS_FIELD_NUMBER: _ClassVar[int]
CONDITIONS_FIELD_NUMBER: _ClassVar[int]
meta: ResponseMeta
desired: State
results: _containers.RepeatedCompositeFieldContainer[Result]
context: _struct_pb2.Struct
requirements: Requirements
conditions: _containers.RepeatedCompositeFieldContainer[Condition]
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]]] = ...) -> None: ...
class RequestMeta(_message.Message):
__slots__ = ("tag",)
TAG_FIELD_NUMBER: _ClassVar[int]
tag: str
def __init__(self, tag: _Optional[str] = ...) -> None: ...
class Requirements(_message.Message):
__slots__ = ("extra_resources",)
class ExtraResourcesEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: ResourceSelector
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[ResourceSelector, _Mapping]] = ...) -> None: ...
EXTRA_RESOURCES_FIELD_NUMBER: _ClassVar[int]
extra_resources: _containers.MessageMap[str, ResourceSelector]
def __init__(self, extra_resources: _Optional[_Mapping[str, ResourceSelector]] = ...) -> None: ...
class ResourceSelector(_message.Message):
__slots__ = ("api_version", "kind", "match_name", "match_labels")
API_VERSION_FIELD_NUMBER: _ClassVar[int]
KIND_FIELD_NUMBER: _ClassVar[int]
MATCH_NAME_FIELD_NUMBER: _ClassVar[int]
MATCH_LABELS_FIELD_NUMBER: _ClassVar[int]
api_version: str
kind: str
match_name: str
match_labels: MatchLabels
def __init__(self, api_version: _Optional[str] = ..., kind: _Optional[str] = ..., match_name: _Optional[str] = ..., match_labels: _Optional[_Union[MatchLabels, _Mapping]] = ...) -> None: ...
class MatchLabels(_message.Message):
__slots__ = ("labels",)
class LabelsEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: str
def __init__(self, key: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
LABELS_FIELD_NUMBER: _ClassVar[int]
labels: _containers.ScalarMap[str, str]
def __init__(self, labels: _Optional[_Mapping[str, str]] = ...) -> None: ...
class ResponseMeta(_message.Message):
__slots__ = ("tag", "ttl")
TAG_FIELD_NUMBER: _ClassVar[int]
TTL_FIELD_NUMBER: _ClassVar[int]
tag: str
ttl: _duration_pb2.Duration
def __init__(self, tag: _Optional[str] = ..., ttl: _Optional[_Union[_duration_pb2.Duration, _Mapping]] = ...) -> None: ...
class State(_message.Message):
__slots__ = ("composite", "resources")
class ResourcesEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: Resource
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[Resource, _Mapping]] = ...) -> None: ...
COMPOSITE_FIELD_NUMBER: _ClassVar[int]
RESOURCES_FIELD_NUMBER: _ClassVar[int]
composite: Resource
resources: _containers.MessageMap[str, Resource]
def __init__(self, composite: _Optional[_Union[Resource, _Mapping]] = ..., resources: _Optional[_Mapping[str, Resource]] = ...) -> None: ...
class Resource(_message.Message):
__slots__ = ("resource", "connection_details", "ready")
class ConnectionDetailsEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: bytes
def __init__(self, key: _Optional[str] = ..., value: _Optional[bytes] = ...) -> None: ...
RESOURCE_FIELD_NUMBER: _ClassVar[int]
CONNECTION_DETAILS_FIELD_NUMBER: _ClassVar[int]
READY_FIELD_NUMBER: _ClassVar[int]
resource: _struct_pb2.Struct
connection_details: _containers.ScalarMap[str, bytes]
ready: Ready
def __init__(self, resource: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., connection_details: _Optional[_Mapping[str, bytes]] = ..., ready: _Optional[_Union[Ready, str]] = ...) -> None: ...
class Result(_message.Message):
__slots__ = ("severity", "message", "reason", "target")
SEVERITY_FIELD_NUMBER: _ClassVar[int]
MESSAGE_FIELD_NUMBER: _ClassVar[int]
REASON_FIELD_NUMBER: _ClassVar[int]
TARGET_FIELD_NUMBER: _ClassVar[int]
severity: Severity
message: str
reason: str
target: Target
def __init__(self, severity: _Optional[_Union[Severity, str]] = ..., message: _Optional[str] = ..., reason: _Optional[str] = ..., target: _Optional[_Union[Target, str]] = ...) -> None: ...
class Condition(_message.Message):
__slots__ = ("type", "status", "reason", "message", "target")
TYPE_FIELD_NUMBER: _ClassVar[int]
STATUS_FIELD_NUMBER: _ClassVar[int]
REASON_FIELD_NUMBER: _ClassVar[int]
MESSAGE_FIELD_NUMBER: _ClassVar[int]
TARGET_FIELD_NUMBER: _ClassVar[int]
type: str
status: Status
reason: str
message: str
target: Target
def __init__(self, type: _Optional[str] = ..., status: _Optional[_Union[Status, str]] = ..., reason: _Optional[str] = ..., message: _Optional[str] = ..., target: _Optional[_Union[Target, str]] = ...) -> None: ...

View File

@ -0,0 +1,106 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
from crossplane.function.proto.v1 import run_function_pb2 as crossplane_dot_function_dot_proto_dot_v1_dot_run__function__pb2
GRPC_GENERATED_VERSION = '1.65.4'
GRPC_VERSION = grpc.__version__
EXPECTED_ERROR_RELEASE = '1.66.0'
SCHEDULED_RELEASE_DATE = 'August 6, 2024'
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
warnings.warn(
f'The grpc package installed is at version {GRPC_VERSION},'
+ f' but the generated code in crossplane/function/proto/v1/run_function_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ f' This warning will become an error in {EXPECTED_ERROR_RELEASE},'
+ f' scheduled for release on {SCHEDULED_RELEASE_DATE}.',
RuntimeWarning
)
class FunctionRunnerServiceStub(object):
"""A FunctionRunnerService is a Composition Function.
"""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.RunFunction = channel.unary_unary(
'/apiextensions.fn.proto.v1.FunctionRunnerService/RunFunction',
request_serializer=crossplane_dot_function_dot_proto_dot_v1_dot_run__function__pb2.RunFunctionRequest.SerializeToString,
response_deserializer=crossplane_dot_function_dot_proto_dot_v1_dot_run__function__pb2.RunFunctionResponse.FromString,
_registered_method=True)
class FunctionRunnerServiceServicer(object):
"""A FunctionRunnerService is a Composition Function.
"""
def RunFunction(self, request, context):
"""RunFunction runs the Composition Function.
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def add_FunctionRunnerServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'RunFunction': grpc.unary_unary_rpc_method_handler(
servicer.RunFunction,
request_deserializer=crossplane_dot_function_dot_proto_dot_v1_dot_run__function__pb2.RunFunctionRequest.FromString,
response_serializer=crossplane_dot_function_dot_proto_dot_v1_dot_run__function__pb2.RunFunctionResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'apiextensions.fn.proto.v1.FunctionRunnerService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers('apiextensions.fn.proto.v1.FunctionRunnerService', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API.
class FunctionRunnerService(object):
"""A FunctionRunnerService is a Composition Function.
"""
@staticmethod
def RunFunction(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/apiextensions.fn.proto.v1.FunctionRunnerService/RunFunction',
crossplane_dot_function_dot_proto_dot_v1_dot_run__function__pb2.RunFunctionRequest.SerializeToString,
crossplane_dot_function_dot_proto_dot_v1_dot_run__function__pb2.RunFunctionResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)

View File

@ -19,11 +19,12 @@ syntax = "proto3";
import "google/protobuf/struct.proto";
import "google/protobuf/duration.proto";
// Note that the authoritative Composition Functions protobuf definition lives
// at the below URL. Each SDK maintains and manually syncs its own copy.
// https://github.com/crossplane/crossplane/tree/master/apis/apiextensions/fn/proto
// Generated from apiextensions/fn/proto/v1/run_function.proto by ../hack/duplicate_proto_type.sh. DO NOT EDIT.
package apiextensions.fn.proto.v1beta1;
option go_package = "github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1beta1";
// A FunctionRunnerService is a Composition Function.
service FunctionRunnerService {
// RunFunction runs the Composition Function.
@ -122,6 +123,10 @@ message RunFunctionResponse {
// Requirements that must be satisfied for this Function to run successfully.
Requirements requirements = 5;
// Status conditions to be applied to the composite resource. Conditions may also
// optionally be applied to the composite resource's associated claim.
repeated Condition conditions = 6;
}
// RequestMeta contains metadata pertaining to a RunFunctionRequest.
@ -140,11 +145,18 @@ message Requirements {
// ResourceSelector selects a group of resources, either by name or by label.
message ResourceSelector {
// API version of resources to select.
string api_version = 1;
// Kind of resources to select.
string kind = 2;
// Resources to match.
oneof match {
// Match the resource with this name.
string match_name = 3;
// Match all resources with these labels.
MatchLabels match_labels = 4;
}
}
@ -237,6 +249,13 @@ message Result {
// Human-readable details about the result.
string message = 2;
// Optional PascalCase, machine-readable reason for this result. If omitted,
// the value will be ComposeResources.
optional string reason = 3;
// The resources this result targets.
optional Target target = 4;
}
// Severity of Function results.
@ -257,3 +276,53 @@ enum Severity {
// with the composite resource.
SEVERITY_NORMAL = 3;
}
// Target of Function results and conditions.
enum Target {
// If the target is unspecified, the result targets the composite resource.
TARGET_UNSPECIFIED = 0;
// Target the composite resource. Results that target the composite resource
// should include detailed, advanced information.
TARGET_COMPOSITE = 1;
// Target the composite and the claim. Results that target the composite and
// the claim should include only end-user friendly information.
TARGET_COMPOSITE_AND_CLAIM = 2;
}
// Status condition to be applied to the composite resource. Condition may also
// optionally be applied to the composite resource's associated claim. For
// detailed information on proper usage of status conditions, please see
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties.
message Condition {
// Type of condition in PascalCase.
string type = 1;
// Status of the condition.
Status status = 2;
// Reason contains a programmatic identifier indicating the reason for the
// condition's last transition. Producers of specific condition types may
// define expected values and meanings for this field, and whether the values
// are considered a guaranteed API. The value should be a PascalCase string.
// This field may not be empty.
string reason = 3;
// Message is a human readable message indicating details about the
// transition. This may be an empty string.
optional string message = 4;
// The resources this condition targets.
optional Target target = 5;
}
enum Status {
STATUS_CONDITION_UNSPECIFIED = 0;
STATUS_CONDITION_UNKNOWN = 1;
STATUS_CONDITION_TRUE = 2;
STATUS_CONDITION_FALSE = 3;
}

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,19 @@ class Severity(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
SEVERITY_FATAL: _ClassVar[Severity]
SEVERITY_WARNING: _ClassVar[Severity]
SEVERITY_NORMAL: _ClassVar[Severity]
class Target(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
TARGET_UNSPECIFIED: _ClassVar[Target]
TARGET_COMPOSITE: _ClassVar[Target]
TARGET_COMPOSITE_AND_CLAIM: _ClassVar[Target]
class Status(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
STATUS_CONDITION_UNSPECIFIED: _ClassVar[Status]
STATUS_CONDITION_UNKNOWN: _ClassVar[Status]
STATUS_CONDITION_TRUE: _ClassVar[Status]
STATUS_CONDITION_FALSE: _ClassVar[Status]
READY_UNSPECIFIED: Ready
READY_TRUE: Ready
READY_FALSE: Ready
@ -27,6 +40,13 @@ SEVERITY_UNSPECIFIED: Severity
SEVERITY_FATAL: Severity
SEVERITY_WARNING: Severity
SEVERITY_NORMAL: Severity
TARGET_UNSPECIFIED: Target
TARGET_COMPOSITE: Target
TARGET_COMPOSITE_AND_CLAIM: Target
STATUS_CONDITION_UNSPECIFIED: Status
STATUS_CONDITION_UNKNOWN: Status
STATUS_CONDITION_TRUE: Status
STATUS_CONDITION_FALSE: Status
class RunFunctionRequest(_message.Message):
__slots__ = ("meta", "observed", "desired", "input", "context", "extra_resources", "credentials")
@ -86,18 +106,20 @@ class Resources(_message.Message):
def __init__(self, items: _Optional[_Iterable[_Union[Resource, _Mapping]]] = ...) -> None: ...
class RunFunctionResponse(_message.Message):
__slots__ = ("meta", "desired", "results", "context", "requirements")
__slots__ = ("meta", "desired", "results", "context", "requirements", "conditions")
META_FIELD_NUMBER: _ClassVar[int]
DESIRED_FIELD_NUMBER: _ClassVar[int]
RESULTS_FIELD_NUMBER: _ClassVar[int]
CONTEXT_FIELD_NUMBER: _ClassVar[int]
REQUIREMENTS_FIELD_NUMBER: _ClassVar[int]
CONDITIONS_FIELD_NUMBER: _ClassVar[int]
meta: ResponseMeta
desired: State
results: _containers.RepeatedCompositeFieldContainer[Result]
context: _struct_pb2.Struct
requirements: Requirements
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]] = ...) -> None: ...
conditions: _containers.RepeatedCompositeFieldContainer[Condition]
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]]] = ...) -> None: ...
class RequestMeta(_message.Message):
__slots__ = ("tag",)
@ -184,9 +206,27 @@ class Resource(_message.Message):
def __init__(self, resource: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., connection_details: _Optional[_Mapping[str, bytes]] = ..., ready: _Optional[_Union[Ready, str]] = ...) -> None: ...
class Result(_message.Message):
__slots__ = ("severity", "message")
__slots__ = ("severity", "message", "reason", "target")
SEVERITY_FIELD_NUMBER: _ClassVar[int]
MESSAGE_FIELD_NUMBER: _ClassVar[int]
REASON_FIELD_NUMBER: _ClassVar[int]
TARGET_FIELD_NUMBER: _ClassVar[int]
severity: Severity
message: str
def __init__(self, severity: _Optional[_Union[Severity, str]] = ..., message: _Optional[str] = ...) -> None: ...
reason: str
target: Target
def __init__(self, severity: _Optional[_Union[Severity, str]] = ..., message: _Optional[str] = ..., reason: _Optional[str] = ..., target: _Optional[_Union[Target, str]] = ...) -> None: ...
class Condition(_message.Message):
__slots__ = ("type", "status", "reason", "message", "target")
TYPE_FIELD_NUMBER: _ClassVar[int]
STATUS_FIELD_NUMBER: _ClassVar[int]
REASON_FIELD_NUMBER: _ClassVar[int]
MESSAGE_FIELD_NUMBER: _ClassVar[int]
TARGET_FIELD_NUMBER: _ClassVar[int]
type: str
status: Status
reason: str
message: str
target: Target
def __init__(self, type: _Optional[str] = ..., status: _Optional[_Union[Status, str]] = ..., reason: _Optional[str] = ..., message: _Optional[str] = ..., target: _Optional[_Union[Target, str]] = ...) -> None: ...

View File

@ -1,9 +1,34 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
from crossplane.function.proto.v1beta1 import run_function_pb2 as crossplane_dot_function_dot_proto_dot_v1beta1_dot_run__function__pb2
GRPC_GENERATED_VERSION = '1.65.4'
GRPC_VERSION = grpc.__version__
EXPECTED_ERROR_RELEASE = '1.66.0'
SCHEDULED_RELEASE_DATE = 'August 6, 2024'
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
warnings.warn(
f'The grpc package installed is at version {GRPC_VERSION},'
+ f' but the generated code in crossplane/function/proto/v1beta1/run_function_pb2_grpc.py depends on'
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ f' This warning will become an error in {EXPECTED_ERROR_RELEASE},'
+ f' scheduled for release on {SCHEDULED_RELEASE_DATE}.',
RuntimeWarning
)
class FunctionRunnerServiceStub(object):
"""A FunctionRunnerService is a Composition Function.
@ -19,7 +44,7 @@ class FunctionRunnerServiceStub(object):
'/apiextensions.fn.proto.v1beta1.FunctionRunnerService/RunFunction',
request_serializer=crossplane_dot_function_dot_proto_dot_v1beta1_dot_run__function__pb2.RunFunctionRequest.SerializeToString,
response_deserializer=crossplane_dot_function_dot_proto_dot_v1beta1_dot_run__function__pb2.RunFunctionResponse.FromString,
)
_registered_method=True)
class FunctionRunnerServiceServicer(object):
@ -45,6 +70,7 @@ def add_FunctionRunnerServiceServicer_to_server(servicer, server):
generic_handler = grpc.method_handlers_generic_handler(
'apiextensions.fn.proto.v1beta1.FunctionRunnerService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers('apiextensions.fn.proto.v1beta1.FunctionRunnerService', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API.
@ -63,8 +89,18 @@ class FunctionRunnerService(object):
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/apiextensions.fn.proto.v1beta1.FunctionRunnerService/RunFunction',
return grpc.experimental.unary_unary(
request,
target,
'/apiextensions.fn.proto.v1beta1.FunctionRunnerService/RunFunction',
crossplane_dot_function_dot_proto_dot_v1beta1_dot_run__function__pb2.RunFunctionRequest.SerializeToString,
crossplane_dot_function_dot_proto_dot_v1beta1_dot_run__function__pb2.RunFunctionResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)

View File

@ -18,16 +18,16 @@ import datetime
from google.protobuf import duration_pb2 as durationpb
import crossplane.function.proto.v1beta1.run_function_pb2 as fnv1beta1
import crossplane.function.proto.v1.run_function_pb2 as fnv1
"""The default TTL for which a RunFunctionResponse may be cached."""
DEFAULT_TTL = datetime.timedelta(minutes=1)
def to(
req: fnv1beta1.RunFunctionRequest,
req: fnv1.RunFunctionRequest,
ttl: datetime.timedelta = DEFAULT_TTL,
) -> fnv1beta1.RunFunctionResponse:
) -> fnv1.RunFunctionResponse:
"""Create a response to the supplied request.
Args:
@ -42,38 +42,38 @@ def to(
"""
dttl = durationpb.Duration()
dttl.FromTimedelta(ttl)
return fnv1beta1.RunFunctionResponse(
meta=fnv1beta1.ResponseMeta(tag=req.meta.tag, ttl=dttl),
return fnv1.RunFunctionResponse(
meta=fnv1.ResponseMeta(tag=req.meta.tag, ttl=dttl),
desired=req.desired,
context=req.context,
)
def normal(rsp: fnv1beta1.RunFunctionResponse, message: str) -> None:
def normal(rsp: fnv1.RunFunctionResponse, message: str) -> None:
"""Add a normal result to the response."""
rsp.results.append(
fnv1beta1.Result(
severity=fnv1beta1.SEVERITY_NORMAL,
fnv1.Result(
severity=fnv1.SEVERITY_NORMAL,
message=message,
)
)
def warning(rsp: fnv1beta1.RunFunctionResponse, message: str) -> None:
def warning(rsp: fnv1.RunFunctionResponse, message: str) -> None:
"""Add a warning result to the response."""
rsp.results.append(
fnv1beta1.Result(
severity=fnv1beta1.SEVERITY_WARNING,
fnv1.Result(
severity=fnv1.SEVERITY_WARNING,
message=message,
)
)
def fatal(rsp: fnv1beta1.RunFunctionResponse, message: str) -> None:
def fatal(rsp: fnv1.RunFunctionResponse, message: str) -> None:
"""Add a fatal result to the response."""
rsp.results.append(
fnv1beta1.Result(
severity=fnv1beta1.SEVERITY_FATAL,
fnv1.Result(
severity=fnv1.SEVERITY_FATAL,
message=message,
)
)

View File

@ -20,11 +20,14 @@ import os
import grpc
from grpc_reflection.v1alpha import reflection
import crossplane.function.proto.v1.run_function_pb2 as fnv1
import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1
import crossplane.function.proto.v1beta1.run_function_pb2 as fnv1beta1
import crossplane.function.proto.v1beta1.run_function_pb2_grpc as grpcv1beta1
SERVICE_NAMES = (
reflection.SERVICE_NAME,
fnv1.DESCRIPTOR.services_by_name["FunctionRunnerService"].full_name,
fnv1beta1.DESCRIPTOR.services_by_name["FunctionRunnerService"].full_name,
)
@ -62,7 +65,7 @@ def load_credentials(tls_certs_dir: str) -> grpc.ServerCredentials:
def serve(
function: grpcv1beta1.FunctionRunnerService,
function: grpcv1.FunctionRunnerService,
address: str,
*,
creds: grpc.ServerCredentials,
@ -87,7 +90,10 @@ def serve(
server = grpc.aio.server()
grpcv1beta1.add_FunctionRunnerServiceServicer_to_server(function, server)
grpcv1.add_FunctionRunnerServiceServicer_to_server(function, server)
grpcv1beta1.add_FunctionRunnerServiceServicer_to_server(
BetaFunctionRunner(wrapped=function), server
)
reflection.enable_server_reflection(SERVICE_NAMES, server)
if creds is None and insecure is False:
@ -112,3 +118,30 @@ def serve(
finally:
loop.run_until_complete(server.stop(grace=5))
loop.close()
class BetaFunctionRunner(grpcv1beta1.FunctionRunnerService):
"""A BetaFunctionRunner handles beta gRPC RunFunctionRequests.
It handles requests by passing them to a wrapped v1.FunctionRunnerService.
Incoming v1beta1 requests are converted to v1 by round-tripping them through
serialization. Outgoing requests are converted from v1 to v1beta1 the same
way.
"""
def __init__(self, wrapped: grpcv1.FunctionRunnerService):
"""Create a new BetaFunctionRunner."""
self.wrapped = wrapped
async def RunFunction( # noqa: N802 # gRPC requires this name.
self, req: fnv1beta1.RunFunctionRequest, context: grpc.aio.ServicerContext
) -> fnv1beta1.RunFunctionResponse:
"""Run the underlying function."""
gareq = fnv1.RunFunctionRequest()
gareq.ParseFromString(req.SerializeToString())
garsp = await self.wrapped.RunFunction(gareq, context)
rsp = fnv1beta1.RunFunctionRequest()
rsp.ParseFromString(garsp.SerializeToString())
return rsp

View File

@ -41,7 +41,7 @@ path = ".venv-generate"
dependencies = ["grpcio-tools==1.65.4"]
[tool.hatch.envs.generate.scripts]
protoc = "python -m grpc_tools.protoc --proto_path=. --python_out=. --pyi_out=. --grpc_python_out=. crossplane/function/proto/v1beta1/run_function.proto"
protoc = "python -m grpc_tools.protoc --proto_path=. --python_out=. --pyi_out=. --grpc_python_out=. crossplane/function/proto/v1beta1/run_function.proto crossplane/function/proto/v1/run_function.proto"
[tool.hatch.envs.lint]
type = "virtual"
@ -50,7 +50,8 @@ path = ".venv-lint"
dependencies = ["ruff==0.6.1"]
[tool.hatch.envs.lint.scripts]
check = "ruff check crossplane tests && ruff format --diff crossplane tests"
check = "ruff format crossplane tests && ruff check --fix crossplane tests"
[tool.hatch.envs.test]
type = "virtual"

View File

@ -20,7 +20,7 @@ from google.protobuf import duration_pb2 as durationpb
from google.protobuf import json_format
from crossplane.function import logging, resource, response
from crossplane.function.proto.v1beta1 import run_function_pb2 as fnv1beta1
from crossplane.function.proto.v1 import run_function_pb2 as fnv1
class TestResponse(unittest.TestCase):
@ -31,30 +31,30 @@ class TestResponse(unittest.TestCase):
@dataclasses.dataclass
class TestCase:
reason: str
req: fnv1beta1.RunFunctionRequest
req: fnv1.RunFunctionRequest
ttl: datetime.timedelta
want: fnv1beta1.RunFunctionResponse
want: fnv1.RunFunctionResponse
cases = [
TestCase(
reason="Tag, desired, and context should be copied.",
req=fnv1beta1.RunFunctionRequest(
meta=fnv1beta1.RequestMeta(tag="hi"),
desired=fnv1beta1.State(
req=fnv1.RunFunctionRequest(
meta=fnv1.RequestMeta(tag="hi"),
desired=fnv1.State(
resources={
"ready-composed-resource": fnv1beta1.Resource(),
"ready-composed-resource": fnv1.Resource(),
}
),
context=resource.dict_to_struct({"cool-key": "cool-value"}),
),
ttl=datetime.timedelta(minutes=10),
want=fnv1beta1.RunFunctionResponse(
meta=fnv1beta1.ResponseMeta(
want=fnv1.RunFunctionResponse(
meta=fnv1.ResponseMeta(
tag="hi", ttl=durationpb.Duration(seconds=60 * 10)
),
desired=fnv1beta1.State(
desired=fnv1.State(
resources={
"ready-composed-resource": fnv1beta1.Resource(),
"ready-composed-resource": fnv1.Resource(),
}
),
context=resource.dict_to_struct({"cool-key": "cool-value"}),