Check request info when updating managed fields during scale

- Test all versions to make sure each resource version is in the
  mappings
- Fail when request info contains an unrecognized version. We have tests
  that guarantee that all known versions are in the mappings. If we
  get a version in request info that is not there we should fail fast to
  prevent inconsistent behaviour (e.g. for some reason the mappings is
  not up to date).

Ensure all known versions are in mappings
This commit is contained in:
Andrea Nodari 2021-03-11 16:51:46 +01:00
parent 816e80206c
commit 09649e58b5
6 changed files with 177 additions and 16 deletions

View File

@ -26,12 +26,14 @@ 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/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage" "k8s.io/apiserver/pkg/storage"
storeerr "k8s.io/apiserver/pkg/storage/errors" storeerr "k8s.io/apiserver/pkg/storage/errors"
"k8s.io/apiserver/pkg/util/dryrun" "k8s.io/apiserver/pkg/util/dryrun"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/apps"
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
appsv1beta2 "k8s.io/kubernetes/pkg/apis/apps/v1beta2" appsv1beta2 "k8s.io/kubernetes/pkg/apis/apps/v1beta2"
@ -55,9 +57,16 @@ type DeploymentStorage struct {
Rollback *RollbackREST Rollback *RollbackREST
} }
// ReplicasPathMappings returns the mappings between each group version and a replicas path
func ReplicasPathMappings() fieldmanager.ResourcePathMappings {
return replicasPathInDeployment
}
// maps a group version to the replicas path in a deployment object // maps a group version to the replicas path in a deployment object
var replicasPathInDeployment = fieldmanager.ResourcePathMappings{ var replicasPathInDeployment = fieldmanager.ResourcePathMappings{
schema.GroupVersion{Group: "apps", Version: "v1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"), schema.GroupVersion{Group: "apps", Version: "v1beta1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"),
schema.GroupVersion{Group: "apps", Version: "v1beta2"}.String(): fieldpath.MakePathOrDie("spec", "replicas"),
schema.GroupVersion{Group: "apps", Version: "v1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"),
} }
// NewStorage returns new instance of DeploymentStorage. // NewStorage returns new instance of DeploymentStorage.
@ -383,9 +392,19 @@ func (i *scaleUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runti
return nil, errors.NewNotFound(apps.Resource("deployments/scale"), i.name) return nil, errors.NewNotFound(apps.Resource("deployments/scale"), i.name)
} }
groupVersion := schema.GroupVersion{Group: "apps", Version: "v1"}
if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
requestGroupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
if _, ok := replicasPathInDeployment[requestGroupVersion.String()]; ok {
groupVersion = requestGroupVersion
} else {
klog.Fatal("Unrecognized group/version in request info %q", requestGroupVersion.String())
}
}
managedFieldsHandler := fieldmanager.NewScaleHandler( managedFieldsHandler := fieldmanager.NewScaleHandler(
deployment.ManagedFields, deployment.ManagedFields,
schema.GroupVersion{Group: "apps", Version: "v1"}, groupVersion,
replicasPathInDeployment, replicasPathInDeployment,
) )

View File

@ -27,9 +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"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/apps"
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
appsv1beta2 "k8s.io/kubernetes/pkg/apis/apps/v1beta2" appsv1beta2 "k8s.io/kubernetes/pkg/apis/apps/v1beta2"
@ -51,9 +53,15 @@ type ReplicaSetStorage struct {
Scale *ScaleREST Scale *ScaleREST
} }
// ReplicasPathMappings returns the mappings between each group version and a replicas path
func ReplicasPathMappings() fieldmanager.ResourcePathMappings {
return replicasPathInReplicaSet
}
// maps a group version to the replicas path in a replicaset object // maps a group version to the replicas path in a replicaset object
var replicasPathInReplicaSet = fieldmanager.ResourcePathMappings{ var replicasPathInReplicaSet = fieldmanager.ResourcePathMappings{
schema.GroupVersion{Group: "apps", Version: "v1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"), schema.GroupVersion{Group: "apps", Version: "v1beta2"}.String(): fieldpath.MakePathOrDie("spec", "replicas"),
schema.GroupVersion{Group: "apps", Version: "v1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"),
} }
// NewStorage returns new instance of ReplicaSetStorage. // NewStorage returns new instance of ReplicaSetStorage.
@ -285,9 +293,19 @@ func (i *scaleUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runti
return nil, errors.NewNotFound(apps.Resource("replicasets/scale"), i.name) return nil, errors.NewNotFound(apps.Resource("replicasets/scale"), i.name)
} }
groupVersion := schema.GroupVersion{Group: "apps", Version: "v1"}
if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
requestGroupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
if _, ok := replicasPathInReplicaSet[requestGroupVersion.String()]; ok {
groupVersion = requestGroupVersion
} else {
klog.Fatal("Unrecognized group/version in request info %q", requestGroupVersion.String())
}
}
managedFieldsHandler := fieldmanager.NewScaleHandler( managedFieldsHandler := fieldmanager.NewScaleHandler(
replicaset.ManagedFields, replicaset.ManagedFields,
schema.GroupVersion{Group: "apps", Version: "v1"}, groupVersion,
replicasPathInReplicaSet, replicasPathInReplicaSet,
) )

View File

@ -25,9 +25,11 @@ 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/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/apps"
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
appsv1beta2 "k8s.io/kubernetes/pkg/apis/apps/v1beta2" appsv1beta2 "k8s.io/kubernetes/pkg/apis/apps/v1beta2"
@ -48,9 +50,16 @@ type StatefulSetStorage struct {
Scale *ScaleREST Scale *ScaleREST
} }
// ReplicasPathMappings returns the mappings between each group version and a replicas path
func ReplicasPathMappings() fieldmanager.ResourcePathMappings {
return replicasPathInStatefulSet
}
// maps a group version to the replicas path in a statefulset object // maps a group version to the replicas path in a statefulset object
var replicasPathInStatefulSet = fieldmanager.ResourcePathMappings{ var replicasPathInStatefulSet = fieldmanager.ResourcePathMappings{
schema.GroupVersion{Group: "apps", Version: "v1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"), schema.GroupVersion{Group: "apps", Version: "v1beta1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"),
schema.GroupVersion{Group: "apps", Version: "v1beta2"}.String(): fieldpath.MakePathOrDie("spec", "replicas"),
schema.GroupVersion{Group: "apps", Version: "v1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"),
} }
// NewStorage returns new instance of StatefulSetStorage. // NewStorage returns new instance of StatefulSetStorage.
@ -271,9 +280,19 @@ func (i *scaleUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runti
return nil, errors.NewNotFound(apps.Resource("statefulsets/scale"), i.name) return nil, errors.NewNotFound(apps.Resource("statefulsets/scale"), i.name)
} }
groupVersion := schema.GroupVersion{Group: "apps", Version: "v1"}
if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
requestGroupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
if _, ok := replicasPathInStatefulSet[requestGroupVersion.String()]; ok {
groupVersion = requestGroupVersion
} else {
klog.Fatal("Unrecognized group/version in request info %q", requestGroupVersion.String())
}
}
managedFieldsHandler := fieldmanager.NewScaleHandler( managedFieldsHandler := fieldmanager.NewScaleHandler(
statefulset.ManagedFields, statefulset.ManagedFields,
schema.GroupVersion{Group: "apps", Version: "v1"}, groupVersion,
replicasPathInStatefulSet, replicasPathInStatefulSet,
) )

View File

@ -28,9 +28,11 @@ 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/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/apis/autoscaling" "k8s.io/kubernetes/pkg/apis/autoscaling"
autoscalingv1 "k8s.io/kubernetes/pkg/apis/autoscaling/v1" autoscalingv1 "k8s.io/kubernetes/pkg/apis/autoscaling/v1"
"k8s.io/kubernetes/pkg/apis/autoscaling/validation" "k8s.io/kubernetes/pkg/apis/autoscaling/validation"
@ -50,6 +52,11 @@ type ControllerStorage struct {
Scale *ScaleREST Scale *ScaleREST
} }
// ReplicasPathMappings returns the mappings between each group version and a replicas path
func ReplicasPathMappings() fieldmanager.ResourcePathMappings {
return replicasPathInReplicationController
}
// maps a group version to the replicas path in a deployment object // maps a group version to the replicas path in a deployment object
var replicasPathInReplicationController = fieldmanager.ResourcePathMappings{ var replicasPathInReplicationController = fieldmanager.ResourcePathMappings{
schema.GroupVersion{Group: "", Version: "v1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"), schema.GroupVersion{Group: "", Version: "v1"}.String(): fieldpath.MakePathOrDie("spec", "replicas"),
@ -245,9 +252,19 @@ func (i *scaleUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runti
return nil, errors.NewNotFound(api.Resource("replicationcontrollers/scale"), i.name) return nil, errors.NewNotFound(api.Resource("replicationcontrollers/scale"), i.name)
} }
groupVersion := schema.GroupVersion{Group: "", Version: "v1"}
if requestInfo, found := genericapirequest.RequestInfoFrom(ctx); found {
requestGroupVersion := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion}
if _, ok := replicasPathInReplicationController[requestGroupVersion.String()]; ok {
groupVersion = requestGroupVersion
} else {
klog.Fatal("Unrecognized group/version in request info %q", requestGroupVersion.String())
}
}
managedFieldsHandler := fieldmanager.NewScaleHandler( managedFieldsHandler := fieldmanager.NewScaleHandler(
replicationcontroller.ManagedFields, replicationcontroller.ManagedFields,
schema.GroupVersion{Group: "", Version: "v1"}, groupVersion,
replicasPathInReplicationController, replicasPathInReplicationController,
) )

View File

@ -39,17 +39,17 @@ type ResourcePathMappings map[string]fieldpath.Path
// ScaleHandler manages the conversion of managed fields between a main // ScaleHandler manages the conversion of managed fields between a main
// resource and the scale subresource // resource and the scale subresource
type ScaleHandler struct { type ScaleHandler struct {
parentEntries []metav1.ManagedFieldsEntry parentEntries []metav1.ManagedFieldsEntry
defaultGroupVersion schema.GroupVersion groupVersion schema.GroupVersion
mappings ResourcePathMappings mappings ResourcePathMappings
} }
// NewScaleHandler creates a new ScaleHandler // NewScaleHandler creates a new ScaleHandler
func NewScaleHandler(parentEntries []metav1.ManagedFieldsEntry, defaultGroupVersion schema.GroupVersion, mappings ResourcePathMappings) *ScaleHandler { func NewScaleHandler(parentEntries []metav1.ManagedFieldsEntry, groupVersion schema.GroupVersion, mappings ResourcePathMappings) *ScaleHandler {
return &ScaleHandler{ return &ScaleHandler{
parentEntries: parentEntries, parentEntries: parentEntries,
defaultGroupVersion: defaultGroupVersion, groupVersion: groupVersion,
mappings: mappings, mappings: mappings,
} }
} }
@ -136,8 +136,8 @@ func (h *ScaleHandler) ToParent(scaleEntries []metav1.ManagedFieldsEntry) ([]met
for manager, versionedSet := range scaleFields { for manager, versionedSet := range scaleFields {
newVersionedSet := fieldpath.NewVersionedSet( newVersionedSet := fieldpath.NewVersionedSet(
fieldpath.NewSet(h.mappings[h.defaultGroupVersion.String()]), fieldpath.NewSet(h.mappings[h.groupVersion.String()]),
fieldpath.APIVersion(h.defaultGroupVersion.String()), fieldpath.APIVersion(h.groupVersion.String()),
versionedSet.Applied(), versionedSet.Applied(),
) )
f[manager] = newVersionedSet f[manager] = newVersionedSet

View File

@ -27,11 +27,18 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
genericfeatures "k8s.io/apiserver/pkg/features" genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
featuregatetesting "k8s.io/component-base/featuregate/testing" featuregatetesting "k8s.io/component-base/featuregate/testing"
deploymentstorage "k8s.io/kubernetes/pkg/registry/apps/deployment/storage"
replicasetstorage "k8s.io/kubernetes/pkg/registry/apps/replicaset/storage"
statefulsetstorage "k8s.io/kubernetes/pkg/registry/apps/statefulset/storage"
replicationcontrollerstorage "k8s.io/kubernetes/pkg/registry/core/replicationcontroller/storage"
) )
type scaleTest struct { type scaleTest struct {
@ -231,6 +238,87 @@ func TestScaleAllResources(t *testing.T) {
} }
} }
func TestScaleUpdateOnlyStatus(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
_, client, closeFn := setup(t)
defer closeFn()
resource := "deployments"
path := "/apis/apps/v1"
validObject := []byte(validAppsV1("Deployment"))
// Create the object
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
AbsPath(path).
Namespace("default").
Resource(resource).
Name("test").
Param("fieldManager", "apply_test").
Body(validObject).
Do(context.TODO()).Get()
if err != nil {
t.Fatalf("Failed to create object using apply: %v", err)
}
obj := retrieveObject(t, client, path, resource)
assertReplicasValue(t, obj, 1)
assertReplicasOwnership(t, obj, "apply_test")
// Call scale subresource to update replicas
_, err = client.CoreV1().RESTClient().
Patch(types.MergePatchType).
AbsPath(path).
Namespace("default").
Resource(resource).
Name("test").
SubResource("scale").
Param("fieldManager", "scale_test").
Body([]byte(`{"status":{"replicas": 42}}`)).
Do(context.TODO()).Get()
if err != nil {
t.Fatalf("Failed to scale object: %v", err)
}
obj = retrieveObject(t, client, path, resource)
assertReplicasValue(t, obj, 1)
assertReplicasOwnership(t, obj, "apply_test")
}
func TestAllKnownVersionsAreInMappings(t *testing.T) {
cases := []struct {
groupKind schema.GroupKind
mappings fieldmanager.ResourcePathMappings
}{
{
groupKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"},
mappings: replicasetstorage.ReplicasPathMappings(),
},
{
groupKind: schema.GroupKind{Group: "apps", Kind: "StatefulSet"},
mappings: statefulsetstorage.ReplicasPathMappings(),
},
{
groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
mappings: deploymentstorage.ReplicasPathMappings(),
},
{
groupKind: schema.GroupKind{Group: "", Kind: "ReplicationController"},
mappings: replicationcontrollerstorage.ReplicasPathMappings(),
},
}
for _, c := range cases {
knownVersions := scheme.Scheme.VersionsForGroupKind(c.groupKind)
for _, version := range knownVersions {
if _, ok := c.mappings[version.String()]; !ok {
t.Errorf("missing version %v for %v mappings", version, c.groupKind)
}
}
if len(knownVersions) != len(c.mappings) {
t.Errorf("%v mappings has extra items: %v vs %v", c.groupKind, c.mappings, knownVersions)
}
}
}
func validAppsV1(kind string) string { func validAppsV1(kind string) string {
return fmt.Sprintf(`{ return fmt.Sprintf(`{
"apiVersion": "apps/v1", "apiVersion": "apps/v1",