diff --git a/pkg/api/pod/util_test.go b/pkg/api/pod/util_test.go index 29ae2cfc4c1..c2cdd1b9d89 100644 --- a/pkg/api/pod/util_test.go +++ b/pkg/api/pod/util_test.go @@ -176,6 +176,10 @@ func collectResourcePaths(t *testing.T, resourcename string, path *field.Path, n case reflect.Ptr: resourcePaths.Insert(collectResourcePaths(t, resourcename, path, name, tp.Elem()).List()...) case reflect.Struct: + // Specifically skip ObjectMeta because it has recursive types + if name == "ObjectMeta" { + break + } for i := 0; i < tp.NumField(); i++ { field := tp.Field(i) resourcePaths.Insert(collectResourcePaths(t, resourcename, path.Child(field.Name), field.Name, field.Type).List()...) diff --git a/pkg/api/testing/defaulting_test.go b/pkg/api/testing/defaulting_test.go index 50453577272..dac954097dc 100644 --- a/pkg/api/testing/defaulting_test.go +++ b/pkg/api/testing/defaulting_test.go @@ -49,7 +49,6 @@ func TestDefaulting(t *testing.T) { {Group: "", Version: "v1", Kind: "ConfigMap"}: {}, {Group: "", Version: "v1", Kind: "ConfigMapList"}: {}, {Group: "", Version: "v1", Kind: "Endpoints"}: {}, - {Group: "", Version: "v1", Kind: "EndpointsList"}: {}, {Group: "", Version: "v1", Kind: "Namespace"}: {}, {Group: "", Version: "v1", Kind: "NamespaceList"}: {}, {Group: "", Version: "v1", Kind: "Node"}: {}, diff --git a/pkg/api/testing/serialization_test.go b/pkg/api/testing/serialization_test.go index 600728439f0..00b7aa309e6 100644 --- a/pkg/api/testing/serialization_test.go +++ b/pkg/api/testing/serialization_test.go @@ -26,7 +26,6 @@ import ( "testing" jsoniter "github.com/json-iterator/go" - appsv1 "k8s.io/api/apps/v1" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" @@ -162,9 +161,10 @@ var nonRoundTrippableTypes = sets.NewString( "DeleteOptions", "CreateOptions", "UpdateOptions", + "PatchOptions", ) -var commonKinds = []string{"Status", "ListOptions", "DeleteOptions", "ExportOptions", "GetOptions", "CreateOptions", "UpdateOptions"} +var commonKinds = []string{"Status", "ListOptions", "DeleteOptions", "ExportOptions", "GetOptions", "CreateOptions", "UpdateOptions", "PatchOptions"} // TestCommonKindsRegistered verifies that all group/versions registered with // the testapi package have the common kinds. diff --git a/pkg/api/v1/persistentvolume/util_test.go b/pkg/api/v1/persistentvolume/util_test.go index 62e60cc6e9e..bf653821464 100644 --- a/pkg/api/v1/persistentvolume/util_test.go +++ b/pkg/api/v1/persistentvolume/util_test.go @@ -18,9 +18,8 @@ package persistentvolume import ( "reflect" - "testing" - "strings" + "testing" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" @@ -248,6 +247,9 @@ func collectSecretPaths(t *testing.T, path *field.Path, name string, tp reflect. case reflect.Ptr: secretPaths.Insert(collectSecretPaths(t, path, name, tp.Elem()).List()...) case reflect.Struct: + if name == "ObjectMeta" { + break + } for i := 0; i < tp.NumField(); i++ { field := tp.Field(i) secretPaths.Insert(collectSecretPaths(t, path.Child(field.Name), field.Name, field.Type).List()...) diff --git a/pkg/api/v1/pod/util_test.go b/pkg/api/v1/pod/util_test.go index 1f0c01daf11..b9f3544dc66 100644 --- a/pkg/api/v1/pod/util_test.go +++ b/pkg/api/v1/pod/util_test.go @@ -340,6 +340,10 @@ func collectResourcePaths(t *testing.T, resourcename string, path *field.Path, n case reflect.Ptr: resourcePaths.Insert(collectResourcePaths(t, resourcename, path, name, tp.Elem()).List()...) case reflect.Struct: + // Specifically skip ObjectMeta because it has recursive types + if name == "ObjectMeta" { + break + } for i := 0; i < tp.NumField(); i++ { field := tp.Field(i) resourcePaths.Insert(collectResourcePaths(t, resourcename, path.Child(field.Name), field.Name, field.Type).List()...) diff --git a/pkg/controller/garbagecollector/operations.go b/pkg/controller/garbagecollector/operations.go index fefec3c5930..93dd771984c 100644 --- a/pkg/controller/garbagecollector/operations.go +++ b/pkg/controller/garbagecollector/operations.go @@ -19,8 +19,6 @@ package garbagecollector import ( "fmt" - "k8s.io/klog" - "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" + "k8s.io/klog" ) // cluster scoped resources don't have namespaces. Default to the item's namespace, but clear it for cluster scoped resources @@ -81,7 +80,7 @@ func (gc *GarbageCollector) patchObject(item objectReference, patch []byte, pt t if err != nil { return nil, err } - return gc.dynamicClient.Resource(resource).Namespace(resourceDefaultNamespace(namespaced, item.Namespace)).Patch(item.Name, pt, patch, metav1.UpdateOptions{}) + return gc.dynamicClient.Resource(resource).Namespace(resourceDefaultNamespace(namespaced, item.Namespace)).Patch(item.Name, pt, patch, metav1.PatchOptions{}) } // TODO: Using Patch when strategicmerge supports deleting an entry from a diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index a3b523a8d77..b0dda32280c 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -484,6 +484,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS genericfeatures.APIResponseCompression: {Default: false, PreRelease: utilfeature.Alpha}, genericfeatures.APIListChunking: {Default: true, PreRelease: utilfeature.Beta}, genericfeatures.DryRun: {Default: true, PreRelease: utilfeature.Beta}, + genericfeatures.ServerSideApply: {Default: false, PreRelease: utilfeature.Alpha}, // inherited features from apiextensions-apiserver, relisted here to get a conflict if it is changed // unintentionally on either side: diff --git a/pkg/kubectl/cmd/apply/apply.go b/pkg/kubectl/cmd/apply/apply.go index d6fce3354b7..11ed554822a 100644 --- a/pkg/kubectl/cmd/apply/apply.go +++ b/pkg/kubectl/cmd/apply/apply.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "net/http" "strings" "time" @@ -65,16 +66,18 @@ type ApplyOptions struct { DeleteFlags *delete.DeleteFlags DeleteOptions *delete.DeleteOptions - Selector string - DryRun bool - ServerDryRun bool - Prune bool - PruneResources []pruneResource - cmdBaseName string - All bool - Overwrite bool - OpenAPIPatch bool - PruneWhitelist []string + ServerSideApply bool + ForceConflicts bool + Selector string + DryRun bool + ServerDryRun bool + Prune bool + PruneResources []pruneResource + cmdBaseName string + All bool + Overwrite bool + OpenAPIPatch bool + PruneWhitelist []string Validator validation.Schema Builder *resource.Builder @@ -178,6 +181,7 @@ func NewCmdApply(baseName string, f cmdutil.Factory, ioStreams genericclioptions cmd.Flags().BoolVar(&o.ServerDryRun, "server-dry-run", o.ServerDryRun, "If true, request will be sent to server with dry-run flag, which means the modifications won't be persisted. This is an alpha feature and flag.") cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it. Warning: --dry-run cannot accurately output the result of merging the local manifest and the server-side data. Use --server-dry-run to get the merged result instead.") cmdutil.AddIncludeUninitializedFlag(cmd) + cmdutil.AddServerSideApplyFlags(cmd) // apply subcommands cmd.AddCommand(NewCmdApplyViewLastApplied(f, ioStreams)) @@ -188,8 +192,18 @@ func NewCmdApply(baseName string, f cmdutil.Factory, ioStreams genericclioptions } func (o *ApplyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { + o.ServerSideApply = cmdutil.GetServerSideApplyFlag(cmd) + o.ForceConflicts = cmdutil.GetForceConflictsFlag(cmd) o.DryRun = cmdutil.GetDryRunFlag(cmd) + if o.ForceConflicts && !o.ServerSideApply { + return fmt.Errorf("--force-conflicts only works with --server-side") + } + + if o.DryRun && o.ServerSideApply { + return fmt.Errorf("--dry-run doesn't work with --server-side") + } + if o.DryRun && o.ServerDryRun { return fmt.Errorf("--dry-run and --server-dry-run can't be used together") } @@ -293,6 +307,16 @@ func parsePruneResources(mapper meta.RESTMapper, gvks []string) ([]pruneResource return pruneResources, nil } +func isIncompatibleServerError(err error) bool { + // 415: Unsupported media type means we're talking to a server which doesn't + // support server-side apply. + if _, ok := err.(*errors.StatusError); !ok { + // Non-StatusError means the error isn't because the server is incompatible. + return false + } + return err.(*errors.StatusError).Status().Code == http.StatusUnsupportedMediaType +} + func (o *ApplyOptions) Run() error { var openapiSchema openapi.Resources if o.OpenAPIPatch { @@ -356,6 +380,50 @@ func (o *ApplyOptions) Run() error { klog.V(4).Infof("error recording current command: %v", err) } + if o.ServerSideApply { + // Send the full object to be applied on the server side. + data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object) + if err != nil { + return cmdutil.AddSourceToErr("serverside-apply", info.Source, err) + } + options := metav1.PatchOptions{ + Force: &o.ForceConflicts, + } + if o.ServerDryRun { + options.DryRun = []string{metav1.DryRunAll} + } + obj, err := resource.NewHelper(info.Client, info.Mapping).Patch( + info.Namespace, + info.Name, + types.ApplyPatchType, + data, + &options, + ) + if err == nil { + info.Refresh(obj, true) + metadata, err := meta.Accessor(info.Object) + if err != nil { + return err + } + visitedUids.Insert(string(metadata.GetUID())) + count++ + if len(output) > 0 && !shortOutput { + objs = append(objs, info.Object) + return nil + } + printer, err := o.ToPrinter("serverside-applied") + if err != nil { + return err + } + return printer.PrintObj(info.Object, o.Out) + } else if !isIncompatibleServerError(err) { + return err + } + // If we're talking to a server which does not implement server-side apply, + // continue with the client side apply after this block. + klog.Warningf("serverside-apply incompatible server: %v", err) + } + // Get the modified configuration of the object. Embed the result // as an annotation in the modified configuration, so that it will appear // in the patch sent to the server. @@ -840,7 +908,7 @@ func (p *Patcher) patchSimple(obj runtime.Object, modified []byte, source, names } } - options := metav1.UpdateOptions{} + options := metav1.PatchOptions{} if p.ServerDryRun { options.DryRun = []string{metav1.DryRunAll} } diff --git a/pkg/kubectl/cmd/diff/BUILD b/pkg/kubectl/cmd/diff/BUILD index 93e0fb64ebd..d5c0923f040 100644 --- a/pkg/kubectl/cmd/diff/BUILD +++ b/pkg/kubectl/cmd/diff/BUILD @@ -18,8 +18,11 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library", "//staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource:go_default_library", + "//staging/src/k8s.io/client-go/discovery:go_default_library", + "//staging/src/k8s.io/client-go/dynamic:go_default_library", "//vendor/github.com/jonboulle/clockwork:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", "//vendor/k8s.io/klog:go_default_library", diff --git a/pkg/kubectl/cmd/diff/diff.go b/pkg/kubectl/cmd/diff/diff.go index 1956cf1bb0f..e64e8a0990d 100644 --- a/pkg/kubectl/cmd/diff/diff.go +++ b/pkg/kubectl/cmd/diff/diff.go @@ -30,8 +30,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions/resource" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" "k8s.io/klog" "k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/kubectl/cmd/apply" @@ -67,21 +70,38 @@ const maxRetries = 4 type DiffOptions struct { FilenameOptions resource.FilenameOptions + + ServerSideApply bool + ForceConflicts bool + + OpenAPISchema openapi.Resources + DiscoveryClient discovery.DiscoveryInterface + DynamicClient dynamic.Interface + DryRunVerifier *apply.DryRunVerifier + CmdNamespace string + EnforceNamespace bool + Builder *resource.Builder + Diff *DiffProgram } -func checkDiffArgs(cmd *cobra.Command, args []string) error { +func validateArgs(cmd *cobra.Command, args []string) error { if len(args) != 0 { return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args) } return nil } -func NewCmdDiff(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - var options DiffOptions - diff := DiffProgram{ - Exec: exec.New(), - IOStreams: streams, +func NewDiffOptions(ioStreams genericclioptions.IOStreams) *DiffOptions { + return &DiffOptions{ + Diff: &DiffProgram{ + Exec: exec.New(), + IOStreams: ioStreams, + }, } +} + +func NewCmdDiff(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + options := NewDiffOptions(streams) cmd := &cobra.Command{ Use: "diff -f FILENAME", DisableFlagsInUseLine: true, @@ -89,13 +109,15 @@ func NewCmdDiff(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C Long: diffLong, Example: diffExample, Run: func(cmd *cobra.Command, args []string) { - cmdutil.CheckErr(checkDiffArgs(cmd, args)) - cmdutil.CheckErr(RunDiff(f, &diff, &options)) + cmdutil.CheckErr(options.Complete(f, cmd)) + cmdutil.CheckErr(validateArgs(cmd, args)) + cmdutil.CheckErr(options.Run()) }, } usage := "contains the configuration to diff" cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) + cmdutil.AddServerSideApplyFlags(cmd) cmd.MarkFlagRequired("filename") return cmd @@ -229,11 +251,13 @@ type Object interface { // InfoObject is an implementation of the Object interface. It gets all // the information from the Info object. type InfoObject struct { - LocalObj runtime.Object - Info *resource.Info - Encoder runtime.Encoder - OpenAPI openapi.Resources - Force bool + LocalObj runtime.Object + Info *resource.Info + Encoder runtime.Encoder + OpenAPI openapi.Resources + Force bool + ServerSideApply bool + ForceConflicts bool } var _ Object = &InfoObject{} @@ -246,6 +270,24 @@ func (obj InfoObject) Live() runtime.Object { // Returns the "merged" object, as it would look like if applied or // created. func (obj InfoObject) Merged() (runtime.Object, error) { + if obj.ServerSideApply { + data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj.LocalObj) + if err != nil { + return nil, err + } + options := metav1.PatchOptions{ + Force: &obj.ForceConflicts, + DryRun: []string{metav1.DryRunAll}, + } + return resource.NewHelper(obj.Info.Client, obj.Info.Mapping).Patch( + obj.Info.Namespace, + obj.Info.Name, + types.ApplyPatchType, + data, + &options, + ) + } + // Build the patcher, and then apply the patch with dry-run, unless the object doesn't exist, in which case we need to create it. if obj.Live() == nil { // Dry-run create if the object doesn't exist. @@ -350,30 +392,50 @@ func isConflict(err error) bool { return err != nil && errors.IsConflict(err) } +func (o *DiffOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { + var err error + + o.ServerSideApply = cmdutil.GetServerSideApplyFlag(cmd) + o.ForceConflicts = cmdutil.GetForceConflictsFlag(cmd) + if o.ForceConflicts && !o.ServerSideApply { + return fmt.Errorf("--force-conflicts only works with --server-side") + } + + if !o.ServerSideApply { + o.OpenAPISchema, err = f.OpenAPISchema() + if err != nil { + return err + } + } + + o.DiscoveryClient, err = f.ToDiscoveryClient() + if err != nil { + return err + } + + o.DynamicClient, err = f.DynamicClient() + if err != nil { + return err + } + + o.DryRunVerifier = &apply.DryRunVerifier{ + Finder: cmdutil.NewCRDFinder(cmdutil.CRDFromDynamic(o.DynamicClient)), + OpenAPIGetter: o.DiscoveryClient, + } + + o.CmdNamespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + + o.Builder = f.NewBuilder() + return nil +} + // RunDiff uses the factory to parse file arguments, find the version to // diff, and find each Info object for each files, and runs against the // differ. -func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { - schema, err := f.OpenAPISchema() - if err != nil { - return err - } - - discovery, err := f.ToDiscoveryClient() - if err != nil { - return err - } - - dynamic, err := f.DynamicClient() - if err != nil { - return err - } - - dryRunVerifier := &apply.DryRunVerifier{ - Finder: cmdutil.NewCRDFinder(cmdutil.CRDFromDynamic(dynamic)), - OpenAPIGetter: discovery, - } - +func (o *DiffOptions) Run() error { differ, err := NewDiffer("LIVE", "MERGED") if err != nil { return err @@ -382,15 +444,10 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { printer := Printer{} - cmdNamespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace() - if err != nil { - return err - } - - r := f.NewBuilder(). + r := o.Builder. Unstructured(). - NamespaceParam(cmdNamespace).DefaultNamespace(). - FilenameParam(enforceNamespace, &options.FilenameOptions). + NamespaceParam(o.CmdNamespace).DefaultNamespace(). + FilenameParam(o.EnforceNamespace, &o.FilenameOptions). Flatten(). Do() if err := r.Err(); err != nil { @@ -402,7 +459,7 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { return err } - if err := dryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil { + if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil { return err } @@ -424,11 +481,13 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { ) } obj := InfoObject{ - LocalObj: local, - Info: info, - Encoder: scheme.DefaultJSONEncoder(), - OpenAPI: schema, - Force: force, + LocalObj: local, + Info: info, + Encoder: scheme.DefaultJSONEncoder(), + OpenAPI: o.OpenAPISchema, + Force: force, + ServerSideApply: o.ServerSideApply, + ForceConflicts: o.ForceConflicts, } err = differ.Diff(obj, printer) @@ -442,5 +501,5 @@ func RunDiff(f cmdutil.Factory, diff *DiffProgram, options *DiffOptions) error { return err } - return differ.Run(diff) + return differ.Run(o.Diff) } diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index 750738cc42f..303b5fea97b 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -29,8 +29,6 @@ import ( "github.com/evanphx/json-patch" "github.com/spf13/cobra" "github.com/spf13/pflag" - "k8s.io/klog" - kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -44,6 +42,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/scale" "k8s.io/client-go/tools/clientcmd" + "k8s.io/klog" utilexec "k8s.io/utils/exec" ) @@ -400,6 +399,11 @@ func AddDryRunFlag(cmd *cobra.Command) { cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it.") } +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.") +} + func AddIncludeUninitializedFlag(cmd *cobra.Command) { cmd.Flags().Bool("include-uninitialized", false, `If true, the kubectl command applies to uninitialized objects. If explicitly set to false, this flag overrides other flags that make the kubectl commands apply to uninitialized objects, e.g., "--all". Objects with empty metadata.initializers are regarded as initialized.`) cmd.Flags().MarkDeprecated("include-uninitialized", "The Initializers feature has been removed. This flag is now a no-op, and will be removed in v1.15") @@ -473,6 +477,14 @@ func DumpReaderToFile(reader io.Reader, filename string) error { return nil } +func GetServerSideApplyFlag(cmd *cobra.Command) bool { + return GetFlagBool(cmd, "server-side") +} + +func GetForceConflictsFlag(cmd *cobra.Command) bool { + return GetFlagBool(cmd, "force-conflicts") +} + func GetDryRunFlag(cmd *cobra.Command) bool { return GetFlagBool(cmd, "dry-run") } diff --git a/pkg/master/master.go b/pkg/master/master.go index be544b44f03..df96b9541ca 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -399,7 +399,7 @@ type RESTStorageProvider interface { // InstallAPIs will install the APIs for the restStorageProviders if they are enabled. func (m *Master) InstallAPIs(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter, restStorageProviders ...RESTStorageProvider) { - apiGroupsInfo := []genericapiserver.APIGroupInfo{} + apiGroupsInfo := []*genericapiserver.APIGroupInfo{} for _, restStorageBuilder := range restStorageProviders { groupName := restStorageBuilder.GroupName() @@ -422,13 +422,11 @@ func (m *Master) InstallAPIs(apiResourceConfigSource serverstorage.APIResourceCo m.GenericAPIServer.AddPostStartHookOrDie(name, hook) } - apiGroupsInfo = append(apiGroupsInfo, apiGroupInfo) + apiGroupsInfo = append(apiGroupsInfo, &apiGroupInfo) } - for i := range apiGroupsInfo { - if err := m.GenericAPIServer.InstallAPIGroup(&apiGroupsInfo[i]); err != nil { - klog.Fatalf("Error in registering group versions: %v", err) - } + if err := m.GenericAPIServer.InstallAPIGroups(apiGroupsInfo...); err != nil { + klog.Fatalf("Error in registering group versions: %v", err) } } diff --git a/pkg/master/master_openapi_test.go b/pkg/master/master_openapi_test.go index ca7cd48aa88..beed6be5f01 100644 --- a/pkg/master/master_openapi_test.go +++ b/pkg/master/master_openapi_test.go @@ -27,15 +27,14 @@ import ( "net/http/httptest" "testing" - openapinamer "k8s.io/apiserver/pkg/endpoints/openapi" - genericapiserver "k8s.io/apiserver/pkg/server" - "k8s.io/kubernetes/pkg/api/legacyscheme" - openapigen "k8s.io/kubernetes/pkg/generated/openapi" - "github.com/go-openapi/loads" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/validate" + openapinamer "k8s.io/apiserver/pkg/endpoints/openapi" + genericapiserver "k8s.io/apiserver/pkg/server" + "k8s.io/kubernetes/pkg/api/legacyscheme" + openapigen "k8s.io/kubernetes/pkg/generated/openapi" ) // TestValidOpenAPISpec verifies that the open api is added diff --git a/staging/src/k8s.io/api/Godeps/Godeps.json b/staging/src/k8s.io/api/Godeps/Godeps.json index e5c05264b10..ba7a314593f 100644 --- a/staging/src/k8s.io/api/Godeps/Godeps.json +++ b/staging/src/k8s.io/api/Godeps/Godeps.json @@ -24,7 +24,7 @@ }, { "ImportPath": "github.com/google/gofuzz", - "Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c" + "Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1" }, { "ImportPath": "github.com/json-iterator/go", diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/basic_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/basic_test.go index b9f34b3cb4d..f9ed60c5f96 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/basic_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/basic_test.go @@ -677,7 +677,7 @@ func TestPatch(t *testing.T) { t.Logf("Patching .num.num2 to 999") patch := []byte(`{"num": {"num2":999}}`) - patchedNoxuInstance, err := noxuNamespacedResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}) + patchedNoxuInstance, err := noxuNamespacedResourceClient.Patch("foo", types.MergePatchType, patch, metav1.PatchOptions{}) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -692,7 +692,7 @@ func TestPatch(t *testing.T) { // a patch with no change t.Logf("Patching .num.num2 again to 999") - patchedNoxuInstance, err = noxuNamespacedResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}) + patchedNoxuInstance, err = noxuNamespacedResourceClient.Patch("foo", types.MergePatchType, patch, metav1.PatchOptions{}) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -702,7 +702,7 @@ func TestPatch(t *testing.T) { // an empty patch t.Logf("Applying empty patch") - patchedNoxuInstance, err = noxuNamespacedResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.UpdateOptions{}) + patchedNoxuInstance, err = noxuNamespacedResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go index c3a7c1cb214..d6c4552e7c3 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go @@ -25,6 +25,10 @@ import ( "testing" autoscaling "k8s.io/api/autoscaling/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" + "k8s.io/apiextensions-apiserver/test/integration/fixtures" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -33,11 +37,6 @@ import ( utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing" "k8s.io/client-go/dynamic" - - apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" - "k8s.io/apiextensions-apiserver/test/integration/fixtures" ) var labelSelectorPath = ".status.labelSelector" @@ -774,7 +773,7 @@ func TestSubresourcePatch(t *testing.T) { t.Logf("Patching .status.num to 999") patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`) - patchedNoxuInstance, err := noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "status") + patchedNoxuInstance, err := noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.PatchOptions{}, "status") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -802,7 +801,7 @@ func TestSubresourcePatch(t *testing.T) { // no-op patch t.Logf("Patching .status.num again to 999") - patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "status") + patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.PatchOptions{}, "status") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -813,7 +812,7 @@ func TestSubresourcePatch(t *testing.T) { // empty patch t.Logf("Applying empty patch") - patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.UpdateOptions{}, "status") + patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}, "status") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -825,7 +824,7 @@ func TestSubresourcePatch(t *testing.T) { t.Logf("Patching .spec.replicas to 7") patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`) - patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "scale") + patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.PatchOptions{}, "scale") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -865,7 +864,7 @@ func TestSubresourcePatch(t *testing.T) { // no-op patch t.Logf("Patching .spec.replicas again to 7") - patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "scale") + patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.PatchOptions{}, "scale") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -876,7 +875,7 @@ func TestSubresourcePatch(t *testing.T) { // empty patch t.Logf("Applying empty patch") - patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.UpdateOptions{}, "scale") + patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}, "scale") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -886,12 +885,12 @@ func TestSubresourcePatch(t *testing.T) { expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") // make sure strategic merge patch is not supported for both status and scale - _, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.UpdateOptions{}, "status") + _, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.PatchOptions{}, "status") if err == nil { t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources") } - _, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.UpdateOptions{}, "scale") + _, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.PatchOptions{}, "scale") if err == nil { t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources") } diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/versioning_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/versioning_test.go index c27945fe7af..4fdccff9dad 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/versioning_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/versioning_test.go @@ -24,13 +24,12 @@ import ( "time" "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" - apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apiextensions-apiserver/test/integration/fixtures" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" ) func TestInternalVersionIsHandlerVersion(t *testing.T) { @@ -88,7 +87,7 @@ func TestInternalVersionIsHandlerVersion(t *testing.T) { patch := []byte(fmt.Sprintf(`{"i": %d}`, i)) i++ - _, err := noxuNamespacedResourceClientV1beta1.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}) + _, err := noxuNamespacedResourceClientV1beta1.Patch("foo", types.MergePatchType, patch, metav1.PatchOptions{}) if err != nil { // work around "grpc: the client connection is closing" error // TODO: fix the grpc error @@ -111,7 +110,7 @@ func TestInternalVersionIsHandlerVersion(t *testing.T) { patch := []byte(fmt.Sprintf(`{"i": %d}`, i)) i++ - _, err := noxuNamespacedResourceClientV1beta2.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}) + _, err := noxuNamespacedResourceClientV1beta2.Patch("foo", types.MergePatchType, patch, metav1.PatchOptions{}) assert.NotNil(t, err) // work around "grpc: the client connection is closing" error diff --git a/staging/src/k8s.io/apimachinery/pkg/api/apitesting/naming/naming.go b/staging/src/k8s.io/apimachinery/pkg/api/apitesting/naming/naming.go index f65e86b6e1e..b000063be56 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/apitesting/naming/naming.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/apitesting/naming/naming.go @@ -69,6 +69,10 @@ func ensureNoTags(gvk schema.GroupVersionKind, tp reflect.Type, parents []reflec return errs } + // Don't look at the same type multiple times + if containsType(parents, tp) { + return nil + } parents = append(parents, tp) switch tp.Kind() { @@ -106,6 +110,10 @@ func ensureTags(gvk schema.GroupVersionKind, tp reflect.Type, parents []reflect. return errs } + // Don't look at the same type multiple times + if containsType(parents, tp) { + return nil + } parents = append(parents, tp) switch tp.Kind() { @@ -144,3 +152,13 @@ func fmtParentString(parents []reflect.Type) string { } return str } + +// containsType returns true if s contains t, false otherwise +func containsType(s []reflect.Type, t reflect.Type) bool { + for _, u := range s { + if t == u { + return true + } + } + return false +} diff --git a/staging/src/k8s.io/apimachinery/pkg/api/errors/BUILD b/staging/src/k8s.io/apimachinery/pkg/api/errors/BUILD index 865a64fc583..027c99dd727 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/errors/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/api/errors/BUILD @@ -31,6 +31,7 @@ go_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/apimachinery/pkg/util/validation/field:go_default_library", + "//vendor/sigs.k8s.io/structured-merge-diff/merge:go_default_library", ], ) diff --git a/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go b/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go index e736a986140..81e5d48bf70 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/structured-merge-diff/merge" ) const ( @@ -184,6 +185,29 @@ func NewConflict(qualifiedResource schema.GroupResource, name string, err error) }} } +// NewApplyConflict returns an error including details on the requests apply conflicts +func NewApplyConflict(conflicts merge.Conflicts) *StatusError { + causes := make([]metav1.StatusCause, 0, len(conflicts)) + for _, conflict := range conflicts { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseType("conflict"), + Message: conflict.Error(), + Field: conflict.Path.String(), + }) + } + + return &StatusError{ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusConflict, + Reason: metav1.StatusReasonConflict, + Details: &metav1.StatusDetails{ + // TODO: Get obj details here? + Causes: causes, + }, + Message: fmt.Sprintf("Apply failed with %d conflicts: %s", len(conflicts), conflicts.Error()), + }} +} + // NewGone returns an error indicating the item no longer available at the server and no forwarding address is known. func NewGone(message string) *StatusError { return &StatusError{metav1.Status{ diff --git a/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go b/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go index 6fe7458f6c4..b50337e13f4 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go @@ -20,14 +20,13 @@ import ( "fmt" "reflect" - "k8s.io/klog" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/klog" ) // errNotList is returned when an object implements the Object style interfaces but not the List style @@ -138,6 +137,7 @@ func AsPartialObjectMetadata(m metav1.Object) *metav1beta1.PartialObjectMetadata Finalizers: m.GetFinalizers(), ClusterName: m.GetClusterName(), Initializers: m.GetInitializers(), + ManagedFields: m.GetManagedFields(), }, } } diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/helpers.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/helpers.go index 604129ea101..b41d549a25b 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/helpers.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/helpers.go @@ -17,6 +17,7 @@ limitations under the License. package v1 import ( + "encoding/json" "fmt" "k8s.io/apimachinery/pkg/fields" @@ -243,4 +244,18 @@ func ResetObjectMetaForStatus(meta, existingMeta Object) { meta.SetAnnotations(existingMeta.GetAnnotations()) meta.SetFinalizers(existingMeta.GetFinalizers()) meta.SetOwnerReferences(existingMeta.GetOwnerReferences()) + meta.SetManagedFields(existingMeta.GetManagedFields()) } + +// MarshalJSON implements json.Marshaler +func (f Fields) MarshalJSON() ([]byte, error) { + return json.Marshal(&f.Map) +} + +// UnmarshalJSON implements json.Unmarshaler +func (f *Fields) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &f.Map) +} + +var _ json.Marshaler = Fields{} +var _ json.Unmarshaler = &Fields{} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/helpers_test.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/helpers_test.go index 656e53af22c..649738c9815 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/helpers_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/helpers_test.go @@ -23,7 +23,6 @@ import ( "testing" "github.com/google/gofuzz" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/diff" @@ -169,7 +168,7 @@ func TestResetObjectMetaForStatus(t *testing.T) { existingMeta := &ObjectMeta{} // fuzz the existingMeta to set every field, no nils - f := fuzz.New().NilChance(0).NumElements(1, 1) + f := fuzz.New().NilChance(0).NumElements(1, 1).MaxDepth(10) f.Fuzz(existingMeta) ResetObjectMetaForStatus(meta, existingMeta) diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go index ee1447541fc..ea12b929cd8 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/meta.go @@ -63,6 +63,8 @@ type Object interface { SetOwnerReferences([]OwnerReference) GetClusterName() string SetClusterName(clusterName string) + GetManagedFields() map[string]VersionedFields + SetManagedFields(lastApplied map[string]VersionedFields) } // ListMetaAccessor retrieves the list interface from an object @@ -168,3 +170,14 @@ 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 { + 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 + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/options_test.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/options_test.go new file mode 100644 index 00000000000..3367a57f07a --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/options_test.go @@ -0,0 +1,62 @@ +/* +Copyright 2017 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 v1 + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + fuzz "github.com/google/gofuzz" +) + +func TestPatchOptionsIsSuperSetOfUpdateOptions(t *testing.T) { + f := fuzz.New() + for i := 0; i < 1000; i++ { + t.Run(fmt.Sprintf("Run %d/1000", i), func(t *testing.T) { + update := UpdateOptions{} + f.Fuzz(&update) + + b, err := json.Marshal(update) + if err != nil { + t.Fatalf("failed to marshal UpdateOptions (%v): %v", err, update) + } + patch := PatchOptions{} + err = json.Unmarshal(b, &patch) + if err != nil { + t.Fatalf("failed to unmarshal UpdateOptions into PatchOptions: %v", err) + } + + b, err = json.Marshal(patch) + if err != nil { + t.Fatalf("failed to marshal PatchOptions (%v): %v", err, patch) + } + got := UpdateOptions{} + err = json.Unmarshal(b, &got) + if err != nil { + t.Fatalf("failed to unmarshal UpdateOptions into UpdateOptions: %v", err) + } + + if !reflect.DeepEqual(update, got) { + t.Fatalf(`UpdateOptions -> PatchOptions -> UpdateOptions round-trip failed: +got: %v +want: %v`, got, update) + } + }) + } +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/register.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/register.go index 0827729d087..76d042a9661 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/register.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/register.go @@ -55,6 +55,7 @@ func AddToGroupVersion(scheme *runtime.Scheme, groupVersion schema.GroupVersion) &DeleteOptions{}, &CreateOptions{}, &UpdateOptions{}, + &PatchOptions{}, ) utilruntime.Must(scheme.AddConversionFuncs( Convert_v1_WatchEvent_To_watch_Event, @@ -90,6 +91,7 @@ func init() { &DeleteOptions{}, &CreateOptions{}, &UpdateOptions{}, + &PatchOptions{}, ) // register manually. This usually goes through the SchemeBuilder, which we cannot use here. diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go index f390bf02fc4..44f77732480 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go @@ -252,6 +252,16 @@ type ObjectMeta struct { // This field is not set anywhere right now and apiserver is going to ignore it if set in create or update request. // +optional ClusterName string `json:"clusterName,omitempty" protobuf:"bytes,15,opt,name=clusterName"` + + // ManagedFields is a map of workflow-id 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. + // +optional + ManagedFields map[string]VersionedFields `json:"managedFields,omitempty" protobuf:"bytes,17,rep,name=managedFields"` } // Initializers tracks the progress of initialization. @@ -494,7 +504,30 @@ type CreateOptions struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// PatchOptions may be provided when patching an API object. +// PatchOptions is meant to be a superset of UpdateOptions. +type PatchOptions struct { + TypeMeta `json:",inline"` + + // When present, indicates that modifications should not be + // persisted. An invalid or unrecognized dryRun directive will + // result in an error response and no further processing of the + // request. Valid values are: + // - All: all dry run stages will be processed + // +optional + DryRun []string `json:"dryRun,omitempty" protobuf:"bytes,1,rep,name=dryRun"` + + // Force is going to "force" Apply requests. It means user will + // re-acquire conflicting fields owned by other people. Force + // flag must be unset for non-apply patch requests. + // +optional + Force *bool `json:"force,omitempty" protobuf:"varint,2,opt,name=force"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + // UpdateOptions may be provided when updating an API object. +// All fields in UpdateOptions should also be present in PatchOptions. type UpdateOptions struct { TypeMeta `json:",inline"` @@ -1009,3 +1042,22 @@ const ( LabelSelectorOpExists LabelSelectorOperator = "Exists" LabelSelectorOpDoesNotExist LabelSelectorOperator = "DoesNotExist" ) + +// VersionedFields is a pair of a FieldSet and the group version of the resource +// that the fieldset applies to. +type VersionedFields struct { + // 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"` + // Fields identifies a set of fields. + Fields Fields `json:"fields,omitempty" protobuf:"bytes,2,opt,name=fields,casttype=Fields"` +} + +// 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 { + // Map stores a set of fields in a data structure like a Trie. + Map map[string]Fields `json:",inline" protobuf:"bytes,1,rep,name=map"` +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go index 781469ec265..f24d65928af 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured.go @@ -450,3 +450,28 @@ 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") + 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 + } + return *out +} + +func (u *Unstructured) SetManagedFields(managedFields map[string]metav1.VersionedFields) { + 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)) + } + u.setNestedField(out, "metadata", "managedFields") +} diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured_test.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured_test.go index 4a03fff654e..c37699779a4 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured_test.go @@ -22,7 +22,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/api/apitesting/fuzzer" "k8s.io/apimachinery/pkg/api/equality" metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" @@ -117,6 +116,7 @@ func TestUnstructuredMetadataOmitempty(t *testing.T) { u.SetInitializers(nil) u.SetFinalizers(nil) u.SetClusterName("") + u.SetManagedFields(nil) gotMetadata, _, err := unstructured.NestedFieldNoCopy(u.UnstructuredContent(), "metadata") if err != nil { @@ -159,4 +159,5 @@ func setObjectMetaUsingAccessors(u, uCopy *unstructured.Unstructured) { uCopy.SetInitializers(u.GetInitializers()) uCopy.SetFinalizers(u.GetFinalizers()) uCopy.SetClusterName(u.GetClusterName()) + uCopy.SetManagedFields(u.GetManagedFields()) } diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/BUILD b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/BUILD index ea9cc91b057..072713cbb4d 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/BUILD @@ -20,6 +20,7 @@ go_library( importpath = "k8s.io/apimachinery/pkg/apis/meta/v1/validation", deps = [ "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go index 81f86fb3068..02364d7fa31 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go @@ -18,6 +18,7 @@ package validation import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" @@ -97,6 +98,15 @@ func ValidateUpdateOptions(options *metav1.UpdateOptions) field.ErrorList { return validateDryRun(field.NewPath("dryRun"), options.DryRun) } +func ValidatePatchOptions(options *metav1.PatchOptions, patchType types.PatchType) field.ErrorList { + allErrs := field.ErrorList{} + if patchType != types.ApplyPatchType && options.Force != nil { + allErrs = append(allErrs, field.Forbidden(field.NewPath("force"), "may not be specified for non-apply patch")) + } + allErrs = append(allErrs, validateDryRun(field.NewPath("dryRun"), options.DryRun)...) + return allErrs +} + var allowedDryRunValues = sets.NewString(metav1.DryRunAll) func validateDryRun(fldPath *field.Path, dryRun []string) field.ErrorList { diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go index e4515d8ed00..1f7f662e075 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go @@ -42,6 +42,7 @@ type TypeMeta struct { const ( ContentTypeJSON string = "application/json" + ContentTypeYAML string = "application/yaml" ) // RawExtension is used to hold extensions in external versions. diff --git a/staging/src/k8s.io/apimachinery/pkg/test/BUILD b/staging/src/k8s.io/apimachinery/pkg/test/BUILD index d37f55ff3e1..baf857eb64e 100644 --- a/staging/src/k8s.io/apimachinery/pkg/test/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/test/BUILD @@ -21,12 +21,14 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/api/apitesting:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/fuzzer:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/testapigroup:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/testapigroup/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/apimachinery/pkg/runtime/serializer:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/protobuf:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", diff --git a/staging/src/k8s.io/apimachinery/pkg/test/api_meta_help_test.go b/staging/src/k8s.io/apimachinery/pkg/test/api_meta_help_test.go index a68da324d67..b5e28cb0cab 100644 --- a/staging/src/k8s.io/apimachinery/pkg/test/api_meta_help_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/test/api_meta_help_test.go @@ -23,12 +23,14 @@ import ( fuzz "github.com/google/gofuzz" "k8s.io/apimachinery/pkg/api/meta" + metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/testapigroup" "k8s.io/apimachinery/pkg/apis/testapigroup/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/diff" ) @@ -308,7 +310,9 @@ func TestSetListToMatchingType(t *testing.T) { } func TestSetExtractListRoundTrip(t *testing.T) { - fuzzer := fuzz.New().NilChance(0).NumElements(1, 5) + scheme := runtime.NewScheme() + codecs := serializer.NewCodecFactory(scheme) + fuzzer := fuzz.New().NilChance(0).NumElements(1, 5).Funcs(metafuzzer.Funcs(codecs)...).MaxDepth(10) for i := 0; i < 5; i++ { start := &testapigroup.CarpList{} fuzzer.Fuzz(&start.Items) diff --git a/staging/src/k8s.io/apimachinery/pkg/test/api_meta_meta_test.go b/staging/src/k8s.io/apimachinery/pkg/test/api_meta_meta_test.go index 9da81d6d5c1..157360a191a 100644 --- a/staging/src/k8s.io/apimachinery/pkg/test/api_meta_meta_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/test/api_meta_meta_test.go @@ -23,10 +23,12 @@ import ( "github.com/google/gofuzz" "k8s.io/apimachinery/pkg/api/meta" + metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/testapigroup" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/types" ) @@ -345,7 +347,9 @@ type MyAPIObject2 struct { } func getObjectMetaAndOwnerReferences() (myAPIObject2 MyAPIObject2, metaOwnerReferences []metav1.OwnerReference) { - fuzz.New().NilChance(.5).NumElements(1, 5).Fuzz(&myAPIObject2) + scheme := runtime.NewScheme() + codecs := serializer.NewCodecFactory(scheme) + fuzz.New().NilChance(.5).NumElements(1, 5).Funcs(metafuzzer.Funcs(codecs)...).MaxDepth(10).Fuzz(&myAPIObject2) references := myAPIObject2.ObjectMeta.OwnerReferences // This is necessary for the test to pass because the getter will return a // non-nil slice. diff --git a/staging/src/k8s.io/apimachinery/pkg/types/patch.go b/staging/src/k8s.io/apimachinery/pkg/types/patch.go index d522d1dbdc6..fe8ecaaffa6 100644 --- a/staging/src/k8s.io/apimachinery/pkg/types/patch.go +++ b/staging/src/k8s.io/apimachinery/pkg/types/patch.go @@ -25,4 +25,5 @@ const ( JSONPatchType PatchType = "application/json-patch+json" MergePatchType PatchType = "application/merge-patch+json" StrategicMergePatchType PatchType = "application/strategic-merge-patch+json" + ApplyPatchType PatchType = "application/apply-patch+yaml" ) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD index ad36f85ea72..1b0d7be70b1 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD @@ -82,10 +82,13 @@ go_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/negotiation:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/metrics:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/features:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library", "//staging/src/k8s.io/apiserver/pkg/server/filters:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", "//vendor/github.com/emicklei/go-restful:go_default_library", "//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library", ], diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD index 009a8d4b2e0..b774f6c73ce 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/BUILD @@ -27,6 +27,7 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example/v1:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", @@ -73,6 +74,7 @@ go_library( "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", "//staging/src/k8s.io/apiserver/pkg/audit:go_default_library", "//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation: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", @@ -86,8 +88,6 @@ 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/kube-openapi/pkg/util/proto:go_default_library", - "//vendor/k8s.io/utils/trace:go_default_library", ], ) @@ -102,6 +102,7 @@ filegroup( name = "all-srcs", srcs = [ ":package-srcs", + "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager:all-srcs", "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/negotiation:all-srcs", "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:all-srcs", ], diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index c28e44b9539..9a2f80e378e 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -133,6 +133,20 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte } } + if scope.FieldManager != nil { + liveObj, err := scope.Creater.New(scope.Kind) + if err != nil { + scope.err(fmt.Errorf("failed to create new object: %v", err), w, req) + return + } + + obj, err = scope.FieldManager.Update(liveObj, obj, "create") + if err != nil { + scope.err(fmt.Errorf("failed to update object managed fields: %v", err), w, req) + return + } + } + trace.Step("About to store object in database") result, err := finishRequest(timeout, func() (runtime.Object, error) { return r.Create( diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/BUILD new file mode 100644 index 00000000000..9ae6f2ce54d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/BUILD @@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["fieldmanager.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager", + importpath = "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/api/errors: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", + "//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library", + "//vendor/sigs.k8s.io/structured-merge-diff/fieldpath:go_default_library", + "//vendor/sigs.k8s.io/structured-merge-diff/merge:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [ + ":package-srcs", + "//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal:all-srcs", + ], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanager.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanager.go new file mode 100644 index 00000000000..9e21d03acf1 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanager.go @@ -0,0 +1,174 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fieldmanager + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" + openapiproto "k8s.io/kube-openapi/pkg/util/proto" + "sigs.k8s.io/structured-merge-diff/fieldpath" + "sigs.k8s.io/structured-merge-diff/merge" +) + +const applyManager = "apply" + +// FieldManager updates the managed fields and merge applied +// configurations. +type FieldManager struct { + typeConverter internal.TypeConverter + objectConverter runtime.ObjectConvertor + objectDefaulter runtime.ObjectDefaulter + groupVersion schema.GroupVersion + hubVersion schema.GroupVersion + updater merge.Updater +} + +// NewFieldManager creates a new FieldManager that merges apply requests +// and update managed fields for other types of requests. +func NewFieldManager(models openapiproto.Models, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion) (*FieldManager, error) { + typeConverter, err := internal.NewTypeConverter(models) + if err != nil { + return nil, err + } + return &FieldManager{ + typeConverter: typeConverter, + objectConverter: objectConverter, + objectDefaulter: objectDefaulter, + groupVersion: gv, + hubVersion: hub, + updater: merge.Updater{ + Converter: internal.NewVersionConverter(typeConverter, objectConverter, hub), + }, + }, nil +} + +// Update is used when the object has already been merged (non-apply +// use-case), and simply updates the managed fields in the output +// object. +func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (runtime.Object, error) { + managed, err := internal.DecodeObjectManagedFields(newObj) + // If the managed field is empty or we failed to decode it, + // let's try the live object + if err != nil || len(managed) == 0 { + managed, err = internal.DecodeObjectManagedFields(liveObj) + if err != nil { + return nil, fmt.Errorf("failed to decode managed fields: %v", err) + } + } + newObjVersioned, err := f.toVersioned(newObj) + if err != nil { + return nil, fmt.Errorf("failed to convert new object to proper version: %v", err) + } + liveObjVersioned, err := f.toVersioned(liveObj) + if err != nil { + return nil, fmt.Errorf("failed to convert live object to proper version: %v", err) + } + if err := internal.RemoveObjectManagedFields(liveObjVersioned); err != nil { + return nil, fmt.Errorf("failed to remove managed fields from live obj: %v", err) + } + if err := internal.RemoveObjectManagedFields(newObjVersioned); err != nil { + return nil, fmt.Errorf("failed to remove managed fields from new obj: %v", err) + } + + newObjTyped, err := f.typeConverter.ObjectToTyped(newObjVersioned) + if err != nil { + return nil, fmt.Errorf("failed to create typed new object: %v", err) + } + liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned) + if err != nil { + return nil, fmt.Errorf("failed to create typed live object: %v", err) + } + apiVersion := fieldpath.APIVersion(f.groupVersion.String()) + managed, err = f.updater.Update(liveObjTyped, newObjTyped, apiVersion, managed, manager) + if err != nil { + return nil, fmt.Errorf("failed to update ManagedFields: %v", err) + } + + if err := internal.EncodeObjectManagedFields(newObj, managed); err != nil { + return nil, fmt.Errorf("failed to encode managed fields: %v", err) + } + + return newObj, nil +} + +// Apply is used when server-side apply is called, as it merges the +// object and update the managed fields. +func (f *FieldManager) Apply(liveObj runtime.Object, patch []byte, force bool) (runtime.Object, error) { + managed, err := internal.DecodeObjectManagedFields(liveObj) + if err != nil { + return nil, fmt.Errorf("failed to decode managed fields: %v", err) + } + // We can assume that patchObj is already on the proper version: + // it shouldn't have to be converted so that it's not defaulted. + liveObjVersioned, err := f.toVersioned(liveObj) + if err != nil { + return nil, fmt.Errorf("failed to convert live object to proper version: %v", err) + } + if err := internal.RemoveObjectManagedFields(liveObjVersioned); err != nil { + return nil, fmt.Errorf("failed to remove managed fields from live obj: %v", err) + } + + patchObjTyped, err := f.typeConverter.YAMLToTyped(patch) + if err != nil { + return nil, fmt.Errorf("failed to create typed patch object: %v", err) + } + liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned) + if err != nil { + return nil, fmt.Errorf("failed to create typed live object: %v", err) + } + apiVersion := fieldpath.APIVersion(f.groupVersion.String()) + newObjTyped, managed, err := f.updater.Apply(liveObjTyped, patchObjTyped, apiVersion, managed, applyManager, force) + if err != nil { + if conflicts, ok := err.(merge.Conflicts); ok { + return nil, errors.NewApplyConflict(conflicts) + } + return nil, err + } + + newObj, err := f.typeConverter.TypedToObject(newObjTyped) + if err != nil { + return nil, fmt.Errorf("failed to convert new typed object to object: %v", err) + } + + if err := internal.EncodeObjectManagedFields(newObj, managed); err != nil { + return nil, fmt.Errorf("failed to encode managed fields: %v", err) + } + + newObjVersioned, err := f.toVersioned(newObj) + if err != nil { + return nil, fmt.Errorf("failed to convert new object to proper version: %v", err) + } + f.objectDefaulter.Default(newObjVersioned) + + newObjUnversioned, err := f.toUnversioned(newObjVersioned) + if err != nil { + return nil, fmt.Errorf("failed to convert to unversioned: %v", err) + } + return newObjUnversioned, nil +} + +func (f *FieldManager) toVersioned(obj runtime.Object) (runtime.Object, error) { + return f.objectConverter.ConvertToVersion(obj, f.groupVersion) +} + +func (f *FieldManager) toUnversioned(obj runtime.Object) (runtime.Object, error) { + return f.objectConverter.ConvertToVersion(obj, f.hubVersion) +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/BUILD new file mode 100644 index 00000000000..d8924d72890 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/BUILD @@ -0,0 +1,68 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "fields.go", + "gvkparser.go", + "managedfields.go", + "pathelement.go", + "typeconverter.go", + "versionconverter.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal", + importpath = "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/schemaconv:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library", + "//vendor/sigs.k8s.io/structured-merge-diff/fieldpath:go_default_library", + "//vendor/sigs.k8s.io/structured-merge-diff/merge:go_default_library", + "//vendor/sigs.k8s.io/structured-merge-diff/typed:go_default_library", + "//vendor/sigs.k8s.io/structured-merge-diff/value:go_default_library", + "//vendor/sigs.k8s.io/yaml:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "fields_test.go", + "managedfields_test.go", + "pathelement_test.go", + "typeconverter_test.go", + "versionconverter_test.go", + ], + data = ["//api/openapi-spec:swagger-spec"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/github.com/ghodss/yaml:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/util/proto:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/util/proto/testing:go_default_library", + "//vendor/sigs.k8s.io/structured-merge-diff/fieldpath:go_default_library", + "//vendor/sigs.k8s.io/yaml:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fields.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fields.go new file mode 100644 index 00000000000..4fbf52c8b5b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fields.go @@ -0,0 +1,95 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/structured-merge-diff/fieldpath" +) + +func newFields() metav1.Fields { + return metav1.Fields{Map: map[string]metav1.Fields{}} +} + +func fieldsSet(f metav1.Fields, path fieldpath.Path, set *fieldpath.Set) error { + if len(f.Map) == 0 { + set.Insert(path) + } + for k := range f.Map { + if k == "." { + set.Insert(path) + continue + } + pe, err := NewPathElement(k) + if err != nil { + return err + } + path = append(path, pe) + err = fieldsSet(f.Map[k], path, set) + if err != nil { + return err + } + path = path[:len(path)-1] + } + return nil +} + +// FieldsToSet creates a set paths from an input trie of fields +func FieldsToSet(f metav1.Fields) (fieldpath.Set, error) { + set := fieldpath.Set{} + return set, fieldsSet(f, fieldpath.Path{}, &set) +} + +func removeUselessDots(f metav1.Fields) metav1.Fields { + if _, ok := f.Map["."]; ok && len(f.Map) == 1 { + delete(f.Map, ".") + return f + } + for k, tf := range f.Map { + f.Map[k] = removeUselessDots(tf) + } + return f +} + +// SetToFields creates a trie of fields from an input set of paths +func SetToFields(s fieldpath.Set) (metav1.Fields, error) { + var err error + f := newFields() + s.Iterate(func(path fieldpath.Path) { + if err != nil { + return + } + tf := f + for _, pe := range path { + var str string + str, err = PathElementString(pe) + if err != nil { + break + } + if _, ok := tf.Map[str]; ok { + tf = tf.Map[str] + } else { + tf.Map[str] = newFields() + tf = tf.Map[str] + } + } + tf.Map["."] = newFields() + }) + f = removeUselessDots(f) + return f, err +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fields_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fields_test.go new file mode 100644 index 00000000000..681c22cbcbb --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/fields_test.go @@ -0,0 +1,109 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + "reflect" + "strings" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/structured-merge-diff/fieldpath" +) + +// TestFieldsRoundTrip tests that a fields trie can be round tripped as a path set +func TestFieldsRoundTrip(t *testing.T) { + tests := []metav1.Fields{ + { + Map: map[string]metav1.Fields{ + "f:metadata": { + Map: map[string]metav1.Fields{ + ".": newFields(), + "f:name": newFields(), + }, + }, + }, + }, + } + + for _, test := range tests { + set, err := FieldsToSet(test) + if err != nil { + t.Fatalf("Failed to create path set: %v", err) + } + output, err := SetToFields(set) + if err != nil { + t.Fatalf("Failed to create fields trie from path set: %v", err) + } + if !reflect.DeepEqual(test, output) { + t.Fatalf("Expected round-trip:\ninput: %v\noutput: %v", test, output) + } + } +} + +// TestFieldsToSetError tests that errors are picked up by FieldsToSet +func TestFieldsToSetError(t *testing.T) { + tests := []struct { + fields metav1.Fields + errString string + }{ + { + fields: metav1.Fields{ + Map: map[string]metav1.Fields{ + "k:{invalid json}": { + Map: map[string]metav1.Fields{ + ".": newFields(), + "f:name": newFields(), + }, + }, + }, + }, + errString: "invalid character", + }, + } + + for _, test := range tests { + _, err := FieldsToSet(test.fields) + if err == nil || !strings.Contains(err.Error(), test.errString) { + t.Fatalf("Expected error to contain %q but got: %v", test.errString, err) + } + } +} + +// TestSetToFieldsError tests that errors are picked up by SetToFields +func TestSetToFieldsError(t *testing.T) { + validName := "ok" + invalidPath := fieldpath.Path([]fieldpath.PathElement{{}, {FieldName: &validName}}) + + tests := []struct { + set fieldpath.Set + errString string + }{ + { + set: *fieldpath.NewSet(invalidPath), + errString: "Invalid type of path element", + }, + } + + for _, test := range tests { + _, err := SetToFields(test.set) + if err == nil || !strings.Contains(err.Error(), test.errString) { + t.Fatalf("Expected error to contain %q but got: %v", test.errString, err) + } + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/gvkparser.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/gvkparser.go new file mode 100644 index 00000000000..ce9a0492445 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/gvkparser.go @@ -0,0 +1,116 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kube-openapi/pkg/schemaconv" + "k8s.io/kube-openapi/pkg/util/proto" + "sigs.k8s.io/structured-merge-diff/typed" +) + +// groupVersionKindExtensionKey is the key used to lookup the +// GroupVersionKind value for an object definition from the +// definition's "extensions" map. +const groupVersionKindExtensionKey = "x-kubernetes-group-version-kind" + +type gvkParser struct { + gvks map[schema.GroupVersionKind]string + parser typed.Parser +} + +func (p *gvkParser) Type(gvk schema.GroupVersionKind) *typed.ParseableType { + typeName, ok := p.gvks[gvk] + if !ok { + return nil + } + return p.parser.Type(typeName) +} + +func newGVKParser(models proto.Models) (*gvkParser, error) { + typeSchema, err := schemaconv.ToSchema(models) + if err != nil { + return nil, fmt.Errorf("failed to convert models to schema: %v", err) + } + parser := gvkParser{ + gvks: map[schema.GroupVersionKind]string{}, + } + parser.parser = typed.Parser{Schema: *typeSchema} + for _, modelName := range models.ListModels() { + model := models.LookupModel(modelName) + if model == nil { + panic("ListModels returns a model that can't be looked-up.") + } + gvkList := parseGroupVersionKind(model) + for _, gvk := range gvkList { + if len(gvk.Kind) > 0 { + parser.gvks[gvk] = modelName + } + } + } + return &parser, nil +} + +// Get and parse GroupVersionKind from the extension. Returns empty if it doesn't have one. +func parseGroupVersionKind(s proto.Schema) []schema.GroupVersionKind { + extensions := s.GetExtensions() + + gvkListResult := []schema.GroupVersionKind{} + + // Get the extensions + gvkExtension, ok := extensions[groupVersionKindExtensionKey] + if !ok { + return []schema.GroupVersionKind{} + } + + // gvk extension must be a list of at least 1 element. + gvkList, ok := gvkExtension.([]interface{}) + if !ok { + return []schema.GroupVersionKind{} + } + + for _, gvk := range gvkList { + // gvk extension list must be a map with group, version, and + // kind fields + gvkMap, ok := gvk.(map[interface{}]interface{}) + if !ok { + continue + } + group, ok := gvkMap["group"].(string) + if !ok { + continue + } + version, ok := gvkMap["version"].(string) + if !ok { + continue + } + kind, ok := gvkMap["kind"].(string) + if !ok { + continue + } + + gvkListResult = append(gvkListResult, schema.GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + }) + } + + return gvkListResult +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go new file mode 100644 index 00000000000..ffe7485e986 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go @@ -0,0 +1,119 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/structured-merge-diff/fieldpath" +) + +// RemoveObjectManagedFields removes the ManagedFields from the object +// before we merge so that it doesn't appear in the ManagedFields +// recursively. +func RemoveObjectManagedFields(obj runtime.Object) error { + accessor, err := meta.Accessor(obj) + if err != nil { + return fmt.Errorf("couldn't get accessor: %v", err) + } + accessor.SetManagedFields(nil) + return nil +} + +// DecodeObjectManagedFields extracts and converts the objects ManagedFields into a fieldpath.ManagedFields. +func DecodeObjectManagedFields(from runtime.Object) (fieldpath.ManagedFields, error) { + if from == nil { + return make(map[string]*fieldpath.VersionedSet), nil + } + accessor, err := meta.Accessor(from) + if err != nil { + return nil, fmt.Errorf("couldn't get accessor: %v", err) + } + + managed, err := decodeManagedFields(accessor.GetManagedFields()) + if err != nil { + return nil, fmt.Errorf("failed to convert managed fields from API: %v", err) + } + return managed, err +} + +// EncodeObjectManagedFields converts and stores the fieldpathManagedFields into the objects ManagedFields +func EncodeObjectManagedFields(obj runtime.Object, fields fieldpath.ManagedFields) error { + accessor, err := meta.Accessor(obj) + if err != nil { + return fmt.Errorf("couldn't get accessor: %v", err) + } + + managed, err := encodeManagedFields(fields) + if err != nil { + return fmt.Errorf("failed to convert back managed fields to API: %v", err) + } + accessor.SetManagedFields(managed) + + return nil +} + +// decodeManagedFields converts ManagedFields from the wire format (api format) +// to the format used by sigs.k8s.io/structured-merge-diff +func decodeManagedFields(encodedManagedFields map[string]metav1.VersionedFields) (managedFields fieldpath.ManagedFields, err error) { + managedFields = make(map[string]*fieldpath.VersionedSet, len(encodedManagedFields)) + for manager, encodedVersionedSet := range encodedManagedFields { + managedFields[manager], err = decodeVersionedSet(&encodedVersionedSet) + if err != nil { + return nil, fmt.Errorf("error decoding versioned set for %v: %v", manager, err) + } + } + return managedFields, nil +} + +func decodeVersionedSet(encodedVersionedSet *metav1.VersionedFields) (versionedSet *fieldpath.VersionedSet, err error) { + versionedSet = &fieldpath.VersionedSet{} + versionedSet.APIVersion = fieldpath.APIVersion(encodedVersionedSet.APIVersion) + set, err := FieldsToSet(encodedVersionedSet.Fields) + if err != nil { + return nil, fmt.Errorf("error decoding set: %v", err) + } + versionedSet.Set = &set + return versionedSet, nil +} + +// 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) + if err != nil { + return nil, fmt.Errorf("error encoding versioned set for %v: %v", manager, err) + } + encodedManagedFields[manager] = *v + } + return encodedManagedFields, nil +} + +func encodeVersionedSet(versionedSet *fieldpath.VersionedSet) (encodedVersionedSet *metav1.VersionedFields, err error) { + encodedVersionedSet = &metav1.VersionedFields{} + encodedVersionedSet.APIVersion = string(versionedSet.APIVersion) + encodedVersionedSet.Fields, err = SetToFields(*versionedSet.Set) + if err != nil { + return nil, fmt.Errorf("error encoding set: %v", err) + } + return encodedVersionedSet, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go new file mode 100644 index 00000000000..dca8b93c73c --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go @@ -0,0 +1,143 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/yaml" +) + +// TestRoundTripManagedFields will roundtrip ManagedFields from the format used by +// sigs.k8s.io/structured-merge-diff to the wire format (api format) and back +func TestRoundTripManagedFields(t *testing.T) { + tests := []string{ + `foo: + apiVersion: v1 + fields: + i:5: + f:i: {} + v:3: + f:alsoPi: {} + v:3.1415: + f:pi: {} + v:false: + f:notTrue: {} +`, + `foo: + apiVersion: v1 + fields: + f:spec: + f:containers: + k:{"name":"c"}: + f:image: {} + f:name: {} +`, + `foo: + apiVersion: v1 + fields: + f:apiVersion: {} + f:kind: {} + f:metadata: + f:labels: + f:app: {} + f:name: {} + f:spec: + f:replicas: {} + f:selector: + f:matchLabels: + f:app: {} + f:template: + f:medatada: + f:labels: + f:app: {} + f:spec: + f:containers: + k:{"name":"nginx"}: + .: {} + f:image: {} + f:name: {} + f:ports: + i:0: + f:containerPort: {} +`, + `foo: + apiVersion: v1 + fields: + f:allowVolumeExpansion: {} + f:apiVersion: {} + f:kind: {} + f:metadata: + f:name: {} + f:parameters: + f:resturl: {} + f:restuser: {} + f:secretName: {} + f:secretNamespace: {} + f:provisioner: {} +`, + `foo: + apiVersion: v1 + fields: + f:apiVersion: {} + f:kind: {} + f:metadata: + f:name: {} + f:spec: + f:group: {} + f:names: + f:kind: {} + f:plural: {} + f:shortNames: + i:0: {} + f:singular: {} + f:scope: {} + f:versions: + k:{"name":"v1"}: + f:name: {} + f:served: {} + f:storage: {} +`, + } + + for _, test := range tests { + t.Run(test, func(t *testing.T) { + var unmarshaled map[string]metav1.VersionedFields + if err := yaml.Unmarshal([]byte(test), &unmarshaled); err != nil { + t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) + } + decoded, err := decodeManagedFields(unmarshaled) + if err != nil { + t.Fatalf("did not expect decoding error but got: %v", err) + } + encoded, err := encodeManagedFields(decoded) + if err != nil { + t.Fatalf("did not expect encoding error but got: %v", err) + } + marshaled, err := yaml.Marshal(&encoded) + if err != nil { + t.Fatalf("did not expect yaml marshalling error but got: %v", err) + } + if !reflect.DeepEqual(string(marshaled), test) { + t.Fatalf("expected:\n%v\nbut got:\n%v", test, string(marshaled)) + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/pathelement.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/pathelement.go new file mode 100644 index 00000000000..e2b63362f37 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/pathelement.go @@ -0,0 +1,140 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "sigs.k8s.io/structured-merge-diff/fieldpath" + "sigs.k8s.io/structured-merge-diff/value" +) + +const ( + // Field indicates that the content of this path element is a field's name + Field = "f" + + // Value indicates that the content of this path element is a field's value + Value = "v" + + // Index indicates that the content of this path element is an index in an array + Index = "i" + + // Key indicates that the content of this path element is a key value map + Key = "k" + + // Separator separates the type of a path element from the contents + Separator = ":" +) + +// NewPathElement parses a serialized path element +func NewPathElement(s string) (fieldpath.PathElement, error) { + split := strings.SplitN(s, Separator, 2) + if len(split) < 2 { + return fieldpath.PathElement{}, fmt.Errorf("missing colon: %v", s) + } + switch split[0] { + case Field: + return fieldpath.PathElement{ + FieldName: &split[1], + }, nil + case Value: + val, err := value.FromJSON([]byte(split[1])) + if err != nil { + return fieldpath.PathElement{}, err + } + return fieldpath.PathElement{ + Value: &val, + }, nil + case Index: + i, err := strconv.Atoi(split[1]) + if err != nil { + return fieldpath.PathElement{}, err + } + return fieldpath.PathElement{ + Index: &i, + }, nil + case Key: + kv := map[string]json.RawMessage{} + err := json.Unmarshal([]byte(split[1]), &kv) + if err != nil { + return fieldpath.PathElement{}, err + } + fields := []value.Field{} + for k, v := range kv { + b, err := json.Marshal(v) + if err != nil { + return fieldpath.PathElement{}, err + } + val, err := value.FromJSON(b) + if err != nil { + return fieldpath.PathElement{}, err + } + + fields = append(fields, value.Field{ + Name: k, + Value: val, + }) + } + return fieldpath.PathElement{ + Key: fields, + }, nil + default: + // Ignore unknown key types + return fieldpath.PathElement{}, nil + } +} + +// PathElementString serializes a path element +func PathElementString(pe fieldpath.PathElement) (string, error) { + switch { + case pe.FieldName != nil: + return Field + Separator + *pe.FieldName, nil + case len(pe.Key) > 0: + kv := map[string]json.RawMessage{} + for _, k := range pe.Key { + b, err := k.Value.ToJSON() + if err != nil { + return "", err + } + m := json.RawMessage{} + err = json.Unmarshal(b, &m) + if err != nil { + return "", err + } + kv[k.Name] = m + } + b, err := json.Marshal(kv) + if err != nil { + return "", err + } + return Key + ":" + string(b), nil + case pe.Value != nil: + b, err := pe.Value.ToJSON() + if err != nil { + return "", err + } + return Value + ":" + string(b), nil + case pe.Index != nil: + return Index + ":" + strconv.Itoa(*pe.Index), nil + default: + return "", errors.New("Invalid type of path element") + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/pathelement_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/pathelement_test.go new file mode 100644 index 00000000000..81b9dd41765 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/pathelement_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import "testing" + +func TestPathElementRoundTrip(t *testing.T) { + tests := []string{ + "i:0", + "i:1234", + "f:", + "f:spec", + "f:more-complicated-string", + "k:{\"name\":\"my-container\"}", + "k:{\"port\":\"8080\",\"protocol\":\"TCP\"}", + "k:{\"optionalField\":null}", + "k:{\"jsonField\":{\"A\":1,\"B\":null,\"C\":\"D\",\"E\":{\"F\":\"G\"}}}", + "k:{\"listField\":[\"1\",\"2\",\"3\"]}", + "v:null", + "v:\"some-string\"", + "v:1234", + "v:{\"some\":\"json\"}", + } + + for _, test := range tests { + t.Run(test, func(t *testing.T) { + pe, err := NewPathElement(test) + if err != nil { + t.Fatalf("Failed to create path element: %v", err) + } + output, err := PathElementString(pe) + if err != nil { + t.Fatalf("Failed to create string from path element: %v", err) + } + if test != output { + t.Fatalf("Expected round-trip:\ninput: %v\noutput: %v", test, output) + } + }) + } +} + +func TestPathElementIgnoreUnknown(t *testing.T) { + _, err := NewPathElement("r:Hello") + if err != nil { + t.Fatalf("Unknown qualifiers should be ignored") + } +} + +func TestNewPathElementError(t *testing.T) { + tests := []string{ + "", + "no-colon", + "i:index is not a number", + "i:1.23", + "i:", + "v:invalid json", + "v:", + "k:invalid json", + "k:{\"name\":invalid}", + } + + for _, test := range tests { + t.Run(test, func(t *testing.T) { + _, err := NewPathElement(test) + if err == nil { + t.Fatalf("Expected error, no error found") + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go new file mode 100644 index 00000000000..79f3017cb43 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter.go @@ -0,0 +1,99 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/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/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) +} + +type typeConverter struct { + parser *gvkParser +} + +var _ TypeConverter = &typeConverter{} + +// NewTypeConverter builds a TypeConverter from a proto.Models. This +// will automatically find the proper version of the object, and the +// corresponding schema information. +func NewTypeConverter(models proto.Models) (TypeConverter, error) { + parser, err := newGVKParser(models) + if err != nil { + return nil, err + } + 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 + } + gvk := obj.GetObjectKind().GroupVersionKind() + t := c.parser.Type(gvk) + if t == nil { + return typed.TypedValue{}, fmt.Errorf("no corresponding type for %v", gvk) + } + return t.FromUnstructured(u) +} + +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 c.ObjectToTyped(unstructured) +} + +func (c *typeConverter) TypedToObject(value typed.TypedValue) (runtime.Object, error) { + vu := value.AsValue().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) + } + return &unstructured.Unstructured{Object: u}, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter_test.go new file mode 100644 index 00000000000..bced6d6328f --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/typeconverter_test.go @@ -0,0 +1,109 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal_test + +import ( + "path/filepath" + "reflect" + "testing" + + "github.com/ghodss/yaml" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" + "k8s.io/kube-openapi/pkg/util/proto" + prototesting "k8s.io/kube-openapi/pkg/util/proto/testing" +) + +var fakeSchema = prototesting.Fake{ + Path: filepath.Join( + "..", "..", "..", "..", "..", "..", "..", "..", "..", + "api", "openapi-spec", "swagger.json"), +} + +func TestTypeConverter(t *testing.T) { + d, err := fakeSchema.OpenAPISchema() + if err != nil { + t.Fatalf("Failed to parse OpenAPI schema: %v", err) + } + m, err := proto.NewOpenAPIData(d) + if err != nil { + t.Fatalf("Failed to build OpenAPI models: %v", err) + } + + tc, err := internal.NewTypeConverter(m) + if err != nil { + t.Fatalf("Failed to build TypeConverter: %v", err) + } + + y := ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.15.4 +` + + obj := &unstructured.Unstructured{Object: map[string]interface{}{}} + if err := yaml.Unmarshal([]byte(y), &obj.Object); err != nil { + t.Fatalf("Failed to parse yaml object: %v", err) + } + typed, err := tc.ObjectToTyped(obj) + if err != nil { + t.Fatalf("Failed to convert object to typed: %v", err) + } + newObj, err := tc.TypedToObject(typed) + if err != nil { + t.Fatalf("Failed to convert typed to object: %v", err) + } + if !reflect.DeepEqual(obj, newObj) { + t.Errorf(`Round-trip failed: +Original object: +%#v +Final object: +%#v`, obj, newObj) + } + + yamlTyped, err := tc.YAMLToTyped([]byte(y)) + if err != nil { + t.Fatalf("Failed to convert yaml to typed: %v", err) + } + newObj, err = tc.TypedToObject(yamlTyped) + if err != nil { + t.Fatalf("Failed to convert typed to object: %v", err) + } + if !reflect.DeepEqual(obj, newObj) { + t.Errorf(`YAML conversion resulted in different object failed: +Original object: +%#v +Final object: +%#v`, obj, newObj) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter.go new file mode 100644 index 00000000000..05770b0aeb0 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter.go @@ -0,0 +1,83 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/structured-merge-diff/fieldpath" + "sigs.k8s.io/structured-merge-diff/merge" + "sigs.k8s.io/structured-merge-diff/typed" +) + +// versionConverter is an implementation of +// sigs.k8s.io/structured-merge-diff/merge.Converter +type versionConverter struct { + typeConverter TypeConverter + objectConvertor runtime.ObjectConvertor + hubVersion schema.GroupVersion +} + +var _ merge.Converter = &versionConverter{} + +// NewVersionConverter builds a VersionConverter from a TypeConverter and an ObjectConvertor. +func NewVersionConverter(t TypeConverter, o runtime.ObjectConvertor, h schema.GroupVersion) merge.Converter { + return &versionConverter{ + typeConverter: t, + objectConvertor: o, + hubVersion: h, + } +} + +// Convert implements sigs.k8s.io/structured-merge-diff/merge.Converter +func (v *versionConverter) Convert(object typed.TypedValue, version fieldpath.APIVersion) (typed.TypedValue, error) { + // Convert the smd typed value to a kubernetes object. + objectToConvert, err := v.typeConverter.TypedToObject(object) + if err != nil { + return object, err + } + + // Parse the target groupVersion. + groupVersion, err := schema.ParseGroupVersion(string(version)) + if err != nil { + return object, err + } + + // If attempting to convert to the same version as we already have, just return it. + if objectToConvert.GetObjectKind().GroupVersionKind().GroupVersion() == groupVersion { + return object, nil + } + + // Convert to internal + internalObject, err := v.objectConvertor.ConvertToVersion(objectToConvert, v.hubVersion) + if err != nil { + return object, fmt.Errorf("failed to convert object (%v to %v): %v", + objectToConvert.GetObjectKind().GroupVersionKind(), v.hubVersion, err) + } + + // Convert the object into the target version + convertedObject, err := v.objectConvertor.ConvertToVersion(internalObject, groupVersion) + if err != nil { + return object, fmt.Errorf("failed to convert object (%v to %v): %v", + internalObject.GetObjectKind().GroupVersionKind(), groupVersion, err) + } + + // Convert the object back to a smd typed value and return it. + return v.typeConverter.ObjectToTyped(convertedObject) +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter_test.go new file mode 100644 index 00000000000..18365f6d3a1 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/versionconverter_test.go @@ -0,0 +1,107 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package internal_test + +import ( + "fmt" + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" + "k8s.io/kube-openapi/pkg/util/proto" + "sigs.k8s.io/structured-merge-diff/fieldpath" +) + +// TestVersionConverter tests the version converter +func TestVersionConverter(t *testing.T) { + d, err := fakeSchema.OpenAPISchema() + if err != nil { + t.Fatalf("Failed to parse OpenAPI schema: %v", err) + } + m, err := proto.NewOpenAPIData(d) + if err != nil { + t.Fatalf("Failed to build OpenAPI models: %v", err) + } + tc, err := internal.NewTypeConverter(m) + if err != nil { + t.Fatalf("Failed to build TypeConverter: %v", err) + } + oc := fakeObjectConvertor{ + gvkForVersion("v1beta1"): objForGroupVersion("apps/v1beta1"), + gvkForVersion("v1"): objForGroupVersion("apps/v1"), + } + vc := internal.NewVersionConverter(tc, oc, schema.GroupVersion{Group: "apps", Version: runtime.APIVersionInternal}) + + input, err := tc.ObjectToTyped(objForGroupVersion("apps/v1beta1")) + if err != nil { + t.Fatalf("error creating converting input object to a typed value: %v", err) + } + expected := objForGroupVersion("apps/v1") + output, err := vc.Convert(input, fieldpath.APIVersion("apps/v1")) + if err != nil { + t.Fatalf("expected err to be nil but got %v", err) + } + actual, err := tc.TypedToObject(output) + if err != nil { + t.Fatalf("error converting output typed value to an object %v", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("expected to get %v but got %v", expected, actual) + } +} + +func gvkForVersion(v string) schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: "apps", + Version: v, + Kind: "Deployment", + } +} + +func objForGroupVersion(gv string) runtime.Object { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": gv, + "kind": "Deployment", + }, + } +} + +type fakeObjectConvertor map[schema.GroupVersionKind]runtime.Object + +var _ runtime.ObjectConvertor = fakeObjectConvertor{} + +func (c fakeObjectConvertor) ConvertToVersion(_ runtime.Object, gv runtime.GroupVersioner) (runtime.Object, error) { + allKinds := make([]schema.GroupVersionKind, 0) + for kind := range c { + allKinds = append(allKinds, kind) + } + gvk, _ := gv.KindForGroupVersionKinds(allKinds) + return c[gvk], nil +} + +func (fakeObjectConvertor) Convert(_, _, _ interface{}) error { + return fmt.Errorf("function not implemented") +} + +func (fakeObjectConvertor) ConvertFieldLabel(_ schema.GroupVersionKind, _, _ string) (string, string, error) { + return "", "", fmt.Errorf("function not implemented") +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 384c276685b..b674dcdd4b1 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -24,8 +24,8 @@ import ( "time" "github.com/evanphx/json-patch" - "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/validation" @@ -38,6 +38,8 @@ import ( "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/features" @@ -94,20 +96,20 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface return } - patchJS, err := readBody(req) + patchBytes, err := readBody(req) if err != nil { scope.err(err, w, req) return } - options := &metav1.UpdateOptions{} + options := &metav1.PatchOptions{} if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, options); err != nil { err = errors.NewBadRequest(err.Error()) scope.err(err, w, req) return } - if errs := validation.ValidateUpdateOptions(options); len(errs) > 0 { - err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "UpdateOptions"}, "", errs) + if errs := validation.ValidatePatchOptions(options, patchType); len(errs) > 0 { + err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "PatchOptions"}, "", errs) scope.err(err, w, req) return } @@ -115,12 +117,16 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface ae := request.AuditEventFrom(ctx) admit = admission.WithAudit(admit, ae) - audit.LogRequestPatch(ae, patchJS) + audit.LogRequestPatch(ae, patchBytes) trace.Step("Recorded the audit event") - s, ok := runtime.SerializerInfoForMediaType(scope.Serializer.SupportedMediaTypes(), runtime.ContentTypeJSON) + baseContentType := runtime.ContentTypeJSON + if patchType == types.ApplyPatchType { + baseContentType = runtime.ContentTypeYAML + } + s, ok := runtime.SerializerInfoForMediaType(scope.Serializer.SupportedMediaTypes(), baseContentType) if !ok { - scope.err(fmt.Errorf("no serializer defined for JSON"), w, req) + scope.err(fmt.Errorf("no serializer defined for %v", baseContentType), w, req) return } gv := scope.Kind.GroupVersion() @@ -131,7 +137,18 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface ) userInfo, _ := request.UserFrom(ctx) - staticAdmissionAttributes := admission.NewAttributesRecord( + staticCreateAttributes := admission.NewAttributesRecord( + nil, + nil, + scope.Kind, + namespace, + name, + scope.Resource, + scope.Subresource, + admission.Create, + dryrun.IsDryRun(options.DryRun), + userInfo) + staticUpdateAttributes := admission.NewAttributesRecord( nil, nil, scope.Kind, @@ -143,38 +160,37 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface dryrun.IsDryRun(options.DryRun), userInfo, ) - admissionCheck := func(updatedObject runtime.Object, currentObject runtime.Object) error { - // if we allow create-on-patch, we have this TODO: call the mutating admission chain with the CREATE verb instead of UPDATE - if mutatingAdmission, ok := admit.(admission.MutationInterface); ok && admit.Handles(admission.Update) { - return mutatingAdmission.Admit(admission.NewAttributesRecord( - updatedObject, - currentObject, - scope.Kind, - namespace, - name, - scope.Resource, - scope.Subresource, - admission.Update, - dryrun.IsDryRun(options.DryRun), - userInfo, - )) - } - return nil + + mutatingAdmission, _ := admit.(admission.MutationInterface) + createAuthorizerAttributes := authorizer.AttributesRecord{ + User: userInfo, + ResourceRequest: true, + Path: req.URL.Path, + Verb: "create", + APIGroup: scope.Resource.Group, + APIVersion: scope.Resource.Version, + Resource: scope.Resource.Resource, + Subresource: scope.Subresource, + Namespace: namespace, + Name: name, } p := patcher{ namer: scope.Namer, creater: scope.Creater, defaulter: scope.Defaulter, + typer: scope.Typer, unsafeConvertor: scope.UnsafeConvertor, kind: scope.Kind, resource: scope.Resource, + subresource: scope.Subresource, + dryRun: dryrun.IsDryRun(options.DryRun), hubGroupVersion: scope.HubGroupVersion, - createValidation: rest.AdmissionToValidateObjectFunc(admit, staticAdmissionAttributes), - updateValidation: rest.AdmissionToValidateObjectUpdateFunc(admit, staticAdmissionAttributes), - admissionCheck: admissionCheck, + createValidation: withAuthorization(rest.AdmissionToValidateObjectFunc(admit, staticCreateAttributes), scope.Authorizer, createAuthorizerAttributes), + updateValidation: rest.AdmissionToValidateObjectUpdateFunc(admit, staticUpdateAttributes), + admissionCheck: mutatingAdmission, codec: codec, @@ -184,20 +200,35 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface restPatcher: r, name: name, patchType: patchType, - patchJS: patchJS, + patchBytes: patchBytes, trace: trace, } - result, err := p.patchResource(ctx) + result, wasCreated, err := p.patchResource(ctx, scope) if err != nil { scope.err(err, w, req) return } trace.Step("Object stored in database") + requestInfo, ok := request.RequestInfoFrom(ctx) + if !ok { + scope.err(fmt.Errorf("missing requestInfo"), w, req) + return + } + if err := setSelfLink(result, requestInfo, scope.Namer); err != nil { + scope.err(err, w, req) + return + } + trace.Step("Self-link added") + + status := http.StatusOK + if wasCreated { + status = http.StatusCreated + } scope.Trace = trace - transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result) + transformResponseObject(ctx, scope, req, w, status, outputMediaType, result) } } @@ -213,27 +244,30 @@ type patcher struct { namer ScopeNamer creater runtime.ObjectCreater defaulter runtime.ObjectDefaulter + typer runtime.ObjectTyper unsafeConvertor runtime.ObjectConvertor resource schema.GroupVersionResource kind schema.GroupVersionKind + subresource string + dryRun bool hubGroupVersion schema.GroupVersion // Validation functions createValidation rest.ValidateObjectFunc updateValidation rest.ValidateObjectUpdateFunc - admissionCheck mutateObjectUpdateFunc + admissionCheck admission.MutationInterface codec runtime.Codec timeout time.Duration - options *metav1.UpdateOptions + options *metav1.PatchOptions // Operation information restPatcher rest.Patcher name string patchType types.PatchType - patchJS []byte + patchBytes []byte trace *utiltrace.Trace @@ -241,14 +275,18 @@ type patcher struct { namespace string updatedObjectInfo rest.UpdatedObjectInfo mechanism patchMechanism + forceAllowCreate bool } type patchMechanism interface { applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) + createNewObject() (runtime.Object, error) } type jsonPatcher struct { *patcher + + fieldManager *fieldmanager.FieldManager } func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) { @@ -270,15 +308,24 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r return nil, err } + if p.fieldManager != nil { + if objToUpdate, err = p.fieldManager.Update(currentObject, objToUpdate, "jsonPatcher"); err != nil { + return nil, fmt.Errorf("failed to update object managed fields: %v", err) + } + } return objToUpdate, nil } -// patchJS applies the patch. Input and output objects must both have +func (p *jsonPatcher) createNewObject() (runtime.Object, error) { + return nil, errors.NewNotFound(p.resource.GroupResource(), p.name) +} + +// applyJSPatch applies the patch. Input and output objects must both have // the external version, since that is what the patch must have been constructed against. func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr error) { switch p.patchType { case types.JSONPatchType: - patchObj, err := jsonpatch.DecodePatch(p.patchJS) + patchObj, err := jsonpatch.DecodePatch(p.patchBytes) if err != nil { return nil, errors.NewBadRequest(err.Error()) } @@ -288,7 +335,7 @@ func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr } return patchedJS, nil case types.MergePatchType: - return jsonpatch.MergePatch(versionedJS, p.patchJS) + return jsonpatch.MergePatch(versionedJS, p.patchBytes) default: // only here as a safety net - go-restful filters content-type return nil, fmt.Errorf("unknown Content-Type header for patch: %v", p.patchType) @@ -300,6 +347,7 @@ type smpPatcher struct { // Schema schemaReferenceObj runtime.Object + fieldManager *fieldmanager.FieldManager } func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) { @@ -313,22 +361,60 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru if err != nil { return nil, err } - if err := strategicPatchObject(p.defaulter, currentVersionedObject, p.patchJS, versionedObjToUpdate, p.schemaReferenceObj); err != nil { + if err := strategicPatchObject(p.defaulter, currentVersionedObject, p.patchBytes, versionedObjToUpdate, p.schemaReferenceObj); err != nil { return nil, err } // Convert the object back to the hub version - return p.unsafeConvertor.ConvertToVersion(versionedObjToUpdate, p.hubGroupVersion) + newObj, err := p.unsafeConvertor.ConvertToVersion(versionedObjToUpdate, p.hubGroupVersion) + if err != nil { + return nil, err + } + + if p.fieldManager != nil { + if newObj, err = p.fieldManager.Update(currentObject, newObj, "smPatcher"); err != nil { + return nil, fmt.Errorf("failed to update object managed fields: %v", err) + } + } + return newObj, nil } -// strategicPatchObject applies a strategic merge patch of to +func (p *smpPatcher) createNewObject() (runtime.Object, error) { + return nil, errors.NewNotFound(p.resource.GroupResource(), p.name) +} + +type applyPatcher struct { + patch []byte + options *metav1.PatchOptions + creater runtime.ObjectCreater + kind schema.GroupVersionKind + fieldManager *fieldmanager.FieldManager +} + +func (p *applyPatcher) applyPatchToCurrentObject(obj runtime.Object) (runtime.Object, error) { + force := false + if p.options.Force != nil { + force = *p.options.Force + } + return p.fieldManager.Apply(obj, p.patch, force) +} + +func (p *applyPatcher) createNewObject() (runtime.Object, error) { + obj, err := p.creater.New(p.kind) + if err != nil { + return nil, fmt.Errorf("failed to create new object: %v", obj) + } + return p.applyPatchToCurrentObject(obj) +} + +// strategicPatchObject applies a strategic merge patch of to // and stores the result in . // It additionally returns the map[string]interface{} representation of the -// and . +// and . // NOTE: Both and are supposed to be versioned. func strategicPatchObject( defaulter runtime.ObjectDefaulter, originalObject runtime.Object, - patchJS []byte, + patchBytes []byte, objToUpdate runtime.Object, schemaReferenceObj runtime.Object, ) error { @@ -338,7 +424,7 @@ func strategicPatchObject( } patchMap := make(map[string]interface{}) - if err := json.Unmarshal(patchJS, &patchMap); err != nil { + if err := json.Unmarshal(patchBytes, &patchMap); err != nil { return errors.NewBadRequest(err.Error()) } @@ -350,52 +436,113 @@ func strategicPatchObject( // applyPatch is called every time GuaranteedUpdate asks for the updated object, // and is given the currently persisted object as input. -func (p *patcher) applyPatch(_ context.Context, _, currentObject runtime.Object) (runtime.Object, error) { +// TODO: rename this function because the name implies it is related to applyPatcher +func (p *patcher) applyPatch(_ context.Context, _, currentObject runtime.Object) (objToUpdate runtime.Object, patchErr error) { // Make sure we actually have a persisted currentObject p.trace.Step("About to apply patch") - if hasUID, err := hasUID(currentObject); err != nil { + currentObjectHasUID, err := hasUID(currentObject) + if err != nil { return nil, err - } else if !hasUID { - return nil, errors.NewNotFound(p.resource.GroupResource(), p.name) + } else if !currentObjectHasUID { + objToUpdate, patchErr = p.mechanism.createNewObject() + } else { + objToUpdate, patchErr = p.mechanism.applyPatchToCurrentObject(currentObject) } - objToUpdate, err := p.mechanism.applyPatchToCurrentObject(currentObject) + if patchErr != nil { + return nil, patchErr + } + + objToUpdateHasUID, err := hasUID(objToUpdate) if err != nil { return nil, err } + if objToUpdateHasUID && !currentObjectHasUID { + accessor, err := meta.Accessor(objToUpdate) + if err != nil { + return nil, err + } + return nil, errors.NewConflict(p.resource.GroupResource(), p.name, fmt.Errorf("uid mismatch: the provided object specified uid %s, and no existing object was found", accessor.GetUID())) + } + if err := checkName(objToUpdate, p.name, p.namespace, p.namer); err != nil { return nil, err } return objToUpdate, nil } +func (p *patcher) admissionAttributes(ctx context.Context, updatedObject runtime.Object, currentObject runtime.Object, operation admission.Operation) admission.Attributes { + userInfo, _ := request.UserFrom(ctx) + return admission.NewAttributesRecord(updatedObject, currentObject, p.kind, p.namespace, p.name, p.resource, p.subresource, operation, p.dryRun, userInfo) +} + // applyAdmission is called every time GuaranteedUpdate asks for the updated object, // and is given the currently persisted object and the patched object as input. +// TODO: rename this function because the name implies it is related to applyPatcher func (p *patcher) applyAdmission(ctx context.Context, patchedObject runtime.Object, currentObject runtime.Object) (runtime.Object, error) { p.trace.Step("About to check admission control") - return patchedObject, p.admissionCheck(patchedObject, currentObject) + var operation admission.Operation + if hasUID, err := hasUID(currentObject); err != nil { + return nil, err + } else if !hasUID { + operation = admission.Create + currentObject = nil + } else { + operation = admission.Update + } + if p.admissionCheck != nil && p.admissionCheck.Handles(operation) { + attributes := p.admissionAttributes(ctx, patchedObject, currentObject, operation) + return patchedObject, p.admissionCheck.Admit(attributes) + } + return patchedObject, nil } // patchResource divides PatchResource for easier unit testing -func (p *patcher) patchResource(ctx context.Context) (runtime.Object, error) { +func (p *patcher) patchResource(ctx context.Context, scope RequestScope) (runtime.Object, bool, error) { p.namespace = request.NamespaceValue(ctx) switch p.patchType { case types.JSONPatchType, types.MergePatchType: - p.mechanism = &jsonPatcher{patcher: p} + p.mechanism = &jsonPatcher{ + patcher: p, + fieldManager: scope.FieldManager, + } case types.StrategicMergePatchType: schemaReferenceObj, err := p.unsafeConvertor.ConvertToVersion(p.restPatcher.New(), p.kind.GroupVersion()) + if err != nil { + return nil, false, err + } + p.mechanism = &smpPatcher{ + patcher: p, + schemaReferenceObj: schemaReferenceObj, + fieldManager: scope.FieldManager, + } + // this case is unreachable if ServerSideApply is not enabled because we will have already rejected the content type + case types.ApplyPatchType: + p.mechanism = &applyPatcher{ + fieldManager: scope.FieldManager, + patch: p.patchBytes, + options: p.options, + creater: p.creater, + kind: p.kind, + } + p.forceAllowCreate = true + default: + return nil, false, fmt.Errorf("%v: unimplemented patch type", p.patchType) + } + + wasCreated := false + p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil, p.applyPatch, p.applyAdmission) + result, err := finishRequest(p.timeout, func() (runtime.Object, error) { + // TODO: Pass in UpdateOptions to override UpdateStrategy.AllowUpdateOnCreate + options, err := patchToUpdateOptions(p.options) if err != nil { return nil, err } - p.mechanism = &smpPatcher{patcher: p, schemaReferenceObj: schemaReferenceObj} - default: - return nil, fmt.Errorf("%v: unimplemented patch type", p.patchType) - } - p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil, p.applyPatch, p.applyAdmission) - return finishRequest(p.timeout, func() (runtime.Object, error) { - updateObject, _, updateErr := p.restPatcher.Update(ctx, p.name, p.updatedObjectInfo, p.createValidation, p.updateValidation, false, p.options) + updateObject, created, updateErr := p.restPatcher.Update(ctx, p.name, p.updatedObjectInfo, p.createValidation, p.updateValidation, p.forceAllowCreate, options) + wasCreated = created return updateObject, updateErr }) + return result, wasCreated, err } // applyPatchToObject applies a strategic merge patch of to @@ -434,3 +581,13 @@ func interpretStrategicMergePatchError(err error) error { return err } } + +func patchToUpdateOptions(po *metav1.PatchOptions) (*metav1.UpdateOptions, error) { + b, err := json.Marshal(po) + if err != nil { + return nil, err + } + uo := metav1.UpdateOptions{} + err = json.Unmarshal(b, &uo) + return &uo, err +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go index 43b0919452f..744ab6ea1ba 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go @@ -27,8 +27,6 @@ import ( "strings" "time" - "k8s.io/klog" - "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -37,11 +35,12 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/apiserver/pkg/endpoints/metrics" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" - openapiproto "k8s.io/kube-openapi/pkg/util/proto" + "k8s.io/klog" utiltrace "k8s.io/utils/trace" ) @@ -61,7 +60,7 @@ type RequestScope struct { Trace *utiltrace.Trace TableConvertor rest.TableConvertor - OpenAPIModels openapiproto.Models + FieldManager *fieldmanager.FieldManager Resource schema.GroupVersionResource Kind schema.GroupVersionKind diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go index d410bc461f4..04febe03a86 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go @@ -27,7 +27,6 @@ import ( "time" "github.com/evanphx/json-patch" - apiequality "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -39,6 +38,7 @@ import ( "k8s.io/apimachinery/pkg/util/diff" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/strategicpatch" + "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/apis/example" examplev1 "k8s.io/apiserver/pkg/apis/example/v1" "k8s.io/apiserver/pkg/endpoints/request" @@ -149,10 +149,10 @@ func TestJSONPatch(t *testing.T) { }, } { p := &patcher{ - patchType: types.JSONPatchType, - patchJS: []byte(test.patch), + patchType: types.JSONPatchType, + patchBytes: []byte(test.patch), } - jp := jsonPatcher{p} + jp := jsonPatcher{patcher: p} codec := codecs.LegacyCodec(examplev1.SchemeGroupVersion) pod := &examplev1.Pod{} pod.Name = "podA" @@ -454,12 +454,12 @@ func (tc *patchTestCase) Run(t *testing.T) { restPatcher: testPatcher, name: name, patchType: patchType, - patchJS: patch, + patchBytes: patch, trace: utiltrace.New("Patch" + name), } - resultObj, err := p.patchResource(ctx) + resultObj, _, err := p.patchResource(ctx, RequestScope{}) if len(tc.expectedError) != 0 { if err == nil || err.Error() != tc.expectedError { t.Errorf("%s: expected error %v, but got %v", tc.name, tc.expectedError, err) @@ -795,6 +795,9 @@ func TestPatchWithVersionConflictThenAdmissionFailure(t *testing.T) { tc.Run(t) } +// TODO: Add test case for "apply with existing uid" verify it gives a conflict error, +// not a creation or an authz creation forbidden message + func TestHasUID(t *testing.T) { testcases := []struct { obj runtime.Object @@ -939,3 +942,11 @@ func setTcPod(tcPod *example.Pod, name string, namespace string, uid types.UID, tcPod.Spec.NodeName = nodeName } } + +func (f mutateObjectUpdateFunc) Handles(operation admission.Operation) bool { + return true +} + +func (f mutateObjectUpdateFunc) Admit(a admission.Attributes) (err error) { + return f(a.GetObject(), a.GetOldObject()) +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index 9080b9b42b3..e78346c5b0a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -121,7 +121,15 @@ func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interfac } userInfo, _ := request.UserFrom(ctx) - var transformers []rest.TransformFunc + 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 { + return nil, fmt.Errorf("failed to update object managed fields: %v", err) + } + return obj, nil + }) + } if mutatingAdmission, ok := admit.(admission.MutationInterface); ok { transformers = append(transformers, func(ctx context.Context, newObj, oldObj runtime.Object) (runtime.Object, error) { isNotZeroObject, err := hasUID(oldObj) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/watch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/watch.go old mode 100755 new mode 100644 diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index 89f1caa97d5..4b9650b5575 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -27,7 +27,6 @@ import ( "unicode" restful "github.com/emicklei/go-restful" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/runtime" @@ -35,10 +34,13 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/endpoints/handlers" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/registry/rest" genericfilters "k8s.io/apiserver/pkg/server/filters" + utilfeature "k8s.io/apiserver/pkg/util/feature" ) const ( @@ -264,6 +266,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag if err != nil { return nil, err } + versionedPatchOptions, err := a.group.Creater.New(optionsExternalVersion.WithKind("PatchOptions")) + if err != nil { + return nil, err + } versionedUpdateOptions, err := a.group.Creater.New(optionsExternalVersion.WithKind("UpdateOptions")) if err != nil { return nil, err @@ -511,7 +517,19 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag if a.group.MetaGroupVersion != nil { reqScope.MetaGroupVersion = *a.group.MetaGroupVersion } - reqScope.OpenAPIModels = a.group.OpenAPIModels + if a.group.OpenAPIModels != nil && utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) { + fm, err := fieldmanager.NewFieldManager( + a.group.OpenAPIModels, + a.group.UnsafeConvertor, + a.group.Defaulter, + fqKindToRegister.GroupVersion(), + reqScope.HubGroupVersion, + ) + if err != nil { + return nil, fmt.Errorf("failed to create field manager: %v", err) + } + reqScope.FieldManager = fm + } for _, action := range actions { producedObject := storageMeta.ProducesObject(action.Verb) if producedObject == nil { @@ -671,17 +689,20 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag string(types.MergePatchType), string(types.StrategicMergePatchType), } + if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) { + supportedTypes = append(supportedTypes, string(types.ApplyPatchType)) + } handler := metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, restfulPatchResource(patcher, reqScope, admit, supportedTypes)) route := ws.PATCH(action.Path).To(handler). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). - Consumes(string(types.JSONPatchType), string(types.MergePatchType), string(types.StrategicMergePatchType)). + Consumes(supportedTypes...). Operation("patch"+namespaced+kind+strings.Title(subresource)+operationSuffix). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Returns(http.StatusOK, "OK", producedObject). Reads(metav1.Patch{}). Writes(producedObject) - if err := addObjectParams(ws, route, versionedUpdateOptions); err != nil { + if err := addObjectParams(ws, route, versionedPatchOptions); err != nil { return nil, err } addParams(route, action.Params) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/patchhandler_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/patchhandler_test.go index 6d0475c773d..a630fd4e058 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/patchhandler_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/patchhandler_test.go @@ -25,7 +25,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" genericapitesting "k8s.io/apiserver/pkg/endpoints/testing" + genericfeatures "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/registry/rest" + utilfeature "k8s.io/apiserver/pkg/util/feature" + utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing" ) func TestPatch(t *testing.T) { @@ -69,6 +72,58 @@ func TestPatch(t *testing.T) { } } +func TestForbiddenForceOnNonApply(t *testing.T) { + storage := map[string]rest.Storage{} + ID := "id" + item := &genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{ + Name: ID, + Namespace: "", // update should allow the client to send an empty namespace + UID: "uid", + }, + Other: "bar", + } + simpleStorage := SimpleRESTStorage{item: *item} + storage["simple"] = &simpleStorage + selfLinker := &setTestSelfLinker{ + t: t, + expectedSet: "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/" + ID, + name: ID, + namespace: metav1.NamespaceDefault, + } + handler := handleLinker(storage, selfLinker) + server := httptest.NewServer(handler) + defer server.Close() + + client := http.Client{} + request, err := http.NewRequest("PATCH", server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/"+ID, bytes.NewReader([]byte(`{"labels":{"foo":"bar"}}`))) + request.Header.Set("Content-Type", "application/merge-patch+json; charset=UTF-8") + _, err = client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + request, err = http.NewRequest("PATCH", server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/"+ID+"?force=true", bytes.NewReader([]byte(`{"labels":{"foo":"bar"}}`))) + request.Header.Set("Content-Type", "application/merge-patch+json; charset=UTF-8") + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusUnprocessableEntity { + t.Errorf("Unexpected response %#v", response) + } + + request, err = http.NewRequest("PATCH", server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/"+ID+"?force=false", bytes.NewReader([]byte(`{"labels":{"foo":"bar"}}`))) + request.Header.Set("Content-Type", "application/merge-patch+json; charset=UTF-8") + response, err = client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusUnprocessableEntity { + t.Errorf("Unexpected response %#v", response) + } +} + func TestPatchRequiresMatchingName(t *testing.T) { storage := map[string]rest.Storage{} ID := "id" @@ -97,3 +152,124 @@ func TestPatchRequiresMatchingName(t *testing.T) { t.Errorf("Unexpected response %#v", response) } } + +func TestPatchApply(t *testing.T) { + t.Skip("apply is being refactored") + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() + storage := map[string]rest.Storage{} + item := &genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{ + Name: "id", + Namespace: "", + UID: "uid", + }, + Other: "bar", + } + simpleStorage := SimpleRESTStorage{item: *item} + storage["simple"] = &simpleStorage + handler := handle(storage) + server := httptest.NewServer(handler) + defer server.Close() + + client := http.Client{} + request, err := http.NewRequest( + "PATCH", + server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/id", + bytes.NewReader([]byte(`{"metadata":{"name":"id"}, "labels": {"test": "yes"}}`)), + ) + request.Header.Set("Content-Type", "application/apply-patch+yaml") + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusOK { + t.Errorf("Unexpected response %#v", response) + } + if simpleStorage.updated.Labels["test"] != "yes" { + t.Errorf(`Expected labels to have "test": "yes", found %q`, simpleStorage.updated.Labels["test"]) + } + 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 { + t.Errorf(`Expected managedFields field to be set, but is empty`) + } +} + +func TestApplyAddsGVK(t *testing.T) { + t.Skip("apply is being refactored") + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() + storage := map[string]rest.Storage{} + item := &genericapitesting.Simple{ + ObjectMeta: metav1.ObjectMeta{ + Name: "id", + Namespace: "", + UID: "uid", + }, + Other: "bar", + } + simpleStorage := SimpleRESTStorage{item: *item} + storage["simple"] = &simpleStorage + handler := handle(storage) + server := httptest.NewServer(handler) + defer server.Close() + + client := http.Client{} + request, err := http.NewRequest( + "PATCH", + server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/id", + bytes.NewReader([]byte(`{"metadata":{"name":"id"}, "labels": {"test": "yes"}}`)), + ) + request.Header.Set("Content-Type", "application/apply-patch+yaml") + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusOK { + t.Errorf("Unexpected response %#v", response) + } + // 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 { + t.Errorf( + `Expected managedFields field to be %q, got %q`, + expected, + simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion, + ) + } +} + +func TestApplyCreatesWithManagedFields(t *testing.T) { + t.Skip("apply is being refactored") + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() + storage := map[string]rest.Storage{} + simpleStorage := SimpleRESTStorage{} + storage["simple"] = &simpleStorage + handler := handle(storage) + server := httptest.NewServer(handler) + defer server.Close() + + client := http.Client{} + request, err := http.NewRequest( + "PATCH", + server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/id", + bytes.NewReader([]byte(`{"metadata":{"name":"id"}, "labels": {"test": "yes"}}`)), + ) + request.Header.Set("Content-Type", "application/apply-patch+yaml") + response, err := client.Do(request) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if response.StatusCode != http.StatusOK { + t.Errorf("Unexpected response %#v", response) + } + // 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 { + t.Errorf( + `Expected managedFields field to be %q, got %q`, + expected, + simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion, + ) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go index ff4aa704449..966e91ac97f 100644 --- a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go +++ b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go @@ -82,6 +82,12 @@ const ( // validation, merging, mutation can be tested without // committing. DryRun utilfeature.Feature = "DryRun" + + // owner: @apelisse, @lavalamp + // alpha: v1.11 + // + // Server-side apply. Merging happens on the server. + ServerSideApply utilfeature.Feature = "ServerSideApply" ) func init() { @@ -99,4 +105,5 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS APIResponseCompression: {Default: false, PreRelease: utilfeature.Alpha}, APIListChunking: {Default: true, PreRelease: utilfeature.Beta}, DryRun: {Default: true, PreRelease: utilfeature.Beta}, + ServerSideApply: {Default: false, PreRelease: utilfeature.Alpha}, } diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go index 8e69cb76b65..7d0c4a1834a 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go @@ -28,7 +28,9 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/storage/names" + utilfeature "k8s.io/apiserver/pkg/util/feature" ) // RESTCreateStrategy defines the minimum validation, accepted input, and @@ -92,6 +94,12 @@ func BeforeCreate(strategy RESTCreateStrategy, ctx context.Context, obj runtime. // Initializers are a deprecated alpha field and should not be saved objectMeta.SetInitializers(nil) + + // Ensure managedFields is not set unless the feature is enabled + if !utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) { + objectMeta.SetManagedFields(nil) + } + // ClusterName is ignored and should not be saved if len(objectMeta.GetClusterName()) > 0 { objectMeta.SetClusterName("") diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go index 048c35fa4ef..290b016f427 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go @@ -28,6 +28,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" ) // RESTUpdateStrategy defines the minimum validation, accepted input, and @@ -106,6 +108,12 @@ func BeforeUpdate(strategy RESTUpdateStrategy, ctx context.Context, obj, old run oldMeta.SetInitializers(nil) objectMeta.SetInitializers(nil) + // Ensure managedFields state is removed unless ServerSideApply is enabled + if !utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) { + oldMeta.SetManagedFields(nil) + objectMeta.SetManagedFields(nil) + } + strategy.PrepareForUpdate(ctx, obj, old) // ClusterName is ignored and should not be saved diff --git a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go index da3c6c04584..3974bf9b5e9 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go +++ b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go @@ -324,11 +324,7 @@ func (s preparedGenericAPIServer) NonBlockingRun(stopCh <-chan struct{}) error { } // installAPIResources is a private method for installing the REST storage backing each api groupversionresource -func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo) error { - openAPIGroupModels, err := s.getOpenAPIModelsForGroup(apiPrefix, apiGroupInfo) - if err != nil { - return fmt.Errorf("unable to get openapi models for group %v: %v", apiPrefix, err) - } +func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, openAPIModels openapiproto.Models) error { for _, groupVersion := range apiGroupInfo.PrioritizedVersions { if len(apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version]) == 0 { klog.Warningf("Skipping API %v because it has no resources.", groupVersion) @@ -339,7 +335,7 @@ func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *A if apiGroupInfo.OptionsExternalVersion != nil { apiGroupVersion.OptionsExternalVersion = apiGroupInfo.OptionsExternalVersion } - apiGroupVersion.OpenAPIModels = openAPIGroupModels + apiGroupVersion.OpenAPIModels = openAPIModels if err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer); err != nil { return fmt.Errorf("unable to setup API %v: %v", apiGroupInfo, err) @@ -353,7 +349,13 @@ func (s *GenericAPIServer) InstallLegacyAPIGroup(apiPrefix string, apiGroupInfo if !s.legacyAPIGroupPrefixes.Has(apiPrefix) { return fmt.Errorf("%q is not in the allowed legacy API prefixes: %v", apiPrefix, s.legacyAPIGroupPrefixes.List()) } - if err := s.installAPIResources(apiPrefix, apiGroupInfo); err != nil { + + openAPIModels, err := s.getOpenAPIModels(apiPrefix, apiGroupInfo) + if err != nil { + return fmt.Errorf("unable to get openapi models: %v", err) + } + + if err := s.installAPIResources(apiPrefix, apiGroupInfo, openAPIModels); err != nil { return err } @@ -364,49 +366,62 @@ func (s *GenericAPIServer) InstallLegacyAPIGroup(apiPrefix string, apiGroupInfo return nil } +// Exposes given api groups in the API. +func (s *GenericAPIServer) InstallAPIGroups(apiGroupInfos ...*APIGroupInfo) error { + for _, apiGroupInfo := range apiGroupInfos { + // Do not register empty group or empty version. Doing so claims /apis/ for the wrong entity to be returned. + // Catching these here places the error much closer to its origin + if len(apiGroupInfo.PrioritizedVersions[0].Group) == 0 { + return fmt.Errorf("cannot register handler with an empty group for %#v", *apiGroupInfo) + } + if len(apiGroupInfo.PrioritizedVersions[0].Version) == 0 { + return fmt.Errorf("cannot register handler with an empty version for %#v", *apiGroupInfo) + } + } + + openAPIModels, err := s.getOpenAPIModels(APIGroupPrefix, apiGroupInfos...) + if err != nil { + return fmt.Errorf("unable to get openapi models: %v", err) + } + + for _, apiGroupInfo := range apiGroupInfos { + if err := s.installAPIResources(APIGroupPrefix, apiGroupInfo, openAPIModels); err != nil { + return fmt.Errorf("unable to install api resources: %v", err) + } + + // setup discovery + // Install the version handler. + // Add a handler at /apis/ to enumerate all versions supported by this group. + apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{} + for _, groupVersion := range apiGroupInfo.PrioritizedVersions { + // Check the config to make sure that we elide versions that don't have any resources + if len(apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version]) == 0 { + continue + } + apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{ + GroupVersion: groupVersion.String(), + Version: groupVersion.Version, + }) + } + preferredVersionForDiscovery := metav1.GroupVersionForDiscovery{ + GroupVersion: apiGroupInfo.PrioritizedVersions[0].String(), + Version: apiGroupInfo.PrioritizedVersions[0].Version, + } + apiGroup := metav1.APIGroup{ + Name: apiGroupInfo.PrioritizedVersions[0].Group, + Versions: apiVersionsForDiscovery, + PreferredVersion: preferredVersionForDiscovery, + } + + s.DiscoveryGroupManager.AddGroup(apiGroup) + s.Handler.GoRestfulContainer.Add(discovery.NewAPIGroupHandler(s.Serializer, apiGroup).WebService()) + } + return nil +} + // Exposes the given api group in the API. func (s *GenericAPIServer) InstallAPIGroup(apiGroupInfo *APIGroupInfo) error { - // Do not register empty group or empty version. Doing so claims /apis/ for the wrong entity to be returned. - // Catching these here places the error much closer to its origin - if len(apiGroupInfo.PrioritizedVersions[0].Group) == 0 { - return fmt.Errorf("cannot register handler with an empty group for %#v", *apiGroupInfo) - } - if len(apiGroupInfo.PrioritizedVersions[0].Version) == 0 { - return fmt.Errorf("cannot register handler with an empty version for %#v", *apiGroupInfo) - } - - if err := s.installAPIResources(APIGroupPrefix, apiGroupInfo); err != nil { - return err - } - - // setup discovery - // Install the version handler. - // Add a handler at /apis/ to enumerate all versions supported by this group. - apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{} - for _, groupVersion := range apiGroupInfo.PrioritizedVersions { - // Check the config to make sure that we elide versions that don't have any resources - if len(apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version]) == 0 { - continue - } - apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{ - GroupVersion: groupVersion.String(), - Version: groupVersion.Version, - }) - } - preferredVersionForDiscovery := metav1.GroupVersionForDiscovery{ - GroupVersion: apiGroupInfo.PrioritizedVersions[0].String(), - Version: apiGroupInfo.PrioritizedVersions[0].Version, - } - apiGroup := metav1.APIGroup{ - Name: apiGroupInfo.PrioritizedVersions[0].Group, - Versions: apiVersionsForDiscovery, - PreferredVersion: preferredVersionForDiscovery, - } - - s.DiscoveryGroupManager.AddGroup(apiGroup) - s.Handler.GoRestfulContainer.Add(discovery.NewAPIGroupHandler(s.Serializer, apiGroup).WebService()) - - return nil + return s.InstallAPIGroups(apiGroupInfo) } func (s *GenericAPIServer) getAPIGroupVersion(apiGroupInfo *APIGroupInfo, groupVersion schema.GroupVersion, apiPrefix string) *genericapi.APIGroupVersion { @@ -455,12 +470,31 @@ func NewDefaultAPIGroupInfo(group string, scheme *runtime.Scheme, parameterCodec } } -// getOpenAPIModelsForGroup is a private method for getting the OpenAPI Schemas for each api group -func (s *GenericAPIServer) getOpenAPIModelsForGroup(apiPrefix string, apiGroupInfo *APIGroupInfo) (openapiproto.Models, error) { +// getOpenAPIModels is a private method for getting the OpenAPI models +func (s *GenericAPIServer) getOpenAPIModels(apiPrefix string, apiGroupInfos ...*APIGroupInfo) (openapiproto.Models, error) { if s.openAPIConfig == nil { return nil, nil } pathsToIgnore := openapiutil.NewTrie(s.openAPIConfig.IgnorePrefixes) + resourceNames := make([]string, 0) + for _, apiGroupInfo := range apiGroupInfos { + groupResources, err := getResourceNamesForGroup(apiPrefix, apiGroupInfo, pathsToIgnore) + if err != nil { + return nil, err + } + resourceNames = append(resourceNames, groupResources...) + } + + // Build the openapi definitions for those resources and convert it to proto models + openAPISpec, err := openapibuilder.BuildOpenAPIDefinitionsForResources(s.openAPIConfig, resourceNames...) + if err != nil { + return nil, err + } + return utilopenapi.ToProtoModels(openAPISpec) +} + +// getResourceNamesForGroup is a private method for getting the canonical names for each resource to build in an api group +func getResourceNamesForGroup(apiPrefix string, apiGroupInfo *APIGroupInfo, pathsToIgnore openapiutil.Trie) ([]string, error) { // Get the canonical names of every resource we need to build in this api group resourceNames := make([]string, 0) for _, groupVersion := range apiGroupInfo.PrioritizedVersions { @@ -481,10 +515,5 @@ func (s *GenericAPIServer) getOpenAPIModelsForGroup(apiPrefix string, apiGroupIn } } - // Build the openapi definitions for those resources and convert it to proto models - openAPISpec, err := openapibuilder.BuildOpenAPIDefinitionsForResources(s.openAPIConfig, resourceNames...) - if err != nil { - return nil, err - } - return utilopenapi.ToProtoModels(openAPISpec) + return resourceNames, nil } diff --git a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/helper.go b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/helper.go index 059d518af22..851351cf166 100644 --- a/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/helper.go +++ b/staging/src/k8s.io/cli-runtime/pkg/genericclioptions/resource/helper.go @@ -138,9 +138,9 @@ func (m *Helper) createResource(c RESTClient, resource, namespace string, obj ru Do(). Get() } -func (m *Helper) Patch(namespace, name string, pt types.PatchType, data []byte, options *metav1.UpdateOptions) (runtime.Object, error) { +func (m *Helper) Patch(namespace, name string, pt types.PatchType, data []byte, options *metav1.PatchOptions) (runtime.Object, error) { if options == nil { - options = &metav1.UpdateOptions{} + options = &metav1.PatchOptions{} } return m.RESTClient.Patch(pt). NamespaceIfScoped(namespace, m.NamespaceScoped). diff --git a/staging/src/k8s.io/client-go/deprecated-dynamic/client.go b/staging/src/k8s.io/client-go/deprecated-dynamic/client.go index 3b8efffab6b..bcbfed706f4 100644 --- a/staging/src/k8s.io/client-go/deprecated-dynamic/client.go +++ b/staging/src/k8s.io/client-go/deprecated-dynamic/client.go @@ -127,5 +127,5 @@ func (s oldResourceShimType) List(opts metav1.ListOptions) (runtime.Object, erro } func (s oldResourceShimType) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) { - return s.ResourceInterface.Patch(name, pt, data, metav1.UpdateOptions{}, s.subresources...) + return s.ResourceInterface.Patch(name, pt, data, metav1.PatchOptions{}, s.subresources...) } diff --git a/staging/src/k8s.io/client-go/dynamic/client_test.go b/staging/src/k8s.io/client-go/dynamic/client_test.go index e74cb832a06..0edf3265f25 100644 --- a/staging/src/k8s.io/client-go/dynamic/client_test.go +++ b/staging/src/k8s.io/client-go/dynamic/client_test.go @@ -638,7 +638,7 @@ func TestPatch(t *testing.T) { } defer srv.Close() - got, err := cl.Resource(resource).Namespace(tc.namespace).Patch(tc.name, types.StrategicMergePatchType, tc.patch, metav1.UpdateOptions{}, tc.subresource...) + got, err := cl.Resource(resource).Namespace(tc.namespace).Patch(tc.name, types.StrategicMergePatchType, tc.patch, metav1.PatchOptions{}, tc.subresource...) if err != nil { t.Errorf("unexpected error when patching %q: %v", tc.name, err) continue diff --git a/staging/src/k8s.io/client-go/dynamic/fake/simple.go b/staging/src/k8s.io/client-go/dynamic/fake/simple.go index 6ce3b9b415b..819b8d9c90c 100644 --- a/staging/src/k8s.io/client-go/dynamic/fake/simple.go +++ b/staging/src/k8s.io/client-go/dynamic/fake/simple.go @@ -333,7 +333,7 @@ func (c *dynamicResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, } // TODO: opts are currently ignored. -func (c *dynamicResourceClient) Patch(name string, pt types.PatchType, data []byte, opts metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) { +func (c *dynamicResourceClient) Patch(name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) { var uncastRet runtime.Object var err error switch { diff --git a/staging/src/k8s.io/client-go/dynamic/fake/simple_test.go b/staging/src/k8s.io/client-go/dynamic/fake/simple_test.go index f099195646e..183420fcd01 100644 --- a/staging/src/k8s.io/client-go/dynamic/fake/simple_test.go +++ b/staging/src/k8s.io/client-go/dynamic/fake/simple_test.go @@ -96,7 +96,7 @@ func (tc *patchTestCase) runner(t *testing.T) { client := NewSimpleDynamicClient(runtime.NewScheme(), tc.object) resourceInterface := client.Resource(schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}).Namespace(testNamespace) - got, recErr := resourceInterface.Patch(testName, tc.patchType, tc.patchBytes, metav1.UpdateOptions{}) + got, recErr := resourceInterface.Patch(testName, tc.patchType, tc.patchBytes, metav1.PatchOptions{}) if err := tc.verifyErr(recErr); err != nil { t.Error(err) diff --git a/staging/src/k8s.io/client-go/dynamic/interface.go b/staging/src/k8s.io/client-go/dynamic/interface.go index c457be1780b..70756a4f588 100644 --- a/staging/src/k8s.io/client-go/dynamic/interface.go +++ b/staging/src/k8s.io/client-go/dynamic/interface.go @@ -37,7 +37,7 @@ type ResourceInterface interface { Get(name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) List(opts metav1.ListOptions) (*unstructured.UnstructuredList, error) Watch(opts metav1.ListOptions) (watch.Interface, error) - Patch(name string, pt types.PatchType, data []byte, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) + Patch(name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) } type NamespaceableResourceInterface interface { diff --git a/staging/src/k8s.io/client-go/dynamic/simple.go b/staging/src/k8s.io/client-go/dynamic/simple.go index 9e21cda6e37..852f0c5120a 100644 --- a/staging/src/k8s.io/client-go/dynamic/simple.go +++ b/staging/src/k8s.io/client-go/dynamic/simple.go @@ -283,7 +283,7 @@ func (c *dynamicResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, WatchWithSpecificDecoders(wrappedDecoderFn, unstructured.UnstructuredJSONScheme) } -func (c *dynamicResourceClient) Patch(name string, pt types.PatchType, data []byte, opts metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) { +func (c *dynamicResourceClient) Patch(name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) { result := c.client.client. Patch(pt). AbsPath(append(c.makeURLSegments(name), subresources...)...). diff --git a/staging/src/k8s.io/cluster-bootstrap/Godeps/Godeps.json b/staging/src/k8s.io/cluster-bootstrap/Godeps/Godeps.json index 4c402fdc979..e200cf466a5 100644 --- a/staging/src/k8s.io/cluster-bootstrap/Godeps/Godeps.json +++ b/staging/src/k8s.io/cluster-bootstrap/Godeps/Godeps.json @@ -16,7 +16,7 @@ }, { "ImportPath": "github.com/google/gofuzz", - "Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c" + "Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1" }, { "ImportPath": "golang.org/x/net/http2", diff --git a/staging/src/k8s.io/component-base/Godeps/Godeps.json b/staging/src/k8s.io/component-base/Godeps/Godeps.json index 9ad4bcc8e77..30dabce53ad 100644 --- a/staging/src/k8s.io/component-base/Godeps/Godeps.json +++ b/staging/src/k8s.io/component-base/Godeps/Godeps.json @@ -16,7 +16,7 @@ }, { "ImportPath": "github.com/google/gofuzz", - "Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c" + "Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1" }, { "ImportPath": "github.com/spf13/pflag", diff --git a/staging/src/k8s.io/csi-translation-lib/Godeps/Godeps.json b/staging/src/k8s.io/csi-translation-lib/Godeps/Godeps.json index 22fc5282f46..e3385165391 100644 --- a/staging/src/k8s.io/csi-translation-lib/Godeps/Godeps.json +++ b/staging/src/k8s.io/csi-translation-lib/Godeps/Godeps.json @@ -16,7 +16,7 @@ }, { "ImportPath": "github.com/google/gofuzz", - "Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c" + "Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1" }, { "ImportPath": "golang.org/x/net/http2", diff --git a/staging/src/k8s.io/kube-controller-manager/Godeps/Godeps.json b/staging/src/k8s.io/kube-controller-manager/Godeps/Godeps.json index fd72952019d..6be6fb20f1b 100644 --- a/staging/src/k8s.io/kube-controller-manager/Godeps/Godeps.json +++ b/staging/src/k8s.io/kube-controller-manager/Godeps/Godeps.json @@ -16,7 +16,7 @@ }, { "ImportPath": "github.com/google/gofuzz", - "Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c" + "Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1" }, { "ImportPath": "golang.org/x/net/http2", diff --git a/staging/src/k8s.io/kube-proxy/Godeps/Godeps.json b/staging/src/k8s.io/kube-proxy/Godeps/Godeps.json index 538d561a2b3..0646f99f054 100644 --- a/staging/src/k8s.io/kube-proxy/Godeps/Godeps.json +++ b/staging/src/k8s.io/kube-proxy/Godeps/Godeps.json @@ -16,7 +16,7 @@ }, { "ImportPath": "github.com/google/gofuzz", - "Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c" + "Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1" }, { "ImportPath": "golang.org/x/net/http2", diff --git a/staging/src/k8s.io/kube-scheduler/Godeps/Godeps.json b/staging/src/k8s.io/kube-scheduler/Godeps/Godeps.json index 5e62cd9764d..d94682be86a 100644 --- a/staging/src/k8s.io/kube-scheduler/Godeps/Godeps.json +++ b/staging/src/k8s.io/kube-scheduler/Godeps/Godeps.json @@ -16,7 +16,7 @@ }, { "ImportPath": "github.com/google/gofuzz", - "Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c" + "Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1" }, { "ImportPath": "golang.org/x/net/http2", diff --git a/staging/src/k8s.io/kubelet/Godeps/Godeps.json b/staging/src/k8s.io/kubelet/Godeps/Godeps.json index e2933e6472b..66be2bfe7bb 100644 --- a/staging/src/k8s.io/kubelet/Godeps/Godeps.json +++ b/staging/src/k8s.io/kubelet/Godeps/Godeps.json @@ -16,7 +16,7 @@ }, { "ImportPath": "github.com/google/gofuzz", - "Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c" + "Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1" }, { "ImportPath": "golang.org/x/net/http2", diff --git a/test/integration/apiserver/BUILD b/test/integration/apiserver/BUILD index bf404be616f..e1afd14fee9 100644 --- a/test/integration/apiserver/BUILD +++ b/test/integration/apiserver/BUILD @@ -68,6 +68,9 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//test/integration/apiserver/apply:all-srcs", + ], tags = ["automanaged"], ) diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 9ecb123f920..0086dadddd5 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -56,6 +56,7 @@ func setupWithResources(t *testing.T, groupVersions []schema.GroupVersion, resou resourceConfig.EnableResources(resources...) masterConfig.ExtraConfig.APIResourceConfigSource = resourceConfig } + masterConfig.GenericConfig.OpenAPIConfig = framework.DefaultOpenAPIConfig() _, s, closeFn := framework.RunAMaster(masterConfig) clientSet, err := clientset.NewForConfig(&restclient.Config{Host: s.URL}) diff --git a/test/integration/apiserver/apply/BUILD b/test/integration/apiserver/apply/BUILD new file mode 100644 index 00000000000..d5633a713d1 --- /dev/null +++ b/test/integration/apiserver/apply/BUILD @@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +go_test( + name = "go_default_test", + srcs = [ + "apply_test.go", + "main_test.go", + ], + deps = [ + "//pkg/master:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/api/errors: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", + "//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/kubernetes:go_default_library", + "//staging/src/k8s.io/client-go/rest:go_default_library", + "//test/integration/framework:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/test/integration/apiserver/apply/apply_test.go b/test/integration/apiserver/apply/apply_test.go new file mode 100644 index 00000000000..4bbbaf59804 --- /dev/null +++ b/test/integration/apiserver/apply/apply_test.go @@ -0,0 +1,206 @@ +/* +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 apiserver + +import ( + "net/http/httptest" + "testing" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "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" + clientset "k8s.io/client-go/kubernetes" + restclient "k8s.io/client-go/rest" + "k8s.io/kubernetes/pkg/master" + "k8s.io/kubernetes/test/integration/framework" +) + +func setup(t *testing.T, groupVersions ...schema.GroupVersion) (*httptest.Server, clientset.Interface, framework.CloseFunc) { + masterConfig := framework.NewIntegrationTestMasterConfig() + if len(groupVersions) > 0 { + resourceConfig := master.DefaultAPIResourceConfigSource() + resourceConfig.EnableVersions(groupVersions...) + masterConfig.ExtraConfig.APIResourceConfigSource = resourceConfig + } + masterConfig.GenericConfig.OpenAPIConfig = framework.DefaultOpenAPIConfig() + _, s, closeFn := framework.RunAMaster(masterConfig) + + clientSet, err := clientset.NewForConfig(&restclient.Config{Host: s.URL}) + if err != nil { + t.Fatalf("Error in create clientset: %v", err) + } + return s, clientSet, closeFn +} + +// TestApplyAlsoCreates makes sure that PATCH requests with the apply content type +// will create the object if it doesn't already exist +// TODO: make a set of test cases in an easy-to-consume place (separate package?) so it's easy to test in both integration and e2e. +func TestApplyAlsoCreates(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("pods"). + Name("test-pod"). + Body([]byte(`{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "test-pod" + }, + "spec": { + "containers": [{ + "name": "test-container", + "image": "test-image" + }] + } + }`)). + Do(). + Get() + if err != nil { + t.Fatalf("Failed to create object using Apply patch: %v", err) + } + + _, err = client.CoreV1().RESTClient().Get().Namespace("default").Resource("pods").Name("test-pod").Do().Get() + if err != nil { + t.Fatalf("Failed to retrieve object: %v", err) + } +} + +// TestCreateOnApplyFailsWithUID makes sure that PATCH requests with the apply content type +// will not create the object if it doesn't already exist and it specifies a UID +func TestCreateOnApplyFailsWithUID(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("pods"). + Name("test-pod-uid"). + Body([]byte(`{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "test-pod-uid", + "uid": "88e00824-7f0e-11e8-94a1-c8d3ffb15800" + }, + "spec": { + "containers": [{ + "name": "test-container", + "image": "test-image" + }] + } + }`)). + Do(). + Get() + if !errors.IsConflict(err) { + t.Fatalf("Expected conflict error but got: %v", err) + } +} + +func TestApplyUpdateApplyConflictForced(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() + + _, client, closeFn := setup(t) + defer closeFn() + + obj := []byte(`{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "deployment", + "labels": {"app": "nginx"} + }, + "spec": { + "replicas": 3, + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "nginx:latest" + }] + } + } + } + }`) + + _, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("deployment"). + Body(obj).Do().Get() + if err != nil { + t.Fatalf("Failed to create object using Apply patch: %v", err) + } + + _, err = client.CoreV1().RESTClient().Patch(types.MergePatchType). + AbsPath("/apis/extensions/v1beta1"). + Namespace("default"). + Resource("deployments"). + Name("deployment"). + Body([]byte(`{"spec":{"replicas": 5}}`)).Do().Get() + if err != nil { + t.Fatalf("Failed to patch object: %v", err) + } + + _, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("deployment"). + Body([]byte(obj)).Do().Get() + if err == nil { + t.Fatalf("Expecting to get conflicts when applying object") + } + 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) + } + + _, err = client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + AbsPath("/apis/apps/v1"). + Namespace("default"). + Resource("deployments"). + Name("deployment"). + Param("force", "true"). + Body([]byte(obj)).Do().Get() + if err != nil { + t.Fatalf("Failed to apply object with force: %v", err) + } +} diff --git a/test/integration/apiserver/apply/main_test.go b/test/integration/apiserver/apply/main_test.go new file mode 100644 index 00000000000..268a3588398 --- /dev/null +++ b/test/integration/apiserver/apply/main_test.go @@ -0,0 +1,27 @@ +/* +Copyright 2017 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 apiserver + +import ( + "testing" + + "k8s.io/kubernetes/test/integration/framework" +) + +func TestMain(m *testing.M) { + framework.EtcdMain(m.Run) +} diff --git a/test/integration/apiserver/print_test.go b/test/integration/apiserver/print_test.go index bbf8bfaf09c..7ff1d604751 100644 --- a/test/integration/apiserver/print_test.go +++ b/test/integration/apiserver/print_test.go @@ -62,6 +62,7 @@ var kindWhiteList = sets.NewString( "ListOptions", "CreateOptions", "UpdateOptions", + "PatchOptions", "NodeProxyOptions", "PodAttachOptions", "PodExecOptions", diff --git a/test/integration/auth/BUILD b/test/integration/auth/BUILD index 4a537465fdd..cd8db061f9e 100644 --- a/test/integration/auth/BUILD +++ b/test/integration/auth/BUILD @@ -72,6 +72,7 @@ go_test( "//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library", "//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", "//staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory: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/util/feature:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/feature/testing:go_default_library", diff --git a/test/integration/auth/rbac_test.go b/test/integration/auth/rbac_test.go index effffb12228..963f872c86f 100644 --- a/test/integration/auth/rbac_test.go +++ b/test/integration/auth/rbac_test.go @@ -27,8 +27,6 @@ import ( "testing" "time" - "k8s.io/klog" - apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -37,12 +35,16 @@ import ( "k8s.io/apiserver/pkg/authentication/token/tokenfile" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" + genericfeatures "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/registry/generic" + utilfeature "k8s.io/apiserver/pkg/util/feature" + utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing" externalclientset "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" watchtools "k8s.io/client-go/tools/watch" "k8s.io/client-go/transport" csiclientset "k8s.io/csi-api/pkg/client/clientset/versioned" + "k8s.io/klog" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/api/testapi" api "k8s.io/kubernetes/pkg/apis/core" @@ -292,6 +294,8 @@ var ( ) func TestRBAC(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)() + superUser := "admin/system:masters" tests := []struct { @@ -478,6 +482,40 @@ func TestRBAC(t *testing.T) { {"limitrange-updater", "PUT", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusOK}, }, }, + // { + // bootstrapRoles: bootstrapRoles{ + // clusterRoles: []rbacapi.ClusterRole{ + // { + // ObjectMeta: metav1.ObjectMeta{Name: "allow-all"}, + // Rules: []rbacapi.PolicyRule{ruleAllowAll}, + // }, + // { + // ObjectMeta: metav1.ObjectMeta{Name: "patch-limitranges"}, + // Rules: []rbacapi.PolicyRule{ + // rbacapi.NewRule("patch").Groups("").Resources("limitranges").RuleOrDie(), + // }, + // }, + // }, + // clusterRoleBindings: []rbacapi.ClusterRoleBinding{ + // { + // ObjectMeta: metav1.ObjectMeta{Name: "patch-limitranges"}, + // Subjects: []rbacapi.Subject{ + // {Kind: "User", Name: "limitrange-patcher"}, + // }, + // RoleRef: rbacapi.RoleRef{Kind: "ClusterRole", Name: "patch-limitranges"}, + // }, + // }, + // }, + // requests: []request{ + // // Create the namespace used later in the test + // {superUser, "POST", "", "namespaces", "", "", limitRangeNamespace, http.StatusCreated}, + + // {"limitrange-patcher", "PATCH", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusForbidden}, + // {superUser, "PATCH", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusCreated}, + // {superUser, "PATCH", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusOK}, + // {"limitrange-patcher", "PATCH", "", "limitranges", "limitrange-namespace", "a", aLimitRange, http.StatusOK}, + // }, + // }, } for i, tc := range tests { @@ -494,6 +532,7 @@ func TestRBAC(t *testing.T) { "nonescalating-rolebinding-writer": {Name: "nonescalating-rolebinding-writer"}, "pod-reader": {Name: "pod-reader"}, "limitrange-updater": {Name: "limitrange-updater"}, + "limitrange-patcher": {Name: "limitrange-patcher"}, "user-with-no-permissions": {Name: "user-with-no-permissions"}, })) _, s, closeFn := framework.RunAMaster(masterConfig) @@ -530,6 +569,12 @@ func TestRBAC(t *testing.T) { } req, err := http.NewRequest(r.verb, s.URL+path, body) + // TODO: Un-comment this when Apply works again + // if r.verb == "PATCH" { + // // For patch operations, use the apply content type + // req.Header.Add("Content-Type", string(types.ApplyPatchType)) + // } + if err != nil { t.Fatalf("failed to create request: %v", err) } diff --git a/test/integration/dryrun/dryrun_test.go b/test/integration/dryrun/dryrun_test.go index 99e039d3539..4d3fd130ae3 100644 --- a/test/integration/dryrun/dryrun_test.go +++ b/test/integration/dryrun/dryrun_test.go @@ -58,7 +58,7 @@ func DryRunCreateTest(t *testing.T, rsc dynamic.ResourceInterface, obj *unstruct func DryRunPatchTest(t *testing.T, rsc dynamic.ResourceInterface, name string) { patch := []byte(`{"metadata":{"annotations":{"patch": "true"}}}`) - obj, err := rsc.Patch(name, types.MergePatchType, patch, metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}}) + obj, err := rsc.Patch(name, types.MergePatchType, patch, metav1.PatchOptions{DryRun: []string{metav1.DryRunAll}}) if err != nil { t.Fatalf("failed to dry-run patch object: %v", err) } @@ -97,7 +97,7 @@ func DryRunScalePatchTest(t *testing.T, rsc dynamic.ResourceInterface, name stri replicas := getReplicasOrFail(t, obj) patch := []byte(`{"spec":{"replicas":10}}`) - patchedObj, err := rsc.Patch(name, types.MergePatchType, patch, metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}}, "scale") + patchedObj, err := rsc.Patch(name, types.MergePatchType, patch, metav1.PatchOptions{DryRun: []string{metav1.DryRunAll}}, "scale") if err != nil { t.Fatalf("failed to dry-run patch object: %v", err) } diff --git a/test/integration/framework/BUILD b/test/integration/framework/BUILD index 5c485081ad1..ff697741ba6 100644 --- a/test/integration/framework/BUILD +++ b/test/integration/framework/BUILD @@ -67,6 +67,7 @@ go_library( "//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/github.com/pborman/uuid:go_default_library", "//vendor/k8s.io/klog:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/common:go_default_library", ], ) diff --git a/test/integration/framework/master_utils.go b/test/integration/framework/master_utils.go index 95b7d3d8378..ad376fe4860 100644 --- a/test/integration/framework/master_utils.go +++ b/test/integration/framework/master_utils.go @@ -27,8 +27,6 @@ import ( "github.com/go-openapi/spec" "github.com/pborman/uuid" - "k8s.io/klog" - apps "k8s.io/api/apps/v1beta1" auditreg "k8s.io/api/auditregistration/v1alpha1" autoscaling "k8s.io/api/autoscaling/v1" @@ -55,6 +53,8 @@ import ( "k8s.io/client-go/informers" clientset "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" + "k8s.io/klog" + openapicommon "k8s.io/kube-openapi/pkg/common" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/apis/batch" @@ -109,6 +109,24 @@ func (h *MasterHolder) SetMaster(m *master.Master) { close(h.Initialized) } +func DefaultOpenAPIConfig() *openapicommon.Config { + openAPIConfig := genericapiserver.DefaultOpenAPIConfig(openapi.GetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(legacyscheme.Scheme)) + openAPIConfig.Info = &spec.Info{ + InfoProps: spec.InfoProps{ + Title: "Kubernetes", + Version: "unversioned", + }, + } + openAPIConfig.DefaultResponse = &spec.Response{ + ResponseProps: spec.ResponseProps{ + Description: "Default Response.", + }, + } + openAPIConfig.GetDefinitions = openapi.GetOpenAPIDefinitions + + return openAPIConfig +} + // startMasterOrDie starts a kubernetes master and an httpserver to handle api requests func startMasterOrDie(masterConfig *master.Config, incomingServer *httptest.Server, masterReceiver MasterReceiver) (*master.Master, *httptest.Server, CloseFunc) { var m *master.Master @@ -138,19 +156,7 @@ func startMasterOrDie(masterConfig *master.Config, incomingServer *httptest.Serv if masterConfig == nil { masterConfig = NewMasterConfig() - masterConfig.GenericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(openapi.GetOpenAPIDefinitions, openapinamer.NewDefinitionNamer(legacyscheme.Scheme)) - masterConfig.GenericConfig.OpenAPIConfig.Info = &spec.Info{ - InfoProps: spec.InfoProps{ - Title: "Kubernetes", - Version: "unversioned", - }, - } - masterConfig.GenericConfig.OpenAPIConfig.DefaultResponse = &spec.Response{ - ResponseProps: spec.ResponseProps{ - Description: "Default Response.", - }, - } - masterConfig.GenericConfig.OpenAPIConfig.GetDefinitions = openapi.GetOpenAPIDefinitions + masterConfig.GenericConfig.OpenAPIConfig = DefaultOpenAPIConfig() } // set the loopback client config