mirror of https://github.com/k3s-io/k3s
Plugin FieldManager in CRD handler, change to API
parent
1751fc013f
commit
b55417f429
|
@ -400,8 +400,8 @@ func AddDryRunFlag(cmd *cobra.Command) {
|
|||
}
|
||||
|
||||
func AddServerSideApplyFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().Bool("server-side", false, "If true, apply runs in the server instead of the client.")
|
||||
cmd.Flags().Bool("force-conflicts", false, "If true, server-side apply will force the changes against conflicts.")
|
||||
cmd.Flags().Bool("server-side", false, "If true, apply runs in the server instead of the client. This is an alpha feature and flag.")
|
||||
cmd.Flags().Bool("force-conflicts", false, "If true, server-side apply will force the changes against conflicts. This is an alpha feature and flag.")
|
||||
}
|
||||
|
||||
func AddIncludeUninitializedFlag(cmd *cobra.Command) {
|
||||
|
|
|
@ -3451,6 +3451,9 @@ type ServiceSpec struct {
|
|||
// More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies
|
||||
// +patchMergeKey=port
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=port
|
||||
// +listMapKey=protocol
|
||||
Ports []ServicePort `json:"ports,omitempty" patchStrategy:"merge" patchMergeKey:"port" protobuf:"bytes,1,rep,name=ports"`
|
||||
|
||||
// Route service traffic to pods with label keys and values matching this
|
||||
|
|
|
@ -55,11 +55,14 @@ go_library(
|
|||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/version:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/discovery:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/handlers:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/metrics:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/features:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||
|
|
|
@ -21,6 +21,18 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
|
||||
internalinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/status"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
@ -32,20 +44,6 @@ import (
|
|||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset"
|
||||
internalinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/status"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition"
|
||||
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -184,6 +182,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
|
|||
c.ExtraConfig.ServiceResolver,
|
||||
c.ExtraConfig.AuthResolverWrapper,
|
||||
c.ExtraConfig.MasterCount,
|
||||
s.GenericAPIServer.Authorizer,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -28,8 +28,17 @@ import (
|
|||
"github.com/go-openapi/spec"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/validate"
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||
"k8s.io/apiextensions-apiserver/pkg/crdserverscheme"
|
||||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
|
@ -44,30 +53,22 @@ import (
|
|||
"k8s.io/apimachinery/pkg/types"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||
apirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/client-go/scale"
|
||||
"k8s.io/client-go/scale/scheme/autoscalingv1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||
"k8s.io/apiextensions-apiserver/pkg/crdserverscheme"
|
||||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
// crdHandler serves the `/apis` endpoint.
|
||||
|
@ -96,6 +97,9 @@ type crdHandler struct {
|
|||
masterCount int
|
||||
|
||||
converterFactory *conversion.CRConverterFactory
|
||||
|
||||
// so that we can do create on update.
|
||||
authorizer authorizer.Authorizer
|
||||
}
|
||||
|
||||
// crdInfo stores enough information to serve the storage for the custom resource
|
||||
|
@ -134,7 +138,8 @@ func NewCustomResourceDefinitionHandler(
|
|||
establishingController *establish.EstablishingController,
|
||||
serviceResolver webhook.ServiceResolver,
|
||||
authResolverWrapper webhook.AuthenticationInfoResolverWrapper,
|
||||
masterCount int) (*crdHandler, error) {
|
||||
masterCount int,
|
||||
authorizer authorizer.Authorizer) (*crdHandler, error) {
|
||||
ret := &crdHandler{
|
||||
versionDiscoveryHandler: versionDiscoveryHandler,
|
||||
groupDiscoveryHandler: groupDiscoveryHandler,
|
||||
|
@ -145,6 +150,7 @@ func NewCustomResourceDefinitionHandler(
|
|||
admission: admission,
|
||||
establishingController: establishingController,
|
||||
masterCount: masterCount,
|
||||
authorizer: authorizer,
|
||||
}
|
||||
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: ret.updateCustomResourceDefinition,
|
||||
|
@ -228,6 +234,9 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
string(types.JSONPatchType),
|
||||
string(types.MergePatchType),
|
||||
}
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
|
||||
supportedTypes = append(supportedTypes, string(types.ApplyPatchType))
|
||||
}
|
||||
|
||||
var handler http.HandlerFunc
|
||||
subresources, err := getSubresourcesForVersion(crd, requestInfo.APIVersion)
|
||||
|
@ -565,6 +574,18 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
|||
MetaGroupVersion: metav1.SchemeGroupVersion,
|
||||
|
||||
TableConvertor: storages[v.Name].CustomResource,
|
||||
|
||||
Authorizer: r.authorizer,
|
||||
}
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
|
||||
reqScope := requestScopes[v.Name]
|
||||
reqScope.FieldManager = fieldmanager.NewCRDFieldManager(
|
||||
reqScope.Convertor,
|
||||
reqScope.Defaulter,
|
||||
reqScope.Kind.GroupVersion(),
|
||||
reqScope.HubGroupVersion,
|
||||
)
|
||||
requestScopes[v.Name] = reqScope
|
||||
}
|
||||
|
||||
// override scaleSpec subresource values
|
||||
|
|
|
@ -9,6 +9,7 @@ load(
|
|||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"apply_test.go",
|
||||
"basic_test.go",
|
||||
"finalization_test.go",
|
||||
"objectmeta_test.go",
|
||||
|
@ -41,6 +42,7 @@ go_test(
|
|||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/features:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature/testing:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/dynamic:go_default_library",
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
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 integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
func TestApplyBasic(t *testing.T) {
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
|
||||
|
||||
tearDown, config, _, err := fixtures.StartDefaultServer(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tearDown()
|
||||
|
||||
apiExtensionClient, err := clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dynamicClient, err := dynamic.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
|
||||
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
kind := noxuDefinition.Spec.Names.Kind
|
||||
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
|
||||
|
||||
rest := apiExtensionClient.Discovery().RESTClient()
|
||||
yamlBody := []byte(fmt.Sprintf(`
|
||||
apiVersion: %s
|
||||
kind: %s
|
||||
metadata:
|
||||
name: mytest
|
||||
values:
|
||||
numVal: 1
|
||||
boolVal: true
|
||||
stringVal: "1"`, apiVersion, kind))
|
||||
result, err := rest.Patch(types.ApplyPatchType).
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
|
||||
Name("mytest").
|
||||
Body(yamlBody).
|
||||
DoRaw()
|
||||
if err != nil {
|
||||
t.Fatal(err, string(result))
|
||||
}
|
||||
|
||||
result, err = rest.Patch(types.MergePatchType).
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
|
||||
Name("mytest").
|
||||
Body([]byte(`{"values":{"numVal": 5}}`)).
|
||||
DoRaw()
|
||||
if err != nil {
|
||||
t.Fatal(err, string(result))
|
||||
}
|
||||
|
||||
// Re-apply the same object, we should get conflicts now.
|
||||
result, err = rest.Patch(types.ApplyPatchType).
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
|
||||
Name("mytest").
|
||||
Body(yamlBody).
|
||||
DoRaw()
|
||||
if err == nil {
|
||||
t.Fatalf("Expecting to get conflicts when applying object, got no error: %s", result)
|
||||
}
|
||||
status, ok := err.(*errors.StatusError)
|
||||
if !ok {
|
||||
t.Fatalf("Expecting to get conflicts as API error")
|
||||
}
|
||||
if len(status.Status().Details.Causes) < 1 {
|
||||
t.Fatalf("Expecting to get at least one conflict when applying object, got: %v", status.Status().Details.Causes)
|
||||
}
|
||||
|
||||
// Re-apply with force, should work fine.
|
||||
result, err = rest.Patch(types.ApplyPatchType).
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
|
||||
Name("mytest").
|
||||
Param("force", "true").
|
||||
Body(yamlBody).
|
||||
DoRaw()
|
||||
if err != nil {
|
||||
t.Fatal(err, string(result))
|
||||
}
|
||||
|
||||
}
|
|
@ -273,6 +273,12 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||
sort.Slice(j.MatchExpressions, func(a, b int) bool { return j.MatchExpressions[a].Key < j.MatchExpressions[b].Key })
|
||||
}
|
||||
},
|
||||
func(j *metav1.ManagedFieldsEntry, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(j)
|
||||
if j.Fields != nil && len(j.Fields.Map) == 0 {
|
||||
j.Fields = nil
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ go_test(
|
|||
"helpers_test.go",
|
||||
"labels_test.go",
|
||||
"micro_time_test.go",
|
||||
"options_test.go",
|
||||
"time_test.go",
|
||||
"types_test.go",
|
||||
],
|
||||
|
|
|
@ -63,8 +63,8 @@ type Object interface {
|
|||
SetOwnerReferences([]OwnerReference)
|
||||
GetClusterName() string
|
||||
SetClusterName(clusterName string)
|
||||
GetManagedFields() map[string]VersionedFields
|
||||
SetManagedFields(lastApplied map[string]VersionedFields)
|
||||
GetManagedFields() []ManagedFieldsEntry
|
||||
SetManagedFields(lastApplied []ManagedFieldsEntry)
|
||||
}
|
||||
|
||||
// ListMetaAccessor retrieves the list interface from an object
|
||||
|
@ -171,13 +171,13 @@ func (meta *ObjectMeta) SetOwnerReferences(references []OwnerReference) {
|
|||
func (meta *ObjectMeta) GetClusterName() string { return meta.ClusterName }
|
||||
func (meta *ObjectMeta) SetClusterName(clusterName string) { meta.ClusterName = clusterName }
|
||||
|
||||
func (meta *ObjectMeta) GetManagedFields() map[string]VersionedFields {
|
||||
func (meta *ObjectMeta) GetManagedFields() []ManagedFieldsEntry {
|
||||
return meta.ManagedFields
|
||||
}
|
||||
|
||||
func (meta *ObjectMeta) SetManagedFields(ManagedFields map[string]VersionedFields) {
|
||||
meta.ManagedFields = make(map[string]VersionedFields, len(ManagedFields))
|
||||
for key, value := range ManagedFields {
|
||||
meta.ManagedFields[key] = value
|
||||
func (meta *ObjectMeta) SetManagedFields(ManagedFields []ManagedFieldsEntry) {
|
||||
meta.ManagedFields = make([]ManagedFieldsEntry, len(ManagedFields))
|
||||
for i, value := range ManagedFields {
|
||||
meta.ManagedFields[i] = value
|
||||
}
|
||||
}
|
||||
|
|
|
@ -253,15 +253,18 @@ type ObjectMeta struct {
|
|||
// +optional
|
||||
ClusterName string `json:"clusterName,omitempty" protobuf:"bytes,15,opt,name=clusterName"`
|
||||
|
||||
// ManagedFields is a map of workflow-id to the set of fields
|
||||
// ManagedFields maps workflow-id and version to the set of fields
|
||||
// that are managed by that workflow. This is mostly for internal
|
||||
// housekeeping, and users typically shouldn't need to set or
|
||||
// understand this field. A workflow can be the user's name, a
|
||||
// controller's name, or the name of a specific apply path like
|
||||
// "ci-cd". The set of fields is always in the version that the
|
||||
// workflow used when modifying the object.
|
||||
//
|
||||
// This field is alpha and can be changed or removed without notice.
|
||||
//
|
||||
// +optional
|
||||
ManagedFields map[string]VersionedFields `json:"managedFields,omitempty" protobuf:"bytes,17,rep,name=managedFields"`
|
||||
ManagedFields []ManagedFieldsEntry `json:"managedFields,omitempty" protobuf:"bytes,17,rep,name=managedFields"`
|
||||
}
|
||||
|
||||
// Initializers tracks the progress of initialization.
|
||||
|
@ -1043,18 +1046,35 @@ const (
|
|||
LabelSelectorOpDoesNotExist LabelSelectorOperator = "DoesNotExist"
|
||||
)
|
||||
|
||||
// VersionedFields is a pair of a FieldSet and the group version of the resource
|
||||
// ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource
|
||||
// that the fieldset applies to.
|
||||
type VersionedFields struct {
|
||||
type ManagedFieldsEntry struct {
|
||||
// Manager is an identifier of the workflow managing these fields.
|
||||
Manager string `json:"manager,omitempty" protobuf:"bytes,1,opt,name=manager"`
|
||||
// Operation is the type of operation which lead to this ManagedFieldsEntry being created.
|
||||
// The only valid values for this field are 'Apply' and 'Update'.
|
||||
Operation ManagedFieldsOperationType `json:"operation,omitempty" protobuf:"bytes,2,opt,name=operation,casttype=ManagedFieldsOperationType"`
|
||||
// APIVersion defines the version of this resource that this field set
|
||||
// applies to. The format is "group/version" just like the top-level
|
||||
// APIVersion field. It is necessary to track the version of a field
|
||||
// set because it cannot be automatically converted.
|
||||
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,1,opt,name=apiVersion"`
|
||||
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,3,opt,name=apiVersion"`
|
||||
// Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply'
|
||||
// +optional
|
||||
Time *Time `json:"time,omitempty" protobuf:"bytes,4,opt,name=time"`
|
||||
// Fields identifies a set of fields.
|
||||
Fields Fields `json:"fields,omitempty" protobuf:"bytes,2,opt,name=fields,casttype=Fields"`
|
||||
// +optional
|
||||
Fields *Fields `json:"fields,omitempty" protobuf:"bytes,5,opt,name=fields,casttype=Fields"`
|
||||
}
|
||||
|
||||
// ManagedFieldsOperationType is the type of operation which lead to a ManagedFieldsEntry being created.
|
||||
type ManagedFieldsOperationType string
|
||||
|
||||
const (
|
||||
ManagedFieldsOperationApply ManagedFieldsOperationType = "Apply"
|
||||
ManagedFieldsOperationUpdate ManagedFieldsOperationType = "Update"
|
||||
)
|
||||
|
||||
// Fields stores a set of fields in a data structure like a Trie.
|
||||
// To understand how this is used, see: https://github.com/kubernetes-sigs/structured-merge-diff
|
||||
type Fields struct {
|
||||
|
|
|
@ -143,13 +143,20 @@ func (u *Unstructured) setNestedField(value interface{}, fields ...string) {
|
|||
SetNestedField(u.Object, value, fields...)
|
||||
}
|
||||
|
||||
func (u *Unstructured) setNestedSlice(value []string, fields ...string) {
|
||||
func (u *Unstructured) setNestedStringSlice(value []string, fields ...string) {
|
||||
if u.Object == nil {
|
||||
u.Object = make(map[string]interface{})
|
||||
}
|
||||
SetNestedStringSlice(u.Object, value, fields...)
|
||||
}
|
||||
|
||||
func (u *Unstructured) setNestedSlice(value []interface{}, fields ...string) {
|
||||
if u.Object == nil {
|
||||
u.Object = make(map[string]interface{})
|
||||
}
|
||||
SetNestedSlice(u.Object, value, fields...)
|
||||
}
|
||||
|
||||
func (u *Unstructured) setNestedMap(value map[string]string, fields ...string) {
|
||||
if u.Object == nil {
|
||||
u.Object = make(map[string]interface{})
|
||||
|
@ -436,7 +443,7 @@ func (u *Unstructured) SetFinalizers(finalizers []string) {
|
|||
RemoveNestedField(u.Object, "metadata", "finalizers")
|
||||
return
|
||||
}
|
||||
u.setNestedSlice(finalizers, "metadata", "finalizers")
|
||||
u.setNestedStringSlice(finalizers, "metadata", "finalizers")
|
||||
}
|
||||
|
||||
func (u *Unstructured) GetClusterName() string {
|
||||
|
@ -451,27 +458,41 @@ func (u *Unstructured) SetClusterName(clusterName string) {
|
|||
u.setNestedField(clusterName, "metadata", "clusterName")
|
||||
}
|
||||
|
||||
func (u *Unstructured) GetManagedFields() map[string]metav1.VersionedFields {
|
||||
m, found, err := nestedMapNoCopy(u.Object, "metadata", "managedFields")
|
||||
func (u *Unstructured) GetManagedFields() []metav1.ManagedFieldsEntry {
|
||||
items, found, err := NestedSlice(u.Object, "metadata", "managedFields")
|
||||
if !found || err != nil {
|
||||
return nil
|
||||
}
|
||||
out := &map[string]metav1.VersionedFields{}
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(m, out); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("unable to retrieve managedFields for object: %v", err))
|
||||
return nil
|
||||
managedFields := []metav1.ManagedFieldsEntry{}
|
||||
for _, item := range items {
|
||||
m, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
utilruntime.HandleError(fmt.Errorf("unable to retrieve managedFields for object, item %v is not a map", item))
|
||||
return nil
|
||||
}
|
||||
out := metav1.ManagedFieldsEntry{}
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(m, &out); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("unable to retrieve managedFields for object: %v", err))
|
||||
return nil
|
||||
}
|
||||
managedFields = append(managedFields, out)
|
||||
}
|
||||
return *out
|
||||
return managedFields
|
||||
}
|
||||
|
||||
func (u *Unstructured) SetManagedFields(managedFields map[string]metav1.VersionedFields) {
|
||||
func (u *Unstructured) SetManagedFields(managedFields []metav1.ManagedFieldsEntry) {
|
||||
if managedFields == nil {
|
||||
RemoveNestedField(u.Object, "metadata", "managedFields")
|
||||
return
|
||||
}
|
||||
out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&managedFields)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("unable to retrieve managedFields for object: %v", err))
|
||||
items := []interface{}{}
|
||||
for _, managedFieldsEntry := range managedFields {
|
||||
out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&managedFieldsEntry)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("unable to set managedFields for object: %v", err))
|
||||
return
|
||||
}
|
||||
items = append(items, out)
|
||||
}
|
||||
u.setNestedField(out, "metadata", "managedFields")
|
||||
u.setNestedSlice(items, "metadata", "managedFields")
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ go_library(
|
|||
"//vendor/github.com/evanphx/json-patch:go_default_library",
|
||||
"//vendor/golang.org/x/net/websocket:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
"//vendor/k8s.io/utils/trace:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
|
@ -140,7 +141,7 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte
|
|||
return
|
||||
}
|
||||
|
||||
obj, err = scope.FieldManager.Update(liveObj, obj, "create")
|
||||
obj, err = scope.FieldManager.Update(liveObj, obj, prefixFromUserAgent(req.UserAgent()))
|
||||
if err != nil {
|
||||
scope.err(fmt.Errorf("failed to update object managed fields: %v", err), w, req)
|
||||
return
|
||||
|
@ -191,3 +192,7 @@ type namedCreaterAdapter struct {
|
|||
func (c *namedCreaterAdapter) Create(ctx context.Context, name string, obj runtime.Object, createValidatingAdmission rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
return c.Creater.Create(ctx, obj, createValidatingAdmission, options)
|
||||
}
|
||||
|
||||
func prefixFromUserAgent(u string) string {
|
||||
return strings.Split(u, "/")[0]
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ go_library(
|
|||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal:go_default_library",
|
||||
|
|
|
@ -18,8 +18,10 @@ package fieldmanager
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
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"
|
||||
|
@ -60,6 +62,22 @@ func NewFieldManager(models openapiproto.Models, objectConverter runtime.ObjectC
|
|||
}, nil
|
||||
}
|
||||
|
||||
// NewCRDFieldManager creates a new FieldManager specifically for
|
||||
// CRDs. This doesn't use openapi models (and it doesn't support the
|
||||
// validation field right now).
|
||||
func NewCRDFieldManager(objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion) *FieldManager {
|
||||
return &FieldManager{
|
||||
typeConverter: internal.DeducedTypeConverter{},
|
||||
objectConverter: objectConverter,
|
||||
objectDefaulter: objectDefaulter,
|
||||
groupVersion: gv,
|
||||
hubVersion: hub,
|
||||
updater: merge.Updater{
|
||||
Converter: internal.NewVersionConverter(internal.DeducedTypeConverter{}, objectConverter, hub),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Update is used when the object has already been merged (non-apply
|
||||
// use-case), and simply updates the managed fields in the output
|
||||
// object.
|
||||
|
@ -97,6 +115,11 @@ func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (r
|
|||
return nil, fmt.Errorf("failed to create typed live object: %v", err)
|
||||
}
|
||||
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
|
||||
manager, err = f.buildManagerInfo(manager, metav1.ManagedFieldsOperationUpdate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build manager identifier: %v", err)
|
||||
}
|
||||
|
||||
managed, err = f.updater.Update(liveObjTyped, newObjTyped, apiVersion, managed, manager)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update ManagedFields: %v", err)
|
||||
|
@ -134,8 +157,13 @@ func (f *FieldManager) Apply(liveObj runtime.Object, patch []byte, force bool) (
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create typed live object: %v", err)
|
||||
}
|
||||
manager, err := f.buildManagerInfo(applyManager, metav1.ManagedFieldsOperationApply)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build manager identifier: %v", err)
|
||||
}
|
||||
|
||||
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
|
||||
newObjTyped, managed, err := f.updater.Apply(liveObjTyped, patchObjTyped, apiVersion, managed, applyManager, force)
|
||||
newObjTyped, managed, err := f.updater.Apply(liveObjTyped, patchObjTyped, apiVersion, managed, manager, force)
|
||||
if err != nil {
|
||||
if conflicts, ok := err.(merge.Conflicts); ok {
|
||||
return nil, errors.NewApplyConflict(conflicts)
|
||||
|
@ -172,3 +200,16 @@ func (f *FieldManager) toVersioned(obj runtime.Object) (runtime.Object, error) {
|
|||
func (f *FieldManager) toUnversioned(obj runtime.Object) (runtime.Object, error) {
|
||||
return f.objectConverter.ConvertToVersion(obj, f.hubVersion)
|
||||
}
|
||||
|
||||
func (f *FieldManager) buildManagerInfo(prefix string, operation metav1.ManagedFieldsOperationType) (string, error) {
|
||||
managerInfo := metav1.ManagedFieldsEntry{
|
||||
Manager: prefix,
|
||||
Operation: operation,
|
||||
APIVersion: f.groupVersion.String(),
|
||||
Time: &metav1.Time{Time: time.Now().UTC()},
|
||||
}
|
||||
if managerInfo.Manager == "" {
|
||||
managerInfo.Manager = "unknown"
|
||||
}
|
||||
return internal.DecodeManager(&managerInfo)
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ go_test(
|
|||
"typeconverter_test.go",
|
||||
"versionconverter_test.go",
|
||||
],
|
||||
data = ["//api/openapi-spec:swagger-spec"],
|
||||
data = ["//api/openapi-spec"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
|
|
|
@ -35,7 +35,7 @@ type gvkParser struct {
|
|||
parser typed.Parser
|
||||
}
|
||||
|
||||
func (p *gvkParser) Type(gvk schema.GroupVersionKind) *typed.ParseableType {
|
||||
func (p *gvkParser) Type(gvk schema.GroupVersionKind) typed.ParseableType {
|
||||
typeName, ok := p.gvks[gvk]
|
||||
if !ok {
|
||||
return nil
|
||||
|
|
|
@ -17,7 +17,9 @@ 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"
|
||||
|
@ -72,21 +74,53 @@ func EncodeObjectManagedFields(obj runtime.Object, fields fieldpath.ManagedField
|
|||
|
||||
// decodeManagedFields converts ManagedFields from the wire format (api format)
|
||||
// to the format used by sigs.k8s.io/structured-merge-diff
|
||||
func decodeManagedFields(encodedManagedFields map[string]metav1.VersionedFields) (managedFields fieldpath.ManagedFields, err error) {
|
||||
func decodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (managedFields fieldpath.ManagedFields, err error) {
|
||||
managedFields = make(map[string]*fieldpath.VersionedSet, len(encodedManagedFields))
|
||||
for manager, encodedVersionedSet := range encodedManagedFields {
|
||||
for _, encodedVersionedSet := range encodedManagedFields {
|
||||
manager, err := DecodeManager(&encodedVersionedSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding manager from %v: %v", encodedVersionedSet, err)
|
||||
}
|
||||
managedFields[manager], err = decodeVersionedSet(&encodedVersionedSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding versioned set for %v: %v", manager, err)
|
||||
return nil, fmt.Errorf("error decoding versioned set from %v: %v", encodedVersionedSet, err)
|
||||
}
|
||||
}
|
||||
return managedFields, nil
|
||||
}
|
||||
|
||||
func decodeVersionedSet(encodedVersionedSet *metav1.VersionedFields) (versionedSet *fieldpath.VersionedSet, err error) {
|
||||
// DecodeManager creates a manager identifier string from a ManagedFieldsEntry
|
||||
func DecodeManager(encodedManager *metav1.ManagedFieldsEntry) (manager string, err error) {
|
||||
encodedManagerCopy := *encodedManager
|
||||
|
||||
// Never include the fields in the manager identifier
|
||||
encodedManagerCopy.Fields = nil
|
||||
|
||||
// For appliers, don't include the APIVersion or Time in the manager identifier,
|
||||
// so it will always have the same manager identifier each time it applied.
|
||||
if encodedManager.Operation == metav1.ManagedFieldsOperationApply {
|
||||
encodedManagerCopy.APIVersion = ""
|
||||
encodedManagerCopy.Time = nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
versionedSet = &fieldpath.VersionedSet{}
|
||||
versionedSet.APIVersion = fieldpath.APIVersion(encodedVersionedSet.APIVersion)
|
||||
set, err := FieldsToSet(encodedVersionedSet.Fields)
|
||||
|
||||
fields := metav1.Fields{}
|
||||
if encodedVersionedSet.Fields != nil {
|
||||
fields = *encodedVersionedSet.Fields
|
||||
}
|
||||
set, err := FieldsToSet(fields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding set: %v", err)
|
||||
}
|
||||
|
@ -96,24 +130,42 @@ func decodeVersionedSet(encodedVersionedSet *metav1.VersionedFields) (versionedS
|
|||
|
||||
// encodeManagedFields converts ManagedFields from the the format used by
|
||||
// sigs.k8s.io/structured-merge-diff to the the wire format (api format)
|
||||
func encodeManagedFields(managedFields fieldpath.ManagedFields) (encodedManagedFields map[string]metav1.VersionedFields, err error) {
|
||||
encodedManagedFields = make(map[string]metav1.VersionedFields, len(managedFields))
|
||||
for manager, versionedSet := range managedFields {
|
||||
v, err := encodeVersionedSet(versionedSet)
|
||||
func encodeManagedFields(managedFields fieldpath.ManagedFields) (encodedManagedFields []metav1.ManagedFieldsEntry, err error) {
|
||||
// Sort the keys so a predictable order will be used.
|
||||
managers := []string{}
|
||||
for manager := range managedFields {
|
||||
managers = append(managers, manager)
|
||||
}
|
||||
sort.Strings(managers)
|
||||
|
||||
encodedManagedFields = []metav1.ManagedFieldsEntry{}
|
||||
for _, manager := range managers {
|
||||
versionedSet := managedFields[manager]
|
||||
v, err := encodeManagerVersionedSet(manager, versionedSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error encoding versioned set for %v: %v", manager, err)
|
||||
}
|
||||
encodedManagedFields[manager] = *v
|
||||
encodedManagedFields = append(encodedManagedFields, *v)
|
||||
}
|
||||
return encodedManagedFields, nil
|
||||
}
|
||||
|
||||
func encodeVersionedSet(versionedSet *fieldpath.VersionedSet) (encodedVersionedSet *metav1.VersionedFields, err error) {
|
||||
encodedVersionedSet = &metav1.VersionedFields{}
|
||||
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 and Fields from the VersionedSet
|
||||
encodedVersionedSet.APIVersion = string(versionedSet.APIVersion)
|
||||
encodedVersionedSet.Fields, err = SetToFields(*versionedSet.Set)
|
||||
fields, err := SetToFields(*versionedSet.Set)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error encoding set: %v", err)
|
||||
}
|
||||
encodedVersionedSet.Fields = &fields
|
||||
|
||||
return encodedVersionedSet, nil
|
||||
}
|
||||
|
|
|
@ -29,29 +29,36 @@ import (
|
|||
// sigs.k8s.io/structured-merge-diff to the wire format (api format) and back
|
||||
func TestRoundTripManagedFields(t *testing.T) {
|
||||
tests := []string{
|
||||
`foo:
|
||||
apiVersion: v1
|
||||
`- apiVersion: v1
|
||||
fields:
|
||||
i:5:
|
||||
f:i: {}
|
||||
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
|
||||
fields:
|
||||
i:5:
|
||||
f:i: {}
|
||||
manager: foo
|
||||
operation: Update
|
||||
time: "2011-12-13T14:15:16Z"
|
||||
`,
|
||||
`foo:
|
||||
apiVersion: v1
|
||||
`- apiVersion: v1
|
||||
fields:
|
||||
f:spec:
|
||||
f:containers:
|
||||
k:{"name":"c"}:
|
||||
f:image: {}
|
||||
f:name: {}
|
||||
manager: foo
|
||||
operation: Apply
|
||||
`,
|
||||
`foo:
|
||||
apiVersion: v1
|
||||
`- apiVersion: v1
|
||||
fields:
|
||||
f:apiVersion: {}
|
||||
f:kind: {}
|
||||
|
@ -77,9 +84,10 @@ func TestRoundTripManagedFields(t *testing.T) {
|
|||
f:ports:
|
||||
i:0:
|
||||
f:containerPort: {}
|
||||
manager: foo
|
||||
operation: Update
|
||||
`,
|
||||
`foo:
|
||||
apiVersion: v1
|
||||
`- apiVersion: v1
|
||||
fields:
|
||||
f:allowVolumeExpansion: {}
|
||||
f:apiVersion: {}
|
||||
|
@ -92,9 +100,10 @@ func TestRoundTripManagedFields(t *testing.T) {
|
|||
f:secretName: {}
|
||||
f:secretNamespace: {}
|
||||
f:provisioner: {}
|
||||
manager: foo
|
||||
operation: Apply
|
||||
`,
|
||||
`foo:
|
||||
apiVersion: v1
|
||||
`- apiVersion: v1
|
||||
fields:
|
||||
f:apiVersion: {}
|
||||
f:kind: {}
|
||||
|
@ -114,12 +123,14 @@ func TestRoundTripManagedFields(t *testing.T) {
|
|||
f:name: {}
|
||||
f:served: {}
|
||||
f:storage: {}
|
||||
manager: foo
|
||||
operation: Update
|
||||
`,
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test, func(t *testing.T) {
|
||||
var unmarshaled map[string]metav1.VersionedFields
|
||||
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)
|
||||
}
|
||||
|
@ -141,3 +152,49 @@ func TestRoundTripManagedFields(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeManager(t *testing.T) {
|
||||
tests := []struct {
|
||||
managedFieldsEntry string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
managedFieldsEntry: `
|
||||
apiVersion: v1
|
||||
fields:
|
||||
f:apiVersion: {}
|
||||
manager: foo
|
||||
operation: Update
|
||||
time: "2001-02-03T04:05:06Z"
|
||||
`,
|
||||
expected: "{\"manager\":\"foo\",\"operation\":\"Update\",\"apiVersion\":\"v1\",\"time\":\"2001-02-03T04:05:06Z\"}",
|
||||
},
|
||||
{
|
||||
managedFieldsEntry: `
|
||||
apiVersion: v1
|
||||
fields:
|
||||
f:apiVersion: {}
|
||||
manager: foo
|
||||
operation: Apply
|
||||
time: "2001-02-03T04:05:06Z"
|
||||
`,
|
||||
expected: "{\"manager\":\"foo\",\"operation\":\"Apply\"}",
|
||||
},
|
||||
}
|
||||
|
||||
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 := DecodeManager(&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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,21 +21,52 @@ import (
|
|||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kube-openapi/pkg/util/proto"
|
||||
"sigs.k8s.io/structured-merge-diff/typed"
|
||||
"sigs.k8s.io/structured-merge-diff/value"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// TypeConverter allows you to convert from runtime.Object to
|
||||
// typed.TypedValue and the other way around.
|
||||
type TypeConverter interface {
|
||||
NewTyped(schema.GroupVersionKind) (typed.TypedValue, error)
|
||||
ObjectToTyped(runtime.Object) (typed.TypedValue, error)
|
||||
YAMLToTyped([]byte) (typed.TypedValue, error)
|
||||
TypedToObject(typed.TypedValue) (runtime.Object, error)
|
||||
}
|
||||
|
||||
// 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).
|
||||
//
|
||||
// Note that this is not going to be sufficient for converting to/from
|
||||
// CRDs that have a schema defined (we don't support that schema yet).
|
||||
type DeducedTypeConverter struct{}
|
||||
|
||||
var _ TypeConverter = DeducedTypeConverter{}
|
||||
|
||||
// ObjectToTyped converts an object into a TypedValue with a "deduced type".
|
||||
func (DeducedTypeConverter) ObjectToTyped(obj runtime.Object) (typed.TypedValue, error) {
|
||||
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return typed.DeducedParseableType{}.FromUnstructured(u)
|
||||
}
|
||||
|
||||
// YAMLToTyped parses a yaml object into a TypedValue with a "deduced type".
|
||||
func (DeducedTypeConverter) YAMLToTyped(from []byte) (typed.TypedValue, error) {
|
||||
return typed.DeducedParseableType{}.FromYAML(typed.YAMLObject(from))
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
type typeConverter struct {
|
||||
parser *gvkParser
|
||||
}
|
||||
|
@ -53,28 +84,15 @@ func NewTypeConverter(models proto.Models) (TypeConverter, error) {
|
|||
return &typeConverter{parser: parser}, nil
|
||||
}
|
||||
|
||||
func (c *typeConverter) NewTyped(gvk schema.GroupVersionKind) (typed.TypedValue, error) {
|
||||
t := c.parser.Type(gvk)
|
||||
if t == nil {
|
||||
return typed.TypedValue{}, fmt.Errorf("no corresponding type for %v", gvk)
|
||||
}
|
||||
|
||||
u, err := t.New()
|
||||
if err != nil {
|
||||
return typed.TypedValue{}, fmt.Errorf("new typed: %v", err)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (c *typeConverter) ObjectToTyped(obj runtime.Object) (typed.TypedValue, error) {
|
||||
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
if err != nil {
|
||||
return typed.TypedValue{}, err
|
||||
return nil, err
|
||||
}
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
t := c.parser.Type(gvk)
|
||||
if t == nil {
|
||||
return typed.TypedValue{}, fmt.Errorf("no corresponding type for %v", gvk)
|
||||
return nil, fmt.Errorf("no corresponding type for %v", gvk)
|
||||
}
|
||||
return t.FromUnstructured(u)
|
||||
}
|
||||
|
@ -83,14 +101,18 @@ func (c *typeConverter) YAMLToTyped(from []byte) (typed.TypedValue, error) {
|
|||
unstructured := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
|
||||
if err := yaml.Unmarshal(from, &unstructured.Object); err != nil {
|
||||
return typed.TypedValue{}, fmt.Errorf("error decoding YAML: %v", err)
|
||||
return nil, fmt.Errorf("error decoding YAML: %v", err)
|
||||
}
|
||||
|
||||
return c.ObjectToTyped(unstructured)
|
||||
}
|
||||
|
||||
func (c *typeConverter) TypedToObject(value typed.TypedValue) (runtime.Object, error) {
|
||||
vu := value.AsValue().ToUnstructured(false)
|
||||
return valueToObject(value.AsValue())
|
||||
}
|
||||
|
||||
func valueToObject(value *value.Value) (runtime.Object, error) {
|
||||
vu := value.ToUnstructured(false)
|
||||
u, ok := vu.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert typed to unstructured: want map, got %T", vu)
|
||||
|
|
|
@ -201,6 +201,7 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
|
|||
name: name,
|
||||
patchType: patchType,
|
||||
patchBytes: patchBytes,
|
||||
userAgent: req.UserAgent(),
|
||||
|
||||
trace: trace,
|
||||
}
|
||||
|
@ -268,6 +269,7 @@ type patcher struct {
|
|||
name string
|
||||
patchType types.PatchType
|
||||
patchBytes []byte
|
||||
userAgent string
|
||||
|
||||
trace *utiltrace.Trace
|
||||
|
||||
|
@ -309,7 +311,7 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r
|
|||
}
|
||||
|
||||
if p.fieldManager != nil {
|
||||
if objToUpdate, err = p.fieldManager.Update(currentObject, objToUpdate, "jsonPatcher"); err != nil {
|
||||
if objToUpdate, err = p.fieldManager.Update(currentObject, objToUpdate, prefixFromUserAgent(p.userAgent)); err != nil {
|
||||
return nil, fmt.Errorf("failed to update object managed fields: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -371,7 +373,7 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru
|
|||
}
|
||||
|
||||
if p.fieldManager != nil {
|
||||
if newObj, err = p.fieldManager.Update(currentObject, newObj, "smPatcher"); err != nil {
|
||||
if newObj, err = p.fieldManager.Update(currentObject, newObj, prefixFromUserAgent(p.userAgent)); err != nil {
|
||||
return nil, fmt.Errorf("failed to update object managed fields: %v", err)
|
||||
}
|
||||
}
|
||||
|
@ -395,6 +397,9 @@ func (p *applyPatcher) applyPatchToCurrentObject(obj runtime.Object) (runtime.Ob
|
|||
if p.options.Force != nil {
|
||||
force = *p.options.Force
|
||||
}
|
||||
if p.fieldManager == nil {
|
||||
panic("FieldManager must be installed to run apply")
|
||||
}
|
||||
return p.fieldManager.Apply(obj, p.patch, force)
|
||||
}
|
||||
|
||||
|
|
|
@ -123,8 +123,8 @@ func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interfac
|
|||
userInfo, _ := request.UserFrom(ctx)
|
||||
transformers := []rest.TransformFunc{}
|
||||
if scope.FieldManager != nil {
|
||||
transformers = append(transformers, func(_ context.Context, liveObj, newObj runtime.Object) (runtime.Object, error) {
|
||||
if obj, err = scope.FieldManager.Update(liveObj, newObj, "update"); err != nil {
|
||||
transformers = append(transformers, func(_ context.Context, newObj, liveObj runtime.Object) (runtime.Object, error) {
|
||||
if obj, err = scope.FieldManager.Update(liveObj, newObj, prefixFromUserAgent(req.UserAgent())); err != nil {
|
||||
return nil, fmt.Errorf("failed to update object managed fields: %v", err)
|
||||
}
|
||||
return obj, nil
|
||||
|
|
|
@ -191,7 +191,7 @@ func TestPatchApply(t *testing.T) {
|
|||
if simpleStorage.updated.Other != "bar" {
|
||||
t.Errorf(`Merge should have kept initial "bar" value for Other: %v`, simpleStorage.updated.Other)
|
||||
}
|
||||
if _, ok := simpleStorage.updated.ObjectMeta.ManagedFields["default"]; !ok {
|
||||
if len(simpleStorage.updated.ObjectMeta.ManagedFields) == 0 {
|
||||
t.Errorf(`Expected managedFields field to be set, but is empty`)
|
||||
}
|
||||
}
|
||||
|
@ -230,11 +230,11 @@ func TestApplyAddsGVK(t *testing.T) {
|
|||
}
|
||||
// TODO: Need to fix this
|
||||
expected := `{"apiVersion":"test.group/version","kind":"Simple","labels":{"test":"yes"},"metadata":{"name":"id"}}`
|
||||
if simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion != expected {
|
||||
if simpleStorage.updated.ObjectMeta.ManagedFields[0].APIVersion != expected {
|
||||
t.Errorf(
|
||||
`Expected managedFields field to be %q, got %q`,
|
||||
expected,
|
||||
simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion,
|
||||
simpleStorage.updated.ObjectMeta.ManagedFields[0].APIVersion,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -265,11 +265,11 @@ func TestApplyCreatesWithManagedFields(t *testing.T) {
|
|||
}
|
||||
// TODO: Need to fix this
|
||||
expected := `{"apiVersion":"test.group/version","kind":"Simple","labels":{"test":"yes"},"metadata":{"name":"id"}}`
|
||||
if simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion != expected {
|
||||
if simpleStorage.updated.ObjectMeta.ManagedFields[0].APIVersion != expected {
|
||||
t.Errorf(
|
||||
`Expected managedFields field to be %q, got %q`,
|
||||
expected,
|
||||
simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion,
|
||||
simpleStorage.updated.ObjectMeta.ManagedFields[0].APIVersion,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,9 @@ go_library(
|
|||
"//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/features:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ go_test(
|
|||
deps = [
|
||||
"//pkg/master:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/features:go_default_library",
|
||||
|
|
|
@ -17,10 +17,13 @@ limitations under the License.
|
|||
package apiserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
|
@ -204,3 +207,98 @@ func TestApplyUpdateApplyConflictForced(t *testing.T) {
|
|||
t.Fatalf("Failed to apply object with force: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyManagedFields makes sure that managedFields api does not change
|
||||
func TestApplyManagedFields(t *testing.T) {
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
|
||||
|
||||
_, client, closeFn := setup(t)
|
||||
defer closeFn()
|
||||
|
||||
_, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType).
|
||||
Namespace("default").
|
||||
Resource("configmaps").
|
||||
Name("test-cm").
|
||||
Body([]byte(`{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"name": "test-cm"
|
||||
},
|
||||
"data": {
|
||||
"key": "value"
|
||||
}
|
||||
}`)).
|
||||
Do().
|
||||
Get()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create object using Apply patch: %v", err)
|
||||
}
|
||||
|
||||
_, err = client.CoreV1().RESTClient().Patch(types.MergePatchType).
|
||||
Namespace("default").
|
||||
Resource("configmaps").
|
||||
Name("test-cm").
|
||||
Body([]byte(`{"data":{"key": "new value"}}`)).Do().Get()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to patch object: %v", err)
|
||||
}
|
||||
|
||||
object, err := client.CoreV1().RESTClient().Get().Namespace("default").Resource("configmaps").Name("test-cm").Do().Get()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve object: %v", err)
|
||||
}
|
||||
|
||||
accessor, err := meta.Accessor(object)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get meta accessor: %v", err)
|
||||
}
|
||||
|
||||
actual, err := json.MarshalIndent(object, "\t", "\t")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal object: %v", err)
|
||||
}
|
||||
|
||||
expected := []byte(`{
|
||||
"metadata": {
|
||||
"name": "test-cm",
|
||||
"namespace": "default",
|
||||
"selfLink": "` + accessor.GetSelfLink() + `",
|
||||
"uid": "` + string(accessor.GetUID()) + `",
|
||||
"resourceVersion": "` + accessor.GetResourceVersion() + `",
|
||||
"creationTimestamp": "` + accessor.GetCreationTimestamp().UTC().Format(time.RFC3339) + `",
|
||||
"managedFields": [
|
||||
{
|
||||
"manager": "apply",
|
||||
"operation": "Apply",
|
||||
"apiVersion": "v1",
|
||||
"fields": {
|
||||
"f:apiVersion": {},
|
||||
"f:kind": {},
|
||||
"f:metadata": {
|
||||
"f:name": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"manager": "` + accessor.GetManagedFields()[1].Manager + `",
|
||||
"operation": "Update",
|
||||
"apiVersion": "v1",
|
||||
"time": "` + accessor.GetManagedFields()[1].Time.UTC().Format(time.RFC3339) + `",
|
||||
"fields": {
|
||||
"f:data": {
|
||||
"f:key": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"key": "new value"
|
||||
}
|
||||
}`)
|
||||
|
||||
if string(expected) != string(actual) {
|
||||
t.Fatalf("Expected:\n%v\nGot:\n%v", string(expected), string(actual))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue