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:
commit
7a3a376fee
8
go.mod
8
go.mod
|
|
@ -43,8 +43,8 @@ require (
|
|||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
k8s.io/api v0.0.0-20230308234233-a4afee70a903
|
||||
k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7
|
||||
k8s.io/client-go v0.0.0-20230309033544-64e2c7ff167c
|
||||
k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc
|
||||
k8s.io/client-go v0.0.0-20230310084516-756a0f3ae9a9
|
||||
k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73
|
||||
k8s.io/klog/v2 v2.90.1
|
||||
k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7
|
||||
|
|
@ -125,8 +125,8 @@ require (
|
|||
|
||||
replace (
|
||||
k8s.io/api => k8s.io/api v0.0.0-20230308234233-a4afee70a903
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7
|
||||
k8s.io/client-go => k8s.io/client-go v0.0.0-20230309033544-64e2c7ff167c
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc
|
||||
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/kms => k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7
|
||||
)
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -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=
|
||||
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/apimachinery v0.0.0-20230303235435-f357b1fa74b7 h1:YN43Lvs3Pj9iQmuWGojeBiFdz1mkrxe0EZn7Ba3TMpQ=
|
||||
k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7/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-20230309033544-64e2c7ff167c/go.mod h1:hjEB5iFHr17qVb6wnh6w2LQvO5DfoP6rzLN8NAE8K6U=
|
||||
k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc h1:iUeJrNH0Issnw4YeNTz6uNuWuA24Eh01HhHiO3IyCg0=
|
||||
k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc/go.mod h1:jlJwObMa4oKAEOMnAeEaqeiM+Fwd/CbAwNyQ7OaEwS0=
|
||||
k8s.io/client-go v0.0.0-20230310084516-756a0f3ae9a9 h1:Nd40oJXB53lEpVRBPPCtUaUKwkKa70OxCDKsESBeGFY=
|
||||
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/go.mod h1:MB0hQ6Wy3OOZ/dr+sy5FwxCJhDJ4hszX743ar8dd2zE=
|
||||
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/storageversion"
|
||||
)
|
||||
|
|
@ -81,7 +81,7 @@ type APIGroupVersion struct {
|
|||
Defaulter runtime.ObjectDefaulter
|
||||
Namer runtime.Namer
|
||||
UnsafeConvertor runtime.ObjectConvertor
|
||||
TypeConverter fieldmanager.TypeConverter
|
||||
TypeConverter managedfields.TypeConverter
|
||||
|
||||
EquivalentResourceRegistry runtime.EquivalentResourceRegistry
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/warning"
|
||||
)
|
||||
|
|
@ -70,7 +71,7 @@ func (admit *managedFieldsValidatingAdmissionController) Admit(ctx context.Conte
|
|||
return err
|
||||
}
|
||||
managedFieldsAfterAdmission := objectMeta.GetManagedFields()
|
||||
if err := ValidateManagedFields(managedFieldsAfterAdmission); err != nil {
|
||||
if err := managedfields.ValidateManagedFields(managedFieldsAfterAdmission); err != nil {
|
||||
objectMeta.SetManagedFields(managedFieldsBeforeAdmission)
|
||||
warning.AddWarning(ctx, "",
|
||||
fmt.Sprintf(InvalidManagedFieldsAfterMutatingAdmissionWarningFormat,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ limitations under the License.
|
|||
package fieldmanager_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
|
@ -27,10 +31,42 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"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"
|
||||
)
|
||||
|
||||
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) {
|
||||
tests := []struct {
|
||||
gvk schema.GroupVersionKind
|
||||
|
|
@ -55,7 +91,7 @@ func BenchmarkNewObject(b *testing.B) {
|
|||
}
|
||||
for _, test := range tests {
|
||||
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())
|
||||
newObj, err := runtime.Decode(decoder, test.obj)
|
||||
|
|
@ -268,7 +304,7 @@ func BenchmarkCompare(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")
|
||||
|
||||
var obj *corev1.Pod
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}()
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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",
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
|
|
@ -297,7 +298,7 @@ type patchMechanism interface {
|
|||
type jsonPatcher struct {
|
||||
*patcher
|
||||
|
||||
fieldManager *fieldmanager.FieldManager
|
||||
fieldManager *managedfields.FieldManager
|
||||
}
|
||||
|
||||
func (p *jsonPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) {
|
||||
|
|
@ -417,7 +418,7 @@ type smpPatcher struct {
|
|||
|
||||
// Schema
|
||||
schemaReferenceObj runtime.Object
|
||||
fieldManager *fieldmanager.FieldManager
|
||||
fieldManager *managedfields.FieldManager
|
||||
}
|
||||
|
||||
func (p *smpPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) {
|
||||
|
|
@ -455,7 +456,7 @@ type applyPatcher struct {
|
|||
options *metav1.PatchOptions
|
||||
creater runtime.ObjectCreater
|
||||
kind schema.GroupVersionKind
|
||||
fieldManager *fieldmanager.FieldManager
|
||||
fieldManager *managedfields.FieldManager
|
||||
userAgent string
|
||||
validationDirective string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
||||
requestmetrics "k8s.io/apiserver/pkg/endpoints/handlers/metrics"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||
|
|
@ -89,7 +89,7 @@ type RequestScope struct {
|
|||
EquivalentResourceMapper runtime.EquivalentResourceMapper
|
||||
|
||||
TableConvertor rest.TableConvertor
|
||||
FieldManager *fieldmanager.FieldManager
|
||||
FieldManager *managedfields.FieldManager
|
||||
|
||||
Resource schema.GroupVersionResource
|
||||
Kind schema.GroupVersionKind
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/endpoints/deprecation"
|
||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||
"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/metrics"
|
||||
utilwarning "k8s.io/apiserver/pkg/endpoints/warning"
|
||||
|
|
@ -683,7 +683,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
|
|||
resetFields = resetFieldsStrategy.GetResetFields()
|
||||
}
|
||||
|
||||
reqScope.FieldManager, err = fieldmanager.NewDefaultFieldManager(
|
||||
reqScope.FieldManager, err = managedfields.NewDefaultFieldManager(
|
||||
a.group.TypeConverter,
|
||||
a.group.UnsafeConvertor,
|
||||
a.group.Defaulter,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilwaitgroup "k8s.io/apimachinery/pkg/util/waitgroup"
|
||||
|
|
@ -44,7 +45,6 @@ import (
|
|||
genericapi "k8s.io/apiserver/pkg/endpoints"
|
||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||
discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"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
|
||||
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 {
|
||||
var err error
|
||||
typeConverter, err = fieldmanager.NewTypeConverter(openAPIModels, false)
|
||||
typeConverter, err = managedfields.NewTypeConverter(openAPIModels, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue