API Machinery, Kubectl and tests
Kubernetes-commit: 0e1d50e70fdc9ed838d75a7a1abbe5fa607d22a1
This commit is contained in:
parent
cc1c63e36e
commit
337fc9ccde
|
|
@ -133,6 +133,20 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if scope.FieldManager != nil {
|
||||||
|
liveObj, err := scope.Creater.New(scope.Kind)
|
||||||
|
if err != nil {
|
||||||
|
scope.err(fmt.Errorf("failed to create new object: %v", err), w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err = scope.FieldManager.Update(liveObj, obj, "create")
|
||||||
|
if err != nil {
|
||||||
|
scope.err(fmt.Errorf("failed to update object managed fields: %v", err), w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trace.Step("About to store object in database")
|
trace.Step("About to store object in database")
|
||||||
result, err := finishRequest(timeout, func() (runtime.Object, error) {
|
result, err := finishRequest(timeout, func() (runtime.Object, error) {
|
||||||
return r.Create(
|
return r.Create(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fieldmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
|
||||||
|
openapiproto "k8s.io/kube-openapi/pkg/util/proto"
|
||||||
|
"sigs.k8s.io/structured-merge-diff/fieldpath"
|
||||||
|
"sigs.k8s.io/structured-merge-diff/merge"
|
||||||
|
)
|
||||||
|
|
||||||
|
const applyManager = "apply"
|
||||||
|
|
||||||
|
// FieldManager updates the managed fields and merge applied
|
||||||
|
// configurations.
|
||||||
|
type FieldManager struct {
|
||||||
|
typeConverter internal.TypeConverter
|
||||||
|
objectConverter runtime.ObjectConvertor
|
||||||
|
objectDefaulter runtime.ObjectDefaulter
|
||||||
|
groupVersion schema.GroupVersion
|
||||||
|
hubVersion schema.GroupVersion
|
||||||
|
updater merge.Updater
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFieldManager creates a new FieldManager that merges apply requests
|
||||||
|
// and update managed fields for other types of requests.
|
||||||
|
func NewFieldManager(models openapiproto.Models, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion) (*FieldManager, error) {
|
||||||
|
typeConverter, err := internal.NewTypeConverter(models)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &FieldManager{
|
||||||
|
typeConverter: typeConverter,
|
||||||
|
objectConverter: objectConverter,
|
||||||
|
objectDefaulter: objectDefaulter,
|
||||||
|
groupVersion: gv,
|
||||||
|
hubVersion: hub,
|
||||||
|
updater: merge.Updater{
|
||||||
|
Converter: internal.NewVersionConverter(typeConverter, objectConverter, hub),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update is used when the object has already been merged (non-apply
|
||||||
|
// use-case), and simply updates the managed fields in the output
|
||||||
|
// object.
|
||||||
|
func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (runtime.Object, error) {
|
||||||
|
managed, err := internal.DecodeObjectManagedFields(newObj)
|
||||||
|
// If the managed field is empty or we failed to decode it,
|
||||||
|
// let's try the live object
|
||||||
|
if err != nil || len(managed) == 0 {
|
||||||
|
managed, err = internal.DecodeObjectManagedFields(liveObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode managed fields: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newObjVersioned, err := f.toVersioned(newObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert new object to proper version: %v", err)
|
||||||
|
}
|
||||||
|
liveObjVersioned, err := f.toVersioned(liveObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert live object to proper version: %v", err)
|
||||||
|
}
|
||||||
|
if err := internal.RemoveObjectManagedFields(liveObjVersioned); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to remove managed fields from live obj: %v", err)
|
||||||
|
}
|
||||||
|
if err := internal.RemoveObjectManagedFields(newObjVersioned); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to remove managed fields from new obj: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newObjTyped, err := f.typeConverter.ObjectToTyped(newObjVersioned)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create typed new object: %v", err)
|
||||||
|
}
|
||||||
|
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create typed live object: %v", err)
|
||||||
|
}
|
||||||
|
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
|
||||||
|
managed, err = f.updater.Update(liveObjTyped, newObjTyped, apiVersion, managed, manager)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update ManagedFields: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := internal.EncodeObjectManagedFields(newObj, managed); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode managed fields: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply is used when server-side apply is called, as it merges the
|
||||||
|
// object and update the managed fields.
|
||||||
|
func (f *FieldManager) Apply(liveObj runtime.Object, patch []byte, force bool) (runtime.Object, error) {
|
||||||
|
managed, err := internal.DecodeObjectManagedFields(liveObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode managed fields: %v", err)
|
||||||
|
}
|
||||||
|
// We can assume that patchObj is already on the proper version:
|
||||||
|
// it shouldn't have to be converted so that it's not defaulted.
|
||||||
|
liveObjVersioned, err := f.toVersioned(liveObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert live object to proper version: %v", err)
|
||||||
|
}
|
||||||
|
if err := internal.RemoveObjectManagedFields(liveObjVersioned); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to remove managed fields from live obj: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
patchObjTyped, err := f.typeConverter.YAMLToTyped(patch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create typed patch object: %v", err)
|
||||||
|
}
|
||||||
|
liveObjTyped, err := f.typeConverter.ObjectToTyped(liveObjVersioned)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create typed live object: %v", err)
|
||||||
|
}
|
||||||
|
apiVersion := fieldpath.APIVersion(f.groupVersion.String())
|
||||||
|
newObjTyped, managed, err := f.updater.Apply(liveObjTyped, patchObjTyped, apiVersion, managed, applyManager, force)
|
||||||
|
if err != nil {
|
||||||
|
if conflicts, ok := err.(merge.Conflicts); ok {
|
||||||
|
return nil, errors.NewApplyConflict(conflicts)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newObj, err := f.typeConverter.TypedToObject(newObjTyped)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert new typed object to object: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := internal.EncodeObjectManagedFields(newObj, managed); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode managed fields: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newObjVersioned, err := f.toVersioned(newObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert new object to proper version: %v", err)
|
||||||
|
}
|
||||||
|
f.objectDefaulter.Default(newObjVersioned)
|
||||||
|
|
||||||
|
newObjUnversioned, err := f.toUnversioned(newObjVersioned)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert to unversioned: %v", err)
|
||||||
|
}
|
||||||
|
return newObjUnversioned, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldManager) toVersioned(obj runtime.Object) (runtime.Object, error) {
|
||||||
|
return f.objectConverter.ConvertToVersion(obj, f.groupVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FieldManager) toUnversioned(obj runtime.Object) (runtime.Object, error) {
|
||||||
|
return f.objectConverter.ConvertToVersion(obj, f.hubVersion)
|
||||||
|
}
|
||||||
|
|
@ -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,119 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"sigs.k8s.io/structured-merge-diff/fieldpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RemoveObjectManagedFields removes the ManagedFields from the object
|
||||||
|
// before we merge so that it doesn't appear in the ManagedFields
|
||||||
|
// recursively.
|
||||||
|
func RemoveObjectManagedFields(obj runtime.Object) error {
|
||||||
|
accessor, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't get accessor: %v", err)
|
||||||
|
}
|
||||||
|
accessor.SetManagedFields(nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeObjectManagedFields extracts and converts the objects ManagedFields into a fieldpath.ManagedFields.
|
||||||
|
func DecodeObjectManagedFields(from runtime.Object) (fieldpath.ManagedFields, error) {
|
||||||
|
if from == nil {
|
||||||
|
return make(map[string]*fieldpath.VersionedSet), nil
|
||||||
|
}
|
||||||
|
accessor, err := meta.Accessor(from)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("couldn't get accessor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
managed, err := decodeManagedFields(accessor.GetManagedFields())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert managed fields from API: %v", err)
|
||||||
|
}
|
||||||
|
return managed, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeObjectManagedFields converts and stores the fieldpathManagedFields into the objects ManagedFields
|
||||||
|
func EncodeObjectManagedFields(obj runtime.Object, fields fieldpath.ManagedFields) error {
|
||||||
|
accessor, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't get accessor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
managed, err := encodeManagedFields(fields)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to convert back managed fields to API: %v", err)
|
||||||
|
}
|
||||||
|
accessor.SetManagedFields(managed)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeManagedFields converts ManagedFields from the wire format (api format)
|
||||||
|
// to the format used by sigs.k8s.io/structured-merge-diff
|
||||||
|
func decodeManagedFields(encodedManagedFields map[string]metav1.VersionedFields) (managedFields fieldpath.ManagedFields, err error) {
|
||||||
|
managedFields = make(map[string]*fieldpath.VersionedSet, len(encodedManagedFields))
|
||||||
|
for manager, encodedVersionedSet := range encodedManagedFields {
|
||||||
|
managedFields[manager], err = decodeVersionedSet(&encodedVersionedSet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding versioned set for %v: %v", manager, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return managedFields, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeVersionedSet(encodedVersionedSet *metav1.VersionedFields) (versionedSet *fieldpath.VersionedSet, err error) {
|
||||||
|
versionedSet = &fieldpath.VersionedSet{}
|
||||||
|
versionedSet.APIVersion = fieldpath.APIVersion(encodedVersionedSet.APIVersion)
|
||||||
|
set, err := FieldsToSet(encodedVersionedSet.Fields)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding set: %v", err)
|
||||||
|
}
|
||||||
|
versionedSet.Set = &set
|
||||||
|
return versionedSet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeManagedFields converts ManagedFields from the the format used by
|
||||||
|
// sigs.k8s.io/structured-merge-diff to the the wire format (api format)
|
||||||
|
func encodeManagedFields(managedFields fieldpath.ManagedFields) (encodedManagedFields map[string]metav1.VersionedFields, err error) {
|
||||||
|
encodedManagedFields = make(map[string]metav1.VersionedFields, len(managedFields))
|
||||||
|
for manager, versionedSet := range managedFields {
|
||||||
|
v, err := encodeVersionedSet(versionedSet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error encoding versioned set for %v: %v", manager, err)
|
||||||
|
}
|
||||||
|
encodedManagedFields[manager] = *v
|
||||||
|
}
|
||||||
|
return encodedManagedFields, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeVersionedSet(versionedSet *fieldpath.VersionedSet) (encodedVersionedSet *metav1.VersionedFields, err error) {
|
||||||
|
encodedVersionedSet = &metav1.VersionedFields{}
|
||||||
|
encodedVersionedSet.APIVersion = string(versionedSet.APIVersion)
|
||||||
|
encodedVersionedSet.Fields, err = SetToFields(*versionedSet.Set)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error encoding set: %v", err)
|
||||||
|
}
|
||||||
|
return encodedVersionedSet, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRoundTripManagedFields will roundtrip ManagedFields from the format used by
|
||||||
|
// sigs.k8s.io/structured-merge-diff to the wire format (api format) and back
|
||||||
|
func TestRoundTripManagedFields(t *testing.T) {
|
||||||
|
tests := []string{
|
||||||
|
`foo:
|
||||||
|
apiVersion: v1
|
||||||
|
fields:
|
||||||
|
i:5:
|
||||||
|
f:i: {}
|
||||||
|
v:3:
|
||||||
|
f:alsoPi: {}
|
||||||
|
v:3.1415:
|
||||||
|
f:pi: {}
|
||||||
|
v:false:
|
||||||
|
f:notTrue: {}
|
||||||
|
`,
|
||||||
|
`foo:
|
||||||
|
apiVersion: v1
|
||||||
|
fields:
|
||||||
|
f:spec:
|
||||||
|
f:containers:
|
||||||
|
k:{"name":"c"}:
|
||||||
|
f:image: {}
|
||||||
|
f:name: {}
|
||||||
|
`,
|
||||||
|
`foo:
|
||||||
|
apiVersion: v1
|
||||||
|
fields:
|
||||||
|
f:apiVersion: {}
|
||||||
|
f:kind: {}
|
||||||
|
f:metadata:
|
||||||
|
f:labels:
|
||||||
|
f:app: {}
|
||||||
|
f:name: {}
|
||||||
|
f:spec:
|
||||||
|
f:replicas: {}
|
||||||
|
f:selector:
|
||||||
|
f:matchLabels:
|
||||||
|
f:app: {}
|
||||||
|
f:template:
|
||||||
|
f:medatada:
|
||||||
|
f:labels:
|
||||||
|
f:app: {}
|
||||||
|
f:spec:
|
||||||
|
f:containers:
|
||||||
|
k:{"name":"nginx"}:
|
||||||
|
.: {}
|
||||||
|
f:image: {}
|
||||||
|
f:name: {}
|
||||||
|
f:ports:
|
||||||
|
i:0:
|
||||||
|
f:containerPort: {}
|
||||||
|
`,
|
||||||
|
`foo:
|
||||||
|
apiVersion: v1
|
||||||
|
fields:
|
||||||
|
f:allowVolumeExpansion: {}
|
||||||
|
f:apiVersion: {}
|
||||||
|
f:kind: {}
|
||||||
|
f:metadata:
|
||||||
|
f:name: {}
|
||||||
|
f:parameters:
|
||||||
|
f:resturl: {}
|
||||||
|
f:restuser: {}
|
||||||
|
f:secretName: {}
|
||||||
|
f:secretNamespace: {}
|
||||||
|
f:provisioner: {}
|
||||||
|
`,
|
||||||
|
`foo:
|
||||||
|
apiVersion: v1
|
||||||
|
fields:
|
||||||
|
f:apiVersion: {}
|
||||||
|
f:kind: {}
|
||||||
|
f:metadata:
|
||||||
|
f:name: {}
|
||||||
|
f:spec:
|
||||||
|
f:group: {}
|
||||||
|
f:names:
|
||||||
|
f:kind: {}
|
||||||
|
f:plural: {}
|
||||||
|
f:shortNames:
|
||||||
|
i:0: {}
|
||||||
|
f:singular: {}
|
||||||
|
f:scope: {}
|
||||||
|
f:versions:
|
||||||
|
k:{"name":"v1"}:
|
||||||
|
f:name: {}
|
||||||
|
f:served: {}
|
||||||
|
f:storage: {}
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test, func(t *testing.T) {
|
||||||
|
var unmarshaled map[string]metav1.VersionedFields
|
||||||
|
if err := yaml.Unmarshal([]byte(test), &unmarshaled); err != nil {
|
||||||
|
t.Fatalf("did not expect yaml unmarshalling error but got: %v", err)
|
||||||
|
}
|
||||||
|
decoded, err := decodeManagedFields(unmarshaled)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("did not expect decoding error but got: %v", err)
|
||||||
|
}
|
||||||
|
encoded, err := encodeManagedFields(decoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("did not expect encoding error but got: %v", err)
|
||||||
|
}
|
||||||
|
marshaled, err := yaml.Marshal(&encoded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("did not expect yaml marshalling error but got: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(string(marshaled), test) {
|
||||||
|
t.Fatalf("expected:\n%v\nbut got:\n%v", test, string(marshaled))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,99 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/kube-openapi/pkg/util/proto"
|
||||||
|
"sigs.k8s.io/structured-merge-diff/typed"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TypeConverter allows you to convert from runtime.Object to
|
||||||
|
// typed.TypedValue and the other way around.
|
||||||
|
type TypeConverter interface {
|
||||||
|
NewTyped(schema.GroupVersionKind) (typed.TypedValue, error)
|
||||||
|
ObjectToTyped(runtime.Object) (typed.TypedValue, error)
|
||||||
|
YAMLToTyped([]byte) (typed.TypedValue, error)
|
||||||
|
TypedToObject(typed.TypedValue) (runtime.Object, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type typeConverter struct {
|
||||||
|
parser *gvkParser
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ TypeConverter = &typeConverter{}
|
||||||
|
|
||||||
|
// NewTypeConverter builds a TypeConverter from a proto.Models. This
|
||||||
|
// will automatically find the proper version of the object, and the
|
||||||
|
// corresponding schema information.
|
||||||
|
func NewTypeConverter(models proto.Models) (TypeConverter, error) {
|
||||||
|
parser, err := newGVKParser(models)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &typeConverter{parser: parser}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *typeConverter) NewTyped(gvk schema.GroupVersionKind) (typed.TypedValue, error) {
|
||||||
|
t := c.parser.Type(gvk)
|
||||||
|
if t == nil {
|
||||||
|
return typed.TypedValue{}, fmt.Errorf("no corresponding type for %v", gvk)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := t.New()
|
||||||
|
if err != nil {
|
||||||
|
return typed.TypedValue{}, fmt.Errorf("new typed: %v", err)
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *typeConverter) ObjectToTyped(obj runtime.Object) (typed.TypedValue, error) {
|
||||||
|
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||||
|
if err != nil {
|
||||||
|
return typed.TypedValue{}, err
|
||||||
|
}
|
||||||
|
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||||
|
t := c.parser.Type(gvk)
|
||||||
|
if t == nil {
|
||||||
|
return typed.TypedValue{}, fmt.Errorf("no corresponding type for %v", gvk)
|
||||||
|
}
|
||||||
|
return t.FromUnstructured(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *typeConverter) YAMLToTyped(from []byte) (typed.TypedValue, error) {
|
||||||
|
unstructured := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(from, &unstructured.Object); err != nil {
|
||||||
|
return typed.TypedValue{}, fmt.Errorf("error decoding YAML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.ObjectToTyped(unstructured)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *typeConverter) TypedToObject(value typed.TypedValue) (runtime.Object, error) {
|
||||||
|
vu := value.AsValue().ToUnstructured(false)
|
||||||
|
u, ok := vu.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("failed to convert typed to unstructured: want map, got %T", vu)
|
||||||
|
}
|
||||||
|
return &unstructured.Unstructured{Object: u}, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package internal_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
|
||||||
|
"k8s.io/kube-openapi/pkg/util/proto"
|
||||||
|
prototesting "k8s.io/kube-openapi/pkg/util/proto/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var fakeSchema = prototesting.Fake{
|
||||||
|
Path: filepath.Join(
|
||||||
|
"..", "..", "..", "..", "..", "..", "..", "..", "..",
|
||||||
|
"api", "openapi-spec", "swagger.json"),
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTypeConverter(t *testing.T) {
|
||||||
|
d, err := fakeSchema.OpenAPISchema()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse OpenAPI schema: %v", err)
|
||||||
|
}
|
||||||
|
m, err := proto.NewOpenAPIData(d)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to build OpenAPI models: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tc, err := internal.NewTypeConverter(m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to build TypeConverter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
y := `
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nginx-deployment
|
||||||
|
labels:
|
||||||
|
app: nginx
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: nginx
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: nginx
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nginx
|
||||||
|
image: nginx:1.15.4
|
||||||
|
`
|
||||||
|
|
||||||
|
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||||
|
if err := yaml.Unmarshal([]byte(y), &obj.Object); err != nil {
|
||||||
|
t.Fatalf("Failed to parse yaml object: %v", err)
|
||||||
|
}
|
||||||
|
typed, err := tc.ObjectToTyped(obj)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to convert object to typed: %v", err)
|
||||||
|
}
|
||||||
|
newObj, err := tc.TypedToObject(typed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to convert typed to object: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(obj, newObj) {
|
||||||
|
t.Errorf(`Round-trip failed:
|
||||||
|
Original object:
|
||||||
|
%#v
|
||||||
|
Final object:
|
||||||
|
%#v`, obj, newObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
yamlTyped, err := tc.YAMLToTyped([]byte(y))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to convert yaml to typed: %v", err)
|
||||||
|
}
|
||||||
|
newObj, err = tc.TypedToObject(yamlTyped)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to convert typed to object: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(obj, newObj) {
|
||||||
|
t.Errorf(`YAML conversion resulted in different object failed:
|
||||||
|
Original object:
|
||||||
|
%#v
|
||||||
|
Final object:
|
||||||
|
%#v`, obj, newObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
"time"
|
||||||
|
|
||||||
"github.com/evanphx/json-patch"
|
"github.com/evanphx/json-patch"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||||
|
|
@ -38,6 +38,8 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/audit"
|
"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/handlers/negotiation"
|
||||||
"k8s.io/apiserver/pkg/endpoints/request"
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/features"
|
"k8s.io/apiserver/pkg/features"
|
||||||
|
|
@ -94,20 +96,20 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
patchJS, err := readBody(req)
|
patchBytes, err := readBody(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scope.err(err, w, req)
|
scope.err(err, w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
options := &metav1.UpdateOptions{}
|
options := &metav1.PatchOptions{}
|
||||||
if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, options); err != nil {
|
if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, options); err != nil {
|
||||||
err = errors.NewBadRequest(err.Error())
|
err = errors.NewBadRequest(err.Error())
|
||||||
scope.err(err, w, req)
|
scope.err(err, w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errs := validation.ValidateUpdateOptions(options); len(errs) > 0 {
|
if errs := validation.ValidatePatchOptions(options, patchType); len(errs) > 0 {
|
||||||
err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "UpdateOptions"}, "", errs)
|
err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "PatchOptions"}, "", errs)
|
||||||
scope.err(err, w, req)
|
scope.err(err, w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -115,12 +117,16 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
|
||||||
ae := request.AuditEventFrom(ctx)
|
ae := request.AuditEventFrom(ctx)
|
||||||
admit = admission.WithAudit(admit, ae)
|
admit = admission.WithAudit(admit, ae)
|
||||||
|
|
||||||
audit.LogRequestPatch(ae, patchJS)
|
audit.LogRequestPatch(ae, patchBytes)
|
||||||
trace.Step("Recorded the audit event")
|
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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
gv := scope.Kind.GroupVersion()
|
gv := scope.Kind.GroupVersion()
|
||||||
|
|
@ -131,7 +137,18 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
|
||||||
)
|
)
|
||||||
|
|
||||||
userInfo, _ := request.UserFrom(ctx)
|
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,
|
||||||
nil,
|
nil,
|
||||||
scope.Kind,
|
scope.Kind,
|
||||||
|
|
@ -143,38 +160,37 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
|
||||||
dryrun.IsDryRun(options.DryRun),
|
dryrun.IsDryRun(options.DryRun),
|
||||||
userInfo,
|
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
|
mutatingAdmission, _ := admit.(admission.MutationInterface)
|
||||||
if mutatingAdmission, ok := admit.(admission.MutationInterface); ok && admit.Handles(admission.Update) {
|
createAuthorizerAttributes := authorizer.AttributesRecord{
|
||||||
return mutatingAdmission.Admit(admission.NewAttributesRecord(
|
User: userInfo,
|
||||||
updatedObject,
|
ResourceRequest: true,
|
||||||
currentObject,
|
Path: req.URL.Path,
|
||||||
scope.Kind,
|
Verb: "create",
|
||||||
namespace,
|
APIGroup: scope.Resource.Group,
|
||||||
name,
|
APIVersion: scope.Resource.Version,
|
||||||
scope.Resource,
|
Resource: scope.Resource.Resource,
|
||||||
scope.Subresource,
|
Subresource: scope.Subresource,
|
||||||
admission.Update,
|
Namespace: namespace,
|
||||||
dryrun.IsDryRun(options.DryRun),
|
Name: name,
|
||||||
userInfo,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p := patcher{
|
p := patcher{
|
||||||
namer: scope.Namer,
|
namer: scope.Namer,
|
||||||
creater: scope.Creater,
|
creater: scope.Creater,
|
||||||
defaulter: scope.Defaulter,
|
defaulter: scope.Defaulter,
|
||||||
|
typer: scope.Typer,
|
||||||
unsafeConvertor: scope.UnsafeConvertor,
|
unsafeConvertor: scope.UnsafeConvertor,
|
||||||
kind: scope.Kind,
|
kind: scope.Kind,
|
||||||
resource: scope.Resource,
|
resource: scope.Resource,
|
||||||
|
subresource: scope.Subresource,
|
||||||
|
dryRun: dryrun.IsDryRun(options.DryRun),
|
||||||
|
|
||||||
hubGroupVersion: scope.HubGroupVersion,
|
hubGroupVersion: scope.HubGroupVersion,
|
||||||
|
|
||||||
createValidation: rest.AdmissionToValidateObjectFunc(admit, staticAdmissionAttributes),
|
createValidation: withAuthorization(rest.AdmissionToValidateObjectFunc(admit, staticCreateAttributes), scope.Authorizer, createAuthorizerAttributes),
|
||||||
updateValidation: rest.AdmissionToValidateObjectUpdateFunc(admit, staticAdmissionAttributes),
|
updateValidation: rest.AdmissionToValidateObjectUpdateFunc(admit, staticUpdateAttributes),
|
||||||
admissionCheck: admissionCheck,
|
admissionCheck: mutatingAdmission,
|
||||||
|
|
||||||
codec: codec,
|
codec: codec,
|
||||||
|
|
||||||
|
|
@ -184,20 +200,35 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
|
||||||
restPatcher: r,
|
restPatcher: r,
|
||||||
name: name,
|
name: name,
|
||||||
patchType: patchType,
|
patchType: patchType,
|
||||||
patchJS: patchJS,
|
patchBytes: patchBytes,
|
||||||
|
|
||||||
trace: trace,
|
trace: trace,
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := p.patchResource(ctx)
|
result, wasCreated, err := p.patchResource(ctx, scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scope.err(err, w, req)
|
scope.err(err, w, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
trace.Step("Object stored in database")
|
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
|
scope.Trace = trace
|
||||||
transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result)
|
transformResponseObject(ctx, scope, req, w, status, outputMediaType, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,27 +244,30 @@ type patcher struct {
|
||||||
namer ScopeNamer
|
namer ScopeNamer
|
||||||
creater runtime.ObjectCreater
|
creater runtime.ObjectCreater
|
||||||
defaulter runtime.ObjectDefaulter
|
defaulter runtime.ObjectDefaulter
|
||||||
|
typer runtime.ObjectTyper
|
||||||
unsafeConvertor runtime.ObjectConvertor
|
unsafeConvertor runtime.ObjectConvertor
|
||||||
resource schema.GroupVersionResource
|
resource schema.GroupVersionResource
|
||||||
kind schema.GroupVersionKind
|
kind schema.GroupVersionKind
|
||||||
|
subresource string
|
||||||
|
dryRun bool
|
||||||
|
|
||||||
hubGroupVersion schema.GroupVersion
|
hubGroupVersion schema.GroupVersion
|
||||||
|
|
||||||
// Validation functions
|
// Validation functions
|
||||||
createValidation rest.ValidateObjectFunc
|
createValidation rest.ValidateObjectFunc
|
||||||
updateValidation rest.ValidateObjectUpdateFunc
|
updateValidation rest.ValidateObjectUpdateFunc
|
||||||
admissionCheck mutateObjectUpdateFunc
|
admissionCheck admission.MutationInterface
|
||||||
|
|
||||||
codec runtime.Codec
|
codec runtime.Codec
|
||||||
|
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
options *metav1.UpdateOptions
|
options *metav1.PatchOptions
|
||||||
|
|
||||||
// Operation information
|
// Operation information
|
||||||
restPatcher rest.Patcher
|
restPatcher rest.Patcher
|
||||||
name string
|
name string
|
||||||
patchType types.PatchType
|
patchType types.PatchType
|
||||||
patchJS []byte
|
patchBytes []byte
|
||||||
|
|
||||||
trace *utiltrace.Trace
|
trace *utiltrace.Trace
|
||||||
|
|
||||||
|
|
@ -241,14 +275,18 @@ type patcher struct {
|
||||||
namespace string
|
namespace string
|
||||||
updatedObjectInfo rest.UpdatedObjectInfo
|
updatedObjectInfo rest.UpdatedObjectInfo
|
||||||
mechanism patchMechanism
|
mechanism patchMechanism
|
||||||
|
forceAllowCreate bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type patchMechanism interface {
|
type patchMechanism interface {
|
||||||
applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error)
|
applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error)
|
||||||
|
createNewObject() (runtime.Object, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsonPatcher struct {
|
type jsonPatcher struct {
|
||||||
*patcher
|
*patcher
|
||||||
|
|
||||||
|
fieldManager *fieldmanager.FieldManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) {
|
func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) {
|
||||||
|
|
@ -270,15 +308,24 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.fieldManager != nil {
|
||||||
|
if objToUpdate, err = p.fieldManager.Update(currentObject, objToUpdate, "jsonPatcher"); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update object managed fields: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return objToUpdate, nil
|
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.
|
// the external version, since that is what the patch must have been constructed against.
|
||||||
func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr error) {
|
func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr error) {
|
||||||
switch p.patchType {
|
switch p.patchType {
|
||||||
case types.JSONPatchType:
|
case types.JSONPatchType:
|
||||||
patchObj, err := jsonpatch.DecodePatch(p.patchJS)
|
patchObj, err := jsonpatch.DecodePatch(p.patchBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.NewBadRequest(err.Error())
|
return nil, errors.NewBadRequest(err.Error())
|
||||||
}
|
}
|
||||||
|
|
@ -288,7 +335,7 @@ func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr
|
||||||
}
|
}
|
||||||
return patchedJS, nil
|
return patchedJS, nil
|
||||||
case types.MergePatchType:
|
case types.MergePatchType:
|
||||||
return jsonpatch.MergePatch(versionedJS, p.patchJS)
|
return jsonpatch.MergePatch(versionedJS, p.patchBytes)
|
||||||
default:
|
default:
|
||||||
// only here as a safety net - go-restful filters content-type
|
// only here as a safety net - go-restful filters content-type
|
||||||
return nil, fmt.Errorf("unknown Content-Type header for patch: %v", p.patchType)
|
return nil, fmt.Errorf("unknown Content-Type header for patch: %v", p.patchType)
|
||||||
|
|
@ -300,6 +347,7 @@ type smpPatcher struct {
|
||||||
|
|
||||||
// Schema
|
// Schema
|
||||||
schemaReferenceObj runtime.Object
|
schemaReferenceObj runtime.Object
|
||||||
|
fieldManager *fieldmanager.FieldManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) {
|
func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) {
|
||||||
|
|
@ -313,22 +361,60 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
// Convert the object back to the hub version
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// strategicPatchObject applies a strategic merge patch of <patchJS> to
|
if p.fieldManager != nil {
|
||||||
|
if newObj, err = p.fieldManager.Update(currentObject, newObj, "smPatcher"); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update object managed fields: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *smpPatcher) createNewObject() (runtime.Object, error) {
|
||||||
|
return nil, errors.NewNotFound(p.resource.GroupResource(), p.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type applyPatcher struct {
|
||||||
|
patch []byte
|
||||||
|
options *metav1.PatchOptions
|
||||||
|
creater runtime.ObjectCreater
|
||||||
|
kind schema.GroupVersionKind
|
||||||
|
fieldManager *fieldmanager.FieldManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *applyPatcher) applyPatchToCurrentObject(obj runtime.Object) (runtime.Object, error) {
|
||||||
|
force := false
|
||||||
|
if p.options.Force != nil {
|
||||||
|
force = *p.options.Force
|
||||||
|
}
|
||||||
|
return p.fieldManager.Apply(obj, p.patch, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *applyPatcher) createNewObject() (runtime.Object, error) {
|
||||||
|
obj, err := p.creater.New(p.kind)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create new object: %v", obj)
|
||||||
|
}
|
||||||
|
return p.applyPatchToCurrentObject(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// strategicPatchObject applies a strategic merge patch of <patchBytes> to
|
||||||
// <originalObject> and stores the result in <objToUpdate>.
|
// <originalObject> and stores the result in <objToUpdate>.
|
||||||
// It additionally returns the map[string]interface{} representation of the
|
// 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.
|
// NOTE: Both <originalObject> and <objToUpdate> are supposed to be versioned.
|
||||||
func strategicPatchObject(
|
func strategicPatchObject(
|
||||||
defaulter runtime.ObjectDefaulter,
|
defaulter runtime.ObjectDefaulter,
|
||||||
originalObject runtime.Object,
|
originalObject runtime.Object,
|
||||||
patchJS []byte,
|
patchBytes []byte,
|
||||||
objToUpdate runtime.Object,
|
objToUpdate runtime.Object,
|
||||||
schemaReferenceObj runtime.Object,
|
schemaReferenceObj runtime.Object,
|
||||||
) error {
|
) error {
|
||||||
|
|
@ -338,7 +424,7 @@ func strategicPatchObject(
|
||||||
}
|
}
|
||||||
|
|
||||||
patchMap := make(map[string]interface{})
|
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())
|
return errors.NewBadRequest(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -350,52 +436,113 @@ func strategicPatchObject(
|
||||||
|
|
||||||
// applyPatch is called every time GuaranteedUpdate asks for the updated object,
|
// applyPatch is called every time GuaranteedUpdate asks for the updated object,
|
||||||
// and is given the currently persisted object as input.
|
// 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
|
// Make sure we actually have a persisted currentObject
|
||||||
p.trace.Step("About to apply patch")
|
p.trace.Step("About to apply patch")
|
||||||
if hasUID, err := hasUID(currentObject); err != nil {
|
currentObjectHasUID, err := hasUID(currentObject)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !hasUID {
|
} else if !currentObjectHasUID {
|
||||||
return nil, errors.NewNotFound(p.resource.GroupResource(), p.name)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err := checkName(objToUpdate, p.name, p.namespace, p.namer); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return objToUpdate, nil
|
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,
|
// applyAdmission is called every time GuaranteedUpdate asks for the updated object,
|
||||||
// and is given the currently persisted object and the patched object as input.
|
// 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) {
|
func (p *patcher) applyAdmission(ctx context.Context, patchedObject runtime.Object, currentObject runtime.Object) (runtime.Object, error) {
|
||||||
p.trace.Step("About to check admission control")
|
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
|
// 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)
|
p.namespace = request.NamespaceValue(ctx)
|
||||||
switch p.patchType {
|
switch p.patchType {
|
||||||
case types.JSONPatchType, types.MergePatchType:
|
case types.JSONPatchType, types.MergePatchType:
|
||||||
p.mechanism = &jsonPatcher{patcher: p}
|
p.mechanism = &jsonPatcher{
|
||||||
|
patcher: p,
|
||||||
|
fieldManager: scope.FieldManager,
|
||||||
|
}
|
||||||
case types.StrategicMergePatchType:
|
case types.StrategicMergePatchType:
|
||||||
schemaReferenceObj, err := p.unsafeConvertor.ConvertToVersion(p.restPatcher.New(), p.kind.GroupVersion())
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
p.mechanism = &smpPatcher{patcher: p, schemaReferenceObj: schemaReferenceObj}
|
updateObject, created, updateErr := p.restPatcher.Update(ctx, p.name, p.updatedObjectInfo, p.createValidation, p.updateValidation, p.forceAllowCreate, options)
|
||||||
default:
|
wasCreated = created
|
||||||
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)
|
|
||||||
return updateObject, updateErr
|
return updateObject, updateErr
|
||||||
})
|
})
|
||||||
|
return result, wasCreated, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyPatchToObject applies a strategic merge patch of <patchMap> to
|
// applyPatchToObject applies a strategic merge patch of <patchMap> to
|
||||||
|
|
@ -434,3 +581,13 @@ func interpretStrategicMergePatchError(err error) error {
|
||||||
return err
|
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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/klog"
|
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
@ -37,11 +35,12 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"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/handlers/responsewriters"
|
||||||
"k8s.io/apiserver/pkg/endpoints/metrics"
|
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||||
"k8s.io/apiserver/pkg/endpoints/request"
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
openapiproto "k8s.io/kube-openapi/pkg/util/proto"
|
"k8s.io/klog"
|
||||||
utiltrace "k8s.io/utils/trace"
|
utiltrace "k8s.io/utils/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -61,7 +60,7 @@ type RequestScope struct {
|
||||||
Trace *utiltrace.Trace
|
Trace *utiltrace.Trace
|
||||||
|
|
||||||
TableConvertor rest.TableConvertor
|
TableConvertor rest.TableConvertor
|
||||||
OpenAPIModels openapiproto.Models
|
FieldManager *fieldmanager.FieldManager
|
||||||
|
|
||||||
Resource schema.GroupVersionResource
|
Resource schema.GroupVersionResource
|
||||||
Kind schema.GroupVersionKind
|
Kind schema.GroupVersionKind
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/evanphx/json-patch"
|
"github.com/evanphx/json-patch"
|
||||||
|
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
@ -39,6 +38,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/util/diff"
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||||
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/apis/example"
|
"k8s.io/apiserver/pkg/apis/example"
|
||||||
examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
|
examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
|
||||||
"k8s.io/apiserver/pkg/endpoints/request"
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
|
@ -150,9 +150,9 @@ func TestJSONPatch(t *testing.T) {
|
||||||
} {
|
} {
|
||||||
p := &patcher{
|
p := &patcher{
|
||||||
patchType: types.JSONPatchType,
|
patchType: types.JSONPatchType,
|
||||||
patchJS: []byte(test.patch),
|
patchBytes: []byte(test.patch),
|
||||||
}
|
}
|
||||||
jp := jsonPatcher{p}
|
jp := jsonPatcher{patcher: p}
|
||||||
codec := codecs.LegacyCodec(examplev1.SchemeGroupVersion)
|
codec := codecs.LegacyCodec(examplev1.SchemeGroupVersion)
|
||||||
pod := &examplev1.Pod{}
|
pod := &examplev1.Pod{}
|
||||||
pod.Name = "podA"
|
pod.Name = "podA"
|
||||||
|
|
@ -454,12 +454,12 @@ func (tc *patchTestCase) Run(t *testing.T) {
|
||||||
restPatcher: testPatcher,
|
restPatcher: testPatcher,
|
||||||
name: name,
|
name: name,
|
||||||
patchType: patchType,
|
patchType: patchType,
|
||||||
patchJS: patch,
|
patchBytes: patch,
|
||||||
|
|
||||||
trace: utiltrace.New("Patch" + name),
|
trace: utiltrace.New("Patch" + name),
|
||||||
}
|
}
|
||||||
|
|
||||||
resultObj, err := p.patchResource(ctx)
|
resultObj, _, err := p.patchResource(ctx, RequestScope{})
|
||||||
if len(tc.expectedError) != 0 {
|
if len(tc.expectedError) != 0 {
|
||||||
if err == nil || err.Error() != tc.expectedError {
|
if err == nil || err.Error() != tc.expectedError {
|
||||||
t.Errorf("%s: expected error %v, but got %v", tc.name, tc.expectedError, err)
|
t.Errorf("%s: expected error %v, but got %v", tc.name, tc.expectedError, err)
|
||||||
|
|
@ -795,6 +795,9 @@ func TestPatchWithVersionConflictThenAdmissionFailure(t *testing.T) {
|
||||||
tc.Run(t)
|
tc.Run(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Add test case for "apply with existing uid" verify it gives a conflict error,
|
||||||
|
// not a creation or an authz creation forbidden message
|
||||||
|
|
||||||
func TestHasUID(t *testing.T) {
|
func TestHasUID(t *testing.T) {
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
obj runtime.Object
|
obj runtime.Object
|
||||||
|
|
@ -939,3 +942,11 @@ func setTcPod(tcPod *example.Pod, name string, namespace string, uid types.UID,
|
||||||
tcPod.Spec.NodeName = nodeName
|
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,15 @@ func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interfac
|
||||||
}
|
}
|
||||||
|
|
||||||
userInfo, _ := request.UserFrom(ctx)
|
userInfo, _ := request.UserFrom(ctx)
|
||||||
var transformers []rest.TransformFunc
|
transformers := []rest.TransformFunc{}
|
||||||
|
if scope.FieldManager != nil {
|
||||||
|
transformers = append(transformers, func(_ context.Context, liveObj, newObj runtime.Object) (runtime.Object, error) {
|
||||||
|
if obj, err = scope.FieldManager.Update(liveObj, newObj, "update"); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update object managed fields: %v", err)
|
||||||
|
}
|
||||||
|
return obj, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
if mutatingAdmission, ok := admit.(admission.MutationInterface); ok {
|
if mutatingAdmission, ok := admit.(admission.MutationInterface); ok {
|
||||||
transformers = append(transformers, func(ctx context.Context, newObj, oldObj runtime.Object) (runtime.Object, error) {
|
transformers = append(transformers, func(ctx context.Context, newObj, oldObj runtime.Object) (runtime.Object, error) {
|
||||||
isNotZeroObject, err := hasUID(oldObj)
|
isNotZeroObject, err := hasUID(oldObj)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ import (
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/conversion"
|
"k8s.io/apimachinery/pkg/conversion"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
@ -35,10 +34,13 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/endpoints/handlers"
|
"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/handlers/negotiation"
|
||||||
"k8s.io/apiserver/pkg/endpoints/metrics"
|
"k8s.io/apiserver/pkg/endpoints/metrics"
|
||||||
|
"k8s.io/apiserver/pkg/features"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
genericfilters "k8s.io/apiserver/pkg/server/filters"
|
genericfilters "k8s.io/apiserver/pkg/server/filters"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -264,6 +266,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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"))
|
versionedUpdateOptions, err := a.group.Creater.New(optionsExternalVersion.WithKind("UpdateOptions"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -511,7 +517,19 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
|
||||||
if a.group.MetaGroupVersion != nil {
|
if a.group.MetaGroupVersion != nil {
|
||||||
reqScope.MetaGroupVersion = *a.group.MetaGroupVersion
|
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 {
|
for _, action := range actions {
|
||||||
producedObject := storageMeta.ProducesObject(action.Verb)
|
producedObject := storageMeta.ProducesObject(action.Verb)
|
||||||
if producedObject == nil {
|
if producedObject == nil {
|
||||||
|
|
@ -671,17 +689,20 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
|
||||||
string(types.MergePatchType),
|
string(types.MergePatchType),
|
||||||
string(types.StrategicMergePatchType),
|
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))
|
handler := metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, restfulPatchResource(patcher, reqScope, admit, supportedTypes))
|
||||||
route := ws.PATCH(action.Path).To(handler).
|
route := ws.PATCH(action.Path).To(handler).
|
||||||
Doc(doc).
|
Doc(doc).
|
||||||
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
|
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).
|
Operation("patch"+namespaced+kind+strings.Title(subresource)+operationSuffix).
|
||||||
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
|
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
|
||||||
Returns(http.StatusOK, "OK", producedObject).
|
Returns(http.StatusOK, "OK", producedObject).
|
||||||
Reads(metav1.Patch{}).
|
Reads(metav1.Patch{}).
|
||||||
Writes(producedObject)
|
Writes(producedObject)
|
||||||
if err := addObjectParams(ws, route, versionedUpdateOptions); err != nil {
|
if err := addObjectParams(ws, route, versionedPatchOptions); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
addParams(route, action.Params)
|
addParams(route, action.Params)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,10 @@ import (
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
genericapitesting "k8s.io/apiserver/pkg/endpoints/testing"
|
genericapitesting "k8s.io/apiserver/pkg/endpoints/testing"
|
||||||
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPatch(t *testing.T) {
|
func TestPatch(t *testing.T) {
|
||||||
|
|
@ -69,6 +72,58 @@ func TestPatch(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForbiddenForceOnNonApply(t *testing.T) {
|
||||||
|
storage := map[string]rest.Storage{}
|
||||||
|
ID := "id"
|
||||||
|
item := &genericapitesting.Simple{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: ID,
|
||||||
|
Namespace: "", // update should allow the client to send an empty namespace
|
||||||
|
UID: "uid",
|
||||||
|
},
|
||||||
|
Other: "bar",
|
||||||
|
}
|
||||||
|
simpleStorage := SimpleRESTStorage{item: *item}
|
||||||
|
storage["simple"] = &simpleStorage
|
||||||
|
selfLinker := &setTestSelfLinker{
|
||||||
|
t: t,
|
||||||
|
expectedSet: "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/" + ID,
|
||||||
|
name: ID,
|
||||||
|
namespace: metav1.NamespaceDefault,
|
||||||
|
}
|
||||||
|
handler := handleLinker(storage, selfLinker)
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := http.Client{}
|
||||||
|
request, err := http.NewRequest("PATCH", server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/"+ID, bytes.NewReader([]byte(`{"labels":{"foo":"bar"}}`)))
|
||||||
|
request.Header.Set("Content-Type", "application/merge-patch+json; charset=UTF-8")
|
||||||
|
_, err = client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err = http.NewRequest("PATCH", server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/"+ID+"?force=true", bytes.NewReader([]byte(`{"labels":{"foo":"bar"}}`)))
|
||||||
|
request.Header.Set("Content-Type", "application/merge-patch+json; charset=UTF-8")
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusUnprocessableEntity {
|
||||||
|
t.Errorf("Unexpected response %#v", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err = http.NewRequest("PATCH", server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/"+ID+"?force=false", bytes.NewReader([]byte(`{"labels":{"foo":"bar"}}`)))
|
||||||
|
request.Header.Set("Content-Type", "application/merge-patch+json; charset=UTF-8")
|
||||||
|
response, err = client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusUnprocessableEntity {
|
||||||
|
t.Errorf("Unexpected response %#v", response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPatchRequiresMatchingName(t *testing.T) {
|
func TestPatchRequiresMatchingName(t *testing.T) {
|
||||||
storage := map[string]rest.Storage{}
|
storage := map[string]rest.Storage{}
|
||||||
ID := "id"
|
ID := "id"
|
||||||
|
|
@ -97,3 +152,124 @@ func TestPatchRequiresMatchingName(t *testing.T) {
|
||||||
t.Errorf("Unexpected response %#v", response)
|
t.Errorf("Unexpected response %#v", response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPatchApply(t *testing.T) {
|
||||||
|
t.Skip("apply is being refactored")
|
||||||
|
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
|
||||||
|
storage := map[string]rest.Storage{}
|
||||||
|
item := &genericapitesting.Simple{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "id",
|
||||||
|
Namespace: "",
|
||||||
|
UID: "uid",
|
||||||
|
},
|
||||||
|
Other: "bar",
|
||||||
|
}
|
||||||
|
simpleStorage := SimpleRESTStorage{item: *item}
|
||||||
|
storage["simple"] = &simpleStorage
|
||||||
|
handler := handle(storage)
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := http.Client{}
|
||||||
|
request, err := http.NewRequest(
|
||||||
|
"PATCH",
|
||||||
|
server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/id",
|
||||||
|
bytes.NewReader([]byte(`{"metadata":{"name":"id"}, "labels": {"test": "yes"}}`)),
|
||||||
|
)
|
||||||
|
request.Header.Set("Content-Type", "application/apply-patch+yaml")
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Unexpected response %#v", response)
|
||||||
|
}
|
||||||
|
if simpleStorage.updated.Labels["test"] != "yes" {
|
||||||
|
t.Errorf(`Expected labels to have "test": "yes", found %q`, simpleStorage.updated.Labels["test"])
|
||||||
|
}
|
||||||
|
if simpleStorage.updated.Other != "bar" {
|
||||||
|
t.Errorf(`Merge should have kept initial "bar" value for Other: %v`, simpleStorage.updated.Other)
|
||||||
|
}
|
||||||
|
if _, ok := simpleStorage.updated.ObjectMeta.ManagedFields["default"]; !ok {
|
||||||
|
t.Errorf(`Expected managedFields field to be set, but is empty`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyAddsGVK(t *testing.T) {
|
||||||
|
t.Skip("apply is being refactored")
|
||||||
|
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
|
||||||
|
storage := map[string]rest.Storage{}
|
||||||
|
item := &genericapitesting.Simple{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "id",
|
||||||
|
Namespace: "",
|
||||||
|
UID: "uid",
|
||||||
|
},
|
||||||
|
Other: "bar",
|
||||||
|
}
|
||||||
|
simpleStorage := SimpleRESTStorage{item: *item}
|
||||||
|
storage["simple"] = &simpleStorage
|
||||||
|
handler := handle(storage)
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := http.Client{}
|
||||||
|
request, err := http.NewRequest(
|
||||||
|
"PATCH",
|
||||||
|
server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/id",
|
||||||
|
bytes.NewReader([]byte(`{"metadata":{"name":"id"}, "labels": {"test": "yes"}}`)),
|
||||||
|
)
|
||||||
|
request.Header.Set("Content-Type", "application/apply-patch+yaml")
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Unexpected response %#v", response)
|
||||||
|
}
|
||||||
|
// TODO: Need to fix this
|
||||||
|
expected := `{"apiVersion":"test.group/version","kind":"Simple","labels":{"test":"yes"},"metadata":{"name":"id"}}`
|
||||||
|
if simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion != expected {
|
||||||
|
t.Errorf(
|
||||||
|
`Expected managedFields field to be %q, got %q`,
|
||||||
|
expected,
|
||||||
|
simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCreatesWithManagedFields(t *testing.T) {
|
||||||
|
t.Skip("apply is being refactored")
|
||||||
|
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
|
||||||
|
storage := map[string]rest.Storage{}
|
||||||
|
simpleStorage := SimpleRESTStorage{}
|
||||||
|
storage["simple"] = &simpleStorage
|
||||||
|
handler := handle(storage)
|
||||||
|
server := httptest.NewServer(handler)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := http.Client{}
|
||||||
|
request, err := http.NewRequest(
|
||||||
|
"PATCH",
|
||||||
|
server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/id",
|
||||||
|
bytes.NewReader([]byte(`{"metadata":{"name":"id"}, "labels": {"test": "yes"}}`)),
|
||||||
|
)
|
||||||
|
request.Header.Set("Content-Type", "application/apply-patch+yaml")
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("Unexpected response %#v", response)
|
||||||
|
}
|
||||||
|
// TODO: Need to fix this
|
||||||
|
expected := `{"apiVersion":"test.group/version","kind":"Simple","labels":{"test":"yes"},"metadata":{"name":"id"}}`
|
||||||
|
if simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion != expected {
|
||||||
|
t.Errorf(
|
||||||
|
`Expected managedFields field to be %q, got %q`,
|
||||||
|
expected,
|
||||||
|
simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,12 @@ const (
|
||||||
// validation, merging, mutation can be tested without
|
// validation, merging, mutation can be tested without
|
||||||
// committing.
|
// committing.
|
||||||
DryRun utilfeature.Feature = "DryRun"
|
DryRun utilfeature.Feature = "DryRun"
|
||||||
|
|
||||||
|
// owner: @apelisse, @lavalamp
|
||||||
|
// alpha: v1.11
|
||||||
|
//
|
||||||
|
// Server-side apply. Merging happens on the server.
|
||||||
|
ServerSideApply utilfeature.Feature = "ServerSideApply"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -99,4 +105,5 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
|
||||||
APIResponseCompression: {Default: false, PreRelease: utilfeature.Alpha},
|
APIResponseCompression: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
APIListChunking: {Default: true, PreRelease: utilfeature.Beta},
|
APIListChunking: {Default: true, PreRelease: utilfeature.Beta},
|
||||||
DryRun: {Default: true, PreRelease: utilfeature.Beta},
|
DryRun: {Default: true, PreRelease: utilfeature.Beta},
|
||||||
|
ServerSideApply: {Default: false, PreRelease: utilfeature.Alpha},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/features"
|
||||||
"k8s.io/apiserver/pkg/storage/names"
|
"k8s.io/apiserver/pkg/storage/names"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RESTCreateStrategy defines the minimum validation, accepted input, and
|
// 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
|
// Initializers are a deprecated alpha field and should not be saved
|
||||||
objectMeta.SetInitializers(nil)
|
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
|
// ClusterName is ignored and should not be saved
|
||||||
if len(objectMeta.GetClusterName()) > 0 {
|
if len(objectMeta.GetClusterName()) > 0 {
|
||||||
objectMeta.SetClusterName("")
|
objectMeta.SetClusterName("")
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"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
|
// 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)
|
oldMeta.SetInitializers(nil)
|
||||||
objectMeta.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)
|
strategy.PrepareForUpdate(ctx, obj, old)
|
||||||
|
|
||||||
// ClusterName is ignored and should not be saved
|
// 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
|
// installAPIResources is a private method for installing the REST storage backing each api groupversionresource
|
||||||
func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo) error {
|
func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, openAPIModels openapiproto.Models) error {
|
||||||
openAPIGroupModels, err := s.getOpenAPIModelsForGroup(apiPrefix, apiGroupInfo)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get openapi models for group %v: %v", apiPrefix, err)
|
|
||||||
}
|
|
||||||
for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
|
for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
|
||||||
if len(apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version]) == 0 {
|
if len(apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version]) == 0 {
|
||||||
klog.Warningf("Skipping API %v because it has no resources.", groupVersion)
|
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 {
|
if apiGroupInfo.OptionsExternalVersion != nil {
|
||||||
apiGroupVersion.OptionsExternalVersion = apiGroupInfo.OptionsExternalVersion
|
apiGroupVersion.OptionsExternalVersion = apiGroupInfo.OptionsExternalVersion
|
||||||
}
|
}
|
||||||
apiGroupVersion.OpenAPIModels = openAPIGroupModels
|
apiGroupVersion.OpenAPIModels = openAPIModels
|
||||||
|
|
||||||
if err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer); err != nil {
|
if err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer); err != nil {
|
||||||
return fmt.Errorf("unable to setup API %v: %v", apiGroupInfo, err)
|
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) {
|
if !s.legacyAPIGroupPrefixes.Has(apiPrefix) {
|
||||||
return fmt.Errorf("%q is not in the allowed legacy API prefixes: %v", apiPrefix, s.legacyAPIGroupPrefixes.List())
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,8 +366,9 @@ func (s *GenericAPIServer) InstallLegacyAPIGroup(apiPrefix string, apiGroupInfo
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exposes the given api group in the API.
|
// Exposes given api groups in the API.
|
||||||
func (s *GenericAPIServer) InstallAPIGroup(apiGroupInfo *APIGroupInfo) error {
|
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.
|
// 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
|
// Catching these here places the error much closer to its origin
|
||||||
if len(apiGroupInfo.PrioritizedVersions[0].Group) == 0 {
|
if len(apiGroupInfo.PrioritizedVersions[0].Group) == 0 {
|
||||||
|
|
@ -374,9 +377,16 @@ func (s *GenericAPIServer) InstallAPIGroup(apiGroupInfo *APIGroupInfo) error {
|
||||||
if len(apiGroupInfo.PrioritizedVersions[0].Version) == 0 {
|
if len(apiGroupInfo.PrioritizedVersions[0].Version) == 0 {
|
||||||
return fmt.Errorf("cannot register handler with an empty version for %#v", *apiGroupInfo)
|
return fmt.Errorf("cannot register handler with an empty version for %#v", *apiGroupInfo)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.installAPIResources(APIGroupPrefix, apiGroupInfo); err != nil {
|
openAPIModels, err := s.getOpenAPIModels(APIGroupPrefix, apiGroupInfos...)
|
||||||
return err
|
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
|
// setup discovery
|
||||||
|
|
@ -405,10 +415,15 @@ func (s *GenericAPIServer) InstallAPIGroup(apiGroupInfo *APIGroupInfo) error {
|
||||||
|
|
||||||
s.DiscoveryGroupManager.AddGroup(apiGroup)
|
s.DiscoveryGroupManager.AddGroup(apiGroup)
|
||||||
s.Handler.GoRestfulContainer.Add(discovery.NewAPIGroupHandler(s.Serializer, apiGroup).WebService())
|
s.Handler.GoRestfulContainer.Add(discovery.NewAPIGroupHandler(s.Serializer, apiGroup).WebService())
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exposes the given api group in the API.
|
||||||
|
func (s *GenericAPIServer) InstallAPIGroup(apiGroupInfo *APIGroupInfo) error {
|
||||||
|
return s.InstallAPIGroups(apiGroupInfo)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *GenericAPIServer) getAPIGroupVersion(apiGroupInfo *APIGroupInfo, groupVersion schema.GroupVersion, apiPrefix string) *genericapi.APIGroupVersion {
|
func (s *GenericAPIServer) getAPIGroupVersion(apiGroupInfo *APIGroupInfo, groupVersion schema.GroupVersion, apiPrefix string) *genericapi.APIGroupVersion {
|
||||||
storage := make(map[string]rest.Storage)
|
storage := make(map[string]rest.Storage)
|
||||||
for k, v := range apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version] {
|
for k, v := range apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version] {
|
||||||
|
|
@ -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
|
// getOpenAPIModels is a private method for getting the OpenAPI models
|
||||||
func (s *GenericAPIServer) getOpenAPIModelsForGroup(apiPrefix string, apiGroupInfo *APIGroupInfo) (openapiproto.Models, error) {
|
func (s *GenericAPIServer) getOpenAPIModels(apiPrefix string, apiGroupInfos ...*APIGroupInfo) (openapiproto.Models, error) {
|
||||||
if s.openAPIConfig == nil {
|
if s.openAPIConfig == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
pathsToIgnore := openapiutil.NewTrie(s.openAPIConfig.IgnorePrefixes)
|
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
|
// Get the canonical names of every resource we need to build in this api group
|
||||||
resourceNames := make([]string, 0)
|
resourceNames := make([]string, 0)
|
||||||
for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
|
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
|
return resourceNames, nil
|
||||||
openAPISpec, err := openapibuilder.BuildOpenAPIDefinitionsForResources(s.openAPIConfig, resourceNames...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return utilopenapi.ToProtoModels(openAPISpec)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue