Add new FieldsType to clarify the format of Fields

This commit is contained in:
Antoine Pelisse 2019-08-21 16:13:53 -07:00
parent 39724859b5
commit ff4e1f3592
13 changed files with 120 additions and 126 deletions

View File

@ -231,8 +231,8 @@ func CompatibilityTestFuzzer(scheme *runtime.Scheme, fuzzFuncs []interface{}) *f
func(f *[]metav1.ManagedFieldsEntry, c fuzz.Continue) { func(f *[]metav1.ManagedFieldsEntry, c fuzz.Continue) {
field := metav1.ManagedFieldsEntry{} field := metav1.ManagedFieldsEntry{}
c.Fuzz(&field) c.Fuzz(&field)
if field.Fields != nil { if field.FieldsV1 != nil {
field.Fields.Raw = []byte("{}") field.FieldsV1.Raw = []byte("{}")
} }
*f = []metav1.ManagedFieldsEntry{field} *f = []metav1.ManagedFieldsEntry{field}
}, },

View File

@ -184,6 +184,7 @@ func ValidateObjectMetaAccessor(meta metav1.Object, requiresNamespace bool, name
allErrs = append(allErrs, ValidateAnnotations(meta.GetAnnotations(), fldPath.Child("annotations"))...) allErrs = append(allErrs, ValidateAnnotations(meta.GetAnnotations(), fldPath.Child("annotations"))...)
allErrs = append(allErrs, ValidateOwnerReferences(meta.GetOwnerReferences(), fldPath.Child("ownerReferences"))...) allErrs = append(allErrs, ValidateOwnerReferences(meta.GetOwnerReferences(), fldPath.Child("ownerReferences"))...)
allErrs = append(allErrs, ValidateFinalizers(meta.GetFinalizers(), fldPath.Child("finalizers"))...) allErrs = append(allErrs, ValidateFinalizers(meta.GetFinalizers(), fldPath.Child("finalizers"))...)
allErrs = append(allErrs, v1validation.ValidateManagedFields(meta.GetManagedFields(), fldPath.Child("managedFields"))...)
return allErrs return allErrs
} }
@ -256,6 +257,7 @@ func ValidateObjectMetaAccessorUpdate(newMeta, oldMeta metav1.Object, fldPath *f
allErrs = append(allErrs, v1validation.ValidateLabels(newMeta.GetLabels(), fldPath.Child("labels"))...) allErrs = append(allErrs, v1validation.ValidateLabels(newMeta.GetLabels(), fldPath.Child("labels"))...)
allErrs = append(allErrs, ValidateAnnotations(newMeta.GetAnnotations(), fldPath.Child("annotations"))...) allErrs = append(allErrs, ValidateAnnotations(newMeta.GetAnnotations(), fldPath.Child("annotations"))...)
allErrs = append(allErrs, ValidateOwnerReferences(newMeta.GetOwnerReferences(), fldPath.Child("ownerReferences"))...) allErrs = append(allErrs, ValidateOwnerReferences(newMeta.GetOwnerReferences(), fldPath.Child("ownerReferences"))...)
allErrs = append(allErrs, v1validation.ValidateManagedFields(newMeta.GetManagedFields(), fldPath.Child("managedFields"))...)
return allErrs return allErrs
} }

View File

@ -272,7 +272,7 @@ func v1FuzzerFuncs(codecs runtimeserializer.CodecFactory) []interface{} {
}, },
func(j *metav1.ManagedFieldsEntry, c fuzz.Continue) { func(j *metav1.ManagedFieldsEntry, c fuzz.Continue) {
c.FuzzNoCustom(j) c.FuzzNoCustom(j)
j.Fields = nil j.FieldsV1 = nil
}, },
} }
} }

View File

@ -1,88 +0,0 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
"encoding/json"
)
// Fields is declared in types.go
// ProtoFields is a struct that is equivalent to Fields, but intended for
// protobuf marshalling/unmarshalling. It is generated into a serialization
// that matches Fields. Do not use in Go structs.
type ProtoFields struct {
// Map is the representation used in the alpha version of this API
Map map[string]Fields `json:"-" protobuf:"bytes,1,rep,name=map"`
// Raw is the underlying serialization of this object.
Raw []byte `json:"-" protobuf:"bytes,2,opt,name=raw"`
}
// ProtoFields returns the Fields as a new ProtoFields value.
func (m *Fields) ProtoFields() *ProtoFields {
if m == nil {
return &ProtoFields{}
}
return &ProtoFields{
Raw: m.Raw,
}
}
// Size implements the protobuf marshalling interface.
func (m *Fields) Size() (n int) {
return m.ProtoFields().Size()
}
// Unmarshal implements the protobuf marshalling interface.
func (m *Fields) Unmarshal(data []byte) error {
if len(data) == 0 {
return nil
}
p := ProtoFields{}
if err := p.Unmarshal(data); err != nil {
return err
}
if len(p.Map) == 0 {
return json.Unmarshal(p.Raw, &m)
}
b, err := json.Marshal(&p.Map)
if err != nil {
return err
}
return json.Unmarshal(b, &m)
}
// Marshal implements the protobuf marshaling interface.
func (m *Fields) Marshal() (data []byte, err error) {
return m.ProtoFields().Marshal()
}
// MarshalTo implements the protobuf marshaling interface.
func (m *Fields) MarshalTo(data []byte) (int, error) {
return m.ProtoFields().MarshalTo(data)
}
// MarshalToSizedBuffer implements the protobuf reverse marshaling interface.
func (m *Fields) MarshalToSizedBuffer(data []byte) (int, error) {
return m.ProtoFields().MarshalToSizedBuffer(data)
}
// String implements the protobuf goproto_stringer interface.
func (m *Fields) String() string {
return m.ProtoFields().String()
}

View File

@ -258,7 +258,7 @@ func ResetObjectMetaForStatus(meta, existingMeta Object) {
// MarshalJSON implements json.Marshaler // MarshalJSON implements json.Marshaler
// MarshalJSON may get called on pointers or values, so implement MarshalJSON on value. // MarshalJSON may get called on pointers or values, so implement MarshalJSON on value.
// http://stackoverflow.com/questions/21390979/custom-marshaljson-never-gets-called-in-go // http://stackoverflow.com/questions/21390979/custom-marshaljson-never-gets-called-in-go
func (f Fields) MarshalJSON() ([]byte, error) { func (f FieldsV1) MarshalJSON() ([]byte, error) {
if f.Raw == nil { if f.Raw == nil {
return []byte("null"), nil return []byte("null"), nil
} }
@ -266,7 +266,7 @@ func (f Fields) MarshalJSON() ([]byte, error) {
} }
// UnmarshalJSON implements json.Unmarshaler // UnmarshalJSON implements json.Unmarshaler
func (f *Fields) UnmarshalJSON(b []byte) error { func (f *FieldsV1) UnmarshalJSON(b []byte) error {
if f == nil { if f == nil {
return errors.New("metav1.Fields: UnmarshalJSON on nil pointer") return errors.New("metav1.Fields: UnmarshalJSON on nil pointer")
} }
@ -276,5 +276,5 @@ func (f *Fields) UnmarshalJSON(b []byte) error {
return nil return nil
} }
var _ json.Marshaler = Fields{} var _ json.Marshaler = FieldsV1{}
var _ json.Unmarshaler = &Fields{} var _ json.Unmarshaler = &FieldsV1{}

View File

@ -1105,9 +1105,12 @@ type ManagedFieldsEntry struct {
// Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply' // Time is timestamp of when these fields were set. It should always be empty if Operation is 'Apply'
// +optional // +optional
Time *Time `json:"time,omitempty" protobuf:"bytes,4,opt,name=time"` Time *Time `json:"time,omitempty" protobuf:"bytes,4,opt,name=time"`
// Fields identifies a set of fields. // FieldsType is the discriminator for the different fields format and version.
// There is currently only one possible value: "FieldsV1"
FieldsType string `json:"fieldsType,omitempty" protobuf:"bytes,6,opt,name=fieldsType"`
// FieldsV1 holds the first JSON version format as described in the "FieldsV1" type.
// +optional // +optional
Fields *Fields `json:"fields,omitempty" protobuf:"bytes,5,opt,name=fields,casttype=Fields"` FieldsV1 *FieldsV1 `json:"fieldsV1,omitempty" protobuf:"bytes,7,opt,name=fieldsV1"`
} }
// ManagedFieldsOperationType is the type of operation which lead to a ManagedFieldsEntry being created. // ManagedFieldsOperationType is the type of operation which lead to a ManagedFieldsEntry being created.
@ -1118,7 +1121,7 @@ const (
ManagedFieldsOperationUpdate ManagedFieldsOperationType = "Update" ManagedFieldsOperationUpdate ManagedFieldsOperationType = "Update"
) )
// Fields stores a set of fields in a data structure like a Trie. // FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.
// //
// Each key is either a '.' representing the field itself, and will always map to an empty set, // Each key is either a '.' representing the field itself, and will always map to an empty set,
// or a string representing a sub-field or item. The string will follow one of these four formats: // or a string representing a sub-field or item. The string will follow one of these four formats:
@ -1129,12 +1132,9 @@ const (
// If a key maps to an empty Fields value, the field that key represents is part of the set. // If a key maps to an empty Fields value, the field that key represents is part of the set.
// //
// The exact format is defined in sigs.k8s.io/structured-merge-diff // The exact format is defined in sigs.k8s.io/structured-merge-diff
// +protobuf.options.marshal=false type FieldsV1 struct {
// +protobuf.as=ProtoFields
// +protobuf.options.(gogoproto.goproto_stringer)=false
type Fields struct {
// Raw is the underlying serialization of this object. // Raw is the underlying serialization of this object.
Raw []byte `json:"-" protobuf:"-"` Raw []byte `json:"-" protobuf:"bytes,1,opt,name=Raw"`
} }
// TODO: Table does not generate to protobuf because of the interface{} - fix protobuf // TODO: Table does not generate to protobuf because of the interface{} - fix protobuf

View File

@ -169,3 +169,18 @@ func ValidateTableOptions(opts *metav1.TableOptions) field.ErrorList {
} }
return allErrs return allErrs
} }
func ValidateManagedFields(fieldsList []metav1.ManagedFieldsEntry, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
for _, fields := range fieldsList {
switch fields.Operation {
case metav1.ManagedFieldsOperationApply, metav1.ManagedFieldsOperationUpdate:
default:
allErrs = append(allErrs, field.Invalid(fldPath.Child("operation"), fields.Operation, "must be `Apply` or `Update`"))
}
if fields.FieldsType != "FieldsV1" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("fieldsType"), fields.FieldsType, "must be `FieldsV1`"))
}
}
return allErrs
}

View File

@ -241,3 +241,55 @@ func TestValidateFieldManagerInvalid(t *testing.T) {
}) })
} }
} }
func TestValidateMangedFieldsInvalid(t *testing.T) {
tests := []metav1.ManagedFieldsEntry{
{
Operation: metav1.ManagedFieldsOperationUpdate,
// FieldsType is missing
},
{
Operation: metav1.ManagedFieldsOperationUpdate,
FieldsType: "RandomVersion",
},
{
Operation: "RandomOperation",
FieldsType: "FieldsV1",
},
{
// Operation is missing
FieldsType: "FieldsV1",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test), func(t *testing.T) {
errs := ValidateManagedFields([]metav1.ManagedFieldsEntry{test}, field.NewPath("managedFields"))
if len(errs) == 0 {
t.Errorf("Validation should have failed")
}
})
}
}
func TestValidateMangedFieldsValid(t *testing.T) {
tests := []metav1.ManagedFieldsEntry{
{
Operation: metav1.ManagedFieldsOperationUpdate,
FieldsType: "FieldsV1",
},
{
Operation: metav1.ManagedFieldsOperationApply,
FieldsType: "FieldsV1",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test), func(t *testing.T) {
err := ValidateManagedFields([]metav1.ManagedFieldsEntry{test}, field.NewPath("managedFields"))
if err != nil {
t.Errorf("Validation failed: %v", err)
}
})
}
}

View File

@ -26,7 +26,7 @@ import (
// EmptyFields represents a set with no paths // EmptyFields represents a set with no paths
// It looks like metav1.Fields{Raw: []byte("{}")} // It looks like metav1.Fields{Raw: []byte("{}")}
var EmptyFields metav1.Fields = func() metav1.Fields { var EmptyFields metav1.FieldsV1 = func() metav1.FieldsV1 {
f, err := SetToFields(*fieldpath.NewSet()) f, err := SetToFields(*fieldpath.NewSet())
if err != nil { if err != nil {
panic("should never happen") panic("should never happen")
@ -35,13 +35,13 @@ var EmptyFields metav1.Fields = func() metav1.Fields {
}() }()
// FieldsToSet creates a set paths from an input trie of fields // FieldsToSet creates a set paths from an input trie of fields
func FieldsToSet(f metav1.Fields) (s fieldpath.Set, err error) { func FieldsToSet(f metav1.FieldsV1) (s fieldpath.Set, err error) {
err = s.FromJSON(bytes.NewReader(f.Raw)) err = s.FromJSON(bytes.NewReader(f.Raw))
return s, err return s, err
} }
// SetToFields creates a trie of fields from an input set of paths // SetToFields creates a trie of fields from an input set of paths
func SetToFields(s fieldpath.Set) (f metav1.Fields, err error) { func SetToFields(s fieldpath.Set) (f metav1.FieldsV1, err error) {
f.Raw, err = s.ToJSON() f.Raw, err = s.ToJSON()
return f, err return f, err
} }

View File

@ -29,7 +29,7 @@ import (
// TestFieldsRoundTrip tests that a fields trie can be round tripped as a path set // TestFieldsRoundTrip tests that a fields trie can be round tripped as a path set
func TestFieldsRoundTrip(t *testing.T) { func TestFieldsRoundTrip(t *testing.T) {
tests := []metav1.Fields{ tests := []metav1.FieldsV1{
{ {
Raw: []byte(`{"f:metadata":{"f:name":{},".":{}}}`), Raw: []byte(`{"f:metadata":{"f:name":{},".":{}}}`),
}, },
@ -54,11 +54,11 @@ func TestFieldsRoundTrip(t *testing.T) {
// TestFieldsToSetError tests that errors are picked up by FieldsToSet // TestFieldsToSetError tests that errors are picked up by FieldsToSet
func TestFieldsToSetError(t *testing.T) { func TestFieldsToSetError(t *testing.T) {
tests := []struct { tests := []struct {
fields metav1.Fields fields metav1.FieldsV1
errString string errString string
}{ }{
{ {
fields: metav1.Fields{ fields: metav1.FieldsV1{
Raw: []byte(`{"k:{invalid json}":{"f:name":{},".":{}}}`), Raw: []byte(`{"k:{invalid json}":{"f:name":{},".":{}}}`),
}, },
errString: "ReadObjectCB", errString: "ReadObjectCB",

View File

@ -101,8 +101,11 @@ func decodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (mana
func BuildManagerIdentifier(encodedManager *metav1.ManagedFieldsEntry) (manager string, err error) { func BuildManagerIdentifier(encodedManager *metav1.ManagedFieldsEntry) (manager string, err error) {
encodedManagerCopy := *encodedManager encodedManagerCopy := *encodedManager
// Never include fields type in the manager identifier
encodedManagerCopy.FieldsType = ""
// Never include the fields in the manager identifier // Never include the fields in the manager identifier
encodedManagerCopy.Fields = nil encodedManagerCopy.FieldsV1 = nil
// Never include the time in the manager identifier // Never include the time in the manager identifier
encodedManagerCopy.Time = nil encodedManagerCopy.Time = nil
@ -124,8 +127,8 @@ func BuildManagerIdentifier(encodedManager *metav1.ManagedFieldsEntry) (manager
func decodeVersionedSet(encodedVersionedSet *metav1.ManagedFieldsEntry) (versionedSet fieldpath.VersionedSet, err error) { func decodeVersionedSet(encodedVersionedSet *metav1.ManagedFieldsEntry) (versionedSet fieldpath.VersionedSet, err error) {
fields := EmptyFields fields := EmptyFields
if encodedVersionedSet.Fields != nil { if encodedVersionedSet.FieldsV1 != nil {
fields = *encodedVersionedSet.Fields fields = *encodedVersionedSet.FieldsV1
} }
set, err := FieldsToSet(fields) set, err := FieldsToSet(fields)
if err != nil { if err != nil {
@ -191,11 +194,12 @@ func encodeManagerVersionedSet(manager string, versionedSet fieldpath.VersionedS
if versionedSet.Applied() { if versionedSet.Applied() {
encodedVersionedSet.Operation = metav1.ManagedFieldsOperationApply encodedVersionedSet.Operation = metav1.ManagedFieldsOperationApply
} }
encodedVersionedSet.FieldsType = "FieldsV1"
fields, err := SetToFields(*versionedSet.Set()) fields, err := SetToFields(*versionedSet.Set())
if err != nil { if err != nil {
return nil, fmt.Errorf("error encoding set: %v", err) return nil, fmt.Errorf("error encoding set: %v", err)
} }
encodedVersionedSet.Fields = &fields encodedVersionedSet.FieldsV1 = &fields
return encodedVersionedSet, nil return encodedVersionedSet, nil
} }

View File

@ -32,7 +32,8 @@ import (
func TestRoundTripManagedFields(t *testing.T) { func TestRoundTripManagedFields(t *testing.T) {
tests := []string{ tests := []string{
`- apiVersion: v1 `- apiVersion: v1
fields: fieldsType: FieldsV1
fieldsV1:
v:3: v:3:
f:alsoPi: {} f:alsoPi: {}
v:3.1415: v:3.1415:
@ -43,7 +44,8 @@ func TestRoundTripManagedFields(t *testing.T) {
operation: Update operation: Update
time: "2001-02-03T04:05:06Z" time: "2001-02-03T04:05:06Z"
- apiVersion: v1beta1 - apiVersion: v1beta1
fields: fieldsType: FieldsV1
fieldsV1:
i:5: i:5:
f:i: {} f:i: {}
manager: foo manager: foo
@ -51,7 +53,8 @@ func TestRoundTripManagedFields(t *testing.T) {
time: "2011-12-13T14:15:16Z" time: "2011-12-13T14:15:16Z"
`, `,
`- apiVersion: v1 `- apiVersion: v1
fields: fieldsType: FieldsV1
fieldsV1:
f:spec: f:spec:
f:containers: f:containers:
k:{"name":"c"}: k:{"name":"c"}:
@ -61,7 +64,8 @@ func TestRoundTripManagedFields(t *testing.T) {
operation: Apply operation: Apply
`, `,
`- apiVersion: v1 `- apiVersion: v1
fields: fieldsType: FieldsV1
fieldsV1:
f:apiVersion: {} f:apiVersion: {}
f:kind: {} f:kind: {}
f:metadata: f:metadata:
@ -90,7 +94,8 @@ func TestRoundTripManagedFields(t *testing.T) {
operation: Update operation: Update
`, `,
`- apiVersion: v1 `- apiVersion: v1
fields: fieldsType: FieldsV1
fieldsV1:
f:allowVolumeExpansion: {} f:allowVolumeExpansion: {}
f:apiVersion: {} f:apiVersion: {}
f:kind: {} f:kind: {}
@ -106,7 +111,8 @@ func TestRoundTripManagedFields(t *testing.T) {
operation: Apply operation: Apply
`, `,
`- apiVersion: v1 `- apiVersion: v1
fields: fieldsType: FieldsV1
fieldsV1:
f:apiVersion: {} f:apiVersion: {}
f:kind: {} f:kind: {}
f:metadata: f:metadata:
@ -163,7 +169,7 @@ func TestBuildManagerIdentifier(t *testing.T) {
{ {
managedFieldsEntry: ` managedFieldsEntry: `
apiVersion: v1 apiVersion: v1
fields: fieldsV1:
f:apiVersion: {} f:apiVersion: {}
manager: foo manager: foo
operation: Update operation: Update
@ -174,7 +180,7 @@ time: "2001-02-03T04:05:06Z"
{ {
managedFieldsEntry: ` managedFieldsEntry: `
apiVersion: v1 apiVersion: v1
fields: fieldsV1:
f:apiVersion: {} f:apiVersion: {}
manager: foo manager: foo
operation: Apply operation: Apply

View File

@ -456,7 +456,8 @@ func TestApplyManagedFields(t *testing.T) {
"operation": "Apply", "operation": "Apply",
"apiVersion": "v1", "apiVersion": "v1",
"time": "` + accessor.GetManagedFields()[0].Time.UTC().Format(time.RFC3339) + `", "time": "` + accessor.GetManagedFields()[0].Time.UTC().Format(time.RFC3339) + `",
"fields": { "fieldsType": "FieldsV1",
"fieldsV1": {
"f:metadata": { "f:metadata": {
"f:labels": { "f:labels": {
"f:test-label": {} "f:test-label": {}
@ -469,7 +470,8 @@ func TestApplyManagedFields(t *testing.T) {
"operation": "Update", "operation": "Update",
"apiVersion": "v1", "apiVersion": "v1",
"time": "` + accessor.GetManagedFields()[1].Time.UTC().Format(time.RFC3339) + `", "time": "` + accessor.GetManagedFields()[1].Time.UTC().Format(time.RFC3339) + `",
"fields": { "fieldsType": "FieldsV1",
"fieldsV1": {
"f:data": { "f:data": {
"f:key": {}, "f:key": {},
"f:new-key": {} "f:new-key": {}
@ -684,7 +686,7 @@ func TestApplyRemoveContainerPort(t *testing.T) {
} }
if len(deployment.Spec.Template.Spec.Containers[0].Ports) > 0 { if len(deployment.Spec.Template.Spec.Containers[0].Ports) > 0 {
t.Fatalf("Expected no container ports but got: %v", deployment.Spec.Template.Spec.Containers[0].Ports) t.Fatalf("Expected no container ports but got: %v, object: \n%#v", deployment.Spec.Template.Spec.Containers[0].Ports, deployment)
} }
} }
@ -804,7 +806,7 @@ func TestApplyConvertsManagedFieldsVersion(t *testing.T) {
"manager": "sidecar_controller", "manager": "sidecar_controller",
"operation": "Apply", "operation": "Apply",
"apiVersion": "extensions/v1beta1", "apiVersion": "extensions/v1beta1",
"fields": { "fieldsV1": {
"f:metadata": { "f:metadata": {
"f:labels": { "f:labels": {
"f:sidecar_version": {} "f:sidecar_version": {}
@ -918,7 +920,8 @@ func TestApplyConvertsManagedFieldsVersion(t *testing.T) {
Operation: metav1.ManagedFieldsOperationApply, Operation: metav1.ManagedFieldsOperationApply,
APIVersion: "apps/v1", APIVersion: "apps/v1",
Time: actual.Time, Time: actual.Time,
Fields: &metav1.Fields{ FieldsType: "FieldsV1",
FieldsV1: &metav1.FieldsV1{
Raw: []byte(`{"f:metadata":{"f:labels":{"f:sidecar_version":{}}},"f:spec":{"f:template":{"f:spec":{"f:containers":{"k:{\"name\":\"sidecar\"}":{".":{},"f:image":{},"f:name":{}}}}}}}`), Raw: []byte(`{"f:metadata":{"f:labels":{"f:sidecar_version":{}}},"f:spec":{"f:template":{"f:spec":{"f:containers":{"k:{\"name\":\"sidecar\"}":{".":{},"f:image":{},"f:name":{}}}}}}}`),
}, },
} }