Add a Composer that uses both P&T and Composition Functions

The idea here is that we completely swap out the Composition
implementation when Composition Functions are enabled. I had considered
implementing Functions as a second stage after P&T, but decided it
wouldn't work well. The main issue is that we'd have to persist
everything from P&T Composition to the APIs server then fun functions,
which could cause flapping/fighting.

Instead the new flow is roughly:

1. Get existing state (composed resources).
2. Build FunctionIO observed object.
3. Do P&T Composition.
4. Build initial FunctionIO desired object (from P&T output).
5. Do Function Composition.
6. Persist all the desired state.
7. Derive XR readiness, connection details, etc.

Signed-off-by: Nic Cope <nicc@rk0n.org>
This commit is contained in:
Nic Cope 2023-01-03 20:27:26 -08:00
parent 6cc1ee1774
commit 95bcbcd2e2
33 changed files with 5301 additions and 1081 deletions

View File

@ -97,8 +97,8 @@ const (
NetworkPolicy_NETWORK_POLICY_UNSPECIFIED NetworkPolicy = 0
// Run the container without network access. The default.
NetworkPolicy_NETWORK_POLICY_ISOLATED NetworkPolicy = 1
// Allow the container to access the network.
NetworkPolicy_NETWORK_POLICY_ACCESSIBLE NetworkPolicy = 2
// Allow the container to access the same network as the function runner.
NetworkPolicy_NETWORK_POLICY_RUNNER NetworkPolicy = 2
)
// Enum value maps for NetworkPolicy.
@ -106,12 +106,12 @@ var (
NetworkPolicy_name = map[int32]string{
0: "NETWORK_POLICY_UNSPECIFIED",
1: "NETWORK_POLICY_ISOLATED",
2: "NETWORK_POLICY_ACCESSIBLE",
2: "NETWORK_POLICY_RUNNER",
}
NetworkPolicy_value = map[string]int32{
"NETWORK_POLICY_UNSPECIFIED": 0,
"NETWORK_POLICY_ISOLATED": 1,
"NETWORK_POLICY_ACCESSIBLE": 2,
"NETWORK_POLICY_RUNNER": 2,
}
)
@ -706,25 +706,25 @@ var file_apiextensions_fn_proto_v1alpha1_run_function_proto_rawDesc = []byte{
0x1c, 0x0a, 0x18, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x50, 0x55, 0x4c, 0x4c, 0x5f, 0x50, 0x4f,
0x4c, 0x49, 0x43, 0x59, 0x5f, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x10, 0x02, 0x12, 0x1b, 0x0a,
0x17, 0x49, 0x4d, 0x41, 0x47, 0x45, 0x5f, 0x50, 0x55, 0x4c, 0x4c, 0x5f, 0x50, 0x4f, 0x4c, 0x49,
0x43, 0x59, 0x5f, 0x4e, 0x45, 0x56, 0x45, 0x52, 0x10, 0x03, 0x2a, 0x6b, 0x0a, 0x0d, 0x4e, 0x65,
0x43, 0x59, 0x5f, 0x4e, 0x45, 0x56, 0x45, 0x52, 0x10, 0x03, 0x2a, 0x67, 0x0a, 0x0d, 0x4e, 0x65,
0x74, 0x77, 0x6f, 0x72, 0x6b, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x1e, 0x0a, 0x1a, 0x4e,
0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x55, 0x4e,
0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x4e,
0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x49, 0x53,
0x4f, 0x4c, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x4e, 0x45, 0x54, 0x57,
0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x53,
0x53, 0x49, 0x42, 0x4c, 0x45, 0x10, 0x02, 0x32, 0x60, 0x0a, 0x22, 0x43, 0x6f, 0x6e, 0x74, 0x61,
0x69, 0x6e, 0x65, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x52, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3a, 0x0a,
0x0b, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x13, 0x2e, 0x52,
0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x14, 0x2e, 0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x47, 0x5a, 0x45, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x70, 0x6c, 0x61,
0x6e, 0x65, 0x2f, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70,
0x69, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73,
0x2f, 0x66, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x4f, 0x4c, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x19, 0x0a, 0x15, 0x4e, 0x45, 0x54, 0x57,
0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x52, 0x55, 0x4e, 0x4e, 0x45,
0x52, 0x10, 0x02, 0x32, 0x60, 0x0a, 0x22, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x69, 0x7a, 0x65, 0x64, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x6e,
0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3a, 0x0a, 0x0b, 0x52, 0x75, 0x6e,
0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x13, 0x2e, 0x52, 0x75, 0x6e, 0x46, 0x75,
0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e,
0x52, 0x75, 0x6e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x47, 0x5a, 0x45, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x72, 0x6f, 0x73, 0x73, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x63,
0x72, 0x6f, 0x73, 0x73, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x73, 0x2f, 0x61,
0x70, 0x69, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x66, 0x6e, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

@ -60,8 +60,8 @@ enum NetworkPolicy {
// Run the container without network access. The default.
NETWORK_POLICY_ISOLATED = 1;
// Allow the container to access the network.
NETWORK_POLICY_ACCESSIBLE = 2;
// Allow the container to access the same network as the function runner.
NETWORK_POLICY_RUNNER = 2;
}
// NetworkConfig configures whether and how a Composition Function container may

View File

@ -47,22 +47,10 @@ type FunctionIO struct {
// particular function may have been mutated by previous functions in the
// pipeline. Functions may mutate any part of the desired state they are
// concerned with, and must pass through any part of the desired state that
// they are not concerned with. Functions may omit desired state that they
// are unconcerned with as long as they don't need to pass it through. For
// example if desired.composite is unset when the function is called it does
// not need to set it. If desired.composite is set the function may mutate
// it and must return it.
// they are not concerned with.
// +optional
Desired Desired `json:"desired,omitempty"`
// Items is a list of Crossplane resources - either XRs or MRs.
//
// A function will read this field in the input FunctionIO and populate
// this field in the output FunctionIO.
// +kubebuilder:validation:EmbeddedResource
// +kubebuilder:pruning:PreserveUnknownFields
Items []runtime.RawExtension `json:"items"`
// Results is an optional list that can be used by function to emit results
// for observability and debugging purposes.
// +optional
@ -124,8 +112,7 @@ type DesiredComposite struct {
// Resource reflects the desired XR. Functions may update the metadata,
// spec, and status of an XR.
// +optional
Resource *runtime.RawExtension `json:"resource,omitempty"`
Resource runtime.RawExtension `json:"resource"`
// ConnectionDetails reflects the desired connection details of the XR.
// +optional
@ -141,12 +128,11 @@ type DesiredResource struct {
// Resource reflects the desired composed resource. Functions may update the
// metadata and spec of a composed resource. Updates to status will be
// discarded. Functions may request that a composed resource be deleted by
// setting this field to null.
// +optional
Resource *runtime.RawExtension `json:"resource"`
// discarded.
Resource runtime.RawExtension `json:"resource"`
// ConnectionDetails reflects the desired connection details of the XR.
// ConnectionDetails reflects _XR_ connection details that should be derived
// from this composed resource.
// +optional
ConnectionDetails []DerivedConnectionDetail `json:"connectionDetails,omitempty"`

View File

@ -86,11 +86,7 @@ func (in *Desired) DeepCopy() *Desired {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DesiredComposite) DeepCopyInto(out *DesiredComposite) {
*out = *in
if in.Resource != nil {
in, out := &in.Resource, &out.Resource
*out = new(runtime.RawExtension)
(*in).DeepCopyInto(*out)
}
in.Resource.DeepCopyInto(&out.Resource)
if in.ConnectionDetails != nil {
in, out := &in.ConnectionDetails, &out.ConnectionDetails
*out = make([]ExplicitConnectionDetail, len(*in))
@ -141,11 +137,7 @@ func (in *DesiredReadinessCheck) DeepCopy() *DesiredReadinessCheck {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DesiredResource) DeepCopyInto(out *DesiredResource) {
*out = *in
if in.Resource != nil {
in, out := &in.Resource, &out.Resource
*out = new(runtime.RawExtension)
(*in).DeepCopyInto(*out)
}
in.Resource.DeepCopyInto(&out.Resource)
if in.ConnectionDetails != nil {
in, out := &in.ConnectionDetails, &out.ConnectionDetails
*out = make([]DerivedConnectionDetail, len(*in))
@ -198,13 +190,6 @@ func (in *FunctionIO) DeepCopyInto(out *FunctionIO) {
}
in.Observed.DeepCopyInto(&out.Observed)
in.Desired.DeepCopyInto(&out.Desired)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]runtime.RawExtension, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Results != nil {
in, out := &in.Results, &out.Results
*out = make([]Result, len(*in))

View File

@ -156,6 +156,10 @@ const (
// ReadinessCheck is used to indicate how to tell whether a resource is ready
// for consumption
type ReadinessCheck struct {
// TODO(negz): Optional fields should be nil in the next version of this
// API. How would we know if we actually wanted to match the empty string,
// or 0?
// Type indicates the type of probe you'd like to use.
// +kubebuilder:validation:Enum="MatchString";"MatchInteger";"NonEmpty";"None"
Type ReadinessCheckType `json:"type"`
@ -178,7 +182,6 @@ type ConnectionDetailType string
// ConnectionDetailType types.
const (
ConnectionDetailTypeUnknown ConnectionDetailType = "Unknown"
ConnectionDetailTypeFromConnectionSecretKey ConnectionDetailType = "FromConnectionSecretKey"
ConnectionDetailTypeFromFieldPath ConnectionDetailType = "FromFieldPath"
ConnectionDetailTypeFromValue ConnectionDetailType = "FromValue"
@ -196,27 +199,29 @@ type ConnectionDetail struct {
// Type sets the connection detail fetching behaviour to be used. Each
// connection detail type may require its own fields to be set on the
// ConnectionDetail object. If the type is omitted Crossplane will attempt
// to infer it based on which other fields were specified.
// to infer it based on which other fields were specified. If multiple
// fields are specified the order of precedence is:
// 1. FromValue
// 2. FromConnectionSecretKey
// 3. FromFieldPath
// +optional
// +kubebuilder:validation:Enum=FromConnectionSecretKey;FromFieldPath;FromValue
Type *ConnectionDetailType `json:"type,omitempty"`
// FromConnectionSecretKey is the key that will be used to fetch the value
// from the given target resource's secret.
// from the composed resource's connection secret.
// +optional
FromConnectionSecretKey *string `json:"fromConnectionSecretKey,omitempty"`
// FromFieldPath is the path of the field on the composed resource whose
// value to be used as input. Name must be specified if the type is
// FromFieldPath is specified.
// FromFieldPath.
// +optional
FromFieldPath *string `json:"fromFieldPath,omitempty"`
// Value that will be propagated to the connection secret of the composition
// instance. Typically you should use FromConnectionSecretKey instead, but
// an explicit value may be set to inject a fixed, non-sensitive connection
// secret values, for example a well-known port. Supercedes
// FromConnectionSecretKey when set.
// Value that will be propagated to the connection secret of the composite
// resource. May be set to inject a fixed, non-sensitive connection secret
// value, for example a well-known port.
// +optional
Value *string `json:"value,omitempty"`
}
@ -280,10 +285,10 @@ type ContainerFunction struct {
// +optional
ImagePullSecrets []corev1.LocalObjectReference `json:"packagePullSecrets,omitempty"`
// Deadline after which the Composition Function will be killed.
// Timeout after which the Composition Function will be killed.
// +optional
// +kubebuilder:default="20s"
Deadline *metav1.Duration `json:"timeout,omitempty"`
Timeout *metav1.Duration `json:"timeout,omitempty"`
// Network configuration for the Composition Function.
// +optional
@ -309,12 +314,12 @@ const (
// ContainerFunctionNetworkPolicyIsolated specifies that the Composition
// Function will not have network access; i.e. invoked inside an isolated
// network namespace.
ContainerFunctionNetworkPolicyIsolated = "Isolated"
ContainerFunctionNetworkPolicyIsolated ContainerFunctionNetworkPolicy = "Isolated"
// ContainerFunctionNetworkPolicyRunner specifies that the Composition
// Function will have the same network access as its runner, i.e. share its
// runner's network namespace.
ContainerFunctionNetworkPolicyRunner = "Runner"
ContainerFunctionNetworkPolicyRunner ContainerFunctionNetworkPolicy = "Runner"
)
// ContainerFunctionNetwork represents configuration for a Composition Function.

View File

@ -222,11 +222,11 @@ func (c *GeneratedRevisionSpecConverter) v1ContainerFunctionToV1beta1ContainerFu
}
v1beta1ContainerFunction.ImagePullSecrets = v1LocalObjectReferenceList
var pV1Duration *v11.Duration
if source.Deadline != nil {
v1Duration := c.v1DurationToV1Duration(*source.Deadline)
if source.Timeout != nil {
v1Duration := c.v1DurationToV1Duration(*source.Timeout)
pV1Duration = &v1Duration
}
v1beta1ContainerFunction.Deadline = pV1Duration
v1beta1ContainerFunction.Timeout = pV1Duration
var pV1beta1ContainerFunctionNetwork *v1beta1.ContainerFunctionNetwork
if source.Network != nil {
v1beta1ContainerFunctionNetwork := c.v1ContainerFunctionNetworkToV1beta1ContainerFunctionNetwork(*source.Network)
@ -746,11 +746,11 @@ func (c *GeneratedRevisionSpecConverter) v1beta1ContainerFunctionToV1ContainerFu
}
v1ContainerFunction.ImagePullSecrets = v1LocalObjectReferenceList
var pV1Duration *v11.Duration
if source.Deadline != nil {
v1Duration := c.v1DurationToV1Duration(*source.Deadline)
if source.Timeout != nil {
v1Duration := c.v1DurationToV1Duration(*source.Timeout)
pV1Duration = &v1Duration
}
v1ContainerFunction.Deadline = pV1Duration
v1ContainerFunction.Timeout = pV1Duration
var pV1ContainerFunctionNetwork *ContainerFunctionNetwork
if source.Network != nil {
v1ContainerFunctionNetwork := c.v1beta1ContainerFunctionNetworkToV1ContainerFunctionNetwork(*source.Network)

View File

@ -474,8 +474,8 @@ func (in *ContainerFunction) DeepCopyInto(out *ContainerFunction) {
*out = make([]corev1.LocalObjectReference, len(*in))
copy(*out, *in)
}
if in.Deadline != nil {
in, out := &in.Deadline, &out.Deadline
if in.Timeout != nil {
in, out := &in.Timeout, &out.Timeout
*out = new(metav1.Duration)
**out = **in
}

View File

@ -289,8 +289,8 @@ func (in *ContainerFunction) DeepCopyInto(out *ContainerFunction) {
*out = make([]corev1.LocalObjectReference, len(*in))
copy(*out, *in)
}
if in.Deadline != nil {
in, out := &in.Deadline, &out.Deadline
if in.Timeout != nil {
in, out := &in.Timeout, &out.Timeout
*out = new(metav1.Duration)
**out = **in
}

View File

@ -806,10 +806,10 @@ type ContainerFunction struct {
// +optional
ImagePullSecrets []corev1.LocalObjectReference `json:"packagePullSecrets,omitempty"`
// Deadline after which the Composition Function will be killed.
// Timeout after which the Composition Function will be killed.
// +optional
// +kubebuilder:default="20s"
Deadline *metav1.Duration `json:"timeout,omitempty"`
Timeout *metav1.Duration `json:"timeout,omitempty"`
// Network configuration for the Composition Function.
// +optional

View File

@ -804,10 +804,10 @@ type ContainerFunction struct {
// +optional
ImagePullSecrets []corev1.LocalObjectReference `json:"packagePullSecrets,omitempty"`
// Deadline after which the Composition Function will be killed.
// Timeout after which the Composition Function will be killed.
// +optional
// +kubebuilder:default="20s"
Deadline *metav1.Duration `json:"timeout,omitempty"`
Timeout *metav1.Duration `json:"timeout,omitempty"`
// Network configuration for the Composition Function.
// +optional

View File

@ -289,8 +289,8 @@ func (in *ContainerFunction) DeepCopyInto(out *ContainerFunction) {
*out = make([]corev1.LocalObjectReference, len(*in))
copy(*out, *in)
}
if in.Deadline != nil {
in, out := &in.Deadline, &out.Deadline
if in.Timeout != nil {
in, out := &in.Timeout, &out.Timeout
*out = new(metav1.Duration)
**out = **in
}

View File

@ -494,7 +494,7 @@ spec:
type: object
timeout:
default: 20s
description: Deadline after which the Composition Function
description: Timeout after which the Composition Function
will be killed.
type: string
required:
@ -1745,7 +1745,7 @@ spec:
type: object
timeout:
default: 20s
description: Deadline after which the Composition Function
description: Timeout after which the Composition Function
will be killed.
type: string
required:

View File

@ -494,7 +494,7 @@ spec:
type: object
timeout:
default: 20s
description: Deadline after which the Composition Function
description: Timeout after which the Composition Function
will be killed.
type: string
required:
@ -855,14 +855,13 @@ spec:
properties:
fromConnectionSecretKey:
description: FromConnectionSecretKey is the key that will
be used to fetch the value from the given target resource's
secret.
be used to fetch the value from the composed resource's
connection secret.
type: string
fromFieldPath:
description: FromFieldPath is the path of the field on
the composed resource whose value to be used as input.
Name must be specified if the type is FromFieldPath
is specified.
Name must be specified if the type is FromFieldPath.
type: string
name:
description: Name of the connection secret key that will
@ -871,11 +870,13 @@ spec:
key name.
type: string
type:
description: Type sets the connection detail fetching
description: 'Type sets the connection detail fetching
behaviour to be used. Each connection detail type may
require its own fields to be set on the ConnectionDetail
object. If the type is omitted Crossplane will attempt
to infer it based on which other fields were specified.
If multiple fields are specified the order of precedence
is: 1. FromValue 2. FromConnectionSecretKey 3. FromFieldPath'
enum:
- FromConnectionSecretKey
- FromFieldPath
@ -883,11 +884,9 @@ spec:
type: string
value:
description: Value that will be propagated to the connection
secret of the composition instance. Typically you should
use FromConnectionSecretKey instead, but an explicit
value may be set to inject a fixed, non-sensitive connection
secret values, for example a well-known port. Supercedes
FromConnectionSecretKey when set.
secret of the composite resource. May be set to inject
a fixed, non-sensitive connection secret value, for
example a well-known port.
type: string
type: object
type: array

View File

@ -78,8 +78,10 @@ type startCommand struct {
MaxReconcileRate int `help:"The global maximum rate per second at which resources may checked for drift from the desired state." default:"10"`
EnableCompositionRevisions bool `group:"Beta Features:" help:"Enable support for CompositionRevisions." default:"true"`
EnableEnvironmentConfigs bool `group:"Alpha Features:" help:"Enable support for EnvironmentConfigs."`
EnableExternalSecretStores bool `group:"Alpha Features:" help:"Enable support for ExternalSecretStores."`
EnableExternalSecretStores bool `group:"Alpha Features:" help:"Enable support for External Secret Stores."`
EnableCompositionFunctions bool `group:"Alpha Features:" help:"Enable support for Composition Functions."`
}
// Run core Crossplane controllers.
@ -123,6 +125,10 @@ func (c *startCommand) Run(s *runtime.Scheme, log logging.Logger) error { //noli
feats.Enable(features.EnableAlphaExternalSecretStores)
log.Info("Alpha feature enabled", "flag", features.EnableAlphaExternalSecretStores)
}
if c.EnableCompositionFunctions {
feats.Enable(features.EnableAlphaCompositionFunctions)
log.Info("Alpha feature enabled", "flag", features.EnableAlphaCompositionFunctions)
}
o := controller.Options{
Logger: log,

View File

@ -41,7 +41,7 @@ type Command struct {
CacheDir string `short:"c" help:"Directory used for caching function images and containers." default:"/xfn"`
Timeout time.Duration `help:"Maximum time for which the function may run before being killed." default:"30s"`
ImagePullPolicy string `help:"Whether the image may be pulled from a remote registry." enum:"Always,Never,IfNotPresent" default:"IfNotPresent"`
NetworkPolicy string `help:"Whether the function may access the network." enum:"Accessible,Isolated" default:"Isolated"`
NetworkPolicy string `help:"Whether the function may access the network." enum:"Runner,Isolated" default:"Isolated"`
MapRootUID int `help:"UID that will map to 0 in the function's user namespace. The following 65336 UIDs must be available. Ignored if xfn does not have CAP_SETUID and CAP_SETGID." default:"100000"`
MapRootGID int `help:"GID that will map to 0 in the function's user namespace. The following 65336 GIDs must be available. Ignored if xfn does not have CAP_SETUID and CAP_SETGID." default:"100000"`
@ -98,8 +98,8 @@ func pullPolicy(p string) v1alpha1.ImagePullPolicy {
func networkPolicy(p string) v1alpha1.NetworkPolicy {
switch p {
case "Accessible":
return v1alpha1.NetworkPolicy_NETWORK_POLICY_ACCESSIBLE
case "Runner":
return v1alpha1.NetworkPolicy_NETWORK_POLICY_RUNNER
case "Isolated":
fallthrough
default:

View File

@ -206,7 +206,7 @@ func FromRunFunctionConfig(cfg *v1alpha1.RunFunctionConfig) spec.Option {
}
}
if cfg.GetNetwork().GetPolicy() == v1alpha1.NetworkPolicy_NETWORK_POLICY_ACCESSIBLE {
if cfg.GetNetwork().GetPolicy() == v1alpha1.NetworkPolicy_NETWORK_POLICY_RUNNER {
if err := spec.WithHostNetwork()(s); err != nil {
return errors.Wrap(err, errHostNetwork)
}

View File

@ -40,8 +40,6 @@ import (
"github.com/crossplane/crossplane/internal/xcrd"
)
var errBoom = errors.New("boom")
func TestPublishConnection(t *testing.T) {
errBoom := errors.New("boom")
@ -154,6 +152,8 @@ func TestPublishConnection(t *testing.T) {
}
func TestFetchComposition(t *testing.T) {
errBoom := errors.New("boom")
type args struct {
ctx context.Context
cr resource.Composite
@ -216,6 +216,7 @@ func TestFetchComposition(t *testing.T) {
}
func TestFetchRevision(t *testing.T) {
errBoom := errors.New("boom")
manual := xpv1.UpdateManual
uid := types.UID("no-you-id")
ctrl := true
@ -572,6 +573,8 @@ func TestFetchRevision(t *testing.T) {
}
func TestConfigure(t *testing.T) {
errBoom := errors.New("boom")
cs := fake.ConnectionSecretWriterTo{Ref: &xpv1.SecretReference{
Name: "foo",
Namespace: "bar",
@ -677,6 +680,8 @@ func TestConfigure(t *testing.T) {
}
func TestSelectorResolver(t *testing.T) {
errBoom := errors.New("boom")
a, k := schema.EmptyObjectKind.GroupVersionKind().ToAPIVersionAndKind()
tref := v1.TypeReference{APIVersion: a, Kind: k}
comp := &v1.Composition{
@ -793,6 +798,7 @@ func TestSelectorResolver(t *testing.T) {
}
func TestAPIDefaultCompositionSelector(t *testing.T) {
errBoom := errors.New("boom")
a, k := schema.EmptyObjectKind.GroupVersionKind().ToAPIVersionAndKind()
tref := v1.TypeReference{APIVersion: a, Kind: k}
comp := &v1.Composition{

View File

@ -0,0 +1,104 @@
/*
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.
*/
package composite
import (
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
"github.com/crossplane/crossplane-runtime/pkg/resource"
iov1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/v1alpha1"
v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
)
// A ComposedResource is an output of the composition process.
type ComposedResource struct {
// ResourceName identifies the composed resource within a Composition or
// FunctionIO. This is not the metadata.name of the actual composed resource
// instance; rather it is the name of an entry in a Composition's resources
// array, and/or a FunctionIO's observed/desired resources array.
ResourceName string
// RenderError encountered while rendering the composed resource. Render
// errors are often temporary, e.g. because rendering depends on information
// that is not yet available.
RenderError error
// Ready indicates whether this composed resource is ready - i.e. whether
// all of its readiness checks passed.
Ready bool
}
// ComposedResourceState tracks the state of a composed resource through the
// Composition process.
type ComposedResourceState struct {
// State that is returned to the caller.
ComposedResource
// Things used to produce a composed resource.
Template *v1.ComposedTemplate
Desired *iov1alpha1.DesiredResource
// The state of the composed resource.
Resource resource.Composed
ConnectionDetails managed.ConnectionDetails
}
// ComposedResourceStates is a map of (Composition) resource name to state. The
// key corresponds to the ResourceName field of the ComposedResource type.
type ComposedResourceStates map[string]ComposedResourceState
// Merge the supplied composed resource state into the map of states. See
// MergeComposedResourceStates for details.
func (rs ComposedResourceStates) Merge(s ComposedResourceState) {
rs[s.ResourceName] = MergeComposedResourceStates(rs[s.ResourceName], s)
}
// MergeComposedResourceStates merges the new ComposedResourceState into the old
// one. It is used to update the result of a P&T composition with the result of
// a subsequent function composition operation on the same composed resource.
func MergeComposedResourceStates(old, new ComposedResourceState) ComposedResourceState {
out := old
if new.ResourceName != "" {
out.ResourceName = new.ResourceName
}
if new.Resource != nil {
out.Resource = new.Resource
}
if new.Template != nil {
out.Template = new.Template
}
if new.Desired != nil {
out.Desired = new.Desired
}
if new.RenderError != nil {
out.RenderError = new.RenderError
}
// TODO(negz): Should Ready be *bool so we can differentiate between false
// and unset? In practice we currently only ever transition _to_ ready.
if new.Ready {
out.Ready = new.Ready
}
if out.ConnectionDetails == nil {
out.ConnectionDetails = make(managed.ConnectionDetails)
}
for k, v := range new.ConnectionDetails {
out.ConnectionDetails[k] = v
}
return out
}

View File

@ -16,7 +16,29 @@ limitations under the License.
package composite
import v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/crossplane/crossplane-runtime/pkg/meta"
v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
)
// Annotation keys.
const (
AnnotationKeyCompositionResourceName = "crossplane.io/composition-resource-name"
)
// SetCompositionResourceName sets the name of the composition template used to
// reconcile a composed resource as an annotation.
func SetCompositionResourceName(o metav1.Object, name string) {
meta.AddAnnotations(o, map[string]string{AnnotationKeyCompositionResourceName: name})
}
// GetCompositionResourceName gets the name of the composition template used to
// reconcile a composed resource from its annotations.
func GetCompositionResourceName(o metav1.Object) string {
return o.GetAnnotations()[AnnotationKeyCompositionResourceName]
}
// Returns types of patches that are from a composed resource _to_ a composite resource.
func patchTypesToXR() []v1.PatchType {

View File

@ -0,0 +1,98 @@
/*
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.
*/
package composite
import (
"context"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
)
// Error strings.
const (
errTriggerFn = "cannot determine which Composer to use"
)
// A TriggerFn lets a FallBackComposer know when it should fall back from the
// preferred to the fallback Composer.
type TriggerFn func(ctx context.Context, xr resource.Composite, req CompositionRequest) (bool, error)
// A FallBackComposer wraps two Composers - one preferred and one fallback. If
// the trigger FallBackFn returns true it will use the fallback rather than the
// preferred Composer.
type FallBackComposer struct {
preferred Composer
fallback Composer
trigger TriggerFn
}
// NewFallBackComposer returns a Composer that calls the preferred Composer
// unless the supplied TriggerFn triggers a fallback to the fallback Composer.
func NewFallBackComposer(preferred, fallback Composer, fn TriggerFn) *FallBackComposer {
return &FallBackComposer{
preferred: preferred,
fallback: fallback,
trigger: fn,
}
}
// Compose calls either the preferred or fallback Composer's Compose method
// depending on the result of the TriggerFn.
func (c *FallBackComposer) Compose(ctx context.Context, xr resource.Composite, req CompositionRequest) (CompositionResult, error) {
fallback, err := c.trigger(ctx, xr, req)
if err != nil {
return CompositionResult{}, errors.Wrap(err, errTriggerFn)
}
if fallback {
return c.fallback.Compose(ctx, xr, req)
}
return c.preferred.Compose(ctx, xr, req)
}
// FallBackForAnonymousTemplates returns a TriggerFn that triggers a fallback if
// the supplied Composition uses anonymous templates, or the supplied XR
// references composed resources that appear to have been created by an
// anonymous template.
func FallBackForAnonymousTemplates(c client.Reader) TriggerFn {
return func(ctx context.Context, xr resource.Composite, req CompositionRequest) (bool, error) {
// Fall back if any templates are unnamed.
for _, t := range req.Composition.Spec.Resources {
if t.Name == nil {
return true, nil
}
}
for _, ref := range xr.GetResourceReferences() {
r := composed.New(composed.FromReference(ref))
if err := c.Get(ctx, types.NamespacedName{Name: r.GetName()}, r); err != nil {
return false, errors.Wrap(resource.IgnoreNotFound(err), errGetComposed)
}
if GetCompositionResourceName(r) == "" {
return true, nil
}
}
return false, nil
}
}

View File

@ -0,0 +1,274 @@
/*
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.
*/
package composite
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
)
type MockComposer struct {
res CompositionResult
err error
}
func (c *MockComposer) Compose(ctx context.Context, xr resource.Composite, req CompositionRequest) (CompositionResult, error) {
return c.res, c.err
}
func TestFallbackComposer(t *testing.T) {
errBoom := errors.New("boom")
conns := managed.ConnectionDetails{"a": []byte("b")}
type params struct {
preferred Composer
fallback Composer
fn TriggerFn
}
type args struct {
ctx context.Context
xr resource.Composite
req CompositionRequest
}
type want struct {
res CompositionResult
err error
}
cases := map[string]struct {
reason string
params params
args args
want want
}{
"SimplePreferred": {
reason: "We should call the preferred Composer if the TriggerFn returns false.",
params: params{
preferred: &MockComposer{res: CompositionResult{
ConnectionDetails: conns,
}},
fallback: &MockComposer{res: CompositionResult{}},
fn: func(ctx context.Context, xr resource.Composite, req CompositionRequest) (bool, error) {
// Don't fall back.
return false, nil
},
},
want: want{
res: CompositionResult{
ConnectionDetails: conns,
},
},
},
"SimpleFallback": {
reason: "We should call the fallback Composer if the TriggerFn returns true.",
params: params{
preferred: &MockComposer{res: CompositionResult{}},
fallback: &MockComposer{res: CompositionResult{
ConnectionDetails: conns,
}},
fn: func(ctx context.Context, xr resource.Composite, req CompositionRequest) (bool, error) {
// Fall back.
return true, nil
},
},
want: want{
res: CompositionResult{
ConnectionDetails: conns,
},
},
},
"TriggerFnError": {
reason: "We should return any error returned by the trigger function.",
params: params{
preferred: &MockComposer{res: CompositionResult{}},
fallback: &MockComposer{res: CompositionResult{}},
fn: func(ctx context.Context, xr resource.Composite, req CompositionRequest) (bool, error) {
return true, errBoom
},
},
want: want{
err: errors.Wrap(errBoom, errTriggerFn),
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
c := NewFallBackComposer(tc.params.preferred, tc.params.fallback, tc.params.fn)
res, err := c.Compose(tc.args.ctx, tc.args.xr, tc.args.req)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nRender(...): -want, +got:\n%s", tc.reason, diff)
}
// We need to EquateErrors here for RenderErrors.
if diff := cmp.Diff(tc.want.res, res, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nComposer(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestFallBackForAnonymousTemplates(t *testing.T) {
errBoom := errors.New("boom")
type params struct {
c client.Reader
}
type args struct {
ctx context.Context
xr resource.Composite
req CompositionRequest
}
type want struct {
fallback bool
err error
}
cases := map[string]struct {
reason string
params params
args args
want want
}{
"HasAnonymousTemplate": {
reason: "We should fallback if the supplied Composition has an anonymous template.",
args: args{
req: CompositionRequest{
Composition: &v1.Composition{
Spec: v1.CompositionSpec{
Resources: []v1.ComposedTemplate{
{}, // A resource without a name.
},
},
},
},
},
want: want{
fallback: true,
},
},
"ReferencesResourceCreatedWithAnonymousTemplate": {
reason: "We should fallback if the supplied XR references a composed resource created using an anonymous template.",
params: params{
c: &test.MockClient{
// We'll return whatever object the supplier passes in
// unmodified, and therefore the object will be missing the
// annotation that indicates the composition resource name
// this resource was created from.
MockGet: test.NewMockGetFn(nil),
},
},
args: args{
xr: &fake.Composite{
ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
Refs: []corev1.ObjectReference{{Name: "cool-resource"}},
},
},
req: CompositionRequest{
Composition: &v1.Composition{},
},
},
want: want{
fallback: true,
},
},
"GetResourceError": {
reason: "We should return an error if we can't get a referenced composed resource.",
params: params{
c: &test.MockClient{
MockGet: test.NewMockGetFn(errBoom),
},
},
args: args{
xr: &fake.Composite{
ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
Refs: []corev1.ObjectReference{{Name: "cool-resource"}},
},
},
req: CompositionRequest{
Composition: &v1.Composition{},
},
},
want: want{
err: errors.Wrap(errBoom, errGetComposed),
},
},
"NoResourceTemplatesOrExistingComposedResources": {
reason: "If there are no resource templates or composed resources (e.g. a new Composition Functions based XR) we should not fallback.",
params: params{
c: &test.MockClient{
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
return kerrors.NewNotFound(schema.GroupResource{}, "")
}),
},
},
args: args{
xr: &fake.Composite{
ComposedResourcesReferencer: fake.ComposedResourcesReferencer{
Refs: []corev1.ObjectReference{},
},
},
req: CompositionRequest{
Composition: &v1.Composition{
Spec: v1.CompositionSpec{
Functions: []v1.Function{{
Name: "cool-fn",
Type: v1.FunctionTypeContainer,
Container: &v1.ContainerFunction{
Image: "cool-img:latest",
},
}},
},
},
},
},
want: want{
fallback: false,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
fn := FallBackForAnonymousTemplates(tc.params.c)
fallback, err := fn(tc.args.ctx, tc.args.xr, tc.args.req)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nFallBackForAnonymousTemplates(...): -want, +got:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.fallback, fallback); diff != "" {
t.Errorf("\n%s\nFallBackForAnonymousTemplates(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}

View File

@ -22,15 +22,12 @@ import (
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
"github.com/crossplane/crossplane-runtime/pkg/resource"
@ -44,18 +41,19 @@ import (
// Error strings
const (
errGetComposed = "cannot get composed resource"
errGCComposed = "cannot garbage collect composed resource"
errApply = "cannot apply composed resource"
errFetchDetails = "cannot fetch connection details"
errReadiness = "cannot check whether composed resource is ready"
errUnmarshal = "cannot unmarshal base template"
errGetSecret = "cannot get connection secret of composed resource"
errNamePrefix = "name prefix is not found in labels"
errKindChanged = "cannot change the kind of an existing composed resource"
errName = "cannot use dry-run create to name composed resource"
errInline = "cannot inline Composition patch sets"
errRenderCR = "cannot render composite resource"
errGetComposed = "cannot get composed resource"
errGCComposed = "cannot garbage collect composed resource"
errApply = "cannot apply composed resource"
errFetchDetails = "cannot fetch connection details"
errExtractDetails = "cannot extract composite resource connection details from composed resource"
errReadiness = "cannot check whether composed resource is ready"
errUnmarshal = "cannot unmarshal base template"
errGetSecret = "cannot get connection secret of composed resource"
errNamePrefix = "name prefix is not found in labels"
errKindChanged = "cannot change the kind of an existing composed resource"
errName = "cannot use dry-run create to name composed resource"
errInline = "cannot inline Composition patch sets"
errRenderCR = "cannot render composite resource"
errFmtPatch = "cannot apply the patch at index %d"
errFmtConnDetailKey = "connection detail of type %q key is not set"
@ -64,60 +62,72 @@ const (
errSetControllerRef = "cannot set controller reference"
)
// Annotation keys.
const (
AnnotationKeyCompositionResourceName = "crossplane.io/composition-resource-name"
)
// TODO(negz): Move P&T Composition logic into its own package?
// A PatchAndTransformComposerOption is used to configure a PatchAndTransformComposer.
type PatchAndTransformComposerOption func(*PatchAndTransformComposer)
// A PTComposerOption is used to configure a PTComposer.
type PTComposerOption func(*PTComposer)
// WithTemplateAssociator configures how a PatchAndTransformComposer associates
// templates with extant composed resources.
func WithTemplateAssociator(a CompositionTemplateAssociator) PatchAndTransformComposerOption {
return func(c *PatchAndTransformComposer) {
func WithTemplateAssociator(a CompositionTemplateAssociator) PTComposerOption {
return func(c *PTComposer) {
c.composition = a
}
}
// WithCompositeRenderer configures how a PatchAndTransformComposer renders the
// composite resource.
func WithCompositeRenderer(r Renderer) PatchAndTransformComposerOption {
return func(c *PatchAndTransformComposer) {
func WithCompositeRenderer(r Renderer) PTComposerOption {
return func(c *PTComposer) {
c.composite = r
}
}
// WithComposedRenderer configures how a PatchAndTransformComposer renders
// composed resources.
func WithComposedRenderer(r Renderer) PatchAndTransformComposerOption {
return func(c *PatchAndTransformComposer) {
func WithComposedRenderer(r Renderer) PTComposerOption {
return func(c *PTComposer) {
c.composed.Renderer = r
}
}
// WithComposedReadinessChecker configures how a PatchAndTransformComposer
// checks composed resource readiness.
func WithComposedReadinessChecker(r ReadinessChecker) PatchAndTransformComposerOption {
return func(c *PatchAndTransformComposer) {
func WithComposedReadinessChecker(r ReadinessChecker) PTComposerOption {
return func(c *PTComposer) {
c.composed.ReadinessChecker = r
}
}
// WithComposedConnectionDetailsFetcher configures how a
// PatchAndTransformComposed fetches composed resource connection details.
func WithComposedConnectionDetailsFetcher(f ConnectionDetailsFetcher) PatchAndTransformComposerOption {
return func(c *PatchAndTransformComposer) {
// PatchAndTransformComposer fetches composed resource connection details.
func WithComposedConnectionDetailsFetcher(f managed.ConnectionDetailsFetcher) PTComposerOption {
return func(c *PTComposer) {
c.composed.ConnectionDetailsFetcher = f
}
}
// A PatchAndTransformComposer composes resources using a Composition's
// 'resources' array, which consist of 'base' resources along with a series of
// patches and transforms.
type PatchAndTransformComposer struct {
// WithComposedConnectionDetailsExtractor configures how a
// PatchAndTransformComposer extracts XR connection details from a composed
// resource.
func WithComposedConnectionDetailsExtractor(e ConnectionDetailsExtractor) PTComposerOption {
return func(c *PTComposer) {
c.composed.ConnectionDetailsExtractor = e
}
}
type composedResource struct {
Renderer
managed.ConnectionDetailsFetcher
ConnectionDetailsExtractor
ReadinessChecker
}
// A PTComposer composes resources using Patch and Transform (P&T) Composition.
// It uses a Composition's 'resources' array, which consist of 'base' resources
// along with a series of patches and transforms. It does not support Functions
// - any entries in the functions array are ignored.
type PTComposer struct {
client resource.ClientApplicator
composite Renderer
@ -125,22 +135,28 @@ type PatchAndTransformComposer struct {
composed composedResource
}
// NewPatchAndTransformComposer returns a Composer that composes resources using
// a Composition's bases, patches, and transforms.
func NewPatchAndTransformComposer(kube client.Client, o ...PatchAndTransformComposerOption) *PatchAndTransformComposer {
// NewPTComposer returns a Composer that composes resources using Patch and
// Transform (P&T) Composition - a Composition's bases, patches, and transforms.
func NewPTComposer(kube client.Client, o ...PTComposerOption) *PTComposer {
// TODO(negz): Can we avoid double-wrapping if the supplied client is
// already wrapped? Or just do away with unstructured.NewClient completely?
kube = unstructured.NewClient(kube)
c := &PatchAndTransformComposer{
c := &PTComposer{
client: resource.ClientApplicator{Client: kube, Applicator: resource.NewAPIPatchingApplicator(kube)},
// TODO(negz): Once Composition Functions are GA this Composer will only
// need to handle legacy Compositions that use anonymous templates. This
// means we will be able to delete the GarbageCollectingAssociator and
// just use AssociateByOrder. Compositions with named templates will be
// handled by the PTFComposer.
composite: RendererFn(RenderComposite),
composition: NewGarbageCollectingAssociator(kube),
composed: composedResource{
Renderer: NewAPIDryRunRenderer(kube),
ReadinessChecker: ReadinessCheckerFn(IsReady),
ConnectionDetailsFetcher: NewAPIConnectionDetailsFetcher(kube),
Renderer: NewAPIDryRunRenderer(kube),
ReadinessChecker: ReadinessCheckerFn(IsReady),
ConnectionDetailsFetcher: NewSecretConnectionDetailsFetcher(kube),
ConnectionDetailsExtractor: ConnectionDetailsExtractorFn(ExtractConnectionDetails),
},
}
@ -153,7 +169,7 @@ func NewPatchAndTransformComposer(kube client.Client, o ...PatchAndTransformComp
// Compose resources using the bases, patches, and transforms specified by the
// supplied Composition.
func (c *PatchAndTransformComposer) Compose(ctx context.Context, xr resource.Composite, req CompositionRequest) (CompositionResult, error) { //nolint:gocyclo // Breaking this up doesn't seem worth yet more layers of abstraction.
func (c *PTComposer) Compose(ctx context.Context, xr resource.Composite, req CompositionRequest) (CompositionResult, error) { //nolint:gocyclo // Breaking this up doesn't seem worth yet more layers of abstraction.
// Inline PatchSets from Composition Spec before composing resources.
ct, err := ComposedTemplates(req.Composition.Spec)
if err != nil {
@ -181,18 +197,20 @@ func (c *PatchAndTransformComposer) Compose(ctx context.Context, xr resource.Com
// input. Errors are recorded, but not considered fatal to the composition
// process.
refs := make([]corev1.ObjectReference, len(tas))
cds := make([]pandtState, len(tas))
for i, ta := range tas {
cd := composed.New(composed.FromReference(ta.Reference))
err := c.composed.Render(ctx, xr, cd, ta.Template, req.Environment)
cds := make([]ComposedResourceState, len(tas))
for i := range tas {
ta := tas[i]
cds[i] = pandtState{
template: ta.Template,
resource: cd,
renderError: err,
appliedPatches: filterPatches(ta.Template.Patches, patchTypesFromXR()...),
r := composed.New(composed.FromReference(ta.Reference))
cds[i] = ComposedResourceState{
ComposedResource: ComposedResource{
ResourceName: pointer.StringDeref(ta.Template.Name, ""),
RenderError: c.composed.Render(ctx, xr, r, ta.Template, req.Environment),
},
Template: &ta.Template,
Resource: r,
}
refs[i] = *meta.ReferenceTo(cd, cd.GetObjectKind().GroupVersionKind())
refs[i] = *meta.ReferenceTo(r, r.GetObjectKind().GroupVersionKind())
}
// We persist references to our composed resources before we create
@ -204,44 +222,49 @@ func (c *PatchAndTransformComposer) Compose(ctx context.Context, xr resource.Com
return CompositionResult{}, errors.Wrap(err, errUpdate)
}
// We apply all of our composed resources before we observe them and
// update the composite resource accordingly in the loop below. This
// ensures that issues observing and processing one composed resource
// won't block the application of another.
// We apply all of our composed resources before we observe them and update
// in the loop below. This ensures that issues observing and processing one
// composed resource won't block the application of another.
for _, cd := range cds {
// If we were unable to render the composed resource we should not try
// and apply it.
if cd.renderError != nil {
if cd.RenderError != nil {
continue
}
if err := c.client.Apply(ctx, cd.resource, append(mergeOptions(cd.appliedPatches), resource.MustBeControllableBy(xr.GetUID()))...); err != nil {
o := []resource.ApplyOption{resource.MustBeControllableBy(xr.GetUID())}
o = append(o, mergeOptions(filterPatches(cd.Template.Patches, patchTypesFromXR()...))...)
if err := c.client.Apply(ctx, cd.Resource, o...); err != nil {
return CompositionResult{}, errors.Wrap(err, errApply)
}
}
conn := managed.ConnectionDetails{}
for i := range cds {
// If we were unable to render the composed resource we should not try
// to observe it.
if cds[i].renderError != nil {
if cds[i].RenderError != nil {
continue
}
if err := c.composite.Render(ctx, xr, cds[i].resource, cds[i].template, req.Environment); err != nil {
if err := c.composite.Render(ctx, xr, cds[i].Resource, *cds[i].Template, req.Environment); err != nil {
return CompositionResult{}, errors.Wrap(err, errRenderCR)
}
cds[i].conn, err = c.composed.FetchConnectionDetails(ctx, cds[i].resource, cds[i].template)
cds[i].ConnectionDetails, err = c.composed.FetchConnection(ctx, cds[i].Resource)
if err != nil {
return CompositionResult{}, errors.Wrap(err, errFetchDetails)
}
for key, val := range cds[i].conn {
e, err := c.composed.ExtractConnection(cds[i].Resource, cds[i].ConnectionDetails, ExtractConfigsFromTemplate(cds[i].Template)...)
if err != nil {
return CompositionResult{}, errors.Wrap(err, errExtractDetails)
}
for key, val := range e {
conn[key] = val
}
cds[i].ready, err = c.composed.IsReady(ctx, cds[i].resource, cds[i].template)
cds[i].Ready, err = c.composed.IsReady(ctx, cds[i].Resource, ReadinessChecksFromTemplate(cds[i].Template)...)
if err != nil {
return CompositionResult{}, errors.Wrap(err, errReadiness)
}
@ -261,49 +284,43 @@ func (c *PatchAndTransformComposer) Compose(ctx context.Context, xr resource.Com
// and we'll proceed to update the status as soon as there are no changes to
// be made to the spec.
copy := xr.DeepCopyObject().(client.Object)
if err := c.client.Apply(ctx, copy, mergeOptions(filterToXRPatches(tas))...); err != nil {
if err := c.client.Apply(ctx, copy, mergeOptions(toXRPatchesFromTAs(tas))...); err != nil {
return CompositionResult{}, errors.Wrap(err, errUpdate)
}
out := make([]ComposedResource, len(cds))
for i := range cds {
out[i] = cds[i].AsComposedResource()
out[i] = cds[i].ComposedResource
}
return CompositionResult{ConnectionDetails: conn, Composed: out}, nil
}
// pandtState tracks the state of Patch and Transform Composition for a
// particular composed resource.
type pandtState struct {
template v1.ComposedTemplate
resource resource.Composed
appliedPatches []v1.Patch
renderError error
conn managed.ConnectionDetails
ready bool
}
func (s pandtState) AsComposedResource() ComposedResource {
return ComposedResource{
Name: pointer.StringDeref(s.template.Name, ""),
Resource: s.resource,
ConnectionDetails: s.conn,
RenderError: s.renderError,
Ready: s.ready,
// toXRPatchesFromTAs selects patches defined in composed templates,
// whose type is one of the XR-targeting patches
// (e.g. v1.PatchTypeToCompositeFieldPath or v1.PatchTypeCombineToComposite)
func toXRPatchesFromTAs(tas []TemplateAssociation) []v1.Patch {
filtered := make([]v1.Patch, 0, len(tas))
for _, ta := range tas {
filtered = append(filtered, filterPatches(ta.Template.Patches,
patchTypesToXR()...)...)
}
return filtered
}
// SetCompositionResourceName sets the name of the composition template used to
// reconcile a composed resource as an annotation.
func SetCompositionResourceName(o metav1.Object, name string) {
meta.AddAnnotations(o, map[string]string{AnnotationKeyCompositionResourceName: name})
}
// GetCompositionResourceName gets the name of the composition template used to
// reconcile a composed resource from its annotations.
func GetCompositionResourceName(o metav1.Object) string {
return o.GetAnnotations()[AnnotationKeyCompositionResourceName]
// filterPatches selects patches whose type belong to the list onlyTypes
func filterPatches(pas []v1.Patch, onlyTypes ...v1.PatchType) []v1.Patch {
filtered := make([]v1.Patch, 0, len(pas))
include := make(map[v1.PatchType]bool)
for _, t := range onlyTypes {
include[t] = true
}
for _, p := range pas {
if include[p.Type] {
filtered = append(filtered, p)
}
}
return filtered
}
// A TemplateAssociation associates a composed resource template with a composed
@ -421,6 +438,9 @@ func (a *GarbageCollectingAssociator) AssociateTemplates(ctx context.Context, cr
continue
}
// TODO(negz): Below should be || not &&. If the controller ref is nil
// we don't control the resource and shouldn't delete it.
// We want to garbage collect this resource, but we don't control it.
if c := metav1.GetControllerOf(cd); c != nil && c.UID != cr.GetUID() {
continue
@ -553,171 +573,3 @@ func RenderComposite(_ context.Context, cp resource.Composite, cd resource.Compo
return nil
}
// An APIConnectionDetailsFetcher may use the API server to read connection
// details from a Secret.
type APIConnectionDetailsFetcher struct {
client client.Client
}
// NewAPIConnectionDetailsFetcher returns a ConnectionDetailsFetcher that may
// use the API server to read connection details from a Secret.
func NewAPIConnectionDetailsFetcher(c client.Client) *APIConnectionDetailsFetcher {
return &APIConnectionDetailsFetcher{client: c}
}
// FetchConnectionDetails of the supplied composed resource, if any.
func (cdf *APIConnectionDetailsFetcher) FetchConnectionDetails(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (managed.ConnectionDetails, error) { //nolint:gocyclo // Relatively simple; complexity is mostly a switch.
data := map[string][]byte{}
if sref := cd.GetWriteConnectionSecretToReference(); sref != nil {
// It's possible that the composed resource does want to write a
// connection secret but has not yet. We presume this isn't an issue and
// that we'll propagate any connection details during a future
// iteration.
s := &corev1.Secret{}
nn := types.NamespacedName{Namespace: sref.Namespace, Name: sref.Name}
if err := cdf.client.Get(ctx, nn, s); client.IgnoreNotFound(err) != nil {
return nil, errors.Wrap(err, errGetSecret)
}
data = s.Data
}
conn := managed.ConnectionDetails{}
for _, d := range t.ConnectionDetails {
switch tp := connectionDetailType(d); tp {
case v1.ConnectionDetailTypeFromValue:
// Name, Value must be set if value type
switch {
case d.Name == nil:
return nil, errors.Errorf(errFmtConnDetailKey, tp)
case d.Value == nil:
return nil, errors.Errorf(errFmtConnDetailVal, tp)
default:
conn[*d.Name] = []byte(*d.Value)
}
case v1.ConnectionDetailTypeFromConnectionSecretKey:
if d.FromConnectionSecretKey == nil {
return nil, errors.Errorf(errFmtConnDetailKey, tp)
}
if data[*d.FromConnectionSecretKey] == nil {
// We don't consider this an error because it's possible the
// key will still be written at some point in the future.
continue
}
key := *d.FromConnectionSecretKey
if d.Name != nil {
key = *d.Name
}
if key != "" {
conn[key] = data[*d.FromConnectionSecretKey]
}
case v1.ConnectionDetailTypeFromFieldPath:
switch {
case d.Name == nil:
return nil, errors.Errorf(errFmtConnDetailKey, tp)
case d.FromFieldPath == nil:
return nil, errors.Errorf(errFmtConnDetailPath, tp)
default:
_ = extractFieldPathValue(cd, d, conn)
}
case v1.ConnectionDetailTypeUnknown:
// We weren't able to determine the type of this connection detail.
}
}
if len(conn) == 0 {
return nil, nil
}
return conn, nil
}
// Originally there was no 'type' determinator field so Crossplane would infer
// the type. We maintain this behaviour for backward compatibility when no type
// is set.
func connectionDetailType(d v1.ConnectionDetail) v1.ConnectionDetailType {
switch {
case d.Type != nil:
return *d.Type
case d.Name != nil && d.Value != nil:
return v1.ConnectionDetailTypeFromValue
case d.FromConnectionSecretKey != nil:
return v1.ConnectionDetailTypeFromConnectionSecretKey
case d.FromFieldPath != nil:
return v1.ConnectionDetailTypeFromFieldPath
default:
return v1.ConnectionDetailTypeUnknown
}
}
func extractFieldPathValue(from runtime.Object, detail v1.ConnectionDetail, conn managed.ConnectionDetails) error {
fromMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(from)
if err != nil {
return err
}
str, err := fieldpath.Pave(fromMap).GetString(*detail.FromFieldPath)
if err == nil {
conn[*detail.Name] = []byte(str)
return nil
}
in, err := fieldpath.Pave(fromMap).GetValue(*detail.FromFieldPath)
if err != nil {
return err
}
buffer, err := json.Marshal(in)
if err != nil {
return err
}
conn[*detail.Name] = buffer
return nil
}
// IsReady returns whether the composed resource is ready.
func IsReady(_ context.Context, cd resource.Composed, t v1.ComposedTemplate) (bool, error) { //nolint:gocyclo // Complexity is mostly due to the switch.
if len(t.ReadinessChecks) == 0 {
return resource.IsConditionTrue(cd.GetCondition(xpv1.TypeReady)), nil
}
// TODO(muvaf): We can probably get rid of resource.Composed interface and fake.Composed
// structs and use *composed.Unstructured everywhere including tests.
u, ok := cd.(*composed.Unstructured)
if !ok {
return false, errors.New("composed resource has to be Unstructured type")
}
paved := fieldpath.Pave(u.UnstructuredContent())
for i, check := range t.ReadinessChecks {
var ready bool
switch check.Type {
case v1.ReadinessCheckTypeNone:
return true, nil
case v1.ReadinessCheckTypeNonEmpty:
_, err := paved.GetValue(check.FieldPath)
if resource.Ignore(fieldpath.IsNotFound, err) != nil {
return false, err
}
ready = !fieldpath.IsNotFound(err)
case v1.ReadinessCheckTypeMatchString:
val, err := paved.GetString(check.FieldPath)
if resource.Ignore(fieldpath.IsNotFound, err) != nil {
return false, err
}
ready = !fieldpath.IsNotFound(err) && val == check.MatchString
case v1.ReadinessCheckTypeMatchInteger:
val, err := paved.GetInteger(check.FieldPath)
if err != nil {
return false, err
}
ready = !fieldpath.IsNotFound(err) && val == check.MatchInteger
default:
return false, errors.Errorf("readiness check at index %d: an unknown type is chosen", i)
}
if !ready {
return false, nil
}
}
return true, nil
}

View File

@ -31,12 +31,10 @@ import (
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
"github.com/crossplane/crossplane-runtime/pkg/test"
v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
@ -44,13 +42,13 @@ import (
"github.com/crossplane/crossplane/internal/xcrd"
)
func TestCompose(t *testing.T) {
func TestPTCompose(t *testing.T) {
errBoom := errors.New("boom")
conn := managed.ConnectionDetails{"a": []byte("b")}
details := managed.ConnectionDetails{"a": []byte("b")}
type params struct {
kube client.Client
o []PatchAndTransformComposerOption
o []PTComposerOption
}
type args struct {
ctx context.Context
@ -93,7 +91,7 @@ func TestCompose(t *testing.T) {
"AssociateTemplatesError": {
reason: "We should return any error encountered while associating Composition templates with composed resources.",
params: params{
o: []PatchAndTransformComposerOption{
o: []PTComposerOption{
WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
return nil, errBoom
})),
@ -119,7 +117,7 @@ func TestCompose(t *testing.T) {
MockGet: test.NewMockGetFn(nil),
MockPatch: test.NewMockPatchFn(nil),
},
o: []PatchAndTransformComposerOption{
o: []PTComposerOption{
WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
tas := []TemplateAssociation{{
Template: v1.ComposedTemplate{
@ -134,7 +132,7 @@ func TestCompose(t *testing.T) {
WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *env.Environment) error {
return nil
})),
WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (managed.ConnectionDetails, error) {
WithComposedConnectionDetailsExtractor(ConnectionDetailsExtractorFn(func(cd resource.Composed, conn managed.ConnectionDetails, cfg ...ConnectionDetailExtractConfig) (managed.ConnectionDetails, error) {
return nil, nil
})),
},
@ -148,9 +146,8 @@ func TestCompose(t *testing.T) {
want: want{
res: CompositionResult{
Composed: []ComposedResource{{
Name: "cool-resource",
RenderError: errBoom,
Resource: composed.New(composed.FromReference(corev1.ObjectReference{})),
ResourceName: "cool-resource",
RenderError: errBoom,
}},
ConnectionDetails: managed.ConnectionDetails{},
},
@ -162,7 +159,7 @@ func TestCompose(t *testing.T) {
kube: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(errBoom),
},
o: []PatchAndTransformComposerOption{
o: []PTComposerOption{
WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
tas := []TemplateAssociation{{
Template: v1.ComposedTemplate{
@ -195,7 +192,7 @@ func TestCompose(t *testing.T) {
// Apply calls Get.
MockGet: test.NewMockGetFn(errBoom),
},
o: []PatchAndTransformComposerOption{
o: []PTComposerOption{
WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
tas := []TemplateAssociation{{
Template: v1.ComposedTemplate{
@ -229,7 +226,7 @@ func TestCompose(t *testing.T) {
MockGet: test.NewMockGetFn(nil),
MockPatch: test.NewMockPatchFn(nil),
},
o: []PatchAndTransformComposerOption{
o: []PTComposerOption{
WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
tas := []TemplateAssociation{{
Template: v1.ComposedTemplate{
@ -266,7 +263,7 @@ func TestCompose(t *testing.T) {
MockGet: test.NewMockGetFn(nil),
MockPatch: test.NewMockPatchFn(nil),
},
o: []PatchAndTransformComposerOption{
o: []PTComposerOption{
WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
tas := []TemplateAssociation{{
Template: v1.ComposedTemplate{
@ -281,7 +278,7 @@ func TestCompose(t *testing.T) {
WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *env.Environment) error {
return nil
})),
WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (managed.ConnectionDetails, error) {
WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
return nil, errBoom
})),
},
@ -296,9 +293,8 @@ func TestCompose(t *testing.T) {
err: errors.Wrap(errBoom, errFetchDetails),
},
},
"CheckReadinessError": {
reason: "We should return any error encountered while checking whether a composed resource is ready.",
"ExtractConnectionDetailsError": {
reason: "We should return any error encountered while extracting a composed resource's connection details.",
params: params{
kube: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
@ -307,7 +303,7 @@ func TestCompose(t *testing.T) {
MockGet: test.NewMockGetFn(nil),
MockPatch: test.NewMockPatchFn(nil),
},
o: []PatchAndTransformComposerOption{
o: []PTComposerOption{
WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
tas := []TemplateAssociation{{
Template: v1.ComposedTemplate{
@ -322,10 +318,56 @@ func TestCompose(t *testing.T) {
WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *env.Environment) error {
return nil
})),
WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (managed.ConnectionDetails, error) {
WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
return nil, nil
})),
WithComposedReadinessChecker(ReadinessCheckerFn(func(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (ready bool, err error) {
WithComposedConnectionDetailsExtractor(ConnectionDetailsExtractorFn(func(cd resource.Composed, conn managed.ConnectionDetails, cfg ...ConnectionDetailExtractConfig) (managed.ConnectionDetails, error) {
return nil, errBoom
})),
},
},
args: args{
xr: &fake.Composite{},
req: CompositionRequest{
Composition: &v1.Composition{},
},
},
want: want{
err: errors.Wrap(errBoom, errExtractDetails),
},
},
"CheckReadinessError": {
reason: "We should return any error encountered while checking whether a composed resource is ready.",
params: params{
kube: &test.MockClient{
MockUpdate: test.NewMockUpdateFn(nil),
// Apply calls Get and Patch
MockGet: test.NewMockGetFn(nil),
MockPatch: test.NewMockPatchFn(nil),
},
o: []PTComposerOption{
WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
tas := []TemplateAssociation{{
Template: v1.ComposedTemplate{
Name: pointer.String("cool-resource"),
},
}}
return tas, nil
})),
WithComposedRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *env.Environment) error {
return nil
})),
WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *env.Environment) error {
return nil
})),
WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
return nil, nil
})),
WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, cd resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
return nil, nil
})),
WithComposedReadinessChecker(ReadinessCheckerFn(func(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error) {
return false, errBoom
})),
},
@ -352,7 +394,7 @@ func TestCompose(t *testing.T) {
MockGet: test.NewMockGetFn(errBoom),
MockPatch: test.NewMockPatchFn(nil),
},
o: []PatchAndTransformComposerOption{
o: []PTComposerOption{
WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
return nil, nil
})),
@ -381,7 +423,7 @@ func TestCompose(t *testing.T) {
MockGet: test.NewMockGetFn(nil),
MockPatch: test.NewMockPatchFn(nil),
},
o: []PatchAndTransformComposerOption{
o: []PTComposerOption{
WithTemplateAssociator(CompositionTemplateAssociatorFn(func(ctx context.Context, c resource.Composite, ct []v1.ComposedTemplate) ([]TemplateAssociation, error) {
tas := []TemplateAssociation{{
Template: v1.ComposedTemplate{
@ -396,10 +438,13 @@ func TestCompose(t *testing.T) {
WithCompositeRenderer(RendererFn(func(ctx context.Context, cp resource.Composite, cd resource.Composed, t v1.ComposedTemplate, env *env.Environment) error {
return nil
})),
WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (managed.ConnectionDetails, error) {
return conn, nil
WithComposedConnectionDetailsFetcher(ConnectionDetailsFetcherFn(func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
return nil, nil
})),
WithComposedReadinessChecker(ReadinessCheckerFn(func(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (ready bool, err error) {
WithComposedConnectionDetailsExtractor(ConnectionDetailsExtractorFn(func(cd resource.Composed, conn managed.ConnectionDetails, cfg ...ConnectionDetailExtractConfig) (managed.ConnectionDetails, error) {
return details, nil
})),
WithComposedReadinessChecker(ReadinessCheckerFn(func(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error) {
return true, nil
})),
},
@ -413,12 +458,10 @@ func TestCompose(t *testing.T) {
want: want{
res: CompositionResult{
Composed: []ComposedResource{{
Name: "cool-resource",
Resource: composed.New(composed.FromReference(corev1.ObjectReference{})),
ConnectionDetails: conn,
Ready: true,
ResourceName: "cool-resource",
Ready: true,
}},
ConnectionDetails: conn,
ConnectionDetails: details,
},
},
},
@ -427,16 +470,16 @@ func TestCompose(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
c := NewPatchAndTransformComposer(tc.params.kube, tc.params.o...)
c := NewPTComposer(tc.params.kube, tc.params.o...)
res, err := c.Compose(tc.args.ctx, tc.args.xr, tc.args.req)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nRender(...): -want, +got:\n%s", tc.reason, diff)
t.Errorf("\n%s\nCompose(...): -want, +got:\n%s", tc.reason, diff)
}
// We need to EquateErrors here for RenderErrors.
if diff := cmp.Diff(tc.want.res, res, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nRender(...): -want, +got:\n%s", tc.reason, diff)
t.Errorf("\n%s\nCompose(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
@ -445,6 +488,7 @@ func TestCompose(t *testing.T) {
func TestRender(t *testing.T) {
ctrl := true
tmpl, _ := json.Marshal(&fake.Managed{})
errBoom := errors.New("boom")
type args struct {
ctx context.Context
@ -817,551 +861,3 @@ func TestGarbageCollectingAssociator(t *testing.T) {
})
}
}
func TestFetch(t *testing.T) {
fromKey := v1.ConnectionDetailTypeFromConnectionSecretKey
fromVal := v1.ConnectionDetailTypeFromValue
fromField := v1.ConnectionDetailTypeFromFieldPath
sref := &xpv1.SecretReference{Name: "foo", Namespace: "bar"}
s := &corev1.Secret{
Data: map[string][]byte{
"foo": []byte("a"),
"bar": []byte("b"),
},
}
type args struct {
kube client.Client
cd resource.Composed
t v1.ComposedTemplate
}
type want struct {
conn managed.ConnectionDetails
err error
}
cases := map[string]struct {
reason string
args
want
}{
"DoesNotPublish": {
reason: "Should not fail if composed resource doesn't publish a connection secret",
args: args{
cd: &fake.Composed{},
},
},
"SecretNotPublishedYet": {
reason: "Should not fail if composed resource has yet to publish the secret",
args: args{
kube: &test.MockClient{MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
cd: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
t: v1.ComposedTemplate{ConnectionDetails: []v1.ConnectionDetail{
{
FromConnectionSecretKey: pointer.StringPtr("bar"),
Type: &fromKey,
},
{
Name: pointer.StringPtr("fixed"),
Type: &fromVal,
Value: pointer.StringPtr("value"),
},
}},
},
want: want{
conn: managed.ConnectionDetails{
"fixed": []byte("value"),
},
},
},
"SecretGetFailed": {
reason: "Should fail if secret retrieval results in some error other than NotFound",
args: args{
kube: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
cd: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
},
want: want{
err: errors.Wrap(errBoom, errGetSecret),
},
},
"Success": {
reason: "Should publish only the selected set of secret keys",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
cd: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
t: v1.ComposedTemplate{ConnectionDetails: []v1.ConnectionDetail{
{
FromConnectionSecretKey: pointer.StringPtr("bar"),
Type: &fromKey,
},
{
FromConnectionSecretKey: pointer.StringPtr("none"),
Type: &fromKey,
},
{
Name: pointer.StringPtr("convfoo"),
FromConnectionSecretKey: pointer.StringPtr("foo"),
Type: &fromKey,
},
{
Name: pointer.StringPtr("fixed"),
Value: pointer.StringPtr("value"),
Type: &fromVal,
},
}},
},
want: want{
conn: managed.ConnectionDetails{
"convfoo": s.Data["foo"],
"bar": s.Data["bar"],
"fixed": []byte("value"),
},
},
},
"ConnectionDetailValueNotSet": {
reason: "Should error if Value type value is not set",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
cd: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
t: v1.ComposedTemplate{ConnectionDetails: []v1.ConnectionDetail{
{
Name: pointer.StringPtr("missingvalue"),
Type: &fromVal,
},
}},
},
want: want{
err: errors.Errorf(errFmtConnDetailVal, v1.ConnectionDetailTypeFromValue),
},
},
"ErrConnectionDetailNameNotSet": {
reason: "Should error if Value type name is not set",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
cd: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
t: v1.ComposedTemplate{ConnectionDetails: []v1.ConnectionDetail{
{
Value: pointer.StringPtr("missingname"),
Type: &fromVal,
},
}},
},
want: want{
err: errors.Errorf(errFmtConnDetailKey, v1.ConnectionDetailTypeFromValue),
},
},
"ErrConnectionDetailFromConnectionSecretKeyNotSet": {
reason: "Should error if ConnectionDetailFromConnectionSecretKey type FromConnectionSecretKey is not set",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
cd: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
t: v1.ComposedTemplate{ConnectionDetails: []v1.ConnectionDetail{
{
Type: &fromKey,
},
}},
},
want: want{
err: errors.Errorf(errFmtConnDetailKey, v1.ConnectionDetailTypeFromConnectionSecretKey),
},
},
"ErrConnectionDetailFromFieldPathNotSet": {
reason: "Should error if ConnectionDetailFromFieldPath type FromFieldPath is not set",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
cd: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
t: v1.ComposedTemplate{ConnectionDetails: []v1.ConnectionDetail{
{
Type: &fromField,
Name: pointer.StringPtr("missingname"),
},
}},
},
want: want{
err: errors.Errorf(errFmtConnDetailPath, v1.ConnectionDetailTypeFromFieldPath),
},
},
"ErrConnectionDetailFromFieldPathNameNotSet": {
reason: "Should error if ConnectionDetailFromFieldPath type Name is not set",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
cd: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
t: v1.ComposedTemplate{ConnectionDetails: []v1.ConnectionDetail{
{
Type: &fromField,
FromFieldPath: pointer.StringPtr("fieldpath"),
},
}},
},
want: want{
err: errors.Errorf(errFmtConnDetailKey, v1.ConnectionDetailTypeFromFieldPath),
},
},
"SuccessFieldPath": {
reason: "Should publish only the selected set of secret keys",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
cd: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
t: v1.ComposedTemplate{ConnectionDetails: []v1.ConnectionDetail{
{
Name: pointer.StringPtr("name"),
FromFieldPath: pointer.StringPtr("objectMeta.name"),
Type: &fromField,
},
}},
},
want: want{
conn: managed.ConnectionDetails{
"name": []byte("test"),
},
},
},
"SuccessFieldPathMarshal": {
reason: "Should publish the secret keys as a JSON value",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
cd: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
ObjectMeta: metav1.ObjectMeta{
Generation: 4,
},
},
t: v1.ComposedTemplate{ConnectionDetails: []v1.ConnectionDetail{
{
Name: pointer.StringPtr("generation"),
FromFieldPath: pointer.StringPtr("objectMeta.generation"),
Type: &fromField,
},
}},
},
want: want{
conn: managed.ConnectionDetails{
"generation": []byte("4"),
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
c := &APIConnectionDetailsFetcher{client: tc.args.kube}
conn, err := c.FetchConnectionDetails(context.Background(), tc.args.cd, tc.args.t)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nFetch(...): -want, +got:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.conn, conn); diff != "" {
t.Errorf("\n%s\nFetch(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestConnectionDetailType(t *testing.T) {
fromVal := v1.ConnectionDetailTypeFromValue
name := "coolsecret"
value := "coolvalue"
key := "coolkey"
field := "coolfield"
cases := map[string]struct {
d v1.ConnectionDetail
want v1.ConnectionDetailType
}{
"FromValueExplicit": {
d: v1.ConnectionDetail{Type: &fromVal},
want: v1.ConnectionDetailTypeFromValue,
},
"FromValueInferred": {
d: v1.ConnectionDetail{
Name: &name,
Value: &value,
// Name and value trump key or field
FromConnectionSecretKey: &key,
FromFieldPath: &field,
},
want: v1.ConnectionDetailTypeFromValue,
},
"FromConnectionSecretKeyInferred": {
d: v1.ConnectionDetail{
Name: &name,
FromConnectionSecretKey: &key,
// From key trumps from field
FromFieldPath: &field,
},
want: v1.ConnectionDetailTypeFromConnectionSecretKey,
},
"FromFieldPathInferred": {
d: v1.ConnectionDetail{
Name: &name,
FromFieldPath: &field,
},
want: v1.ConnectionDetailTypeFromFieldPath,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := connectionDetailType(tc.d)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("connectionDetailType(...): -want, +got\n%s", diff)
}
})
}
}
func TestIsReady(t *testing.T) {
type args struct {
cd *composed.Unstructured
t v1.ComposedTemplate
}
type want struct {
ready bool
err error
}
cases := map[string]struct {
reason string
args
want
}{
"NoCustomCheck": {
reason: "If no custom check is given, Ready condition should be used",
args: args{
cd: composed.New(composed.WithConditions(xpv1.Available())),
},
want: want{
ready: true,
},
},
"ExplictNone": {
reason: "If the only readiness check is explicitly 'None' the resource is always ready.",
args: args{
cd: composed.New(),
t: v1.ComposedTemplate{ReadinessChecks: []v1.ReadinessCheck{{Type: v1.ReadinessCheckTypeNone}}},
},
want: want{
ready: true,
},
},
"NonEmptyErr": {
reason: "If the value cannot be fetched due to fieldPath being misconfigured, error should be returned",
args: args{
cd: composed.New(),
t: v1.ComposedTemplate{ReadinessChecks: []v1.ReadinessCheck{{Type: "NonEmpty", FieldPath: "metadata..uid"}}},
},
want: want{
err: errors.Wrapf(errors.New("unexpected '.' at position 9"), "cannot parse path %q", "metadata..uid"),
},
},
"NonEmptyFalse": {
reason: "If the field does not have value, NonEmpty check should return false",
args: args{
cd: composed.New(),
t: v1.ComposedTemplate{ReadinessChecks: []v1.ReadinessCheck{{Type: "NonEmpty", FieldPath: "metadata.uid"}}},
},
want: want{
ready: false,
},
},
"NonEmptyTrue": {
reason: "If the field does have a value, NonEmpty check should return true",
args: args{
cd: composed.New(func(r *composed.Unstructured) {
r.SetUID("olala")
}),
t: v1.ComposedTemplate{ReadinessChecks: []v1.ReadinessCheck{{Type: "NonEmpty", FieldPath: "metadata.uid"}}},
},
want: want{
ready: true,
},
},
"MatchStringErr": {
reason: "If the value cannot be fetched due to fieldPath being misconfigured, error should be returned",
args: args{
cd: composed.New(),
t: v1.ComposedTemplate{ReadinessChecks: []v1.ReadinessCheck{{Type: "MatchString", FieldPath: "metadata..uid"}}},
},
want: want{
err: errors.Wrapf(errors.New("unexpected '.' at position 9"), "cannot parse path %q", "metadata..uid"),
},
},
"MatchStringFalse": {
reason: "If the value of the field does not match, it should return false",
args: args{
cd: composed.New(),
t: v1.ComposedTemplate{ReadinessChecks: []v1.ReadinessCheck{{Type: "MatchString", FieldPath: "metadata.uid", MatchString: "olala"}}},
},
want: want{
ready: false,
},
},
"MatchStringTrue": {
reason: "If the value of the field does match, it should return true",
args: args{
cd: composed.New(func(r *composed.Unstructured) {
r.SetUID("olala")
}),
t: v1.ComposedTemplate{ReadinessChecks: []v1.ReadinessCheck{{Type: "MatchString", FieldPath: "metadata.uid", MatchString: "olala"}}},
},
want: want{
ready: true,
},
},
"MatchIntegerErr": {
reason: "If the value cannot be fetched due to fieldPath being misconfigured, error should be returned",
args: args{
cd: composed.New(),
t: v1.ComposedTemplate{ReadinessChecks: []v1.ReadinessCheck{{Type: "MatchInteger", FieldPath: "metadata..uid"}}},
},
want: want{
err: errors.Wrapf(errors.New("unexpected '.' at position 9"), "cannot parse path %q", "metadata..uid"),
},
},
"MatchIntegerFalse": {
reason: "If the value of the field does not match, it should return false",
args: args{
cd: composed.New(func(r *composed.Unstructured) {
r.Object = map[string]any{
"spec": map[string]any{
"someNum": int64(6),
},
}
}),
t: v1.ComposedTemplate{ReadinessChecks: []v1.ReadinessCheck{{Type: "MatchInteger", FieldPath: "spec.someNum", MatchInteger: 5}}},
},
want: want{
ready: false,
},
},
"MatchIntegerTrue": {
reason: "If the value of the field does match, it should return true",
args: args{
cd: composed.New(func(r *composed.Unstructured) {
r.Object = map[string]any{
"spec": map[string]any{
"someNum": int64(5),
},
}
}),
t: v1.ComposedTemplate{ReadinessChecks: []v1.ReadinessCheck{{Type: "MatchInteger", FieldPath: "spec.someNum", MatchInteger: 5}}},
},
want: want{
ready: true,
},
},
"UnknownType": {
reason: "If unknown type is chosen, it should return an error",
args: args{
cd: composed.New(),
t: v1.ComposedTemplate{ReadinessChecks: []v1.ReadinessCheck{{Type: "Olala"}}},
},
want: want{
err: errors.New("readiness check at index 0: an unknown type is chosen"),
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ready, err := IsReady(context.Background(), tc.args.cd, tc.args.t)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nIsReady(...): -want, +got:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.ready, ready); diff != "" {
t.Errorf("\n%s\nIsReady(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}

View File

@ -0,0 +1,956 @@
/*
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.
*/
package composite
import (
"context"
"fmt"
"sort"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/durationpb"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kunstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured"
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
fnv1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1alpha1"
iov1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/v1alpha1"
v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
"github.com/crossplane/crossplane/internal/xcrd"
)
// Error strings.
const (
errFetchXRConnectionDetails = "cannot fetch composite resource connection details"
errGetExistingCDs = "cannot get existing composed resources"
errBuildFunctionIOObserved = "cannot build FunctionIO observed state"
errBuildFunctionIODesired = "cannot build initial FunctionIO desired state"
errMarshalXR = "cannot marshal composite resource"
errMarshalCD = "cannot marshal composed resource"
errPatchAndTransform = "cannot patch and transform resources"
errRunFunctionPipeline = "cannot run Composition Function pipeline"
errDeleteUndesiredCDs = "cannot delete undesired composed resources"
errApplyXR = "cannot apply composite resource"
errObserveCDs = "cannot observe composed resources"
errAnonymousCD = "encountered composed resource without required \"" + AnnotationKeyCompositionResourceName + "\" annotation"
errUnmarshalDesiredXR = "cannot unmarshal desired composite resource from FunctionIO"
errUnmarshalDesiredCD = "cannot unmarshal desired composed resource from FunctionIO"
errMarshalFnIO = "cannot marshal input FunctionIO"
errDialRunner = "cannot dial container runner"
errRunFnContainer = "cannot run container"
errCloseRunner = "cannot close connection to container runner"
errUnmarshalFnIO = "cannot unmarshal output FunctionIO"
errFmtApplyCD = "cannot apply composed resource %q"
errFmtFetchCDConnectionDetails = "cannot fetch connection details for composed resource %q (a %s named %s)"
errFmtRenderXR = "cannot render (patch and transform) composite resource from composed resource %q (%s)"
errFmtRunFn = "cannot run function %q"
errFmtUnsupportedFnType = "unsupported function type %q"
errFmtParseDesiredCD = "cannot parse desired composed resource %q from FunctionIO"
errFmtDeleteCD = "cannot delete composed resource %q (a %s named %s)"
errFmtReadiness = "cannot determine whether composed resource %q (a %s named %s) is ready"
errFmtExtractConnectionDetails = "cannot extract connection details from composed resource %q (a %s named %s)"
)
// DefaultTarget is the default function runner target endpoint.
const DefaultTarget = "unix-abstract:crossplane/fn/default.sock"
// A PTFComposer (i.e. Patch, Transform, and Function Composer) supports
// composing resources using both Patch and Transform (P&T) logic and a pipeline
// of Composition Functions. Callers may mix P&T with Composition Functions or
// use only one or the other.
type PTFComposer struct {
client resource.ClientApplicator
composite ptfComposite
composition ptfComposition
}
type ptfComposite struct {
managed.ConnectionDetailsFetcher
ComposedResourceGetter
ComposedResourceDeleter
ComposedResourceObserver
}
type ptfComposition struct {
PatchAndTransformer
FunctionPipelineRunner
}
// A ComposedResourceGetter gets composed resource state.
type ComposedResourceGetter interface {
GetComposedResources(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error)
}
// A ComposedResourceGetterFn gets composed resource state.
type ComposedResourceGetterFn func(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error)
// GetComposedResources gets composed resource state.
func (fn ComposedResourceGetterFn) GetComposedResources(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
return fn(ctx, xr)
}
// A ComposedResourceDeleter deletes composed resources (and their state).
type ComposedResourceDeleter interface {
DeleteComposedResources(ctx context.Context, s *PTFCompositionState) error
}
// A ComposedResourceDeleterFn deletes composed resources (and their state).
type ComposedResourceDeleterFn func(ctx context.Context, s *PTFCompositionState) error
// DeleteComposedResources deletes composed resources (and their state).
func (fn ComposedResourceDeleterFn) DeleteComposedResources(ctx context.Context, s *PTFCompositionState) error {
return fn(ctx, s)
}
// A ComposedResourceObserver derives additional state by observing composed
// resources.
type ComposedResourceObserver interface {
ObserveComposedResources(ctx context.Context, s *PTFCompositionState) error
}
// An ComposedResourceObserverFn derives additional state by observing composed
// resources.
type ComposedResourceObserverFn func(ctx context.Context, s *PTFCompositionState) error
// ObserveComposedResources derives additional state by observing composed
// resources.
func (fn ComposedResourceObserverFn) ObserveComposedResources(ctx context.Context, s *PTFCompositionState) error {
return fn(ctx, s)
}
// A PatchAndTransformer runs P&T Composition.
type PatchAndTransformer interface {
PatchAndTransform(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error
}
// A PatchAndTransformerFn runs P&T Composition.
type PatchAndTransformerFn func(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error
// PatchAndTransform runs P&T Composition.
func (fn PatchAndTransformerFn) PatchAndTransform(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
return fn(ctx, req, s)
}
// A FunctionPipelineRunner runs a pipeline of Composition Functions.
type FunctionPipelineRunner interface {
RunFunctionPipeline(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error
}
// A FunctionPipelineRunnerFn runs a pipeline of Composition Functions.
type FunctionPipelineRunnerFn func(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error
// RunFunctionPipeline runs a pipeline of Composition Functions.
func (fn FunctionPipelineRunnerFn) RunFunctionPipeline(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error {
return fn(ctx, req, s, o, d)
}
// A PTFComposerOption is used to configure a PTFComposer.
type PTFComposerOption func(*PTFComposer)
// WithCompositeConnectionDetailsFetcher configures how the PTFComposer should
// get the composite resource's connection details.
func WithCompositeConnectionDetailsFetcher(f managed.ConnectionDetailsFetcher) PTFComposerOption {
return func(p *PTFComposer) {
p.composite.ConnectionDetailsFetcher = f
}
}
// WithComposedResourceGetter configures how the PTFComposer should get existing
// composed resources.
func WithComposedResourceGetter(g ComposedResourceGetter) PTFComposerOption {
return func(p *PTFComposer) {
p.composite.ComposedResourceGetter = g
}
}
// WithComposedResourceDeleter configures how the PTFComposer should delete
// undesired composed resources.
func WithComposedResourceDeleter(d ComposedResourceDeleter) PTFComposerOption {
return func(p *PTFComposer) {
p.composite.ComposedResourceDeleter = d
}
}
// WithComposedResourceObserver configures how the PTFComposer should observe
// composed resources after applying them.
func WithComposedResourceObserver(o ComposedResourceObserver) PTFComposerOption {
return func(p *PTFComposer) {
p.composite.ComposedResourceObserver = o
}
}
// WithPatchAndTransformer configures how the PTFComposer should run Patch &
// Transform (P&T) Composition.
func WithPatchAndTransformer(pt PatchAndTransformer) PTFComposerOption {
return func(p *PTFComposer) {
p.composition.PatchAndTransformer = pt
}
}
// WithFunctionPipelineRunner configures how the PTFComposer should run a
// pipeline of Composition Functions.
func WithFunctionPipelineRunner(r FunctionPipelineRunner) PTFComposerOption {
return func(p *PTFComposer) {
p.composition.FunctionPipelineRunner = r
}
}
// NewPTFComposer returns a new Composer that supports composing resources using
// both Patch and Transform (P&T) logic and a pipeline of Composition Functions.
func NewPTFComposer(kube client.Client, o ...PTFComposerOption) *PTFComposer {
// TODO(negz): Can we avoid double-wrapping if the supplied client is
// already wrapped? Or just do away with unstructured.NewClient completely?
kube = unstructured.NewClient(kube)
f := NewSecretConnectionDetailsFetcher(kube)
c := &PTFComposer{
client: resource.ClientApplicator{Client: kube, Applicator: resource.NewAPIUpdatingApplicator(kube)},
composite: ptfComposite{
ConnectionDetailsFetcher: f,
ComposedResourceGetter: NewExistingComposedResourceGetter(kube, f),
ComposedResourceDeleter: NewUndesiredComposedResourceDeleter(kube),
ComposedResourceObserver: ComposedResourceObserverChain{
NewConnectionDetailsObserver(ConnectionDetailsExtractorFn(ExtractConnectionDetails)),
NewReadinessObserver(ReadinessCheckerFn(IsReady)),
},
},
composition: ptfComposition{
PatchAndTransformer: NewXRCDPatchAndTransformer(RendererFn(RenderComposite), NewAPIDryRunRenderer(kube)),
FunctionPipelineRunner: NewFunctionPipeline(ContainerFunctionRunnerFn(RunFunction)),
},
}
for _, fn := range o {
fn(c)
}
return c
}
// PTFCompositionState is used throughout the PTFComposer to track its state.
type PTFCompositionState struct {
Composite resource.Composite
ConnectionDetails managed.ConnectionDetails
ComposedResources ComposedResourceStates
}
// Compose resources using both either the Patch & Transform style resources
// array, the functions array, or both.
func (c *PTFComposer) Compose(ctx context.Context, xr resource.Composite, req CompositionRequest) (CompositionResult, error) { //nolint:gocyclo // We probably don't want any further abstraction for the sake of reduced complexity.
xc, err := c.composite.FetchConnection(ctx, xr)
if err != nil {
return CompositionResult{}, errors.Wrap(err, errFetchXRConnectionDetails)
}
cds, err := c.composite.GetComposedResources(ctx, xr)
if err != nil {
return CompositionResult{}, errors.Wrap(err, errGetExistingCDs)
}
state := &PTFCompositionState{
Composite: xr,
ConnectionDetails: xc,
ComposedResources: cds,
}
// Build observed state to be passed to our Composition Function pipeline.
// Doing this before we patch and transform ensures we report the state we
// actually o before we made any mutations.
o, err := FunctionIOObserved(state)
if err != nil {
return CompositionResult{}, errors.Wrap(err, errBuildFunctionIOObserved)
}
// Run P&T logic, updating the composition state accordingly.
if err := c.composition.PatchAndTransform(ctx, req, state); err != nil {
return CompositionResult{}, errors.Wrap(err, errPatchAndTransform)
}
// Build the initial desired state to be passed to our Composition Function
// pipeline. It's expected that each function in the pipeline will mutate
// this state. It includes any d state accumulated by the P&T logic.
d, err := FunctionIODesired(state)
if err != nil {
return CompositionResult{}, errors.Wrap(err, errBuildFunctionIODesired)
}
// Run Composition Functions, updating the composition state accordingly.
if err := c.composition.RunFunctionPipeline(ctx, req, state, o, d); err != nil {
return CompositionResult{}, errors.Wrap(err, errRunFunctionPipeline)
}
// Garbage collect any resources that aren't part of our final desired
// state. We must do this before we update the XR's resource references to
// ensure that we don't forget and leak them if a delete fails. Note that
// we're iterating over the ComposedResourceStates as they existed before
// they were passed to and potentially mutated by the Composition Function
// pipeline.
if err := c.composite.DeleteComposedResources(ctx, state); err != nil {
return CompositionResult{}, errors.Wrap(err, errDeleteUndesiredCDs)
}
// Record references to all desired composed resources.
UpdateResourceRefs(state)
// Call Apply so that we do not just replace fields on existing XR but merge
// fields for which a merge configuration has been specified. For fields for
// which a merge configuration does not exist, the behavior will be a
// replace from copy. Keep in mind that we're operating on a copy of the XR
// that was passed to this method. This means that unless the Apply is a
// no-op the XR's meta.resourceVersion will be updated in the API server and
// subsequent attempts to apply/update the xr object that was passed to this
// method will fail due to its outdated resourceVersion. This should be
// okay; the caller should keep trying until this is a no-op.
ao := mergeOptions(filterPatches(allPatches(state.ComposedResources), patchTypesToXR()...))
if err := c.client.Apply(ctx, state.Composite, ao...); err != nil {
return CompositionResult{}, errors.Wrap(err, errApplyXR)
}
// We apply all of our composed resources before we observe them and update
// in the loop below. This ensures that issues observing and processing one
// composed resource won't block the application of another.
for _, cd := range state.ComposedResources {
// Don't try to apply this resource if we didn't render it successfully.
// Note that this doesn't mean this resource won't exist; it might have
// been created previously.
if cd.RenderError != nil {
continue
}
ao := []resource.ApplyOption{resource.MustBeControllableBy(state.Composite.GetUID())}
if cd.Template != nil {
ao = append(ao, mergeOptions(filterPatches(cd.Template.Patches, patchTypesFromXR()...))...)
}
if err := c.client.Apply(ctx, cd.Resource, ao...); err != nil {
return CompositionResult{}, errors.Wrapf(err, errFmtApplyCD, cd.ResourceName)
}
}
// Observe all existing composed resources. This derives the XR's connection
// details from those of the composed resources. It also runs any readiness
// checks found in either P&T templates or the FunctionIO desired state.
if err := c.composite.ObserveComposedResources(ctx, state); err != nil {
return CompositionResult{}, errors.Wrap(err, "cannot observe composed resources")
}
out := make([]ComposedResource, 0, len(state.ComposedResources))
for _, cd := range state.ComposedResources {
out = append(out, cd.ComposedResource)
}
return CompositionResult{ConnectionDetails: state.ConnectionDetails, Composed: out}, nil
}
func allPatches(cds ComposedResourceStates) []v1.Patch {
out := make([]v1.Patch, 0, len(cds))
for _, cd := range cds {
if cd.Template == nil {
continue
}
out = append(out, cd.Template.Patches...)
}
return out
}
// An ExistingComposedResourceGetter uses an XR's resource references to load
// any existing composed resources from the API server. It also loads their
// connection details.
type ExistingComposedResourceGetter struct {
resource client.Reader
details managed.ConnectionDetailsFetcher
}
// NewExistingComposedResourceGetter returns a ComposedResourceGetter that
// fetches an XR's existing composed resources.
func NewExistingComposedResourceGetter(c client.Reader, f managed.ConnectionDetailsFetcher) *ExistingComposedResourceGetter {
return &ExistingComposedResourceGetter{resource: c, details: f}
}
// GetComposedResources begins building composed resource state by
// fetching any existing composed resources referenced by the supplied composite
// resource, as well as their connection details.
func (g *ExistingComposedResourceGetter) GetComposedResources(ctx context.Context, xr resource.Composite) (ComposedResourceStates, error) {
cds := ComposedResourceStates{}
for _, ref := range xr.GetResourceReferences() {
// The PTComposer writes references to resources that it didn't actually
// render or create. It has to create these placeholder refs because it
// supports anonymous (unnamed) resource templates; It needs to be able
// associate entries a Composition's spec.resources array with entries
// in an XR's spec.resourceRefs array by their index. These references
// won't have a name - we won't be able to get them because they don't
// reference a resource that actually exists.
if ref.Name == "" {
continue
}
r := composed.New(composed.FromReference(ref))
nn := types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}
err := g.resource.Get(ctx, nn, r)
if kerrors.IsNotFound(err) {
// We believe we created this resource, but it doesn't exist.
continue
}
if err != nil {
return nil, errors.Wrap(err, errGetComposed)
}
if c := metav1.GetControllerOf(r); c != nil && c.UID != xr.GetUID() {
// If we don't control this resource we just pretend it doesn't
// exist. We might try to render and re-create it later, but that
// should fail because we check the controller ref there too.
continue
}
name := GetCompositionResourceName(r)
if name == "" {
return nil, errors.New(errAnonymousCD)
}
conn, err := g.details.FetchConnection(ctx, r)
if err != nil {
return nil, errors.Wrapf(err, errFmtFetchCDConnectionDetails, name, r.GetKind(), r.GetName())
}
cds.Merge(ComposedResourceState{
ComposedResource: ComposedResource{ResourceName: name},
Resource: r,
ConnectionDetails: conn,
})
}
return cds, nil
}
// FunctionIOObserved builds observed state for a FunctionIO from the XR and any
// existing composed resources. This reflects the observed state of the world
// before any Composition (P&T or function-based) has taken place.
func FunctionIOObserved(s *PTFCompositionState) (iov1alpha1.Observed, error) {
raw, err := json.Marshal(s.Composite)
if err != nil {
return iov1alpha1.Observed{}, errors.Wrap(err, errMarshalXR)
}
rs := runtime.RawExtension{Raw: raw}
econn := make([]iov1alpha1.ExplicitConnectionDetail, 0, len(s.ConnectionDetails))
for n, v := range s.ConnectionDetails {
econn = append(econn, iov1alpha1.ExplicitConnectionDetail{Name: n, Value: string(v)})
}
oxr := iov1alpha1.ObservedComposite{Resource: rs, ConnectionDetails: econn}
ocds := make([]iov1alpha1.ObservedResource, 0, len(s.ComposedResources))
for _, cd := range s.ComposedResources {
raw, err := json.Marshal(cd.Resource)
if err != nil {
return iov1alpha1.Observed{}, errors.Wrap(err, errMarshalCD)
}
rs := runtime.RawExtension{Raw: raw}
ecds := make([]iov1alpha1.ExplicitConnectionDetail, 0, len(cd.ConnectionDetails))
for n, v := range cd.ConnectionDetails {
ecds = append(ecds, iov1alpha1.ExplicitConnectionDetail{Name: n, Value: string(v)})
}
ocds = append(ocds, iov1alpha1.ObservedResource{
Name: cd.ResourceName,
Resource: rs,
ConnectionDetails: ecds,
})
}
return iov1alpha1.Observed{Composite: oxr, Resources: ocds}, nil
}
// An XRCDPatchAndTransformer runs a Composition's Patches & Transforms against
// both the XR and composed resources.
type XRCDPatchAndTransformer struct {
composite Renderer
composed Renderer
}
// NewXRCDPatchAndTransformer returns a PatchAndTransformer that runs Patches
// and Transforms against both the XR and composed resources.
func NewXRCDPatchAndTransformer(composite, composed Renderer) *XRCDPatchAndTransformer {
return &XRCDPatchAndTransformer{composite: composite, composed: composed}
}
// PatchAndTransform updates the supplied composition state by running all
// patches and transforms within the CompositionRequest.
func (pt *XRCDPatchAndTransformer) PatchAndTransform(ctx context.Context, req CompositionRequest, s *PTFCompositionState) error {
// Inline PatchSets from Composition Spec before composing resources.
ct, err := ComposedTemplates(req.Composition.Spec)
if err != nil {
return errors.Wrap(err, errInline)
}
// If we have an environment, run all environment patches before composing
// resources.
if req.Environment != nil && req.Composition.Spec.Environment != nil {
for i, p := range req.Composition.Spec.Environment.Patches {
if err := ApplyEnvironmentPatch(p, s.Composite, req.Environment); err != nil {
return errors.Wrapf(err, errFmtPatchEnvironment, i)
}
}
}
// Render composite and composed resources using any P&T resource templates.
// Note that we require templates to be named; a CompositionValidator should
// enforce this.
for i := range ct {
t := ct[i]
var r resource.Composed = composed.New()
var details = ""
// Templates must be named. This is a requirement to use Composition
// Functions and thus this Composer implementation.
if cd, exists := s.ComposedResources[*t.Name]; exists {
r = cd.Resource
details = fmt.Sprintf("a %s named %s", r.GetObjectKind().GroupVersionKind().Kind, r.GetName())
// Typically we'll patch from composed resource status to the XR so
// we only want to render (i.e. patch) the XR from composed
// resources that actually exist.
if err := pt.composite.Render(ctx, s.Composite, r, t, req.Environment); err != nil {
// TODO(negz): Why is it that an error rendering CD->XR is
// terminal, but an error rendering XR->CD is not?
return errors.Wrapf(err, errFmtRenderXR, *t.Name, details)
}
}
rerr := pt.composed.Render(ctx, s.Composite, r, t, req.Environment)
// Wrap the error with some details if we can. We don't include the
// template name here because the Reconciler that calls us should (it
// has to account for the PTComposer too, where there may not be a named
// template).
if details != "" {
rerr = errors.Wrap(rerr, details)
}
s.ComposedResources.Merge(ComposedResourceState{
ComposedResource: ComposedResource{
ResourceName: *t.Name,
RenderError: rerr,
},
Resource: r,
Template: &t,
})
}
return nil
}
// FunctionIODesired builds the initial desired state for a FunctionIO from the XR
// and any existing or impending composed resources. This reflects the observed
// state of the world plus the initial desired state as built up by any P&T
// Composition that has taken place.
func FunctionIODesired(s *PTFCompositionState) (iov1alpha1.Desired, error) {
raw, err := json.Marshal(s.Composite)
if err != nil {
return iov1alpha1.Desired{}, errors.Wrap(err, errMarshalXR)
}
rs := runtime.RawExtension{Raw: raw}
econn := make([]iov1alpha1.ExplicitConnectionDetail, 0, len(s.ConnectionDetails))
for n, v := range s.ConnectionDetails {
econn = append(econn, iov1alpha1.ExplicitConnectionDetail{Name: n, Value: string(v)})
}
dxr := iov1alpha1.DesiredComposite{Resource: rs, ConnectionDetails: econn}
dcds := make([]iov1alpha1.DesiredResource, 0, len(s.ComposedResources))
for _, cd := range s.ComposedResources {
raw, err := json.Marshal(cd.Resource)
if err != nil {
return iov1alpha1.Desired{}, errors.Wrap(err, errMarshalCD)
}
dcds = append(dcds, iov1alpha1.DesiredResource{
Name: cd.ResourceName,
Resource: runtime.RawExtension{Raw: raw},
})
}
return iov1alpha1.Desired{Composite: dxr, Resources: dcds}, nil
}
// A ContainerFunctionRunner runs a containerized Composition Function.
type ContainerFunctionRunner interface {
RunFunction(ctx context.Context, fnio *iov1alpha1.FunctionIO, fn *v1.ContainerFunction) (*iov1alpha1.FunctionIO, error)
}
// A ContainerFunctionRunnerFn runs a containerized Composition Function.
type ContainerFunctionRunnerFn func(ctx context.Context, fnio *iov1alpha1.FunctionIO, fn *v1.ContainerFunction) (*iov1alpha1.FunctionIO, error)
// RunFunction runs a containerized Composition Function.
func (fn ContainerFunctionRunnerFn) RunFunction(ctx context.Context, fnio *iov1alpha1.FunctionIO, fnc *v1.ContainerFunction) (*iov1alpha1.FunctionIO, error) {
return fn(ctx, fnio, fnc)
}
// A FunctionPipeline runs a pipeline of Composition Functions.
type FunctionPipeline struct {
container ContainerFunctionRunner
}
// NewFunctionPipeline returns a FunctionPipeline that runs functions using the
// supplied ContainerFunctionRunner.
func NewFunctionPipeline(c ContainerFunctionRunner) *FunctionPipeline {
return &FunctionPipeline{container: c}
}
// RunFunctionPipeline runs a pipeline of Composition Functions.
func (p *FunctionPipeline) RunFunctionPipeline(ctx context.Context, req CompositionRequest, s *PTFCompositionState, o iov1alpha1.Observed, d iov1alpha1.Desired) error {
for _, fn := range req.Composition.Spec.Functions {
switch fn.Type {
case v1.FunctionTypeContainer:
fnio, err := p.container.RunFunction(ctx, &iov1alpha1.FunctionIO{Config: fn.Config, Observed: o, Desired: d}, fn.Container)
if err != nil {
return errors.Wrapf(err, errFmtRunFn, fn.Name)
}
// We require each function to pass through any desired state from
// previous functions in the pipeline that they're unconcerned with, as
// well as their own desired state.
d = fnio.Desired
default:
return errors.Wrapf(errors.Errorf(errFmtUnsupportedFnType, fn.Type), errFmtRunFn, fn.Name)
}
}
u := &kunstructured.Unstructured{}
if err := json.Unmarshal(d.Composite.Resource.Raw, u); err != nil {
return errors.Wrap(err, errUnmarshalDesiredXR)
}
s.Composite = &composite.Unstructured{Unstructured: *u}
s.ConnectionDetails = managed.ConnectionDetails{}
for _, cd := range d.Composite.ConnectionDetails {
s.ConnectionDetails[cd.Name] = []byte(cd.Value)
}
// TODO(negz): Parse the Results array too. Can we map them to resources?
for _, dr := range d.Resources {
cd, err := ParseDesiredResource(dr, s.Composite)
if err != nil {
return errors.Wrapf(err, errFmtParseDesiredCD, dr.Name)
}
s.ComposedResources.Merge(cd)
}
return nil
}
// RunFunction calls an external container function runner via gRPC.
func RunFunction(ctx context.Context, fnio *iov1alpha1.FunctionIO, fn *v1.ContainerFunction) (*iov1alpha1.FunctionIO, error) {
in, err := yaml.Marshal(fnio)
if err != nil {
return nil, errors.Wrap(err, errMarshalFnIO)
}
target := DefaultTarget
if fn.Runner != nil && fn.Runner.Endpoint != nil {
target = *fn.Runner.Endpoint
}
conn, err := grpc.DialContext(ctx, target, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, errors.Wrap(err, errDialRunner)
}
req := &fnv1alpha1.RunFunctionRequest{
Image: fn.Image,
Input: in,
ImagePullConfig: ImagePullConfig(fn),
RunFunctionConfig: RunFunctionConfig(fn),
}
rsp, err := fnv1alpha1.NewContainerizedFunctionRunnerServiceClient(conn).RunFunction(ctx, req)
if err != nil {
// TODO(negz): Parse any gRPC status codes.
_ = conn.Close()
return nil, errors.Wrap(err, errRunFnContainer)
}
if err := conn.Close(); err != nil {
return nil, errors.Wrap(err, errCloseRunner)
}
// TODO(negz): Sanity check this FunctionIO. Does it contain at least a
// desired Composite resource?
out := &iov1alpha1.FunctionIO{}
return out, errors.Wrap(yaml.Unmarshal(rsp.Output, out), errUnmarshalFnIO)
}
// ParseDesiredResource parses a (composed) DesiredResource from a FunctionIO.
func ParseDesiredResource(dr iov1alpha1.DesiredResource, owner resource.Object) (ComposedResourceState, error) {
u := &kunstructured.Unstructured{}
if err := json.Unmarshal(dr.Resource.Raw, u); err != nil {
return ComposedResourceState{}, errors.Wrap(err, errUnmarshalDesiredCD)
}
r := &composed.Unstructured{Unstructured: *u}
// Annotate the desired resource so we know which P&T resource template
// and/or FunctionIO desired.resources entry it is associated with.
SetCompositionResourceName(r, dr.Name)
meta.AddLabels(r, map[string]string{
xcrd.LabelKeyNamePrefixForComposed: owner.GetLabels()[xcrd.LabelKeyNamePrefixForComposed],
xcrd.LabelKeyClaimName: owner.GetLabels()[xcrd.LabelKeyClaimName],
xcrd.LabelKeyClaimNamespace: owner.GetLabels()[xcrd.LabelKeyClaimNamespace],
})
// Ensure our XR is the controller of the resource.
ref := meta.TypedReferenceTo(owner, owner.GetObjectKind().GroupVersionKind())
if err := meta.AddControllerReference(r, meta.AsController(ref)); err != nil {
return ComposedResourceState{}, errors.Wrap(err, errSetControllerRef)
}
cd := ComposedResourceState{
ComposedResource: ComposedResource{ResourceName: dr.Name},
Resource: r,
Desired: &dr,
}
return cd, nil
}
// ImagePullConfig builds an ImagePullConfig for a FunctionIO.
func ImagePullConfig(fn *v1.ContainerFunction) *fnv1alpha1.ImagePullConfig {
// TODO(negz): Use k8schain at this end to resolve auth.
cfg := &fnv1alpha1.ImagePullConfig{}
if fn.ImagePullPolicy != nil {
switch *fn.ImagePullPolicy {
case corev1.PullAlways:
cfg.PullPolicy = fnv1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_ALWAYS
case corev1.PullNever:
cfg.PullPolicy = fnv1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_NEVER
case corev1.PullIfNotPresent:
fallthrough
default:
cfg.PullPolicy = fnv1alpha1.ImagePullPolicy_IMAGE_PULL_POLICY_IF_NOT_PRESENT
}
}
return cfg
}
// RunFunctionConfig builds a RunFunctionConfig for a FunctionIO.
func RunFunctionConfig(fn *v1.ContainerFunction) *fnv1alpha1.RunFunctionConfig {
out := &fnv1alpha1.RunFunctionConfig{}
if fn.Timeout != nil {
out.Timeout = durationpb.New(fn.Timeout.Duration)
}
if fn.Resources != nil {
out.Resources = &fnv1alpha1.ResourceConfig{}
if fn.Resources.Limits != nil {
out.Resources.Limits = &fnv1alpha1.ResourceLimits{}
if fn.Resources.Limits.CPU != nil {
out.Resources.Limits.Cpu = fn.Resources.Limits.CPU.String()
}
if fn.Resources.Limits.Memory != nil {
out.Resources.Limits.Memory = fn.Resources.Limits.Memory.String()
}
}
}
if fn.Network != nil {
out.Network = &fnv1alpha1.NetworkConfig{}
if fn.Network.Policy != nil {
switch *fn.Network.Policy {
case v1.ContainerFunctionNetworkPolicyIsolated:
out.Network.Policy = fnv1alpha1.NetworkPolicy_NETWORK_POLICY_ISOLATED
case v1.ContainerFunctionNetworkPolicyRunner:
out.Network.Policy = fnv1alpha1.NetworkPolicy_NETWORK_POLICY_RUNNER
}
}
}
return out
}
// An UndesiredComposedResourceDeleter deletes composed resources from the API
// server and from Composition state if their state doesn't include a FunctionIO
// desired resource. This indicates the composed resource either exists or was
// going to be created by P&T composition, but didn't survive the Composition
// Function pipeline.
type UndesiredComposedResourceDeleter struct {
client client.Writer
}
// NewUndesiredComposedResourceDeleter returns a ComposedResourceDeleter that
// deletes undesired composed resources from both the API server and Composition
// state.
func NewUndesiredComposedResourceDeleter(c client.Writer) *UndesiredComposedResourceDeleter {
return &UndesiredComposedResourceDeleter{client: c}
}
// DeleteComposedResources deletes any composed resource that didn't come out the other
// end of the Composition Function pipeline (i.e. that wasn't in the final
// desired state after running the pipeline). Composed resources are deleted
// from both the supposed composition state and from the API server.
func (d *UndesiredComposedResourceDeleter) DeleteComposedResources(ctx context.Context, s *PTFCompositionState) error {
for name, cd := range s.ComposedResources {
// We know this resource is still desired because we recorded its
// desired state after running the FunctionIO pipeline. Don't garbage
// collect it.
if cd.Desired != nil {
continue
}
// We don't desire this resource to exist; remove it from our state.
delete(s.ComposedResources, name)
// No need to garbage collect resources that don't exist.
if !meta.WasCreated(cd.Resource) {
continue
}
// We want to garbage collect this resource, but we don't control it.
if c := metav1.GetControllerOf(cd.Resource); c == nil || c.UID != s.Composite.GetUID() {
continue
}
if err := d.client.Delete(ctx, cd.Resource); resource.IgnoreNotFound(err) != nil {
return errors.Wrapf(err, errFmtDeleteCD, cd.ResourceName, cd.Resource.GetObjectKind().GroupVersionKind().Kind, cd.Resource.GetName())
}
}
return nil
}
// UpdateResourceRefs updates the supplied state to ensure the XR references all
// composed resources that exist or are pending creation.
func UpdateResourceRefs(s *PTFCompositionState) {
refs := make([]corev1.ObjectReference, 0, len(s.ComposedResources))
for _, cd := range s.ComposedResources {
// Don't record references to resources that don't exist and that failed
// to render. We won't apply (i.e. create) these resources this time
// around, so there's no need to create dangling references to them.
if !meta.WasCreated(cd.Resource) && cd.RenderError != nil {
continue
}
ref := meta.ReferenceTo(cd.Resource, cd.Resource.GetObjectKind().GroupVersionKind())
refs = append(refs, *ref)
}
// We want to ensure our refs are stable.
sort.Slice(refs, func(i, j int) bool {
ri, rj := refs[i], refs[j]
return ri.APIVersion+ri.Kind+ri.Name < rj.APIVersion+rj.Kind+rj.Name
})
s.Composite.SetResourceReferences(refs)
}
// A ComposedResourceObserverChain runs a slice of ComposedResourceObservers.
type ComposedResourceObserverChain []ComposedResourceObserver
// ObserveComposedResources runs the slice of ComposedResourceObservers.
func (o ComposedResourceObserverChain) ObserveComposedResources(ctx context.Context, s *PTFCompositionState) error {
for _, cro := range o {
if err := cro.ObserveComposedResources(ctx, s); err != nil {
return err
}
}
return nil
}
// A ReadinessObserver observes composed resource state and updates it to
// indicate whether each composed resource is ready per the readiness checks
// associated with each resource, which are derived from their P&T resource
// template and/or Composition Function desired state.
type ReadinessObserver struct {
check ReadinessChecker
}
// NewReadinessObserver returns a ComposedResourceObserver that observes whether
// composed resources are ready.
func NewReadinessObserver(c ReadinessChecker) *ReadinessObserver {
return &ReadinessObserver{check: c}
}
// ObserveComposedResources to determine their readiness.
func (o *ReadinessObserver) ObserveComposedResources(ctx context.Context, s *PTFCompositionState) error {
for _, cd := range s.ComposedResources {
rcfgs := append(ReadinessChecksFromTemplate(cd.Template), ReadinessChecksFromDesired(cd.Desired)...)
ready, err := o.check.IsReady(ctx, cd.Resource, rcfgs...)
if err != nil {
return errors.Wrapf(err, errFmtReadiness, cd.ResourceName, cd.Resource.GetObjectKind().GroupVersionKind().Kind, cd.Resource.GetName())
}
s.ComposedResources.Merge(ComposedResourceState{
ComposedResource: ComposedResource{
ResourceName: cd.ResourceName,
Ready: ready,
},
})
}
return nil
}
// A ConnectionDetailsObserver extracts XR connection details from composed
// resource state. The details to extract are derived from each composed
// resource's P&T resource template and/or Composition Function desired state.
type ConnectionDetailsObserver struct {
details ConnectionDetailsExtractor
}
// NewConnectionDetailsObserver returns a ComposedResourceObserver that observes
// composed resources in order to extract XR connection details.
func NewConnectionDetailsObserver(e ConnectionDetailsExtractor) *ConnectionDetailsObserver {
return &ConnectionDetailsObserver{details: e}
}
// ObserveComposedResources to extract XR connection details.
func (o *ConnectionDetailsObserver) ObserveComposedResources(ctx context.Context, s *PTFCompositionState) error {
for _, cd := range s.ComposedResources {
ecfgs := append(ExtractConfigsFromTemplate(cd.Template), ExtractConfigsFromDesired(cd.Desired)...)
e, err := o.details.ExtractConnection(cd.Resource, cd.ConnectionDetails, ecfgs...)
if err != nil {
return errors.Wrapf(err, errFmtExtractConnectionDetails, cd.ResourceName, cd.Resource.GetObjectKind().GroupVersionKind().Kind, cd.Resource.GetName())
}
if s.ConnectionDetails == nil {
s.ConnectionDetails = managed.ConnectionDetails{}
}
for key, val := range e {
s.ConnectionDetails[key] = val
}
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -19,28 +19,39 @@ package composite
import (
"context"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
"sigs.k8s.io/controller-runtime/pkg/client"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
"github.com/crossplane/crossplane-runtime/pkg/resource"
iov1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/v1alpha1"
v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
)
// ConnectionDetailsFetcher fetches the connection details of the Composed resource.
type ConnectionDetailsFetcher interface {
FetchConnectionDetails(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (managed.ConnectionDetails, error)
// A ConnectionDetailsFetcherFn fetches the connection details of the supplied
// resource, if any.
type ConnectionDetailsFetcherFn func(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error)
// FetchConnection calls the FetchConnectionDetailsFn.
func (f ConnectionDetailsFetcherFn) FetchConnection(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
return f(ctx, o)
}
// A ConnectionDetailsFetcherChain chains multiple ConnectionDetailsFetchers.
type ConnectionDetailsFetcherChain []ConnectionDetailsFetcher
type ConnectionDetailsFetcherChain []managed.ConnectionDetailsFetcher
// FetchConnectionDetails of the supplied composed resource, if any.
func (fc ConnectionDetailsFetcherChain) FetchConnectionDetails(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (managed.ConnectionDetails, error) {
// FetchConnection details of the supplied composed resource, if any.
func (fc ConnectionDetailsFetcherChain) FetchConnection(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
all := make(managed.ConnectionDetails)
for _, p := range fc {
conn, err := p.FetchConnectionDetails(ctx, cd, t)
conn, err := p.FetchConnection(ctx, o)
if err != nil {
return all, err
}
@ -51,6 +62,35 @@ func (fc ConnectionDetailsFetcherChain) FetchConnectionDetails(ctx context.Conte
return all, nil
}
// An SecretConnectionDetailsFetcher may use the API server to read connection
// details from a Kubernetes Secret.
type SecretConnectionDetailsFetcher struct {
client client.Reader
}
// NewSecretConnectionDetailsFetcher returns a ConnectionDetailsFetcher that may
// use the API server to read connection details from a Kubernetes Secret.
func NewSecretConnectionDetailsFetcher(c client.Client) *SecretConnectionDetailsFetcher {
return &SecretConnectionDetailsFetcher{client: c}
}
// FetchConnection details of the supplied composed resource from its Kubernetes
// connection secret, per its WriteConnectionSecretToRef, if any.
func (cdf *SecretConnectionDetailsFetcher) FetchConnection(ctx context.Context, o resource.ConnectionSecretOwner) (managed.ConnectionDetails, error) {
sref := o.GetWriteConnectionSecretToReference()
if sref == nil {
// secret but has not yet. We presume this isn't an issue and that we'll
// propagate any connection details during a future iteration.
return nil, nil
}
s := &corev1.Secret{}
nn := types.NamespacedName{Namespace: sref.Namespace, Name: sref.Name}
if err := cdf.client.Get(ctx, nn, s); client.IgnoreNotFound(err) != nil {
return nil, errors.Wrap(err, errGetSecret)
}
return s.Data, nil
}
// SecretStoreConnectionPublisher is a ConnectionPublisher that stores
// connection details on the configured SecretStore.
type SecretStoreConnectionPublisher struct {
@ -109,50 +149,6 @@ func NewSecretStoreConnectionDetailsFetcher(f managed.ConnectionDetailsFetcher)
}
}
// FetchConnectionDetails of the supplied composed resource, if any.
func (f *SecretStoreConnectionDetailsFetcher) FetchConnectionDetails(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (managed.ConnectionDetails, error) { //nolint:gocyclo // NOTE(turkenh): This can be refactored with the removal of "WriteConnectionSecretRef" API.
so := cd.(resource.ConnectionSecretOwner)
data, err := f.fetcher.FetchConnection(ctx, so)
if err != nil {
return nil, errors.Wrap(err, errFetchDetails)
}
conn := managed.ConnectionDetails{}
for _, d := range t.ConnectionDetails {
switch tp := connectionDetailType(d); tp {
case v1.ConnectionDetailTypeFromConnectionSecretKey:
if d.FromConnectionSecretKey == nil {
return nil, errors.Errorf(errFmtConnDetailKey, tp)
}
if data == nil || data[*d.FromConnectionSecretKey] == nil {
// We don't consider this an error because it's possible the
// key will still be written at some point in the future.
continue
}
key := *d.FromConnectionSecretKey
if d.Name != nil {
key = *d.Name
}
if key != "" {
conn[key] = data[*d.FromConnectionSecretKey]
}
case v1.ConnectionDetailTypeFromFieldPath, v1.ConnectionDetailTypeFromValue, v1.ConnectionDetailTypeUnknown:
// We do nothing here with these cases, either:
// - ConnectionDetailTypeFromFieldPath,ConnectionDetailTypeFromValue
// => Already covered by APIConnectionDetailsFetcher.FetchConnectionDetails
// - ConnectionDetailTypeUnknown
// => We weren't able to determine the type of this connection detail.
}
}
if len(conn) == 0 {
return nil, nil
}
return conn, nil
}
// NewSecretStoreConnectionDetailsConfigurator returns a Configurator that
// configures a composite resource using its composition.
func NewSecretStoreConnectionDetailsConfigurator(c client.Client) *SecretStoreConnectionDetailsConfigurator {
@ -186,3 +182,188 @@ func (c *SecretStoreConnectionDetailsConfigurator) Configure(ctx context.Context
return errors.Wrap(c.client.Update(ctx, cp), errUpdateComposite)
}
// ConnectionDetailsExtractor extracts the connection details of a resource.
type ConnectionDetailsExtractor interface {
// ExtractConnection of the supplied resource.
ExtractConnection(cd resource.Composed, conn managed.ConnectionDetails, cfg ...ConnectionDetailExtractConfig) (managed.ConnectionDetails, error)
}
// A ConnectionDetailsExtractorFn is a function that satisfies
// ConnectionDetailsExtractor.
type ConnectionDetailsExtractorFn func(cd resource.Composed, conn managed.ConnectionDetails, cfg ...ConnectionDetailExtractConfig) (managed.ConnectionDetails, error)
// ExtractConnection of the supplied resource.
func (fn ConnectionDetailsExtractorFn) ExtractConnection(cd resource.Composed, conn managed.ConnectionDetails, cfg ...ConnectionDetailExtractConfig) (managed.ConnectionDetails, error) {
return fn(cd, conn, cfg...)
}
// ExtractConnectionDetails extracts XR connection details from the supplied
// composed resource. If no ExtractConfigs are supplied no connection details
// will be returned.
func ExtractConnectionDetails(cd resource.Composed, data managed.ConnectionDetails, cfg ...ConnectionDetailExtractConfig) (managed.ConnectionDetails, error) {
out := map[string][]byte{}
for _, cfg := range cfg {
switch tp := cfg.Type; tp {
case ConnectionDetailTypeFromValue:
if cfg.Value == nil {
return nil, errors.Errorf(errFmtConnDetailVal, tp)
}
out[cfg.Name] = []byte(*cfg.Value)
case ConnectionDetailTypeFromConnectionSecretKey:
if cfg.FromConnectionDetailKey == nil {
return nil, errors.Errorf(errFmtConnDetailKey, tp)
}
if data[*cfg.FromConnectionDetailKey] == nil {
// We don't consider this an error because it's possible the
// key will still be written at some point in the future.
continue
}
out[cfg.Name] = data[*cfg.FromConnectionDetailKey]
case ConnectionDetailTypeFromFieldPath:
if cfg.FromFieldPath == nil {
return nil, errors.Errorf(errFmtConnDetailPath, tp)
}
// Note we're checking that the error _is_ nil. If we hit an error
// we silently avoid including this connection secret. It's possible
// the path will start existing with a valid value in future.
if b, err := fromFieldPath(cd, *cfg.FromFieldPath); err == nil {
out[cfg.Name] = b
}
}
}
return out, nil
}
// A ConnectionDetailType is a type of connection detail.
type ConnectionDetailType string
// ConnectionDetailType types.
const (
ConnectionDetailTypeFromConnectionSecretKey ConnectionDetailType = "FromConnectionSecretKey"
ConnectionDetailTypeFromFieldPath ConnectionDetailType = "FromFieldPath"
ConnectionDetailTypeFromValue ConnectionDetailType = "FromValue"
)
// A ConnectionDetailExtractConfig configures how an XR connection detail should
// be extracted.
type ConnectionDetailExtractConfig struct {
// Type sets the connection detail fetching behaviour to be used. Each
// connection detail type may require its own fields to be set on the
// ConnectionDetail object.
Type ConnectionDetailType
// Name of the connection secret key that will be propagated to the
// connection secret of the composition instance.
Name string
// FromConnectionDetailKey is the key that will be used to fetch the value
// from the given target resource's connection details.
FromConnectionDetailKey *string
// FromFieldPath is the path of the field on the composed resource whose
// value to be used as input. Name must be specified if the type is
// FromFieldPath is specified.
FromFieldPath *string
// Value that will be propagated to the connection secret of the composition
// instance. Typically you should use FromConnectionSecretKey instead, but
// an explicit value may be set to inject a fixed, non-sensitive connection
// secret values, for example a well-known port.
Value *string
}
// ExtractConfigsFromTemplate builds extract configs for the supplied P&T style
// composed resource template.
func ExtractConfigsFromTemplate(t *v1.ComposedTemplate) []ConnectionDetailExtractConfig {
if t == nil {
return nil
}
out := make([]ConnectionDetailExtractConfig, len(t.ConnectionDetails))
for i := range t.ConnectionDetails {
out[i] = ConnectionDetailExtractConfig{
Type: connectionDetailType(t.ConnectionDetails[i]),
Value: t.ConnectionDetails[i].Value,
FromConnectionDetailKey: t.ConnectionDetails[i].FromConnectionSecretKey,
FromFieldPath: t.ConnectionDetails[i].FromFieldPath,
}
if t.ConnectionDetails[i].Name != nil {
out[i].Name = *t.ConnectionDetails[i].Name
}
if out[i].Type == ConnectionDetailTypeFromConnectionSecretKey && out[i].FromConnectionDetailKey != nil {
out[i].Name = *out[i].FromConnectionDetailKey
}
}
return out
}
// ExtractConfigsFromDesired builds extract configs for the supplied Composition
// Function desired state.
func ExtractConfigsFromDesired(dr *iov1alpha1.DesiredResource) []ConnectionDetailExtractConfig {
if dr == nil {
return nil
}
out := make([]ConnectionDetailExtractConfig, len(dr.ConnectionDetails))
for i := range dr.ConnectionDetails {
out[i] = ConnectionDetailExtractConfig{
Type: ConnectionDetailType(dr.ConnectionDetails[i].Type),
Value: dr.ConnectionDetails[i].Value,
FromConnectionDetailKey: dr.ConnectionDetails[i].FromConnectionDetailKey,
FromFieldPath: dr.ConnectionDetails[i].FromFieldPath,
}
if dr.ConnectionDetails[i].Name != nil {
out[i].Name = *dr.ConnectionDetails[i].Name
}
if out[i].Type == ConnectionDetailTypeFromConnectionSecretKey && out[i].FromConnectionDetailKey != nil {
out[i].Name = *out[i].FromConnectionDetailKey
}
}
return out
}
// Originally there was no 'type' determinator field so Crossplane would infer
// the type. We maintain this behaviour for backward compatibility when no type
// is set.
func connectionDetailType(d v1.ConnectionDetail) ConnectionDetailType {
switch {
case d.Type != nil:
return ConnectionDetailType(*d.Type)
case d.Value != nil:
return ConnectionDetailTypeFromValue
case d.FromConnectionSecretKey != nil:
return ConnectionDetailTypeFromConnectionSecretKey
case d.FromFieldPath != nil:
return ConnectionDetailTypeFromFieldPath
default:
// If nothing was specified, assume FromConnectionSecretKey, which was
// the only value we originally supported. We don't have enough
// information (i.e. the key name) to actually fetch it, so we'll still
// return an error eventually.
return ConnectionDetailTypeFromConnectionSecretKey
}
}
// fromFieldPath tries to read the value from the supplied field path first as a
// plain string. If this fails, it falls back to reading it as JSON.
func fromFieldPath(from runtime.Object, path string) ([]byte, error) {
fromMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(from)
if err != nil {
return nil, err
}
str, err := fieldpath.Pave(fromMap).GetString(path)
if err == nil {
return []byte(str), nil
}
in, err := fieldpath.Pave(fromMap).GetValue(path)
if err != nil {
return nil, err
}
return json.Marshal(in)
}

View File

@ -0,0 +1,498 @@
/*
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.
*/
package composite
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/fake"
"github.com/crossplane/crossplane-runtime/pkg/test"
v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
)
var (
_ managed.ConnectionDetailsFetcher = &SecretConnectionDetailsFetcher{}
_ managed.ConnectionDetailsFetcher = ConnectionDetailsFetcherChain{}
)
func TestFetchConnection(t *testing.T) {
errBoom := errors.New("boom")
sref := &xpv1.SecretReference{Name: "foo", Namespace: "bar"}
s := &corev1.Secret{
Data: map[string][]byte{
"foo": []byte("a"),
"bar": []byte("b"),
},
}
type args struct {
kube client.Client
o resource.ConnectionSecretOwner
}
type want struct {
conn managed.ConnectionDetails
err error
}
cases := map[string]struct {
reason string
args
want
}{
"DoesNotPublish": {
reason: "Should not fail if composed resource doesn't publish a connection secret",
args: args{
o: &fake.Composed{},
},
},
"SecretNotPublishedYet": {
reason: "Should not fail if composed resource has yet to publish the secret",
args: args{
kube: &test.MockClient{MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
},
want: want{
conn: nil,
},
},
"SecretGetFailed": {
reason: "Should fail if secret retrieval results in some error other than NotFound",
args: args{
kube: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
},
want: want{
err: errors.Wrap(errBoom, errGetSecret),
},
},
"Success": {
reason: "Should fetch all connection details from the connection secret.",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
},
want: want{
conn: managed.ConnectionDetails{
"foo": s.Data["foo"],
"bar": s.Data["bar"],
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
c := &SecretConnectionDetailsFetcher{client: tc.args.kube}
conn, err := c.FetchConnection(context.Background(), tc.args.o)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nFetchConnection(...): -want, +got:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.conn, conn, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("\n%s\nFetchFetchConnection(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
/*
func TestExtractConnectionDetails(t *testing.T) {
sref := &xpv1.SecretReference{Name: "foo", Namespace: "bar"}
s := &corev1.Secret{
Data: map[string][]byte{
"foo": []byte("a"),
"bar": []byte("b"),
},
}
type args struct {
kube client.Client
o resource.ConnectionSecretOwner
cfg []ExtractConfig
}
type want struct {
conn managed.ConnectionDetails
err error
}
cases := map[string]struct {
reason string
args
want
}{
"DoesNotPublish": {
reason: "Should not fail if composed resource doesn't publish a connection secret",
args: args{
o: &fake.Composed{},
},
},
"SecretNotPublishedYet": {
reason: "Should not fail if composed resource has yet to publish the secret",
args: args{
kube: &test.MockClient{MockGet: test.NewMockGetFn(kerrors.NewNotFound(schema.GroupResource{}, ""))},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
cfg: []ExtractConfig{
{
Type: ConnectionDetailTypeFromConnectionSecretKey,
Name: "bar",
FromConnectionDetailKey: pointer.StringPtr("bar"),
},
{
Type: ConnectionDetailTypeFromValue,
Name: "fixed",
Value: pointer.StringPtr("value"),
},
},
},
want: want{
conn: managed.ConnectionDetails{
"fixed": []byte("value"),
},
},
},
"SecretGetFailed": {
reason: "Should fail if secret retrieval results in some error other than NotFound",
args: args{
kube: &test.MockClient{MockGet: test.NewMockGetFn(errBoom)},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
},
want: want{
err: errors.Wrap(errBoom, errGetSecret),
},
},
"FetchConfigSuccess": {
reason: "Should publish only the selected set of secret keys",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
cfg: []ExtractConfig{
{
Type: ConnectionDetailTypeFromConnectionSecretKey,
Name: "bar",
FromConnectionDetailKey: pointer.StringPtr("bar"),
},
{
Type: ConnectionDetailTypeFromConnectionSecretKey,
Name: "none",
FromConnectionDetailKey: pointer.StringPtr("none"),
},
{
Type: ConnectionDetailTypeFromConnectionSecretKey,
Name: "convfoo",
FromConnectionDetailKey: pointer.StringPtr("foo"),
},
{
Type: ConnectionDetailTypeFromValue,
Name: "fixed",
Value: pointer.StringPtr("value"),
},
},
},
want: want{
conn: managed.ConnectionDetails{
"convfoo": s.Data["foo"],
"bar": s.Data["bar"],
"fixed": []byte("value"),
},
},
},
"NoFetchConfigSuccess": {
reason: "Should publish all connection details from the connection secret if no FetchConfigs are supplied",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
},
want: want{
conn: managed.ConnectionDetails{
"foo": s.Data["foo"],
"bar": s.Data["bar"],
},
},
},
"ConnectionDetailValueNotSet": {
reason: "Should error if Value type value is not set",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
cfg: []ExtractConfig{
{
Type: ConnectionDetailTypeFromValue,
Name: "missingvalue",
},
},
},
want: want{
err: errors.Errorf(errFmtConnDetailVal, v1.ConnectionDetailTypeFromValue),
},
},
"ErrConnectionDetailFromConnectionSecretKeyNotSet": {
reason: "Should error if ConnectionDetailFromConnectionSecretKey type FromConnectionSecretKey is not set",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
cfg: []ExtractConfig{
{
Type: ConnectionDetailTypeFromConnectionSecretKey,
Name: "missing-key",
},
},
},
want: want{
err: errors.Errorf(errFmtConnDetailKey, v1.ConnectionDetailTypeFromConnectionSecretKey),
},
},
"ErrConnectionDetailFromFieldPathNotSet": {
reason: "Should error if ConnectionDetailFromFieldPath type FromFieldPath is not set",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
},
cfg: []ExtractConfig{
{
Type: ConnectionDetailTypeFromFieldPath,
Name: "missing-path",
},
},
},
want: want{
err: errors.Errorf(errFmtConnDetailPath, v1.ConnectionDetailTypeFromFieldPath),
},
},
"SuccessFieldPath": {
reason: "Should publish only the selected set of secret keys",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
},
cfg: []ExtractConfig{
{
Type: ConnectionDetailTypeFromFieldPath,
Name: "name",
FromFieldPath: pointer.StringPtr("objectMeta.name"),
},
},
},
want: want{
conn: managed.ConnectionDetails{
"name": []byte("test"),
},
},
},
"SuccessFieldPathMarshal": {
reason: "Should publish the secret keys as a JSON value",
args: args{
kube: &test.MockClient{MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
if sobj, ok := obj.(*corev1.Secret); ok {
if key.Name == sref.Name && key.Namespace == sref.Namespace {
s.DeepCopyInto(sobj)
return nil
}
}
t.Errorf("wrong secret is queried")
return errBoom
}},
o: &fake.Composed{
ConnectionSecretWriterTo: fake.ConnectionSecretWriterTo{Ref: sref},
ObjectMeta: metav1.ObjectMeta{
Generation: 4,
},
},
cfg: []ExtractConfig{
{
Type: ConnectionDetailTypeFromFieldPath,
Name: "generation",
FromFieldPath: pointer.StringPtr("objectMeta.generation"),
},
},
},
want: want{
conn: managed.ConnectionDetails{
"generation": []byte("4"),
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
c := &SecretConnectionDetailsFetcher{client: tc.args.kube}
conn, err := c.FetchConnectionDetails(context.Background(), tc.args.o, tc.args.cfg...)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nFetch(...): -want, +got:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.conn, conn, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("\n%s\nFetch(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
*/
func TestConnectionDetailType(t *testing.T) {
fromVal := v1.ConnectionDetailTypeFromValue
name := "coolsecret"
value := "coolvalue"
key := "coolkey"
field := "coolfield"
cases := map[string]struct {
d v1.ConnectionDetail
want ConnectionDetailType
}{
"FromValueExplicit": {
d: v1.ConnectionDetail{Type: &fromVal},
want: ConnectionDetailTypeFromValue,
},
"FromValueInferred": {
d: v1.ConnectionDetail{
Name: &name,
Value: &value,
// Name and value trump key or field
FromConnectionSecretKey: &key,
FromFieldPath: &field,
},
want: ConnectionDetailTypeFromValue,
},
"FromConnectionSecretKeyInferred": {
d: v1.ConnectionDetail{
Name: &name,
FromConnectionSecretKey: &key,
// From key trumps from field
FromFieldPath: &field,
},
want: ConnectionDetailTypeFromConnectionSecretKey,
},
"FromFieldPathInferred": {
d: v1.ConnectionDetail{
Name: &name,
FromFieldPath: &field,
},
want: ConnectionDetailTypeFromFieldPath,
},
"DefaultToFromConnectionSecretKey": {
d: v1.ConnectionDetail{
Name: &name,
},
want: ConnectionDetailTypeFromConnectionSecretKey,
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := connectionDetailType(tc.d)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("connectionDetailType(...): -want, +got\n%s", diff)
}
})
}
}

View File

@ -0,0 +1,215 @@
/*
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.
*/
package composite
import (
"context"
"k8s.io/utils/pointer"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/pkg/resource"
iov1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/v1alpha1"
v1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
)
// Error strings
const (
errInvalidCheck = "invalid"
errPaveObject = "cannot lookup field paths in supplied object"
errFmtRequiresFieldPath = "type %q requires a field path"
errFmtRequiresMatchString = "type %q requires a match string"
errFmtRequiresMatchInteger = "type %q requires a match integer"
errFmtUnknownCheck = "unknown type %q"
errFmtRunCheck = "cannot run readiness check at index %d"
)
// ReadinessCheckType is used for readiness check types.
type ReadinessCheckType string
// The possible values for readiness check type.
const (
ReadinessCheckTypeNonEmpty ReadinessCheckType = "NonEmpty"
ReadinessCheckTypeMatchString ReadinessCheckType = "MatchString"
ReadinessCheckTypeMatchInteger ReadinessCheckType = "MatchInteger"
ReadinessCheckTypeNone ReadinessCheckType = "None"
)
// ReadinessCheck is used to indicate how to tell whether a resource is ready
// for consumption
type ReadinessCheck struct {
// Type indicates the type of probe you'd like to use.
Type ReadinessCheckType
// FieldPath shows the path of the field whose value will be used.
FieldPath *string
// MatchString is the value you'd like to match if you're using "MatchString" type.
MatchString *string
// MatchInt is the value you'd like to match if you're using "MatchInt" type.
MatchInteger *int64
}
// ReadinessChecksFromTemplate derives readiness checks from the supplied
// template.
func ReadinessChecksFromTemplate(t *v1.ComposedTemplate) []ReadinessCheck {
if t == nil {
return nil
}
out := make([]ReadinessCheck, len(t.ReadinessChecks))
for i := range t.ReadinessChecks {
out[i] = ReadinessCheck{Type: ReadinessCheckType(t.ReadinessChecks[i].Type)}
if t.ReadinessChecks[i].FieldPath != "" {
out[i].FieldPath = pointer.String(t.ReadinessChecks[i].FieldPath)
}
// NOTE(negz): ComposedTemplate doesn't use pointer values for optional
// strings, so today the empty string and 0 are equivalent to "unset".
if t.ReadinessChecks[i].MatchString != "" {
out[i].MatchString = pointer.String(t.ReadinessChecks[i].MatchString)
}
if t.ReadinessChecks[i].MatchInteger != 0 {
out[i].MatchInteger = pointer.Int64(t.ReadinessChecks[i].MatchInteger)
}
}
return out
}
// ReadinessChecksFromDesired derives readiness checks from the supplied desired
// resource.
func ReadinessChecksFromDesired(dr *iov1alpha1.DesiredResource) []ReadinessCheck {
if dr == nil {
return nil
}
out := make([]ReadinessCheck, len(dr.ReadinessChecks))
for i := range dr.ReadinessChecks {
out[i] = ReadinessCheck{
Type: ReadinessCheckType(dr.ReadinessChecks[i].Type),
FieldPath: dr.ReadinessChecks[i].FieldPath,
MatchString: dr.ReadinessChecks[i].MatchString,
MatchInteger: dr.ReadinessChecks[i].MatchInteger,
}
}
return out
}
// TODO(negz): Ideally we'd validate P&T readiness checks (which are specified
// in the Composition) using a webhook. We still need to validate the output of
// a Composition Function Pipeline, though.
// Validate returns an error if the readiness check is invalid.
func (c ReadinessCheck) Validate() error {
switch c.Type {
case ReadinessCheckTypeNone:
// This type has no dependencies.
return nil
case ReadinessCheckTypeNonEmpty:
// This type only needs a field path.
case ReadinessCheckTypeMatchString:
if c.MatchString == nil {
return errors.Errorf(errFmtRequiresMatchString, c.Type)
}
case ReadinessCheckTypeMatchInteger:
if c.MatchInteger == nil {
return errors.Errorf(errFmtRequiresMatchInteger, c.Type)
}
default:
return errors.Errorf(errFmtUnknownCheck, c.Type)
}
if c.FieldPath == nil {
return errors.Errorf(errFmtRequiresFieldPath, c.Type)
}
return nil
}
// IsReady runs the readiness check against the supplied object.
func (c ReadinessCheck) IsReady(p *fieldpath.Paved) (bool, error) {
if err := c.Validate(); err != nil {
return false, errors.Wrap(err, errInvalidCheck)
}
switch c.Type {
case ReadinessCheckTypeNone:
return true, nil
case ReadinessCheckTypeNonEmpty:
if _, err := p.GetValue(*c.FieldPath); err != nil {
return false, resource.Ignore(fieldpath.IsNotFound, err)
}
return true, nil
case ReadinessCheckTypeMatchString:
val, err := p.GetString(*c.FieldPath)
if err != nil {
return false, resource.Ignore(fieldpath.IsNotFound, err)
}
return val == *c.MatchString, nil
case ReadinessCheckTypeMatchInteger:
val, err := p.GetInteger(*c.FieldPath)
if err != nil {
return false, resource.Ignore(fieldpath.IsNotFound, err)
}
return val == *c.MatchInteger, nil
}
return false, nil
}
// A ReadinessChecker checks whether a composed resource is ready or not.
type ReadinessChecker interface {
IsReady(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error)
}
// A ReadinessCheckerFn checks whether a composed resource is ready or not.
type ReadinessCheckerFn func(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error)
// IsReady reports whether a composed resource is ready or not.
func (fn ReadinessCheckerFn) IsReady(ctx context.Context, o ConditionedObject, rc ...ReadinessCheck) (ready bool, err error) {
return fn(ctx, o, rc...)
}
// A ConditionedObject is a runtime object with conditions.
type ConditionedObject interface {
resource.Object
resource.Conditioned
}
// IsReady returns whether the composed resource is ready.
func IsReady(_ context.Context, o ConditionedObject, rc ...ReadinessCheck) (bool, error) {
if len(rc) == 0 {
return resource.IsConditionTrue(o.GetCondition(xpv1.TypeReady)), nil
}
paved, err := fieldpath.PaveObject(o)
if err != nil {
return false, errors.Wrap(err, errPaveObject)
}
for i := range rc {
ready, err := rc[i].IsReady(paved)
if err != nil {
return false, errors.Wrapf(err, errFmtRunCheck, i)
}
if !ready {
return false, nil
}
}
return true, nil
}

View File

@ -0,0 +1,277 @@
/*
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.
*/
package composite
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"k8s.io/utils/pointer"
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
var _ ReadinessChecker = ReadinessCheckerFn(IsReady)
func TestIsReady(t *testing.T) {
type args struct {
ctx context.Context
o ConditionedObject
rc []ReadinessCheck
}
type want struct {
ready bool
err error
}
cases := map[string]struct {
reason string
args
want
}{
"NoCustomCheckReady": {
reason: "If no custom check is given, Ready condition should be used",
args: args{
o: composed.New(composed.WithConditions(xpv1.Available())),
},
want: want{
ready: true,
},
},
"NoCustomCheckNotReady": {
reason: "If no custom check is given, Ready condition should be used",
args: args{
o: composed.New(composed.WithConditions(xpv1.Unavailable())),
},
want: want{
ready: false,
},
},
"ExplictNone": {
reason: "If the only readiness check is explicitly 'None' the resource is always ready.",
args: args{
o: composed.New(),
rc: []ReadinessCheck{{Type: ReadinessCheckTypeNone}},
},
want: want{
ready: true,
},
},
"NonEmptyMissingFieldPath": {
reason: "If the value cannot be fetched due to fieldPath being missing, an error should be returned",
args: args{
o: composed.New(),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeNonEmpty,
}},
},
want: want{
err: errors.Wrapf(errors.Wrap(errors.Errorf(errFmtRequiresFieldPath, ReadinessCheckTypeNonEmpty), errInvalidCheck), errFmtRunCheck, 0),
},
},
"NonEmptyErr": {
reason: "If the value cannot be fetched due to fieldPath being misconfigured, error should be returned",
args: args{
o: composed.New(),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeNonEmpty,
FieldPath: pointer.String("metadata..uid"),
}},
},
want: want{
err: errors.Wrapf(fieldpath.Pave(nil).GetValueInto("metadata..uid", nil), errFmtRunCheck, 0),
},
},
"NonEmptyFalse": {
reason: "If the field does not have value, NonEmpty check should return false",
args: args{
o: composed.New(),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeNonEmpty,
FieldPath: pointer.String("metadata.uid"),
}},
},
want: want{
ready: false,
},
},
"NonEmptyTrue": {
reason: "If the field does have a value, NonEmpty check should return true",
args: args{
o: composed.New(func(r *composed.Unstructured) {
r.SetUID("olala")
}),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeNonEmpty,
FieldPath: pointer.String("metadata.uid"),
}},
},
want: want{
ready: true,
},
},
"MatchStringErr": {
reason: "If the value cannot be fetched due to fieldPath being misconfigured, error should be returned",
args: args{
o: composed.New(),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeMatchString,
FieldPath: pointer.String("metadata..uid"),
MatchString: pointer.String("cool"),
}},
},
want: want{
err: errors.Wrapf(fieldpath.Pave(nil).GetValueInto("metadata..uid", nil), errFmtRunCheck, 0),
},
},
"MatchStringMissing": {
reason: "If the value cannot be fetched due to a missing matchstring, we should return an error",
args: args{
o: composed.New(),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeMatchString,
FieldPath: pointer.String("metadata..uid"),
}},
},
want: want{
err: errors.Wrapf(errors.Wrap(errors.Errorf(errFmtRequiresMatchString, ReadinessCheckTypeMatchString), errInvalidCheck), errFmtRunCheck, 0),
},
},
"MatchStringFalse": {
reason: "If the value of the field does not match, it should return false",
args: args{
o: composed.New(),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeMatchString,
FieldPath: pointer.String("metadata.uid"),
MatchString: pointer.String("olala"),
}},
},
want: want{
ready: false,
},
},
"MatchStringTrue": {
reason: "If the value of the field does match, it should return true",
args: args{
o: composed.New(func(r *composed.Unstructured) {
r.SetUID("olala")
}),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeMatchString,
FieldPath: pointer.String("metadata.uid"),
MatchString: pointer.String("olala"),
}},
},
want: want{
ready: true,
},
},
"MatchIntegerErr": {
reason: "If the value cannot be fetched due to fieldPath being misconfigured, error should be returned",
args: args{
o: composed.New(),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeMatchInteger,
FieldPath: pointer.String("metadata..uid"),
MatchInteger: pointer.Int64(42),
}},
},
want: want{
err: errors.Wrapf(fieldpath.Pave(nil).GetValueInto("metadata..uid", nil), errFmtRunCheck, 0),
},
},
"MatchIntegerMissing": {
reason: "If the value cannot be fetched due to a missing matchinteger, we should return an error",
args: args{
o: composed.New(),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeMatchInteger,
FieldPath: pointer.String("metadata..uid"),
}},
},
want: want{
err: errors.Wrapf(errors.Wrap(errors.Errorf(errFmtRequiresMatchInteger, ReadinessCheckTypeMatchInteger), errInvalidCheck), errFmtRunCheck, 0),
},
},
"MatchIntegerFalse": {
reason: "If the value of the field does not match, it should return false",
args: args{
o: composed.New(func(r *composed.Unstructured) {
r.Object = map[string]any{
"spec": map[string]any{
"someNum": int64(6),
},
}
}),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeMatchInteger,
FieldPath: pointer.String("spec.someNum"),
MatchInteger: pointer.Int64(5),
}},
},
want: want{
ready: false,
},
},
"MatchIntegerTrue": {
reason: "If the value of the field does match, it should return true",
args: args{
o: composed.New(func(r *composed.Unstructured) {
r.Object = map[string]any{
"spec": map[string]any{
"someNum": int64(5),
},
}
}),
rc: []ReadinessCheck{{
Type: ReadinessCheckTypeMatchInteger,
FieldPath: pointer.String("spec.someNum"),
MatchInteger: pointer.Int64(5),
}},
},
want: want{
ready: true,
},
},
"UnknownType": {
reason: "If unknown type is chosen, it should return an error",
args: args{
o: composed.New(),
rc: []ReadinessCheck{{Type: "Olala"}},
},
want: want{
err: errors.Wrapf(errors.Wrap(errors.Errorf(errFmtUnknownCheck, "Olala"), errInvalidCheck), errFmtRunCheck, 0),
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
ready, err := IsReady(tc.args.ctx, tc.args.o, tc.args.rc...)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nIsReady(...): -want, +got:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.ready, ready); diff != "" {
t.Errorf("\n%s\nIsReady(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}

View File

@ -177,28 +177,6 @@ func (fn RendererFn) Render(ctx context.Context, cp resource.Composite, cd resou
return fn(ctx, cp, cd, t, env)
}
// A ConnectionDetailsFetcherFn fetches the connection details of the supplied
// composed resource, if any.
type ConnectionDetailsFetcherFn func(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (managed.ConnectionDetails, error)
// FetchConnectionDetails calls the FetchConnectionDetailsFn.
func (f ConnectionDetailsFetcherFn) FetchConnectionDetails(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (managed.ConnectionDetails, error) {
return f(ctx, cd, t)
}
// A ReadinessChecker checks whether a composed resource is ready or not.
type ReadinessChecker interface {
IsReady(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (ready bool, err error)
}
// A ReadinessCheckerFn checks whether a composed resource is ready or not.
type ReadinessCheckerFn func(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (ready bool, err error)
// IsReady reports whether a composed resource is ready or not.
func (fn ReadinessCheckerFn) IsReady(ctx context.Context, cd resource.Composed, t v1.ComposedTemplate) (ready bool, err error) {
return fn(ctx, cd, t)
}
// A CompositionRequest is a request to compose resources.
// It should be treated as immutable.
type CompositionRequest struct {
@ -212,15 +190,6 @@ type CompositionResult struct {
ConnectionDetails managed.ConnectionDetails
}
// A ComposedResource is an output of the composition process.
type ComposedResource struct {
Name string
Resource resource.Composed
ConnectionDetails managed.ConnectionDetails
RenderError error
Ready bool
}
// A Composer composes (i.e. creates, updates, or deletes) resources given the
// supplied composite resource and composition request.
type Composer interface {
@ -369,12 +338,6 @@ type compositeResource struct {
managed.ConnectionPublisher
}
type composedResource struct {
Renderer
ConnectionDetailsFetcher
ReadinessChecker
}
// NewReconciler returns a new Reconciler of composite resources.
func NewReconciler(mgr manager.Manager, of resource.CompositeKind, opts ...ReconcilerOption) *Reconciler {
nc := func() resource.Composite {
@ -389,10 +352,10 @@ func NewReconciler(mgr manager.Manager, of resource.CompositeKind, opts ...Recon
composition: composition{
CompositionFetcher: NewAPICompositionFetcher(kube),
CompositionValidator: ValidationChain{
CompositionValidatorFn(RejectAnonymousTemplatesWithFunctions),
CompositionValidatorFn(RejectFunctionsWithoutRequiredConfig),
CompositionValidatorFn(RejectMixedTemplates),
CompositionValidatorFn(RejectDuplicateNames),
CompositionValidatorFn(RejectAnonymousTemplatesWithFunctions),
CompositionValidatorFn(RejectFunctionsWithoutRequiredConfig),
},
},
@ -412,7 +375,7 @@ func NewReconciler(mgr manager.Manager, of resource.CompositeKind, opts ...Recon
ConnectionPublisher: NewAPIFilteredSecretPublisher(kube, []string{}),
},
resource: NewPatchAndTransformComposer(kube),
resource: NewPTComposer(kube),
log: logging.NewNopLogger(),
record: event.NewNopRecorder(),
@ -566,6 +529,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
return reconcile.Result{}, err
}
// TODO(negz): Pass this method a copy of xr, to make very clear that
// anything it does won't be reflected in the state of xr?
res, err := r.resource.Compose(ctx, xr, CompositionRequest{Composition: cp, Environment: env})
if err != nil {
log.Debug(errCompose, "error", err)
@ -593,7 +558,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
for i, cd := range res.Composed {
// Specifying a name for P&T templates is optional but encouraged.
// If there was no name, fall back to using the index.
id := cd.Name
id := cd.ResourceName
if id == "" {
id = strconv.Itoa(i)
}
@ -636,30 +601,3 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
xr.SetConditions(xpv1.Available())
return reconcile.Result{RequeueAfter: r.pollInterval}, errors.Wrap(r.client.Status().Update(ctx, xr), errUpdateStatus)
}
// filterToXRPatches selects patches defined in composed templates,
// whose type is one of the XR-targeting patches
// (e.g. v1.PatchTypeToCompositeFieldPath or v1.PatchTypeCombineToComposite)
func filterToXRPatches(tas []TemplateAssociation) []v1.Patch {
filtered := make([]v1.Patch, 0, len(tas))
for _, ta := range tas {
filtered = append(filtered, filterPatches(ta.Template.Patches,
patchTypesToXR()...)...)
}
return filtered
}
// filterPatches selects patches whose type belong to the list onlyTypes
func filterPatches(pas []v1.Patch, onlyTypes ...v1.PatchType) []v1.Patch {
filtered := make([]v1.Patch, 0, len(pas))
include := make(map[v1.PatchType]bool)
for _, t := range onlyTypes {
include[t] = true
}
for _, p := range pas {
if include[p.Type] {
filtered = append(filtered, p)
}
}
return filtered
}

View File

@ -799,7 +799,7 @@ func TestFilterToXRPatches(t *testing.T) {
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
if diff := cmp.Diff(tc.want, filterToXRPatches(tc.args.tas)); diff != "" {
if diff := cmp.Diff(tc.want, toXRPatchesFromTAs(tc.args.tas)); diff != "" {
t.Errorf("\nfilterToXRPatches(...): -want, +got:\n%s", diff)
}
})

View File

@ -456,6 +456,10 @@ func CompositeReconcilerOptions(f *feature.Flags, d *v1.CompositeResourceDefinit
composite.WithEnvironmentFetcher(environment.NewAPIEnvironmentFetcher(c)))
}
// If external secret stores aren't enabled we just fetch connection details
// from Kubernetes secrets.
var fetcher managed.ConnectionDetailsFetcher = composite.NewSecretConnectionDetailsFetcher(c)
// We only want to enable ExternalSecretStore support if the relevant
// feature flag is enabled. Otherwise, we start the XR reconcilers with
// their default ConnectionPublisher and ConnectionDetailsFetcher.
@ -468,9 +472,11 @@ func CompositeReconcilerOptions(f *feature.Flags, d *v1.CompositeResourceDefinit
composite.NewSecretStoreConnectionPublisher(connection.NewDetailsManager(c, v1alpha1.StoreConfigGroupVersionKind), d.GetConnectionSecretKeys()),
}
fc := composite.ConnectionDetailsFetcherChain{
composite.NewAPIConnectionDetailsFetcher(c),
composite.NewSecretStoreConnectionDetailsFetcher(connection.NewDetailsManager(c, v1alpha1.StoreConfigGroupVersionKind)),
// If external secret stores are enabled we need to support fetching
// connection details from both secrets and external stores.
fetcher = composite.ConnectionDetailsFetcherChain{
composite.NewSecretConnectionDetailsFetcher(c),
connection.NewDetailsManager(c, v1alpha1.StoreConfigGroupVersionKind),
}
cc := composite.NewConfiguratorChain(
@ -482,7 +488,31 @@ func CompositeReconcilerOptions(f *feature.Flags, d *v1.CompositeResourceDefinit
o = append(o,
composite.WithConnectionPublishers(pc...),
composite.WithConfigurator(cc),
composite.WithComposer(composite.NewPatchAndTransformComposer(c, composite.WithComposedConnectionDetailsFetcher(fc))))
composite.WithComposer(composite.NewPTComposer(c, composite.WithComposedConnectionDetailsFetcher(fetcher))))
}
// If Composition Functions are enabled we want to try to use the
// PTFComposer. This Composer supports using P&T Composition alone,
// Functions alone, or mixing both. It does not support anonymous resource
// templates - resource templates with a nil name - because it needs the
// name to match entries in the resource templates array to entries in the
// FunctionIO used by the templates array. We therefore 'fall back' to the
// PTComposer if we encounter a Composition with anonymous templates.
// Composition validation ensures that a Composition that uses functions
// must have named resources templates.
if f.Enabled(features.EnableAlphaCompositionFunctions) {
fb := composite.NewFallBackComposer(
composite.NewPTFComposer(c,
composite.WithComposedResourceGetter(composite.NewExistingComposedResourceGetter(c, fetcher)),
composite.WithCompositeConnectionDetailsFetcher(fetcher),
),
composite.NewPTComposer(c, composite.WithComposedConnectionDetailsFetcher(fetcher)),
composite.FallBackForAnonymousTemplates(c),
)
// Note that if external secret stores are enabled this will supercede
// the WithComposer option specified in that block.
o = append(o, composite.WithComposer(fb))
}
return o

View File

@ -34,4 +34,9 @@ const (
// External Secret Stores. See the below design for more details.
// https://github.com/crossplane/crossplane/blob/390ddd/design/design-doc-external-secret-stores.md
EnableAlphaExternalSecretStores feature.Flag = "EnableAlphaExternalSecretStores"
// EnableAlphaCompositionFunctions enables alpha support for composition
// functions. See the below design for more details.
// https://github.com/crossplane/crossplane/blob/9ee7a2/design/design-doc-composition-functions.md
EnableAlphaCompositionFunctions feature.Flag = "EnableAlphaCompositionFunctions"
)