Merge pull request #115065 from apelisse/apimachinery-managed-fields

managedfields: Move most of fieldmanager package to managefields

Kubernetes-commit: e8ae6658ed13fb7dbeb595cf29418f74a523d895
This commit is contained in:
Kubernetes Publisher 2023-03-09 21:34:22 -08:00
commit 7a3a376fee
48 changed files with 62 additions and 12455 deletions

8
go.mod
View File

@ -43,8 +43,8 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/square/go-jose.v2 v2.6.0
k8s.io/api v0.0.0-20230308234233-a4afee70a903 k8s.io/api v0.0.0-20230308234233-a4afee70a903
k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7 k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc
k8s.io/client-go v0.0.0-20230309033544-64e2c7ff167c k8s.io/client-go v0.0.0-20230310084516-756a0f3ae9a9
k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73 k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73
k8s.io/klog/v2 v2.90.1 k8s.io/klog/v2 v2.90.1
k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7 k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7
@ -125,8 +125,8 @@ require (
replace ( replace (
k8s.io/api => k8s.io/api v0.0.0-20230308234233-a4afee70a903 k8s.io/api => k8s.io/api v0.0.0-20230308234233-a4afee70a903
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7 k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc
k8s.io/client-go => k8s.io/client-go v0.0.0-20230309033544-64e2c7ff167c k8s.io/client-go => k8s.io/client-go v0.0.0-20230310084516-756a0f3ae9a9
k8s.io/component-base => k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73 k8s.io/component-base => k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73
k8s.io/kms => k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7 k8s.io/kms => k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7
) )

8
go.sum
View File

@ -876,10 +876,10 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.0.0-20230308234233-a4afee70a903 h1:TmxUf1tDcGUHE8qZKLRWmn2nr2FypkwvqC2qviOUmQc= k8s.io/api v0.0.0-20230308234233-a4afee70a903 h1:TmxUf1tDcGUHE8qZKLRWmn2nr2FypkwvqC2qviOUmQc=
k8s.io/api v0.0.0-20230308234233-a4afee70a903/go.mod h1:esKbT+6XB9TZUHyxlJVQ3zUM0abhQZ81Ic68eirO+xM= k8s.io/api v0.0.0-20230308234233-a4afee70a903/go.mod h1:esKbT+6XB9TZUHyxlJVQ3zUM0abhQZ81Ic68eirO+xM=
k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7 h1:YN43Lvs3Pj9iQmuWGojeBiFdz1mkrxe0EZn7Ba3TMpQ= k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc h1:iUeJrNH0Issnw4YeNTz6uNuWuA24Eh01HhHiO3IyCg0=
k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7/go.mod h1:jlJwObMa4oKAEOMnAeEaqeiM+Fwd/CbAwNyQ7OaEwS0= k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc/go.mod h1:jlJwObMa4oKAEOMnAeEaqeiM+Fwd/CbAwNyQ7OaEwS0=
k8s.io/client-go v0.0.0-20230309033544-64e2c7ff167c h1:1IXuG9QQvPMR3GbYgBhOKre47MAIq+U41cWOGoAHpd8= k8s.io/client-go v0.0.0-20230310084516-756a0f3ae9a9 h1:Nd40oJXB53lEpVRBPPCtUaUKwkKa70OxCDKsESBeGFY=
k8s.io/client-go v0.0.0-20230309033544-64e2c7ff167c/go.mod h1:hjEB5iFHr17qVb6wnh6w2LQvO5DfoP6rzLN8NAE8K6U= k8s.io/client-go v0.0.0-20230310084516-756a0f3ae9a9/go.mod h1:hjEB5iFHr17qVb6wnh6w2LQvO5DfoP6rzLN8NAE8K6U=
k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73 h1:MEKvhkstqrRFmA9+qQlnkA/jPbZUH/VnMKiEfBeLbf8= k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73 h1:MEKvhkstqrRFmA9+qQlnkA/jPbZUH/VnMKiEfBeLbf8=
k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73/go.mod h1:MB0hQ6Wy3OOZ/dr+sy5FwxCJhDJ4hszX743ar8dd2zE= k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73/go.mod h1:MB0hQ6Wy3OOZ/dr+sy5FwxCJhDJ4hszX743ar8dd2zE=
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=

View File

@ -27,11 +27,11 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
utilerrors "k8s.io/apimachinery/pkg/util/errors" utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/discovery" "k8s.io/apiserver/pkg/endpoints/discovery"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storageversion" "k8s.io/apiserver/pkg/storageversion"
) )
@ -81,7 +81,7 @@ type APIGroupVersion struct {
Defaulter runtime.ObjectDefaulter Defaulter runtime.ObjectDefaulter
Namer runtime.Namer Namer runtime.Namer
UnsafeConvertor runtime.ObjectConvertor UnsafeConvertor runtime.ObjectConvertor
TypeConverter fieldmanager.TypeConverter TypeConverter managedfields.TypeConverter
EquivalentResourceRegistry runtime.EquivalentResourceRegistry EquivalentResourceRegistry runtime.EquivalentResourceRegistry

View File

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/warning" "k8s.io/apiserver/pkg/warning"
) )
@ -70,7 +71,7 @@ func (admit *managedFieldsValidatingAdmissionController) Admit(ctx context.Conte
return err return err
} }
managedFieldsAfterAdmission := objectMeta.GetManagedFields() managedFieldsAfterAdmission := objectMeta.GetManagedFields()
if err := ValidateManagedFields(managedFieldsAfterAdmission); err != nil { if err := managedfields.ValidateManagedFields(managedFieldsAfterAdmission); err != nil {
objectMeta.SetManagedFields(managedFieldsBeforeAdmission) objectMeta.SetManagedFields(managedFieldsBeforeAdmission)
warning.AddWarning(ctx, "", warning.AddWarning(ctx, "",
fmt.Sprintf(InvalidManagedFieldsAfterMutatingAdmissionWarningFormat, fmt.Sprintf(InvalidManagedFieldsAfterMutatingAdmissionWarningFormat,

View File

@ -17,7 +17,11 @@ limitations under the License.
package fieldmanager_test package fieldmanager_test
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"path/filepath"
"strings"
"testing" "testing"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -27,10 +31,42 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest" "k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apimachinery/pkg/util/managedfields/managedfieldstest"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
var fakeTypeConverter = func() managedfields.TypeConverter {
data, err := ioutil.ReadFile(filepath.Join(strings.Repeat(".."+string(filepath.Separator), 8),
"api", "openapi-spec", "swagger.json"))
if err != nil {
panic(err)
}
swagger := spec.Swagger{}
if err := json.Unmarshal(data, &swagger); err != nil {
panic(err)
}
definitions := map[string]*spec.Schema{}
for k, v := range swagger.Definitions {
p := v
definitions[k] = &p
}
typeConverter, err := managedfields.NewTypeConverter(definitions, false)
if err != nil {
panic(err)
}
return typeConverter
}()
func getObjectBytes(file string) []byte {
s, err := ioutil.ReadFile(file)
if err != nil {
panic(err)
}
return s
}
func BenchmarkNewObject(b *testing.B) { func BenchmarkNewObject(b *testing.B) {
tests := []struct { tests := []struct {
gvk schema.GroupVersionKind gvk schema.GroupVersionKind
@ -55,7 +91,7 @@ func BenchmarkNewObject(b *testing.B) {
} }
for _, test := range tests { for _, test := range tests {
b.Run(test.gvk.Kind, func(b *testing.B) { b.Run(test.gvk.Kind, func(b *testing.B) {
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, test.gvk) f := managedfieldstest.NewTestFieldManager(fakeTypeConverter, test.gvk)
decoder := serializer.NewCodecFactory(scheme).UniversalDecoder(test.gvk.GroupVersion()) decoder := serializer.NewCodecFactory(scheme).UniversalDecoder(test.gvk.GroupVersion())
newObj, err := runtime.Decode(decoder, test.obj) newObj, err := runtime.Decode(decoder, test.obj)
@ -268,7 +304,7 @@ func BenchmarkCompare(b *testing.B) {
} }
func BenchmarkRepeatedUpdate(b *testing.B) { func BenchmarkRepeatedUpdate(b *testing.B) {
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod")) f := managedfieldstest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"))
podBytes := getObjectBytes("pod.yaml") podBytes := getObjectBytes("pod.yaml")
var obj *corev1.Pod var obj *corev1.Pod

View File

@ -1,57 +0,0 @@
/*
Copyright 2018 The Kubernetes 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 fieldmanager
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
// FieldManager updates the managed fields and merges applied
// configurations.
type FieldManager = internal.FieldManager
// NewDefaultFieldManager creates a new FieldManager that merges apply requests
// and update managed fields for other types of requests.
func NewDefaultFieldManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, hub schema.GroupVersion, subresource string, resetFields map[fieldpath.APIVersion]*fieldpath.Set) (*FieldManager, error) {
f, err := internal.NewStructuredMergeManager(typeConverter, objectConverter, objectDefaulter, kind.GroupVersion(), hub, resetFields)
if err != nil {
return nil, fmt.Errorf("failed to create field manager: %v", err)
}
return internal.NewDefaultFieldManager(f, typeConverter, objectConverter, objectCreater, kind, subresource), nil
}
// NewDefaultCRDFieldManager creates a new FieldManager specifically for
// CRDs. This allows for the possibility of fields which are not defined
// in models, as well as having no models defined at all.
func NewDefaultCRDFieldManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, hub schema.GroupVersion, subresource string, resetFields map[fieldpath.APIVersion]*fieldpath.Set) (_ *FieldManager, err error) {
f, err := internal.NewCRDStructuredMergeManager(typeConverter, objectConverter, objectDefaulter, kind.GroupVersion(), hub, resetFields)
if err != nil {
return nil, fmt.Errorf("failed to create field manager: %v", err)
}
return internal.NewDefaultFieldManager(f, typeConverter, objectConverter, objectCreater, kind, subresource), nil
}
func ValidateManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) error {
_, err := internal.DecodeManagedFields(encodedManagedFields)
return err
}

File diff suppressed because it is too large Load Diff

View File

@ -1,66 +0,0 @@
/*
Copyright 2022 The Kubernetes 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 fieldmanagertest
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/testing"
)
// TestFieldManager is a FieldManager that can be used in test to
// simulate the behavior of Server-Side Apply and field tracking. This
// also has a few methods to get a sense of the state of the object.
//
// This TestFieldManager uses a series of "fake" objects to simulate
// some behavior which come with the limitation that you can only use
// one version since there is no version conversion logic.
//
// You can use this rather than NewDefaultTestFieldManager if you want
// to specify either a sub-resource, or a set of modified Manager to
// test them specifically.
type TestFieldManager interface {
// APIVersion of the object that we're tracking.
APIVersion() string
// Reset resets the state of the liveObject by resetting it to an empty object.
Reset()
// Live returns a copy of the current liveObject.
Live() runtime.Object
// Apply applies the given object on top of the current liveObj, for the
// given manager and force flag.
Apply(obj runtime.Object, manager string, force bool) error
// Update will updates the managed fields in the liveObj based on the
// changes performed by the update.
Update(obj runtime.Object, manager string) error
// ManagedFields returns the list of existing managed fields for the
// liveObj.
ManagedFields() []metav1.ManagedFieldsEntry
}
// NewTestFieldManager returns a new TestFieldManager built for the
// given gvk, on the main resource.
func NewTestFieldManager(typeConverter fieldmanager.TypeConverter, gvk schema.GroupVersionKind) TestFieldManager {
return testing.NewTestFieldManagerImpl(typeConverter, gvk, "", nil)
}
// NewTestFieldManagerSubresource returns a new TestFieldManager built
// for the given gvk, on the given sub-resource.
func NewTestFieldManagerSubresource(typeConverter fieldmanager.TypeConverter, gvk schema.GroupVersionKind, subresource string) TestFieldManager {
return testing.NewTestFieldManagerImpl(typeConverter, gvk, subresource, nil)
}

View File

@ -1,60 +0,0 @@
/*
Copyright 2020 The Kubernetes 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 internal
import (
"sync"
"time"
)
// AtMostEvery will never run the method more than once every specified
// duration.
type AtMostEvery struct {
delay time.Duration
lastCall time.Time
mutex sync.Mutex
}
// NewAtMostEvery creates a new AtMostEvery, that will run the method at
// most every given duration.
func NewAtMostEvery(delay time.Duration) *AtMostEvery {
return &AtMostEvery{
delay: delay,
}
}
// updateLastCall returns true if the lastCall time has been updated,
// false if it was too early.
func (s *AtMostEvery) updateLastCall() bool {
s.mutex.Lock()
defer s.mutex.Unlock()
if time.Since(s.lastCall) < s.delay {
return false
}
s.lastCall = time.Now()
return true
}
// Do will run the method if enough time has passed, and return true.
// Otherwise, it does nothing and returns false.
func (s *AtMostEvery) Do(fn func()) bool {
if !s.updateLastCall() {
return false
}
fn()
return true
}

View File

@ -1,51 +0,0 @@
/*
Copyright 2020 The Kubernetes 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 internal_test
import (
"testing"
"time"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
)
func TestAtMostEvery(t *testing.T) {
duration := time.Second
delay := 179 * time.Millisecond
atMostEvery := internal.NewAtMostEvery(delay)
count := 0
exit := time.NewTicker(duration)
tick := time.NewTicker(2 * time.Millisecond)
defer exit.Stop()
defer tick.Stop()
done := false
for !done {
select {
case <-exit.C:
done = true
case <-tick.C:
atMostEvery.Do(func() {
count++
})
}
}
if expected := int(duration/delay) + 1; count > expected {
t.Fatalf("Function called %d times, should have been called less than or equal to %d times", count, expected)
}
}

View File

@ -1,74 +0,0 @@
/*
Copyright 2019 The Kubernetes 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 internal
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type buildManagerInfoManager struct {
fieldManager Manager
groupVersion schema.GroupVersion
subresource string
}
var _ Manager = &buildManagerInfoManager{}
// NewBuildManagerInfoManager creates a new Manager that converts the manager name into a unique identifier
// combining operation and version for update requests, and just operation for apply requests.
func NewBuildManagerInfoManager(f Manager, gv schema.GroupVersion, subresource string) Manager {
return &buildManagerInfoManager{
fieldManager: f,
groupVersion: gv,
subresource: subresource,
}
}
// Update implements Manager.
func (f *buildManagerInfoManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
manager, err := f.buildManagerInfo(manager, metav1.ManagedFieldsOperationUpdate)
if err != nil {
return nil, nil, fmt.Errorf("failed to build manager identifier: %v", err)
}
return f.fieldManager.Update(liveObj, newObj, managed, manager)
}
// Apply implements Manager.
func (f *buildManagerInfoManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
manager, err := f.buildManagerInfo(manager, metav1.ManagedFieldsOperationApply)
if err != nil {
return nil, nil, fmt.Errorf("failed to build manager identifier: %v", err)
}
return f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force)
}
func (f *buildManagerInfoManager) buildManagerInfo(prefix string, operation metav1.ManagedFieldsOperationType) (string, error) {
managerInfo := metav1.ManagedFieldsEntry{
Manager: prefix,
Operation: operation,
APIVersion: f.groupVersion.String(),
Subresource: f.subresource,
}
if managerInfo.Manager == "" {
managerInfo.Manager = "unknown"
}
return BuildManagerIdentifier(&managerInfo)
}

View File

@ -1,133 +0,0 @@
/*
Copyright 2019 The Kubernetes 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 internal
import (
"fmt"
"sort"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
type capManagersManager struct {
fieldManager Manager
maxUpdateManagers int
oldUpdatesManagerName string
}
var _ Manager = &capManagersManager{}
// NewCapManagersManager creates a new wrapped FieldManager which ensures that the number of managers from updates
// does not exceed maxUpdateManagers, by merging some of the oldest entries on each update.
func NewCapManagersManager(fieldManager Manager, maxUpdateManagers int) Manager {
return &capManagersManager{
fieldManager: fieldManager,
maxUpdateManagers: maxUpdateManagers,
oldUpdatesManagerName: "ancient-changes",
}
}
// Update implements Manager.
func (f *capManagersManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
object, managed, err := f.fieldManager.Update(liveObj, newObj, managed, manager)
if err != nil {
return object, managed, err
}
if managed, err = f.capUpdateManagers(managed); err != nil {
return nil, nil, fmt.Errorf("failed to cap update managers: %v", err)
}
return object, managed, nil
}
// Apply implements Manager.
func (f *capManagersManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
return f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
}
// capUpdateManagers merges a number of the oldest update entries into versioned buckets,
// such that the number of entries from updates does not exceed f.maxUpdateManagers.
func (f *capManagersManager) capUpdateManagers(managed Managed) (newManaged Managed, err error) {
// Gather all entries from updates
updaters := []string{}
for manager, fields := range managed.Fields() {
if !fields.Applied() {
updaters = append(updaters, manager)
}
}
if len(updaters) <= f.maxUpdateManagers {
return managed, nil
}
// If we have more than the maximum, sort the update entries by time, oldest first.
sort.Slice(updaters, func(i, j int) bool {
iTime, jTime, iSeconds, jSeconds := managed.Times()[updaters[i]], managed.Times()[updaters[j]], int64(0), int64(0)
if iTime != nil {
iSeconds = iTime.Unix()
}
if jTime != nil {
jSeconds = jTime.Unix()
}
if iSeconds != jSeconds {
return iSeconds < jSeconds
}
return updaters[i] < updaters[j]
})
// Merge the oldest updaters with versioned bucket managers until the number of updaters is under the cap
versionToFirstManager := map[string]string{}
for i, length := 0, len(updaters); i < len(updaters) && length > f.maxUpdateManagers; i++ {
manager := updaters[i]
vs := managed.Fields()[manager]
time := managed.Times()[manager]
version := string(vs.APIVersion())
// Create a new manager identifier for the versioned bucket entry.
// The version for this manager comes from the version of the update being merged into the bucket.
bucket, err := BuildManagerIdentifier(&metav1.ManagedFieldsEntry{
Manager: f.oldUpdatesManagerName,
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: version,
})
if err != nil {
return managed, fmt.Errorf("failed to create bucket manager for version %v: %v", version, err)
}
// Merge the fieldets if this is not the first time the version was seen.
// Otherwise just record the manager name in versionToFirstManager
if first, ok := versionToFirstManager[version]; ok {
// If the bucket doesn't exists yet, create one.
if _, ok := managed.Fields()[bucket]; !ok {
s := managed.Fields()[first]
delete(managed.Fields(), first)
managed.Fields()[bucket] = s
}
managed.Fields()[bucket] = fieldpath.NewVersionedSet(vs.Set().Union(managed.Fields()[bucket].Set()), vs.APIVersion(), vs.Applied())
delete(managed.Fields(), manager)
length--
// Use the time from the update being merged into the bucket, since it is more recent.
managed.Times()[bucket] = time
} else {
versionToFirstManager[version] = manager
}
}
return managed, nil
}

View File

@ -1,294 +0,0 @@
/*
Copyright 2019 The Kubernetes 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 internal_test
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"time"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
internaltesting "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/testing"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
type fakeManager struct{}
var _ internal.Manager = &fakeManager{}
func (*fakeManager) Update(_, newObj runtime.Object, managed internal.Managed, _ string) (runtime.Object, internal.Managed, error) {
return newObj, managed, nil
}
func (*fakeManager) Apply(_, _ runtime.Object, _ internal.Managed, _ string, _ bool) (runtime.Object, internal.Managed, error) {
panic("not implemented")
}
func TestCapManagersManagerMergesEntries(t *testing.T) {
f := internaltesting.NewTestFieldManagerImpl(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"),
"",
func(m internal.Manager) internal.Manager {
return internal.NewCapManagersManager(m, 3)
})
podWithLabels := func(labels ...string) runtime.Object {
labelMap := map[string]interface{}{}
for _, key := range labels {
labelMap[key] = "true"
}
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"labels": labelMap,
},
},
}
obj.SetKind("Pod")
obj.SetAPIVersion("v1")
return obj
}
if err := f.Update(podWithLabels("one"), "fieldmanager_test_update_1"); err != nil {
t.Fatalf("failed to update object: %v", err)
}
expectIdempotence(t, f)
if err := f.Update(podWithLabels("one", "two"), "fieldmanager_test_update_2"); err != nil {
t.Fatalf("failed to update object: %v", err)
}
expectIdempotence(t, f)
if err := f.Update(podWithLabels("one", "two", "three"), "fieldmanager_test_update_3"); err != nil {
t.Fatalf("failed to update object: %v", err)
}
expectIdempotence(t, f)
if err := f.Update(podWithLabels("one", "two", "three", "four"), "fieldmanager_test_update_4"); err != nil {
t.Fatalf("failed to update object: %v", err)
}
expectIdempotence(t, f)
if e, a := 3, len(f.ManagedFields()); e != a {
t.Fatalf("exected %v entries in managedFields, but got %v: %#v", e, a, f.ManagedFields())
}
if e, a := "ancient-changes", f.ManagedFields()[0].Manager; e != a {
t.Fatalf("exected first manager name to be %v, but got %v: %#v", e, a, f.ManagedFields())
}
if e, a := "fieldmanager_test_update_3", f.ManagedFields()[1].Manager; e != a {
t.Fatalf("exected second manager name to be %v, but got %v: %#v", e, a, f.ManagedFields())
}
if e, a := "fieldmanager_test_update_4", f.ManagedFields()[2].Manager; e != a {
t.Fatalf("exected third manager name to be %v, but got %v: %#v", e, a, f.ManagedFields())
}
expectManagesField(t, f, "ancient-changes", fieldpath.MakePathOrDie("metadata", "labels", "one"))
expectManagesField(t, f, "ancient-changes", fieldpath.MakePathOrDie("metadata", "labels", "two"))
expectManagesField(t, f, "fieldmanager_test_update_3", fieldpath.MakePathOrDie("metadata", "labels", "three"))
expectManagesField(t, f, "fieldmanager_test_update_4", fieldpath.MakePathOrDie("metadata", "labels", "four"))
}
func TestCapUpdateManagers(t *testing.T) {
f := internaltesting.NewTestFieldManagerImpl(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"),
"",
func(m internal.Manager) internal.Manager {
return internal.NewCapManagersManager(m, 3)
})
set := func(fields ...string) *metav1.FieldsV1 {
s := fieldpath.NewSet()
for _, f := range fields {
s.Insert(fieldpath.MakePathOrDie(f))
}
b, err := s.ToJSON()
if err != nil {
panic(fmt.Sprintf("error building ManagedFieldsEntry for test: %v", err))
}
return &metav1.FieldsV1{Raw: b}
}
entry := func(name string, version string, order int, fields *metav1.FieldsV1) metav1.ManagedFieldsEntry {
return metav1.ManagedFieldsEntry{
Manager: name,
APIVersion: version,
Operation: "Update",
FieldsType: "FieldsV1",
FieldsV1: fields,
Time: &metav1.Time{Time: time.Time{}.Add(time.Hour * time.Duration(order))},
}
}
testCases := []struct {
name string
input []metav1.ManagedFieldsEntry
expected []metav1.ManagedFieldsEntry
}{
{
name: "one version, no ancient changes",
input: []metav1.ManagedFieldsEntry{
entry("update-manager1", "v1", 1, set("a")),
entry("update-manager2", "v1", 2, set("b")),
entry("update-manager3", "v1", 3, set("c")),
entry("update-manager4", "v1", 4, set("d")),
},
expected: []metav1.ManagedFieldsEntry{
entry("ancient-changes", "v1", 2, set("a", "b")),
entry("update-manager3", "v1", 3, set("c")),
entry("update-manager4", "v1", 4, set("d")),
},
}, {
name: "one version, one ancient changes",
input: []metav1.ManagedFieldsEntry{
entry("ancient-changes", "v1", 2, set("a", "b")),
entry("update-manager3", "v1", 3, set("c")),
entry("update-manager4", "v1", 4, set("d")),
entry("update-manager5", "v1", 5, set("e")),
},
expected: []metav1.ManagedFieldsEntry{
entry("ancient-changes", "v1", 3, set("a", "b", "c")),
entry("update-manager4", "v1", 4, set("d")),
entry("update-manager5", "v1", 5, set("e")),
},
}, {
name: "two versions, no ancient changes",
input: []metav1.ManagedFieldsEntry{
entry("update-manager1", "v1", 1, set("a")),
entry("update-manager2", "v2", 2, set("b")),
entry("update-manager3", "v1", 3, set("c")),
entry("update-manager4", "v1", 4, set("d")),
entry("update-manager5", "v1", 5, set("e")),
},
expected: []metav1.ManagedFieldsEntry{
entry("update-manager2", "v2", 2, set("b")),
entry("ancient-changes", "v1", 4, set("a", "c", "d")),
entry("update-manager5", "v1", 5, set("e")),
},
}, {
name: "three versions, one ancient changes",
input: []metav1.ManagedFieldsEntry{
entry("update-manager2", "v2", 2, set("b")),
entry("ancient-changes", "v1", 4, set("a", "c", "d")),
entry("update-manager5", "v1", 5, set("e")),
entry("update-manager6", "v3", 6, set("f")),
entry("update-manager7", "v2", 7, set("g")),
},
expected: []metav1.ManagedFieldsEntry{
entry("ancient-changes", "v1", 5, set("a", "c", "d", "e")),
entry("update-manager6", "v3", 6, set("f")),
entry("ancient-changes", "v2", 7, set("b", "g")),
},
}, {
name: "three versions, two ancient changes",
input: []metav1.ManagedFieldsEntry{
entry("ancient-changes", "v1", 5, set("a", "c", "d", "e")),
entry("update-manager6", "v3", 6, set("f")),
entry("ancient-changes", "v2", 7, set("b", "g")),
entry("update-manager8", "v3", 8, set("h")),
},
expected: []metav1.ManagedFieldsEntry{
entry("ancient-changes", "v1", 5, set("a", "c", "d", "e")),
entry("ancient-changes", "v2", 7, set("b", "g")),
entry("ancient-changes", "v3", 8, set("f", "h")),
},
}, {
name: "four versions, two ancient changes",
input: []metav1.ManagedFieldsEntry{
entry("ancient-changes", "v1", 5, set("a", "c", "d", "e")),
entry("update-manager6", "v3", 6, set("f")),
entry("ancient-changes", "v2", 7, set("b", "g")),
entry("update-manager8", "v4", 8, set("h")),
},
expected: []metav1.ManagedFieldsEntry{
entry("ancient-changes", "v1", 5, set("a", "c", "d", "e")),
entry("update-manager6", "v3", 6, set("f")),
entry("ancient-changes", "v2", 7, set("b", "g")),
entry("update-manager8", "v4", 8, set("h")),
},
},
}
for _, tc := range testCases {
f.Reset()
live := f.Live()
accessor, err := meta.Accessor(live)
if err != nil {
t.Fatalf("%v: couldn't get accessor: %v", tc.name, err)
}
accessor.SetManagedFields(tc.input)
if err := f.Update(live, "no-op-update"); err != nil {
t.Fatalf("%v: failed to do no-op update to object: %v", tc.name, err)
}
if e, a := tc.expected, f.ManagedFields(); !apiequality.Semantic.DeepEqual(e, a) {
t.Errorf("%v: unexpected value for managedFields:\nexpected: %v\n but got: %v", tc.name, mustMarshal(e), mustMarshal(a))
}
expectIdempotence(t, f)
}
}
// expectIdempotence does a no-op update and ensures that managedFields doesn't change by calling capUpdateManagers.
func expectIdempotence(t *testing.T, f fieldmanagertest.TestFieldManager) {
before := []metav1.ManagedFieldsEntry{}
for _, m := range f.ManagedFields() {
before = append(before, *m.DeepCopy())
}
if err := f.Update(f.Live(), "no-op-update"); err != nil {
t.Fatalf("failed to do no-op update to object: %v", err)
}
if after := f.ManagedFields(); !apiequality.Semantic.DeepEqual(before, after) {
t.Fatalf("exected idempotence, but managedFields changed:\nbefore: %v\n after: %v", mustMarshal(before), mustMarshal(after))
}
}
// expectManagesField ensures that manager m currently manages field path p.
func expectManagesField(t *testing.T, f fieldmanagertest.TestFieldManager, m string, p fieldpath.Path) {
for _, e := range f.ManagedFields() {
if e.Manager == m {
var s fieldpath.Set
err := s.FromJSON(bytes.NewReader(e.FieldsV1.Raw))
if err != nil {
t.Fatalf("error parsing managedFields for %v: %v: %#v", m, err, f.ManagedFields())
}
if !s.Has(p) {
t.Fatalf("expected managedFields for %v to contain %v, but got:\n%v", m, p.String(), s.String())
}
return
}
}
t.Fatalf("exected to find manager name %v, but got: %#v", m, f.ManagedFields())
}
func mustMarshal(i interface{}) string {
b, err := json.MarshalIndent(i, "", " ")
if err != nil {
panic(fmt.Sprintf("error marshalling %v to json: %v", i, err))
}
return string(b)
}

View File

@ -1,89 +0,0 @@
/*
Copyright 2019 The Kubernetes 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 internal
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
)
// NewConflictError returns an error including details on the requests apply conflicts
func NewConflictError(conflicts merge.Conflicts) *errors.StatusError {
causes := []metav1.StatusCause{}
for _, conflict := range conflicts {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldManagerConflict,
Message: fmt.Sprintf("conflict with %v", printManager(conflict.Manager)),
Field: conflict.Path.String(),
})
}
return errors.NewApplyConflict(causes, getConflictMessage(conflicts))
}
func getConflictMessage(conflicts merge.Conflicts) string {
if len(conflicts) == 1 {
return fmt.Sprintf("Apply failed with 1 conflict: conflict with %v: %v", printManager(conflicts[0].Manager), conflicts[0].Path)
}
m := map[string][]fieldpath.Path{}
for _, conflict := range conflicts {
m[conflict.Manager] = append(m[conflict.Manager], conflict.Path)
}
uniqueManagers := []string{}
for manager := range m {
uniqueManagers = append(uniqueManagers, manager)
}
// Print conflicts by sorted managers.
sort.Strings(uniqueManagers)
messages := []string{}
for _, manager := range uniqueManagers {
messages = append(messages, fmt.Sprintf("conflicts with %v:", printManager(manager)))
for _, path := range m[manager] {
messages = append(messages, fmt.Sprintf("- %v", path))
}
}
return fmt.Sprintf("Apply failed with %d conflicts: %s", len(conflicts), strings.Join(messages, "\n"))
}
func printManager(manager string) string {
encodedManager := &metav1.ManagedFieldsEntry{}
if err := json.Unmarshal([]byte(manager), encodedManager); err != nil {
return fmt.Sprintf("%q", manager)
}
managerStr := fmt.Sprintf("%q", encodedManager.Manager)
if encodedManager.Subresource != "" {
managerStr = fmt.Sprintf("%s with subresource %q", managerStr, encodedManager.Subresource)
}
if encodedManager.Operation == metav1.ManagedFieldsOperationUpdate {
if encodedManager.Time == nil {
return fmt.Sprintf("%s using %v", managerStr, encodedManager.APIVersion)
}
return fmt.Sprintf("%s using %v at %v", managerStr, encodedManager.APIVersion, encodedManager.Time.UTC().Format(time.RFC3339))
}
return managerStr
}

View File

@ -1,131 +0,0 @@
/*
Copyright 2019 The Kubernetes 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 internal_test
import (
"net/http"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
)
// TestNewConflictError tests that NewConflictError creates the correct StatusError for a given smd Conflicts
func TestNewConflictError(t *testing.T) {
testCases := []struct {
conflict merge.Conflicts
expected *errors.StatusError
}{
{
conflict: merge.Conflicts{
merge.Conflict{
Manager: `{"manager":"foo","operation":"Update","apiVersion":"v1","time":"2001-02-03T04:05:06Z"}`,
Path: fieldpath.MakePathOrDie("spec", "replicas"),
},
},
expected: &errors.StatusError{
ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusConflict,
Reason: metav1.StatusReasonConflict,
Details: &metav1.StatusDetails{
Causes: []metav1.StatusCause{
{
Type: metav1.CauseTypeFieldManagerConflict,
Message: `conflict with "foo" using v1 at 2001-02-03T04:05:06Z`,
Field: ".spec.replicas",
},
},
},
Message: `Apply failed with 1 conflict: conflict with "foo" using v1 at 2001-02-03T04:05:06Z: .spec.replicas`,
},
},
},
{
conflict: merge.Conflicts{
merge.Conflict{
Manager: `{"manager":"foo","operation":"Update","apiVersion":"v1","time":"2001-02-03T04:05:06Z"}`,
Path: fieldpath.MakePathOrDie("spec", "replicas"),
},
merge.Conflict{
Manager: `{"manager":"bar","operation":"Apply"}`,
Path: fieldpath.MakePathOrDie("metadata", "labels", "app"),
},
},
expected: &errors.StatusError{
ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusConflict,
Reason: metav1.StatusReasonConflict,
Details: &metav1.StatusDetails{
Causes: []metav1.StatusCause{
{
Type: metav1.CauseTypeFieldManagerConflict,
Message: `conflict with "foo" using v1 at 2001-02-03T04:05:06Z`,
Field: ".spec.replicas",
},
{
Type: metav1.CauseTypeFieldManagerConflict,
Message: `conflict with "bar"`,
Field: ".metadata.labels.app",
},
},
},
Message: `Apply failed with 2 conflicts: conflicts with "bar":
- .metadata.labels.app
conflicts with "foo" using v1 at 2001-02-03T04:05:06Z:
- .spec.replicas`,
},
},
},
{
conflict: merge.Conflicts{
merge.Conflict{
Manager: `{"manager":"foo","operation":"Update","subresource":"scale","apiVersion":"v1","time":"2001-02-03T04:05:06Z"}`,
Path: fieldpath.MakePathOrDie("spec", "replicas"),
},
},
expected: &errors.StatusError{
ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: http.StatusConflict,
Reason: metav1.StatusReasonConflict,
Details: &metav1.StatusDetails{
Causes: []metav1.StatusCause{
{
Type: metav1.CauseTypeFieldManagerConflict,
Message: `conflict with "foo" with subresource "scale" using v1 at 2001-02-03T04:05:06Z`,
Field: ".spec.replicas",
},
},
},
Message: `Apply failed with 1 conflict: conflict with "foo" with subresource "scale" using v1 at 2001-02-03T04:05:06Z: .spec.replicas`,
},
},
},
}
for _, tc := range testCases {
actual := internal.NewConflictError(tc.conflict)
if !reflect.DeepEqual(tc.expected, actual) {
t.Errorf("Expected to get\n%+v\nbut got\n%+v", tc.expected.ErrStatus, actual.ErrStatus)
}
}
}

View File

@ -1,206 +0,0 @@
/*
Copyright 2022 The Kubernetes 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 internal
import (
"fmt"
"reflect"
"time"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
"sigs.k8s.io/structured-merge-diff/v4/merge"
)
// DefaultMaxUpdateManagers defines the default maximum retained number of managedFields entries from updates
// if the number of update managers exceeds this, the oldest entries will be merged until the number is below the maximum.
// TODO(jennybuckley): Determine if this is really the best value. Ideally we wouldn't unnecessarily merge too many entries.
const DefaultMaxUpdateManagers int = 10
// DefaultTrackOnCreateProbability defines the default probability that the field management of an object
// starts being tracked from the object's creation, instead of from the first time the object is applied to.
const DefaultTrackOnCreateProbability float32 = 1
var atMostEverySecond = NewAtMostEvery(time.Second)
// FieldManager updates the managed fields and merges applied
// configurations.
type FieldManager struct {
fieldManager Manager
subresource string
}
// NewFieldManager creates a new FieldManager that decodes, manages, then re-encodes managedFields
// on update and apply requests.
func NewFieldManager(f Manager, subresource string) *FieldManager {
return &FieldManager{fieldManager: f, subresource: subresource}
}
// newDefaultFieldManager is a helper function which wraps a Manager with certain default logic.
func NewDefaultFieldManager(f Manager, typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectCreater runtime.ObjectCreater, kind schema.GroupVersionKind, subresource string) *FieldManager {
return NewFieldManager(
NewLastAppliedUpdater(
NewLastAppliedManager(
NewProbabilisticSkipNonAppliedManager(
NewCapManagersManager(
NewBuildManagerInfoManager(
NewManagedFieldsUpdater(
NewStripMetaManager(f),
), kind.GroupVersion(), subresource,
), DefaultMaxUpdateManagers,
), objectCreater, kind, DefaultTrackOnCreateProbability,
), typeConverter, objectConverter, kind.GroupVersion()),
), subresource,
)
}
func decodeLiveOrNew(liveObj, newObj runtime.Object, ignoreManagedFieldsFromRequestObject bool) (Managed, error) {
liveAccessor, err := meta.Accessor(liveObj)
if err != nil {
return nil, err
}
// We take the managedFields of the live object in case the request tries to
// manually set managedFields via a subresource.
if ignoreManagedFieldsFromRequestObject {
return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields()))
}
// If the object doesn't have metadata, we should just return without trying to
// set the managedFields at all, so creates/updates/patches will work normally.
newAccessor, err := meta.Accessor(newObj)
if err != nil {
return nil, err
}
if isResetManagedFields(newAccessor.GetManagedFields()) {
return NewEmptyManaged(), nil
}
// If the managed field is empty or we failed to decode it,
// let's try the live object. This is to prevent clients who
// don't understand managedFields from deleting it accidentally.
managed, err := DecodeManagedFields(newAccessor.GetManagedFields())
if err != nil || len(managed.Fields()) == 0 {
return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields()))
}
return managed, nil
}
func emptyManagedFieldsOnErr(managed Managed, err error) (Managed, error) {
if err != nil {
return NewEmptyManaged(), nil
}
return managed, nil
}
// Update is used when the object has already been merged (non-apply
// use-case), and simply updates the managed fields in the output
// object.
func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (object runtime.Object, err error) {
// First try to decode the managed fields provided in the update,
// This is necessary to allow directly updating managed fields.
isSubresource := f.subresource != ""
managed, err := decodeLiveOrNew(liveObj, newObj, isSubresource)
if err != nil {
return newObj, nil
}
RemoveObjectManagedFields(newObj)
if object, managed, err = f.fieldManager.Update(liveObj, newObj, managed, manager); err != nil {
return nil, err
}
if err = EncodeObjectManagedFields(object, managed); err != nil {
return nil, fmt.Errorf("failed to encode managed fields: %v", err)
}
return object, nil
}
// UpdateNoErrors is the same as Update, but it will not return
// errors. If an error happens, the object is returned with
// managedFields cleared.
func (f *FieldManager) UpdateNoErrors(liveObj, newObj runtime.Object, manager string) runtime.Object {
obj, err := f.Update(liveObj, newObj, manager)
if err != nil {
atMostEverySecond.Do(func() {
ns, name := "unknown", "unknown"
if accessor, err := meta.Accessor(newObj); err == nil {
ns = accessor.GetNamespace()
name = accessor.GetName()
}
klog.ErrorS(err, "[SHOULD NOT HAPPEN] failed to update managedFields", "versionKind",
newObj.GetObjectKind().GroupVersionKind(), "namespace", ns, "name", name)
})
// Explicitly remove managedFields on failure, so that
// we can't have garbage in it.
RemoveObjectManagedFields(newObj)
return newObj
}
return obj
}
// Returns true if the managedFields indicate that the user is trying to
// reset the managedFields, i.e. if the list is non-nil but empty, or if
// the list has one empty item.
func isResetManagedFields(managedFields []metav1.ManagedFieldsEntry) bool {
if len(managedFields) == 0 {
return managedFields != nil
}
if len(managedFields) == 1 {
return reflect.DeepEqual(managedFields[0], metav1.ManagedFieldsEntry{})
}
return false
}
// Apply is used when server-side apply is called, as it merges the
// object and updates the managed fields.
func (f *FieldManager) Apply(liveObj, appliedObj runtime.Object, manager string, force bool) (object runtime.Object, err error) {
// If the object doesn't have metadata, apply isn't allowed.
accessor, err := meta.Accessor(liveObj)
if err != nil {
return nil, fmt.Errorf("couldn't get accessor: %v", err)
}
// Decode the managed fields in the live object, since it isn't allowed in the patch.
managed, err := DecodeManagedFields(accessor.GetManagedFields())
if err != nil {
return nil, fmt.Errorf("failed to decode managed fields: %v", err)
}
object, managed, err = f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force)
if err != nil {
if conflicts, ok := err.(merge.Conflicts); ok {
return nil, NewConflictError(conflicts)
}
return nil, err
}
if err = EncodeObjectManagedFields(object, managed); err != nil {
return nil, fmt.Errorf("failed to encode managed fields: %v", err)
}
return object, nil
}

View File

@ -1,52 +0,0 @@
/*
Copyright 2019 The Kubernetes 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 internal_test
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"strings"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"k8s.io/kube-openapi/pkg/validation/spec"
)
var fakeTypeConverter = func() internal.TypeConverter {
data, err := ioutil.ReadFile(filepath.Join(
strings.Repeat(".."+string(filepath.Separator), 9),
"api", "openapi-spec", "swagger.json"))
if err != nil {
panic(err)
}
convertedDefs := map[string]*spec.Schema{}
spec := spec.Swagger{}
if err := json.Unmarshal(data, &spec); err != nil {
panic(err)
}
for k, v := range spec.Definitions {
vCopy := v
convertedDefs[k] = &vCopy
}
typeConverter, err := internal.NewTypeConverter(convertedDefs, false)
if err != nil {
panic(err)
}
return typeConverter
}()

View File

@ -1,47 +0,0 @@
/*
Copyright 2018 The Kubernetes 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 internal
import (
"bytes"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
// EmptyFields represents a set with no paths
// It looks like metav1.Fields{Raw: []byte("{}")}
var EmptyFields = func() metav1.FieldsV1 {
f, err := SetToFields(*fieldpath.NewSet())
if err != nil {
panic("should never happen")
}
return f
}()
// FieldsToSet creates a set paths from an input trie of fields
func FieldsToSet(f metav1.FieldsV1) (s fieldpath.Set, err error) {
err = s.FromJSON(bytes.NewReader(f.Raw))
return s, err
}
// SetToFields creates a trie of fields from an input set of paths
func SetToFields(s fieldpath.Set) (f metav1.FieldsV1, err error) {
f.Raw, err = s.ToJSON()
return f, err
}

View File

@ -1,145 +0,0 @@
/*
Copyright 2018 The Kubernetes 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 internal
import (
"reflect"
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
// TestFieldsRoundTrip tests that a fields trie can be round tripped as a path set
func TestFieldsRoundTrip(t *testing.T) {
tests := []metav1.FieldsV1{
{
Raw: []byte(`{"f:metadata":{".":{},"f:name":{}}}`),
},
EmptyFields,
}
for _, test := range tests {
set, err := FieldsToSet(test)
if err != nil {
t.Fatalf("Failed to create path set: %v", err)
}
output, err := SetToFields(set)
if err != nil {
t.Fatalf("Failed to create fields trie from path set: %v", err)
}
if !reflect.DeepEqual(test, output) {
t.Fatalf("Expected round-trip:\ninput: %v\noutput: %v", test, output)
}
}
}
// TestFieldsToSetError tests that errors are picked up by FieldsToSet
func TestFieldsToSetError(t *testing.T) {
tests := []struct {
fields metav1.FieldsV1
errString string
}{
{
fields: metav1.FieldsV1{
Raw: []byte(`{"k:{invalid json}":{"f:name":{},".":{}}}`),
},
errString: "ReadObjectCB",
},
}
for _, test := range tests {
_, err := FieldsToSet(test.fields)
if err == nil || !strings.Contains(err.Error(), test.errString) {
t.Fatalf("Expected error to contain %q but got: %v", test.errString, err)
}
}
}
// TestSetToFieldsError tests that errors are picked up by SetToFields
func TestSetToFieldsError(t *testing.T) {
validName := "ok"
invalidPath := fieldpath.Path([]fieldpath.PathElement{{}, {FieldName: &validName}})
tests := []struct {
set fieldpath.Set
errString string
}{
{
set: *fieldpath.NewSet(invalidPath),
errString: "invalid PathElement",
},
}
for _, test := range tests {
_, err := SetToFields(test.set)
if err == nil || !strings.Contains(err.Error(), test.errString) {
t.Fatalf("Expected error to contain %q but got: %v", test.errString, err)
}
}
}
func BenchmarkSetToFields(b *testing.B) {
set := fieldpath.NewSet(
fieldpath.MakePathOrDie("foo", 0, "bar", "baz"),
fieldpath.MakePathOrDie("foo", 0, "bar", "zot"),
fieldpath.MakePathOrDie("foo", 0, "bar"),
fieldpath.MakePathOrDie("foo", 0),
fieldpath.MakePathOrDie("foo", 1, "bar", "baz"),
fieldpath.MakePathOrDie("foo", 1, "bar"),
fieldpath.MakePathOrDie("qux", fieldpath.KeyByFields("name", "first")),
fieldpath.MakePathOrDie("qux", fieldpath.KeyByFields("name", "first"), "bar"),
fieldpath.MakePathOrDie("qux", fieldpath.KeyByFields("name", "second"), "bar"),
)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
_, err := SetToFields(*set)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkFieldsToSet(b *testing.B) {
set := fieldpath.NewSet(
fieldpath.MakePathOrDie("foo", 0, "bar", "baz"),
fieldpath.MakePathOrDie("foo", 0, "bar", "zot"),
fieldpath.MakePathOrDie("foo", 0, "bar"),
fieldpath.MakePathOrDie("foo", 0),
fieldpath.MakePathOrDie("foo", 1, "bar", "baz"),
fieldpath.MakePathOrDie("foo", 1, "bar"),
fieldpath.MakePathOrDie("qux", fieldpath.KeyByFields("name", "first")),
fieldpath.MakePathOrDie("qux", fieldpath.KeyByFields("name", "first"), "bar"),
fieldpath.MakePathOrDie("qux", fieldpath.KeyByFields("name", "second"), "bar"),
)
fields, err := SetToFields(*set)
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
_, err := FieldsToSet(fields)
if err != nil {
b.Fatal(err)
}
}
}

View File

@ -1,50 +0,0 @@
/*
Copyright 2022 The Kubernetes 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 internal
import (
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/runtime"
)
// LastAppliedConfigAnnotation is the annotation used to store the previous
// configuration of a resource for use in a three way diff by UpdateApplyAnnotation.
//
// This is a copy of the corev1 annotation since we don't want to depend on the whole package.
const LastAppliedConfigAnnotation = "kubectl.kubernetes.io/last-applied-configuration"
// SetLastApplied sets the last-applied annotation the given value in
// the object.
func SetLastApplied(obj runtime.Object, value string) error {
accessor, err := meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
var annotations = accessor.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[LastAppliedConfigAnnotation] = value
if err := apimachineryvalidation.ValidateAnnotationsSize(annotations); err != nil {
delete(annotations, LastAppliedConfigAnnotation)
}
accessor.SetAnnotations(annotations)
return nil
}

View File

@ -1,171 +0,0 @@
/*
Copyright 2020 The Kubernetes 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 internal
import (
"encoding/json"
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
)
type lastAppliedManager struct {
fieldManager Manager
typeConverter TypeConverter
objectConverter runtime.ObjectConvertor
groupVersion schema.GroupVersion
}
var _ Manager = &lastAppliedManager{}
// NewLastAppliedManager converts the client-side apply annotation to
// server-side apply managed fields
func NewLastAppliedManager(fieldManager Manager, typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, groupVersion schema.GroupVersion) Manager {
return &lastAppliedManager{
fieldManager: fieldManager,
typeConverter: typeConverter,
objectConverter: objectConverter,
groupVersion: groupVersion,
}
}
// Update implements Manager.
func (f *lastAppliedManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
return f.fieldManager.Update(liveObj, newObj, managed, manager)
}
// Apply will consider the last-applied annotation
// for upgrading an object managed by client-side apply to server-side apply
// without conflicts.
func (f *lastAppliedManager) Apply(liveObj, newObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
newLiveObj, newManaged, newErr := f.fieldManager.Apply(liveObj, newObj, managed, manager, force)
// Upgrade the client-side apply annotation only from kubectl server-side-apply.
// To opt-out of this behavior, users may specify a different field manager.
if manager != "kubectl" {
return newLiveObj, newManaged, newErr
}
// Check if we have conflicts
if newErr == nil {
return newLiveObj, newManaged, newErr
}
conflicts, ok := newErr.(merge.Conflicts)
if !ok {
return newLiveObj, newManaged, newErr
}
conflictSet := conflictsToSet(conflicts)
// Check if conflicts are allowed due to client-side apply,
// and if so, then force apply
allowedConflictSet, err := f.allowedConflictsFromLastApplied(liveObj)
if err != nil {
return newLiveObj, newManaged, newErr
}
if !conflictSet.Difference(allowedConflictSet).Empty() {
newConflicts := conflictsDifference(conflicts, allowedConflictSet)
return newLiveObj, newManaged, newConflicts
}
return f.fieldManager.Apply(liveObj, newObj, managed, manager, true)
}
func (f *lastAppliedManager) allowedConflictsFromLastApplied(liveObj runtime.Object) (*fieldpath.Set, error) {
var accessor, err = meta.Accessor(liveObj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
// If there is no client-side apply annotation, then there is nothing to do
var annotations = accessor.GetAnnotations()
if annotations == nil {
return nil, fmt.Errorf("no last applied annotation")
}
var lastApplied, ok = annotations[LastAppliedConfigAnnotation]
if !ok || lastApplied == "" {
return nil, fmt.Errorf("no last applied annotation")
}
liveObjVersioned, err := f.objectConverter.ConvertToVersion(liveObj, f.groupVersion)
if err != nil {
return nil, fmt.Errorf("failed to convert live obj to versioned: %v", err)
}
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned)
if err != nil {
return nil, fmt.Errorf("failed to convert live obj to typed: %v", err)
}
var lastAppliedObj = &unstructured.Unstructured{Object: map[string]interface{}{}}
err = json.Unmarshal([]byte(lastApplied), lastAppliedObj)
if err != nil {
return nil, fmt.Errorf("failed to decode last applied obj: %v in '%s'", err, lastApplied)
}
if lastAppliedObj.GetAPIVersion() != f.groupVersion.String() {
return nil, fmt.Errorf("expected version of last applied to match live object '%s', but got '%s': %v", f.groupVersion.String(), lastAppliedObj.GetAPIVersion(), err)
}
lastAppliedObjTyped, err := f.typeConverter.ObjectToTyped(lastAppliedObj)
if err != nil {
return nil, fmt.Errorf("failed to convert last applied to typed: %v", err)
}
lastAppliedObjFieldSet, err := lastAppliedObjTyped.ToFieldSet()
if err != nil {
return nil, fmt.Errorf("failed to create fieldset for last applied object: %v", err)
}
comparison, err := lastAppliedObjTyped.Compare(liveObjTyped)
if err != nil {
return nil, fmt.Errorf("failed to compare last applied object and live object: %v", err)
}
// Remove fields in last applied that are different, added, or missing in
// the live object.
// Because last-applied fields don't match the live object fields,
// then we don't own these fields.
lastAppliedObjFieldSet = lastAppliedObjFieldSet.
Difference(comparison.Modified).
Difference(comparison.Added).
Difference(comparison.Removed)
return lastAppliedObjFieldSet, nil
}
// TODO: replace with merge.Conflicts.ToSet()
func conflictsToSet(conflicts merge.Conflicts) *fieldpath.Set {
conflictSet := fieldpath.NewSet()
for _, conflict := range []merge.Conflict(conflicts) {
conflictSet.Insert(conflict.Path)
}
return conflictSet
}
func conflictsDifference(conflicts merge.Conflicts, s *fieldpath.Set) merge.Conflicts {
newConflicts := []merge.Conflict{}
for _, conflict := range []merge.Conflict(conflicts) {
if !s.Has(conflict.Path) {
newConflicts = append(newConflicts, conflict)
}
}
return newConflicts
}

View File

@ -1,102 +0,0 @@
/*
Copyright 2020 The Kubernetes 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 internal
import (
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
type lastAppliedUpdater struct {
fieldManager Manager
}
var _ Manager = &lastAppliedUpdater{}
// NewLastAppliedUpdater sets the client-side apply annotation up to date with
// server-side apply managed fields
func NewLastAppliedUpdater(fieldManager Manager) Manager {
return &lastAppliedUpdater{
fieldManager: fieldManager,
}
}
// Update implements Manager.
func (f *lastAppliedUpdater) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
return f.fieldManager.Update(liveObj, newObj, managed, manager)
}
// server-side apply managed fields
func (f *lastAppliedUpdater) Apply(liveObj, newObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
liveObj, managed, err := f.fieldManager.Apply(liveObj, newObj, managed, manager, force)
if err != nil {
return liveObj, managed, err
}
// Sync the client-side apply annotation only from kubectl server-side apply.
// To opt-out of this behavior, users may specify a different field manager.
//
// If the client-side apply annotation doesn't exist,
// then continue because we have no annotation to update
if manager == "kubectl" && hasLastApplied(liveObj) {
lastAppliedValue, err := buildLastApplied(newObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to build last-applied annotation: %v", err)
}
err = SetLastApplied(liveObj, lastAppliedValue)
if err != nil {
return nil, nil, fmt.Errorf("failed to set last-applied annotation: %v", err)
}
}
return liveObj, managed, err
}
func hasLastApplied(obj runtime.Object) bool {
var accessor, err = meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
var annotations = accessor.GetAnnotations()
if annotations == nil {
return false
}
lastApplied, ok := annotations[LastAppliedConfigAnnotation]
return ok && len(lastApplied) > 0
}
func buildLastApplied(obj runtime.Object) (string, error) {
obj = obj.DeepCopyObject()
var accessor, err = meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
// Remove the annotation from the object before encoding the object
var annotations = accessor.GetAnnotations()
delete(annotations, LastAppliedConfigAnnotation)
accessor.SetAnnotations(annotations)
lastApplied, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
if err != nil {
return "", fmt.Errorf("couldn't encode object into last applied annotation: %v", err)
}
return string(lastApplied), nil
}

View File

@ -1,268 +0,0 @@
/*
Copyright 2020 The Kubernetes 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 internal_test
import (
"encoding/json"
"fmt"
"strings"
"testing"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
internaltesting "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/testing"
"sigs.k8s.io/yaml"
)
func TestLastAppliedUpdater(t *testing.T) {
f := internaltesting.NewTestFieldManagerImpl(fakeTypeConverter, schema.FromAPIVersionAndKind("apps/v1", "Deployment"),
"",
func(m internal.Manager) internal.Manager {
return internal.NewLastAppliedUpdater(m)
})
originalLastApplied := `nonempty`
appliedObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
appliedDeployment := []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
annotations:
"kubectl.kubernetes.io/last-applied-configuration": "` + originalLastApplied + `"
labels:
app: my-app
spec:
replicas: 20
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-c
image: my-image
`)
if err := yaml.Unmarshal(appliedDeployment, &appliedObj.Object); err != nil {
t.Errorf("error decoding YAML: %v", err)
}
if err := f.Apply(appliedObj, "NOT-KUBECTL", false); err != nil {
t.Errorf("error applying object: %v", err)
}
lastApplied, err := getLastApplied(f.Live())
if err != nil {
t.Errorf("failed to get last applied: %v", err)
}
if lastApplied != originalLastApplied {
t.Errorf("expected last applied annotation to be %q and NOT be updated, but got: %q", originalLastApplied, lastApplied)
}
if err := f.Apply(appliedObj, "kubectl", false); err != nil {
t.Errorf("error applying object: %v", err)
}
lastApplied, err = getLastApplied(f.Live())
if err != nil {
t.Errorf("failed to get last applied: %v", err)
}
if lastApplied == originalLastApplied ||
!strings.Contains(lastApplied, "my-app") ||
!strings.Contains(lastApplied, "my-image") {
t.Errorf("expected last applied annotation to be updated, but got: %q", lastApplied)
}
}
func TestLargeLastApplied(t *testing.T) {
tests := []struct {
name string
oldObject *unstructured.Unstructured
newObject *unstructured.Unstructured
}{
{
name: "old object + new object last-applied annotation is too big",
oldObject: func() *unstructured.Unstructured {
u := &unstructured.Unstructured{}
err := json.Unmarshal([]byte(`
{
"metadata": {
"name": "large-update-test-cm",
"namespace": "default",
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "nonempty"
}
},
"apiVersion": "v1",
"kind": "ConfigMap",
"data": {
"k": "v"
}
}`), &u)
if err != nil {
panic(err)
}
return u
}(),
newObject: func() *unstructured.Unstructured {
u := &unstructured.Unstructured{}
err := json.Unmarshal([]byte(`
{
"metadata": {
"name": "large-update-test-cm",
"namespace": "default",
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "nonempty"
}
},
"apiVersion": "v1",
"kind": "ConfigMap",
"data": {
"k": "v"
}
}`), &u)
if err != nil {
panic(err)
}
for i := 0; i < 9999; i++ {
unique := fmt.Sprintf("this-key-is-very-long-so-as-to-create-a-very-large-serialized-fieldset-%v", i)
unstructured.SetNestedField(u.Object, "A", "data", unique)
}
return u
}(),
},
{
name: "old object + new object annotations + new object last-applied annotation is too big",
oldObject: func() *unstructured.Unstructured {
u := &unstructured.Unstructured{}
err := json.Unmarshal([]byte(`
{
"metadata": {
"name": "large-update-test-cm",
"namespace": "default",
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "nonempty"
}
},
"apiVersion": "v1",
"kind": "ConfigMap",
"data": {
"k": "v"
}
}`), &u)
if err != nil {
panic(err)
}
for i := 0; i < 2000; i++ {
unique := fmt.Sprintf("this-key-is-very-long-so-as-to-create-a-very-large-serialized-fieldset-%v", i)
unstructured.SetNestedField(u.Object, "A", "data", unique)
}
return u
}(),
newObject: func() *unstructured.Unstructured {
u := &unstructured.Unstructured{}
err := json.Unmarshal([]byte(`
{
"metadata": {
"name": "large-update-test-cm",
"namespace": "default",
"annotations": {
"kubectl.kubernetes.io/last-applied-configuration": "nonempty"
}
},
"apiVersion": "v1",
"kind": "ConfigMap",
"data": {
"k": "v"
}
}`), &u)
if err != nil {
panic(err)
}
for i := 0; i < 2000; i++ {
unique := fmt.Sprintf("this-key-is-very-long-so-as-to-create-a-very-large-serialized-fieldset-%v", i)
unstructured.SetNestedField(u.Object, "A", "data", unique)
unstructured.SetNestedField(u.Object, "A", "metadata", "annotations", unique)
}
return u
}(),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f := internaltesting.NewTestFieldManagerImpl(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ConfigMap"),
"",
func(m internal.Manager) internal.Manager {
return internal.NewLastAppliedUpdater(m)
})
if err := f.Apply(test.oldObject, "kubectl", false); err != nil {
t.Errorf("Error applying object: %v", err)
}
lastApplied, err := getLastApplied(f.Live())
if err != nil {
t.Errorf("Failed to access last applied annotation: %v", err)
}
if len(lastApplied) == 0 || lastApplied == "nonempty" {
t.Errorf("Expected an updated last-applied annotation, but got: %q", lastApplied)
}
if err := f.Apply(test.newObject, "kubectl", false); err != nil {
t.Errorf("Error applying object: %v", err)
}
accessor := meta.NewAccessor()
annotations, err := accessor.Annotations(f.Live())
if err != nil {
t.Errorf("Failed to access annotations: %v", err)
}
if annotations == nil {
t.Errorf("No annotations on obj: %v", f.Live())
}
lastApplied, ok := annotations[internal.LastAppliedConfigAnnotation]
if ok || len(lastApplied) > 0 {
t.Errorf("Expected no last applied annotation, but got last applied with length: %d", len(lastApplied))
}
})
}
}
func getLastApplied(obj runtime.Object) (string, error) {
accessor := meta.NewAccessor()
annotations, err := accessor.Annotations(obj)
if err != nil {
return "", fmt.Errorf("failed to access annotations: %v", err)
}
if annotations == nil {
return "", fmt.Errorf("no annotations on obj: %v", obj)
}
lastApplied, ok := annotations[internal.LastAppliedConfigAnnotation]
if !ok {
return "", fmt.Errorf("expected last applied annotation, but got none for object: %v", obj)
}
return lastApplied, nil
}

View File

@ -1,248 +0,0 @@
/*
Copyright 2018 The Kubernetes 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 internal
import (
"encoding/json"
"fmt"
"sort"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
// ManagedInterface groups a fieldpath.ManagedFields together with the timestamps associated with each operation.
type ManagedInterface interface {
// Fields gets the fieldpath.ManagedFields.
Fields() fieldpath.ManagedFields
// Times gets the timestamps associated with each operation.
Times() map[string]*metav1.Time
}
type managedStruct struct {
fields fieldpath.ManagedFields
times map[string]*metav1.Time
}
var _ ManagedInterface = &managedStruct{}
// Fields implements ManagedInterface.
func (m *managedStruct) Fields() fieldpath.ManagedFields {
return m.fields
}
// Times implements ManagedInterface.
func (m *managedStruct) Times() map[string]*metav1.Time {
return m.times
}
// NewEmptyManaged creates an empty ManagedInterface.
func NewEmptyManaged() ManagedInterface {
return NewManaged(fieldpath.ManagedFields{}, map[string]*metav1.Time{})
}
// NewManaged creates a ManagedInterface from a fieldpath.ManagedFields and the timestamps associated with each operation.
func NewManaged(f fieldpath.ManagedFields, t map[string]*metav1.Time) ManagedInterface {
return &managedStruct{
fields: f,
times: t,
}
}
// RemoveObjectManagedFields removes the ManagedFields from the object
// before we merge so that it doesn't appear in the ManagedFields
// recursively.
func RemoveObjectManagedFields(obj runtime.Object) {
accessor, err := meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
accessor.SetManagedFields(nil)
}
// EncodeObjectManagedFields converts and stores the fieldpathManagedFields into the objects ManagedFields
func EncodeObjectManagedFields(obj runtime.Object, managed ManagedInterface) error {
accessor, err := meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
encodedManagedFields, err := encodeManagedFields(managed)
if err != nil {
return fmt.Errorf("failed to convert back managed fields to API: %v", err)
}
accessor.SetManagedFields(encodedManagedFields)
return nil
}
// DecodeManagedFields converts ManagedFields from the wire format (api format)
// to the format used by sigs.k8s.io/structured-merge-diff
func DecodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (ManagedInterface, error) {
managed := managedStruct{}
managed.fields = make(fieldpath.ManagedFields, len(encodedManagedFields))
managed.times = make(map[string]*metav1.Time, len(encodedManagedFields))
for i, encodedVersionedSet := range encodedManagedFields {
switch encodedVersionedSet.Operation {
case metav1.ManagedFieldsOperationApply, metav1.ManagedFieldsOperationUpdate:
default:
return nil, fmt.Errorf("operation must be `Apply` or `Update`")
}
if len(encodedVersionedSet.APIVersion) < 1 {
return nil, fmt.Errorf("apiVersion must not be empty")
}
switch encodedVersionedSet.FieldsType {
case "FieldsV1":
// Valid case.
case "":
return nil, fmt.Errorf("missing fieldsType in managed fields entry %d", i)
default:
return nil, fmt.Errorf("invalid fieldsType %q in managed fields entry %d", encodedVersionedSet.FieldsType, i)
}
manager, err := BuildManagerIdentifier(&encodedVersionedSet)
if err != nil {
return nil, fmt.Errorf("error decoding manager from %v: %v", encodedVersionedSet, err)
}
managed.fields[manager], err = decodeVersionedSet(&encodedVersionedSet)
if err != nil {
return nil, fmt.Errorf("error decoding versioned set from %v: %v", encodedVersionedSet, err)
}
managed.times[manager] = encodedVersionedSet.Time
}
return &managed, nil
}
// BuildManagerIdentifier creates a manager identifier string from a ManagedFieldsEntry
func BuildManagerIdentifier(encodedManager *metav1.ManagedFieldsEntry) (manager string, err error) {
encodedManagerCopy := *encodedManager
// Never include fields type in the manager identifier
encodedManagerCopy.FieldsType = ""
// Never include the fields in the manager identifier
encodedManagerCopy.FieldsV1 = nil
// Never include the time in the manager identifier
encodedManagerCopy.Time = nil
// For appliers, don't include the APIVersion in the manager identifier,
// so it will always have the same manager identifier each time it applied.
if encodedManager.Operation == metav1.ManagedFieldsOperationApply {
encodedManagerCopy.APIVersion = ""
}
// Use the remaining fields to build the manager identifier
b, err := json.Marshal(&encodedManagerCopy)
if err != nil {
return "", fmt.Errorf("error marshalling manager identifier: %v", err)
}
return string(b), nil
}
func decodeVersionedSet(encodedVersionedSet *metav1.ManagedFieldsEntry) (versionedSet fieldpath.VersionedSet, err error) {
fields := EmptyFields
if encodedVersionedSet.FieldsV1 != nil {
fields = *encodedVersionedSet.FieldsV1
}
set, err := FieldsToSet(fields)
if err != nil {
return nil, fmt.Errorf("error decoding set: %v", err)
}
return fieldpath.NewVersionedSet(&set, fieldpath.APIVersion(encodedVersionedSet.APIVersion), encodedVersionedSet.Operation == metav1.ManagedFieldsOperationApply), nil
}
// encodeManagedFields converts ManagedFields from the format used by
// sigs.k8s.io/structured-merge-diff to the wire format (api format)
func encodeManagedFields(managed ManagedInterface) (encodedManagedFields []metav1.ManagedFieldsEntry, err error) {
if len(managed.Fields()) == 0 {
return nil, nil
}
encodedManagedFields = []metav1.ManagedFieldsEntry{}
for manager := range managed.Fields() {
versionedSet := managed.Fields()[manager]
v, err := encodeManagerVersionedSet(manager, versionedSet)
if err != nil {
return nil, fmt.Errorf("error encoding versioned set for %v: %v", manager, err)
}
if t, ok := managed.Times()[manager]; ok {
v.Time = t
}
encodedManagedFields = append(encodedManagedFields, *v)
}
return sortEncodedManagedFields(encodedManagedFields)
}
func sortEncodedManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (sortedManagedFields []metav1.ManagedFieldsEntry, err error) {
sort.Slice(encodedManagedFields, func(i, j int) bool {
p, q := encodedManagedFields[i], encodedManagedFields[j]
if p.Operation != q.Operation {
return p.Operation < q.Operation
}
pSeconds, qSeconds := int64(0), int64(0)
if p.Time != nil {
pSeconds = p.Time.Unix()
}
if q.Time != nil {
qSeconds = q.Time.Unix()
}
if pSeconds != qSeconds {
return pSeconds < qSeconds
}
if p.Manager != q.Manager {
return p.Manager < q.Manager
}
if p.APIVersion != q.APIVersion {
return p.APIVersion < q.APIVersion
}
return p.Subresource < q.Subresource
})
return encodedManagedFields, nil
}
func encodeManagerVersionedSet(manager string, versionedSet fieldpath.VersionedSet) (encodedVersionedSet *metav1.ManagedFieldsEntry, err error) {
encodedVersionedSet = &metav1.ManagedFieldsEntry{}
// Get as many fields as we can from the manager identifier
err = json.Unmarshal([]byte(manager), encodedVersionedSet)
if err != nil {
return nil, fmt.Errorf("error unmarshalling manager identifier %v: %v", manager, err)
}
// Get the APIVersion, Operation, and Fields from the VersionedSet
encodedVersionedSet.APIVersion = string(versionedSet.APIVersion())
if versionedSet.Applied() {
encodedVersionedSet.Operation = metav1.ManagedFieldsOperationApply
}
encodedVersionedSet.FieldsType = "FieldsV1"
fields, err := SetToFields(*versionedSet.Set())
if err != nil {
return nil, fmt.Errorf("error encoding set: %v", err)
}
encodedVersionedSet.FieldsV1 = &fields
return encodedVersionedSet, nil
}

View File

@ -1,520 +0,0 @@
/*
Copyright 2018 The Kubernetes 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 internal
import (
"fmt"
"reflect"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
// TestHasFieldsType makes sure that we fail if we don't have a
// FieldsType set properly.
func TestHasFieldsType(t *testing.T) {
var unmarshaled []metav1.ManagedFieldsEntry
if err := yaml.Unmarshal([]byte(`- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:field: {}
manager: foo
operation: Apply
`), &unmarshaled); err != nil {
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
}
if _, err := DecodeManagedFields(unmarshaled); err != nil {
t.Fatalf("did not expect decoding error but got: %v", err)
}
// Invalid fieldsType V2.
if err := yaml.Unmarshal([]byte(`- apiVersion: v1
fieldsType: FieldsV2
fieldsV1:
f:field: {}
manager: foo
operation: Apply
`), &unmarshaled); err != nil {
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
}
if _, err := DecodeManagedFields(unmarshaled); err == nil {
t.Fatal("Expect decoding error but got none")
}
// Missing fieldsType.
if err := yaml.Unmarshal([]byte(`- apiVersion: v1
fieldsV1:
f:field: {}
manager: foo
operation: Apply
`), &unmarshaled); err != nil {
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
}
if _, err := DecodeManagedFields(unmarshaled); err == nil {
t.Fatal("Expect decoding error but got none")
}
}
// TestHasAPIVersion makes sure that we fail if we don't have an
// APIVersion set.
func TestHasAPIVersion(t *testing.T) {
var unmarshaled []metav1.ManagedFieldsEntry
if err := yaml.Unmarshal([]byte(`- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:field: {}
manager: foo
operation: Apply
`), &unmarshaled); err != nil {
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
}
if _, err := DecodeManagedFields(unmarshaled); err != nil {
t.Fatalf("did not expect decoding error but got: %v", err)
}
// Missing apiVersion.
unmarshaled = nil
if err := yaml.Unmarshal([]byte(`- fieldsType: FieldsV1
fieldsV1:
f:field: {}
manager: foo
operation: Apply
`), &unmarshaled); err != nil {
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
}
if _, err := DecodeManagedFields(unmarshaled); err == nil {
t.Fatal("Expect decoding error but got none")
}
}
// TestHasOperation makes sure that we fail if we don't have an
// Operation set properly.
func TestHasOperation(t *testing.T) {
var unmarshaled []metav1.ManagedFieldsEntry
if err := yaml.Unmarshal([]byte(`- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:field: {}
manager: foo
operation: Apply
`), &unmarshaled); err != nil {
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
}
if _, err := DecodeManagedFields(unmarshaled); err != nil {
t.Fatalf("did not expect decoding error but got: %v", err)
}
// Invalid operation.
if err := yaml.Unmarshal([]byte(`- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:field: {}
manager: foo
operation: Invalid
`), &unmarshaled); err != nil {
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
}
if _, err := DecodeManagedFields(unmarshaled); err == nil {
t.Fatal("Expect decoding error but got none")
}
// Missing operation.
unmarshaled = nil
if err := yaml.Unmarshal([]byte(`- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:field: {}
manager: foo
`), &unmarshaled); err != nil {
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
}
if _, err := DecodeManagedFields(unmarshaled); err == nil {
t.Fatal("Expect decoding error but got none")
}
}
// TestRoundTripManagedFields will roundtrip ManagedFields from the wire format
// (api format) to the format used by sigs.k8s.io/structured-merge-diff and back
func TestRoundTripManagedFields(t *testing.T) {
tests := []string{
`null
`,
`- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
v:3:
f:alsoPi: {}
v:3.1415:
f:pi: {}
v:false:
f:notTrue: {}
manager: foo
operation: Update
time: "2001-02-03T04:05:06Z"
- apiVersion: v1beta1
fieldsType: FieldsV1
fieldsV1:
i:5:
f:i: {}
manager: foo
operation: Update
time: "2011-12-13T14:15:16Z"
`,
`- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:spec:
f:containers:
k:{"name":"c"}:
f:image: {}
f:name: {}
manager: foo
operation: Apply
`,
`- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:apiVersion: {}
f:kind: {}
f:metadata:
f:labels:
f:app: {}
f:name: {}
f:spec:
f:replicas: {}
f:selector:
f:matchLabels:
f:app: {}
f:template:
f:medatada:
f:labels:
f:app: {}
f:spec:
f:containers:
k:{"name":"nginx"}:
.: {}
f:image: {}
f:name: {}
f:ports:
i:0:
f:containerPort: {}
manager: foo
operation: Update
`,
`- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:allowVolumeExpansion: {}
f:apiVersion: {}
f:kind: {}
f:metadata:
f:name: {}
f:parameters:
f:resturl: {}
f:restuser: {}
f:secretName: {}
f:secretNamespace: {}
f:provisioner: {}
manager: foo
operation: Apply
`,
`- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:apiVersion: {}
f:kind: {}
f:metadata:
f:name: {}
f:spec:
f:group: {}
f:names:
f:kind: {}
f:plural: {}
f:shortNames:
i:0: {}
f:singular: {}
f:scope: {}
f:versions:
k:{"name":"v1"}:
f:name: {}
f:served: {}
f:storage: {}
manager: foo
operation: Update
`,
`- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:spec:
f:replicas: {}
manager: foo
operation: Update
subresource: scale
`,
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
var unmarshaled []metav1.ManagedFieldsEntry
if err := yaml.Unmarshal([]byte(test), &unmarshaled); err != nil {
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
}
decoded, err := DecodeManagedFields(unmarshaled)
if err != nil {
t.Fatalf("did not expect decoding error but got: %v", err)
}
encoded, err := encodeManagedFields(decoded)
if err != nil {
t.Fatalf("did not expect encoding error but got: %v", err)
}
marshaled, err := yaml.Marshal(&encoded)
if err != nil {
t.Fatalf("did not expect yaml marshalling error but got: %v", err)
}
if !reflect.DeepEqual(string(marshaled), test) {
t.Fatalf("expected:\n%v\nbut got:\n%v", test, string(marshaled))
}
})
}
}
func TestBuildManagerIdentifier(t *testing.T) {
tests := []struct {
managedFieldsEntry string
expected string
}{
{
managedFieldsEntry: `
apiVersion: v1
fieldsV1:
f:apiVersion: {}
manager: foo
operation: Update
time: "2001-02-03T04:05:06Z"
`,
expected: "{\"manager\":\"foo\",\"operation\":\"Update\",\"apiVersion\":\"v1\"}",
},
{
managedFieldsEntry: `
apiVersion: v1
fieldsV1:
f:apiVersion: {}
manager: foo
operation: Apply
time: "2001-02-03T04:05:06Z"
`,
expected: "{\"manager\":\"foo\",\"operation\":\"Apply\"}",
},
{
managedFieldsEntry: `
apiVersion: v1
fieldsV1:
f:apiVersion: {}
manager: foo
operation: Apply
subresource: scale
time: "2001-02-03T04:05:06Z"
`,
expected: "{\"manager\":\"foo\",\"operation\":\"Apply\",\"subresource\":\"scale\"}",
},
}
for _, test := range tests {
t.Run(test.managedFieldsEntry, func(t *testing.T) {
var unmarshaled metav1.ManagedFieldsEntry
if err := yaml.Unmarshal([]byte(test.managedFieldsEntry), &unmarshaled); err != nil {
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
}
decoded, err := BuildManagerIdentifier(&unmarshaled)
if err != nil {
t.Fatalf("did not expect decoding error but got: %v", err)
}
if !reflect.DeepEqual(decoded, test.expected) {
t.Fatalf("expected:\n%v\nbut got:\n%v", test.expected, decoded)
}
})
}
}
func TestSortEncodedManagedFields(t *testing.T) {
tests := []struct {
name string
managedFields []metav1.ManagedFieldsEntry
expected []metav1.ManagedFieldsEntry
}{
{
name: "empty",
managedFields: []metav1.ManagedFieldsEntry{},
expected: []metav1.ManagedFieldsEntry{},
},
{
name: "nil",
managedFields: nil,
expected: nil,
},
{
name: "remains untouched",
managedFields: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "a", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
},
expected: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "a", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
},
},
{
name: "manager without time first",
managedFields: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
},
expected: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
},
},
{
name: "manager without time first name last",
managedFields: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
{Manager: "b", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
},
expected: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "b", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
},
},
{
name: "apply first",
managedFields: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
},
expected: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "a", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
},
},
{
name: "newest last",
managedFields: []metav1.ManagedFieldsEntry{
{Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2002-01-01T01:00:00Z")},
{Manager: "b", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
},
expected: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "b", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
{Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2002-01-01T01:00:00Z")},
},
},
{
name: "manager last",
managedFields: []metav1.ManagedFieldsEntry{
{Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "d", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
{Manager: "b", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
},
expected: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "b", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
{Manager: "d", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
},
},
{
name: "manager sorted",
managedFields: []metav1.ManagedFieldsEntry{
{Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "g", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "f", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2002-01-01T01:00:00Z")},
{Manager: "i", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "d", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2002-01-01T01:00:00Z")},
{Manager: "h", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "e", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2003-01-01T01:00:00Z")},
{Manager: "b", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
},
expected: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "b", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "g", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "h", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "i", Operation: metav1.ManagedFieldsOperationApply, Time: nil},
{Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2001-01-01T01:00:00Z")},
{Manager: "d", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2002-01-01T01:00:00Z")},
{Manager: "f", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2002-01-01T01:00:00Z")},
{Manager: "e", Operation: metav1.ManagedFieldsOperationUpdate, Time: parseTimeOrPanic("2003-01-01T01:00:00Z")},
},
},
{
name: "sort drops nanoseconds",
managedFields: []metav1.ManagedFieldsEntry{
{Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{Time: time.Date(2000, time.January, 0, 0, 0, 0, 1, time.UTC)}},
{Manager: "a", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{Time: time.Date(2000, time.January, 0, 0, 0, 0, 2, time.UTC)}},
{Manager: "b", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{Time: time.Date(2000, time.January, 0, 0, 0, 0, 3, time.UTC)}},
},
expected: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{Time: time.Date(2000, time.January, 0, 0, 0, 0, 2, time.UTC)}},
{Manager: "b", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{Time: time.Date(2000, time.January, 0, 0, 0, 0, 3, time.UTC)}},
{Manager: "c", Operation: metav1.ManagedFieldsOperationUpdate, Time: &metav1.Time{Time: time.Date(2000, time.January, 0, 0, 0, 0, 1, time.UTC)}},
},
},
{
name: "entries with subresource field",
managedFields: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Subresource: "status"},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Subresource: "scale"},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply},
},
expected: []metav1.ManagedFieldsEntry{
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Subresource: "scale"},
{Manager: "a", Operation: metav1.ManagedFieldsOperationApply, Subresource: "status"},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sorted, err := sortEncodedManagedFields(test.managedFields)
if err != nil {
t.Fatalf("did not expect error when sorting but got: %v", err)
}
if !reflect.DeepEqual(sorted, test.expected) {
t.Fatalf("expected:\n%v\nbut got:\n%v", test.expected, sorted)
}
})
}
}
func parseTimeOrPanic(s string) *metav1.Time {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
panic(fmt.Sprintf("failed to parse time %s, got: %v", s, err))
}
return &metav1.Time{Time: t.UTC()}
}

View File

@ -1,82 +0,0 @@
/*
Copyright 2020 The Kubernetes 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 internal
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
type managedFieldsUpdater struct {
fieldManager Manager
}
var _ Manager = &managedFieldsUpdater{}
// NewManagedFieldsUpdater is responsible for updating the managedfields
// in the object, updating the time of the operation as necessary. For
// updates, it uses a hard-coded manager to detect if things have
// changed, and swaps back the correct manager after the operation is
// done.
func NewManagedFieldsUpdater(fieldManager Manager) Manager {
return &managedFieldsUpdater{
fieldManager: fieldManager,
}
}
// Update implements Manager.
func (f *managedFieldsUpdater) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
self := "current-operation"
object, managed, err := f.fieldManager.Update(liveObj, newObj, managed, self)
if err != nil {
return object, managed, err
}
// If the current operation took any fields from anything, it means the object changed,
// so update the timestamp of the managedFieldsEntry and merge with any previous updates from the same manager
if vs, ok := managed.Fields()[self]; ok {
delete(managed.Fields(), self)
if previous, ok := managed.Fields()[manager]; ok {
managed.Fields()[manager] = fieldpath.NewVersionedSet(vs.Set().Union(previous.Set()), vs.APIVersion(), vs.Applied())
} else {
managed.Fields()[manager] = vs
}
managed.Times()[manager] = &metav1.Time{Time: time.Now().UTC()}
}
return object, managed, nil
}
// Apply implements Manager.
func (f *managedFieldsUpdater) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
object, managed, err := f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
if err != nil {
return object, managed, err
}
if object != nil {
managed.Times()[fieldManager] = &metav1.Time{Time: time.Now().UTC()}
} else {
object = liveObj.DeepCopyObject()
RemoveObjectManagedFields(object)
}
return object, managed, nil
}

View File

@ -1,524 +0,0 @@
/*
Copyright 2020 The Kubernetes 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 internal_test
import (
"fmt"
"reflect"
"testing"
"time"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"sigs.k8s.io/yaml"
)
func TestManagedFieldsUpdateDoesModifyTime(t *testing.T) {
var err error
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ConfigMap"))
err = updateObject(f, "fieldmanager_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key": "value"
}
}`))
if err != nil {
t.Fatal(err)
}
previousManagedFields := f.ManagedFields()
time.Sleep(time.Second)
err = updateObject(f, "fieldmanager_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key": "new-value"
}
}`))
if err != nil {
t.Fatal(err)
}
newManagedFields := f.ManagedFields()
if previousManagedFields[0].Time.Equal(newManagedFields[0].Time) {
t.Errorf("ManagedFields time has not been updated:\n%v", newManagedFields)
}
}
func TestManagedFieldsApplyDoesModifyTime(t *testing.T) {
var err error
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ConfigMap"))
err = applyObject(f, "fieldmanager_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key": "value"
}
}`))
if err != nil {
t.Fatal(err)
}
previousManagedFields := f.ManagedFields()
time.Sleep(time.Second)
err = applyObject(f, "fieldmanager_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key": "new-value"
}
}`))
if err != nil {
t.Fatal(err)
}
newManagedFields := f.ManagedFields()
if previousManagedFields[0].Time.Equal(newManagedFields[0].Time) {
t.Errorf("ManagedFields time has not been updated:\n%v", newManagedFields)
}
}
func TestManagedFieldsUpdateWithoutChangesDoesNotModifyTime(t *testing.T) {
var err error
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ConfigMap"))
err = updateObject(f, "fieldmanager_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key": "value"
}
}`))
if err != nil {
t.Fatal(err)
}
previousManagedFields := f.ManagedFields()
time.Sleep(time.Second)
err = updateObject(f, "fieldmanager_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key": "value"
}
}`))
if err != nil {
t.Fatal(err)
}
newManagedFields := f.ManagedFields()
if !previousManagedFields[0].Time.Equal(newManagedFields[0].Time) {
t.Errorf("ManagedFields time has changed:\nBefore:\n%v\nAfter:\n%v", previousManagedFields, newManagedFields)
}
}
func TestManagedFieldsApplyWithoutChangesDoesNotModifyTime(t *testing.T) {
var err error
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ConfigMap"))
err = applyObject(f, "fieldmanager_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key": "value"
}
}`))
if err != nil {
t.Fatal(err)
}
previousManagedFields := f.ManagedFields()
time.Sleep(time.Second)
err = applyObject(f, "fieldmanager_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key": "value"
}
}`))
if err != nil {
t.Fatal(err)
}
newManagedFields := f.ManagedFields()
if !previousManagedFields[0].Time.Equal(newManagedFields[0].Time) {
t.Errorf("ManagedFields time has changed:\nBefore:\n%v\nAfter:\n%v", previousManagedFields, newManagedFields)
}
}
func TestNonManagedFieldsUpdateDoesNotModifyTime(t *testing.T) {
var err error
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ConfigMap"))
err = updateObject(f, "fieldmanager_a_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key_a": "value"
}
}`))
if err != nil {
t.Fatal(err)
}
err = updateObject(f, "fieldmanager_b_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key_b": "value"
}
}`))
if err != nil {
t.Fatal(err)
}
previousManagedFields := f.ManagedFields()
previousEntries := map[string]v1.ManagedFieldsEntry{}
for _, entry := range previousManagedFields {
previousEntries[entry.Manager] = entry
}
time.Sleep(time.Second)
err = updateObject(f, "fieldmanager_a_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key_a": "value",
"key_b": "new-value"
}
}`))
if err != nil {
t.Fatal(err)
}
newManagedFields := f.ManagedFields()
newEntries := map[string]v1.ManagedFieldsEntry{}
for _, entry := range newManagedFields {
newEntries[entry.Manager] = entry
}
if _, ok := newEntries["fieldmanager_b_test"]; ok {
t.Errorf("FieldManager B ManagedFields has changed:\n%v", newEntries["fieldmanager_b_test"])
}
}
func TestNonManagedFieldsApplyDoesNotModifyTime(t *testing.T) {
var err error
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ConfigMap"))
err = applyObject(f, "fieldmanager_a_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key_a": "value"
}
}`))
if err != nil {
t.Fatal(err)
}
err = applyObject(f, "fieldmanager_b_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key_b": "value"
}
}`))
if err != nil {
t.Fatal(err)
}
previousManagedFields := f.ManagedFields()
previousEntries := map[string]v1.ManagedFieldsEntry{}
for _, entry := range previousManagedFields {
previousEntries[entry.Manager] = entry
}
time.Sleep(time.Second)
err = applyObject(f, "fieldmanager_a_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key_a": "new-value"
}
}`))
if err != nil {
t.Fatal(err)
}
newManagedFields := f.ManagedFields()
newEntries := map[string]v1.ManagedFieldsEntry{}
for _, entry := range newManagedFields {
newEntries[entry.Manager] = entry
}
if !previousEntries["fieldmanager_b_test"].Time.Equal(newEntries["fieldmanager_b_test"].Time) {
t.Errorf("FieldManager B ManagedFields time changed:\nBefore:\n%v\nAfter:\n%v",
previousEntries["fieldmanager_b_test"], newEntries["fieldmanager_b_test"])
}
}
func TestTakingOverManagedFieldsDuringUpdateDoesNotModifyPreviousManagerTime(t *testing.T) {
var err error
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ConfigMap"))
err = updateObject(f, "fieldmanager_a_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key_a": "value",
"key_b": value"
}
}`))
if err != nil {
t.Fatal(err)
}
previousManagedFields := f.ManagedFields()
previousEntries := map[string]v1.ManagedFieldsEntry{}
for _, entry := range previousManagedFields {
previousEntries[entry.Manager] = entry
}
time.Sleep(time.Second)
err = updateObject(f, "fieldmanager_b_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key_b": "new-value"
}
}`))
if err != nil {
t.Fatal(err)
}
newManagedFields := f.ManagedFields()
newEntries := map[string]v1.ManagedFieldsEntry{}
for _, entry := range newManagedFields {
newEntries[entry.Manager] = entry
}
if !previousEntries["fieldmanager_a_test"].Time.Equal(newEntries["fieldmanager_a_test"].Time) {
t.Errorf("FieldManager A ManagedFields time has been updated:\nBefore:\n%v\nAfter:\n%v",
previousEntries["fieldmanager_a_test"], newEntries["fieldmanager_a_test"])
}
}
func TestTakingOverManagedFieldsDuringApplyDoesNotModifyPreviousManagerTime(t *testing.T) {
var err error
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ConfigMap"))
err = applyObject(f, "fieldmanager_a_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key_a": "value",
"key_b": value"
}
}`))
if err != nil {
t.Fatal(err)
}
previousManagedFields := f.ManagedFields()
previousEntries := map[string]v1.ManagedFieldsEntry{}
for _, entry := range previousManagedFields {
previousEntries[entry.Manager] = entry
}
time.Sleep(time.Second)
err = applyObject(f, "fieldmanager_b_test", []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "configmap"
},
"data": {
"key_b": "new-value"
}
}`))
if err != nil {
t.Fatal(err)
}
newManagedFields := f.ManagedFields()
newEntries := map[string]v1.ManagedFieldsEntry{}
for _, entry := range newManagedFields {
newEntries[entry.Manager] = entry
}
if !previousEntries["fieldmanager_a_test"].Time.Equal(newEntries["fieldmanager_a_test"].Time) {
t.Errorf("FieldManager A ManagedFields time has been updated:\nBefore:\n%v\nAfter:\n%v",
previousEntries["fieldmanager_a_test"], newEntries["fieldmanager_a_test"])
}
}
type NoopManager struct{}
func (NoopManager) Apply(liveObj, appliedObj runtime.Object, managed internal.Managed, fieldManager string, force bool) (runtime.Object, internal.Managed, error) {
return nil, managed, nil
}
func (NoopManager) Update(liveObj, newObj runtime.Object, managed internal.Managed, manager string) (runtime.Object, internal.Managed, error) {
return nil, nil, nil
}
func updateObject(f fieldmanagertest.TestFieldManager, fieldManagerName string, object []byte) error {
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal(object, &obj.Object); err != nil {
return fmt.Errorf("error decoding YAML: %v", err)
}
if err := f.Update(obj, fieldManagerName); err != nil {
return fmt.Errorf("failed to update object: %v", err)
}
return nil
}
func applyObject(f fieldmanagertest.TestFieldManager, fieldManagerName string, object []byte) error {
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal(object, &obj.Object); err != nil {
return fmt.Errorf("error decoding YAML: %v", err)
}
if err := f.Apply(obj, fieldManagerName, true); err != nil {
return fmt.Errorf("failed to apply object: %v", err)
}
return nil
}
// Ensures that if ManagedFieldsUpdater gets a nil value from its nested manager
// chain (meaning the operation was a no-op), then the ManagedFieldsUpdater
// itself will return a copy of the input live object, with its managed fields
// removed
func TestNilNewObjectReplacedWithDeepCopyExcludingManagedFields(t *testing.T) {
// Initialize our "live object" with some managed fields
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal([]byte(`{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "pod",
"labels": {"app": "nginx"},
"managedFields": [
{
"apiVersion": "v1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:metadata": {
"f:labels": {
"f:app": {}
}
}
},
"manager": "fieldmanager_test",
"operation": "Apply",
"time": "2021-11-11T18:41:17Z"
}
]
}
}`), &obj.Object); err != nil {
t.Fatalf("error decoding YAML: %v", err)
}
accessor, err := meta.Accessor(obj)
if err != nil {
t.Fatalf("couldn't get accessor: %v", err)
}
// Decode the managed fields in the live object, since it isn't allowed in the patch.
managed, err := internal.DecodeManagedFields(accessor.GetManagedFields())
if err != nil {
t.Fatalf("failed to decode managed fields: %v", err)
}
updater := internal.NewManagedFieldsUpdater(NoopManager{})
newObject, _, err := updater.Apply(obj, obj.DeepCopyObject(), managed, "some_manager", false)
if err != nil {
t.Fatalf("failed to apply configuration %v", err)
}
if newObject == obj {
t.Fatalf("returned newObject must not be the same instance as the passed in liveObj")
}
// Rip off managed fields of live, and check that it is deeply
// equal to newObject
liveWithoutManaged := obj.DeepCopyObject()
internal.RemoveObjectManagedFields(liveWithoutManaged)
if !reflect.DeepEqual(liveWithoutManaged, newObject) {
t.Fatalf("returned newObject must be deeply equal to the input live object, without managed fields")
}
}

View File

@ -1,52 +0,0 @@
/*
Copyright 2022 The Kubernetes 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 internal
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
// Managed groups a fieldpath.ManagedFields together with the timestamps associated with each operation.
type Managed interface {
// Fields gets the fieldpath.ManagedFields.
Fields() fieldpath.ManagedFields
// Times gets the timestamps associated with each operation.
Times() map[string]*metav1.Time
}
// Manager updates the managed fields and merges applied configurations.
type Manager interface {
// Update is used when the object has already been merged (non-apply
// use-case), and simply updates the managed fields in the output
// object.
// * `liveObj` is not mutated by this function
// * `newObj` may be mutated by this function
// Returns the new object with managedFields removed, and the object's new
// proposed managedFields separately.
Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error)
// Apply is used when server-side apply is called, as it merges the
// object and updates the managed fields.
// * `liveObj` is not mutated by this function
// * `newObj` may be mutated by this function
// Returns the new object with managedFields removed, and the object's new
// proposed managedFields separately.
Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error)
}

View File

@ -1,140 +0,0 @@
/*
Copyright 2018 The Kubernetes 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 internal
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
const (
// Field indicates that the content of this path element is a field's name
Field = "f"
// Value indicates that the content of this path element is a field's value
Value = "v"
// Index indicates that the content of this path element is an index in an array
Index = "i"
// Key indicates that the content of this path element is a key value map
Key = "k"
// Separator separates the type of a path element from the contents
Separator = ":"
)
// NewPathElement parses a serialized path element
func NewPathElement(s string) (fieldpath.PathElement, error) {
split := strings.SplitN(s, Separator, 2)
if len(split) < 2 {
return fieldpath.PathElement{}, fmt.Errorf("missing colon: %v", s)
}
switch split[0] {
case Field:
return fieldpath.PathElement{
FieldName: &split[1],
}, nil
case Value:
val, err := value.FromJSON([]byte(split[1]))
if err != nil {
return fieldpath.PathElement{}, err
}
return fieldpath.PathElement{
Value: &val,
}, nil
case Index:
i, err := strconv.Atoi(split[1])
if err != nil {
return fieldpath.PathElement{}, err
}
return fieldpath.PathElement{
Index: &i,
}, nil
case Key:
kv := map[string]json.RawMessage{}
err := json.Unmarshal([]byte(split[1]), &kv)
if err != nil {
return fieldpath.PathElement{}, err
}
fields := value.FieldList{}
for k, v := range kv {
b, err := json.Marshal(v)
if err != nil {
return fieldpath.PathElement{}, err
}
val, err := value.FromJSON(b)
if err != nil {
return fieldpath.PathElement{}, err
}
fields = append(fields, value.Field{
Name: k,
Value: val,
})
}
return fieldpath.PathElement{
Key: &fields,
}, nil
default:
// Ignore unknown key types
return fieldpath.PathElement{}, nil
}
}
// PathElementString serializes a path element
func PathElementString(pe fieldpath.PathElement) (string, error) {
switch {
case pe.FieldName != nil:
return Field + Separator + *pe.FieldName, nil
case pe.Key != nil:
kv := map[string]json.RawMessage{}
for _, k := range *pe.Key {
b, err := value.ToJSON(k.Value)
if err != nil {
return "", err
}
m := json.RawMessage{}
err = json.Unmarshal(b, &m)
if err != nil {
return "", err
}
kv[k.Name] = m
}
b, err := json.Marshal(kv)
if err != nil {
return "", err
}
return Key + ":" + string(b), nil
case pe.Value != nil:
b, err := value.ToJSON(*pe.Value)
if err != nil {
return "", err
}
return Value + ":" + string(b), nil
case pe.Index != nil:
return Index + ":" + strconv.Itoa(*pe.Index), nil
default:
return "", errors.New("Invalid type of path element")
}
}

View File

@ -1,84 +0,0 @@
/*
Copyright 2018 The Kubernetes 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 internal
import "testing"
func TestPathElementRoundTrip(t *testing.T) {
tests := []string{
`i:0`,
`i:1234`,
`f:`,
`f:spec`,
`f:more-complicated-string`,
`k:{"name":"my-container"}`,
`k:{"port":"8080","protocol":"TCP"}`,
`k:{"optionalField":null}`,
`k:{"jsonField":{"A":1,"B":null,"C":"D","E":{"F":"G"}}}`,
`k:{"listField":["1","2","3"]}`,
`v:null`,
`v:"some-string"`,
`v:1234`,
`v:{"some":"json"}`,
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
pe, err := NewPathElement(test)
if err != nil {
t.Fatalf("Failed to create path element: %v", err)
}
output, err := PathElementString(pe)
if err != nil {
t.Fatalf("Failed to create string from path element: %v", err)
}
if test != output {
t.Fatalf("Expected round-trip:\ninput: %v\noutput: %v", test, output)
}
})
}
}
func TestPathElementIgnoreUnknown(t *testing.T) {
_, err := NewPathElement("r:Hello")
if err != nil {
t.Fatalf("Unknown qualifiers should be ignored")
}
}
func TestNewPathElementError(t *testing.T) {
tests := []string{
``,
`no-colon`,
`i:index is not a number`,
`i:1.23`,
`i:`,
`v:invalid json`,
`v:`,
`k:invalid json`,
`k:{"name":invalid}`,
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
_, err := NewPathElement(test)
if err == nil {
t.Fatalf("Expected error, no error found")
}
})
}
}

View File

@ -1,91 +0,0 @@
/*
Copyright 2019 The Kubernetes 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 internal
import (
"fmt"
"math/rand"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type skipNonAppliedManager struct {
fieldManager Manager
objectCreater runtime.ObjectCreater
gvk schema.GroupVersionKind
beforeApplyManagerName string
probability float32
}
var _ Manager = &skipNonAppliedManager{}
// NewSkipNonAppliedManager creates a new wrapped FieldManager that only starts tracking managers after the first apply.
func NewSkipNonAppliedManager(fieldManager Manager, objectCreater runtime.ObjectCreater, gvk schema.GroupVersionKind) Manager {
return NewProbabilisticSkipNonAppliedManager(fieldManager, objectCreater, gvk, 0.0)
}
// NewProbabilisticSkipNonAppliedManager creates a new wrapped FieldManager that starts tracking managers after the first apply,
// or starts tracking on create with p probability.
func NewProbabilisticSkipNonAppliedManager(fieldManager Manager, objectCreater runtime.ObjectCreater, gvk schema.GroupVersionKind, p float32) Manager {
return &skipNonAppliedManager{
fieldManager: fieldManager,
objectCreater: objectCreater,
gvk: gvk,
beforeApplyManagerName: "before-first-apply",
probability: p,
}
}
// Update implements Manager.
func (f *skipNonAppliedManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
accessor, err := meta.Accessor(liveObj)
if err != nil {
return newObj, managed, nil
}
// If managed fields is empty, we need to determine whether to skip tracking managed fields.
if len(managed.Fields()) == 0 {
// Check if the operation is a create, by checking whether lastObj's UID is empty.
// If the operation is create, P(tracking managed fields) = f.probability
// If the operation is update, skip tracking managed fields, since we already know managed fields is empty.
if len(accessor.GetUID()) == 0 {
if f.probability <= rand.Float32() {
return newObj, managed, nil
}
} else {
return newObj, managed, nil
}
}
return f.fieldManager.Update(liveObj, newObj, managed, manager)
}
// Apply implements Manager.
func (f *skipNonAppliedManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) {
if len(managed.Fields()) == 0 {
emptyObj, err := f.objectCreater.New(f.gvk)
if err != nil {
return nil, nil, fmt.Errorf("failed to create empty object of type %v: %v", f.gvk, err)
}
liveObj, managed, err = f.fieldManager.Update(emptyObj, liveObj, managed, f.beforeApplyManagerName)
if err != nil {
return nil, nil, fmt.Errorf("failed to create manager for existing fields: %v", err)
}
}
return f.fieldManager.Apply(liveObj, appliedObj, managed, fieldManager, force)
}

View File

@ -1,141 +0,0 @@
/*
Copyright 2019 The Kubernetes 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 internal_test
import (
"encoding/json"
"strings"
"testing"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
internaltesting "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/testing"
"sigs.k8s.io/yaml"
)
func TestNoUpdateBeforeFirstApply(t *testing.T) {
f := internaltesting.NewTestFieldManagerImpl(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"), "", func(m internal.Manager) internal.Manager {
return internal.NewSkipNonAppliedManager(
m,
internaltesting.NewFakeObjectCreater(),
schema.FromAPIVersionAndKind("v1", "Pod"),
)
})
appliedObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal([]byte(`{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "pod",
"labels": {"app": "nginx"}
},
"spec": {
"containers": [{
"name": "nginx",
"image": "nginx:latest"
}]
}
}`), &appliedObj.Object); err != nil {
t.Fatalf("error decoding YAML: %v", err)
}
if err := f.Apply(appliedObj, "fieldmanager_test_apply", false); err != nil {
t.Fatalf("failed to update object: %v", err)
}
if e, a := 1, len(f.ManagedFields()); e != a {
t.Fatalf("exected %v entries in managedFields, but got %v: %#v", e, a, f.ManagedFields())
}
if e, a := "fieldmanager_test_apply", f.ManagedFields()[0].Manager; e != a {
t.Fatalf("exected manager name to be %v, but got %v: %#v", e, a, f.ManagedFields())
}
}
func TestUpdateBeforeFirstApply(t *testing.T) {
f := internaltesting.NewTestFieldManagerImpl(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"), "", func(m internal.Manager) internal.Manager {
return internal.NewSkipNonAppliedManager(
m,
internaltesting.NewFakeObjectCreater(),
schema.FromAPIVersionAndKind("v1", "Pod"),
)
})
updatedObj := &unstructured.Unstructured{}
if err := json.Unmarshal([]byte(`{"kind": "Pod", "apiVersion": "v1", "metadata": {"labels": {"app": "my-nginx"}}}`), updatedObj); err != nil {
t.Fatalf("Failed to unmarshal object: %v", err)
}
if err := f.Update(updatedObj, "fieldmanager_test_update"); err != nil {
t.Fatalf("failed to update object: %v", err)
}
if m := f.ManagedFields(); len(m) != 0 {
t.Fatalf("managedFields were tracked on update only: %v", m)
}
appliedObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal([]byte(`{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "pod",
"labels": {"app": "nginx"}
},
"spec": {
"containers": [{
"name": "nginx",
"image": "nginx:latest"
}]
}
}`), &appliedObj.Object); err != nil {
t.Fatalf("error decoding YAML: %v", err)
}
err := f.Apply(appliedObj, "fieldmanager_test_apply", false)
apiStatus, _ := err.(apierrors.APIStatus)
if err == nil || !apierrors.IsConflict(err) || len(apiStatus.Status().Details.Causes) != 1 {
t.Fatalf("Expecting to get one conflict but got %v", err)
}
if e, a := ".metadata.labels.app", apiStatus.Status().Details.Causes[0].Field; e != a {
t.Fatalf("Expecting to conflict on field %q but conflicted on field %q: %v", e, a, err)
}
if e, a := "before-first-apply", apiStatus.Status().Details.Causes[0].Message; !strings.Contains(a, e) {
t.Fatalf("Expecting conflict message to contain %q but got %q: %v", e, a, err)
}
if err := f.Apply(appliedObj, "fieldmanager_test_apply", true); err != nil {
t.Fatalf("failed to update object: %v", err)
}
if e, a := 2, len(f.ManagedFields()); e != a {
t.Fatalf("exected %v entries in managedFields, but got %v: %#v", e, a, f.ManagedFields())
}
if e, a := "fieldmanager_test_apply", f.ManagedFields()[0].Manager; e != a {
t.Fatalf("exected first manager name to be %v, but got %v: %#v", e, a, f.ManagedFields())
}
if e, a := "before-first-apply", f.ManagedFields()[1].Manager; e != a {
t.Fatalf("exected second manager name to be %v, but got %v: %#v", e, a, f.ManagedFields())
}
}

View File

@ -1,90 +0,0 @@
/*
Copyright 2019 The Kubernetes 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 internal
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
type stripMetaManager struct {
fieldManager Manager
// stripSet is the list of fields that should never be part of a mangedFields.
stripSet *fieldpath.Set
}
var _ Manager = &stripMetaManager{}
// NewStripMetaManager creates a new Manager that strips metadata and typemeta fields from the manager's fieldset.
func NewStripMetaManager(fieldManager Manager) Manager {
return &stripMetaManager{
fieldManager: fieldManager,
stripSet: fieldpath.NewSet(
fieldpath.MakePathOrDie("apiVersion"),
fieldpath.MakePathOrDie("kind"),
fieldpath.MakePathOrDie("metadata"),
fieldpath.MakePathOrDie("metadata", "name"),
fieldpath.MakePathOrDie("metadata", "namespace"),
fieldpath.MakePathOrDie("metadata", "creationTimestamp"),
fieldpath.MakePathOrDie("metadata", "selfLink"),
fieldpath.MakePathOrDie("metadata", "uid"),
fieldpath.MakePathOrDie("metadata", "clusterName"),
fieldpath.MakePathOrDie("metadata", "generation"),
fieldpath.MakePathOrDie("metadata", "managedFields"),
fieldpath.MakePathOrDie("metadata", "resourceVersion"),
),
}
}
// Update implements Manager.
func (f *stripMetaManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
newObj, managed, err := f.fieldManager.Update(liveObj, newObj, managed, manager)
if err != nil {
return nil, nil, err
}
f.stripFields(managed.Fields(), manager)
return newObj, managed, nil
}
// Apply implements Manager.
func (f *stripMetaManager) Apply(liveObj, appliedObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
newObj, managed, err := f.fieldManager.Apply(liveObj, appliedObj, managed, manager, force)
if err != nil {
return nil, nil, err
}
f.stripFields(managed.Fields(), manager)
return newObj, managed, nil
}
// stripFields removes a predefined set of paths found in typed from managed
func (f *stripMetaManager) stripFields(managed fieldpath.ManagedFields, manager string) {
vs, ok := managed[manager]
if ok {
if vs == nil {
panic(fmt.Sprintf("Found unexpected nil manager which should never happen: %s", manager))
}
newSet := vs.Set().Difference(f.stripSet)
if newSet.Empty() {
delete(managed, manager)
} else {
managed[manager] = fieldpath.NewVersionedSet(newSet, vs.APIVersion(), vs.Applied())
}
}
}

View File

@ -1,183 +0,0 @@
/*
Copyright 2019 The Kubernetes 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 internal
import (
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
)
type structuredMergeManager struct {
typeConverter TypeConverter
objectConverter runtime.ObjectConvertor
objectDefaulter runtime.ObjectDefaulter
groupVersion schema.GroupVersion
hubVersion schema.GroupVersion
updater merge.Updater
}
var _ Manager = &structuredMergeManager{}
// NewStructuredMergeManager creates a new Manager that merges apply requests
// and update managed fields for other types of requests.
func NewStructuredMergeManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion, resetFields map[fieldpath.APIVersion]*fieldpath.Set) (Manager, error) {
return &structuredMergeManager{
typeConverter: typeConverter,
objectConverter: objectConverter,
objectDefaulter: objectDefaulter,
groupVersion: gv,
hubVersion: hub,
updater: merge.Updater{
Converter: newVersionConverter(typeConverter, objectConverter, hub), // This is the converter provided to SMD from k8s
IgnoredFields: resetFields,
},
}, nil
}
// NewCRDStructuredMergeManager creates a new Manager specifically for
// CRDs. This allows for the possibility of fields which are not defined
// in models, as well as having no models defined at all.
func NewCRDStructuredMergeManager(typeConverter TypeConverter, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion, resetFields map[fieldpath.APIVersion]*fieldpath.Set) (_ Manager, err error) {
return &structuredMergeManager{
typeConverter: typeConverter,
objectConverter: objectConverter,
objectDefaulter: objectDefaulter,
groupVersion: gv,
hubVersion: hub,
updater: merge.Updater{
Converter: newCRDVersionConverter(typeConverter, objectConverter, hub),
IgnoredFields: resetFields,
},
}, nil
}
func objectGVKNN(obj runtime.Object) string {
name := "<unknown>"
namespace := "<unknown>"
if accessor, err := meta.Accessor(obj); err == nil {
name = accessor.GetName()
namespace = accessor.GetNamespace()
}
return fmt.Sprintf("%v/%v; %v", namespace, name, obj.GetObjectKind().GroupVersionKind())
}
// Update implements Manager.
func (f *structuredMergeManager) Update(liveObj, newObj runtime.Object, managed Managed, manager string) (runtime.Object, Managed, error) {
newObjVersioned, err := f.toVersioned(newObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert new object (%v) to proper version (%v): %v", objectGVKNN(newObj), f.groupVersion, err)
}
liveObjVersioned, err := f.toVersioned(liveObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert live object (%v) to proper version: %v", objectGVKNN(liveObj), err)
}
newObjTyped, err := f.typeConverter.ObjectToTyped(newObjVersioned)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert new object (%v) to smd typed: %v", objectGVKNN(newObjVersioned), err)
}
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert live object (%v) to smd typed: %v", objectGVKNN(liveObjVersioned), err)
}
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
// TODO(apelisse) use the first return value when unions are implemented
_, managedFields, err := f.updater.Update(liveObjTyped, newObjTyped, apiVersion, managed.Fields(), manager)
if err != nil {
return nil, nil, fmt.Errorf("failed to update ManagedFields (%v): %v", objectGVKNN(newObjVersioned), err)
}
managed = NewManaged(managedFields, managed.Times())
return newObj, managed, nil
}
// Apply implements Manager.
func (f *structuredMergeManager) Apply(liveObj, patchObj runtime.Object, managed Managed, manager string, force bool) (runtime.Object, Managed, error) {
// Check that the patch object has the same version as the live object
if patchVersion := patchObj.GetObjectKind().GroupVersionKind().GroupVersion(); patchVersion != f.groupVersion {
return nil, nil,
errors.NewBadRequest(
fmt.Sprintf("Incorrect version specified in apply patch. "+
"Specified patch version: %s, expected: %s",
patchVersion, f.groupVersion))
}
patchObjMeta, err := meta.Accessor(patchObj)
if err != nil {
return nil, nil, fmt.Errorf("couldn't get accessor: %v", err)
}
if patchObjMeta.GetManagedFields() != nil {
return nil, nil, errors.NewBadRequest("metadata.managedFields must be nil")
}
liveObjVersioned, err := f.toVersioned(liveObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert live object (%v) to proper version: %v", objectGVKNN(liveObj), err)
}
patchObjTyped, err := f.typeConverter.ObjectToTyped(patchObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to create typed patch object (%v): %v", objectGVKNN(patchObj), err)
}
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned)
if err != nil {
return nil, nil, fmt.Errorf("failed to create typed live object (%v): %v", objectGVKNN(liveObjVersioned), err)
}
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
newObjTyped, managedFields, err := f.updater.Apply(liveObjTyped, patchObjTyped, apiVersion, managed.Fields(), manager, force)
if err != nil {
return nil, nil, err
}
managed = NewManaged(managedFields, managed.Times())
if newObjTyped == nil {
return nil, managed, nil
}
newObj, err := f.typeConverter.TypedToObject(newObjTyped)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert new typed object (%v) to object: %v", objectGVKNN(patchObj), err)
}
newObjVersioned, err := f.toVersioned(newObj)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert new object (%v) to proper version: %v", objectGVKNN(patchObj), err)
}
f.objectDefaulter.Default(newObjVersioned)
newObjUnversioned, err := f.toUnversioned(newObjVersioned)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert to unversioned (%v): %v", objectGVKNN(patchObj), err)
}
return newObjUnversioned, managed, nil
}
func (f *structuredMergeManager) toVersioned(obj runtime.Object) (runtime.Object, error) {
return f.objectConverter.ConvertToVersion(obj, f.groupVersion)
}
func (f *structuredMergeManager) toUnversioned(obj runtime.Object) (runtime.Object, error) {
return f.objectConverter.ConvertToVersion(obj, f.hubVersion)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,182 +0,0 @@
/*
Copyright 2022 The Kubernetes 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 testing
import (
"errors"
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
"sigs.k8s.io/structured-merge-diff/v4/typed"
)
// NewFakeObjectCreater implements ObjectCreater, it can create empty
// objects (unstructured) of the given GVK.
func NewFakeObjectCreater() runtime.ObjectCreater {
return &fakeObjectCreater{}
}
type fakeObjectCreater struct{}
func (f *fakeObjectCreater) New(gvk schema.GroupVersionKind) (runtime.Object, error) {
u := unstructured.Unstructured{Object: map[string]interface{}{}}
u.SetAPIVersion(gvk.GroupVersion().String())
u.SetKind(gvk.Kind)
return &u, nil
}
type fakeObjectConvertor struct {
converter merge.Converter
apiVersion fieldpath.APIVersion
}
//nolint:staticcheck,ineffassign // SA4009 backwards compatibility
func (c *fakeObjectConvertor) Convert(in, out, context interface{}) error {
if typedValue, ok := in.(*typed.TypedValue); ok {
var err error
out, err = c.converter.Convert(typedValue, c.apiVersion)
return err
}
return nil
}
func (c *fakeObjectConvertor) ConvertToVersion(in runtime.Object, _ runtime.GroupVersioner) (runtime.Object, error) {
return in, nil
}
func (c *fakeObjectConvertor) ConvertFieldLabel(_ schema.GroupVersionKind, _, _ string) (string, string, error) {
return "", "", errors.New("not implemented")
}
type fakeObjectDefaulter struct{}
func (d *fakeObjectDefaulter) Default(in runtime.Object) {}
type sameVersionConverter struct{}
func (sameVersionConverter) Convert(object *typed.TypedValue, version fieldpath.APIVersion) (*typed.TypedValue, error) {
return object, nil
}
func (sameVersionConverter) IsMissingVersionError(error) bool {
return false
}
type TestFieldManagerImpl struct {
fieldManager *internal.FieldManager
apiVersion string
emptyObj runtime.Object
liveObj runtime.Object
}
// APIVersion of the object that we're tracking.
func (f *TestFieldManagerImpl) APIVersion() string {
return f.apiVersion
}
// Reset resets the state of the liveObject by resetting it to an empty object.
func (f *TestFieldManagerImpl) Reset() {
f.liveObj = f.emptyObj.DeepCopyObject()
}
// Live returns a copy of the current liveObject.
func (f *TestFieldManagerImpl) Live() runtime.Object {
return f.liveObj.DeepCopyObject()
}
// Apply applies the given object on top of the current liveObj, for the
// given manager and force flag.
func (f *TestFieldManagerImpl) Apply(obj runtime.Object, manager string, force bool) error {
out, err := f.fieldManager.Apply(f.liveObj, obj, manager, force)
if err == nil {
f.liveObj = out
}
return err
}
// Update will updates the managed fields in the liveObj based on the
// changes performed by the update.
func (f *TestFieldManagerImpl) Update(obj runtime.Object, manager string) error {
out, err := f.fieldManager.Update(f.liveObj, obj, manager)
if err == nil {
f.liveObj = out
}
return err
}
// ManagedFields returns the list of existing managed fields for the
// liveObj.
func (f *TestFieldManagerImpl) ManagedFields() []metav1.ManagedFieldsEntry {
accessor, err := meta.Accessor(f.liveObj)
if err != nil {
panic(fmt.Errorf("couldn't get accessor: %v", err))
}
return accessor.GetManagedFields()
}
// NewTestFieldManager creates a new manager for the given GVK.
func NewTestFieldManagerImpl(typeConverter fieldmanager.TypeConverter, gvk schema.GroupVersionKind, subresource string, chainFieldManager func(internal.Manager) internal.Manager) *TestFieldManagerImpl {
apiVersion := fieldpath.APIVersion(gvk.GroupVersion().String())
objectConverter := &fakeObjectConvertor{sameVersionConverter{}, apiVersion}
f, err := internal.NewStructuredMergeManager(
typeConverter,
objectConverter,
&fakeObjectDefaulter{},
gvk.GroupVersion(),
gvk.GroupVersion(),
nil,
)
if err != nil {
panic(err)
}
live := &unstructured.Unstructured{}
live.SetKind(gvk.Kind)
live.SetAPIVersion(gvk.GroupVersion().String())
// This is different from `internal.NewDefaultFieldManager` because:
// 1. We don't want to create a `internal.FieldManager`
// 2. We don't want to use the CapManager that is tested separately with
// a smaller than the default cap.
f = internal.NewLastAppliedUpdater(
internal.NewLastAppliedManager(
internal.NewProbabilisticSkipNonAppliedManager(
internal.NewBuildManagerInfoManager(
internal.NewManagedFieldsUpdater(
internal.NewStripMetaManager(f),
), gvk.GroupVersion(), subresource,
), NewFakeObjectCreater(), gvk, internal.DefaultTrackOnCreateProbability,
), typeConverter, objectConverter, gvk.GroupVersion(),
),
)
if chainFieldManager != nil {
f = chainFieldManager(f)
}
return &TestFieldManagerImpl{
fieldManager: internal.NewFieldManager(f, subresource),
apiVersion: gvk.GroupVersion().String(),
emptyObj: live,
liveObj: live.DeepCopyObject(),
}
}

View File

@ -1,193 +0,0 @@
/*
Copyright 2022 The Kubernetes 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 internal
import (
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/schemaconv"
"k8s.io/kube-openapi/pkg/validation/spec"
smdschema "sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/typed"
"sigs.k8s.io/structured-merge-diff/v4/value"
)
// TypeConverter allows you to convert from runtime.Object to
// typed.TypedValue and the other way around.
type TypeConverter interface {
ObjectToTyped(runtime.Object) (*typed.TypedValue, error)
TypedToObject(*typed.TypedValue) (runtime.Object, error)
}
type typeConverter struct {
parser map[schema.GroupVersionKind]*typed.ParseableType
}
var _ TypeConverter = &typeConverter{}
func NewTypeConverter(openapiSpec map[string]*spec.Schema, preserveUnknownFields bool) (TypeConverter, error) {
typeSchema, err := schemaconv.ToSchemaFromOpenAPI(openapiSpec, preserveUnknownFields)
if err != nil {
return nil, fmt.Errorf("failed to convert models to schema: %v", err)
}
typeParser := typed.Parser{Schema: smdschema.Schema{Types: typeSchema.Types}}
tr := indexModels(&typeParser, openapiSpec)
return &typeConverter{parser: tr}, nil
}
func (c *typeConverter) ObjectToTyped(obj runtime.Object) (*typed.TypedValue, error) {
gvk := obj.GetObjectKind().GroupVersionKind()
t := c.parser[gvk]
if t == nil {
return nil, NewNoCorrespondingTypeError(gvk)
}
switch o := obj.(type) {
case *unstructured.Unstructured:
return t.FromUnstructured(o.UnstructuredContent())
default:
return t.FromStructured(obj)
}
}
func (c *typeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, error) {
return valueToObject(value.AsValue())
}
type deducedTypeConverter struct{}
// DeducedTypeConverter is a TypeConverter for CRDs that don't have a
// schema. It does implement the same interface though (and create the
// same types of objects), so that everything can still work the same.
// CRDs are merged with all their fields being "atomic" (lists
// included).
func NewDeducedTypeConverter() TypeConverter {
return deducedTypeConverter{}
}
// ObjectToTyped converts an object into a TypedValue with a "deduced type".
func (deducedTypeConverter) ObjectToTyped(obj runtime.Object) (*typed.TypedValue, error) {
switch o := obj.(type) {
case *unstructured.Unstructured:
return typed.DeducedParseableType.FromUnstructured(o.UnstructuredContent())
default:
return typed.DeducedParseableType.FromStructured(obj)
}
}
// TypedToObject transforms the typed value into a runtime.Object. That
// is not specific to deduced type.
func (deducedTypeConverter) TypedToObject(value *typed.TypedValue) (runtime.Object, error) {
return valueToObject(value.AsValue())
}
func valueToObject(val value.Value) (runtime.Object, error) {
vu := val.Unstructured()
switch o := vu.(type) {
case map[string]interface{}:
return &unstructured.Unstructured{Object: o}, nil
default:
return nil, fmt.Errorf("failed to convert value to unstructured for type %T", vu)
}
}
func indexModels(
typeParser *typed.Parser,
openAPISchemas map[string]*spec.Schema,
) map[schema.GroupVersionKind]*typed.ParseableType {
tr := map[schema.GroupVersionKind]*typed.ParseableType{}
for modelName, model := range openAPISchemas {
gvkList := parseGroupVersionKind(model.Extensions)
if len(gvkList) == 0 {
continue
}
parsedType := typeParser.Type(modelName)
for _, gvk := range gvkList {
if len(gvk.Kind) > 0 {
tr[schema.GroupVersionKind(gvk)] = &parsedType
}
}
}
return tr
}
// Get and parse GroupVersionKind from the extension. Returns empty if it doesn't have one.
func parseGroupVersionKind(extensions map[string]interface{}) []schema.GroupVersionKind {
gvkListResult := []schema.GroupVersionKind{}
// Get the extensions
gvkExtension, ok := extensions["x-kubernetes-group-version-kind"]
if !ok {
return []schema.GroupVersionKind{}
}
// gvk extension must be a list of at least 1 element.
gvkList, ok := gvkExtension.([]interface{})
if !ok {
return []schema.GroupVersionKind{}
}
for _, gvk := range gvkList {
var group, version, kind string
// gvk extension list must be a map with group, version, and
// kind fields
if gvkMap, ok := gvk.(map[interface{}]interface{}); ok {
group, ok = gvkMap["group"].(string)
if !ok {
continue
}
version, ok = gvkMap["version"].(string)
if !ok {
continue
}
kind, ok = gvkMap["kind"].(string)
if !ok {
continue
}
} else if gvkMap, ok := gvk.(map[string]interface{}); ok {
group, ok = gvkMap["group"].(string)
if !ok {
continue
}
version, ok = gvkMap["version"].(string)
if !ok {
continue
}
kind, ok = gvkMap["kind"].(string)
if !ok {
continue
}
} else {
continue
}
gvkListResult = append(gvkListResult, schema.GroupVersionKind{
Group: group,
Version: version,
Kind: kind,
})
}
return gvkListResult
}

View File

@ -1,317 +0,0 @@
/*
Copyright 2018 The Kubernetes 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 internal
import (
"fmt"
"reflect"
"testing"
"github.com/stretchr/testify/require"
smdschema "sigs.k8s.io/structured-merge-diff/v4/schema"
"sigs.k8s.io/structured-merge-diff/v4/typed"
"sigs.k8s.io/yaml"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/validation/spec"
)
func TestTypeConverter(t *testing.T) {
dtc := NewDeducedTypeConverter()
testCases := []struct {
name string
yaml string
}{
{
name: "apps/v1.Deployment",
yaml: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.15.4
`,
}, {
name: "extensions/v1beta1.Deployment",
yaml: `
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.15.4
`,
}, {
name: "v1.Pod",
yaml: `
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.15.4
`,
},
}
for _, testCase := range testCases {
t.Run(fmt.Sprintf("%v ObjectToTyped with TypeConverter", testCase.name), func(t *testing.T) {
testObjectToTyped(t, testTypeConverter, testCase.yaml)
})
t.Run(fmt.Sprintf("%v ObjectToTyped with DeducedTypeConverter", testCase.name), func(t *testing.T) {
testObjectToTyped(t, dtc, testCase.yaml)
})
}
}
func testObjectToTyped(t *testing.T, tc TypeConverter, y string) {
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal([]byte(y), &obj.Object); err != nil {
t.Fatalf("Failed to parse yaml object: %v", err)
}
typed, err := tc.ObjectToTyped(obj)
if err != nil {
t.Fatalf("Failed to convert object to typed: %v", err)
}
newObj, err := tc.TypedToObject(typed)
if err != nil {
t.Fatalf("Failed to convert typed to object: %v", err)
}
if !reflect.DeepEqual(obj, newObj) {
t.Errorf(`Round-trip failed:
Original object:
%#v
Final object:
%#v`, obj, newObj)
}
}
var result typed.TypedValue
func BenchmarkObjectToTyped(b *testing.B) {
y := `
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.15.4
`
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal([]byte(y), &obj.Object); err != nil {
b.Fatalf("Failed to parse yaml object: %v", err)
}
b.ResetTimer()
b.ReportAllocs()
var r *typed.TypedValue
for i := 0; i < b.N; i++ {
var err error
r, err = testTypeConverter.ObjectToTyped(obj)
if err != nil {
b.Fatalf("Failed to convert object to typed: %v", err)
}
}
result = *r
}
func TestIndexModels(t *testing.T) {
myDefs := map[string]*spec.Schema{
// Show empty GVK extension is ignored
"def0": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-group-version-kind": []interface{}{},
},
},
},
// Show nil GVK is ignored
"def0.0": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-group-version-kind": nil,
},
},
},
// Show this is ignored
"def0.1": {},
// Show allows binding a single GVK
"def1": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-group-version-kind": []interface{}{
map[string]interface{}{
"group": "mygroup",
"version": "v1",
"kind": "MyKind",
},
},
},
},
},
// Show allows bindings with two versions
"def2": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-group-version-kind": []interface{}{
map[string]interface{}{
"group": "mygroup",
"version": "v1",
"kind": "MyOtherKind",
},
map[string]interface{}{
"group": "mygroup",
"version": "v2",
"kind": "MyOtherKind",
},
},
},
},
},
// Show that we can mix and match GVKs from other definitions, and
// that both map[interface{}]interface{} and map[string]interface{}
// are allowed
"def3": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-group-version-kind": []interface{}{
map[string]interface{}{
"group": "mygroup",
"version": "v3",
"kind": "MyKind",
},
map[interface{}]interface{}{
"group": "mygroup",
"version": "v3",
"kind": "MyOtherKind",
},
},
},
},
},
}
myTypes := []smdschema.TypeDef{
{
Name: "def0",
Atom: smdschema.Atom{},
},
{
Name: "def0.1",
Atom: smdschema.Atom{},
},
{
Name: "def0.2",
Atom: smdschema.Atom{},
},
{
Name: "def1",
Atom: smdschema.Atom{},
},
{
Name: "def2",
Atom: smdschema.Atom{},
},
{
Name: "def3",
Atom: smdschema.Atom{},
},
}
parser := typed.Parser{Schema: smdschema.Schema{Types: myTypes}}
gvkIndex := indexModels(&parser, myDefs)
require.Len(t, gvkIndex, 5)
resultNames := map[schema.GroupVersionKind]string{}
for k, v := range gvkIndex {
require.NotNil(t, v.TypeRef.NamedType)
resultNames[k] = *v.TypeRef.NamedType
}
require.Equal(t, resultNames, map[schema.GroupVersionKind]string{
{
Group: "mygroup",
Version: "v1",
Kind: "MyKind",
}: "def1",
{
Group: "mygroup",
Version: "v1",
Kind: "MyOtherKind",
}: "def2",
{
Group: "mygroup",
Version: "v2",
Kind: "MyOtherKind",
}: "def2",
{
Group: "mygroup",
Version: "v3",
Kind: "MyKind",
}: "def3",
{
Group: "mygroup",
Version: "v3",
Kind: "MyOtherKind",
}: "def3",
})
}

View File

@ -1,123 +0,0 @@
/*
Copyright 2018 The Kubernetes 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 internal
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/merge"
"sigs.k8s.io/structured-merge-diff/v4/typed"
)
// versionConverter is an implementation of
// sigs.k8s.io/structured-merge-diff/merge.Converter
type versionConverter struct {
typeConverter TypeConverter
objectConvertor runtime.ObjectConvertor
hubGetter func(from schema.GroupVersion) schema.GroupVersion
}
var _ merge.Converter = &versionConverter{}
// NewVersionConverter builds a VersionConverter from a TypeConverter and an ObjectConvertor.
func newVersionConverter(t TypeConverter, o runtime.ObjectConvertor, h schema.GroupVersion) merge.Converter {
return &versionConverter{
typeConverter: t,
objectConvertor: o,
hubGetter: func(from schema.GroupVersion) schema.GroupVersion {
return schema.GroupVersion{
Group: from.Group,
Version: h.Version,
}
},
}
}
// NewCRDVersionConverter builds a VersionConverter for CRDs from a TypeConverter and an ObjectConvertor.
func newCRDVersionConverter(t TypeConverter, o runtime.ObjectConvertor, h schema.GroupVersion) merge.Converter {
return &versionConverter{
typeConverter: t,
objectConvertor: o,
hubGetter: func(from schema.GroupVersion) schema.GroupVersion {
return h
},
}
}
// Convert implements sigs.k8s.io/structured-merge-diff/merge.Converter
func (v *versionConverter) Convert(object *typed.TypedValue, version fieldpath.APIVersion) (*typed.TypedValue, error) {
// Convert the smd typed value to a kubernetes object.
objectToConvert, err := v.typeConverter.TypedToObject(object)
if err != nil {
return object, err
}
// Parse the target groupVersion.
groupVersion, err := schema.ParseGroupVersion(string(version))
if err != nil {
return object, err
}
// If attempting to convert to the same version as we already have, just return it.
fromVersion := objectToConvert.GetObjectKind().GroupVersionKind().GroupVersion()
if fromVersion == groupVersion {
return object, nil
}
// Convert to internal
internalObject, err := v.objectConvertor.ConvertToVersion(objectToConvert, v.hubGetter(fromVersion))
if err != nil {
return object, err
}
// Convert the object into the target version
convertedObject, err := v.objectConvertor.ConvertToVersion(internalObject, groupVersion)
if err != nil {
return object, err
}
// Convert the object back to a smd typed value and return it.
return v.typeConverter.ObjectToTyped(convertedObject)
}
// IsMissingVersionError
func (v *versionConverter) IsMissingVersionError(err error) bool {
return runtime.IsNotRegisteredError(err) || isNoCorrespondingTypeError(err)
}
type noCorrespondingTypeErr struct {
gvk schema.GroupVersionKind
}
func NewNoCorrespondingTypeError(gvk schema.GroupVersionKind) error {
return &noCorrespondingTypeErr{gvk: gvk}
}
func (k *noCorrespondingTypeErr) Error() string {
return fmt.Sprintf("no corresponding type for %v", k.gvk)
}
func isNoCorrespondingTypeError(err error) bool {
if err == nil {
return false
}
_, ok := err.(*noCorrespondingTypeErr)
return ok
}

View File

@ -1,119 +0,0 @@
/*
Copyright 2018 The Kubernetes 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 internal
import (
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
var testTypeConverter = func() TypeConverter {
data, err := ioutil.ReadFile(filepath.Join("testdata", "swagger.json"))
if err != nil {
panic(err)
}
swag := spec.Swagger{}
if err := json.Unmarshal(data, &swag); err != nil {
panic(err)
}
convertedDefs := map[string]*spec.Schema{}
for k, v := range swag.Definitions {
vCopy := v
convertedDefs[k] = &vCopy
}
typeConverter, err := NewTypeConverter(convertedDefs, false)
if err != nil {
panic(err)
}
return typeConverter
}()
// TestVersionConverter tests the version converter
func TestVersionConverter(t *testing.T) {
oc := fakeObjectConvertorForTestSchema{
gvkForVersion("v1beta1"): objForGroupVersion("apps/v1beta1"),
gvkForVersion("v1"): objForGroupVersion("apps/v1"),
}
vc := newVersionConverter(testTypeConverter, oc, schema.GroupVersion{Group: "apps", Version: runtime.APIVersionInternal})
input, err := testTypeConverter.ObjectToTyped(objForGroupVersion("apps/v1beta1"))
if err != nil {
t.Fatalf("error creating converting input object to a typed value: %v", err)
}
expected := objForGroupVersion("apps/v1")
output, err := vc.Convert(input, fieldpath.APIVersion("apps/v1"))
if err != nil {
t.Fatalf("expected err to be nil but got %v", err)
}
actual, err := testTypeConverter.TypedToObject(output)
if err != nil {
t.Fatalf("error converting output typed value to an object %v", err)
}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("expected to get %v but got %v", expected, actual)
}
}
func gvkForVersion(v string) schema.GroupVersionKind {
return schema.GroupVersionKind{
Group: "apps",
Version: v,
Kind: "Deployment",
}
}
func objForGroupVersion(gv string) runtime.Object {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": gv,
"kind": "Deployment",
},
}
}
type fakeObjectConvertorForTestSchema map[schema.GroupVersionKind]runtime.Object
var _ runtime.ObjectConvertor = fakeObjectConvertorForTestSchema{}
func (c fakeObjectConvertorForTestSchema) ConvertToVersion(_ runtime.Object, gv runtime.GroupVersioner) (runtime.Object, error) {
allKinds := make([]schema.GroupVersionKind, 0)
for kind := range c {
allKinds = append(allKinds, kind)
}
gvk, _ := gv.KindForGroupVersionKinds(allKinds)
return c[gvk], nil
}
func (fakeObjectConvertorForTestSchema) Convert(_, _, _ interface{}) error {
return fmt.Errorf("function not implemented")
}
func (fakeObjectConvertorForTestSchema) ConvertFieldLabel(_ schema.GroupVersionKind, _, _ string) (string, string, error) {
return "", "", fmt.Errorf("function not implemented")
}

View File

@ -1,174 +0,0 @@
/*
Copyright 2021 The Kubernetes 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 fieldmanager
import (
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
var (
scaleGroupVersion = schema.GroupVersion{Group: "autoscaling", Version: "v1"}
replicasPathInScale = fieldpath.MakePathOrDie("spec", "replicas")
)
// ResourcePathMappings maps a group/version to its replicas path. The
// assumption is that all the paths correspond to leaf fields.
type ResourcePathMappings map[string]fieldpath.Path
// ScaleHandler manages the conversion of managed fields between a main
// resource and the scale subresource
type ScaleHandler struct {
parentEntries []metav1.ManagedFieldsEntry
groupVersion schema.GroupVersion
mappings ResourcePathMappings
}
// NewScaleHandler creates a new ScaleHandler
func NewScaleHandler(parentEntries []metav1.ManagedFieldsEntry, groupVersion schema.GroupVersion, mappings ResourcePathMappings) *ScaleHandler {
return &ScaleHandler{
parentEntries: parentEntries,
groupVersion: groupVersion,
mappings: mappings,
}
}
// ToSubresource filter the managed fields of the main resource and convert
// them so that they can be handled by scale.
// For the managed fields that have a replicas path it performs two changes:
// 1. APIVersion is changed to the APIVersion of the scale subresource
// 2. Replicas path of the main resource is transformed to the replicas path of
// the scale subresource
func (h *ScaleHandler) ToSubresource() ([]metav1.ManagedFieldsEntry, error) {
managed, err := internal.DecodeManagedFields(h.parentEntries)
if err != nil {
return nil, err
}
f := fieldpath.ManagedFields{}
t := map[string]*metav1.Time{}
for manager, versionedSet := range managed.Fields() {
path, ok := h.mappings[string(versionedSet.APIVersion())]
// Skip the entry if the APIVersion is unknown
if !ok || path == nil {
continue
}
if versionedSet.Set().Has(path) {
newVersionedSet := fieldpath.NewVersionedSet(
fieldpath.NewSet(replicasPathInScale),
fieldpath.APIVersion(scaleGroupVersion.String()),
versionedSet.Applied(),
)
f[manager] = newVersionedSet
t[manager] = managed.Times()[manager]
}
}
return managedFieldsEntries(internal.NewManaged(f, t))
}
// ToParent merges `scaleEntries` with the entries of the main resource and
// transforms them accordingly
func (h *ScaleHandler) ToParent(scaleEntries []metav1.ManagedFieldsEntry) ([]metav1.ManagedFieldsEntry, error) {
decodedParentEntries, err := internal.DecodeManagedFields(h.parentEntries)
if err != nil {
return nil, err
}
parentFields := decodedParentEntries.Fields()
decodedScaleEntries, err := internal.DecodeManagedFields(scaleEntries)
if err != nil {
return nil, err
}
scaleFields := decodedScaleEntries.Fields()
f := fieldpath.ManagedFields{}
t := map[string]*metav1.Time{}
for manager, versionedSet := range parentFields {
// Get the main resource "replicas" path
path, ok := h.mappings[string(versionedSet.APIVersion())]
// Drop the entry if the APIVersion is unknown.
if !ok {
continue
}
// If the parent entry does not have the replicas path or it is nil, just
// keep it as it is. The path is nil for Custom Resources without scale
// subresource.
if path == nil || !versionedSet.Set().Has(path) {
f[manager] = versionedSet
t[manager] = decodedParentEntries.Times()[manager]
continue
}
if _, ok := scaleFields[manager]; !ok {
// "Steal" the replicas path from the main resource entry
newSet := versionedSet.Set().Difference(fieldpath.NewSet(path))
if !newSet.Empty() {
newVersionedSet := fieldpath.NewVersionedSet(
newSet,
versionedSet.APIVersion(),
versionedSet.Applied(),
)
f[manager] = newVersionedSet
t[manager] = decodedParentEntries.Times()[manager]
}
} else {
// Field wasn't stolen, let's keep the entry as it is.
f[manager] = versionedSet
t[manager] = decodedParentEntries.Times()[manager]
delete(scaleFields, manager)
}
}
for manager, versionedSet := range scaleFields {
if !versionedSet.Set().Has(replicasPathInScale) {
continue
}
newVersionedSet := fieldpath.NewVersionedSet(
fieldpath.NewSet(h.mappings[h.groupVersion.String()]),
fieldpath.APIVersion(h.groupVersion.String()),
versionedSet.Applied(),
)
f[manager] = newVersionedSet
t[manager] = decodedParentEntries.Times()[manager]
}
return managedFieldsEntries(internal.NewManaged(f, t))
}
func managedFieldsEntries(entries internal.ManagedInterface) ([]metav1.ManagedFieldsEntry, error) {
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := internal.EncodeObjectManagedFields(obj, entries); err != nil {
return nil, err
}
accessor, err := meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
return accessor.GetManagedFields(), nil
}

View File

@ -1,785 +0,0 @@
/*
Copyright 2021 The Kubernetes 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 fieldmanager
import (
"reflect"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
)
func TestTransformManagedFieldsToSubresource(t *testing.T) {
testTime, _ := time.ParseInLocation("2006-Jan-02", "2013-Feb-03", time.Local)
managedFieldTime := metav1.NewTime(testTime)
tests := []struct {
desc string
input []metav1.ManagedFieldsEntry
expected []metav1.ManagedFieldsEntry
}{
{
desc: "filter one entry and transform it into a subresource entry",
input: []metav1.ManagedFieldsEntry{
{
Manager: "manager-1",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:another-field":{}}}`)},
},
{
Manager: "manager-2",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Time: &managedFieldTime,
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "manager-2",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Time: &managedFieldTime,
},
},
},
{
desc: "transform all entries",
input: []metav1.ManagedFieldsEntry{
{
Manager: "manager-1",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
},
{
Manager: "manager-2",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
},
{
Manager: "manager-3",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "manager-1",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
},
{
Manager: "manager-2",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
},
{
Manager: "manager-3",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
},
{
desc: "drops fields if the api version is unknown",
input: []metav1.ManagedFieldsEntry{
{
Manager: "manager-1",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v10",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
},
},
expected: nil,
},
}
for _, test := range tests {
handler := NewScaleHandler(
test.input,
schema.GroupVersion{Group: "apps", Version: "v1"},
defaultMappings(),
)
subresourceEntries, err := handler.ToSubresource()
if err != nil {
t.Fatalf("test %q - expected no error but got %v", test.desc, err)
}
if !reflect.DeepEqual(subresourceEntries, test.expected) {
t.Fatalf("test %q - expected output to be:\n%v\n\nbut got:\n%v", test.desc, test.expected, subresourceEntries)
}
}
}
func TestTransformingManagedFieldsToParent(t *testing.T) {
tests := []struct {
desc string
parent []metav1.ManagedFieldsEntry
subresource []metav1.ManagedFieldsEntry
expected []metav1.ManagedFieldsEntry
}{
{
desc: "different-managers: apply -> update",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{},"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
},
{
desc: "different-managers: apply -> apply",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{},"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
},
},
{
desc: "different-managers: update -> update",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{},"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
},
},
{
desc: "different-managers: update -> apply",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{},"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
},
},
{
desc: "same manager: apply -> apply",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{},"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
},
{
desc: "same manager: update -> update",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{},"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
},
{
desc: "same manager: update -> apply",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{},"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
},
},
{
desc: "same manager: apply -> update",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{},"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
},
{
desc: "subresource doesn't own the path anymore",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:status":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
},
},
{
desc: "Subresource steals all the fields of the parent resource",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
},
{
desc: "apply without stealing",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{},"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
},
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{},"f:selector":{}}}`)},
},
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
},
{
desc: "drops the entry if the api version is unknown",
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
},
{
Manager: "another-manager",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v10",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
handler := NewScaleHandler(
test.parent,
schema.GroupVersion{Group: "apps", Version: "v1"},
defaultMappings(),
)
parentEntries, err := handler.ToParent(test.subresource)
if err != nil {
t.Fatalf("test: %q - expected no error but got %v", test.desc, err)
}
if !reflect.DeepEqual(parentEntries, test.expected) {
t.Fatalf("test: %q - expected output to be:\n%v\n\nbut got:\n%v", test.desc, test.expected, parentEntries)
}
})
}
}
func TestTransformingManagedFieldsToParentMultiVersion(t *testing.T) {
tests := []struct {
desc string
groupVersion schema.GroupVersion
mappings ResourcePathMappings
parent []metav1.ManagedFieldsEntry
subresource []metav1.ManagedFieldsEntry
expected []metav1.ManagedFieldsEntry
}{
{
desc: "multi-version",
groupVersion: schema.GroupVersion{Group: "apps", Version: "v1"},
mappings: ResourcePathMappings{
"apps/v1": fieldpath.MakePathOrDie("spec", "the-replicas"),
"apps/v2": fieldpath.MakePathOrDie("spec", "not-the-replicas"),
},
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:the-replicas":{},"f:selector":{}}}`)},
},
{
Manager: "test-other",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v2",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:not-the-replicas":{},"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
{
Manager: "test-other",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v2",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "apps/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:the-replicas":{}}}`)},
Subresource: "scale",
},
},
},
{
desc: "Custom resource without scale subresource, scaling a version with `scale`",
groupVersion: schema.GroupVersion{Group: "mygroup", Version: "v1"},
mappings: ResourcePathMappings{
"mygroup/v1": fieldpath.MakePathOrDie("spec", "the-replicas"),
"mygroup/v2": nil,
},
parent: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "mygroup/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:the-replicas":{},"f:selector":{}}}`)},
},
{
Manager: "test-other",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "mygroup/v2",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:test-other":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
Subresource: "scale",
},
},
expected: []metav1.ManagedFieldsEntry{
{
Manager: "test",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "mygroup/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:selector":{}}}`)},
},
{
Manager: "test-other",
Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "mygroup/v2",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:test-other":{}}}`)},
},
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "mygroup/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:the-replicas":{}}}`)},
Subresource: "scale",
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
handler := NewScaleHandler(
test.parent,
test.groupVersion,
test.mappings,
)
parentEntries, err := handler.ToParent(test.subresource)
if err != nil {
t.Fatalf("test: %q - expected no error but got %v", test.desc, err)
}
if !reflect.DeepEqual(parentEntries, test.expected) {
t.Fatalf("test: %q - expected output to be:\n%v\n\nbut got:\n%v", test.desc, test.expected, parentEntries)
}
})
}
}
func defaultMappings() ResourcePathMappings {
return ResourcePathMappings{
"apps/v1": fieldpath.MakePathOrDie("spec", "replicas"),
}
}

View File

@ -1,47 +0,0 @@
/*
Copyright 2018 The Kubernetes 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 fieldmanager
import (
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// TypeConverter allows you to convert from runtime.Object to
// typed.TypedValue and the other way around.
type TypeConverter = internal.TypeConverter
// NewDeducedTypeConverter creates a TypeConverter for CRDs that don't
// have a schema. It does implement the same interface though (and
// create the same types of objects), so that everything can still work
// the same. CRDs are merged with all their fields being "atomic" (lists
// included).
func NewDeducedTypeConverter() TypeConverter {
return internal.NewDeducedTypeConverter()
}
// NewTypeConverter builds a TypeConverter from a map of OpenAPIV3 schemas.
// This will automatically find the proper version of the object, and the
// corresponding schema information.
// The keys to the map must be consistent with the names
// used by Refs within the schemas.
// The schemas should conform to the Kubernetes Structural Schema OpenAPI
// restrictions found in docs:
// https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema
func NewTypeConverter(openapiSpec map[string]*spec.Schema, preserveUnknownFields bool) (TypeConverter, error) {
return internal.NewTypeConverter(openapiSpec, preserveUnknownFields)
}

View File

@ -36,6 +36,7 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apimachinery/pkg/util/mergepatch" "k8s.io/apimachinery/pkg/util/mergepatch"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/strategicpatch"
@ -297,7 +298,7 @@ type patchMechanism interface {
type jsonPatcher struct { type jsonPatcher struct {
*patcher *patcher
fieldManager *fieldmanager.FieldManager fieldManager *managedfields.FieldManager
} }
func (p *jsonPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) { func (p *jsonPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) {
@ -417,7 +418,7 @@ type smpPatcher struct {
// Schema // Schema
schemaReferenceObj runtime.Object schemaReferenceObj runtime.Object
fieldManager *fieldmanager.FieldManager fieldManager *managedfields.FieldManager
} }
func (p *smpPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) { func (p *smpPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) {
@ -455,7 +456,7 @@ type applyPatcher struct {
options *metav1.PatchOptions options *metav1.PatchOptions
creater runtime.ObjectCreater creater runtime.ObjectCreater
kind schema.GroupVersionKind kind schema.GroupVersionKind
fieldManager *fieldmanager.FieldManager fieldManager *managedfields.FieldManager
userAgent string userAgent string
validationDirective string validationDirective string
} }

View File

@ -38,9 +38,9 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
requestmetrics "k8s.io/apiserver/pkg/endpoints/handlers/metrics" requestmetrics "k8s.io/apiserver/pkg/endpoints/handlers/metrics"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/endpoints/metrics"
@ -89,7 +89,7 @@ type RequestScope struct {
EquivalentResourceMapper runtime.EquivalentResourceMapper EquivalentResourceMapper runtime.EquivalentResourceMapper
TableConvertor rest.TableConvertor TableConvertor rest.TableConvertor
FieldManager *fieldmanager.FieldManager FieldManager *managedfields.FieldManager
Resource schema.GroupVersionResource Resource schema.GroupVersionResource
Kind schema.GroupVersionKind Kind schema.GroupVersionKind

View File

@ -32,12 +32,12 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/managedfields"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/endpoints/deprecation" "k8s.io/apiserver/pkg/endpoints/deprecation"
"k8s.io/apiserver/pkg/endpoints/discovery" "k8s.io/apiserver/pkg/endpoints/discovery"
"k8s.io/apiserver/pkg/endpoints/handlers" "k8s.io/apiserver/pkg/endpoints/handlers"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/endpoints/metrics"
utilwarning "k8s.io/apiserver/pkg/endpoints/warning" utilwarning "k8s.io/apiserver/pkg/endpoints/warning"
@ -683,7 +683,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
resetFields = resetFieldsStrategy.GetResetFields() resetFields = resetFieldsStrategy.GetResetFields()
} }
reqScope.FieldManager, err = fieldmanager.NewDefaultFieldManager( reqScope.FieldManager, err = managedfields.NewDefaultFieldManager(
a.group.TypeConverter, a.group.TypeConverter,
a.group.UnsafeConvertor, a.group.UnsafeConvertor,
a.group.Defaulter, a.group.Defaulter,

View File

@ -34,6 +34,7 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/managedfields"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
utilwaitgroup "k8s.io/apimachinery/pkg/util/waitgroup" utilwaitgroup "k8s.io/apimachinery/pkg/util/waitgroup"
@ -44,7 +45,6 @@ import (
genericapi "k8s.io/apiserver/pkg/endpoints" genericapi "k8s.io/apiserver/pkg/endpoints"
"k8s.io/apiserver/pkg/endpoints/discovery" "k8s.io/apiserver/pkg/endpoints/discovery"
discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated" discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
"k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/server/healthz" "k8s.io/apiserver/pkg/server/healthz"
@ -737,11 +737,11 @@ func (s preparedGenericAPIServer) NonBlockingRun(stopCh <-chan struct{}, shutdow
// installAPIResources is a private method for installing the REST storage backing each api groupversionresource // installAPIResources is a private method for installing the REST storage backing each api groupversionresource
func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, openAPIModels map[string]*spec.Schema) error { func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, openAPIModels map[string]*spec.Schema) error {
var typeConverter fieldmanager.TypeConverter var typeConverter managedfields.TypeConverter
if len(openAPIModels) > 0 { if len(openAPIModels) > 0 {
var err error var err error
typeConverter, err = fieldmanager.NewTypeConverter(openAPIModels, false) typeConverter, err = managedfields.NewTypeConverter(openAPIModels, false)
if err != nil { if err != nil {
return err return err
} }