Repository navigation
feat: Add version check mechanism to gRPC calls - #260
Conversation
|
I've tested this on my cluster against a variety of combinations: old/current (no version check logic) client and server with new (with version check logic) client and server, as well as different future cases with different version dependency and current version combinations in client and server. Note that this initial release has min client/daemon version requirements of old client <-> old server [ok, obviously]min-versions 0.0.0 [all ok for backwards compatibility on initial release]new client <-> old server [ok]old client <-> new server [ok]new client <-> new server [ok]min-versions 0.0.1 (or whatever the first real compatibility version break will be)new client <-> old server [warning because version was unchecked, but no way to prevent what happened]old client <-> new server [incompatible]new client <-> new server [ok]min-client-version > client version [incompatible according to daemon]min-daemon-version > daemon version [incompatible according to client] |
|
Also, tested the streaming case: new client, min-daemon=0.0.0 <-> old servernew client, min-daemon=0.0.1 <-> old serverold client <-> new server, min-client=0.0.1new client <-> new server, min-client=999.0.0new client, min-daemon=999.0.0 <-> new server |
psviderski
left a comment
There was a problem hiding this comment.
I'm sorry it took that long for me to take a look at this PR.
Overall it's clear what it does but I'm not sure I understand the idea behind MinCLIVersion and MinDaemonVersion.
Why would you want to set them globally for a client or server/daemon?
I wonder if we can make it simpler and stateless.
I can think of only one use case for this: when evolving the API and adding a new method, we want the client to not fail with Unimplemented. But instead fail with a human-friendly FailedPrecondition error that says that version X.Y.Z is required on the server, please upgrade.
Let's say the current version is 0.17.1, we're adding a new method Foo that is going to be released in 0.18.0. At the beginning of the Foo method in the client we will add something like this:
ctx = metadata.AppendToOutgoingContext(ctx, MetadataKeyMinDaemonVersion, "0.18.0")
When the user then use an updated uc CLI to 0.18.0 against a machine that is still on <0.18.0, the commands that use the new method Foo will fail with an error that minimal daemon version is not satisfied for this command. But other older commands will continue to work.
Some CLI commands may also check the server version and branch the logic based on it if needed, e.g. use old deprecated methods for older servers but new methods for new ones.
Ah, I just recalled the big change to the proxy layer to use a new format for machines header. I guess this is the case when you'd consider setting global min version, right?
I wonder whether this is still needed. There are two cases:
- old client -> new server
On the server we can assume new clients use the newmachinesformat, so the director can check if the client is>=0.18.0then parse the new format. If missing, then use the old ipv6 format. So still support both for a while. - new client -> old server
We can refactor the code so that we use onlyProxyMachinesContexteverywhere (replaceproxyToMachine). Then make some direct grpc call fromProxyMachinesContextto extract the version of the server from the header in the response. Then based on it decide whatmachinesformat to use. So basically still have the current logic of resolving names to IPs as a fallback for the old servers.
This looks like we can even make the change to the proxy backward compatible this way if I'm not missing anything. But as I mentioned earlier it might not worth it.
I was just thinking out loud. I hope this makes sense. Please let me know what you think when you have time.
| proxyServer := grpc.NewServer( | ||
| grpc.ForceServerCodecV2(proxy.Codec()), | ||
| grpc.UnaryInterceptor(versionpkg.ServerUnaryInterceptor), | ||
| grpc.StreamInterceptor(versionpkg.ServerStreamInterceptor), |
There was a problem hiding this comment.
Not sure I understand why we put server interceptors on the proxies. Especially this non-local one that can proxy to different gRPC servers which could have different versions.
I'd put them on the local machine server:
uncloud/internal/machine/machine.go
Line 303 in 51c7b5b
The actual server that responds, not the proxies that just relay messages so that no matter where the request comes from, it goes through the interceptors on each machine. Am I missing something?
There was a problem hiding this comment.
This isn't necessary for the proxy refactor change, but it could be relevant for other kinds of backwards-compatibility breaking changes.
For example, if the semantics of a gRPC function itself changed, then it's the target daemon we are proxying to that needs to be checking versions as well.
The client has a min-daemon of "1.2" and connects to a machine running daemon "1.2" (so that check passes) but it wants the command proxy'd to another machine. And that machine is running daemon "1.1" still, so it needs to check the version, too.
Otherwise the new "foobar" implementation in 1.2 that the client is expecting is going to actually get the old 1.1 response. 😄
There was a problem hiding this comment.
And that machine is running daemon "1.1" still, so it needs to check the version, too.
Still not following why this should be checked at the proxy, not at the server on the machine with 1.1 version.
For example, if the semantics of a gRPC function itself changed, then it's the target daemon we are proxying to that needs to be checking versions as well.
I think I know what you're talking about but that should be a super rare case. Changing the semantics of an API call is shooting yourself in the foot and asking for trouble. A changed semantics should usually be implemented as a new method to not deal with any versioning, only the minimal version the method was added in.
There was a problem hiding this comment.
Still not following why this should be checked at the proxy, not at the server on the machine with 1.1 version.
Ah, I see. It's my understanding that the client connection (or remote server proxying to it) comes in at this proxy server (and then routes it to its local machine server and/or remote servers' proxy server entry point).
So we need to do the version check there -- at the proxy entry point and before we get to the local machine server -- in order to make use of it for changes to the proxy routing logic itself (like we need for the proxy refactor branch).
It's entirely possible I missed something here, though, and have the connection flow wrong.
Also, we might need to add it to the local machine server, as well, if it is possible something connects directly to it and not via its proxy server.
Yes, this was intended for complex, backwards incompatible changes, possibly affecting many commands. A change like the proxy refactor doesn't just throw errors when things are mismatched, it can silently do the wrong thing in some cases. For example, it could run a command on a different server than the user intended and return that result with no obvious errors or exceptions. It was also meant to be a general solution for changes like it that may be required in the future. It's certainly overkill for something as simple as a new gRPC method missing, but it would enable forcing an upgrade if we ever need to change the "semantics" of an existing call. e.g. something with the deploy process that would be similarly painful to support multiple versions of for the sake of backwards compatibility. Also, this implementation has some bits that are dealing with the initial transition from no version info available, which is why things like the client check on the server response is needed to at least warn the user after the fact.
That's the pre-flighting approach, which I was trying to avoid needing with this implementation. For the proxy refactor, that means a preflight check on a significant fraction of all calls the the cli makes. |
|
Also, for clarity, the reason for the two different "min-versions":
This is so we don't need to preflight requests: the newer client code knows it needs a daemon of at least version x.y.z to handle its request properly. And any version-checking daemon is capable of recognizing it is below the threshold and can then abort with an immediate error.
This is so the server can just tell the client it is too old and no longer supported. We could still support a transition phase of backwards compatibility with the server having conditional responses based on the client version (e.g. return v0.1 payload, v0.2 payload, etc). But when we finally drop support for an old client (whether that is immediately or eventual), the min-client-version gives us a cut-off where we can safely know that old clients will just stop working until upgraded. |
|
I'm happy to merge this if we can remove the checks that are necessary only for the transition period after a few more releases. Adding TODOs for this and transferring explanations from here to the code comments will also be helpful, especially the version constants in |
Sounds good. I still have a few more follow-ups to make and then re-run my manual tests (hopefully in the next day or two). |
…ed by both pkg/api/ and pkg/client/
…can be removed eventually after transition to version checking client/daemons
|
Okay, I believe have made all of the updates we discussed. Let me know if you have any other questions, particularly about the server interceptors on the proxy endpoints. |
psviderski
left a comment
There was a problem hiding this comment.
Thank you for the updates. Will resolve conflicts and merge
This adds a minimum version mechanism to gRPC communication to ensure CLI and daemon versions are compatible. It enables future breaking changes by allowing minimum version requirements to be enforced.
See #247 for example use-case.
Changes:
pkg/versioncheckpackage with gRPC interceptorsx-uncloud-cli-version,x-uncloud-min-daemon-version) into requestsx-uncloud-daemon-versionBehavior:
Files changed:
pkg/versioncheck/interceptor.go- new package with interceptorspkg/client/connector/*.go- added client interceptorsinternal/machine/machine.go- added server interceptors to proxy servers