Have the claim controller support both CSA and SSA
This reintroduces the old client-side apply claim logic. Instead of two 'Configurators' (one for the XR and one for the claim) it merges the logic into one 'Syncer'. It keeps the new server-side apply claim logic, but also merges it from two 'Configurators' into one 'Syncer'. This allows us to toggle server-side apply on and off by switching which Syncer the controller uses, allowing SSA to be put behind a feature flag. Signed-off-by: Nic Cope <nicc@rk0n.org>
This commit is contained in:
parent
6d4f6d92c2
commit
5b3650ae8b
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 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 claim
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Error strings.
|
||||
const (
|
||||
errBindClaimConflict = "cannot bind claim that references a different composite resource"
|
||||
errGetSecret = "cannot get composite resource's connection secret"
|
||||
errSecretConflict = "cannot establish control of existing connection secret"
|
||||
errCreateOrUpdateSecret = "cannot create or update connection secret"
|
||||
)
|
||||
|
||||
// An APIConnectionPropagator propagates connection details by reading
|
||||
// them from and writing them to a Kubernetes API server.
|
||||
type APIConnectionPropagator struct {
|
||||
client resource.ClientApplicator
|
||||
}
|
||||
|
||||
// NewAPIConnectionPropagator returns a new APIConnectionPropagator.
|
||||
func NewAPIConnectionPropagator(c client.Client) *APIConnectionPropagator {
|
||||
return &APIConnectionPropagator{
|
||||
client: resource.ClientApplicator{Client: c, Applicator: resource.NewAPIUpdatingApplicator(c)},
|
||||
}
|
||||
}
|
||||
|
||||
// PropagateConnection details from the supplied resource.
|
||||
func (a *APIConnectionPropagator) PropagateConnection(ctx context.Context, to resource.LocalConnectionSecretOwner, from resource.ConnectionSecretOwner) (bool, error) {
|
||||
// Either from does not expose a connection secret, or to does not want one.
|
||||
if from.GetWriteConnectionSecretToReference() == nil || to.GetWriteConnectionSecretToReference() == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
n := types.NamespacedName{
|
||||
Namespace: from.GetWriteConnectionSecretToReference().Namespace,
|
||||
Name: from.GetWriteConnectionSecretToReference().Name,
|
||||
}
|
||||
fs := &corev1.Secret{}
|
||||
if err := a.client.Get(ctx, n, fs); err != nil {
|
||||
return false, errors.Wrap(err, errGetSecret)
|
||||
}
|
||||
|
||||
// Make sure 'from' is the controller of the connection secret it references
|
||||
// before we propagate it. This ensures a resource cannot use Crossplane to
|
||||
// circumvent RBAC by propagating a secret it does not own.
|
||||
if c := metav1.GetControllerOf(fs); c == nil || c.UID != from.GetUID() {
|
||||
return false, errors.New(errSecretConflict)
|
||||
}
|
||||
|
||||
ts := resource.LocalConnectionSecretFor(to, to.GetObjectKind().GroupVersionKind())
|
||||
ts.Data = fs.Data
|
||||
|
||||
err := a.client.Apply(ctx, ts,
|
||||
resource.ConnectionSecretMustBeControllableBy(to.GetUID()),
|
||||
resource.AllowUpdateIf(func(current, desired runtime.Object) bool {
|
||||
// We consider the update to be a no-op and don't allow it if the
|
||||
// current and existing secret data are identical.
|
||||
return !cmp.Equal(current.(*corev1.Secret).Data, desired.(*corev1.Secret).Data, cmpopts.EquateEmpty())
|
||||
}),
|
||||
)
|
||||
if resource.IsNotAllowed(err) {
|
||||
// The update was not allowed because it was a no-op.
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, errCreateOrUpdateSecret)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
|
@ -1,308 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 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 claim
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/errors"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/claim"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
|
||||
|
||||
"github.com/crossplane/crossplane/internal/names"
|
||||
"github.com/crossplane/crossplane/internal/xcrd"
|
||||
)
|
||||
|
||||
const (
|
||||
errUnsupportedClaimSpec = "composite resource claim spec was not an object"
|
||||
errUnsupportedDstObject = "destination object was not valid object"
|
||||
errUnsupportedSrcObject = "source object was not valid object"
|
||||
|
||||
errMergeClaimSpec = "unable to merge claim spec"
|
||||
errMergeClaimStatus = "unable to merge claim status"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrBindCompositeConflict can occur if the composite refers a different claim
|
||||
ErrBindCompositeConflict = errors.New("cannot bind composite resource that references a different claim")
|
||||
)
|
||||
|
||||
type apiCompositeConfigurator struct {
|
||||
names.NameGenerator
|
||||
}
|
||||
|
||||
// Configure configures the supplied composite patch
|
||||
// by propagating configuration from the supplied claim.
|
||||
// Both create and update scenarios are supported; i.e. the
|
||||
// composite may or may not have been created in the API server
|
||||
// when passed to this method.
|
||||
func (c *apiCompositeConfigurator) Configure(ctx context.Context, cmObserved *claim.Unstructured, cpObserved, cpPatch *composite.Unstructured) error { //nolint:gocyclo // Only slightly over (12).
|
||||
|
||||
icmSpec := cmObserved.Object["spec"]
|
||||
spec, ok := icmSpec.(map[string]any)
|
||||
if !ok {
|
||||
return errors.New(errUnsupportedClaimSpec)
|
||||
}
|
||||
|
||||
existing := cpObserved.GetClaimReference()
|
||||
proposed := cmObserved.GetReference()
|
||||
if existing != nil && !cmp.Equal(existing, proposed) {
|
||||
return ErrBindCompositeConflict
|
||||
}
|
||||
|
||||
// It's possible we're being asked to configure a statically provisioned
|
||||
// composite resource in which case we should respect its existing name and
|
||||
// external name.
|
||||
en := meta.GetExternalName(cpObserved)
|
||||
|
||||
// Do not propagate *.kubernetes.io annotations/labels down to the composite
|
||||
// For example: when a claim gets deployed using kubectl,
|
||||
// its kubectl.kubernetes.io/last-applied-configuration annotation
|
||||
// should not be propagated to the corresponding composite resource,
|
||||
// because:
|
||||
// * XR was not created using kubectl
|
||||
// * The content of the annotaton refers to the claim, not XR
|
||||
// See https://kubernetes.io/docs/reference/labels-annotations-taints/
|
||||
// for all annotations and their semantic
|
||||
if ann := withoutReservedK8sEntries(cmObserved.GetAnnotations()); len(ann) > 0 {
|
||||
meta.AddAnnotations(cpPatch, withoutReservedK8sEntries(cmObserved.GetAnnotations()))
|
||||
}
|
||||
meta.AddLabels(cpPatch, withoutReservedK8sEntries(cmObserved.GetLabels()))
|
||||
meta.AddLabels(cpPatch, map[string]string{
|
||||
xcrd.LabelKeyClaimName: cmObserved.GetName(),
|
||||
xcrd.LabelKeyClaimNamespace: cmObserved.GetNamespace(),
|
||||
})
|
||||
|
||||
// If our composite resource already exists we want to restore its
|
||||
// original external name (if set) in order to ensure we don't try to
|
||||
// rename anything after the fact.
|
||||
if meta.WasCreated(cpObserved) && en != "" {
|
||||
meta.SetExternalName(cpPatch, en)
|
||||
}
|
||||
|
||||
// We want to propagate the claim's spec to the composite's spec, but
|
||||
// first we must filter out any well-known fields that are unique to
|
||||
// claims. We do this by:
|
||||
// 1. Grabbing a map whose keys represent all well-known claim fields.
|
||||
// 2. Deleting any well-known fields that we want to propagate.
|
||||
// 3. Using the resulting map keys to filter the claim's spec.
|
||||
wellKnownClaimFields := xcrd.CompositeResourceClaimSpecProps()
|
||||
for _, field := range xcrd.PropagateSpecProps {
|
||||
delete(wellKnownClaimFields, field)
|
||||
}
|
||||
|
||||
// CompositionRevision is a special field which needs to be propagated
|
||||
// based on the Update policy. If the policy is `Manual`, we need to
|
||||
// remove CompositionRevisionRef from wellKnownClaimFields, so it
|
||||
// does not get filtered out and is set correctly in composite
|
||||
if cpObserved.GetCompositionUpdatePolicy() != nil && *cpObserved.GetCompositionUpdatePolicy() == xpv1.UpdateManual {
|
||||
delete(wellKnownClaimFields, xcrd.CompositionRevisionRef)
|
||||
}
|
||||
|
||||
claimSpecFilter := xcrd.GetPropFields(wellKnownClaimFields)
|
||||
cpPatch.Object["spec"] = withoutKeys(spec, claimSpecFilter...)
|
||||
|
||||
// Note that we overwrite the entire composite spec above, so we wait
|
||||
// until this point to set the claim reference. We compute the reference
|
||||
// earlier so we can return early if it would not be allowed.
|
||||
cpPatch.SetClaimReference(proposed)
|
||||
|
||||
if meta.WasCreated(cpObserved) {
|
||||
cpPatch.SetName(cpObserved.GetName())
|
||||
return nil
|
||||
}
|
||||
|
||||
// The composite was not found in the informer cache,
|
||||
// or in the apiserver watch cache,
|
||||
// or really does not exist.
|
||||
// If the claim contains the composite reference,
|
||||
// try to use it to set the composite name.
|
||||
// This protects us against stale caches:
|
||||
// 1. If the composite exists, but the cache was not up-to-date,
|
||||
// then its creation is going to fail, and after requeue,
|
||||
// the cache eventually gets up-to-date and everything is good.
|
||||
// 2. If the composite really does not exist, it means that
|
||||
// the claim got bound in one of previous loop,
|
||||
// but something went wrong at composite creation and we requeued.
|
||||
// It is alright to try to use the very same name again.
|
||||
if ref := cmObserved.GetResourceReference(); ref != nil &&
|
||||
ref.APIVersion == cpObserved.GetAPIVersion() && ref.Kind == cpObserved.GetKind() {
|
||||
cpPatch.SetName(ref.Name)
|
||||
return nil
|
||||
}
|
||||
// Otherwise, generate name with a random suffix, hoping it is not already taken
|
||||
cpObserved.SetGenerateName(fmt.Sprintf("%s-", cmObserved.GetName()))
|
||||
// Generated name is likely (but not guaranteed) to be available
|
||||
// when we create the composite resource. If taken,
|
||||
// then we are going to update an existing composite,
|
||||
// hijacking it from another claim. Depending on context/environment
|
||||
// the consequences could be more or less serious.
|
||||
// TODO: decide if we must prevent it.
|
||||
if err := c.GenerateName(ctx, cpObserved); err != nil {
|
||||
return err
|
||||
}
|
||||
cpPatch.SetName(cpObserved.GetName())
|
||||
return nil
|
||||
}
|
||||
|
||||
func withoutReservedK8sEntries(a map[string]string) map[string]string {
|
||||
for k := range a {
|
||||
s := strings.Split(k, "/")
|
||||
if strings.HasSuffix(s[0], "kubernetes.io") || strings.HasSuffix(s[0], "k8s.io") {
|
||||
delete(a, k)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func withoutKeys(in map[string]any, keys ...string) map[string]any {
|
||||
filter := map[string]bool{}
|
||||
for _, k := range keys {
|
||||
filter[k] = true
|
||||
}
|
||||
|
||||
out := map[string]any{}
|
||||
for k, v := range in {
|
||||
if filter[k] {
|
||||
continue
|
||||
}
|
||||
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func onlyKeys(in map[string]any, keys ...string) map[string]any {
|
||||
filter := map[string]bool{}
|
||||
for _, k := range keys {
|
||||
filter[k] = true
|
||||
}
|
||||
|
||||
out := map[string]any{}
|
||||
for k, v := range in {
|
||||
if filter[k] {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// configure the supplied claim patch with fields from the composite.
|
||||
// This includes late-initializing spec values and updating status fields in claim.
|
||||
func configureClaim(_ context.Context, cmObserved *claim.Unstructured, cmPatch *claim.Unstructured, cpObserved *composite.Unstructured, cpPatch *composite.Unstructured) error { //nolint:gocyclo // Only slightly over (10)
|
||||
existing := cmObserved.GetResourceReference()
|
||||
proposed := meta.ReferenceTo(cpPatch, cpPatch.GetObjectKind().GroupVersionKind())
|
||||
equal := cmp.Equal(existing, proposed, cmpopts.IgnoreFields(corev1.ObjectReference{}, "UID"))
|
||||
|
||||
// We refuse to 're-bind' a claim that is already bound to a different
|
||||
// composite resource.
|
||||
if existing != nil && !equal {
|
||||
return errors.New(errBindClaimConflict)
|
||||
}
|
||||
|
||||
cmPatch.SetResourceReference(proposed)
|
||||
|
||||
cmPatch.Object["status"] = map[string]any{}
|
||||
// If existing claim has the status set,
|
||||
// copy the conditions to the patch
|
||||
if s, ok := cmObserved.Object["status"]; ok {
|
||||
fs, ok := s.(map[string]any)
|
||||
if !ok {
|
||||
return errors.Wrap(errors.New(errUnsupportedSrcObject), errMergeClaimStatus)
|
||||
}
|
||||
cmPatch.Object["status"] = onlyKeys(fs, "conditions")
|
||||
}
|
||||
|
||||
// merge from the composite status everything
|
||||
// except conditions and connectionDetails
|
||||
if v := cpObserved.Object["status"]; v != nil {
|
||||
cpObservedStatus, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return errors.Wrap(errors.New(errUnsupportedSrcObject), errMergeClaimStatus)
|
||||
}
|
||||
if err := merge(cmPatch.Object["status"], withoutKeys(cpObservedStatus, xcrd.GetPropFields(xcrd.CompositeResourceStatusProps())...),
|
||||
// Status fields from composite overwrite non-empty fields in claim
|
||||
mergo.WithOverride); err != nil {
|
||||
return errors.Wrap(err, errMergeClaimStatus)
|
||||
}
|
||||
}
|
||||
// Propagate the actual external name back from the composite to the
|
||||
// claim if it's set. The name we're propagating here will may be a name
|
||||
// the XR must enforce (i.e. overriding any requested by the claim) but
|
||||
// will often actually just be propagating back a name that was already
|
||||
// propagated forward from the claim to the XR during the
|
||||
// preceding configure phase.
|
||||
if en := meta.GetExternalName(cpObserved); en != "" {
|
||||
meta.SetExternalName(cmPatch, en)
|
||||
}
|
||||
|
||||
// CompositionRevision is a special field which needs to be propagated
|
||||
// based on the Update policy. If the policy is `Automatic`, we need to
|
||||
// overwrite the claim's value with the composite's which should be the
|
||||
// `currentRevision`
|
||||
if cpObserved.GetCompositionUpdatePolicy() != nil &&
|
||||
*cpObserved.GetCompositionUpdatePolicy() == xpv1.UpdateAutomatic && cpObserved.GetCompositionRevisionReference() != nil {
|
||||
cmPatch.SetCompositionRevisionReference(cpObserved.GetCompositionRevisionReference())
|
||||
}
|
||||
|
||||
// propagate to the claim only the following fields:
|
||||
// - "compositionRef"
|
||||
// - "compositionSelector"
|
||||
// - "compositionUpdatePolicy"
|
||||
// - "compositionRevisionSelector"
|
||||
if v := cpObserved.Object["spec"]; v != nil {
|
||||
cpObservedSpec, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return errors.Wrap(errors.New(errUnsupportedSrcObject), errMergeClaimSpec)
|
||||
}
|
||||
if err := merge(cmPatch.Object["spec"], onlyKeys(cpObservedSpec, xcrd.PropagateSpecProps...)); err != nil {
|
||||
return errors.Wrap(err, errMergeClaimSpec)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// merge a src map into dst map
|
||||
func merge(dst, src any, opts ...func(*mergo.Config)) error {
|
||||
if dst == nil || src == nil {
|
||||
// Nothing available to merge if dst or src are nil.
|
||||
// This can occur early on in reconciliation when the
|
||||
// status subresource has not been set yet.
|
||||
return nil
|
||||
}
|
||||
|
||||
dstMap, ok := dst.(map[string]any)
|
||||
if !ok {
|
||||
return errors.New(errUnsupportedDstObject)
|
||||
}
|
||||
|
||||
srcMap, ok := src.(map[string]any)
|
||||
if !ok {
|
||||
return errors.New(errUnsupportedSrcObject)
|
||||
}
|
||||
|
||||
return mergo.Merge(&dstMap, srcMap, opts...)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -3,11 +3,27 @@ package claim
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Error strings.
|
||||
const (
|
||||
errGetSecret = "cannot get composite resource's connection secret"
|
||||
errSecretConflict = "cannot establish control of existing connection secret"
|
||||
errCreateOrUpdateSecret = "cannot create or update connection secret"
|
||||
)
|
||||
|
||||
// NopConnectionUnpublisher is a ConnectionUnpublisher that does nothing.
|
||||
type NopConnectionUnpublisher struct{}
|
||||
|
||||
|
@ -69,3 +85,61 @@ func (s soClaim) GetWriteConnectionSecretToReference() *xpv1.SecretReference {
|
|||
// just to satisfy resource.ConnectionSecretOwner interface.
|
||||
return nil
|
||||
}
|
||||
|
||||
// An APIConnectionPropagator propagates connection details by reading
|
||||
// them from and writing them to a Kubernetes API server.
|
||||
type APIConnectionPropagator struct {
|
||||
client resource.ClientApplicator
|
||||
}
|
||||
|
||||
// NewAPIConnectionPropagator returns a new APIConnectionPropagator.
|
||||
func NewAPIConnectionPropagator(c client.Client) *APIConnectionPropagator {
|
||||
return &APIConnectionPropagator{
|
||||
client: resource.ClientApplicator{Client: c, Applicator: resource.NewAPIUpdatingApplicator(c)},
|
||||
}
|
||||
}
|
||||
|
||||
// PropagateConnection details from the supplied resource.
|
||||
func (a *APIConnectionPropagator) PropagateConnection(ctx context.Context, to resource.LocalConnectionSecretOwner, from resource.ConnectionSecretOwner) (bool, error) {
|
||||
// Either from does not expose a connection secret, or to does not want one.
|
||||
if from.GetWriteConnectionSecretToReference() == nil || to.GetWriteConnectionSecretToReference() == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
n := types.NamespacedName{
|
||||
Namespace: from.GetWriteConnectionSecretToReference().Namespace,
|
||||
Name: from.GetWriteConnectionSecretToReference().Name,
|
||||
}
|
||||
fs := &corev1.Secret{}
|
||||
if err := a.client.Get(ctx, n, fs); err != nil {
|
||||
return false, errors.Wrap(err, errGetSecret)
|
||||
}
|
||||
|
||||
// Make sure 'from' is the controller of the connection secret it references
|
||||
// before we propagate it. This ensures a resource cannot use Crossplane to
|
||||
// circumvent RBAC by propagating a secret it does not own.
|
||||
if c := metav1.GetControllerOf(fs); c == nil || c.UID != from.GetUID() {
|
||||
return false, errors.New(errSecretConflict)
|
||||
}
|
||||
|
||||
ts := resource.LocalConnectionSecretFor(to, to.GetObjectKind().GroupVersionKind())
|
||||
ts.Data = fs.Data
|
||||
|
||||
err := a.client.Apply(ctx, ts,
|
||||
resource.ConnectionSecretMustBeControllableBy(to.GetUID()),
|
||||
resource.AllowUpdateIf(func(current, desired runtime.Object) bool {
|
||||
// We consider the update to be a no-op and don't allow it if the
|
||||
// current and existing secret data are identical.
|
||||
return !cmp.Equal(current.(*corev1.Secret).Data, desired.(*corev1.Secret).Data, cmpopts.EquateEmpty())
|
||||
}),
|
||||
)
|
||||
if resource.IsNotAllowed(err) {
|
||||
// The update was not allowed because it was a no-op.
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, errCreateOrUpdateSecret)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
Copyright 2024 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 claim
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"dario.cat/mergo"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
errUnsupportedDstObject = "destination object was not valid object"
|
||||
errUnsupportedSrcObject = "source object was not valid object"
|
||||
)
|
||||
|
||||
func withoutReservedK8sEntries(a map[string]string) map[string]string {
|
||||
for k := range a {
|
||||
s := strings.Split(k, "/")
|
||||
if strings.HasSuffix(s[0], "kubernetes.io") || strings.HasSuffix(s[0], "k8s.io") {
|
||||
delete(a, k)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func withoutKeys(in map[string]any, keys ...string) map[string]any {
|
||||
filter := map[string]bool{}
|
||||
for _, k := range keys {
|
||||
filter[k] = true
|
||||
}
|
||||
|
||||
out := map[string]any{}
|
||||
for k, v := range in {
|
||||
if filter[k] {
|
||||
continue
|
||||
}
|
||||
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type mergeConfig struct {
|
||||
mergeOptions []func(*mergo.Config)
|
||||
srcfilter []string
|
||||
}
|
||||
|
||||
// withMergeOptions allows custom mergo.Config options
|
||||
func withMergeOptions(opts ...func(*mergo.Config)) func(*mergeConfig) {
|
||||
return func(config *mergeConfig) {
|
||||
config.mergeOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
// withSrcFilter filters supplied keys from src map before merging
|
||||
func withSrcFilter(keys ...string) func(*mergeConfig) {
|
||||
return func(config *mergeConfig) {
|
||||
config.srcfilter = keys
|
||||
}
|
||||
}
|
||||
|
||||
// merge a src map into dst map
|
||||
func merge(dst, src any, opts ...func(*mergeConfig)) error {
|
||||
if dst == nil || src == nil {
|
||||
// Nothing available to merge if dst or src are nil.
|
||||
// This can occur early on in reconciliation when the
|
||||
// status subresource has not been set yet.
|
||||
return nil
|
||||
}
|
||||
|
||||
config := &mergeConfig{}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(config)
|
||||
}
|
||||
|
||||
dstMap, ok := dst.(map[string]any)
|
||||
if !ok {
|
||||
return errors.New(errUnsupportedDstObject)
|
||||
}
|
||||
|
||||
srcMap, ok := src.(map[string]any)
|
||||
if !ok {
|
||||
return errors.New(errUnsupportedSrcObject)
|
||||
}
|
||||
|
||||
return mergo.Merge(&dstMap, withoutKeys(srcMap, config.srcfilter...), config.mergeOptions...)
|
||||
}
|
|
@ -25,7 +25,6 @@ import (
|
|||
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/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
|
@ -48,49 +47,35 @@ import (
|
|||
const (
|
||||
finalizer = "finalizer.apiextensions.crossplane.io"
|
||||
reconcileTimeout = 1 * time.Minute
|
||||
|
||||
// fieldOwnerName used by all claim controllers when patch/update requests
|
||||
// are sent to API server.
|
||||
// Upstream k8s uses a unique field owner name per controller.
|
||||
// During the review, we moved towards the use of a single name
|
||||
// for all claim controllers, see this thread for more details:
|
||||
// https://github.com/crossplane/crossplane/pull/4896#discussion_r1392023074
|
||||
fieldOwnerName = "apiextensions.crossplane.io/claim"
|
||||
)
|
||||
|
||||
// Error strings.
|
||||
const (
|
||||
errGetClaim = "cannot get composite resource claim"
|
||||
errGetComposite = "cannot get referenced composite resource"
|
||||
errDeleteComposite = "cannot delete referenced composite resource"
|
||||
errDeleteUnbound = "refusing to delete composite resource that is not bound to this claim"
|
||||
errDeleteCDs = "cannot delete connection details"
|
||||
errRemoveFinalizer = "cannot remove composite resource claim finalizer"
|
||||
errConfigureComposite = "cannot configure composite resource"
|
||||
errPatchComposite = "cannot patch composite resource"
|
||||
errFixFieldOwnershipComposite = "cannot fix field ownerships on composite resource"
|
||||
errFixFieldOwnershipClaim = "cannot fix field ownerships on claim resource"
|
||||
errConfigureClaim = "cannot configure composite resource claim"
|
||||
errPropagateCDs = "cannot propagate connection details from composite"
|
||||
errGetClaim = "cannot get claim"
|
||||
errGetComposite = "cannot get bound composite resource"
|
||||
errDeleteComposite = "cannot delete bound composite resource"
|
||||
errDeleteCDs = "cannot delete connection details"
|
||||
errRemoveFinalizer = "cannot remove finalizer from claim"
|
||||
errAddFinalizer = "cannot add finalizer to claim"
|
||||
errUpgradeManagedFields = "cannot upgrade composite resource's managed fields from client-side to server-side apply"
|
||||
errSync = "cannot bind and sync claim with composite resource"
|
||||
errPropagateCDs = "cannot propagate connection details from composite resource"
|
||||
errUpdateClaimStatus = "cannot update claim status"
|
||||
|
||||
errUpdateClaimStatus = "cannot update composite resource claim status"
|
||||
|
||||
reconcilePausedMsg = "Reconciliation (including deletion) is paused via the pause annotation"
|
||||
errFmtUnbound = "refusing to operate on composite resource %q that is not bound to this claim: bound to claim %q"
|
||||
)
|
||||
|
||||
const reconcilePausedMsg = "Reconciliation (including deletion) is paused via the pause annotation"
|
||||
|
||||
// Event reasons.
|
||||
const (
|
||||
reasonBind event.Reason = "BindCompositeResource"
|
||||
reasonDelete event.Reason = "DeleteCompositeResource"
|
||||
reasonCompositeConfigure event.Reason = "ConfigureCompositeResource"
|
||||
reasonClaimConfigure event.Reason = "ConfigureClaim"
|
||||
reasonPropagate event.Reason = "PropagateConnectionSecret"
|
||||
reasonPaused event.Reason = "ReconciliationPaused"
|
||||
)
|
||||
|
||||
var (
|
||||
// field manager names used by previous Crossplane versions
|
||||
csaManagerNames = []string{"Go-http-client", "crossplane"}
|
||||
reasonBind event.Reason = "BindCompositeResource"
|
||||
reasonDelete event.Reason = "DeleteCompositeResource"
|
||||
reasonCompositeConfigure event.Reason = "ConfigureCompositeResource"
|
||||
reasonClaimConfigure event.Reason = "ConfigureClaim"
|
||||
reasonClaimSelectDefaults event.Reason = "SelectClaimDefaults"
|
||||
reasonPropagate event.Reason = "PropagateConnectionSecret"
|
||||
reasonPaused event.Reason = "ReconciliationPaused"
|
||||
)
|
||||
|
||||
// ControllerName returns the recommended name for controllers that use this
|
||||
|
@ -99,8 +84,28 @@ func ControllerName(name string) string {
|
|||
return "claim/" + name
|
||||
}
|
||||
|
||||
type compositeConfiguratorFn func(ctx context.Context, cm *claim.Unstructured, cp, desiredCp *composite.Unstructured) error
|
||||
type claimConfiguratorFn func(ctx context.Context, cm, desiredCm *claim.Unstructured, cp, desiredCp *composite.Unstructured) error
|
||||
// A ManagedFieldsUpgrader upgrades an objects managed fields from client-side
|
||||
// apply to server-side apply. This is necessary when an object was previously
|
||||
// managed using client-side apply, but should now be managed using server-side
|
||||
// apply. See https://github.com/kubernetes/kubernetes/issues/99003 for details.
|
||||
type ManagedFieldsUpgrader interface {
|
||||
Upgrade(ctx context.Context, obj client.Object, ssaManager string, csaManagers ...string) error
|
||||
}
|
||||
|
||||
// A CompositeSyncer binds and syncs the supplied claim with the supplied
|
||||
// composite resource (XR).
|
||||
type CompositeSyncer interface {
|
||||
Sync(ctx context.Context, cm *claim.Unstructured, xr *composite.Unstructured) error
|
||||
}
|
||||
|
||||
// A CompositeSyncerFn binds and syncs the supplied claim with the supplied
|
||||
// composite resource (XR).
|
||||
type CompositeSyncerFn func(ctx context.Context, cm *claim.Unstructured, xr *composite.Unstructured) error
|
||||
|
||||
// Sync the supplied claim with the supplied composite resource..
|
||||
func (fn CompositeSyncerFn) Sync(ctx context.Context, cm *claim.Unstructured, xr *composite.Unstructured) error {
|
||||
return fn(ctx, cm, xr)
|
||||
}
|
||||
|
||||
// A ConnectionPropagator is responsible for propagating information required to
|
||||
// connect to a resource.
|
||||
|
@ -168,20 +173,22 @@ func (fn DefaultsSelectorFn) SelectDefaults(ctx context.Context, cm resource.Com
|
|||
return fn(ctx, cm)
|
||||
}
|
||||
|
||||
// A Reconciler reconciles composite resource claims by creating exactly one kind of
|
||||
// concrete composite resource. Each composite resource claim kind should create an instance
|
||||
// of this controller for each composite resource kind they can bind to, using
|
||||
// watch predicates to ensure each controller is responsible for exactly one
|
||||
// type of resource class provisioner. Each controller must watch its subset of
|
||||
// composite resource claims and any composite resources they control.
|
||||
// A Reconciler reconciles claims by creating exactly one kind of composite
|
||||
// resource (XR). Each claim kind should create an instance of this controller
|
||||
// for each XR kind they can bind to. Each controller must watch its subset of
|
||||
// claims and any XRs they bind to.
|
||||
type Reconciler struct {
|
||||
client client.Client
|
||||
newClaim func() *claim.Unstructured
|
||||
newComposite func() *composite.Unstructured
|
||||
client client.Client
|
||||
|
||||
gvkClaim schema.GroupVersionKind
|
||||
gvkXR schema.GroupVersionKind
|
||||
|
||||
managedFields ManagedFieldsUpgrader
|
||||
|
||||
// The below structs embed the set of interfaces used to implement the
|
||||
// composite resource claim reconciler. We do this primarily for readability, so that
|
||||
// the reconciler logic reads r.composite.Create(), r.claim.Finalize(), etc.
|
||||
// composite resource claim reconciler. We do this primarily for
|
||||
// readability, so that the reconciler logic reads r.composite.Sync(),
|
||||
// r.claim.Finalize(), etc.
|
||||
composite crComposite
|
||||
claim crClaim
|
||||
|
||||
|
@ -191,21 +198,20 @@ type Reconciler struct {
|
|||
}
|
||||
|
||||
type crComposite struct {
|
||||
Configure compositeConfiguratorFn
|
||||
CompositeSyncer
|
||||
ConnectionPropagator
|
||||
}
|
||||
|
||||
func defaultCRComposite(c client.Client) crComposite {
|
||||
conf := &apiCompositeConfigurator{NameGenerator: names.NewNameGenerator(c)}
|
||||
return crComposite{
|
||||
Configure: conf.Configure,
|
||||
// TODO(negz): Use CSA syncer unless feature flag is enabled.
|
||||
CompositeSyncer: NewServerSideCompositeSyncer(c, names.NewNameGenerator(c)),
|
||||
ConnectionPropagator: NewAPIConnectionPropagator(c),
|
||||
}
|
||||
}
|
||||
|
||||
type crClaim struct {
|
||||
resource.Finalizer
|
||||
Configure claimConfiguratorFn
|
||||
ConnectionUnpublisher
|
||||
}
|
||||
|
||||
|
@ -213,34 +219,34 @@ func defaultCRClaim(c client.Client) crClaim {
|
|||
return crClaim{
|
||||
Finalizer: resource.NewAPIFinalizer(c, finalizer),
|
||||
ConnectionUnpublisher: NewNopConnectionUnpublisher(),
|
||||
Configure: configureClaim,
|
||||
}
|
||||
}
|
||||
|
||||
// A ReconcilerOption configures a Reconciler.
|
||||
type ReconcilerOption func(*Reconciler)
|
||||
|
||||
// WithClient specifies how the Reconciler should interact with the
|
||||
// Kubernetes API.
|
||||
// WithClient specifies how the Reconciler should interact with the Kubernetes
|
||||
// API.
|
||||
func WithClient(c client.Client) ReconcilerOption {
|
||||
return func(r *Reconciler) {
|
||||
r.client = c
|
||||
}
|
||||
}
|
||||
|
||||
// withCompositeConfigurator specifies how the Reconciler should configure the bound
|
||||
// composite resource.
|
||||
func withCompositeConfigurator(cf compositeConfiguratorFn) ReconcilerOption {
|
||||
// WithManagedFieldsUpgrader specifies how the Reconciler should upgrade claim
|
||||
// and composite resource (XR) managed fields from client-side apply to
|
||||
// server-side apply.
|
||||
func WithManagedFieldsUpgrader(u ManagedFieldsUpgrader) ReconcilerOption {
|
||||
return func(r *Reconciler) {
|
||||
r.composite.Configure = cf
|
||||
r.managedFields = u
|
||||
}
|
||||
}
|
||||
|
||||
// WithClaimConfigurator specifies how the Reconciler should configure the bound
|
||||
// claim resource.
|
||||
func withClaimConfigurator(cf claimConfiguratorFn) ReconcilerOption {
|
||||
// WithCompositeSyncer specifies how the Reconciler should sync claims with
|
||||
// composite resources (XRs).
|
||||
func WithCompositeSyncer(cs CompositeSyncer) ReconcilerOption {
|
||||
return func(r *Reconciler) {
|
||||
r.claim.Configure = cf
|
||||
r.composite.CompositeSyncer = cs
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -301,17 +307,14 @@ func WithPollInterval(after time.Duration) ReconcilerOption {
|
|||
func NewReconciler(m manager.Manager, of resource.CompositeClaimKind, with resource.CompositeKind, o ...ReconcilerOption) *Reconciler {
|
||||
c := unstructured.NewClient(m.GetClient())
|
||||
r := &Reconciler{
|
||||
client: c,
|
||||
newClaim: func() *claim.Unstructured {
|
||||
return claim.New(claim.WithGroupVersionKind(schema.GroupVersionKind(of)))
|
||||
},
|
||||
newComposite: func() *composite.Unstructured {
|
||||
return composite.New(composite.WithGroupVersionKind(schema.GroupVersionKind(with)))
|
||||
},
|
||||
composite: defaultCRComposite(c),
|
||||
claim: defaultCRClaim(c),
|
||||
log: logging.NewNopLogger(),
|
||||
record: event.NewNopRecorder(),
|
||||
client: c,
|
||||
gvkClaim: schema.GroupVersionKind(of),
|
||||
gvkXR: schema.GroupVersionKind(with),
|
||||
managedFields: NewPatchingManagedFieldsUpgrader(c), // TODO(negz): Use Nop unless flag is enabled.
|
||||
composite: defaultCRComposite(c),
|
||||
claim: defaultCRClaim(c),
|
||||
log: logging.NewNopLogger(),
|
||||
record: event.NewNopRecorder(),
|
||||
}
|
||||
|
||||
for _, ro := range o {
|
||||
|
@ -323,17 +326,16 @@ func NewReconciler(m manager.Manager, of resource.CompositeClaimKind, with resou
|
|||
|
||||
// Reconcile a composite resource claim with a concrete composite resource.
|
||||
func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { //nolint:gocyclo // Complexity is tough to avoid here.
|
||||
|
||||
log := r.log.WithValues("request", req)
|
||||
log.Debug("Reconciling")
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, reconcileTimeout)
|
||||
defer cancel()
|
||||
|
||||
cm := r.newClaim()
|
||||
cm := claim.New(claim.WithGroupVersionKind(r.gvkClaim))
|
||||
if err := r.client.Get(ctx, req.NamespacedName, cm); err != nil {
|
||||
// There's no need to requeue if we no longer exist. Otherwise
|
||||
// we'll be requeued implicitly because we return an error.
|
||||
// There's no need to requeue if we no longer exist. Otherwise we'll be
|
||||
// requeued implicitly because we return an error.
|
||||
log.Debug(errGetClaim, "error", err)
|
||||
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetClaim)
|
||||
}
|
||||
|
@ -345,84 +347,88 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
|
|||
"external-name", meta.GetExternalName(cm),
|
||||
)
|
||||
|
||||
// Previous versions of crossplane did not use server-side apply for updating claims.
|
||||
// We need to fix the manager name so that future reconciliations do not
|
||||
// create shared field ownership between old and actual managers
|
||||
if ownershipFixed := r.maybeFixFieldOwnership(cm.GetUnstructured(), true, fieldOwnerName, csaManagerNames...); ownershipFixed {
|
||||
if err := r.client.Update(ctx, cm); err != nil {
|
||||
if kerrors.IsConflict(err) {
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
record.Event(cm, event.Warning(reasonClaimConfigure, err))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(err, errFixFieldOwnershipClaim)
|
||||
}
|
||||
}
|
||||
|
||||
// Check the pause annotation and return if it has the value "true"
|
||||
// after logging, publishing an event and updating the SYNC status condition
|
||||
// Check the pause annotation and return if it has the value "true" after
|
||||
// logging, publishing an event and updating the Synced status condition.
|
||||
if meta.IsPaused(cm) {
|
||||
r.record.Event(cm, event.Normal(reasonPaused, reconcilePausedMsg))
|
||||
cm.SetConditions(xpv1.ReconcilePaused().WithMessage(reconcilePausedMsg))
|
||||
// If the pause annotation is removed, we will have a chance to reconcile again and resume
|
||||
// and if status update fails, we will reconcile again to retry to update the status
|
||||
return reconcile.Result{}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
// If the pause annotation is removed, we will have a chance to
|
||||
// reconcile again and resume and if status update fails, we will
|
||||
// reconcile again to retry to update the status.
|
||||
return reconcile.Result{}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
cp := r.newComposite()
|
||||
xr := composite.New(composite.WithGroupVersionKind(r.gvkXR))
|
||||
if ref := cm.GetResourceReference(); ref != nil {
|
||||
record = record.WithAnnotations("composite-name", cm.GetResourceReference().Name)
|
||||
log = log.WithValues("composite-name", cm.GetResourceReference().Name)
|
||||
|
||||
if err := r.client.Get(ctx, meta.NamespacedNameOf(ref), cp); resource.IgnoreNotFound(err) != nil {
|
||||
if err := r.client.Get(ctx, meta.NamespacedNameOf(ref), xr); resource.IgnoreNotFound(err) != nil {
|
||||
err = errors.Wrap(err, errGetComposite)
|
||||
record.Event(cm, event.Warning(reasonBind, err))
|
||||
cm.SetConditions(xpv1.ReconcileError(err))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// Return early if the claim references an XR that doesn't reference it.
|
||||
//
|
||||
// We don't requeue in this situation because the claim will need human
|
||||
// intervention before we can proceed (e.g. fixing the ref), and we'll be
|
||||
// queued implicitly when the claim is edited.
|
||||
//
|
||||
// A claim might be able to delete an XR it's not bound to, as long as the
|
||||
// XR is bindable but not yet bound. This is because we only check the claim
|
||||
// ref if the XR has one - this allows us to bind unbound claims. Given that
|
||||
// the claim could bind this XR, then be deleted and in turn delete the XR
|
||||
// this is not an issue.
|
||||
if ref := xr.GetClaimReference(); meta.WasCreated(xr) && ref != nil && !cmp.Equal(cm.GetReference(), ref) {
|
||||
err := errors.Errorf(errFmtUnbound, xr.GetName(), ref.Name)
|
||||
record.Event(cm, event.Warning(reasonBind, err))
|
||||
cm.SetConditions(xpv1.ReconcileError(err))
|
||||
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
// TODO(negz): Remove this call to Upgrade once no supported version of
|
||||
// Crossplane uses client-side apply to sync claims with XRs. We only need
|
||||
// to upgrade field managers if _this controller_ might have applied the XR
|
||||
// before using the default client-side apply field manager "crossplane",
|
||||
// but now wants to use server-side apply instead.
|
||||
if err := r.managedFields.Upgrade(ctx, xr, FieldOwnerXR, "crossplane"); err != nil {
|
||||
if kerrors.IsConflict(err) {
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
err = errors.Wrap(err, errUpgradeManagedFields)
|
||||
record.Event(cm, event.Warning(reasonBind, err))
|
||||
cm.SetConditions(xpv1.ReconcileError(err))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
if meta.WasDeleted(cm) {
|
||||
log = log.WithValues("deletion-timestamp", cm.GetDeletionTimestamp())
|
||||
|
||||
cm.SetConditions(xpv1.Deleting())
|
||||
if meta.WasCreated(cp) {
|
||||
if meta.WasCreated(xr) {
|
||||
requiresForegroundDeletion := false
|
||||
if cdp := cm.GetCompositeDeletePolicy(); cdp != nil && *cdp == xpv1.CompositeDeleteForeground {
|
||||
requiresForegroundDeletion = true
|
||||
}
|
||||
if meta.WasDeleted(cp) {
|
||||
if requiresForegroundDeletion {
|
||||
log.Debug("Waiting for the Composite to finish deleting (foreground deletion)")
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
if meta.WasDeleted(xr) && requiresForegroundDeletion {
|
||||
log.Debug("Waiting for the XR to finish deleting (foreground deletion)")
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
ref := cp.GetClaimReference()
|
||||
want := cm.GetReference()
|
||||
if !cmp.Equal(want, ref) {
|
||||
// We don't requeue (or return an error, which
|
||||
// would requeue) in this situation because the
|
||||
// claim will need human intervention before we
|
||||
// can proceed (e.g. fixing the ref), and we'll
|
||||
// be queued implicitly when the claim is
|
||||
// edited.
|
||||
err := errors.New(errDeleteUnbound)
|
||||
record.Event(cm, event.Warning(reasonDelete, err))
|
||||
cm.SetConditions(xpv1.ReconcileError(err))
|
||||
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
do := &client.DeleteOptions{}
|
||||
if requiresForegroundDeletion {
|
||||
client.PropagationPolicy(metav1.DeletePropagationForeground).ApplyToDelete(do)
|
||||
}
|
||||
if err := r.client.Delete(ctx, cp, do); resource.IgnoreNotFound(err) != nil {
|
||||
if err := r.client.Delete(ctx, xr, do); resource.IgnoreNotFound(err) != nil {
|
||||
err = errors.Wrap(err, errDeleteComposite)
|
||||
record.Event(cm, event.Warning(reasonDelete, err))
|
||||
cm.SetConditions(xpv1.ReconcileError(err))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
if requiresForegroundDeletion {
|
||||
log.Debug("Requeue to wait for the Composite to finish deleting (foreground deletion)")
|
||||
log.Debug("Waiting for the XR to finish deleting (foreground deletion)")
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
}
|
||||
|
@ -434,7 +440,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
|
|||
err = errors.Wrap(err, errDeleteCDs)
|
||||
record.Event(cm, event.Warning(reasonDelete, err))
|
||||
cm.SetConditions(xpv1.ReconcileError(err))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
record.Event(cm, event.Normal(reasonDelete, "Successfully deleted composite resource"))
|
||||
|
@ -443,125 +449,57 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
|
|||
err = errors.Wrap(err, errRemoveFinalizer)
|
||||
record.Event(cm, event.Warning(reasonDelete, err))
|
||||
cm.SetConditions(xpv1.ReconcileError(err))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
log.Debug("Successfully deleted composite resource claim")
|
||||
log.Debug("Successfully deleted claim")
|
||||
cm.SetConditions(xpv1.ReconcileSuccess())
|
||||
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
// If composite exists, i.e. got created in the previous iteration,
|
||||
// it might be needed to fix field ownerships,
|
||||
// so that the claim controller becomes an exclusive owner
|
||||
// of the fields mirrored from the claim to the composite
|
||||
if meta.WasCreated(cp) {
|
||||
if ownershipFixed := r.maybeFixFieldOwnership(cp.GetUnstructured(), false, fieldOwnerName, csaManagerNames...); ownershipFixed {
|
||||
if err := r.client.Update(ctx, cp); err != nil {
|
||||
if kerrors.IsConflict(err) {
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
record.Event(cp, event.Warning(reasonCompositeConfigure, err))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(err, errFixFieldOwnershipComposite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create object that is going to hold the full claim patch eventually
|
||||
cmPatch := r.newClaim()
|
||||
cmPatch.SetName(cm.GetName())
|
||||
cmPatch.SetNamespace(cm.GetNamespace())
|
||||
|
||||
meta.AddFinalizer(cmPatch, finalizer)
|
||||
|
||||
// create object that is going to hold the full composite patch eventually
|
||||
cpPatch := r.newComposite()
|
||||
if err := r.composite.Configure(ctx, cm, cp, cpPatch); err != nil {
|
||||
if err := r.claim.AddFinalizer(ctx, cm); err != nil {
|
||||
if kerrors.IsConflict(err) {
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
err = errors.Wrap(err, errConfigureComposite)
|
||||
record.Event(cm, event.Warning(reasonCompositeConfigure, err))
|
||||
err = errors.Wrap(err, errAddFinalizer)
|
||||
record.Event(cm, event.Warning(reasonBind, err))
|
||||
cm.SetConditions(xpv1.ReconcileError(err))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
// We'll know our composite resource's name at this point because it was
|
||||
// set by the above configure step.
|
||||
record = record.WithAnnotations("composite-name", cpPatch.GetName())
|
||||
log = log.WithValues("composite-name", cpPatch.GetName())
|
||||
|
||||
if err := r.claim.Configure(ctx, cm, cmPatch, cp, cpPatch); err != nil {
|
||||
// Create (if necessary), bind, and sync an XR with the claim.
|
||||
if err := r.composite.Sync(ctx, cm, xr); err != nil {
|
||||
if kerrors.IsConflict(err) {
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
err = errors.Wrap(err, errConfigureClaim)
|
||||
record.Event(cm, event.Warning(reasonClaimConfigure, err))
|
||||
err = errors.Wrap(err, errSync)
|
||||
record.Event(cm, event.Warning(reasonBind, err))
|
||||
cm.SetConditions(xpv1.ReconcileError(err))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
// The following patch operation is going to override the status part of the claim patch
|
||||
// with the status of the actual claim version.
|
||||
// given that the status update come later, we need to preserve it temporarily.
|
||||
desiredClaimStatus := cmPatch.Object["status"]
|
||||
log.Debug("Patching claim", "patch", cmPatch.Object)
|
||||
err := r.client.Patch(ctx, cmPatch, client.Apply, client.ForceOwnership, client.FieldOwner(fieldOwnerName))
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
if cmPatch.GetResourceVersion() == cm.GetResourceVersion() {
|
||||
log.Debug("Patching claim was no-op")
|
||||
}
|
||||
cm = cmPatch
|
||||
// Restore the status calculated in this round
|
||||
// and use cmPatch until the end of the reconciliation
|
||||
// for setting claim conditions
|
||||
cm.Object["status"] = desiredClaimStatus
|
||||
|
||||
// If composite did not exist at the beginning of the loop, we want to create it
|
||||
// but there is a probability (very unlikely) that the generated name
|
||||
// gets taken before we submit the request. In that case, we are going to
|
||||
// update a composite, hijacking it from another claim.
|
||||
// Reported in https://github.com/crossplane/crossplane/issues/5104
|
||||
// TODO: Investigate if we need to prevent it.
|
||||
log.Debug("Patching composite", "patch", cpPatch.Object)
|
||||
if err = r.client.Patch(ctx, cpPatch, client.Apply, client.ForceOwnership, client.FieldOwner(fieldOwnerName)); err != nil {
|
||||
if kerrors.IsConflict(err) {
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
err = errors.Wrap(err, errPatchComposite)
|
||||
record.Event(cm, event.Warning(reasonCompositeConfigure, err))
|
||||
cm.SetConditions(xpv1.ReconcileError(err))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
if cpPatch.GetResourceVersion() == cp.GetResourceVersion() {
|
||||
log.Debug("Composite patch was no-op.")
|
||||
} else {
|
||||
record.Event(cm, event.Normal(reasonCompositeConfigure, "Successfully patched composite resource"))
|
||||
}
|
||||
cp = cpPatch
|
||||
|
||||
cm.SetConditions(xpv1.ReconcileSuccess())
|
||||
|
||||
if !resource.IsConditionTrue(cp.GetCondition(xpv1.TypeReady)) {
|
||||
if !resource.IsConditionTrue(xr.GetCondition(xpv1.TypeReady)) {
|
||||
record.Event(cm, event.Normal(reasonBind, "Composite resource is not yet ready"))
|
||||
|
||||
// We should be watching the composite resource and will have a
|
||||
// request queued if it changes, so no need to requeue.
|
||||
cm.SetConditions(Waiting())
|
||||
return reconcile.Result{}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
return reconcile.Result{}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
record.Event(cm, event.Normal(reasonBind, "Composite resource is ready"))
|
||||
// TODO(negz): Don't emit this event every time we reconcile. Perhaps emit
|
||||
// if if the XR doesn't reference this claim before calling sync, but does
|
||||
// after.
|
||||
record.Event(cm, event.Normal(reasonBind, "Successfully bound composite resource"))
|
||||
|
||||
propagated, err := r.composite.PropagateConnection(ctx, cm, cp)
|
||||
propagated, err := r.composite.PropagateConnection(ctx, cm, xr)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, errPropagateCDs)
|
||||
record.Event(cm, event.Warning(reasonPropagate, err))
|
||||
cm.SetConditions(xpv1.ReconcileError(err))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
if propagated {
|
||||
cm.SetConnectionDetailsLastPublishedTime(&metav1.Time{Time: time.Now()})
|
||||
|
@ -571,46 +509,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco
|
|||
// We have a watch on both the claim and its composite, so there's no
|
||||
// need to requeue here.
|
||||
cm.SetConditions(xpv1.Available())
|
||||
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, cm, client.FieldOwner(fieldOwnerName)), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
// For the background, check https://github.com/kubernetes/kubernetes/issues/99003
|
||||
// Managed fields owner key is a pair (manager name, used operation).
|
||||
// Previous versions of Crossplane create a composite using patching applicator.
|
||||
// Even if the server-side apply is not used, api server derives manager name
|
||||
// from the submitted user agent (see net/http/request.go).
|
||||
// After Crossplane update, we need to replace the ownership so that
|
||||
// field removals can be propagated properly.
|
||||
// In order to fix that, we need to manually change operation to "Apply",
|
||||
// and the manager name, before the first composite patch is sent to k8s api server.
|
||||
// Returns true if the ownership was fixed.
|
||||
// TODO: this code can be removed once Crossplane v1.13 is not longer supported
|
||||
func (r *Reconciler) maybeFixFieldOwnership(obj *kunstructured.Unstructured, fixSubresource bool, ssaManagerName string, csaManagerNames ...string) bool {
|
||||
mfs := obj.GetManagedFields()
|
||||
umfs := make([]metav1.ManagedFieldsEntry, len(mfs))
|
||||
copy(umfs, mfs)
|
||||
fixed := false
|
||||
for j := range csaManagerNames {
|
||||
for i := range umfs {
|
||||
if umfs[i].Manager == ssaManagerName {
|
||||
return false
|
||||
}
|
||||
if umfs[i].Subresource != "" && !fixSubresource {
|
||||
continue
|
||||
}
|
||||
|
||||
if umfs[i].Manager == csaManagerNames[j] && umfs[i].Operation == metav1.ManagedFieldsOperationUpdate {
|
||||
umfs[i].Operation = metav1.ManagedFieldsOperationApply
|
||||
umfs[i].Manager = ssaManagerName
|
||||
fixed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if fixed {
|
||||
obj.SetManagedFields(umfs)
|
||||
}
|
||||
return fixed
|
||||
|
||||
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
// Waiting returns a condition that indicates the composite resource claim is
|
||||
|
@ -621,6 +520,6 @@ func Waiting() xpv1.Condition {
|
|||
Status: corev1.ConditionFalse,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: xpv1.ConditionReason("Waiting"),
|
||||
Message: "Composite resource claim is waiting for composite resource to become Ready",
|
||||
Message: "Claim is waiting for composite resource to become Ready",
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
Copyright 2024 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 claim
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"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/meta"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/claim"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
|
||||
|
||||
"github.com/crossplane/crossplane/internal/names"
|
||||
"github.com/crossplane/crossplane/internal/xcrd"
|
||||
)
|
||||
|
||||
const (
|
||||
errUpdateClaim = "cannot update claim"
|
||||
errUnsupportedClaimSpec = "claim spec was not an object"
|
||||
errBindCompositeConflict = "cannot bind composite resource that references a different claim"
|
||||
errGenerateName = "cannot generate a name for composite resource"
|
||||
errApplyComposite = "cannot apply composite resource"
|
||||
|
||||
errMergeClaimSpec = "unable to merge claim spec"
|
||||
errMergeClaimStatus = "unable to merge claim status"
|
||||
)
|
||||
|
||||
// A ClientSideCompositeSyncer binds and syncs a claim with a composite resource
|
||||
// (XR). It uses client-side apply to update the claim and the composite.
|
||||
type ClientSideCompositeSyncer struct {
|
||||
client resource.ClientApplicator
|
||||
names names.NameGenerator
|
||||
}
|
||||
|
||||
// NewClientSideCompositeSyncer returns a CompositeSyncer that uses client-side
|
||||
// apply to sync a claim with a composite resource.
|
||||
func NewClientSideCompositeSyncer(c client.Client, ng names.NameGenerator) *ClientSideCompositeSyncer {
|
||||
return &ClientSideCompositeSyncer{
|
||||
client: resource.ClientApplicator{
|
||||
Client: c,
|
||||
Applicator: resource.NewAPIPatchingApplicator(c),
|
||||
},
|
||||
names: ng,
|
||||
}
|
||||
}
|
||||
|
||||
// Sync the supplied claim with the supplied composite resource (XR). Syncing
|
||||
// may involve creating and binding the XR.
|
||||
func (s *ClientSideCompositeSyncer) Sync(ctx context.Context, cm *claim.Unstructured, xr *composite.Unstructured) error { //nolint:gocyclo // This complex process seems easier to follow in one long method.
|
||||
// Refuse to bind and sync an XR bound to a different claim.
|
||||
existing := xr.GetClaimReference()
|
||||
proposed := cm.GetReference()
|
||||
if existing != nil && !cmp.Equal(existing, proposed) {
|
||||
return errors.New(errBindCompositeConflict)
|
||||
}
|
||||
|
||||
// First we sync claim -> XR.
|
||||
|
||||
// It's possible we're being asked to configure a statically provisioned XR.
|
||||
// We should respect its existing external name annotation.
|
||||
en := meta.GetExternalName(xr)
|
||||
|
||||
// Do not propagate *.kubernetes.io annotations/labels from claim to XR. For
|
||||
// example kubectl.kubernetes.io/last-applied-configuration should not be
|
||||
// propagated.
|
||||
// See https://kubernetes.io/docs/reference/labels-annotations-taints/
|
||||
// for all annotations and their semantic
|
||||
meta.AddAnnotations(xr, withoutReservedK8sEntries(cm.GetAnnotations()))
|
||||
meta.AddLabels(xr, withoutReservedK8sEntries(cm.GetLabels()))
|
||||
meta.AddLabels(xr, map[string]string{
|
||||
xcrd.LabelKeyClaimName: cm.GetName(),
|
||||
xcrd.LabelKeyClaimNamespace: cm.GetNamespace(),
|
||||
})
|
||||
|
||||
// If the bound XR already exists we want to restore its original external
|
||||
// name annotation in order to ensure we don't try to rename anything after
|
||||
// the fact.
|
||||
if meta.WasCreated(xr) && en != "" {
|
||||
meta.SetExternalName(xr, en)
|
||||
}
|
||||
|
||||
// We want to propagate the claim's spec to the composite's spec, but first
|
||||
// we must filter out any well-known fields that are unique to claims. We do
|
||||
// this by:
|
||||
// 1. Grabbing a map whose keys represent all well-known claim fields.
|
||||
// 2. Deleting any well-known fields that we want to propagate.
|
||||
// 3. Using the resulting map keys to filter the claim's spec.
|
||||
wellKnownClaimFields := xcrd.CompositeResourceClaimSpecProps()
|
||||
for _, field := range xcrd.PropagateSpecProps {
|
||||
delete(wellKnownClaimFields, field)
|
||||
}
|
||||
|
||||
// CompositionRevisionRef is a special field which needs to be propagated
|
||||
// based on the Update policy. If the policy is `Manual`, we need to remove
|
||||
// CompositionRevisionRef from wellKnownClaimFields, so it is propagated
|
||||
// from the claim to the XR.
|
||||
if xr.GetCompositionUpdatePolicy() != nil && *xr.GetCompositionUpdatePolicy() == xpv1.UpdateManual {
|
||||
delete(wellKnownClaimFields, xcrd.CompositionRevisionRef)
|
||||
}
|
||||
|
||||
cmSpec, ok := cm.Object["spec"].(map[string]any)
|
||||
if !ok {
|
||||
return errors.New(errUnsupportedClaimSpec)
|
||||
}
|
||||
|
||||
// Propagate the claim's spec (minus well known fields) to the XR's spec.
|
||||
xr.Object["spec"] = withoutKeys(cmSpec, xcrd.GetPropFields(wellKnownClaimFields)...)
|
||||
|
||||
// We overwrite the entire XR spec above, so we wait until this point to set
|
||||
// the claim reference.
|
||||
xr.SetClaimReference(proposed)
|
||||
|
||||
// If the claim references an XR, make sure we're going to apply that XR. We
|
||||
// do this just in case the XR exists, but we couldn't get it due to a stale
|
||||
// cache.
|
||||
if ref := cm.GetResourceReference(); ref != nil {
|
||||
xr.SetName(ref.Name)
|
||||
}
|
||||
|
||||
// If the XR doesn't exist, derive a name from the claim. The generated name
|
||||
// is likely (but not guaranteed) to be available when we create the XR. If
|
||||
// taken, then we are going to update an existing XR, probably hijacking it
|
||||
// from another claim.
|
||||
if !meta.WasCreated(xr) {
|
||||
xr.SetGenerateName(fmt.Sprintf("%s-", cm.GetName()))
|
||||
|
||||
// GenerateName is a no-op if the xr already has a name set.
|
||||
if err := s.names.GenerateName(ctx, xr); err != nil {
|
||||
return errors.Wrap(err, errGenerateName)
|
||||
}
|
||||
}
|
||||
|
||||
// We're now done syncing the XR from the claim. If this is a new XR it's
|
||||
// important that we update the claim to reference it before we create it.
|
||||
// This ensures we don't leak an XR. We could leak an XR if we created an XR
|
||||
// then crashed before saving a reference to it. We'd create another XR on
|
||||
// the next reconcile.
|
||||
cm.SetResourceReference(meta.ReferenceTo(xr, xr.GetObjectKind().GroupVersionKind()))
|
||||
if err := s.client.Update(ctx, cm); err != nil {
|
||||
return errors.Wrap(err, errUpdateClaim)
|
||||
}
|
||||
|
||||
// Apply the XR, unless it's a no-op change.
|
||||
err := s.client.Apply(ctx, xr, resource.AllowUpdateIf(func(old, obj runtime.Object) bool { return !cmp.Equal(old, obj) }))
|
||||
if err := resource.Ignore(resource.IsNotAllowed, err); err != nil {
|
||||
return errors.Wrap(err, errApplyComposite)
|
||||
}
|
||||
|
||||
// Below this point we're syncing XR status -> claim status.
|
||||
|
||||
// Merge the XR's status into the claim's status.
|
||||
if err := merge(cm.Object["status"], xr.Object["status"],
|
||||
// XR status fields overwrite non-empty claim fields.
|
||||
withMergeOptions(mergo.WithOverride),
|
||||
// Don't sync XR machinery (i.e. status conditions, connection details).
|
||||
withSrcFilter(xcrd.GetPropFields(xcrd.CompositeResourceStatusProps())...)); err != nil {
|
||||
return errors.Wrap(err, errMergeClaimStatus)
|
||||
}
|
||||
|
||||
if err := s.client.Status().Update(ctx, cm); err != nil {
|
||||
return errors.Wrap(err, errUpdateClaimStatus)
|
||||
}
|
||||
|
||||
// Propagate the actual external name back from the XR to the claim if it's
|
||||
// set. The name we're propagating here will may be a name the XR must
|
||||
// enforce (i.e. overriding any requested by the claim) but will often
|
||||
// actually just be propagating back a name that was already propagated
|
||||
// forward from the claim to the XR earlier in this method.
|
||||
if en := meta.GetExternalName(xr); en != "" {
|
||||
meta.SetExternalName(cm, en)
|
||||
}
|
||||
|
||||
// We want to propagate the XR's spec to the claim's spec, but first we must
|
||||
// filter out any well-known fields that are unique to XR. We do this by:
|
||||
// 1. Grabbing a map whose keys represent all well-known XR fields.
|
||||
// 2. Deleting any well-known fields that we want to propagate.
|
||||
// 3. Filtering OUT the remaining map keys from the XR's spec so that we end
|
||||
// up adding only the well-known fields to the claim's spec.
|
||||
wellKnownXRFields := xcrd.CompositeResourceSpecProps()
|
||||
for _, field := range xcrd.PropagateSpecProps {
|
||||
delete(wellKnownXRFields, field)
|
||||
}
|
||||
|
||||
// CompositionRevisionRef is a special field which needs to be propagated
|
||||
// based on the Update policy. If the policy is `Automatic`, we need to
|
||||
// overwrite the claim's value with the XR's which should be the
|
||||
// `currentRevision`
|
||||
if xr.GetCompositionUpdatePolicy() != nil && *xr.GetCompositionUpdatePolicy() == xpv1.UpdateAutomatic {
|
||||
cm.SetCompositionRevisionReference(xr.GetCompositionRevisionReference())
|
||||
}
|
||||
|
||||
// Propagate the XR's spec (minus well known fields) to the claim's spec.
|
||||
if err := merge(cm.Object["spec"], xr.Object["spec"],
|
||||
withSrcFilter(xcrd.GetPropFields(wellKnownXRFields)...)); err != nil {
|
||||
return errors.Wrap(err, errMergeClaimSpec)
|
||||
}
|
||||
|
||||
return errors.Wrap(s.client.Update(ctx, cm), errUpdateClaim)
|
||||
}
|
|
@ -0,0 +1,441 @@
|
|||
/*
|
||||
Copyright 2024 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 claim
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"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/meta"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/claim"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
|
||||
"github.com/crossplane/crossplane/internal/names"
|
||||
"github.com/crossplane/crossplane/internal/xcrd"
|
||||
)
|
||||
|
||||
func TestClientSideSync(t *testing.T) {
|
||||
errBoom := errors.New("boom")
|
||||
|
||||
type params struct {
|
||||
c client.Client
|
||||
ng names.NameGenerator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
cm *claim.Unstructured
|
||||
xr *composite.Unstructured
|
||||
}
|
||||
type want struct {
|
||||
cm *claim.Unstructured
|
||||
xr *composite.Unstructured
|
||||
err error
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
params params
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
"RefuseToBindBoundXR": {
|
||||
reason: "We should return an error if the XR is already bound to a different claim.",
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetName("cool-claim")
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetClaimReference(&claim.Reference{Name: "another-claim"})
|
||||
}),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetName("cool-claim")
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetClaimReference(&claim.Reference{Name: "another-claim"})
|
||||
}),
|
||||
err: errors.New(errBindCompositeConflict),
|
||||
},
|
||||
},
|
||||
"WeirdClaimSpec": {
|
||||
reason: "We should return an error if the claim spec is not an object.",
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.Object["spec"] = 42
|
||||
}),
|
||||
xr: NewComposite(),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.Object["spec"] = 42
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetLabels(map[string]string{
|
||||
xcrd.LabelKeyClaimNamespace: "default",
|
||||
xcrd.LabelKeyClaimName: "cool-claim",
|
||||
})
|
||||
}),
|
||||
err: errors.New(errUnsupportedClaimSpec),
|
||||
},
|
||||
},
|
||||
"GenerateXRNameError": {
|
||||
reason: "We should return an error if we can't generate an XR name.",
|
||||
params: params{
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
return errBoom
|
||||
}),
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
|
||||
// To make sure the claim spec is an object.
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetGenerateName("cool-claim-")
|
||||
xr.SetLabels(map[string]string{
|
||||
xcrd.LabelKeyClaimNamespace: "default",
|
||||
xcrd.LabelKeyClaimName: "cool-claim",
|
||||
})
|
||||
xr.SetClaimReference(&claim.Reference{
|
||||
Namespace: "default",
|
||||
Name: "cool-claim",
|
||||
})
|
||||
xr.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
}),
|
||||
err: errors.Wrap(errBoom, errGenerateName),
|
||||
},
|
||||
},
|
||||
"UpdateClaimResourceRefError": {
|
||||
reason: "We should return an error if we can't update the claim to persist its resourceRef.",
|
||||
params: params{
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
cd.SetName("cool-claim-random")
|
||||
return nil
|
||||
}),
|
||||
c: &test.MockClient{
|
||||
MockUpdate: test.NewMockUpdateFn(errBoom),
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
|
||||
// To make sure the claim spec is an object.
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "cool-claim-random",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetGenerateName("cool-claim-")
|
||||
xr.SetName("cool-claim-random")
|
||||
xr.SetLabels(map[string]string{
|
||||
xcrd.LabelKeyClaimNamespace: "default",
|
||||
xcrd.LabelKeyClaimName: "cool-claim",
|
||||
})
|
||||
xr.SetClaimReference(&claim.Reference{
|
||||
Namespace: "default",
|
||||
Name: "cool-claim",
|
||||
})
|
||||
xr.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
}),
|
||||
err: errors.Wrap(errBoom, errUpdateClaim),
|
||||
},
|
||||
},
|
||||
"ApplyXRError": {
|
||||
reason: "We should return an error if we can't apply the XR.",
|
||||
params: params{
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
cd.SetName("cool-claim-random")
|
||||
return nil
|
||||
}),
|
||||
c: &test.MockClient{
|
||||
// Updating the claim should succeed.
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
// Applying the XR will first call Get, which should fail.
|
||||
MockGet: test.NewMockGetFn(errBoom),
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
|
||||
// To make sure the claim spec is an object.
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "cool-claim-random",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetGenerateName("cool-claim-")
|
||||
xr.SetName("cool-claim-random")
|
||||
xr.SetLabels(map[string]string{
|
||||
xcrd.LabelKeyClaimNamespace: "default",
|
||||
xcrd.LabelKeyClaimName: "cool-claim",
|
||||
})
|
||||
xr.SetClaimReference(&claim.Reference{
|
||||
Namespace: "default",
|
||||
Name: "cool-claim",
|
||||
})
|
||||
xr.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
}),
|
||||
err: errors.Wrap(errors.Wrap(errBoom, "cannot get object"), errApplyComposite),
|
||||
},
|
||||
},
|
||||
"UpdateClaimStatusError": {
|
||||
reason: "We should return an error if we can't update the claim's status.",
|
||||
params: params{
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
cd.SetName("cool-claim-random")
|
||||
return nil
|
||||
}),
|
||||
c: &test.MockClient{
|
||||
// Updating the claim should succeed.
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
// Applying the XR will call get.
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
// Updating the claim's status should fail.
|
||||
MockStatusUpdate: test.NewMockSubResourceUpdateFn(errBoom),
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
|
||||
// To make sure the claim spec is an object.
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "cool-claim-random",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetGenerateName("cool-claim-")
|
||||
xr.SetName("cool-claim-random")
|
||||
xr.SetLabels(map[string]string{
|
||||
xcrd.LabelKeyClaimNamespace: "default",
|
||||
xcrd.LabelKeyClaimName: "cool-claim",
|
||||
})
|
||||
xr.SetClaimReference(&claim.Reference{
|
||||
Namespace: "default",
|
||||
Name: "cool-claim",
|
||||
})
|
||||
xr.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
}),
|
||||
err: errors.Wrap(errBoom, errUpdateClaimStatus),
|
||||
},
|
||||
},
|
||||
"XRDoesNotExist": {
|
||||
reason: "We should create, bind, and sync with an XR when none exists.",
|
||||
params: params{
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
cd.SetName("cool-claim-random")
|
||||
return nil
|
||||
}),
|
||||
c: &test.MockClient{
|
||||
// Updating the claim the first time to set the resourceRef
|
||||
// should succeed. The second time we update it should fail.
|
||||
MockUpdate: test.NewMockUpdateFn(nil, func(obj client.Object) error {
|
||||
if obj.(*claim.Unstructured).GetCompositionSelector() != nil {
|
||||
return errBoom
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
// Applying the XR will call get.
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
// Updating the claim's status should succeed.
|
||||
MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil),
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
meta.SetExternalName(cm, "external-name")
|
||||
|
||||
// Kube stuff should not be propagated to the XR.
|
||||
cm.SetLabels(map[string]string{
|
||||
"k8s.io/some-label": "filter-me-out",
|
||||
})
|
||||
cm.SetAnnotations(map[string]string{
|
||||
"kubernetes.io/some-anno": "filter-me-out",
|
||||
"example.org/propagate-me": "true",
|
||||
})
|
||||
|
||||
// To make sure the claim spec is an object.
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
|
||||
// A user-defined spec field we should propagate.
|
||||
cm.Object["spec"].(map[string]any)["propagateMe"] = "true"
|
||||
}),
|
||||
// A non-existent, empty XR.
|
||||
xr: NewComposite(),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
meta.SetExternalName(cm, "external-name")
|
||||
|
||||
cm.SetLabels(map[string]string{
|
||||
"k8s.io/some-label": "filter-me-out",
|
||||
})
|
||||
cm.SetAnnotations(map[string]string{
|
||||
"kubernetes.io/some-anno": "filter-me-out",
|
||||
"example.org/propagate-me": "true",
|
||||
})
|
||||
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "cool-claim-random",
|
||||
})
|
||||
|
||||
cm.Object["spec"].(map[string]any)["propagateMe"] = "true"
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetGenerateName("cool-claim-")
|
||||
xr.SetName("cool-claim-random")
|
||||
meta.SetExternalName(xr, "external-name")
|
||||
|
||||
xr.SetLabels(map[string]string{
|
||||
xcrd.LabelKeyClaimNamespace: "default",
|
||||
xcrd.LabelKeyClaimName: "cool-claim",
|
||||
})
|
||||
xr.SetAnnotations(map[string]string{
|
||||
"example.org/propagate-me": "true",
|
||||
})
|
||||
|
||||
xr.SetClaimReference(&claim.Reference{
|
||||
Namespace: "default",
|
||||
Name: "cool-claim",
|
||||
})
|
||||
xr.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "some-composition",
|
||||
})
|
||||
|
||||
xr.Object["spec"].(map[string]any)["propagateMe"] = "true"
|
||||
}),
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
s := NewClientSideCompositeSyncer(tc.params.c, tc.params.ng)
|
||||
err := s.Sync(tc.args.ctx, tc.args.cm, tc.args.xr)
|
||||
|
||||
if diff := cmp.Diff(tc.want.cm, tc.args.cm); diff != "" {
|
||||
t.Errorf("\n%s\ns.Sync(...): -want, +got:\n%s", tc.reason, diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want.xr, tc.args.xr); diff != "" {
|
||||
t.Errorf("\n%s\ns.Sync(...): -want, +got:\n%s", tc.reason, diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\ns.Sync(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type CompositeModifier func(xr *composite.Unstructured)
|
||||
|
||||
func NewComposite(m ...CompositeModifier) *composite.Unstructured {
|
||||
xr := composite.New(composite.WithGroupVersionKind(schema.GroupVersionKind{}))
|
||||
for _, fn := range m {
|
||||
fn(xr)
|
||||
}
|
||||
return xr
|
||||
}
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
Copyright 2024 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 claim
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/client-go/util/csaupgrade"
|
||||
"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/meta"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/claim"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
|
||||
|
||||
"github.com/crossplane/crossplane/internal/names"
|
||||
"github.com/crossplane/crossplane/internal/xcrd"
|
||||
)
|
||||
|
||||
// Error strings
|
||||
const (
|
||||
errCreatePatch = "cannot create patch"
|
||||
errPatchFieldManagers = "cannot patch field managers"
|
||||
errUnsupportedCompositeStatus = "composite resource status was not an object"
|
||||
)
|
||||
|
||||
// Server-side-apply field owners.
|
||||
const (
|
||||
// FieldOwnerXR owns the fields this controller mutates on composite
|
||||
// resources (XRs).
|
||||
FieldOwnerXR = "apiextensions.crossplane.io/claim"
|
||||
)
|
||||
|
||||
// A NopManagedFieldsUpgrader does nothing.
|
||||
type NopManagedFieldsUpgrader struct{}
|
||||
|
||||
// Upgrade does nothing.
|
||||
func (u *NopManagedFieldsUpgrader) Upgrade(_ context.Context, _ client.Object, _ string, _ ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// A PatchingManagedFieldsUpgrader uses a JSON patch to upgrade an object's
|
||||
// managed fields from client-side to server-side apply. The upgrade is a no-op
|
||||
// if the object does not need upgrading.
|
||||
type PatchingManagedFieldsUpgrader struct {
|
||||
client client.Writer
|
||||
}
|
||||
|
||||
// NewPatchingManagedFieldsUpgrader returns a ManagedFieldsUpgrader that uses a
|
||||
// JSON patch to upgrade and object's managed fields from client-side to
|
||||
// server-side apply.
|
||||
func NewPatchingManagedFieldsUpgrader(w client.Writer) *PatchingManagedFieldsUpgrader {
|
||||
return &PatchingManagedFieldsUpgrader{client: w}
|
||||
}
|
||||
|
||||
// Upgrade the supplied object's field managers from client-side to server-side
|
||||
// apply.
|
||||
func (u *PatchingManagedFieldsUpgrader) Upgrade(ctx context.Context, obj client.Object, ssaManager string, csaManagers ...string) error {
|
||||
// UpgradeManagedFieldsPatch removes or replaces the specified CSA managers.
|
||||
// Unfortunately most Crossplane controllers use CSA manager "crossplane".
|
||||
// So we could for example fight with the XR controller:
|
||||
//
|
||||
// 1. We remove CSA manager "crossplane", triggering XR controller watch
|
||||
// 2. XR controller uses CSA manager "crossplane", triggering our watch
|
||||
// 3. Back to step 1 :)
|
||||
//
|
||||
// In practice we only need to upgrade once, to ensure we don't share fields
|
||||
// that only this controller has ever applied with "crossplane". We assume
|
||||
// that if our SSA manager already exists, we've done the upgrade.
|
||||
for _, e := range obj.GetManagedFields() {
|
||||
if e.Manager == ssaManager {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
p, err := csaupgrade.UpgradeManagedFieldsPatch(obj, sets.New[string](csaManagers...), ssaManager)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, errCreatePatch)
|
||||
}
|
||||
if p == nil {
|
||||
// No patch means there's nothing to upgrade.
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(resource.IgnoreNotFound(u.client.Patch(ctx, obj, client.RawPatch(types.JSONPatchType, p))), errPatchFieldManagers)
|
||||
}
|
||||
|
||||
// A ServerSideCompositeSyncer binds and syncs a claim with a composite resource
|
||||
// (XR). It uses server-side apply to update the XR.
|
||||
type ServerSideCompositeSyncer struct {
|
||||
client client.Client
|
||||
names names.NameGenerator
|
||||
}
|
||||
|
||||
// NewServerSideCompositeSyncer returns a CompositeSyncer that uses server-side
|
||||
// apply to sync a claim with a composite resource.
|
||||
func NewServerSideCompositeSyncer(c client.Client, ng names.NameGenerator) *ServerSideCompositeSyncer {
|
||||
return &ServerSideCompositeSyncer{client: c, names: ng}
|
||||
}
|
||||
|
||||
// Sync the supplied claim with the supplied composite resource (XR). Syncing
|
||||
// may involve creating and binding the XR.
|
||||
func (s *ServerSideCompositeSyncer) Sync(ctx context.Context, cm *claim.Unstructured, xr *composite.Unstructured) error { //nolint:gocyclo // This complex process seems easier to follow in one long method.
|
||||
// Refuse to bind and sync an XR bound to a different claim.
|
||||
existing := xr.GetClaimReference()
|
||||
proposed := cm.GetReference()
|
||||
if existing != nil && !cmp.Equal(existing, proposed) {
|
||||
return errors.New(errBindCompositeConflict)
|
||||
}
|
||||
|
||||
// First we sync claim -> XR.
|
||||
|
||||
// Create an empty XR patch object. We'll use this object to ensure we only
|
||||
// SSA our desired state, not the state we previously read from the API
|
||||
// server.
|
||||
xrPatch := composite.New(composite.WithGroupVersionKind(xr.GroupVersionKind()))
|
||||
|
||||
// If the claim references an XR, make sure we're going to apply that XR. We
|
||||
// do this instead of using the supplied XR's name just in case the XR
|
||||
// exists, but we couldn't get it due to a stale cache.
|
||||
if ref := cm.GetResourceReference(); ref != nil {
|
||||
xrPatch.SetName(ref.Name)
|
||||
}
|
||||
|
||||
// If the XR doesn't have a name (i.e. doesn't exist), derive a name from
|
||||
// the claim. The generated name is likely (but not guaranteed) to be
|
||||
// available when we create the XR. If taken, then we are going to update an
|
||||
// existing XR, probably hijacking it from another claim.
|
||||
if xrPatch.GetName() == "" {
|
||||
xrPatch.SetGenerateName(fmt.Sprintf("%s-", cm.GetName()))
|
||||
if err := s.names.GenerateName(ctx, xrPatch); err != nil {
|
||||
return errors.Wrap(err, errGenerateName)
|
||||
}
|
||||
}
|
||||
|
||||
// It's possible we're being asked to configure a statically provisioned XR.
|
||||
// We should respect its existing external name annotation.
|
||||
en := meta.GetExternalName(xr)
|
||||
|
||||
// Do not propagate *.kubernetes.io annotations/labels from claim to XR. For
|
||||
// example kubectl.kubernetes.io/last-applied-configuration should not be
|
||||
// propagated.
|
||||
// See https://kubernetes.io/docs/reference/labels-annotations-taints/
|
||||
// for all annotations and their semantic
|
||||
if ann := withoutReservedK8sEntries(cm.GetAnnotations()); len(ann) > 0 {
|
||||
meta.AddAnnotations(xrPatch, withoutReservedK8sEntries(cm.GetAnnotations()))
|
||||
}
|
||||
meta.AddLabels(xrPatch, withoutReservedK8sEntries(cm.GetLabels()))
|
||||
meta.AddLabels(xrPatch, map[string]string{
|
||||
xcrd.LabelKeyClaimName: cm.GetName(),
|
||||
xcrd.LabelKeyClaimNamespace: cm.GetNamespace(),
|
||||
})
|
||||
|
||||
// Restore the XR's original external name annotation in order to ensure we
|
||||
// don't try to rename anything after the fact.
|
||||
if en != "" {
|
||||
meta.SetExternalName(xrPatch, en)
|
||||
}
|
||||
|
||||
// We want to propagate the claim's spec to the composite's spec, but first
|
||||
// we must filter out any well-known fields that are unique to claims. We do
|
||||
// this by:
|
||||
// 1. Grabbing a map whose keys represent all well-known claim fields.
|
||||
// 2. Deleting any well-known fields that we want to propagate.
|
||||
// 3. Using the resulting map keys to filter the claim's spec.
|
||||
wellKnownClaimFields := xcrd.CompositeResourceClaimSpecProps()
|
||||
for _, field := range xcrd.PropagateSpecProps {
|
||||
delete(wellKnownClaimFields, field)
|
||||
}
|
||||
|
||||
// Propagate composition revision ref from the claim if the update policy is
|
||||
// manual. When the update policy is manual the claim controller is
|
||||
// authoritative for this field. See below for the automatic case.
|
||||
if xr.GetCompositionUpdatePolicy() != nil && *xr.GetCompositionUpdatePolicy() == xpv1.UpdateManual {
|
||||
delete(wellKnownClaimFields, xcrd.CompositionRevisionRef)
|
||||
}
|
||||
|
||||
cmSpec, ok := cm.Object["spec"].(map[string]any)
|
||||
if !ok {
|
||||
return errors.New(errUnsupportedClaimSpec)
|
||||
}
|
||||
|
||||
// Propagate the claim's spec (minus well known fields) to the XR's spec.
|
||||
xrPatch.Object["spec"] = withoutKeys(cmSpec, xcrd.GetPropFields(wellKnownClaimFields)...)
|
||||
|
||||
// We overwrite the entire XR spec above, so we wait until this point to set
|
||||
// the claim reference.
|
||||
xrPatch.SetClaimReference(proposed)
|
||||
|
||||
// Below this point we're syncing XR -> claim.
|
||||
|
||||
// Bind the claim to the XR. If this is a new XR it's important that we
|
||||
// apply the claim before we create it. This ensures we don't leak an XR. We
|
||||
// could leak an XR if we created an XR then crashed before saving a
|
||||
// reference to it. We'd create another XR on the next reconcile.
|
||||
cm.SetResourceReference(meta.ReferenceTo(xrPatch, xrPatch.GroupVersionKind()))
|
||||
|
||||
// Propagate the actual external name back from the composite to the
|
||||
// claim if it's set. The name we're propagating here will may be a name
|
||||
// the XR must enforce (i.e. overriding any requested by the claim) but
|
||||
// will often actually just be propagating back a name that was already
|
||||
// propagated forward from the claim to the XR during the
|
||||
// preceding configure phase.
|
||||
if en := meta.GetExternalName(xr); en != "" {
|
||||
meta.SetExternalName(cm, en)
|
||||
}
|
||||
|
||||
// Propagate composition ref from the XR if the claim doesn't have an
|
||||
// opinion. Composition and revision selectors only propagate from claim ->
|
||||
// XR. When a claim has selectors **and no reference** the flow should be:
|
||||
//
|
||||
// 1. Claim controller propagates selectors claim -> XR.
|
||||
// 2. XR controller uses selectors to set XR's composition ref.
|
||||
// 3. Claim controller propagates ref XR -> claim.
|
||||
//
|
||||
// When a claim sets a composition ref, it supercedes selectors. It should
|
||||
// only be propagated claim -> XR.
|
||||
if ref := xr.GetCompositionReference(); ref != nil && cm.GetCompositionReference() == nil {
|
||||
cm.SetCompositionReference(ref)
|
||||
}
|
||||
|
||||
// Propagate composition revision ref from the XR if the update policy is
|
||||
// automatic. When the update policy is automatic the XR controller is
|
||||
// authoritative for this field. It will update the XR's ref as new
|
||||
// revisions become available, and we want to propgate the ref XR -> claim.
|
||||
if p := xr.GetCompositionUpdatePolicy(); p != nil && *p == xpv1.UpdateAutomatic && xr.GetCompositionRevisionReference() != nil {
|
||||
cm.SetCompositionRevisionReference(xr.GetCompositionRevisionReference())
|
||||
}
|
||||
|
||||
// It's important that we update the claim before we apply the XR, to make
|
||||
// sure the claim's resourceRef points to the XR before we create the XR.
|
||||
// Otherwise we risk leaking an XR.
|
||||
//
|
||||
// It's also important that the API server will reject this update if we're
|
||||
// reconciling an old claim, e.g. due to a stale cache. It's possible that
|
||||
// we're seeing an old version without a resourceRef set, but in reality an
|
||||
// XR has already been created. We don't want to leak it and create another.
|
||||
if err := s.client.Update(ctx, cm); err != nil {
|
||||
return errors.Wrap(err, errUpdateClaim)
|
||||
}
|
||||
|
||||
if err := s.client.Patch(ctx, xrPatch, client.Apply, client.ForceOwnership, client.FieldOwner(FieldOwnerXR)); err != nil {
|
||||
return errors.Wrap(err, errApplyComposite)
|
||||
}
|
||||
|
||||
// Update the XR passed to this method to reflect the state returned by the
|
||||
// API server when we patched it.
|
||||
xr.Object = xrPatch.Object
|
||||
|
||||
m, ok := xr.Object["status"]
|
||||
if !ok {
|
||||
// If the XR doesn't have a status yet there's nothing else to sync.
|
||||
// Just update the claim passed to this method to reflect the state
|
||||
// returned by the API server when we patched it.
|
||||
return nil
|
||||
}
|
||||
|
||||
xrStatus, ok := m.(map[string]any)
|
||||
if !ok {
|
||||
return errors.New(errUnsupportedCompositeStatus)
|
||||
}
|
||||
|
||||
// Preserve Crossplane machinery, like status conditions.
|
||||
synced := cm.GetCondition(xpv1.TypeSynced)
|
||||
ready := cm.GetCondition(xpv1.TypeReady)
|
||||
pub := cm.GetConnectionDetailsLastPublishedTime()
|
||||
|
||||
// Update the claim's user-defined status fields to match the XRs.
|
||||
cm.Object["status"] = withoutKeys(xrStatus, xcrd.GetPropFields(xcrd.CompositeResourceStatusProps())...)
|
||||
|
||||
if !synced.Equal(xpv1.Condition{}) {
|
||||
cm.SetConditions(synced)
|
||||
}
|
||||
if !ready.Equal(xpv1.Condition{}) {
|
||||
cm.SetConditions(ready)
|
||||
}
|
||||
if pub != nil {
|
||||
cm.SetConnectionDetailsLastPublishedTime(pub)
|
||||
}
|
||||
|
||||
return errors.Wrap(s.client.Status().Update(ctx, cm), errUpdateClaimStatus)
|
||||
}
|
|
@ -0,0 +1,467 @@
|
|||
/*
|
||||
Copyright 2024 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 claim
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"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/meta"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/claim"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
|
||||
"github.com/crossplane/crossplane/internal/names"
|
||||
"github.com/crossplane/crossplane/internal/xcrd"
|
||||
)
|
||||
|
||||
func TestServerSideSync(t *testing.T) {
|
||||
errBoom := errors.New("boom")
|
||||
now := metav1.Now()
|
||||
|
||||
type params struct {
|
||||
c client.Client
|
||||
ng names.NameGenerator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
cm *claim.Unstructured
|
||||
xr *composite.Unstructured
|
||||
}
|
||||
type want struct {
|
||||
cm *claim.Unstructured
|
||||
xr *composite.Unstructured
|
||||
err error
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
params params
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
"RefuseToBindBoundXR": {
|
||||
reason: "We should return an error if the XR is already bound to a different claim.",
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetName("cool-claim")
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetClaimReference(&claim.Reference{Name: "another-claim"})
|
||||
}),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetName("cool-claim")
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetClaimReference(&claim.Reference{Name: "another-claim"})
|
||||
}),
|
||||
err: errors.New(errBindCompositeConflict),
|
||||
},
|
||||
},
|
||||
"GenerateXRNameError": {
|
||||
reason: "We should return an error if we can't generate an XR name.",
|
||||
params: params{
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
return errBoom
|
||||
}),
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
}),
|
||||
xr: NewComposite(),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
}),
|
||||
xr: NewComposite(),
|
||||
err: errors.Wrap(errBoom, errGenerateName),
|
||||
},
|
||||
},
|
||||
"WeirdClaimSpec": {
|
||||
reason: "We should return an error if the claim spec is not an object.",
|
||||
params: params{
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.Object["spec"] = 42
|
||||
}),
|
||||
xr: NewComposite(),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.Object["spec"] = 42
|
||||
}),
|
||||
xr: NewComposite(),
|
||||
err: errors.New(errUnsupportedClaimSpec),
|
||||
},
|
||||
},
|
||||
"UpdateClaimError": {
|
||||
reason: "We should return an error if we can't update the claim.",
|
||||
params: params{
|
||||
c: &test.MockClient{
|
||||
// Fail to update the claim.
|
||||
MockUpdate: test.NewMockUpdateFn(errBoom),
|
||||
},
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
|
||||
// To make sure the claim spec is an object.
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "existing-composite",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetName("existing-composite")
|
||||
xr.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "cool-composition",
|
||||
})
|
||||
}),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "existing-composite",
|
||||
})
|
||||
|
||||
// Back-propagated from the XR.
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "cool-composition",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetName("existing-composite")
|
||||
xr.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "cool-composition",
|
||||
})
|
||||
}),
|
||||
err: errors.Wrap(errBoom, errUpdateClaim),
|
||||
},
|
||||
},
|
||||
"ApplyXRError": {
|
||||
reason: "We should return an error if we can't apply (i.e. patch) the XR.",
|
||||
params: params{
|
||||
c: &test.MockClient{
|
||||
// Update the claim.
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
|
||||
// Fail to patch the XR.
|
||||
MockPatch: test.NewMockPatchFn(errBoom),
|
||||
},
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
|
||||
// To make sure the claim spec is an object.
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "existing-composite",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetName("existing-composite")
|
||||
xr.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "cool-composition",
|
||||
})
|
||||
}),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "existing-composite",
|
||||
})
|
||||
|
||||
// Back-propagated from the XR.
|
||||
cm.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "cool-composition",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetName("existing-composite")
|
||||
xr.SetCompositionReference(&corev1.ObjectReference{
|
||||
Name: "cool-composition",
|
||||
})
|
||||
}),
|
||||
err: errors.Wrap(errBoom, errApplyComposite),
|
||||
},
|
||||
},
|
||||
"WeirdXRStatus": {
|
||||
reason: "We should return an error if the XR status is not an object.",
|
||||
params: params{
|
||||
c: &test.MockClient{
|
||||
// Update the claim.
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
|
||||
// Patch the XR. We reset the XR passed to Sync to the
|
||||
// result of this patch, so we need to make its status
|
||||
// something other than an object here.
|
||||
MockPatch: test.NewMockPatchFn(nil, func(obj client.Object) error {
|
||||
obj.(*composite.Unstructured).Object["status"] = 42
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
|
||||
// To make sure the claim spec is an object.
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "existing-composite",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetName("existing-composite")
|
||||
}),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "existing-composite",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetName("existing-composite")
|
||||
xr.SetLabels(map[string]string{
|
||||
xcrd.LabelKeyClaimNamespace: "default",
|
||||
xcrd.LabelKeyClaimName: "cool-claim",
|
||||
})
|
||||
xr.SetClaimReference(&claim.Reference{
|
||||
Namespace: "default",
|
||||
Name: "cool-claim",
|
||||
})
|
||||
xr.Object["status"] = 42
|
||||
}),
|
||||
err: errors.New(errUnsupportedCompositeStatus),
|
||||
},
|
||||
},
|
||||
"UpdateClaimStatusError": {
|
||||
reason: "We should return an error if we can't update the claim's status.",
|
||||
params: params{
|
||||
c: &test.MockClient{
|
||||
// Update the claim.
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
|
||||
// Patch the XR. Make sure it has a valid status.
|
||||
MockPatch: test.NewMockPatchFn(nil, func(obj client.Object) error {
|
||||
obj.(*composite.Unstructured).SetConditions(xpv1.Creating())
|
||||
return nil
|
||||
}),
|
||||
|
||||
// Fail to update the claim's status.
|
||||
MockStatusUpdate: test.NewMockSubResourceUpdateFn(errBoom),
|
||||
},
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
|
||||
// To make sure the claim spec is an object.
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "existing-composite",
|
||||
})
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetName("existing-composite")
|
||||
}),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "existing-composite",
|
||||
})
|
||||
cm.Object["status"] = map[string]any{}
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetName("existing-composite")
|
||||
xr.SetLabels(map[string]string{
|
||||
xcrd.LabelKeyClaimNamespace: "default",
|
||||
xcrd.LabelKeyClaimName: "cool-claim",
|
||||
})
|
||||
xr.SetClaimReference(&claim.Reference{
|
||||
Namespace: "default",
|
||||
Name: "cool-claim",
|
||||
})
|
||||
xr.SetConditions(xpv1.Creating())
|
||||
}),
|
||||
err: errors.Wrap(errBoom, errUpdateClaimStatus),
|
||||
},
|
||||
},
|
||||
"XRDoesNotExist": {
|
||||
reason: "We should create, bind, and sync with an XR when none exists.",
|
||||
params: params{
|
||||
c: &test.MockClient{
|
||||
// Update the claim.
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
|
||||
// Patch the XR. Make sure it has a valid status.
|
||||
MockPatch: test.NewMockPatchFn(nil, func(obj client.Object) error {
|
||||
obj.(*composite.Unstructured).Object["status"] = map[string]any{
|
||||
"userDefinedField": "status",
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
|
||||
// Update the claim's status.
|
||||
MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil),
|
||||
},
|
||||
ng: names.NameGeneratorFn(func(ctx context.Context, cd resource.Object) error {
|
||||
// Generate a name for the XR.
|
||||
cd.SetName("cool-claim-random")
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
args: args{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
meta.SetExternalName(cm, "external-name")
|
||||
|
||||
// Kube stuff should not be propagated to the XR.
|
||||
cm.SetLabels(map[string]string{
|
||||
"k8s.io/some-label": "filter-me-out",
|
||||
})
|
||||
cm.SetAnnotations(map[string]string{
|
||||
"kubernetes.io/some-anno": "filter-me-out",
|
||||
"example.org/propagate-me": "true",
|
||||
})
|
||||
|
||||
// Make sure user-defined fields are propagated to the XR.
|
||||
cm.Object["spec"] = map[string]any{
|
||||
"userDefinedField": "spec",
|
||||
}
|
||||
|
||||
// Make sure these don't get lost when we propagate status
|
||||
// from the XR.
|
||||
cm.SetConditions(xpv1.ReconcileSuccess(), Waiting())
|
||||
cm.SetConnectionDetailsLastPublishedTime(&now)
|
||||
}),
|
||||
xr: NewComposite(),
|
||||
},
|
||||
want: want{
|
||||
cm: NewClaim(func(cm *claim.Unstructured) {
|
||||
cm.SetNamespace("default")
|
||||
cm.SetName("cool-claim")
|
||||
meta.SetExternalName(cm, "external-name")
|
||||
cm.SetLabels(map[string]string{
|
||||
"k8s.io/some-label": "filter-me-out",
|
||||
})
|
||||
cm.SetAnnotations(map[string]string{
|
||||
"kubernetes.io/some-anno": "filter-me-out",
|
||||
"example.org/propagate-me": "true",
|
||||
})
|
||||
cm.Object["spec"] = map[string]any{
|
||||
"userDefinedField": "spec",
|
||||
}
|
||||
cm.SetResourceReference(&corev1.ObjectReference{
|
||||
Name: "cool-claim-random",
|
||||
})
|
||||
cm.Object["status"] = map[string]any{
|
||||
"userDefinedField": "status",
|
||||
}
|
||||
cm.SetConditions(xpv1.ReconcileSuccess(), Waiting())
|
||||
cm.SetConnectionDetailsLastPublishedTime(&now)
|
||||
}),
|
||||
xr: NewComposite(func(xr *composite.Unstructured) {
|
||||
xr.SetGenerateName("cool-claim-")
|
||||
xr.SetName("cool-claim-random")
|
||||
meta.SetExternalName(xr, "external-name")
|
||||
xr.SetLabels(map[string]string{
|
||||
xcrd.LabelKeyClaimNamespace: "default",
|
||||
xcrd.LabelKeyClaimName: "cool-claim",
|
||||
})
|
||||
xr.SetAnnotations(map[string]string{
|
||||
"example.org/propagate-me": "true",
|
||||
})
|
||||
xr.Object["spec"] = map[string]any{
|
||||
"userDefinedField": "spec",
|
||||
}
|
||||
xr.SetClaimReference(&claim.Reference{
|
||||
Namespace: "default",
|
||||
Name: "cool-claim",
|
||||
})
|
||||
xr.Object["status"] = map[string]any{
|
||||
"userDefinedField": "status",
|
||||
}
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
s := NewServerSideCompositeSyncer(tc.params.c, tc.params.ng)
|
||||
err := s.Sync(tc.args.ctx, tc.args.cm, tc.args.xr)
|
||||
|
||||
if diff := cmp.Diff(tc.want.cm, tc.args.cm); diff != "" {
|
||||
t.Errorf("\n%s\ns.Sync(...): -want, +got:\n%s", tc.reason, diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want.xr, tc.args.xr); diff != "" {
|
||||
t.Errorf("\n%s\ns.Sync(...): -want, +got:\n%s", tc.reason, diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\ns.Sync(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ const (
|
|||
var CompositionRevisionRef = "compositionRevisionRef"
|
||||
|
||||
// PropagateSpecProps is the list of XRC spec properties to propagate
|
||||
// when translating an XRC into an XR and vice-versa.
|
||||
// when translating an XRC into an XR.
|
||||
var PropagateSpecProps = []string{"compositionRef", "compositionSelector", "compositionUpdatePolicy", "compositionRevisionSelector"}
|
||||
|
||||
// TODO(negz): Add descriptions to schema fields.
|
||||
|
|
Loading…
Reference in New Issue