Track ownership of deployments scale subresource

Kubernetes-commit: a9ea98b3b9272a7f7788a0d37891e4b13b9be38d
This commit is contained in:
Andrea Nodari 2021-01-23 18:50:14 +01:00 committed by Kubernetes Publisher
parent c883d6c994
commit 8df8282eaf
2 changed files with 844 additions and 0 deletions

View File

@ -0,0 +1,160 @@
/*
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
defaultGroupVersion schema.GroupVersion
mappings ResourcePathMappings
}
// NewScaleHandler creates a new ScaleHandler
func NewScaleHandler(parentEntries []metav1.ManagedFieldsEntry, defaultGroupVersion schema.GroupVersion, mappings ResourcePathMappings) *ScaleHandler {
return &ScaleHandler{
parentEntries: parentEntries,
defaultGroupVersion: defaultGroupVersion,
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 := DecodeManagedFields(h.parentEntries)
if err != nil {
return nil, err
}
f := fieldpath.ManagedFields{}
t := map[string]*metav1.Time{}
for manager, versionedSet := range managed.Fields() {
path := h.mappings[string(versionedSet.APIVersion())]
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 := DecodeManagedFields(h.parentEntries)
if err != nil {
return nil, err
}
parentFields := decodedParentEntries.Fields()
decodedScaleEntries, err := 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 := h.mappings[string(versionedSet.APIVersion())]
// If the parent entry does not have the replicas path, just keep it as it is
if !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 {
newVersionedSet := fieldpath.NewVersionedSet(
fieldpath.NewSet(h.mappings[h.defaultGroupVersion.String()]),
fieldpath.APIVersion(h.defaultGroupVersion.String()),
versionedSet.Applied(),
)
f[manager] = newVersionedSet
t[manager] = decodedParentEntries.Times()[manager]
}
return managedFieldsEntries(internal.NewManaged(f, t))
}
func managedFieldsEntries(entries internal.ManagedInterface) ([]metav1.ManagedFieldsEntry, error) {
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := internal.EncodeObjectManagedFields(obj, entries); err != nil {
return nil, err
}
accessor, err := meta.Accessor(obj)
if err != nil {
panic(fmt.Sprintf("couldn't get accessor: %v", err))
}
return accessor.GetManagedFields(), nil
}

View File

@ -0,0 +1,684 @@
/*
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"
v1 "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 := v1.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",
},
},
},
}
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:replicas":{},"f:selector":{}}}`)},
},
},
subresource: []metav1.ManagedFieldsEntry{
{
Manager: "scale",
Operation: metav1.ManagedFieldsOperationUpdate,
APIVersion: "autoscaling/v1",
FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:another":{}}}`)},
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: "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",
},
},
},
}
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
mappings ResourcePathMappings
parent []metav1.ManagedFieldsEntry
subresource []metav1.ManagedFieldsEntry
expected []metav1.ManagedFieldsEntry
}{
{
desc: "multi-version",
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",
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
handler := NewScaleHandler(
test.parent,
schema.GroupVersion{Group: "apps", Version: "v1"},
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"),
}
}