API Machinery, Kubectl and tests

Kubernetes-commit: 0e1d50e70fdc9ed838d75a7a1abbe5fa607d22a1
This commit is contained in:
Antoine Pelisse 2019-01-16 21:14:42 -08:00 committed by Kubernetes Publisher
parent cc1c63e36e
commit 337fc9ccde
24 changed files with 1946 additions and 130 deletions

View File

@ -133,6 +133,20 @@ func createHandler(r rest.NamedCreater, scope RequestScope, admit admission.Inte
}
}
if scope.FieldManager != nil {
liveObj, err := scope.Creater.New(scope.Kind)
if err != nil {
scope.err(fmt.Errorf("failed to create new object: %v", err), w, req)
return
}
obj, err = scope.FieldManager.Update(liveObj, obj, "create")
if err != nil {
scope.err(fmt.Errorf("failed to update object managed fields: %v", err), w, req)
return
}
}
trace.Step("About to store object in database")
result, err := finishRequest(timeout, func() (runtime.Object, error) {
return r.Create(

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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))
}
})
}
}

View File

@ -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")
}
}

View File

@ -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")
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -24,8 +24,8 @@ import (
"time"
"github.com/evanphx/json-patch"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
@ -38,6 +38,8 @@ import (
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/audit"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/features"
@ -94,20 +96,20 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
return
}
patchJS, err := readBody(req)
patchBytes, err := readBody(req)
if err != nil {
scope.err(err, w, req)
return
}
options := &metav1.UpdateOptions{}
options := &metav1.PatchOptions{}
if err := metainternalversion.ParameterCodec.DecodeParameters(req.URL.Query(), scope.MetaGroupVersion, options); err != nil {
err = errors.NewBadRequest(err.Error())
scope.err(err, w, req)
return
}
if errs := validation.ValidateUpdateOptions(options); len(errs) > 0 {
err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "UpdateOptions"}, "", errs)
if errs := validation.ValidatePatchOptions(options, patchType); len(errs) > 0 {
err := errors.NewInvalid(schema.GroupKind{Group: metav1.GroupName, Kind: "PatchOptions"}, "", errs)
scope.err(err, w, req)
return
}
@ -115,12 +117,16 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
ae := request.AuditEventFrom(ctx)
admit = admission.WithAudit(admit, ae)
audit.LogRequestPatch(ae, patchJS)
audit.LogRequestPatch(ae, patchBytes)
trace.Step("Recorded the audit event")
s, ok := runtime.SerializerInfoForMediaType(scope.Serializer.SupportedMediaTypes(), runtime.ContentTypeJSON)
baseContentType := runtime.ContentTypeJSON
if patchType == types.ApplyPatchType {
baseContentType = runtime.ContentTypeYAML
}
s, ok := runtime.SerializerInfoForMediaType(scope.Serializer.SupportedMediaTypes(), baseContentType)
if !ok {
scope.err(fmt.Errorf("no serializer defined for JSON"), w, req)
scope.err(fmt.Errorf("no serializer defined for %v", baseContentType), w, req)
return
}
gv := scope.Kind.GroupVersion()
@ -131,7 +137,18 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
)
userInfo, _ := request.UserFrom(ctx)
staticAdmissionAttributes := admission.NewAttributesRecord(
staticCreateAttributes := admission.NewAttributesRecord(
nil,
nil,
scope.Kind,
namespace,
name,
scope.Resource,
scope.Subresource,
admission.Create,
dryrun.IsDryRun(options.DryRun),
userInfo)
staticUpdateAttributes := admission.NewAttributesRecord(
nil,
nil,
scope.Kind,
@ -143,38 +160,37 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
dryrun.IsDryRun(options.DryRun),
userInfo,
)
admissionCheck := func(updatedObject runtime.Object, currentObject runtime.Object) error {
// if we allow create-on-patch, we have this TODO: call the mutating admission chain with the CREATE verb instead of UPDATE
if mutatingAdmission, ok := admit.(admission.MutationInterface); ok && admit.Handles(admission.Update) {
return mutatingAdmission.Admit(admission.NewAttributesRecord(
updatedObject,
currentObject,
scope.Kind,
namespace,
name,
scope.Resource,
scope.Subresource,
admission.Update,
dryrun.IsDryRun(options.DryRun),
userInfo,
))
}
return nil
mutatingAdmission, _ := admit.(admission.MutationInterface)
createAuthorizerAttributes := authorizer.AttributesRecord{
User: userInfo,
ResourceRequest: true,
Path: req.URL.Path,
Verb: "create",
APIGroup: scope.Resource.Group,
APIVersion: scope.Resource.Version,
Resource: scope.Resource.Resource,
Subresource: scope.Subresource,
Namespace: namespace,
Name: name,
}
p := patcher{
namer: scope.Namer,
creater: scope.Creater,
defaulter: scope.Defaulter,
typer: scope.Typer,
unsafeConvertor: scope.UnsafeConvertor,
kind: scope.Kind,
resource: scope.Resource,
subresource: scope.Subresource,
dryRun: dryrun.IsDryRun(options.DryRun),
hubGroupVersion: scope.HubGroupVersion,
createValidation: rest.AdmissionToValidateObjectFunc(admit, staticAdmissionAttributes),
updateValidation: rest.AdmissionToValidateObjectUpdateFunc(admit, staticAdmissionAttributes),
admissionCheck: admissionCheck,
createValidation: withAuthorization(rest.AdmissionToValidateObjectFunc(admit, staticCreateAttributes), scope.Authorizer, createAuthorizerAttributes),
updateValidation: rest.AdmissionToValidateObjectUpdateFunc(admit, staticUpdateAttributes),
admissionCheck: mutatingAdmission,
codec: codec,
@ -184,20 +200,35 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface
restPatcher: r,
name: name,
patchType: patchType,
patchJS: patchJS,
patchBytes: patchBytes,
trace: trace,
}
result, err := p.patchResource(ctx)
result, wasCreated, err := p.patchResource(ctx, scope)
if err != nil {
scope.err(err, w, req)
return
}
trace.Step("Object stored in database")
requestInfo, ok := request.RequestInfoFrom(ctx)
if !ok {
scope.err(fmt.Errorf("missing requestInfo"), w, req)
return
}
if err := setSelfLink(result, requestInfo, scope.Namer); err != nil {
scope.err(err, w, req)
return
}
trace.Step("Self-link added")
status := http.StatusOK
if wasCreated {
status = http.StatusCreated
}
scope.Trace = trace
transformResponseObject(ctx, scope, req, w, http.StatusOK, outputMediaType, result)
transformResponseObject(ctx, scope, req, w, status, outputMediaType, result)
}
}
@ -213,27 +244,30 @@ type patcher struct {
namer ScopeNamer
creater runtime.ObjectCreater
defaulter runtime.ObjectDefaulter
typer runtime.ObjectTyper
unsafeConvertor runtime.ObjectConvertor
resource schema.GroupVersionResource
kind schema.GroupVersionKind
subresource string
dryRun bool
hubGroupVersion schema.GroupVersion
// Validation functions
createValidation rest.ValidateObjectFunc
updateValidation rest.ValidateObjectUpdateFunc
admissionCheck mutateObjectUpdateFunc
admissionCheck admission.MutationInterface
codec runtime.Codec
timeout time.Duration
options *metav1.UpdateOptions
options *metav1.PatchOptions
// Operation information
restPatcher rest.Patcher
name string
patchType types.PatchType
patchJS []byte
patchBytes []byte
trace *utiltrace.Trace
@ -241,14 +275,18 @@ type patcher struct {
namespace string
updatedObjectInfo rest.UpdatedObjectInfo
mechanism patchMechanism
forceAllowCreate bool
}
type patchMechanism interface {
applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error)
createNewObject() (runtime.Object, error)
}
type jsonPatcher struct {
*patcher
fieldManager *fieldmanager.FieldManager
}
func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) {
@ -270,15 +308,24 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r
return nil, err
}
if p.fieldManager != nil {
if objToUpdate, err = p.fieldManager.Update(currentObject, objToUpdate, "jsonPatcher"); err != nil {
return nil, fmt.Errorf("failed to update object managed fields: %v", err)
}
}
return objToUpdate, nil
}
// patchJS applies the patch. Input and output objects must both have
func (p *jsonPatcher) createNewObject() (runtime.Object, error) {
return nil, errors.NewNotFound(p.resource.GroupResource(), p.name)
}
// applyJSPatch applies the patch. Input and output objects must both have
// the external version, since that is what the patch must have been constructed against.
func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr error) {
switch p.patchType {
case types.JSONPatchType:
patchObj, err := jsonpatch.DecodePatch(p.patchJS)
patchObj, err := jsonpatch.DecodePatch(p.patchBytes)
if err != nil {
return nil, errors.NewBadRequest(err.Error())
}
@ -288,7 +335,7 @@ func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr
}
return patchedJS, nil
case types.MergePatchType:
return jsonpatch.MergePatch(versionedJS, p.patchJS)
return jsonpatch.MergePatch(versionedJS, p.patchBytes)
default:
// only here as a safety net - go-restful filters content-type
return nil, fmt.Errorf("unknown Content-Type header for patch: %v", p.patchType)
@ -300,6 +347,7 @@ type smpPatcher struct {
// Schema
schemaReferenceObj runtime.Object
fieldManager *fieldmanager.FieldManager
}
func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) {
@ -313,22 +361,60 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru
if err != nil {
return nil, err
}
if err := strategicPatchObject(p.defaulter, currentVersionedObject, p.patchJS, versionedObjToUpdate, p.schemaReferenceObj); err != nil {
if err := strategicPatchObject(p.defaulter, currentVersionedObject, p.patchBytes, versionedObjToUpdate, p.schemaReferenceObj); err != nil {
return nil, err
}
// Convert the object back to the hub version
return p.unsafeConvertor.ConvertToVersion(versionedObjToUpdate, p.hubGroupVersion)
newObj, err := p.unsafeConvertor.ConvertToVersion(versionedObjToUpdate, p.hubGroupVersion)
if err != nil {
return nil, err
}
if p.fieldManager != nil {
if newObj, err = p.fieldManager.Update(currentObject, newObj, "smPatcher"); err != nil {
return nil, fmt.Errorf("failed to update object managed fields: %v", err)
}
}
return newObj, nil
}
// strategicPatchObject applies a strategic merge patch of <patchJS> to
func (p *smpPatcher) createNewObject() (runtime.Object, error) {
return nil, errors.NewNotFound(p.resource.GroupResource(), p.name)
}
type applyPatcher struct {
patch []byte
options *metav1.PatchOptions
creater runtime.ObjectCreater
kind schema.GroupVersionKind
fieldManager *fieldmanager.FieldManager
}
func (p *applyPatcher) applyPatchToCurrentObject(obj runtime.Object) (runtime.Object, error) {
force := false
if p.options.Force != nil {
force = *p.options.Force
}
return p.fieldManager.Apply(obj, p.patch, force)
}
func (p *applyPatcher) createNewObject() (runtime.Object, error) {
obj, err := p.creater.New(p.kind)
if err != nil {
return nil, fmt.Errorf("failed to create new object: %v", obj)
}
return p.applyPatchToCurrentObject(obj)
}
// strategicPatchObject applies a strategic merge patch of <patchBytes> to
// <originalObject> and stores the result in <objToUpdate>.
// It additionally returns the map[string]interface{} representation of the
// <originalObject> and <patchJS>.
// <originalObject> and <patchBytes>.
// NOTE: Both <originalObject> and <objToUpdate> are supposed to be versioned.
func strategicPatchObject(
defaulter runtime.ObjectDefaulter,
originalObject runtime.Object,
patchJS []byte,
patchBytes []byte,
objToUpdate runtime.Object,
schemaReferenceObj runtime.Object,
) error {
@ -338,7 +424,7 @@ func strategicPatchObject(
}
patchMap := make(map[string]interface{})
if err := json.Unmarshal(patchJS, &patchMap); err != nil {
if err := json.Unmarshal(patchBytes, &patchMap); err != nil {
return errors.NewBadRequest(err.Error())
}
@ -350,52 +436,113 @@ func strategicPatchObject(
// applyPatch is called every time GuaranteedUpdate asks for the updated object,
// and is given the currently persisted object as input.
func (p *patcher) applyPatch(_ context.Context, _, currentObject runtime.Object) (runtime.Object, error) {
// TODO: rename this function because the name implies it is related to applyPatcher
func (p *patcher) applyPatch(_ context.Context, _, currentObject runtime.Object) (objToUpdate runtime.Object, patchErr error) {
// Make sure we actually have a persisted currentObject
p.trace.Step("About to apply patch")
if hasUID, err := hasUID(currentObject); err != nil {
currentObjectHasUID, err := hasUID(currentObject)
if err != nil {
return nil, err
} else if !hasUID {
return nil, errors.NewNotFound(p.resource.GroupResource(), p.name)
} else if !currentObjectHasUID {
objToUpdate, patchErr = p.mechanism.createNewObject()
} else {
objToUpdate, patchErr = p.mechanism.applyPatchToCurrentObject(currentObject)
}
objToUpdate, err := p.mechanism.applyPatchToCurrentObject(currentObject)
if patchErr != nil {
return nil, patchErr
}
objToUpdateHasUID, err := hasUID(objToUpdate)
if err != nil {
return nil, err
}
if objToUpdateHasUID && !currentObjectHasUID {
accessor, err := meta.Accessor(objToUpdate)
if err != nil {
return nil, err
}
return nil, errors.NewConflict(p.resource.GroupResource(), p.name, fmt.Errorf("uid mismatch: the provided object specified uid %s, and no existing object was found", accessor.GetUID()))
}
if err := checkName(objToUpdate, p.name, p.namespace, p.namer); err != nil {
return nil, err
}
return objToUpdate, nil
}
func (p *patcher) admissionAttributes(ctx context.Context, updatedObject runtime.Object, currentObject runtime.Object, operation admission.Operation) admission.Attributes {
userInfo, _ := request.UserFrom(ctx)
return admission.NewAttributesRecord(updatedObject, currentObject, p.kind, p.namespace, p.name, p.resource, p.subresource, operation, p.dryRun, userInfo)
}
// applyAdmission is called every time GuaranteedUpdate asks for the updated object,
// and is given the currently persisted object and the patched object as input.
// TODO: rename this function because the name implies it is related to applyPatcher
func (p *patcher) applyAdmission(ctx context.Context, patchedObject runtime.Object, currentObject runtime.Object) (runtime.Object, error) {
p.trace.Step("About to check admission control")
return patchedObject, p.admissionCheck(patchedObject, currentObject)
var operation admission.Operation
if hasUID, err := hasUID(currentObject); err != nil {
return nil, err
} else if !hasUID {
operation = admission.Create
currentObject = nil
} else {
operation = admission.Update
}
if p.admissionCheck != nil && p.admissionCheck.Handles(operation) {
attributes := p.admissionAttributes(ctx, patchedObject, currentObject, operation)
return patchedObject, p.admissionCheck.Admit(attributes)
}
return patchedObject, nil
}
// patchResource divides PatchResource for easier unit testing
func (p *patcher) patchResource(ctx context.Context) (runtime.Object, error) {
func (p *patcher) patchResource(ctx context.Context, scope RequestScope) (runtime.Object, bool, error) {
p.namespace = request.NamespaceValue(ctx)
switch p.patchType {
case types.JSONPatchType, types.MergePatchType:
p.mechanism = &jsonPatcher{patcher: p}
p.mechanism = &jsonPatcher{
patcher: p,
fieldManager: scope.FieldManager,
}
case types.StrategicMergePatchType:
schemaReferenceObj, err := p.unsafeConvertor.ConvertToVersion(p.restPatcher.New(), p.kind.GroupVersion())
if err != nil {
return nil, false, err
}
p.mechanism = &smpPatcher{
patcher: p,
schemaReferenceObj: schemaReferenceObj,
fieldManager: scope.FieldManager,
}
// this case is unreachable if ServerSideApply is not enabled because we will have already rejected the content type
case types.ApplyPatchType:
p.mechanism = &applyPatcher{
fieldManager: scope.FieldManager,
patch: p.patchBytes,
options: p.options,
creater: p.creater,
kind: p.kind,
}
p.forceAllowCreate = true
default:
return nil, false, fmt.Errorf("%v: unimplemented patch type", p.patchType)
}
wasCreated := false
p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil, p.applyPatch, p.applyAdmission)
result, err := finishRequest(p.timeout, func() (runtime.Object, error) {
// TODO: Pass in UpdateOptions to override UpdateStrategy.AllowUpdateOnCreate
options, err := patchToUpdateOptions(p.options)
if err != nil {
return nil, err
}
p.mechanism = &smpPatcher{patcher: p, schemaReferenceObj: schemaReferenceObj}
default:
return nil, fmt.Errorf("%v: unimplemented patch type", p.patchType)
}
p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil, p.applyPatch, p.applyAdmission)
return finishRequest(p.timeout, func() (runtime.Object, error) {
updateObject, _, updateErr := p.restPatcher.Update(ctx, p.name, p.updatedObjectInfo, p.createValidation, p.updateValidation, false, p.options)
updateObject, created, updateErr := p.restPatcher.Update(ctx, p.name, p.updatedObjectInfo, p.createValidation, p.updateValidation, p.forceAllowCreate, options)
wasCreated = created
return updateObject, updateErr
})
return result, wasCreated, err
}
// applyPatchToObject applies a strategic merge patch of <patchMap> to
@ -434,3 +581,13 @@ func interpretStrategicMergePatchError(err error) error {
return err
}
}
func patchToUpdateOptions(po *metav1.PatchOptions) (*metav1.UpdateOptions, error) {
b, err := json.Marshal(po)
if err != nil {
return nil, err
}
uo := metav1.UpdateOptions{}
err = json.Unmarshal(b, &uo)
return &uo, err
}

View File

@ -27,8 +27,6 @@ import (
"strings"
"time"
"k8s.io/klog"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -37,11 +35,12 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/metrics"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
openapiproto "k8s.io/kube-openapi/pkg/util/proto"
"k8s.io/klog"
utiltrace "k8s.io/utils/trace"
)
@ -61,7 +60,7 @@ type RequestScope struct {
Trace *utiltrace.Trace
TableConvertor rest.TableConvertor
OpenAPIModels openapiproto.Models
FieldManager *fieldmanager.FieldManager
Resource schema.GroupVersionResource
Kind schema.GroupVersionKind

View File

@ -27,7 +27,6 @@ import (
"time"
"github.com/evanphx/json-patch"
apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -39,6 +38,7 @@ import (
"k8s.io/apimachinery/pkg/util/diff"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/apis/example"
examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
"k8s.io/apiserver/pkg/endpoints/request"
@ -149,10 +149,10 @@ func TestJSONPatch(t *testing.T) {
},
} {
p := &patcher{
patchType: types.JSONPatchType,
patchJS: []byte(test.patch),
patchType: types.JSONPatchType,
patchBytes: []byte(test.patch),
}
jp := jsonPatcher{p}
jp := jsonPatcher{patcher: p}
codec := codecs.LegacyCodec(examplev1.SchemeGroupVersion)
pod := &examplev1.Pod{}
pod.Name = "podA"
@ -454,12 +454,12 @@ func (tc *patchTestCase) Run(t *testing.T) {
restPatcher: testPatcher,
name: name,
patchType: patchType,
patchJS: patch,
patchBytes: patch,
trace: utiltrace.New("Patch" + name),
}
resultObj, err := p.patchResource(ctx)
resultObj, _, err := p.patchResource(ctx, RequestScope{})
if len(tc.expectedError) != 0 {
if err == nil || err.Error() != tc.expectedError {
t.Errorf("%s: expected error %v, but got %v", tc.name, tc.expectedError, err)
@ -795,6 +795,9 @@ func TestPatchWithVersionConflictThenAdmissionFailure(t *testing.T) {
tc.Run(t)
}
// TODO: Add test case for "apply with existing uid" verify it gives a conflict error,
// not a creation or an authz creation forbidden message
func TestHasUID(t *testing.T) {
testcases := []struct {
obj runtime.Object
@ -939,3 +942,11 @@ func setTcPod(tcPod *example.Pod, name string, namespace string, uid types.UID,
tcPod.Spec.NodeName = nodeName
}
}
func (f mutateObjectUpdateFunc) Handles(operation admission.Operation) bool {
return true
}
func (f mutateObjectUpdateFunc) Admit(a admission.Attributes) (err error) {
return f(a.GetObject(), a.GetOldObject())
}

View File

@ -121,7 +121,15 @@ func UpdateResource(r rest.Updater, scope RequestScope, admit admission.Interfac
}
userInfo, _ := request.UserFrom(ctx)
var transformers []rest.TransformFunc
transformers := []rest.TransformFunc{}
if scope.FieldManager != nil {
transformers = append(transformers, func(_ context.Context, liveObj, newObj runtime.Object) (runtime.Object, error) {
if obj, err = scope.FieldManager.Update(liveObj, newObj, "update"); err != nil {
return nil, fmt.Errorf("failed to update object managed fields: %v", err)
}
return obj, nil
})
}
if mutatingAdmission, ok := admit.(admission.MutationInterface); ok {
transformers = append(transformers, func(ctx context.Context, newObj, oldObj runtime.Object) (runtime.Object, error) {
isNotZeroObject, err := hasUID(oldObj)

0
pkg/endpoints/handlers/watch.go Executable file → Normal file
View File

View File

@ -27,7 +27,6 @@ import (
"unicode"
restful "github.com/emicklei/go-restful"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/conversion"
"k8s.io/apimachinery/pkg/runtime"
@ -35,10 +34,13 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/endpoints/handlers"
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/metrics"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
genericfilters "k8s.io/apiserver/pkg/server/filters"
utilfeature "k8s.io/apiserver/pkg/util/feature"
)
const (
@ -264,6 +266,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
if err != nil {
return nil, err
}
versionedPatchOptions, err := a.group.Creater.New(optionsExternalVersion.WithKind("PatchOptions"))
if err != nil {
return nil, err
}
versionedUpdateOptions, err := a.group.Creater.New(optionsExternalVersion.WithKind("UpdateOptions"))
if err != nil {
return nil, err
@ -511,7 +517,19 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
if a.group.MetaGroupVersion != nil {
reqScope.MetaGroupVersion = *a.group.MetaGroupVersion
}
reqScope.OpenAPIModels = a.group.OpenAPIModels
if a.group.OpenAPIModels != nil && utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
fm, err := fieldmanager.NewFieldManager(
a.group.OpenAPIModels,
a.group.UnsafeConvertor,
a.group.Defaulter,
fqKindToRegister.GroupVersion(),
reqScope.HubGroupVersion,
)
if err != nil {
return nil, fmt.Errorf("failed to create field manager: %v", err)
}
reqScope.FieldManager = fm
}
for _, action := range actions {
producedObject := storageMeta.ProducesObject(action.Verb)
if producedObject == nil {
@ -671,17 +689,20 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
string(types.MergePatchType),
string(types.StrategicMergePatchType),
}
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
supportedTypes = append(supportedTypes, string(types.ApplyPatchType))
}
handler := metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, restfulPatchResource(patcher, reqScope, admit, supportedTypes))
route := ws.PATCH(action.Path).To(handler).
Doc(doc).
Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
Consumes(string(types.JSONPatchType), string(types.MergePatchType), string(types.StrategicMergePatchType)).
Consumes(supportedTypes...).
Operation("patch"+namespaced+kind+strings.Title(subresource)+operationSuffix).
Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...).
Returns(http.StatusOK, "OK", producedObject).
Reads(metav1.Patch{}).
Writes(producedObject)
if err := addObjectParams(ws, route, versionedUpdateOptions); err != nil {
if err := addObjectParams(ws, route, versionedPatchOptions); err != nil {
return nil, err
}
addParams(route, action.Params)

View File

@ -25,7 +25,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
genericapitesting "k8s.io/apiserver/pkg/endpoints/testing"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
)
func TestPatch(t *testing.T) {
@ -69,6 +72,58 @@ func TestPatch(t *testing.T) {
}
}
func TestForbiddenForceOnNonApply(t *testing.T) {
storage := map[string]rest.Storage{}
ID := "id"
item := &genericapitesting.Simple{
ObjectMeta: metav1.ObjectMeta{
Name: ID,
Namespace: "", // update should allow the client to send an empty namespace
UID: "uid",
},
Other: "bar",
}
simpleStorage := SimpleRESTStorage{item: *item}
storage["simple"] = &simpleStorage
selfLinker := &setTestSelfLinker{
t: t,
expectedSet: "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/" + ID,
name: ID,
namespace: metav1.NamespaceDefault,
}
handler := handleLinker(storage, selfLinker)
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
request, err := http.NewRequest("PATCH", server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/"+ID, bytes.NewReader([]byte(`{"labels":{"foo":"bar"}}`)))
request.Header.Set("Content-Type", "application/merge-patch+json; charset=UTF-8")
_, err = client.Do(request)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
request, err = http.NewRequest("PATCH", server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/"+ID+"?force=true", bytes.NewReader([]byte(`{"labels":{"foo":"bar"}}`)))
request.Header.Set("Content-Type", "application/merge-patch+json; charset=UTF-8")
response, err := client.Do(request)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if response.StatusCode != http.StatusUnprocessableEntity {
t.Errorf("Unexpected response %#v", response)
}
request, err = http.NewRequest("PATCH", server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/"+ID+"?force=false", bytes.NewReader([]byte(`{"labels":{"foo":"bar"}}`)))
request.Header.Set("Content-Type", "application/merge-patch+json; charset=UTF-8")
response, err = client.Do(request)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if response.StatusCode != http.StatusUnprocessableEntity {
t.Errorf("Unexpected response %#v", response)
}
}
func TestPatchRequiresMatchingName(t *testing.T) {
storage := map[string]rest.Storage{}
ID := "id"
@ -97,3 +152,124 @@ func TestPatchRequiresMatchingName(t *testing.T) {
t.Errorf("Unexpected response %#v", response)
}
}
func TestPatchApply(t *testing.T) {
t.Skip("apply is being refactored")
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
storage := map[string]rest.Storage{}
item := &genericapitesting.Simple{
ObjectMeta: metav1.ObjectMeta{
Name: "id",
Namespace: "",
UID: "uid",
},
Other: "bar",
}
simpleStorage := SimpleRESTStorage{item: *item}
storage["simple"] = &simpleStorage
handler := handle(storage)
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
request, err := http.NewRequest(
"PATCH",
server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/id",
bytes.NewReader([]byte(`{"metadata":{"name":"id"}, "labels": {"test": "yes"}}`)),
)
request.Header.Set("Content-Type", "application/apply-patch+yaml")
response, err := client.Do(request)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if response.StatusCode != http.StatusOK {
t.Errorf("Unexpected response %#v", response)
}
if simpleStorage.updated.Labels["test"] != "yes" {
t.Errorf(`Expected labels to have "test": "yes", found %q`, simpleStorage.updated.Labels["test"])
}
if simpleStorage.updated.Other != "bar" {
t.Errorf(`Merge should have kept initial "bar" value for Other: %v`, simpleStorage.updated.Other)
}
if _, ok := simpleStorage.updated.ObjectMeta.ManagedFields["default"]; !ok {
t.Errorf(`Expected managedFields field to be set, but is empty`)
}
}
func TestApplyAddsGVK(t *testing.T) {
t.Skip("apply is being refactored")
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
storage := map[string]rest.Storage{}
item := &genericapitesting.Simple{
ObjectMeta: metav1.ObjectMeta{
Name: "id",
Namespace: "",
UID: "uid",
},
Other: "bar",
}
simpleStorage := SimpleRESTStorage{item: *item}
storage["simple"] = &simpleStorage
handler := handle(storage)
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
request, err := http.NewRequest(
"PATCH",
server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/id",
bytes.NewReader([]byte(`{"metadata":{"name":"id"}, "labels": {"test": "yes"}}`)),
)
request.Header.Set("Content-Type", "application/apply-patch+yaml")
response, err := client.Do(request)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if response.StatusCode != http.StatusOK {
t.Errorf("Unexpected response %#v", response)
}
// TODO: Need to fix this
expected := `{"apiVersion":"test.group/version","kind":"Simple","labels":{"test":"yes"},"metadata":{"name":"id"}}`
if simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion != expected {
t.Errorf(
`Expected managedFields field to be %q, got %q`,
expected,
simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion,
)
}
}
func TestApplyCreatesWithManagedFields(t *testing.T) {
t.Skip("apply is being refactored")
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
storage := map[string]rest.Storage{}
simpleStorage := SimpleRESTStorage{}
storage["simple"] = &simpleStorage
handler := handle(storage)
server := httptest.NewServer(handler)
defer server.Close()
client := http.Client{}
request, err := http.NewRequest(
"PATCH",
server.URL+"/"+prefix+"/"+testGroupVersion.Group+"/"+testGroupVersion.Version+"/namespaces/default/simple/id",
bytes.NewReader([]byte(`{"metadata":{"name":"id"}, "labels": {"test": "yes"}}`)),
)
request.Header.Set("Content-Type", "application/apply-patch+yaml")
response, err := client.Do(request)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if response.StatusCode != http.StatusOK {
t.Errorf("Unexpected response %#v", response)
}
// TODO: Need to fix this
expected := `{"apiVersion":"test.group/version","kind":"Simple","labels":{"test":"yes"},"metadata":{"name":"id"}}`
if simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion != expected {
t.Errorf(
`Expected managedFields field to be %q, got %q`,
expected,
simpleStorage.updated.ObjectMeta.ManagedFields["default"].APIVersion,
)
}
}

View File

@ -82,6 +82,12 @@ const (
// validation, merging, mutation can be tested without
// committing.
DryRun utilfeature.Feature = "DryRun"
// owner: @apelisse, @lavalamp
// alpha: v1.11
//
// Server-side apply. Merging happens on the server.
ServerSideApply utilfeature.Feature = "ServerSideApply"
)
func init() {
@ -99,4 +105,5 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
APIResponseCompression: {Default: false, PreRelease: utilfeature.Alpha},
APIListChunking: {Default: true, PreRelease: utilfeature.Beta},
DryRun: {Default: true, PreRelease: utilfeature.Beta},
ServerSideApply: {Default: false, PreRelease: utilfeature.Alpha},
}

View File

@ -28,7 +28,9 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
)
// RESTCreateStrategy defines the minimum validation, accepted input, and
@ -92,6 +94,12 @@ func BeforeCreate(strategy RESTCreateStrategy, ctx context.Context, obj runtime.
// Initializers are a deprecated alpha field and should not be saved
objectMeta.SetInitializers(nil)
// Ensure managedFields is not set unless the feature is enabled
if !utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
objectMeta.SetManagedFields(nil)
}
// ClusterName is ignored and should not be saved
if len(objectMeta.GetClusterName()) > 0 {
objectMeta.SetClusterName("")

View File

@ -28,6 +28,8 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
)
// RESTUpdateStrategy defines the minimum validation, accepted input, and
@ -106,6 +108,12 @@ func BeforeUpdate(strategy RESTUpdateStrategy, ctx context.Context, obj, old run
oldMeta.SetInitializers(nil)
objectMeta.SetInitializers(nil)
// Ensure managedFields state is removed unless ServerSideApply is enabled
if !utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
oldMeta.SetManagedFields(nil)
objectMeta.SetManagedFields(nil)
}
strategy.PrepareForUpdate(ctx, obj, old)
// ClusterName is ignored and should not be saved

View File

@ -324,11 +324,7 @@ func (s preparedGenericAPIServer) NonBlockingRun(stopCh <-chan struct{}) error {
}
// installAPIResources is a private method for installing the REST storage backing each api groupversionresource
func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo) error {
openAPIGroupModels, err := s.getOpenAPIModelsForGroup(apiPrefix, apiGroupInfo)
if err != nil {
return fmt.Errorf("unable to get openapi models for group %v: %v", apiPrefix, err)
}
func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *APIGroupInfo, openAPIModels openapiproto.Models) error {
for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
if len(apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version]) == 0 {
klog.Warningf("Skipping API %v because it has no resources.", groupVersion)
@ -339,7 +335,7 @@ func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *A
if apiGroupInfo.OptionsExternalVersion != nil {
apiGroupVersion.OptionsExternalVersion = apiGroupInfo.OptionsExternalVersion
}
apiGroupVersion.OpenAPIModels = openAPIGroupModels
apiGroupVersion.OpenAPIModels = openAPIModels
if err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer); err != nil {
return fmt.Errorf("unable to setup API %v: %v", apiGroupInfo, err)
@ -353,7 +349,13 @@ func (s *GenericAPIServer) InstallLegacyAPIGroup(apiPrefix string, apiGroupInfo
if !s.legacyAPIGroupPrefixes.Has(apiPrefix) {
return fmt.Errorf("%q is not in the allowed legacy API prefixes: %v", apiPrefix, s.legacyAPIGroupPrefixes.List())
}
if err := s.installAPIResources(apiPrefix, apiGroupInfo); err != nil {
openAPIModels, err := s.getOpenAPIModels(apiPrefix, apiGroupInfo)
if err != nil {
return fmt.Errorf("unable to get openapi models: %v", err)
}
if err := s.installAPIResources(apiPrefix, apiGroupInfo, openAPIModels); err != nil {
return err
}
@ -364,49 +366,62 @@ func (s *GenericAPIServer) InstallLegacyAPIGroup(apiPrefix string, apiGroupInfo
return nil
}
// Exposes given api groups in the API.
func (s *GenericAPIServer) InstallAPIGroups(apiGroupInfos ...*APIGroupInfo) error {
for _, apiGroupInfo := range apiGroupInfos {
// Do not register empty group or empty version. Doing so claims /apis/ for the wrong entity to be returned.
// Catching these here places the error much closer to its origin
if len(apiGroupInfo.PrioritizedVersions[0].Group) == 0 {
return fmt.Errorf("cannot register handler with an empty group for %#v", *apiGroupInfo)
}
if len(apiGroupInfo.PrioritizedVersions[0].Version) == 0 {
return fmt.Errorf("cannot register handler with an empty version for %#v", *apiGroupInfo)
}
}
openAPIModels, err := s.getOpenAPIModels(APIGroupPrefix, apiGroupInfos...)
if err != nil {
return fmt.Errorf("unable to get openapi models: %v", err)
}
for _, apiGroupInfo := range apiGroupInfos {
if err := s.installAPIResources(APIGroupPrefix, apiGroupInfo, openAPIModels); err != nil {
return fmt.Errorf("unable to install api resources: %v", err)
}
// setup discovery
// Install the version handler.
// Add a handler at /apis/<groupName> to enumerate all versions supported by this group.
apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{}
for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
// Check the config to make sure that we elide versions that don't have any resources
if len(apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version]) == 0 {
continue
}
apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{
GroupVersion: groupVersion.String(),
Version: groupVersion.Version,
})
}
preferredVersionForDiscovery := metav1.GroupVersionForDiscovery{
GroupVersion: apiGroupInfo.PrioritizedVersions[0].String(),
Version: apiGroupInfo.PrioritizedVersions[0].Version,
}
apiGroup := metav1.APIGroup{
Name: apiGroupInfo.PrioritizedVersions[0].Group,
Versions: apiVersionsForDiscovery,
PreferredVersion: preferredVersionForDiscovery,
}
s.DiscoveryGroupManager.AddGroup(apiGroup)
s.Handler.GoRestfulContainer.Add(discovery.NewAPIGroupHandler(s.Serializer, apiGroup).WebService())
}
return nil
}
// Exposes the given api group in the API.
func (s *GenericAPIServer) InstallAPIGroup(apiGroupInfo *APIGroupInfo) error {
// Do not register empty group or empty version. Doing so claims /apis/ for the wrong entity to be returned.
// Catching these here places the error much closer to its origin
if len(apiGroupInfo.PrioritizedVersions[0].Group) == 0 {
return fmt.Errorf("cannot register handler with an empty group for %#v", *apiGroupInfo)
}
if len(apiGroupInfo.PrioritizedVersions[0].Version) == 0 {
return fmt.Errorf("cannot register handler with an empty version for %#v", *apiGroupInfo)
}
if err := s.installAPIResources(APIGroupPrefix, apiGroupInfo); err != nil {
return err
}
// setup discovery
// Install the version handler.
// Add a handler at /apis/<groupName> to enumerate all versions supported by this group.
apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{}
for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
// Check the config to make sure that we elide versions that don't have any resources
if len(apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version]) == 0 {
continue
}
apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{
GroupVersion: groupVersion.String(),
Version: groupVersion.Version,
})
}
preferredVersionForDiscovery := metav1.GroupVersionForDiscovery{
GroupVersion: apiGroupInfo.PrioritizedVersions[0].String(),
Version: apiGroupInfo.PrioritizedVersions[0].Version,
}
apiGroup := metav1.APIGroup{
Name: apiGroupInfo.PrioritizedVersions[0].Group,
Versions: apiVersionsForDiscovery,
PreferredVersion: preferredVersionForDiscovery,
}
s.DiscoveryGroupManager.AddGroup(apiGroup)
s.Handler.GoRestfulContainer.Add(discovery.NewAPIGroupHandler(s.Serializer, apiGroup).WebService())
return nil
return s.InstallAPIGroups(apiGroupInfo)
}
func (s *GenericAPIServer) getAPIGroupVersion(apiGroupInfo *APIGroupInfo, groupVersion schema.GroupVersion, apiPrefix string) *genericapi.APIGroupVersion {
@ -455,12 +470,31 @@ func NewDefaultAPIGroupInfo(group string, scheme *runtime.Scheme, parameterCodec
}
}
// getOpenAPIModelsForGroup is a private method for getting the OpenAPI Schemas for each api group
func (s *GenericAPIServer) getOpenAPIModelsForGroup(apiPrefix string, apiGroupInfo *APIGroupInfo) (openapiproto.Models, error) {
// getOpenAPIModels is a private method for getting the OpenAPI models
func (s *GenericAPIServer) getOpenAPIModels(apiPrefix string, apiGroupInfos ...*APIGroupInfo) (openapiproto.Models, error) {
if s.openAPIConfig == nil {
return nil, nil
}
pathsToIgnore := openapiutil.NewTrie(s.openAPIConfig.IgnorePrefixes)
resourceNames := make([]string, 0)
for _, apiGroupInfo := range apiGroupInfos {
groupResources, err := getResourceNamesForGroup(apiPrefix, apiGroupInfo, pathsToIgnore)
if err != nil {
return nil, err
}
resourceNames = append(resourceNames, groupResources...)
}
// Build the openapi definitions for those resources and convert it to proto models
openAPISpec, err := openapibuilder.BuildOpenAPIDefinitionsForResources(s.openAPIConfig, resourceNames...)
if err != nil {
return nil, err
}
return utilopenapi.ToProtoModels(openAPISpec)
}
// getResourceNamesForGroup is a private method for getting the canonical names for each resource to build in an api group
func getResourceNamesForGroup(apiPrefix string, apiGroupInfo *APIGroupInfo, pathsToIgnore openapiutil.Trie) ([]string, error) {
// Get the canonical names of every resource we need to build in this api group
resourceNames := make([]string, 0)
for _, groupVersion := range apiGroupInfo.PrioritizedVersions {
@ -481,10 +515,5 @@ func (s *GenericAPIServer) getOpenAPIModelsForGroup(apiPrefix string, apiGroupIn
}
}
// Build the openapi definitions for those resources and convert it to proto models
openAPISpec, err := openapibuilder.BuildOpenAPIDefinitionsForResources(s.openAPIConfig, resourceNames...)
if err != nil {
return nil, err
}
return utilopenapi.ToProtoModels(openAPISpec)
return resourceNames, nil
}