mirror of https://github.com/helm/helm
parent
bf104d1af0
commit
603e88af6f
@ -0,0 +1,245 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/cli-runtime/pkg/resource"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
)
|
||||
|
||||
func migrateManagedFields(
|
||||
helper *resource.Helper,
|
||||
info *resource.Info,
|
||||
managersToAdopt []string,
|
||||
helmManager string,
|
||||
) (didMigrate bool, err error) {
|
||||
// retry a few times on conflict errors.
|
||||
for i := 0; i < 5; i++ {
|
||||
var patchData []byte
|
||||
var obj runtime.Object
|
||||
|
||||
patchData, err := createMigrateManagedFieldsPatch(info.Object, managersToAdopt, helmManager)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to generate patch for upgrading managed fields")
|
||||
} else if patchData == nil {
|
||||
// no work to do.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
obj, err = helper.Patch(info.Namespace, info.Name, types.JSONPatchType, patchData, nil)
|
||||
if err != nil {
|
||||
if !apierrors.IsConflict(err) {
|
||||
return false, errors.Wrap(err, "unexpected error patching managed fields on object")
|
||||
}
|
||||
// retry on conflicts, but refresh object first
|
||||
if err = info.Get(); err != nil {
|
||||
return false, errors.Wrap(err, "unexpected error refreshing object")
|
||||
}
|
||||
continue
|
||||
}
|
||||
info.Refresh(obj, true)
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// createMigrateManagedFieldsPatch Calculates a minimal JSON Patch to send to upgrade managed fields
|
||||
func createMigrateManagedFieldsPatch(
|
||||
obj runtime.Object,
|
||||
managersToAdopt []string,
|
||||
helmManager string,
|
||||
) ([]byte, error) {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
managedFields := accessor.GetManagedFields()
|
||||
filteredManagers := accessor.GetManagedFields()
|
||||
// note that we also adopt previous non-apply operations from the helm manager.
|
||||
for managerName := range sets.New(managersToAdopt...).Insert(helmManager) {
|
||||
filteredManagers, err = upgradedManagedFields(
|
||||
filteredManagers,
|
||||
managerName,
|
||||
helmManager,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(managedFields, filteredManagers) {
|
||||
// If the managed fields have not changed from the transformed version,
|
||||
// there is no patch to perform
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create a patch with a diff between old and new objects.
|
||||
// Just include all managed fields since that is only thing that will change
|
||||
//
|
||||
// Also include test for RV to avoid race condition
|
||||
jsonPatch := []map[string]interface{}{
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/metadata/managedFields",
|
||||
"value": filteredManagers,
|
||||
},
|
||||
{
|
||||
// Use "replace" instead of "test" operation so that etcd rejects with
|
||||
// 409 conflict instead of apiserver with an invalid request
|
||||
"op": "replace",
|
||||
"path": "/metadata/resourceVersion",
|
||||
"value": accessor.GetResourceVersion(),
|
||||
},
|
||||
}
|
||||
|
||||
return json.Marshal(jsonPatch)
|
||||
}
|
||||
|
||||
// Returns a copy of the provided managed fields that has been migrated from
|
||||
// client-side-apply to server-side-apply, or an error if there was an issue
|
||||
func upgradedManagedFields(
|
||||
managedFields []metav1.ManagedFieldsEntry,
|
||||
oldManager,
|
||||
newManager string,
|
||||
) ([]metav1.ManagedFieldsEntry, error) {
|
||||
if managedFields == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create managed fields clone since we modify the values
|
||||
managedFieldsCopy := make([]metav1.ManagedFieldsEntry, len(managedFields))
|
||||
if copy(managedFieldsCopy, managedFields) != len(managedFields) {
|
||||
return nil, errors.New("failed to copy managed fields")
|
||||
}
|
||||
managedFields = managedFieldsCopy
|
||||
|
||||
// Locate new manager
|
||||
replaceIndex, ok := findFirstIndex(managedFields,
|
||||
func(entry metav1.ManagedFieldsEntry) bool {
|
||||
return entry.Manager == newManager &&
|
||||
entry.Operation == metav1.ManagedFieldsOperationApply
|
||||
})
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("apply: unexpected error - no manager found")
|
||||
}
|
||||
err := unionManagerIntoIndex(managedFields, replaceIndex, oldManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create version of managed fields without the old field manager.
|
||||
filteredManagers := filter(managedFields, func(entry metav1.ManagedFieldsEntry) bool {
|
||||
if entry.Manager != oldManager {
|
||||
// keep unaffected entries
|
||||
return true
|
||||
} else if oldManager != newManager {
|
||||
// remove if a different field manager entirely.
|
||||
return false
|
||||
}
|
||||
// special-case: if migrating the same field manager, only remove the old non-Apply entries.
|
||||
return (entry.Manager == newManager && entry.Operation == metav1.ManagedFieldsOperationApply)
|
||||
})
|
||||
|
||||
return filteredManagers, nil
|
||||
}
|
||||
|
||||
func unionManagerIntoIndex(
|
||||
entries []metav1.ManagedFieldsEntry,
|
||||
targetIndex int,
|
||||
oldManager string,
|
||||
) error {
|
||||
ssaManager := entries[targetIndex]
|
||||
|
||||
// find any other manager of same APIVersion, union ssa fields with it.
|
||||
oldManagerIndex, ok := findFirstIndex(entries,
|
||||
func(entry metav1.ManagedFieldsEntry) bool {
|
||||
return entry.Manager == oldManager &&
|
||||
entry.Operation == metav1.ManagedFieldsOperationUpdate &&
|
||||
entry.APIVersion == ssaManager.APIVersion
|
||||
})
|
||||
|
||||
targetFieldSet, err := decodeManagedFieldsEntrySet(ssaManager)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert fields to set: %w", err)
|
||||
}
|
||||
|
||||
combinedFieldSet := &targetFieldSet
|
||||
|
||||
// Union the old manager with the new manager. Do nothing if
|
||||
// there was no good candidate found
|
||||
if ok {
|
||||
csaManager := entries[oldManagerIndex]
|
||||
|
||||
csaFieldSet, err := decodeManagedFieldsEntrySet(csaManager)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert fields to set: %w", err)
|
||||
}
|
||||
|
||||
combinedFieldSet = combinedFieldSet.Union(&csaFieldSet)
|
||||
}
|
||||
|
||||
// Encode the fields back to the serialized format
|
||||
err = encodeManagedFieldsEntrySet(&entries[targetIndex], *combinedFieldSet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode field set: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findFirstIndex[T any](
|
||||
collection []T,
|
||||
predicate func(T) bool,
|
||||
) (int, bool) {
|
||||
for idx, entry := range collection {
|
||||
if predicate(entry) {
|
||||
return idx, true
|
||||
}
|
||||
}
|
||||
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func filter[T any](
|
||||
collection []T,
|
||||
predicate func(T) bool,
|
||||
) []T {
|
||||
result := make([]T, 0, len(collection))
|
||||
|
||||
for _, value := range collection {
|
||||
if predicate(value) {
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Included from fieldmanager.internal to avoid dependency cycle
|
||||
// FieldsToSet creates a set paths from an input trie of fields
|
||||
func decodeManagedFieldsEntrySet(f metav1.ManagedFieldsEntry) (s fieldpath.Set, err error) {
|
||||
err = s.FromJSON(bytes.NewReader(f.FieldsV1.Raw))
|
||||
return s, err
|
||||
}
|
||||
|
||||
// SetToFields creates a trie of fields from an input set of paths
|
||||
func encodeManagedFieldsEntrySet(f *metav1.ManagedFieldsEntry, s fieldpath.Set) (err error) {
|
||||
f.FieldsV1.Raw, err = s.ToJSON()
|
||||
return err
|
||||
}
|
Loading…
Reference in new issue