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:
parent
6cc1ee1774
commit
95bcbcd2e2
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue