Skip to content
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

Add leafref data tree validation. #117

Merged
merged 14 commits into from
Dec 7, 2017
Merged

Add leafref data tree validation. #117

merged 14 commits into from
Dec 7, 2017

Conversation

ostromart
Copy link
Contributor

Previously, leafref validation simply validated that the referring node conformed to the schema at the leafref path, without attempting to compare with the referred-to value.
This change adds a data tree verification at the root node that compares the actual leafref values.
To accomplish this, a mirror data tree is built from the existing gostruct tree with additional information at each node to allow traversing back up to the parent, which is not possible with gostructs alone and which is required to follow relative leafref paths.
Then, for each leafref encountered in the traversal, the new tree is passed in which allows traversal back up to the root of the leafref. From this point, a new function traverses down the data tree and returns all data nodes that match the specified key (which selects the entire list when empty).
Currently, while the node selection components supports partial keys, the leafref path parsing does not. Therefore, leafref paths like "current()/../a[k1 = ../path1 and k2 = ../path2]" will lead to a parse error.

@coveralls
Copy link

Coverage Status

Coverage decreased (-2.5%) to 88.441% when pulling 6b73be5 on ostromart_dev into ec5d0ba on master.

@coveralls
Copy link

Coverage Status

Coverage decreased (-2.5%) to 88.535% when pulling 6d00726 on ostromart_dev into d3ecb54 on master.

Copy link
Contributor

@robshakir robshakir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First review: I didn't yet review any of the test code, since I wanted to get to the end of this before doing so.

@@ -44,3 +46,42 @@ func stringMapSetToSlice(m map[string]interface{}) []string {
}
return out
}

// TODO(mostrowski): move below functions into path package.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today, path functions are in ygot. Should we create a paths package specifically?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #119

return false
}
for i := range prefix {
if prefix[i] != path.GetElem()[i].GetName() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to prefer GetElem() over just straight Elem here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just convention for reading vs. writing. In C++ it returns a const value, in Go it populates unset fields with the default.

util/common.go Outdated
return path
}
return &gpb.Path{
Origin: path.GetOrigin(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably semantics, but origin becomes meaningless when it isn't a root-level element. It's specifically used to specify different overlapping trees such that say a server can support /ietf-interfaces/interfaces and /openconfig-interfaces:interfaces.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming this means I should remove the Origin field?

util/reflect.go Outdated

// ForEachField recursively iterates through the fields of value (which may be
// any Go type) and executes iterFunction on each field.
// any Go type) and executes iterFunction on each field. Any nil fields
// (including value) are traversed in the schema tree only.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If value, or any of its descendents, are nil, they are examined in the schema provided ? It might also be worth clarifying here why we need to look at values that are nil in the data tree in the schema tree. I think that this is because we want to ensure that there isn't something there that is a nil leafref, but it's not immediately clear in other scenarios why one wants to walk these entities.

In the case of building the lookup tree, we maybe don't want to define schema nodes that don't exist in the resulting tree.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the comment. Do you mean the data nodes that don't exist? (the schema nodes do exist as they are being traversed)
Data nodes are left nil in any part of the tree that exists in the schema only.

util/reflect.go Outdated
@@ -367,61 +413,368 @@ type NodeInfo struct {
// in, out are passed through from the caller to the iteration and can be used
// to pass state in and out.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any expectation as to what in and out are? I assume not, because they are only handled within FieldIteratorFunc right? It might be worth specifying here that it is safe for in and out to be type-asserted since we do not expect that there can ever be a case that they are set by anything else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return matchNodes, matchSchemas, nil
}

// pathStructTagKey returns the string label of the struct field sf when it is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only true when compression is enabled -- this code is robust to both cases, but the comment isn't entirely true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If path compression not enabled then I think the element that would've been compressed (e.g. config/name) cannot be part of the key anymore because it's now a container or list. In that case pathToSchema would just return "name" (if that is part of the key). I may be missing your point - let's talk about it live.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, yes this was a nit that I was just pointing out. Feel free to ignore.

util/schema.go Outdated
}

// SchemaPaths returns all the paths in the path tag.
func SchemaPaths(f reflect.StructField) ([][]string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have multiple implementations of this function. I wonder whether we can make them common. We should create an issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return nil
}

// FindFirstNonChoiceOrCase recursively traverses the schema tree and returns a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, now this is public, we have multiple implementations of this function (the other is in ygen), let's create an issue/TODO to fix that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grouped in comment with the same issue since these are related.

util/schema.go Outdated
if schema == nil {
return nil, nil
}
// TODO(mostrowski): this should only be possible in fakeroot. Add an
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already an annotation in the schema for this, see here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -228,6 +216,21 @@ func childSchema(schema *yang.Entry, f reflect.StructField) (*yang.Entry, error)
return nil, nil
}

// findFirstNonChoiceOrCase recursively traverses the schema tree and populates
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you have two implementations of this even in this code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@coveralls
Copy link

Coverage Status

Coverage decreased (-2.6%) to 88.417% when pulling bc9c0d4 on ostromart_dev into d3ecb54 on master.

@robshakir
Copy link
Contributor

@ostromart (not sure whether you meant to change to @maoghub :-)) -- is this ready for me to review the test code? Did we say we'd fix the coverage % in a subsequent CL?

@coveralls
Copy link

Coverage Status

Coverage decreased (-2.7%) to 88.313% when pulling 15575a4 on ostromart_dev into bbd0df1 on master.

Copy link
Contributor

@robshakir robshakir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ostromart Reviewed the test code here -- few suggestions and questions.

Key : "keyval1" [key (leaf)]
LeafField : 42 [leaf-field (leaf)]
`
if got != want {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using pretty.Compare or similar to generate a diff here? Seems like it might be easier to spot what the diff is in the case that the tests fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For small protos I find dumping out the whole thing makes the diffs easier to spot. I agree though once it's more than a dozen lines or so, pretty's the way to go.

@@ -771,31 +849,64 @@ func TestForEachField(t *testing.T) {
}

func TestUpdateFieldUsingForEachField(t *testing.T) {
containerSchema := &yang.Entry{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this schema be made general for the file such that each test function doesn't need to define the same schema and the associated types that represent its generated code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I've put it above the tests to keep some locality, otherwise the tests become very hard to understand.

StructKeyList map[string]*ListElemStruct1 `path:"config/simple-key-list"`
}

func (*InnerContainerType1) IsYANGGoStruct() {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think conventionally these should follow the definition of the type - rather than be in a block like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my education -- it seems like here you could run the anonymous function here directly within the test - with the only difference that it wouldn't run in its own goroutine. Is there another motivating factor for using t.Run here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The short answer is so we can run subtests by name.
The long answer is in this video (worth a look if you have time): https://www.youtube.com/watch?v=8hQG7QlcLBk&index=12&list=PL2ntRZ1ySWBdD9bru6IR-_WXUgJqvrtx9

StructKeyList map[KeyStruct2]*ListElemStruct2 `path:"struct-key-list"`
}

func (*InnerContainerType2) IsYANGGoStruct() {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: ordering of these functions with their receiver type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Kind: yang.LeafEntry,
Type: &yang.YangType{
Kind: yang.Yleafref,
Path: "../../config/inner/leaf-field",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider also testing absolute paths here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Kind: yang.Yleafref,
Path: "../../list[key = current()/../../key]/int32",
},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding test cases for:

  • leaf-list containing leafref.
  • typedef that defines a leafref.
  • (for Y1.1) union that defines a leafref.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #124

wantErr: `pointed-to value with path ../../list[key = current()/../../key]/int32 from field LeafRefToList value 43 (int32 ptr) schema /int32-ref-to-list is empty set`,
},
}
for _, tt := range tests {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linebreak here (nit).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

},
{
desc: "path",
in: "b[key = ../a/b/c]",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unquoted keys aren't valid XPATH - so wouldn't ever be seen here, IMHO, this should return an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

},
{
"config": {
"name": "METRO-AR"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please anonymise these policy names a bit :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@coveralls
Copy link

Coverage Status

Coverage decreased (-2.2%) to 88.784% when pulling 4406d06 on ostromart_dev into bbd0df1 on master.

@robshakir
Copy link
Contributor

Apologies for the delay. LGTM. Let's fix the coverage so we get our green logo back!

@coveralls
Copy link

coveralls commented Dec 7, 2017

Coverage Status

Coverage decreased (-2.2%) to 88.784% when pulling 9a5275e on ostromart_dev into 9f53e29 on master.

@ostromart ostromart merged commit 81b28cb into master Dec 7, 2017
@ostromart ostromart deleted the ostromart_dev branch December 7, 2017 23:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants