Closed frebib closed 3 years ago
We talked a bit about this on IRC... I think this is blocked on https://github.com/golang/go/issues/25401 ... So:
1) I don't want to dig down into this myself until 24501 is fixed. 2) But eventually I might have to do so and either fix that upstream or make a workaround here 3) And I'd probably want a simple failing test case as a PR before I started 4) That last one is easy to do, but I'm focused on other areas at the moment, so this issue is free if someone is interested in taking it.
// TODO: should we create a
Type
interface?
I should probably remove this comment, but maybe it's fine to leave in to at least provoke thought in future passers by?
I'm happy to work on this. It's something I want to rely on in a resource that I'm working on, and I think fixing/changing this behavior will appear more intuitive and consistent in the long run. I've been trying to wrap my head around how this works, and this is where I'm at currently:
I will submit a PR for a failing test case, then attempt to actually make the field mapping work, which is proving rather difficult, unsurprisingly.
The TODO causes no harm as it is, but also it would be good to clean through the old comments and prune anything inaccurate or resolved as an act of declaring stability.
After thoroughly straining every neuron in my brain trying to trace through the several layers of the code, I think I understand the variable journey from mcl to resource now. One thing I'm reasonably sure about is this only affects specifically structs inside a resource, or nested within a resource eventually through other structs, maps, lists etc.
Fixing the primary part of this is simple, by tracking the name of the Golang fields, the variables can be mapped when matching the structs up. The code change for that part is pretty simple (https://gist.github.com/frebib/f5409ff4719a465489c6ec464eefc66c/raw, with some cleanup; I'm still hacking at this)
Now the part I'm stuck at wasn't apparent to me at first, but makes sense: above takes the data from the mcl struct into the intermediate AST/graph form. We haven't covered converting it back into the resource yet. Values in the Resource struct are set here: https://github.com/purpleidea/mgmt/blob/01c2131436dc77bb9ad922c62440efab2ff7ec60/lang/structs.go#L729 but are set at the top-level in the struct, i.e. there is no recursion through the structs, maps etc to resolve anything.
An example to explain ^ better:
type Res {
X struct{ SomeName string }
}
value := struct{ somename string }{
somename: "this field will not appear",
}
...
// will succeed but SomeName will be an empty/default value
x.FieldByName("X").Set(value)
The problem is that we have the data in the value
but the struct fields have the incorrect name, so are unexported/missing/unmatched/something? Anyway, the data is just absent in the Golang struct because the names don't match even though it's passed all the way through the graph engine just fine.
The fix for this is to map the field names in ExprStruct.Value()
, the inverse lookup used to map from from the mcl:
https://github.com/purpleidea/mgmt/blob/01c2131436dc77bb9ad922c62440efab2ff7ec60/lang/structs.go#L6537-L6558
I can come up with two solutions, neither of which are straight forward:
ExprStruct.Value()
. This is a bit nasty because the unification feels like a read-only stage to me.f.Set(value)
line is rewritten into a recursive descent set of functions in the lang/types
package that sets Golang types from lang types.I'm not which option I prefer. Numero 2 would be cleaner, but is also a larger change, and possibly slower to execute too
There's a lot to discuss here, and TBH, I think we might be near the limit of how efficient we can be over text... (Of course please do attempt to send appropriate patches and tests that I can merge if you like...)
However, if it would help, we can do a gmeet session on the weekend and try and live hack this out if you'd like. It would likely take at least an hour I think because I've not looked at this code recently, but if that's helpful to you, lmk...
The TODO causes no harm as it is, but also it would be good to clean through the old comments and prune anything inaccurate or resolved as an act of declaring stability.
I don't disagree... Kindly remember that when maintaining a big complex project with no budget you heavily prioritize what you work on and aren't always happy with everything.
The code change for that part is pretty simple (https://gist.github.com/frebib/f5409ff4719a465489c6ec464eefc66c/raw, with some cleanup; I'm still hacking at this)
I think I'm roughly okay with this sort of thing modulo some harsh style nitpicking ;) -- This lib is used everywhere so I need to be careful too.
f.Set(value) // set it !
This relies on the core golang reflect magic.
There may be situations where a more complex, verbose implementation could replace something I've taken a short-cut on. I can't guarantee this, but I think it's likely. Set may be part of it... Also some of my methods in the types package are implemented as "hacks"... You should know what I mean if you have a deeper look.
HTH
Seems this is fixed with your recent patches! Thanks =D
Versions:
mgmt --version
): 01c2131436dc77bb9ad922c62440efab2ff7ec60 (master HEAD)uname -a
): Archgo version
): 1.15.6Description:
This bug (oversight? I'm not sure) was actually already discovered 2 years ago https://github.com/purpleidea/mgmt/issues/432#issuecomment-450700956 in a similar issue that may actually be the same root problem as this. The error thrown is
Using the example from that comment we can see that the resource struct has a struct child, which in theory mcl should support just fine:
with some matching mcl ala
logic would dictate that this should work fine. The structs are tagged with the
lang
tag to indicate that the mcl fields should be matched to the struct field with the same tag: mclrange.from
should match to the Go fieldRange.From
. This is exactly how it works for resource struct fields. Even thelang
struct tags are superfluous because the "default" behaviour is to use the struct field name in lowercase. That is the expected behavior and if it's not already explicitly documented, it probably should be.The lang tags are the first clue as to what the issue is here. Looking at the code, we'd expect to find something doing roughly:
What the code actually does, is convert the Go
reflect.Type
for the struct into a mgmttype.Type
withTypeOf
https://github.com/purpleidea/mgmt/blob/01c2131436dc77bb9ad922c62440efab2ff7ec60/lang/types/type.go#L137-L157 and compare that newtype.Type
struct against the struct parsed from the mcl. Note there is no "conversion" in there of Go struct field names to mcl field names: they are simply copied verbatim. This entirely ignores thelang
tag on fields if it's provided, and the implicit "lowercase by default" behaviour is skipped here too.Herein lies the first issue. Looking forward a little, the "converted" Go struct into this lang representation with uppercase named fields is compared directly with the mcl parsed struct during the unification phase, which is a logical contradiction. The two structs with fields of differing names will never match. The naive answer to this would be to just make the Go struct use lowercase names to match the mcl struct, so then they will match, but of course this isn't the correct answer either because lowercase field names in Go are unexported, therefore cannot be reflected. With these constraints understood it's clear that the current "take structs at face value" approach can never work.
Now, of course, we could just fix the code to convert Go structs to take the name conversion into account. A tiny change such as this is enough to convert uppercase Go struct field names to lowercase according to the
lang
tag, which actually makes the above example work:but this isn't all ☀️ and 🌈 either. It does make the type unification succeed because the struct field names match now, but a little while after starting mgmt, it panics because
type.Reflect
is called on a struct with unexported fields here: https://github.com/purpleidea/mgmt/blob/01c2131436dc77bb9ad922c62440efab2ff7ec60/lang/types/type.go#L742-L767432 was raised for exactly this problem, and I think only got half way to discovering what the underlying issues are.
I do find this situation a bit odd though, because this behaviour isn't present for the struct that represents the resource. Both name conversions (implicit and explicit) work fine between the Go struct and the mcl resource definition. This indicates that the codepaths for this behaviour are different and it doesn't use the general case struct code, which seems to be the case https://github.com/purpleidea/mgmt/blob/01c2131436dc77bb9ad922c62440efab2ff7ec60/lang/structs.go#L612 Should the special-casing be removed and this code be unified with the struct handling code? idk
So some takeaways from this that I have gleaned
lang
tags on struct fields are ignored sometimes but they probably shouldn't betypes.Type
data so it's available for consistent bi-directional conversionsMy gut reaction for how to tackle this is probably to track the additional Go field names in the
type.Type
data structure. The field name in that type should always be the lowercase mcl variant, but then additionally (inOrd
or in another field) the Go field name should also be stored, because it will probably never match. I do have concerns about inflating the memory usage of that struct though, so this would have to be a carefully considered change.A side-note to this is I've noticed this comment, which seems to be a good idea, and perhaps this is the appropriate time to rewrite this system to use an interface and sub-types instead of the monolithic Type struct. (A union here would be perfect) https://github.com/purpleidea/mgmt/blob/01c2131436dc77bb9ad922c62440efab2ff7ec60/lang/types/type.go#L58