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/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0
|
gopkg.in/square/go-jose.v2 v2.6.0
|
||||||
k8s.io/api v0.0.0-20230308234233-a4afee70a903
|
k8s.io/api v0.0.0-20230308234233-a4afee70a903
|
||||||
k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7
|
k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc
|
||||||
k8s.io/client-go v0.0.0-20230309033544-64e2c7ff167c
|
k8s.io/client-go v0.0.0-20230310084516-756a0f3ae9a9
|
||||||
k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73
|
k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73
|
||||||
k8s.io/klog/v2 v2.90.1
|
k8s.io/klog/v2 v2.90.1
|
||||||
k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7
|
k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7
|
||||||
|
|
@ -125,8 +125,8 @@ require (
|
||||||
|
|
||||||
replace (
|
replace (
|
||||||
k8s.io/api => k8s.io/api v0.0.0-20230308234233-a4afee70a903
|
k8s.io/api => k8s.io/api v0.0.0-20230308234233-a4afee70a903
|
||||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7
|
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc
|
||||||
k8s.io/client-go => k8s.io/client-go v0.0.0-20230309033544-64e2c7ff167c
|
k8s.io/client-go => k8s.io/client-go v0.0.0-20230310084516-756a0f3ae9a9
|
||||||
k8s.io/component-base => k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73
|
k8s.io/component-base => k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73
|
||||||
k8s.io/kms => k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7
|
k8s.io/kms => k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7
|
||||||
)
|
)
|
||||||
|
|
|
||||||
8
go.sum
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=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
k8s.io/api v0.0.0-20230308234233-a4afee70a903 h1:TmxUf1tDcGUHE8qZKLRWmn2nr2FypkwvqC2qviOUmQc=
|
k8s.io/api v0.0.0-20230308234233-a4afee70a903 h1:TmxUf1tDcGUHE8qZKLRWmn2nr2FypkwvqC2qviOUmQc=
|
||||||
k8s.io/api v0.0.0-20230308234233-a4afee70a903/go.mod h1:esKbT+6XB9TZUHyxlJVQ3zUM0abhQZ81Ic68eirO+xM=
|
k8s.io/api v0.0.0-20230308234233-a4afee70a903/go.mod h1:esKbT+6XB9TZUHyxlJVQ3zUM0abhQZ81Ic68eirO+xM=
|
||||||
k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7 h1:YN43Lvs3Pj9iQmuWGojeBiFdz1mkrxe0EZn7Ba3TMpQ=
|
k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc h1:iUeJrNH0Issnw4YeNTz6uNuWuA24Eh01HhHiO3IyCg0=
|
||||||
k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7/go.mod h1:jlJwObMa4oKAEOMnAeEaqeiM+Fwd/CbAwNyQ7OaEwS0=
|
k8s.io/apimachinery v0.0.0-20230310053422-8ac57a9747bc/go.mod h1:jlJwObMa4oKAEOMnAeEaqeiM+Fwd/CbAwNyQ7OaEwS0=
|
||||||
k8s.io/client-go v0.0.0-20230309033544-64e2c7ff167c h1:1IXuG9QQvPMR3GbYgBhOKre47MAIq+U41cWOGoAHpd8=
|
k8s.io/client-go v0.0.0-20230310084516-756a0f3ae9a9 h1:Nd40oJXB53lEpVRBPPCtUaUKwkKa70OxCDKsESBeGFY=
|
||||||
k8s.io/client-go v0.0.0-20230309033544-64e2c7ff167c/go.mod h1:hjEB5iFHr17qVb6wnh6w2LQvO5DfoP6rzLN8NAE8K6U=
|
k8s.io/client-go v0.0.0-20230310084516-756a0f3ae9a9/go.mod h1:hjEB5iFHr17qVb6wnh6w2LQvO5DfoP6rzLN8NAE8K6U=
|
||||||
k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73 h1:MEKvhkstqrRFmA9+qQlnkA/jPbZUH/VnMKiEfBeLbf8=
|
k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73 h1:MEKvhkstqrRFmA9+qQlnkA/jPbZUH/VnMKiEfBeLbf8=
|
||||||
k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73/go.mod h1:MB0hQ6Wy3OOZ/dr+sy5FwxCJhDJ4hszX743ar8dd2zE=
|
k8s.io/component-base v0.0.0-20230308075123-cfc68dcaff73/go.mod h1:MB0hQ6Wy3OOZ/dr+sy5FwxCJhDJ4hszX743ar8dd2zE=
|
||||||
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
|
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,11 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||||
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
"k8s.io/apiserver/pkg/storageversion"
|
"k8s.io/apiserver/pkg/storageversion"
|
||||||
)
|
)
|
||||||
|
|
@ -81,7 +81,7 @@ type APIGroupVersion struct {
|
||||||
Defaulter runtime.ObjectDefaulter
|
Defaulter runtime.ObjectDefaulter
|
||||||
Namer runtime.Namer
|
Namer runtime.Namer
|
||||||
UnsafeConvertor runtime.ObjectConvertor
|
UnsafeConvertor runtime.ObjectConvertor
|
||||||
TypeConverter fieldmanager.TypeConverter
|
TypeConverter managedfields.TypeConverter
|
||||||
|
|
||||||
EquivalentResourceRegistry runtime.EquivalentResourceRegistry
|
EquivalentResourceRegistry runtime.EquivalentResourceRegistry
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/warning"
|
"k8s.io/apiserver/pkg/warning"
|
||||||
)
|
)
|
||||||
|
|
@ -70,7 +71,7 @@ func (admit *managedFieldsValidatingAdmissionController) Admit(ctx context.Conte
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
managedFieldsAfterAdmission := objectMeta.GetManagedFields()
|
managedFieldsAfterAdmission := objectMeta.GetManagedFields()
|
||||||
if err := ValidateManagedFields(managedFieldsAfterAdmission); err != nil {
|
if err := managedfields.ValidateManagedFields(managedFieldsAfterAdmission); err != nil {
|
||||||
objectMeta.SetManagedFields(managedFieldsBeforeAdmission)
|
objectMeta.SetManagedFields(managedFieldsBeforeAdmission)
|
||||||
warning.AddWarning(ctx, "",
|
warning.AddWarning(ctx, "",
|
||||||
fmt.Sprintf(InvalidManagedFieldsAfterMutatingAdmissionWarningFormat,
|
fmt.Sprintf(InvalidManagedFieldsAfterMutatingAdmissionWarningFormat,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,11 @@ limitations under the License.
|
||||||
package fieldmanager_test
|
package fieldmanager_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
|
@ -27,10 +31,42 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest"
|
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||||
|
"k8s.io/apimachinery/pkg/util/managedfields/managedfieldstest"
|
||||||
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var fakeTypeConverter = func() managedfields.TypeConverter {
|
||||||
|
data, err := ioutil.ReadFile(filepath.Join(strings.Repeat(".."+string(filepath.Separator), 8),
|
||||||
|
"api", "openapi-spec", "swagger.json"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
swagger := spec.Swagger{}
|
||||||
|
if err := json.Unmarshal(data, &swagger); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
definitions := map[string]*spec.Schema{}
|
||||||
|
for k, v := range swagger.Definitions {
|
||||||
|
p := v
|
||||||
|
definitions[k] = &p
|
||||||
|
}
|
||||||
|
typeConverter, err := managedfields.NewTypeConverter(definitions, false)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return typeConverter
|
||||||
|
}()
|
||||||
|
|
||||||
|
func getObjectBytes(file string) []byte {
|
||||||
|
s, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkNewObject(b *testing.B) {
|
func BenchmarkNewObject(b *testing.B) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
gvk schema.GroupVersionKind
|
gvk schema.GroupVersionKind
|
||||||
|
|
@ -55,7 +91,7 @@ func BenchmarkNewObject(b *testing.B) {
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
b.Run(test.gvk.Kind, func(b *testing.B) {
|
b.Run(test.gvk.Kind, func(b *testing.B) {
|
||||||
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, test.gvk)
|
f := managedfieldstest.NewTestFieldManager(fakeTypeConverter, test.gvk)
|
||||||
|
|
||||||
decoder := serializer.NewCodecFactory(scheme).UniversalDecoder(test.gvk.GroupVersion())
|
decoder := serializer.NewCodecFactory(scheme).UniversalDecoder(test.gvk.GroupVersion())
|
||||||
newObj, err := runtime.Decode(decoder, test.obj)
|
newObj, err := runtime.Decode(decoder, test.obj)
|
||||||
|
|
@ -268,7 +304,7 @@ func BenchmarkCompare(b *testing.B) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkRepeatedUpdate(b *testing.B) {
|
func BenchmarkRepeatedUpdate(b *testing.B) {
|
||||||
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"))
|
f := managedfieldstest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"))
|
||||||
podBytes := getObjectBytes("pod.yaml")
|
podBytes := getObjectBytes("pod.yaml")
|
||||||
|
|
||||||
var obj *corev1.Pod
|
var obj *corev1.Pod
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||||
"k8s.io/apimachinery/pkg/util/mergepatch"
|
"k8s.io/apimachinery/pkg/util/mergepatch"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||||
|
|
@ -297,7 +298,7 @@ type patchMechanism interface {
|
||||||
type jsonPatcher struct {
|
type jsonPatcher struct {
|
||||||
*patcher
|
*patcher
|
||||||
|
|
||||||
fieldManager *fieldmanager.FieldManager
|
fieldManager *managedfields.FieldManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *jsonPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) {
|
func (p *jsonPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) {
|
||||||
|
|
@ -417,7 +418,7 @@ type smpPatcher struct {
|
||||||
|
|
||||||
// Schema
|
// Schema
|
||||||
schemaReferenceObj runtime.Object
|
schemaReferenceObj runtime.Object
|
||||||
fieldManager *fieldmanager.FieldManager
|
fieldManager *managedfields.FieldManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *smpPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) {
|
func (p *smpPatcher) applyPatchToCurrentObject(requestContext context.Context, currentObject runtime.Object) (runtime.Object, error) {
|
||||||
|
|
@ -455,7 +456,7 @@ type applyPatcher struct {
|
||||||
options *metav1.PatchOptions
|
options *metav1.PatchOptions
|
||||||
creater runtime.ObjectCreater
|
creater runtime.ObjectCreater
|
||||||
kind schema.GroupVersionKind
|
kind schema.GroupVersionKind
|
||||||
fieldManager *fieldmanager.FieldManager
|
fieldManager *managedfields.FieldManager
|
||||||
userAgent string
|
userAgent string
|
||||||
validationDirective string
|
validationDirective string
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,9 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
|
||||||
requestmetrics "k8s.io/apiserver/pkg/endpoints/handlers/metrics"
|
requestmetrics "k8s.io/apiserver/pkg/endpoints/handlers/metrics"
|
||||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||||
"k8s.io/apiserver/pkg/endpoints/metrics"
|
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||||
|
|
@ -89,7 +89,7 @@ type RequestScope struct {
|
||||||
EquivalentResourceMapper runtime.EquivalentResourceMapper
|
EquivalentResourceMapper runtime.EquivalentResourceMapper
|
||||||
|
|
||||||
TableConvertor rest.TableConvertor
|
TableConvertor rest.TableConvertor
|
||||||
FieldManager *fieldmanager.FieldManager
|
FieldManager *managedfields.FieldManager
|
||||||
|
|
||||||
Resource schema.GroupVersionResource
|
Resource schema.GroupVersionResource
|
||||||
Kind schema.GroupVersionKind
|
Kind schema.GroupVersionKind
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,12 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/endpoints/deprecation"
|
"k8s.io/apiserver/pkg/endpoints/deprecation"
|
||||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||||
"k8s.io/apiserver/pkg/endpoints/handlers"
|
"k8s.io/apiserver/pkg/endpoints/handlers"
|
||||||
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
|
||||||
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||||
"k8s.io/apiserver/pkg/endpoints/metrics"
|
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||||
utilwarning "k8s.io/apiserver/pkg/endpoints/warning"
|
utilwarning "k8s.io/apiserver/pkg/endpoints/warning"
|
||||||
|
|
@ -683,7 +683,7 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
|
||||||
resetFields = resetFieldsStrategy.GetResetFields()
|
resetFields = resetFieldsStrategy.GetResetFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
reqScope.FieldManager, err = fieldmanager.NewDefaultFieldManager(
|
reqScope.FieldManager, err = managedfields.NewDefaultFieldManager(
|
||||||
a.group.TypeConverter,
|
a.group.TypeConverter,
|
||||||
a.group.UnsafeConvertor,
|
a.group.UnsafeConvertor,
|
||||||
a.group.Defaulter,
|
a.group.Defaulter,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
utilwaitgroup "k8s.io/apimachinery/pkg/util/waitgroup"
|
utilwaitgroup "k8s.io/apimachinery/pkg/util/waitgroup"
|
||||||
|
|
@ -44,7 +45,6 @@ import (
|
||||||
genericapi "k8s.io/apiserver/pkg/endpoints"
|
genericapi "k8s.io/apiserver/pkg/endpoints"
|
||||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||||
discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
|
discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
|
||||||
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
|
||||||
"k8s.io/apiserver/pkg/features"
|
"k8s.io/apiserver/pkg/features"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
"k8s.io/apiserver/pkg/server/healthz"
|
"k8s.io/apiserver/pkg/server/healthz"
|
||||||
|
|
@ -737,11 +737,11 @@ func (s preparedGenericAPIServer) NonBlockingRun(stopCh <-chan struct{}, shutdow
|
||||||
|
|
||||||
// installAPIResources is a private method for installing the REST storage backing each api groupversionresource
|
// installAPIResources is a private method for installing the REST storage backing each api groupversionresource
|
||||||
func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, openAPIModels map[string]*spec.Schema) error {
|
func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, openAPIModels map[string]*spec.Schema) error {
|
||||||
var typeConverter fieldmanager.TypeConverter
|
var typeConverter managedfields.TypeConverter
|
||||||
|
|
||||||
if len(openAPIModels) > 0 {
|
if len(openAPIModels) > 0 {
|
||||||
var err error
|
var err error
|
||||||
typeConverter, err = fieldmanager.NewTypeConverter(openAPIModels, false)
|
typeConverter, err = managedfields.NewTypeConverter(openAPIModels, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue