-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Server Side Field Validation #2
Changes from all commits
e29d84f
2591d7d
f424ec6
9923290
3cb499e
526e0cd
0564cca
0d8d87e
5b82235
488bdf1
853193f
3229e2a
4194448
e5bb865
8df8d69
81f8e01
9a2e946
256d442
b3e58bd
32d4bc4
802ec6d
9da68d7
e19c591
d45a665
42ccfc9
3a5136e
a09a237
e6d2249
904316f
9275598
f06b582
62ead39
712623e
58fffc8
772b5c6
2685b57
9bb72d5
a2f0549
d607258
d5fec06
c077c26
68fa22c
acc474a
7a42d3a
5d75f7a
15e6eb5
892b2c0
17327a5
8c67e1e
98e0901
cc03962
35399e4
73eb90a
5ff723b
8339937
67ddac6
2e084cb
9e24323
d57bcc9
fb910cd
8bc736a
d445e14
7731220
adcd819
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -841,12 +841,13 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd | |||||||||||||||||||||
|
||||||||||||||||||||||
// CRDs explicitly do not support protobuf, but some objects returned by the API server do | ||||||||||||||||||||||
negotiatedSerializer := unstructuredNegotiatedSerializer{ | ||||||||||||||||||||||
typer: typer, | ||||||||||||||||||||||
creator: creator, | ||||||||||||||||||||||
converter: safeConverter, | ||||||||||||||||||||||
structuralSchemas: structuralSchemas, | ||||||||||||||||||||||
structuralSchemaGK: kind.GroupKind(), | ||||||||||||||||||||||
preserveUnknownFields: crd.Spec.PreserveUnknownFields, | ||||||||||||||||||||||
typer: typer, | ||||||||||||||||||||||
creator: creator, | ||||||||||||||||||||||
converter: safeConverter, | ||||||||||||||||||||||
structuralSchemas: structuralSchemas, | ||||||||||||||||||||||
structuralSchemaGK: kind.GroupKind(), | ||||||||||||||||||||||
preserveUnknownFields: crd.Spec.PreserveUnknownFields, | ||||||||||||||||||||||
persistStrictDecodingErrors: true, | ||||||||||||||||||||||
kevindelgado marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
} | ||||||||||||||||||||||
var standardSerializers []runtime.SerializerInfo | ||||||||||||||||||||||
for _, s := range negotiatedSerializer.SupportedMediaTypes() { | ||||||||||||||||||||||
|
@@ -1032,9 +1033,10 @@ type unstructuredNegotiatedSerializer struct { | |||||||||||||||||||||
creator runtime.ObjectCreater | ||||||||||||||||||||||
converter runtime.ObjectConvertor | ||||||||||||||||||||||
|
||||||||||||||||||||||
structuralSchemas map[string]*structuralschema.Structural // by version | ||||||||||||||||||||||
structuralSchemaGK schema.GroupKind | ||||||||||||||||||||||
preserveUnknownFields bool | ||||||||||||||||||||||
structuralSchemas map[string]*structuralschema.Structural // by version | ||||||||||||||||||||||
structuralSchemaGK schema.GroupKind | ||||||||||||||||||||||
preserveUnknownFields bool | ||||||||||||||||||||||
persistStrictDecodingErrors bool | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo { | ||||||||||||||||||||||
|
@@ -1077,7 +1079,7 @@ func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Enco | |||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder { | ||||||||||||||||||||||
d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{structuralSchemas: s.structuralSchemas, structuralSchemaGK: s.structuralSchemaGK, preserveUnknownFields: s.preserveUnknownFields}} | ||||||||||||||||||||||
d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{structuralSchemas: s.structuralSchemas, structuralSchemaGK: s.structuralSchemaGK, preserveUnknownFields: s.preserveUnknownFields, persistStrictDecodingErrors: s.persistStrictDecodingErrors}} | ||||||||||||||||||||||
return versioning.NewCodec(nil, d, runtime.UnsafeObjectConvertor(Scheme), Scheme, Scheme, unstructuredDefaulter{ | ||||||||||||||||||||||
delegate: Scheme, | ||||||||||||||||||||||
structuralSchemas: s.structuralSchemas, | ||||||||||||||||||||||
|
@@ -1237,6 +1239,9 @@ func (d schemaCoercingDecoder) Decode(data []byte, defaults *schema.GroupVersion | |||||||||||||||||||||
} | ||||||||||||||||||||||
if u, ok := obj.(*unstructured.Unstructured); ok { | ||||||||||||||||||||||
if err := d.validator.apply(u); err != nil { | ||||||||||||||||||||||
if runtime.IsStrictDecodingError(err) { | ||||||||||||||||||||||
return obj, gvk, err | ||||||||||||||||||||||
} | ||||||||||||||||||||||
return nil, gvk, err | ||||||||||||||||||||||
Comment on lines
+1243
to
1245
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've never seen this type of return signature where one doesn't check for |
||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
@@ -1296,9 +1301,10 @@ type unstructuredSchemaCoercer struct { | |||||||||||||||||||||
dropInvalidMetadata bool | ||||||||||||||||||||||
repairGeneration bool | ||||||||||||||||||||||
|
||||||||||||||||||||||
structuralSchemas map[string]*structuralschema.Structural | ||||||||||||||||||||||
structuralSchemaGK schema.GroupKind | ||||||||||||||||||||||
preserveUnknownFields bool | ||||||||||||||||||||||
structuralSchemas map[string]*structuralschema.Structural | ||||||||||||||||||||||
structuralSchemaGK schema.GroupKind | ||||||||||||||||||||||
preserveUnknownFields bool | ||||||||||||||||||||||
persistStrictDecodingErrors bool | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "persist" reads strangely... maybe |
||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since this signature is private, would it be clearer to make the list of unknown fields be returned as a separate return value?
That would let the caller determine how to use the list of unknown field paths, would preserve order, and would distinguish between validation errors and unknown fields |
||||||||||||||||||||||
|
@@ -1321,10 +1327,12 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { | |||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||
return err | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
pruned := map[string]bool{} | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could possibly have used an empty struct here, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also prefer |
||||||||||||||||||||||
if gv.Group == v.structuralSchemaGK.Group && kind == v.structuralSchemaGK.Kind { | ||||||||||||||||||||||
if !v.preserveUnknownFields { | ||||||||||||||||||||||
// TODO: switch over pruning and coercing at the root to schemaobjectmeta.Coerce too | ||||||||||||||||||||||
structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) | ||||||||||||||||||||||
// TODO: switch over pruning and coercing at the root to schemaobjectmeta.Coerce too | ||||||||||||||||||||||
pruned = structuralpruning.Prune(u.Object, v.structuralSchemas[gv.Version], false) | ||||||||||||||||||||||
structuraldefaulting.PruneNonNullableNullsWithoutDefaults(u.Object, v.structuralSchemas[gv.Version]) | ||||||||||||||||||||||
} | ||||||||||||||||||||||
Comment on lines
+1335
to
1337
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So if we're not pruning, we can't fail on unknown fields? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that seems correct... if we're preserving fields not mentioned in the schema, we're effectively saying all field names are permissible There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the other hand, this mostly exists for compatibility reason, doesn't mean people necessarily wanted arbitrary fields? I think it's useful even as a warning for example. |
||||||||||||||||||||||
if err := schemaobjectmeta.Coerce(nil, u.Object, v.structuralSchemas[gv.Version], false, v.dropInvalidMetadata); err != nil { | ||||||||||||||||||||||
|
@@ -1348,6 +1356,17 @@ func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error { | |||||||||||||||||||||
return err | ||||||||||||||||||||||
} | ||||||||||||||||||||||
} | ||||||||||||||||||||||
// collect all the strict decoding errors and return them | ||||||||||||||||||||||
if len(pruned) > 0 && v.persistStrictDecodingErrors { | ||||||||||||||||||||||
allStrictErrs := make([]error, len(pruned)) | ||||||||||||||||||||||
i := 0 | ||||||||||||||||||||||
for unknownField, _ := range pruned { | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. iterating over a map makes the order non-deterministic, which is not ideal also see suggestion above about making this low-level function just return the list of unknown field paths and letting the caller turn that into warnings/messages |
||||||||||||||||||||||
allStrictErrs[i] = fmt.Errorf("unknown field: %s", unknownField) | ||||||||||||||||||||||
i++ | ||||||||||||||||||||||
} | ||||||||||||||||||||||
Comment on lines
+1361
to
+1366
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
err := runtime.NewStrictDecodingError(allStrictErrs) | ||||||||||||||||||||||
return err | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
||||||||||||||||||||||
return nil | ||||||||||||||||||||||
} | ||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,8 @@ import ( | |
// Prune removes object fields in obj which are not specified in s. It skips TypeMeta and ObjectMeta fields | ||
// if XEmbeddedResource is set to true, or for the root if isResourceRoot=true, i.e. it does not | ||
// prune unknown metadata fields. | ||
func Prune(obj interface{}, s *structuralschema.Structural, isResourceRoot bool) { | ||
// It returns the set of fields that it prunes. | ||
func Prune(obj interface{}, s *structuralschema.Structural, isResourceRoot bool) map[string]bool { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this have to be a map, or can it be a list? What are the string? The type is not explaining a lot about what it is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see suggestion above about making this return a list of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
if isResourceRoot { | ||
if s == nil { | ||
s = &structuralschema.Structural{} | ||
|
@@ -34,7 +35,7 @@ func Prune(obj interface{}, s *structuralschema.Structural, isResourceRoot bool) | |
s = &clone | ||
} | ||
} | ||
prune(obj, s) | ||
return prune(obj, s) | ||
} | ||
|
||
var metaFields = map[string]bool{ | ||
|
@@ -43,51 +44,71 @@ var metaFields = map[string]bool{ | |
"metadata": true, | ||
} | ||
|
||
func prune(x interface{}, s *structuralschema.Structural) { | ||
func prune(x interface{}, s *structuralschema.Structural) map[string]bool { | ||
if s != nil && s.XPreserveUnknownFields { | ||
skipPrune(x, s) | ||
return | ||
return skipPrune(x, s) | ||
} | ||
|
||
pruning := map[string]bool{} | ||
switch x := x.(type) { | ||
case map[string]interface{}: | ||
if s == nil { | ||
for k := range x { | ||
if !metaFields[k] { | ||
pruning[k] = true | ||
} | ||
delete(x, k) | ||
} | ||
return | ||
return pruning | ||
} | ||
for k, v := range x { | ||
if s.XEmbeddedResource && metaFields[k] { | ||
continue | ||
} | ||
prop, ok := s.Properties[k] | ||
if ok { | ||
prune(v, &prop) | ||
pruned := prune(v, &prop) | ||
for k, b := range pruned { | ||
pruning[k] = b | ||
} | ||
} else if s.AdditionalProperties != nil { | ||
prune(v, s.AdditionalProperties.Structural) | ||
pruned := prune(v, s.AdditionalProperties.Structural) | ||
for k, b := range pruned { | ||
pruning[k] = b | ||
} | ||
} else { | ||
if !metaFields[k] { | ||
pruning[k] = true | ||
} | ||
delete(x, k) | ||
} | ||
} | ||
case []interface{}: | ||
if s == nil { | ||
for _, v := range x { | ||
prune(v, nil) | ||
pruned := prune(v, nil) | ||
for k, b := range pruned { | ||
pruning[k] = b | ||
} | ||
} | ||
return | ||
return pruning | ||
} | ||
for _, v := range x { | ||
prune(v, s.Items) | ||
pruned := prune(v, s.Items) | ||
for k, b := range pruned { | ||
pruning[k] = b | ||
} | ||
} | ||
default: | ||
// scalars, do nothing | ||
} | ||
return pruning | ||
} | ||
|
||
func skipPrune(x interface{}, s *structuralschema.Structural) { | ||
func skipPrune(x interface{}, s *structuralschema.Structural) map[string]bool { | ||
pruning := map[string]bool{} | ||
if s == nil { | ||
return | ||
return pruning | ||
} | ||
|
||
switch x := x.(type) { | ||
|
@@ -97,16 +118,26 @@ func skipPrune(x interface{}, s *structuralschema.Structural) { | |
continue | ||
} | ||
if prop, ok := s.Properties[k]; ok { | ||
prune(v, &prop) | ||
pruned := prune(v, &prop) | ||
for k, b := range pruned { | ||
pruning[k] = b | ||
} | ||
} else if s.AdditionalProperties != nil { | ||
prune(v, s.AdditionalProperties.Structural) | ||
pruned := prune(v, s.AdditionalProperties.Structural) | ||
for k, b := range pruned { | ||
pruning[k] = b | ||
} | ||
} | ||
} | ||
case []interface{}: | ||
for _, v := range x { | ||
skipPrune(v, s.Items) | ||
skipPruned := skipPrune(v, s.Items) | ||
for k, b := range skipPruned { | ||
pruning[k] = b | ||
} | ||
} | ||
default: | ||
// scalars, do nothing | ||
} | ||
return pruning | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -544,6 +544,16 @@ type CreateOptions struct { | |
// as defined by https://golang.org/pkg/unicode/#IsPrint. | ||
// +optional | ||
FieldManager string `json:"fieldManager,omitempty" protobuf:"bytes,3,name=fieldManager"` | ||
|
||
// fieldValidation determines how the server should respond to | ||
kevindelgado marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// unknown/duplicate fields. | ||
// TODO: Do we still need a protobuf tag if protobuf is not supported? | ||
kevindelgado marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Valid values are: | ||
// - Ignore: ignore's unknown/duplicate fields | ||
// - Strict: fail the request on unknown/duplicate fields | ||
// - Warn: respond with a warning, but successfully serve the request. | ||
Comment on lines
+552
to
+554
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So have we decided that warn is to expensive to be the default (and that Ignore is unnecessary?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest ordering these in increasing severity: Ignore And document what the default is on servers that support the fieldValidation option (I expected we could default to Warn) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reordered the values. Waiting on another round of feedback before defaulting to warn to confirm that is what we want to do |
||
// +optional | ||
FieldValidation string `json:"fieldValidation,omitempty"` | ||
} | ||
|
||
// +k8s:conversion-gen:explicit-from=net/url.Values | ||
|
@@ -577,6 +587,16 @@ type PatchOptions struct { | |
// types (JsonPatch, MergePatch, StrategicMergePatch). | ||
// +optional | ||
FieldManager string `json:"fieldManager,omitempty" protobuf:"bytes,3,name=fieldManager"` | ||
|
||
// fieldValidation determines how the server should respond to | ||
// unknown/duplicate fields. | ||
// TODO: Do we still need a protobuf tag if protobuf is not supported? | ||
// Valid values are: | ||
// - Ignore: ignore's unknown/duplicate fields | ||
kevindelgado marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// - Strict: fail the request on unknown/duplicate fields | ||
// - Warn: respond with a warning, but successfully serve the request. | ||
// +optional | ||
FieldValidation string `json:"fieldValidation,omitempty"` | ||
} | ||
|
||
// ApplyOptions may be provided when applying an API object. | ||
|
@@ -632,6 +652,16 @@ type UpdateOptions struct { | |
// as defined by https://golang.org/pkg/unicode/#IsPrint. | ||
// +optional | ||
FieldManager string `json:"fieldManager,omitempty" protobuf:"bytes,2,name=fieldManager"` | ||
|
||
// fieldValidation determines how the server should respond to | ||
// unknown/duplicate fields. | ||
// TODO: Do we still need a protobuf tag if protobuf is not supported? | ||
// Valid values are: | ||
// - Ignore: ignore's unknown/duplicate fields | ||
// - Strict: fail the request on unknown/duplicate fields | ||
// - Warn: respond with a warning, but successfully serve the request. | ||
// +optional | ||
FieldValidation string `json:"fieldValidation,omitempty"` | ||
} | ||
|
||
// Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not completely sure about the name yet, what about ...
StrictValidation
, or bothStrictFieldsValidation
?