Merge pull request #72947 from apelisse/wip-feature-serverside-apply-merge
Merge feature-serverside-apply back in master
This commit is contained in:
26
Godeps/Godeps.json
generated
26
Godeps/Godeps.json
generated
@@ -2095,7 +2095,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/uuid",
|
||||
@@ -4083,6 +4083,10 @@
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/handler",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/schemaconv",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/util",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
@@ -4159,6 +4163,26 @@
|
||||
"ImportPath": "k8s.io/utils/trace",
|
||||
"Rev": "ed37f7428a91fc2a81070808937195dcd46d320e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/merge",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/schema",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/typed",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/value",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/yaml",
|
||||
"Comment": "v1.1.0",
|
||||
|
1255
Godeps/LICENSES
generated
1255
Godeps/LICENSES
generated
File diff suppressed because it is too large
Load Diff
875
api/openapi-spec/swagger.json
generated
875
api/openapi-spec/swagger.json
generated
File diff suppressed because it is too large
Load Diff
@@ -176,6 +176,11 @@ 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:
|
||||
// ObjectMeta is generic and therefore should never have a field with a specific resource's name;
|
||||
// it contains cycles so it's easiest to just skip it.
|
||||
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()...)
|
||||
|
@@ -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.
|
||||
|
@@ -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,11 @@ 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:
|
||||
// ObjectMeta should not have any field with the word "secret" in it;
|
||||
// it contains cycles so it's easiest to just skip it.
|
||||
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()...)
|
||||
|
@@ -340,6 +340,11 @@ 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:
|
||||
// ObjectMeta is generic and therefore should never have a field with a specific resource's name;
|
||||
// it contains cycles so it's easiest to just skip it.
|
||||
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()...)
|
||||
|
@@ -56,7 +56,9 @@ func ValidateSelfSubjectRulesReview(review *authorizationapi.SelfSubjectRulesRev
|
||||
|
||||
func ValidateSubjectAccessReview(sar *authorizationapi.SubjectAccessReview) field.ErrorList {
|
||||
allErrs := ValidateSubjectAccessReviewSpec(sar.Spec, field.NewPath("spec"))
|
||||
if !apiequality.Semantic.DeepEqual(metav1.ObjectMeta{}, sar.ObjectMeta) {
|
||||
objectMetaShallowCopy := sar.ObjectMeta
|
||||
objectMetaShallowCopy.ManagedFields = nil
|
||||
if !apiequality.Semantic.DeepEqual(metav1.ObjectMeta{}, objectMetaShallowCopy) {
|
||||
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata"), sar.ObjectMeta, `must be empty`))
|
||||
}
|
||||
return allErrs
|
||||
@@ -64,7 +66,9 @@ func ValidateSubjectAccessReview(sar *authorizationapi.SubjectAccessReview) fiel
|
||||
|
||||
func ValidateSelfSubjectAccessReview(sar *authorizationapi.SelfSubjectAccessReview) field.ErrorList {
|
||||
allErrs := ValidateSelfSubjectAccessReviewSpec(sar.Spec, field.NewPath("spec"))
|
||||
if !apiequality.Semantic.DeepEqual(metav1.ObjectMeta{}, sar.ObjectMeta) {
|
||||
objectMetaShallowCopy := sar.ObjectMeta
|
||||
objectMetaShallowCopy.ManagedFields = nil
|
||||
if !apiequality.Semantic.DeepEqual(metav1.ObjectMeta{}, objectMetaShallowCopy) {
|
||||
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata"), sar.ObjectMeta, `must be empty`))
|
||||
}
|
||||
return allErrs
|
||||
@@ -75,6 +79,7 @@ func ValidateLocalSubjectAccessReview(sar *authorizationapi.LocalSubjectAccessRe
|
||||
|
||||
objectMetaShallowCopy := sar.ObjectMeta
|
||||
objectMetaShallowCopy.Namespace = ""
|
||||
objectMetaShallowCopy.ManagedFields = nil
|
||||
if !apiequality.Semantic.DeepEqual(metav1.ObjectMeta{}, objectMetaShallowCopy) {
|
||||
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata"), sar.ObjectMeta, `must be empty except for namespace`))
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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}
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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. This is an alpha feature and flag.")
|
||||
cmd.Flags().Bool("force-conflicts", false, "If true, server-side apply will force the changes against conflicts. This is an alpha feature and flag.")
|
||||
}
|
||||
|
||||
func AddIncludeUninitializedFlag(cmd *cobra.Command) {
|
||||
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")
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
2
staging/src/k8s.io/api/Godeps/Godeps.json
generated
2
staging/src/k8s.io/api/Godeps/Godeps.json
generated
@@ -24,7 +24,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/json-iterator/go",
|
||||
|
@@ -4248,6 +4248,9 @@ message ServiceSpec {
|
||||
// More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies
|
||||
// +patchMergeKey=port
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=port
|
||||
// +listMapKey=protocol
|
||||
repeated ServicePort ports = 1;
|
||||
|
||||
// Route service traffic to pods with label keys and values matching this
|
||||
|
@@ -3451,6 +3451,9 @@ type ServiceSpec struct {
|
||||
// More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies
|
||||
// +patchMergeKey=port
|
||||
// +patchStrategy=merge
|
||||
// +listType=map
|
||||
// +listMapKey=port
|
||||
// +listMapKey=protocol
|
||||
Ports []ServicePort `json:"ports,omitempty" patchStrategy:"merge" patchMergeKey:"port" protobuf:"bytes,1,rep,name=ports"`
|
||||
|
||||
// Route service traffic to pods with label keys and values matching this
|
||||
|
@@ -484,7 +484,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/googleapis/gnostic/OpenAPIv2",
|
||||
@@ -1478,6 +1478,14 @@
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/handlers",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/handlers/negotiation",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
@@ -2254,6 +2262,10 @@
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/handler",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/schemaconv",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/util",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
@@ -2402,6 +2414,10 @@
|
||||
"ImportPath": "k8s.io/apiserver/pkg/admission",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/authorization/authorizer",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/discovery",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
@@ -2410,6 +2426,10 @@
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/handlers",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
@@ -2422,6 +2442,10 @@
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/request",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/features",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/registry/generic",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
@@ -2566,6 +2590,26 @@
|
||||
"ImportPath": "k8s.io/utils/trace",
|
||||
"Rev": "ed37f7428a91fc2a81070808937195dcd46d320e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/merge",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/schema",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/typed",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/value",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/yaml",
|
||||
"Rev": "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
|
||||
|
@@ -55,11 +55,14 @@ go_library(
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/version:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/discovery:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/handlers:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/metrics:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/features:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||
|
@@ -21,6 +21,18 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
|
||||
internalinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/status"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -32,20 +44,6 @@ import (
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset"
|
||||
internalinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/status"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition"
|
||||
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
|
||||
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -184,6 +182,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
|
||||
c.ExtraConfig.ServiceResolver,
|
||||
c.ExtraConfig.AuthResolverWrapper,
|
||||
c.ExtraConfig.MasterCount,
|
||||
s.GenericAPIServer.Authorizer,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@@ -28,8 +28,17 @@ import (
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/validate"
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||
"k8s.io/apiextensions-apiserver/pkg/crdserverscheme"
|
||||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
@@ -44,30 +53,22 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||
apirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/client-go/scale"
|
||||
"k8s.io/client-go/scale/scheme/autoscalingv1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
|
||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||
"k8s.io/apiextensions-apiserver/pkg/crdserverscheme"
|
||||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
// crdHandler serves the `/apis` endpoint.
|
||||
@@ -96,6 +97,9 @@ type crdHandler struct {
|
||||
masterCount int
|
||||
|
||||
converterFactory *conversion.CRConverterFactory
|
||||
|
||||
// so that we can do create on update.
|
||||
authorizer authorizer.Authorizer
|
||||
}
|
||||
|
||||
// crdInfo stores enough information to serve the storage for the custom resource
|
||||
@@ -134,7 +138,8 @@ func NewCustomResourceDefinitionHandler(
|
||||
establishingController *establish.EstablishingController,
|
||||
serviceResolver webhook.ServiceResolver,
|
||||
authResolverWrapper webhook.AuthenticationInfoResolverWrapper,
|
||||
masterCount int) (*crdHandler, error) {
|
||||
masterCount int,
|
||||
authorizer authorizer.Authorizer) (*crdHandler, error) {
|
||||
ret := &crdHandler{
|
||||
versionDiscoveryHandler: versionDiscoveryHandler,
|
||||
groupDiscoveryHandler: groupDiscoveryHandler,
|
||||
@@ -145,6 +150,7 @@ func NewCustomResourceDefinitionHandler(
|
||||
admission: admission,
|
||||
establishingController: establishingController,
|
||||
masterCount: masterCount,
|
||||
authorizer: authorizer,
|
||||
}
|
||||
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: ret.updateCustomResourceDefinition,
|
||||
@@ -228,6 +234,9 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
string(types.JSONPatchType),
|
||||
string(types.MergePatchType),
|
||||
}
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
|
||||
supportedTypes = append(supportedTypes, string(types.ApplyPatchType))
|
||||
}
|
||||
|
||||
var handler http.HandlerFunc
|
||||
subresources, err := getSubresourcesForVersion(crd, requestInfo.APIVersion)
|
||||
@@ -565,6 +574,18 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
||||
MetaGroupVersion: metav1.SchemeGroupVersion,
|
||||
|
||||
TableConvertor: storages[v.Name].CustomResource,
|
||||
|
||||
Authorizer: r.authorizer,
|
||||
}
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
|
||||
reqScope := requestScopes[v.Name]
|
||||
reqScope.FieldManager = fieldmanager.NewCRDFieldManager(
|
||||
reqScope.Convertor,
|
||||
reqScope.Defaulter,
|
||||
reqScope.Kind.GroupVersion(),
|
||||
reqScope.HubGroupVersion,
|
||||
)
|
||||
requestScopes[v.Name] = reqScope
|
||||
}
|
||||
|
||||
// override scaleSpec subresource values
|
||||
|
@@ -9,6 +9,7 @@ load(
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"apply_test.go",
|
||||
"basic_test.go",
|
||||
"finalization_test.go",
|
||||
"objectmeta_test.go",
|
||||
@@ -41,6 +42,7 @@ go_test(
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/features:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature/testing:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/dynamic:go_default_library",
|
||||
|
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
func TestApplyBasic(t *testing.T) {
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
|
||||
|
||||
tearDown, config, _, err := fixtures.StartDefaultServer(t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tearDown()
|
||||
|
||||
apiExtensionClient, err := clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dynamicClient, err := dynamic.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
|
||||
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
kind := noxuDefinition.Spec.Names.Kind
|
||||
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
|
||||
|
||||
rest := apiExtensionClient.Discovery().RESTClient()
|
||||
yamlBody := []byte(fmt.Sprintf(`
|
||||
apiVersion: %s
|
||||
kind: %s
|
||||
metadata:
|
||||
name: mytest
|
||||
values:
|
||||
numVal: 1
|
||||
boolVal: true
|
||||
stringVal: "1"`, apiVersion, kind))
|
||||
result, err := rest.Patch(types.ApplyPatchType).
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
|
||||
Name("mytest").
|
||||
Body(yamlBody).
|
||||
DoRaw()
|
||||
if err != nil {
|
||||
t.Fatal(err, string(result))
|
||||
}
|
||||
|
||||
result, err = rest.Patch(types.MergePatchType).
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
|
||||
Name("mytest").
|
||||
Body([]byte(`{"values":{"numVal": 5}}`)).
|
||||
DoRaw()
|
||||
if err != nil {
|
||||
t.Fatal(err, string(result))
|
||||
}
|
||||
|
||||
// Re-apply the same object, we should get conflicts now.
|
||||
result, err = rest.Patch(types.ApplyPatchType).
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
|
||||
Name("mytest").
|
||||
Body(yamlBody).
|
||||
DoRaw()
|
||||
if err == nil {
|
||||
t.Fatalf("Expecting to get conflicts when applying object, got no error: %s", result)
|
||||
}
|
||||
status, ok := err.(*errors.StatusError)
|
||||
if !ok {
|
||||
t.Fatalf("Expecting to get conflicts as API error")
|
||||
}
|
||||
if len(status.Status().Details.Causes) < 1 {
|
||||
t.Fatalf("Expecting to get at least one conflict when applying object, got: %v", status.Status().Details.Causes)
|
||||
}
|
||||
|
||||
// Re-apply with force, should work fine.
|
||||
result, err = rest.Patch(types.ApplyPatchType).
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
|
||||
Name("mytest").
|
||||
Param("force", "true").
|
||||
Body(yamlBody).
|
||||
DoRaw()
|
||||
if err != nil {
|
||||
t.Fatal(err, string(result))
|
||||
}
|
||||
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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
|
||||
|
22
staging/src/k8s.io/apimachinery/Godeps/Godeps.json
generated
22
staging/src/k8s.io/apimachinery/Godeps/Godeps.json
generated
@@ -60,7 +60,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/googleapis/gnostic/OpenAPIv2",
|
||||
@@ -178,6 +178,26 @@
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/merge",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/schema",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/typed",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/value",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/yaml",
|
||||
"Rev": "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
@@ -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{
|
||||
|
@@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@@ -273,6 +273,12 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
|
||||
sort.Slice(j.MatchExpressions, func(a, b int) bool { return j.MatchExpressions[a].Key < j.MatchExpressions[b].Key })
|
||||
}
|
||||
},
|
||||
func(j *metav1.ManagedFieldsEntry, c fuzz.Continue) {
|
||||
c.FuzzNoCustom(j)
|
||||
if j.Fields != nil && len(j.Fields.Map) == 0 {
|
||||
j.Fields = nil
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ go_test(
|
||||
"helpers_test.go",
|
||||
"labels_test.go",
|
||||
"micro_time_test.go",
|
||||
"options_test.go",
|
||||
"time_test.go",
|
||||
"types_test.go",
|
||||
],
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -202,6 +202,23 @@ message ExportOptions {
|
||||
optional bool exact = 2;
|
||||
}
|
||||
|
||||
// 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
|
||||
message Fields {
|
||||
// Map stores a set of fields in a data structure like a Trie.
|
||||
//
|
||||
// Each key is either a '.' representing the field itself, and will always map to an empty set,
|
||||
// or a string representing a sub-field or item. The string will follow one of these four formats:
|
||||
// 'f:<name>', where <name> is the name of a field in a struct, or key in a map
|
||||
// 'v:<value>', where <value> is the exact json formatted value of a list item
|
||||
// 'i:<index>', where <index> is position of a item in a list
|
||||
// 'k:<keys>', where <keys> is a map of a list item's key fields to their unique values
|
||||
// If a key maps to an empty Fields value, the field that key represents is part of the set.
|
||||
//
|
||||
// The exact format is defined in k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal
|
||||
map<string, Fields> map = 1;
|
||||
}
|
||||
|
||||
// GetOptions is the standard query options to the standard REST get call.
|
||||
message GetOptions {
|
||||
// When specified:
|
||||
@@ -436,6 +453,31 @@ message ListOptions {
|
||||
optional string continue = 8;
|
||||
}
|
||||
|
||||
// ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource
|
||||
// that the fieldset applies to.
|
||||
message ManagedFieldsEntry {
|
||||
// Manager is an identifier of the workflow managing these fields.
|
||||
optional string manager = 1;
|
||||
|
||||
// Operation is the type of operation which lead to this ManagedFieldsEntry being created.
|
||||
// The only valid values for this field are 'Apply' and 'Update'.
|
||||
optional string operation = 2;
|
||||
|
||||
// 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.
|
||||
optional string apiVersion = 3;
|
||||
|
||||
// Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply'
|
||||
// +optional
|
||||
optional Time time = 4;
|
||||
|
||||
// Fields identifies a set of fields.
|
||||
// +optional
|
||||
optional Fields fields = 5;
|
||||
}
|
||||
|
||||
// MicroTime is version of Time with microsecond level precision.
|
||||
//
|
||||
// +protobuf.options.marshal=false
|
||||
@@ -617,6 +659,19 @@ message ObjectMeta {
|
||||
// This field is not set anywhere right now and apiserver is going to ignore it if set in create or update request.
|
||||
// +optional
|
||||
optional string clusterName = 15;
|
||||
|
||||
// ManagedFields maps workflow-id and version to the set of fields
|
||||
// that are managed by that workflow. This is mostly for internal
|
||||
// housekeeping, and users typically shouldn't need to set or
|
||||
// understand this field. A workflow can be the user's name, a
|
||||
// controller's name, or the name of a specific apply path like
|
||||
// "ci-cd". The set of fields is always in the version that the
|
||||
// workflow used when modifying the object.
|
||||
//
|
||||
// This field is alpha and can be changed or removed without notice.
|
||||
//
|
||||
// +optional
|
||||
repeated ManagedFieldsEntry managedFields = 17;
|
||||
}
|
||||
|
||||
// OwnerReference contains enough information to let you identify an owning
|
||||
@@ -656,6 +711,24 @@ message OwnerReference {
|
||||
message Patch {
|
||||
}
|
||||
|
||||
// PatchOptions may be provided when patching an API object.
|
||||
// PatchOptions is meant to be a superset of UpdateOptions.
|
||||
message PatchOptions {
|
||||
// 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
|
||||
repeated string dryRun = 1;
|
||||
|
||||
// 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
|
||||
optional bool force = 2;
|
||||
}
|
||||
|
||||
// Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.
|
||||
message Preconditions {
|
||||
// Specifies the target UID.
|
||||
@@ -841,6 +914,7 @@ message TypeMeta {
|
||||
}
|
||||
|
||||
// UpdateOptions may be provided when updating an API object.
|
||||
// All fields in UpdateOptions should also be present in PatchOptions.
|
||||
message UpdateOptions {
|
||||
// When present, indicates that modifications should not be
|
||||
// persisted. An invalid or unrecognized dryRun directive will
|
||||
|
@@ -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{}
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -63,6 +63,8 @@ type Object interface {
|
||||
SetOwnerReferences([]OwnerReference)
|
||||
GetClusterName() string
|
||||
SetClusterName(clusterName string)
|
||||
GetManagedFields() []ManagedFieldsEntry
|
||||
SetManagedFields(managedFields []ManagedFieldsEntry)
|
||||
}
|
||||
|
||||
// ListMetaAccessor retrieves the list interface from an object
|
||||
@@ -166,5 +168,9 @@ func (meta *ObjectMeta) GetOwnerReferences() []OwnerReference { return m
|
||||
func (meta *ObjectMeta) SetOwnerReferences(references []OwnerReference) {
|
||||
meta.OwnerReferences = references
|
||||
}
|
||||
func (meta *ObjectMeta) GetClusterName() string { return meta.ClusterName }
|
||||
func (meta *ObjectMeta) SetClusterName(clusterName string) { meta.ClusterName = clusterName }
|
||||
func (meta *ObjectMeta) GetClusterName() string { return meta.ClusterName }
|
||||
func (meta *ObjectMeta) SetClusterName(clusterName string) { meta.ClusterName = clusterName }
|
||||
func (meta *ObjectMeta) GetManagedFields() []ManagedFieldsEntry { return meta.ManagedFields }
|
||||
func (meta *ObjectMeta) SetManagedFields(managedFields []ManagedFieldsEntry) {
|
||||
meta.ManagedFields = managedFields
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -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.
|
||||
|
@@ -252,6 +252,19 @@ 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 maps workflow-id and version to the set of fields
|
||||
// that are managed by that workflow. This is mostly for internal
|
||||
// housekeeping, and users typically shouldn't need to set or
|
||||
// understand this field. A workflow can be the user's name, a
|
||||
// controller's name, or the name of a specific apply path like
|
||||
// "ci-cd". The set of fields is always in the version that the
|
||||
// workflow used when modifying the object.
|
||||
//
|
||||
// This field is alpha and can be changed or removed without notice.
|
||||
//
|
||||
// +optional
|
||||
ManagedFields []ManagedFieldsEntry `json:"managedFields,omitempty" protobuf:"bytes,17,rep,name=managedFields"`
|
||||
}
|
||||
|
||||
// Initializers tracks the progress of initialization.
|
||||
@@ -494,7 +507,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 +1045,49 @@ const (
|
||||
LabelSelectorOpExists LabelSelectorOperator = "Exists"
|
||||
LabelSelectorOpDoesNotExist LabelSelectorOperator = "DoesNotExist"
|
||||
)
|
||||
|
||||
// ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource
|
||||
// that the fieldset applies to.
|
||||
type ManagedFieldsEntry struct {
|
||||
// Manager is an identifier of the workflow managing these fields.
|
||||
Manager string `json:"manager,omitempty" protobuf:"bytes,1,opt,name=manager"`
|
||||
// Operation is the type of operation which lead to this ManagedFieldsEntry being created.
|
||||
// The only valid values for this field are 'Apply' and 'Update'.
|
||||
Operation ManagedFieldsOperationType `json:"operation,omitempty" protobuf:"bytes,2,opt,name=operation,casttype=ManagedFieldsOperationType"`
|
||||
// APIVersion defines the version of this resource that this field set
|
||||
// applies to. The format is "group/version" just like the top-level
|
||||
// APIVersion field. It is necessary to track the version of a field
|
||||
// set because it cannot be automatically converted.
|
||||
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,3,opt,name=apiVersion"`
|
||||
// Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply'
|
||||
// +optional
|
||||
Time *Time `json:"time,omitempty" protobuf:"bytes,4,opt,name=time"`
|
||||
// Fields identifies a set of fields.
|
||||
// +optional
|
||||
Fields *Fields `json:"fields,omitempty" protobuf:"bytes,5,opt,name=fields,casttype=Fields"`
|
||||
}
|
||||
|
||||
// ManagedFieldsOperationType is the type of operation which lead to a ManagedFieldsEntry being created.
|
||||
type ManagedFieldsOperationType string
|
||||
|
||||
const (
|
||||
ManagedFieldsOperationApply ManagedFieldsOperationType = "Apply"
|
||||
ManagedFieldsOperationUpdate ManagedFieldsOperationType = "Update"
|
||||
)
|
||||
|
||||
// Fields stores a set of fields in a data structure like a Trie.
|
||||
// To understand how this is used, see: https://github.com/kubernetes-sigs/structured-merge-diff
|
||||
type Fields struct {
|
||||
// Map stores a set of fields in a data structure like a Trie.
|
||||
//
|
||||
// Each key is either a '.' representing the field itself, and will always map to an empty set,
|
||||
// or a string representing a sub-field or item. The string will follow one of these four formats:
|
||||
// 'f:<name>', where <name> is the name of a field in a struct, or key in a map
|
||||
// 'v:<value>', where <value> is the exact json formatted value of a list item
|
||||
// 'i:<index>', where <index> is position of a item in a list
|
||||
// 'k:<keys>', where <keys> is a map of a list item's key fields to their unique values
|
||||
// If a key maps to an empty Fields value, the field that key represents is part of the set.
|
||||
//
|
||||
// The exact format is defined in k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal
|
||||
Map map[string]Fields `json:",inline" protobuf:"bytes,1,rep,name=map"`
|
||||
}
|
||||
|
@@ -118,6 +118,14 @@ func (ExportOptions) SwaggerDoc() map[string]string {
|
||||
return map_ExportOptions
|
||||
}
|
||||
|
||||
var map_Fields = map[string]string{
|
||||
"": "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",
|
||||
}
|
||||
|
||||
func (Fields) SwaggerDoc() map[string]string {
|
||||
return map_Fields
|
||||
}
|
||||
|
||||
var map_GetOptions = map[string]string{
|
||||
"": "GetOptions is the standard query options to the standard REST get call.",
|
||||
"resourceVersion": "When specified: - if unset, then the result is returned from remote storage based on quorum-read flag; - if it's 0, then we simply return what we currently have in cache, no guarantee; - if set to non zero, then the result is at least as fresh as given rv.",
|
||||
@@ -213,6 +221,19 @@ func (ListOptions) SwaggerDoc() map[string]string {
|
||||
return map_ListOptions
|
||||
}
|
||||
|
||||
var map_ManagedFieldsEntry = map[string]string{
|
||||
"": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.",
|
||||
"manager": "Manager is an identifier of the workflow managing these fields.",
|
||||
"operation": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.",
|
||||
"apiVersion": "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.",
|
||||
"time": "Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply'",
|
||||
"fields": "Fields identifies a set of fields.",
|
||||
}
|
||||
|
||||
func (ManagedFieldsEntry) SwaggerDoc() map[string]string {
|
||||
return map_ManagedFieldsEntry
|
||||
}
|
||||
|
||||
var map_ObjectMeta = map[string]string{
|
||||
"": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.",
|
||||
"name": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names",
|
||||
@@ -231,6 +252,7 @@ var map_ObjectMeta = map[string]string{
|
||||
"initializers": "An initializer is a controller which enforces some system invariant at object creation time. This field is a list of initializers that have not yet acted on this object. If nil or empty, this object has been completely initialized. Otherwise, the object is considered uninitialized and is hidden (in list/watch and get calls) from clients that haven't explicitly asked to observe uninitialized objects.\n\nWhen an object is created, the system will populate this list with the current set of initializers. Only privileged users may set or modify this list. Once it is empty, it may not be modified further by any user.\n\nDEPRECATED - initializers are an alpha field and will be removed in v1.15.",
|
||||
"finalizers": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed.",
|
||||
"clusterName": "The name of the cluster which the object belongs to. This is used to distinguish resources with same name and namespace in different clusters. This field is not set anywhere right now and apiserver is going to ignore it if set in create or update request.",
|
||||
"managedFields": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.\n\nThis field is alpha and can be changed or removed without notice.",
|
||||
}
|
||||
|
||||
func (ObjectMeta) SwaggerDoc() map[string]string {
|
||||
@@ -259,6 +281,16 @@ func (Patch) SwaggerDoc() map[string]string {
|
||||
return map_Patch
|
||||
}
|
||||
|
||||
var map_PatchOptions = map[string]string{
|
||||
"": "PatchOptions may be provided when patching an API object. PatchOptions is meant to be a superset of UpdateOptions.",
|
||||
"dryRun": "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",
|
||||
"force": "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.",
|
||||
}
|
||||
|
||||
func (PatchOptions) SwaggerDoc() map[string]string {
|
||||
return map_PatchOptions
|
||||
}
|
||||
|
||||
var map_Preconditions = map[string]string{
|
||||
"": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.",
|
||||
"uid": "Specifies the target UID.",
|
||||
@@ -337,7 +369,7 @@ func (TypeMeta) SwaggerDoc() map[string]string {
|
||||
}
|
||||
|
||||
var map_UpdateOptions = map[string]string{
|
||||
"": "UpdateOptions may be provided when updating an API object.",
|
||||
"": "UpdateOptions may be provided when updating an API object. All fields in UpdateOptions should also be present in PatchOptions.",
|
||||
"dryRun": "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",
|
||||
}
|
||||
|
||||
|
@@ -143,13 +143,20 @@ func (u *Unstructured) setNestedField(value interface{}, fields ...string) {
|
||||
SetNestedField(u.Object, value, fields...)
|
||||
}
|
||||
|
||||
func (u *Unstructured) setNestedSlice(value []string, fields ...string) {
|
||||
func (u *Unstructured) setNestedStringSlice(value []string, fields ...string) {
|
||||
if u.Object == nil {
|
||||
u.Object = make(map[string]interface{})
|
||||
}
|
||||
SetNestedStringSlice(u.Object, value, fields...)
|
||||
}
|
||||
|
||||
func (u *Unstructured) setNestedSlice(value []interface{}, fields ...string) {
|
||||
if u.Object == nil {
|
||||
u.Object = make(map[string]interface{})
|
||||
}
|
||||
SetNestedSlice(u.Object, value, fields...)
|
||||
}
|
||||
|
||||
func (u *Unstructured) setNestedMap(value map[string]string, fields ...string) {
|
||||
if u.Object == nil {
|
||||
u.Object = make(map[string]interface{})
|
||||
@@ -436,7 +443,7 @@ func (u *Unstructured) SetFinalizers(finalizers []string) {
|
||||
RemoveNestedField(u.Object, "metadata", "finalizers")
|
||||
return
|
||||
}
|
||||
u.setNestedSlice(finalizers, "metadata", "finalizers")
|
||||
u.setNestedStringSlice(finalizers, "metadata", "finalizers")
|
||||
}
|
||||
|
||||
func (u *Unstructured) GetClusterName() string {
|
||||
@@ -450,3 +457,42 @@ func (u *Unstructured) SetClusterName(clusterName string) {
|
||||
}
|
||||
u.setNestedField(clusterName, "metadata", "clusterName")
|
||||
}
|
||||
|
||||
func (u *Unstructured) GetManagedFields() []metav1.ManagedFieldsEntry {
|
||||
items, found, err := NestedSlice(u.Object, "metadata", "managedFields")
|
||||
if !found || err != nil {
|
||||
return nil
|
||||
}
|
||||
managedFields := []metav1.ManagedFieldsEntry{}
|
||||
for _, item := range items {
|
||||
m, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
utilruntime.HandleError(fmt.Errorf("unable to retrieve managedFields for object, item %v is not a map", item))
|
||||
return nil
|
||||
}
|
||||
out := metav1.ManagedFieldsEntry{}
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(m, &out); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("unable to retrieve managedFields for object: %v", err))
|
||||
return nil
|
||||
}
|
||||
managedFields = append(managedFields, out)
|
||||
}
|
||||
return managedFields
|
||||
}
|
||||
|
||||
func (u *Unstructured) SetManagedFields(managedFields []metav1.ManagedFieldsEntry) {
|
||||
if managedFields == nil {
|
||||
RemoveNestedField(u.Object, "metadata", "managedFields")
|
||||
return
|
||||
}
|
||||
items := []interface{}{}
|
||||
for _, managedFieldsEntry := range managedFields {
|
||||
out, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&managedFieldsEntry)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("unable to set managedFields for object: %v", err))
|
||||
return
|
||||
}
|
||||
items = append(items, out)
|
||||
}
|
||||
u.setNestedSlice(items, "metadata", "managedFields")
|
||||
}
|
||||
|
@@ -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())
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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 {
|
||||
|
@@ -312,6 +312,29 @@ func (in *ExportOptions) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Fields) DeepCopyInto(out *Fields) {
|
||||
*out = *in
|
||||
if in.Map != nil {
|
||||
in, out := &in.Map, &out.Map
|
||||
*out = make(map[string]Fields, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = *val.DeepCopy()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Fields.
|
||||
func (in *Fields) DeepCopy() *Fields {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Fields)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GetOptions) DeepCopyInto(out *GetOptions) {
|
||||
*out = *in
|
||||
@@ -624,6 +647,31 @@ func (in *ListOptions) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ManagedFieldsEntry) DeepCopyInto(out *ManagedFieldsEntry) {
|
||||
*out = *in
|
||||
if in.Time != nil {
|
||||
in, out := &in.Time, &out.Time
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.Fields != nil {
|
||||
in, out := &in.Fields, &out.Fields
|
||||
*out = new(Fields)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedFieldsEntry.
|
||||
func (in *ManagedFieldsEntry) DeepCopy() *ManagedFieldsEntry {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ManagedFieldsEntry)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MicroTime.
|
||||
func (in *MicroTime) DeepCopy() *MicroTime {
|
||||
if in == nil {
|
||||
@@ -678,6 +726,13 @@ func (in *ObjectMeta) DeepCopyInto(out *ObjectMeta) {
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.ManagedFields != nil {
|
||||
in, out := &in.ManagedFields, &out.ManagedFields
|
||||
*out = make([]ManagedFieldsEntry, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -733,6 +788,41 @@ func (in *Patch) DeepCopy() *Patch {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PatchOptions) DeepCopyInto(out *PatchOptions) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.DryRun != nil {
|
||||
in, out := &in.DryRun, &out.DryRun
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Force != nil {
|
||||
in, out := &in.Force, &out.Force
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PatchOptions.
|
||||
func (in *PatchOptions) DeepCopy() *PatchOptions {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PatchOptions)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *PatchOptions) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Preconditions) DeepCopyInto(out *Preconditions) {
|
||||
*out = *in
|
||||
|
@@ -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.
|
||||
|
@@ -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",
|
||||
|
@@ -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)
|
||||
|
@@ -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.
|
||||
|
@@ -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"
|
||||
)
|
||||
|
30
staging/src/k8s.io/apiserver/Godeps/Godeps.json
generated
30
staging/src/k8s.io/apiserver/Godeps/Godeps.json
generated
@@ -448,7 +448,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/googleapis/gnostic/OpenAPIv2",
|
||||
@@ -1954,6 +1954,10 @@
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/handler",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/schemaconv",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/util",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
@@ -1962,6 +1966,10 @@
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/util/proto/testing",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/discovery",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
@@ -2066,6 +2074,26 @@
|
||||
"ImportPath": "k8s.io/utils/trace",
|
||||
"Rev": "ed37f7428a91fc2a81070808937195dcd46d320e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/merge",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/schema",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/typed",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/value",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/yaml",
|
||||
"Rev": "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
|
||||
|
@@ -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",
|
||||
],
|
||||
|
@@ -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,7 +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 +103,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",
|
||||
],
|
||||
|
@@ -20,6 +20,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -133,6 +134,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 (Create for %v): %v", scope.Kind, err), w, req)
|
||||
return
|
||||
}
|
||||
|
||||
obj, err = scope.FieldManager.Update(liveObj, obj, prefixFromUserAgent(req.UserAgent()))
|
||||
if err != nil {
|
||||
scope.err(fmt.Errorf("failed to update object (Create for %v) managed fields: %v", scope.Kind, err), w, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
trace.Step("About to store object in database")
|
||||
result, err := finishRequest(timeout, func() (runtime.Object, error) {
|
||||
return r.Create(
|
||||
@@ -177,3 +192,7 @@ type namedCreaterAdapter struct {
|
||||
func (c *namedCreaterAdapter) Create(ctx context.Context, name string, obj runtime.Object, createValidatingAdmission rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
return c.Creater.Create(ctx, obj, createValidatingAdmission, options)
|
||||
}
|
||||
|
||||
func prefixFromUserAgent(u string) string {
|
||||
return strings.Split(u, "/")[0]
|
||||
}
|
||||
|
@@ -0,0 +1,37 @@
|
||||
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/api/meta:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal:go_default_library",
|
||||
"//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"],
|
||||
)
|
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
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"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/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
|
||||
}
|
||||
|
||||
// NewCRDFieldManager creates a new FieldManager specifically for
|
||||
// CRDs. This doesn't use openapi models (and it doesn't support the
|
||||
// validation field right now).
|
||||
func NewCRDFieldManager(objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion) *FieldManager {
|
||||
return &FieldManager{
|
||||
typeConverter: internal.DeducedTypeConverter{},
|
||||
objectConverter: objectConverter,
|
||||
objectDefaulter: objectDefaulter,
|
||||
groupVersion: gv,
|
||||
hubVersion: hub,
|
||||
updater: merge.Updater{
|
||||
Converter: internal.NewVersionConverter(internal.DeducedTypeConverter{}, objectConverter, hub),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Update is used when the object has already been merged (non-apply
|
||||
// use-case), and simply updates the managed fields in the output
|
||||
// object.
|
||||
func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (runtime.Object, error) {
|
||||
// If the object doesn't have metadata, we should just return without trying to
|
||||
// set the managedFields at all, so creates/updates/patches will work normally.
|
||||
if _, err := meta.Accessor(newObj); err != nil {
|
||||
return newObj, nil
|
||||
}
|
||||
|
||||
// First try to decode the managed fields provided in the update,
|
||||
// This is necessary to allow directly updating managed fields.
|
||||
managed, err := internal.DecodeObjectManagedFields(newObj)
|
||||
|
||||
// If the managed field is empty or we failed to decode it,
|
||||
// let's try the live object. This is to prevent clients who
|
||||
// don't understand managedFields from deleting it accidentally.
|
||||
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)
|
||||
}
|
||||
internal.RemoveObjectManagedFields(liveObjVersioned)
|
||||
internal.RemoveObjectManagedFields(newObjVersioned)
|
||||
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())
|
||||
manager, err = f.buildManagerInfo(manager, metav1.ManagedFieldsOperationUpdate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build manager identifier: %v", err)
|
||||
}
|
||||
|
||||
managed, err = f.updater.Update(liveObjTyped, newObjTyped, apiVersion, managed, manager)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update ManagedFields: %v", err)
|
||||
}
|
||||
|
||||
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) {
|
||||
// If the object doesn't have metadata, apply isn't allowed.
|
||||
if _, err := meta.Accessor(liveObj); err != nil {
|
||||
return nil, fmt.Errorf("couldn't get accessor: %v", err)
|
||||
}
|
||||
|
||||
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.
|
||||
// TODO (jennybuckley): Explicitly checkt that patchObj is in the proper version.
|
||||
liveObjVersioned, err := f.toVersioned(liveObj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert live object to proper version: %v", err)
|
||||
}
|
||||
internal.RemoveObjectManagedFields(liveObjVersioned)
|
||||
|
||||
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)
|
||||
}
|
||||
manager, err := f.buildManagerInfo(applyManager, metav1.ManagedFieldsOperationApply)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build manager identifier: %v", err)
|
||||
}
|
||||
|
||||
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
|
||||
newObjTyped, managed, err := f.updater.Apply(liveObjTyped, patchObjTyped, apiVersion, managed, manager, 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)
|
||||
}
|
||||
|
||||
func (f *FieldManager) buildManagerInfo(prefix string, operation metav1.ManagedFieldsOperationType) (string, error) {
|
||||
managerInfo := metav1.ManagedFieldsEntry{
|
||||
Manager: prefix,
|
||||
Operation: operation,
|
||||
APIVersion: f.groupVersion.String(),
|
||||
Time: &metav1.Time{Time: time.Now().UTC()},
|
||||
}
|
||||
if managerInfo.Manager == "" {
|
||||
managerInfo.Manager = "unknown"
|
||||
}
|
||||
return internal.BuildManagerIdentifier(&managerInfo)
|
||||
}
|
@@ -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"],
|
||||
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"],
|
||||
)
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/structured-merge-diff/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) {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("couldn't get accessor: %v", err))
|
||||
}
|
||||
accessor.SetManagedFields(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 {
|
||||
panic(fmt.Sprintf("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 {
|
||||
panic(fmt.Sprintf("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 []metav1.ManagedFieldsEntry) (managedFields fieldpath.ManagedFields, err error) {
|
||||
managedFields = make(map[string]*fieldpath.VersionedSet, len(encodedManagedFields))
|
||||
for _, encodedVersionedSet := range encodedManagedFields {
|
||||
manager, err := BuildManagerIdentifier(&encodedVersionedSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding manager from %v: %v", encodedVersionedSet, err)
|
||||
}
|
||||
managedFields[manager], err = decodeVersionedSet(&encodedVersionedSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding versioned set from %v: %v", encodedVersionedSet, err)
|
||||
}
|
||||
}
|
||||
return managedFields, nil
|
||||
}
|
||||
|
||||
// BuildManagerIdentifier creates a manager identifier string from a ManagedFieldsEntry
|
||||
func BuildManagerIdentifier(encodedManager *metav1.ManagedFieldsEntry) (manager string, err error) {
|
||||
encodedManagerCopy := *encodedManager
|
||||
|
||||
// Never include the fields in the manager identifier
|
||||
encodedManagerCopy.Fields = nil
|
||||
|
||||
// For appliers, don't include the APIVersion or Time in the manager identifier,
|
||||
// so it will always have the same manager identifier each time it applied.
|
||||
if encodedManager.Operation == metav1.ManagedFieldsOperationApply {
|
||||
encodedManagerCopy.APIVersion = ""
|
||||
encodedManagerCopy.Time = nil
|
||||
}
|
||||
|
||||
// Use the remaining fields to build the manager identifier
|
||||
b, err := json.Marshal(&encodedManagerCopy)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error marshalling manager identifier: %v", err)
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func decodeVersionedSet(encodedVersionedSet *metav1.ManagedFieldsEntry) (versionedSet *fieldpath.VersionedSet, err error) {
|
||||
versionedSet = &fieldpath.VersionedSet{}
|
||||
versionedSet.APIVersion = fieldpath.APIVersion(encodedVersionedSet.APIVersion)
|
||||
|
||||
fields := metav1.Fields{}
|
||||
if encodedVersionedSet.Fields != nil {
|
||||
fields = *encodedVersionedSet.Fields
|
||||
}
|
||||
set, err := FieldsToSet(fields)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding set: %v", err)
|
||||
}
|
||||
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 []metav1.ManagedFieldsEntry, err error) {
|
||||
// Sort the keys so a predictable order will be used.
|
||||
managers := []string{}
|
||||
for manager := range managedFields {
|
||||
managers = append(managers, manager)
|
||||
}
|
||||
sort.Strings(managers)
|
||||
|
||||
encodedManagedFields = []metav1.ManagedFieldsEntry{}
|
||||
for _, manager := range managers {
|
||||
versionedSet := managedFields[manager]
|
||||
v, err := encodeManagerVersionedSet(manager, versionedSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error encoding versioned set for %v: %v", manager, err)
|
||||
}
|
||||
encodedManagedFields = append(encodedManagedFields, *v)
|
||||
}
|
||||
return encodedManagedFields, nil
|
||||
}
|
||||
|
||||
func encodeManagerVersionedSet(manager string, versionedSet *fieldpath.VersionedSet) (encodedVersionedSet *metav1.ManagedFieldsEntry, err error) {
|
||||
encodedVersionedSet = &metav1.ManagedFieldsEntry{}
|
||||
|
||||
// Get as many fields as we can from the manager identifier
|
||||
err = json.Unmarshal([]byte(manager), encodedVersionedSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling manager identifier %v: %v", manager, err)
|
||||
}
|
||||
|
||||
// Get the APIVersion and Fields from the VersionedSet
|
||||
encodedVersionedSet.APIVersion = string(versionedSet.APIVersion)
|
||||
fields, err := SetToFields(*versionedSet.Set)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error encoding set: %v", err)
|
||||
}
|
||||
encodedVersionedSet.Fields = &fields
|
||||
|
||||
return encodedVersionedSet, nil
|
||||
}
|
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
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 wire format
|
||||
// (api format) to the format used by sigs.k8s.io/structured-merge-diff and back
|
||||
func TestRoundTripManagedFields(t *testing.T) {
|
||||
tests := []string{
|
||||
`- apiVersion: v1
|
||||
fields:
|
||||
v:3:
|
||||
f:alsoPi: {}
|
||||
v:3.1415:
|
||||
f:pi: {}
|
||||
v:false:
|
||||
f:notTrue: {}
|
||||
manager: foo
|
||||
operation: Update
|
||||
time: "2001-02-03T04:05:06Z"
|
||||
- apiVersion: v1beta1
|
||||
fields:
|
||||
i:5:
|
||||
f:i: {}
|
||||
manager: foo
|
||||
operation: Update
|
||||
time: "2011-12-13T14:15:16Z"
|
||||
`,
|
||||
`- apiVersion: v1
|
||||
fields:
|
||||
f:spec:
|
||||
f:containers:
|
||||
k:{"name":"c"}:
|
||||
f:image: {}
|
||||
f:name: {}
|
||||
manager: foo
|
||||
operation: Apply
|
||||
`,
|
||||
`- 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: {}
|
||||
manager: foo
|
||||
operation: Update
|
||||
`,
|
||||
`- 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: {}
|
||||
manager: foo
|
||||
operation: Apply
|
||||
`,
|
||||
`- 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: {}
|
||||
manager: foo
|
||||
operation: Update
|
||||
`,
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test, func(t *testing.T) {
|
||||
var unmarshaled []metav1.ManagedFieldsEntry
|
||||
if err := yaml.Unmarshal([]byte(test), &unmarshaled); err != nil {
|
||||
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
|
||||
}
|
||||
decoded, err := decodeManagedFields(unmarshaled)
|
||||
if err != nil {
|
||||
t.Fatalf("did not expect decoding error but got: %v", err)
|
||||
}
|
||||
encoded, err := encodeManagedFields(decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("did not expect encoding error but got: %v", err)
|
||||
}
|
||||
marshaled, err := yaml.Marshal(&encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("did not expect yaml marshalling error but got: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(string(marshaled), test) {
|
||||
t.Fatalf("expected:\n%v\nbut got:\n%v", test, string(marshaled))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildManagerIdentifier(t *testing.T) {
|
||||
tests := []struct {
|
||||
managedFieldsEntry string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
managedFieldsEntry: `
|
||||
apiVersion: v1
|
||||
fields:
|
||||
f:apiVersion: {}
|
||||
manager: foo
|
||||
operation: Update
|
||||
time: "2001-02-03T04:05:06Z"
|
||||
`,
|
||||
expected: "{\"manager\":\"foo\",\"operation\":\"Update\",\"apiVersion\":\"v1\",\"time\":\"2001-02-03T04:05:06Z\"}",
|
||||
},
|
||||
{
|
||||
managedFieldsEntry: `
|
||||
apiVersion: v1
|
||||
fields:
|
||||
f:apiVersion: {}
|
||||
manager: foo
|
||||
operation: Apply
|
||||
time: "2001-02-03T04:05:06Z"
|
||||
`,
|
||||
expected: "{\"manager\":\"foo\",\"operation\":\"Apply\"}",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.managedFieldsEntry, func(t *testing.T) {
|
||||
var unmarshaled metav1.ManagedFieldsEntry
|
||||
if err := yaml.Unmarshal([]byte(test.managedFieldsEntry), &unmarshaled); err != nil {
|
||||
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
|
||||
}
|
||||
decoded, err := BuildManagerIdentifier(&unmarshaled)
|
||||
if err != nil {
|
||||
t.Fatalf("did not expect decoding error but got: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(decoded, test.expected) {
|
||||
t.Fatalf("expected:\n%v\nbut got:\n%v", test.expected, decoded)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -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")
|
||||
}
|
||||
}
|
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
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/kube-openapi/pkg/util/proto"
|
||||
"sigs.k8s.io/structured-merge-diff/typed"
|
||||
"sigs.k8s.io/structured-merge-diff/value"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// TypeConverter allows you to convert from runtime.Object to
|
||||
// typed.TypedValue and the other way around.
|
||||
type TypeConverter interface {
|
||||
ObjectToTyped(runtime.Object) (typed.TypedValue, error)
|
||||
YAMLToTyped([]byte) (typed.TypedValue, error)
|
||||
TypedToObject(typed.TypedValue) (runtime.Object, error)
|
||||
}
|
||||
|
||||
// DeducedTypeConverter is a TypeConverter for CRDs that don't have a
|
||||
// schema. It does implement the same interface though (and create the
|
||||
// same types of objects), so that everything can still work the same.
|
||||
// CRDs are merged with all their fields being "atomic" (lists
|
||||
// included).
|
||||
//
|
||||
// Note that this is not going to be sufficient for converting to/from
|
||||
// CRDs that have a schema defined (we don't support that schema yet).
|
||||
// TODO(jennybuckley): Use the schema provided by a CRD if it exists.
|
||||
type DeducedTypeConverter struct{}
|
||||
|
||||
var _ TypeConverter = DeducedTypeConverter{}
|
||||
|
||||
// ObjectToTyped converts an object into a TypedValue with a "deduced type".
|
||||
func (DeducedTypeConverter) ObjectToTyped(obj runtime.Object) (typed.TypedValue, error) {
|
||||
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return typed.DeducedParseableType{}.FromUnstructured(u)
|
||||
}
|
||||
|
||||
// YAMLToTyped parses a yaml object into a TypedValue with a "deduced type".
|
||||
func (DeducedTypeConverter) YAMLToTyped(from []byte) (typed.TypedValue, error) {
|
||||
return typed.DeducedParseableType{}.FromYAML(typed.YAMLObject(from))
|
||||
}
|
||||
|
||||
// TypedToObject transforms the typed value into a runtime.Object. That
|
||||
// is not specific to deduced type.
|
||||
func (DeducedTypeConverter) TypedToObject(value typed.TypedValue) (runtime.Object, error) {
|
||||
return valueToObject(value.AsValue())
|
||||
}
|
||||
|
||||
type typeConverter struct {
|
||||
parser *gvkParser
|
||||
}
|
||||
|
||||
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) ObjectToTyped(obj runtime.Object) (typed.TypedValue, error) {
|
||||
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
t := c.parser.Type(gvk)
|
||||
if t == nil {
|
||||
return nil, 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 nil, fmt.Errorf("error decoding YAML: %v", err)
|
||||
}
|
||||
|
||||
return c.ObjectToTyped(unstructured)
|
||||
}
|
||||
|
||||
func (c *typeConverter) TypedToObject(value typed.TypedValue) (runtime.Object, error) {
|
||||
return valueToObject(value.AsValue())
|
||||
}
|
||||
|
||||
func valueToObject(value *value.Value) (runtime.Object, error) {
|
||||
vu := value.ToUnstructured(false)
|
||||
u, ok := vu.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert typed to unstructured: want map, got %T", vu)
|
||||
}
|
||||
return &unstructured.Unstructured{Object: u}, nil
|
||||
}
|
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
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"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"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(
|
||||
strings.Repeat(".."+string(filepath.Separator), 9),
|
||||
"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)
|
||||
}
|
||||
|
||||
dtc := internal.DeducedTypeConverter{}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
yaml string
|
||||
}{
|
||||
{
|
||||
name: "apps/v1.Deployment",
|
||||
yaml: `
|
||||
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
|
||||
`,
|
||||
}, {
|
||||
name: "extensions/v1beta1.Deployment",
|
||||
yaml: `
|
||||
apiVersion: extensions/v1beta1
|
||||
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
|
||||
`,
|
||||
}, {
|
||||
name: "v1.Pod",
|
||||
yaml: `
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: nginx-pod
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.15.4
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(fmt.Sprintf("%v ObjectToTyped with TypeConverter", testCase.name), func(t *testing.T) {
|
||||
testObjectToTyped(t, tc, testCase.yaml)
|
||||
})
|
||||
t.Run(fmt.Sprintf("%v YAMLToTyped with TypeConverter", testCase.name), func(t *testing.T) {
|
||||
testYAMLToTyped(t, tc, testCase.yaml)
|
||||
})
|
||||
t.Run(fmt.Sprintf("%v ObjectToTyped with DeducedTypeConverter", testCase.name), func(t *testing.T) {
|
||||
testObjectToTyped(t, dtc, testCase.yaml)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testObjectToTyped(t *testing.T, tc internal.TypeConverter, y string) {
|
||||
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
if err := yaml.Unmarshal([]byte(y), &obj.Object); err != nil {
|
||||
t.Fatalf("Failed to parse yaml object: %v", err)
|
||||
}
|
||||
typed, err := tc.ObjectToTyped(obj)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to convert object to typed: %v", err)
|
||||
}
|
||||
newObj, err := tc.TypedToObject(typed)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to convert typed to object: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(obj, newObj) {
|
||||
t.Errorf(`Round-trip failed:
|
||||
Original object:
|
||||
%#v
|
||||
Final object:
|
||||
%#v`, obj, newObj)
|
||||
}
|
||||
}
|
||||
|
||||
func testYAMLToTyped(t *testing.T, tc internal.TypeConverter, y string) {
|
||||
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
if err := yaml.Unmarshal([]byte(y), &obj.Object); err != nil {
|
||||
t.Fatalf("Failed to parse yaml object: %v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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")
|
||||
}
|
@@ -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,36 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
|
||||
restPatcher: r,
|
||||
name: name,
|
||||
patchType: patchType,
|
||||
patchJS: patchJS,
|
||||
patchBytes: patchBytes,
|
||||
userAgent: req.UserAgent(),
|
||||
|
||||
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 +245,31 @@ 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
|
||||
userAgent string
|
||||
|
||||
trace *utiltrace.Trace
|
||||
|
||||
@@ -241,14 +277,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 +310,24 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.fieldManager != nil {
|
||||
if objToUpdate, err = p.fieldManager.Update(currentObject, objToUpdate, prefixFromUserAgent(p.userAgent)); err != nil {
|
||||
return nil, fmt.Errorf("failed to update object (json PATCH for %v) managed fields: %v", p.kind, 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 +337,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 +349,7 @@ type smpPatcher struct {
|
||||
|
||||
// Schema
|
||||
schemaReferenceObj runtime.Object
|
||||
fieldManager *fieldmanager.FieldManager
|
||||
}
|
||||
|
||||
func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) {
|
||||
@@ -313,22 +363,63 @@ 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, prefixFromUserAgent(p.userAgent)); err != nil {
|
||||
return nil, fmt.Errorf("failed to update object (smp PATCH for %v) managed fields: %v", p.kind, err)
|
||||
}
|
||||
}
|
||||
return newObj, nil
|
||||
}
|
||||
|
||||
// strategicPatchObject applies a strategic merge patch of <patchJS> 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
|
||||
}
|
||||
if p.fieldManager == nil {
|
||||
panic("FieldManager must be installed to run apply")
|
||||
}
|
||||
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 <patchBytes> to
|
||||
// <originalObject> and stores the result in <objToUpdate>.
|
||||
// It additionally returns the map[string]interface{} representation of the
|
||||
// <originalObject> and <patchJS>.
|
||||
// <originalObject> and <patchBytes>.
|
||||
// NOTE: Both <originalObject> and <objToUpdate> 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 +429,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 +441,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 <patchMap> to
|
||||
@@ -434,3 +586,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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
@@ -939,3 +939,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())
|
||||
}
|
||||
|
@@ -121,7 +121,16 @@ 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, newObj, liveObj runtime.Object) (runtime.Object, error) {
|
||||
obj, err := scope.FieldManager.Update(liveObj, newObj, prefixFromUserAgent(req.UserAgent()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update object (Update for %v) managed fields: %v", scope.Kind, 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)
|
||||
|
0
staging/src/k8s.io/apiserver/pkg/endpoints/handlers/watch.go
Executable file → Normal file
0
staging/src/k8s.io/apiserver/pkg/endpoints/handlers/watch.go
Executable file → Normal file
@@ -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)
|
||||
|
@@ -69,6 +69,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"
|
||||
|
@@ -82,6 +82,12 @@ const (
|
||||
// validation, merging, mutation can be tested without
|
||||
// committing.
|
||||
DryRun utilfeature.Feature = "DryRun"
|
||||
|
||||
// owner: @apelisse, @lavalamp
|
||||
// alpha: v1.14
|
||||
//
|
||||
// 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},
|
||||
}
|
||||
|
@@ -48,7 +48,9 @@ go_library(
|
||||
"//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/features:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
@@ -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("")
|
||||
|
@@ -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
|
||||
|
@@ -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/<groupName> 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/<groupName> 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
|
||||
}
|
||||
|
22
staging/src/k8s.io/cli-runtime/Godeps/Godeps.json
generated
22
staging/src/k8s.io/cli-runtime/Godeps/Godeps.json
generated
@@ -48,7 +48,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/googleapis/gnostic/OpenAPIv2",
|
||||
@@ -614,6 +614,26 @@
|
||||
"ImportPath": "k8s.io/utils/integer",
|
||||
"Rev": "ed37f7428a91fc2a81070808937195dcd46d320e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/merge",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/schema",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/typed",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/value",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/yaml",
|
||||
"Rev": "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
|
||||
|
@@ -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).
|
||||
|
22
staging/src/k8s.io/client-go/Godeps/Godeps.json
generated
22
staging/src/k8s.io/client-go/Godeps/Godeps.json
generated
@@ -96,7 +96,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/googleapis/gnostic/OpenAPIv2",
|
||||
@@ -626,6 +626,26 @@
|
||||
"ImportPath": "k8s.io/utils/trace",
|
||||
"Rev": "ed37f7428a91fc2a81070808937195dcd46d320e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/merge",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/schema",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/typed",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/value",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/yaml",
|
||||
"Rev": "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
|
||||
|
@@ -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...)
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
|
@@ -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 {
|
||||
|
@@ -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...)...).
|
||||
|
22
staging/src/k8s.io/cloud-provider/Godeps/Godeps.json
generated
22
staging/src/k8s.io/cloud-provider/Godeps/Godeps.json
generated
@@ -44,7 +44,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/googleapis/gnostic/OpenAPIv2",
|
||||
@@ -990,6 +990,26 @@
|
||||
"ImportPath": "k8s.io/utils/trace",
|
||||
"Rev": "ed37f7428a91fc2a81070808937195dcd46d320e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/merge",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/schema",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/typed",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/value",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/yaml",
|
||||
"Rev": "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
|
||||
|
@@ -16,7 +16,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/http2",
|
||||
|
@@ -16,7 +16,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/spf13/pflag",
|
||||
|
22
staging/src/k8s.io/csi-api/Godeps/Godeps.json
generated
22
staging/src/k8s.io/csi-api/Godeps/Godeps.json
generated
@@ -48,7 +48,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/googleapis/gnostic/OpenAPIv2",
|
||||
@@ -558,6 +558,26 @@
|
||||
"ImportPath": "k8s.io/utils/trace",
|
||||
"Rev": "ed37f7428a91fc2a81070808937195dcd46d320e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/merge",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/schema",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/typed",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/value",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/yaml",
|
||||
"Rev": "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
|
||||
|
@@ -16,7 +16,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/http2",
|
||||
|
@@ -160,7 +160,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/googleapis/gnostic/OpenAPIv2",
|
||||
@@ -1078,6 +1078,14 @@
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/handlers",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/endpoints/handlers/negotiation",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
@@ -1822,6 +1830,10 @@
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/handler",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/schemaconv",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/kube-openapi/pkg/util",
|
||||
"Rev": "ced9eb3070a5f1c548ef46e8dfe2a97c208d9f03"
|
||||
@@ -1842,6 +1854,26 @@
|
||||
"ImportPath": "k8s.io/utils/trace",
|
||||
"Rev": "ed37f7428a91fc2a81070808937195dcd46d320e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/fieldpath",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/merge",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/schema",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/typed",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/structured-merge-diff/value",
|
||||
"Rev": "e5e029740eb81ee0217ecf9d950c25a0eeb9688a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "sigs.k8s.io/yaml",
|
||||
"Rev": "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
|
||||
|
@@ -16,7 +16,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
"Rev": "24818f796faf91cd76ec7bddd72458fbced7a6c1"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/http2",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user