196 lines
8.0 KiB
Go
196 lines
8.0 KiB
Go
/*
|
|
Copyright 2025 SUSE.
|
|
|
|
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 ssa contains utils related to Server-Side-Apply.
|
|
package ssa
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
|
|
"github.com/pkg/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/klog/v2"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
|
|
|
"github.com/rancher/cluster-api-provider-rke2/controlplane/internal/contract"
|
|
)
|
|
|
|
const classicManager = "manager"
|
|
|
|
// DropManagedFields modifies the managedFields entries on the object that belong to "manager" (Operation=Update)
|
|
// to drop ownership of the given paths if there is no field yet that is managed by `ssaManager`.
|
|
//
|
|
// If we want to be able to drop fields that were previously owned by the "manager" we have to ensure that
|
|
// fields are not co-owned by "manager" and `ssaManager`. Otherwise, when we drop the fields with SSA
|
|
// (i.e. `ssaManager`) the fields would remain as they are still owned by "manager".
|
|
// The following code will do a one-time update on the managed fields.
|
|
// We won't do this on subsequent reconciles. This case will be identified by checking if `ssaManager` owns any fields.
|
|
// Dropping ownership in paths for existing "manager" entries (which could also be from other controllers) is safe,
|
|
// as we assume that if other controllers are still writing fields on the object they will just do it again and thus
|
|
// gain ownership again.
|
|
func DropManagedFields(ctx context.Context, c client.Client, obj client.Object, ssaManager string, paths []contract.Path) error {
|
|
// Return if `ssaManager` already owns any fields.
|
|
if hasFieldsManagedBy(obj, ssaManager) {
|
|
return nil
|
|
}
|
|
|
|
// Since there is no field managed by `ssaManager` it means that
|
|
// this is the first time this object is being processed after the controller calling this function
|
|
// started to use SSA patches.
|
|
// It is required to clean-up managedFields from entries created by the regular patches.
|
|
// This will ensure that `ssaManager` will be able to modify the fields that
|
|
// were originally owned by "manager".
|
|
baseObj, ok := obj.DeepCopyObject().(client.Object)
|
|
if !ok {
|
|
return errors.New("failed to assert type to client.Object")
|
|
}
|
|
|
|
base := baseObj
|
|
|
|
// Modify managedFieldEntry for manager=manager and operation=update to drop ownership
|
|
// for the given paths to avoid having two managers holding values.
|
|
originalManagedFields := obj.GetManagedFields()
|
|
managedFields := make([]metav1.ManagedFieldsEntry, 0, len(originalManagedFields))
|
|
|
|
for _, managedField := range originalManagedFields {
|
|
if managedField.Manager == classicManager &&
|
|
managedField.Operation == metav1.ManagedFieldsOperationUpdate {
|
|
// Unmarshal the managed fields into a map[string]interface{}
|
|
fieldsV1 := map[string]interface{}{}
|
|
if err := json.Unmarshal(managedField.FieldsV1.Raw, &fieldsV1); err != nil {
|
|
return errors.Wrap(err, "failed to unmarshal managed fields")
|
|
}
|
|
|
|
// Filter out the ownership for the given paths.
|
|
FilterIntent(&FilterIntentInput{
|
|
Path: contract.Path{},
|
|
Value: fieldsV1,
|
|
ShouldFilter: IsPathIgnored(paths),
|
|
})
|
|
|
|
fieldsV1Raw, err := json.Marshal(fieldsV1)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to marshal managed fields")
|
|
}
|
|
|
|
managedField.FieldsV1.Raw = fieldsV1Raw
|
|
|
|
managedFields = append(managedFields, managedField)
|
|
} else {
|
|
// Do not modify the entry. Use as is.
|
|
managedFields = append(managedFields, managedField)
|
|
}
|
|
}
|
|
|
|
obj.SetManagedFields(managedFields)
|
|
|
|
return c.Patch(ctx, obj, client.MergeFrom(base))
|
|
}
|
|
|
|
// CleanUpManagedFieldsForSSAAdoption deletes the managedFields entries on the object that belong to "manager" (Operation=Update)
|
|
// if there is no field yet that is managed by `ssaManager`.
|
|
// It adds an "empty" entry in managedFields of the object if no field is currently managed by `ssaManager`.
|
|
//
|
|
// If we want to be able to drop fields that were previously owned by the "manager" we have to ensure that
|
|
// fields are not co-owned by "manager" and `ssaManager`. Otherwise, when we drop the fields with SSA
|
|
// (i.e. `ssaManager`) the fields would remain as they are still owned by "manager".
|
|
// The following code will do a one-time update on the managed fields to drop all entries for "manager".
|
|
// We won't do this on subsequent reconciles. This case will be identified by checking if `ssaManager` owns any fields.
|
|
// Dropping all existing "manager" entries (which could also be from other controllers) is safe, as we assume that if
|
|
// other controllers are still writing fields on the object they will just do it again and thus gain ownership again.
|
|
func CleanUpManagedFieldsForSSAAdoption(ctx context.Context, c client.Client, obj client.Object, ssaManager string) error {
|
|
// Return if `ssaManager` already owns any fields.
|
|
if hasFieldsManagedBy(obj, ssaManager) {
|
|
return nil
|
|
}
|
|
|
|
// Since there is no field managed by `ssaManager` it means that
|
|
// this is the first time this object is being processed after the controller calling this function
|
|
// started to use SSA patches.
|
|
// It is required to clean-up managedFields from entries created by the regular patches.
|
|
// This will ensure that `ssaManager` will be able to modify the fields that
|
|
// were originally owned by "manager".
|
|
baseObj, ok := obj.DeepCopyObject().(client.Object)
|
|
if !ok {
|
|
return errors.New("failed to assert type to client.Object")
|
|
}
|
|
|
|
base := baseObj
|
|
|
|
// Remove managedFieldEntry for manager=manager and operation=update to prevent having two managers holding values.
|
|
originalManagedFields := obj.GetManagedFields()
|
|
managedFields := make([]metav1.ManagedFieldsEntry, 0, len(originalManagedFields))
|
|
|
|
for i := range originalManagedFields {
|
|
if originalManagedFields[i].Manager == classicManager &&
|
|
originalManagedFields[i].Operation == metav1.ManagedFieldsOperationUpdate {
|
|
continue
|
|
}
|
|
|
|
managedFields = append(managedFields, originalManagedFields[i])
|
|
}
|
|
|
|
// Add a seeding managedFieldEntry for SSA executed by the management controller, to prevent SSA to create/infer
|
|
// a default managedFieldEntry when the first SSA is applied.
|
|
// More specifically, if an existing object doesn't have managedFields when applying the first SSA the API server
|
|
// creates an entry with operation=Update (kind of guessing where the object comes from), but this entry ends up
|
|
// acting as a co-ownership and we want to prevent this.
|
|
// NOTE: fieldV1Map cannot be empty, so we add metadata.name which will be cleaned up at the first SSA patch of the same fieldManager.
|
|
fieldV1Map := map[string]interface{}{
|
|
"f:metadata": map[string]interface{}{
|
|
"f:name": map[string]interface{}{},
|
|
},
|
|
}
|
|
fieldV1, err := json.Marshal(fieldV1Map)
|
|
if err != nil { //nolint:wsl
|
|
return errors.Wrap(err, "failed to create seeding fieldV1Map for cleaning up legacy managed fields")
|
|
}
|
|
|
|
now := metav1.Now()
|
|
|
|
gvk, err := apiutil.GVKForObject(obj, c.Scheme())
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get GroupVersionKind of object %s", klog.KObj(obj))
|
|
}
|
|
|
|
managedFields = append(managedFields, metav1.ManagedFieldsEntry{
|
|
Manager: ssaManager,
|
|
Operation: metav1.ManagedFieldsOperationApply,
|
|
APIVersion: gvk.GroupVersion().String(),
|
|
Time: &now,
|
|
FieldsType: "FieldsV1",
|
|
FieldsV1: &metav1.FieldsV1{Raw: fieldV1},
|
|
})
|
|
|
|
obj.SetManagedFields(managedFields)
|
|
|
|
return c.Patch(ctx, obj, client.MergeFrom(base))
|
|
}
|
|
|
|
// hasFieldsManagedBy returns true if any of the fields in obj are managed by manager.
|
|
func hasFieldsManagedBy(obj client.Object, manager string) bool {
|
|
managedFields := obj.GetManagedFields()
|
|
for _, mf := range managedFields {
|
|
if mf.Manager == manager {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|