-
Notifications
You must be signed in to change notification settings - Fork 18.7k
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
allow client to talk to an older server #27745
Conversation
242b025
to
13f5cf3
Compare
a86f4b7
to
ec3a34b
Compare
|
@vieux I don't understand that error message - perhaps a rewording would help? |
@duglin thanks, I'll reword the error message. and thanks good catch, the client server should either say |
6ac2bb8
to
3395d09
Compare
@vieux in this mode of operation am I correct that this is using the data itself (ie. incoming fields vs what the server knows about) to determine if it can support the incoming request rather than the version string itself? |
3395d09
to
bb78253
Compare
@vieux As an extended response to your comment on #27830, instead of using a header, perhaps an additional API endpoint could be used that reports the latest API version. Something like:
(could also be something like In a potential future where a version is required, this could be used as the entrypoint for clients to figure out what versions can be used. Seems more solid and explicit than using a header on another API endpoint. |
bb78253
to
24f8419
Compare
return nil | ||
} | ||
|
||
// OriginalClientVersion returns the origin client version. |
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.
returns the original client version
? 😝
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.
:)
@@ -183,9 +183,9 @@ func (cli *Client) Close() error { | |||
|
|||
// getAPIPath returns the versioned request path to call the api. | |||
// It appends the query parameters to the path if they are not empty. | |||
func (cli *Client) getAPIPath(p string, query url.Values) string { | |||
func (cli *Client) getAPIPath(p string, query url.Values, ignoreVersion bool) string { |
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.
ignoreVersion
is only for the _ping
endpoint, right ?
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.
yes
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.
I wonder if it better to have a ignoreVersion
bool or handle the _ping
endpoint by itself in the method 😛
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.
I think that's a good idea (have _ping
not call this)
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.
I think we still need to go through the rest of the function:
u := &url.URL{
Path: apiPath,
}
if len(query) > 0 {
u.RawQuery = query.Encode()
}
return u.String()
I don't like this solution either, but from client.Ping
to getAPIPath
it goes through rougly 4 functions. not sure how to handle that better. I could replace the Context
part by a bool
but not sure it would be enough ?
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.
and by that I mean:
Client.Ping
-> Client.get
Client.get
-> Client.sendRequest
Client.sendRequest
-> Client.sendClientRequest
Client.sendClientRequest
-> Client.newRequest
Client.newRequest
-> Client.getAPIPath
The best I could see is to add a bool to newRequest and getAPIPath and do:
Client.Ping
-> Client.sendClientRequestIgnoreVersion
Client.sendClientRequestIgnoreVersion
-> Client.newRequest(ignoreVersion=true)
Client.newRequest
-> Client.getAPIPath(ignoreVersion=true)
wdyt @dnephin @vdemeester
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.
@vieux this client/request.go
has a few issues I think. I've been meaning to refactor these methods for a while now. I think the problem we're seeing here is a symptom of those issues.
I refactored the send methods here: master...dnephin:refactor-client
That branch includes a commit which uses the refactored functions to call Ping()
without the api version in the path. It removes the need to have the ignoreVersion
parameter entirely.
If this works for you, would you like to cherry pick the first commit into this branch? Otherwise I can drop the change to ping.go
and open it as a PR. Let me know which you prefer.
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.
Opened #27916
24f8419
to
56d08df
Compare
I think adding it to |
keyFile string | ||
client client.APIClient | ||
hasExperimental bool | ||
originalClientVersion string |
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.
What do you think about another word for original
?
Maybe defaultVersion
, maxSupportedVersion
, or preferredVersion
?
@@ -72,13 +72,19 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { | |||
hide(system.NewInspectCommand(dockerCli)), | |||
) | |||
|
|||
if versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.25") { | |||
cmd.AddCommand(system.NewVersionCommand(dockerCli)) | |||
} |
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.
hmm, why is the version command gated by server version?
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.
Sorry, I tought "system." was for the new data management commands, I'll remove that.
@@ -183,9 +183,9 @@ func (cli *Client) Close() error { | |||
|
|||
// getAPIPath returns the versioned request path to call the api. | |||
// It appends the query parameters to the path if they are not empty. | |||
func (cli *Client) getAPIPath(p string, query url.Values) string { | |||
func (cli *Client) getAPIPath(p string, query url.Values, ignoreVersion bool) string { |
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.
I think that's a good idea (have _ping
not call this)
|
||
if config != nil && config.StopTimeout != nil && versions.LessThan(cli.version, "1.25") { | ||
return response, fmt.Errorf("Server only supports API version %s, %q support requires at least %s", cli.version, "stop timeout", "1.25") | ||
} |
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.
Could you make this a function, so that all the error messages have a consistent format and wording.
func NewVersionError(version, required, feature string) error {
...
}
If you make it a method on Client
you could even drop the version
argument.
For the wording how about:
"stop timeout" requires API version 1.25, but the Docker server is version 1.24.
} | ||
|
||
return true, nil | ||
return true, apiVersion, nil |
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.
Instead of three return values, maybe a struct like we use for other responses?
@@ -50,6 +48,11 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { | |||
flags.StringVar(&opts.ConfigDir, "config", cliconfig.ConfigDir(), "Location of client config files") | |||
opts.Common.InstallFlags(flags) | |||
|
|||
// flags must be the top-level command flags, not cmd.Flags() | |||
opts.Common.SetDefaultOptions(flags) |
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.
I think this is wrong. The flags have not been parsed yet so the values that it sets are going to be wrong I believe.
} | ||
|
||
// Ping pings the server and retrieve the version to use and if experimental is enabled. | ||
func (cli *DockerCli) Ping() (err error) { |
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.
Ping()
is maybe not the best name for this, how about making this an unexpected func that is called from Initialize()
? initializeVersion()
?
@@ -50,6 +48,11 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { | |||
flags.StringVar(&opts.ConfigDir, "config", cliconfig.ConfigDir(), "Location of client config files") | |||
opts.Common.InstallFlags(flags) | |||
|
|||
// flags must be the top-level command flags, not cmd.Flags() | |||
opts.Common.SetDefaultOptions(flags) | |||
dockerCli.Initialize(opts) |
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.
same here, this is too early to call Initialize()
because the flags haven't been parsed yet.
// flags must be the top-level command flags, not cmd.Flags() | ||
opts.Common.SetDefaultOptions(flags) | ||
dockerCli.Initialize(opts) | ||
dockerCli.Ping() |
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.
I think this presents a serious UX problem.
If I run docker
or docker --help
to find out how to connect to an alternative host I'm going to get a connection error instead of the help text.
We can't make any API calls before flag parsing is complete, because we won't know which host we're connecting to until after flag parsing. But flag parsing has to happen after we setup the child commands. Which means the experimental support also has this issue. I don't think we can ship 1.13 this way.
The order of events needs to be this:
- setup child commands and flags
- parse flags
- initialize client
- execute command
That means that flags and child commands can not depend on the server version.
I think we should do this:
- do not try and ping the server
- go back to the original plan with the version header, where we make a request first then fall back on error.
- for experimental use
Hidden: isExperimental
on the experimental commands. Add an environment variableDOCKER_EXPERIMENTAL
that enables showing the commands by setting isExperimental=true`.
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.
If I run docker or docker --help to find out how to connect to an alternative host I'm going to get a connection error instead of the help text.
This is not correct, the error is not checked at this point. (At least that's how I planned it, maybe I did a mistake in the code)
In my option it works ok:
docker --help
that is able to talk to an experimental server will display experimental commands and commands supported by the serverdocker --help
that is able to talk to a stable server will not display experimental commands and commands supported by the serverdocker --help
that is not able to connect to anything will not display experimental commands and all the commands known to the cli
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.
ping @mlaventure since it's not related specifically to the API versioning, but also experimental.
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.
Ok, I guess my example was wrong, but I think the rest is still correct.
The server that it checks will be either the default (or the one set by DOCKER_HOST
maybe, I'm not sure if that's read at this time). The --host
or -H
flag isn't parsed yet, so the behaviour is wrong if you have to use --host
.
This could lead to some really strange behaviour where there is both a local daemon and some cloud edition. The command line is based on the local daemon, but then after flag parsing it points at the remote engine, which results in the incorrect behaviour when using any unsupported flags, instead of the helpful error message from the client.
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.
Hum, yes correct. It does not work with --host
(it works with DOCKER_HOST
)
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.
oh ok I get what you mean @dnephin .
So how about we remove all the checks from the cli
pkg, so we always display commands/flags but we error out in the client
package only instead ?
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.
That might fix the issue. I think we could still hide the commands with this strategy: #27745 (comment)
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.
c698021
to
d65d16e
Compare
591eabe
to
e98e4a7
Compare
Pull request moby#27745 added support for the client to talk to older versions of the daemon. Various flags were added to docker 1.13 that are not compatible with older daemons. This PR adds annotations to those flags, so that they are automatically hidden if the daemon is older than docker 1.13 (API 1.25). Not all new flags affect the API (some are client-side only). The following PR's added new flags to docker 1.13 that affect the API; - moby#23430 added `--cpu-rt-period`and `--cpu-rt-runtime` - moby#27800 / moby#25317 added `--group` / `--group-add` / `--group-rm` - moby#27702 added `--network` to `docker build` - moby#25962 added `--attachable` to `docker network create` - moby#27998 added `--compose-file` to `docker stack deploy` - moby#22566 added `--stop-timeout` to `docker run` and `docker create` - moby#26061 added `--init` to `docker run` and `docker create` - moby#26941 added `--init-path` to `docker run` and `docker create` - moby#27958 added `--cpus` on `docker run` / `docker create` - moby#27567 added `--dns`, `--dns-opt`, and `--dns-search` to `docker service create` - moby#27596 added `--force` to `docker service update` - moby#27857 added `--hostname` to `docker service create` - moby#28031 added `--hosts`, `--host-add` / `--host-rm` to `docker service create` and `docker service update` - moby#28076 added `--tty` on `docker service create` / `docker service update` - moby#26421 added `--update-max-failure-ratio`, `--update-monitor` and `--rollback` on `docker service update` - moby#27369 added `--health-cmd`, `--health-interval`, `--health-retries`, `--health-timeout` and `--no-healthcheck` options to `docker service create` and `docker service update` Signed-off-by: Sebastiaan van Stijn <[email protected]>
Maybe I'm missing something but it looks like I cannot use a Docker 1.13 client to send an info request to a Docker 1.10 server. Reproduction steps: # Start a Docker 1.10 in Docker container and expose the Docker API port locally
$ docker run --privileged --name docker110 -d -p 7110:2375 docker:1.10-dind
e937dbff2126d02d0b33c337b50bf3069261a131c04eaf77dc219964a0efdf0d
# Query the Docker 1.10 engine
$ docker -H localhost:7110 info
Error response from daemon: client is newer than server (client API version: 1.24, server API version: 1.22)
# Docker version output
$ docker version
Client:
Version: 1.13.0
API version: 1.25
Go version: go1.7.3
Git commit: 49bf474
Built: Tue Jan 17 09:50:17 2017
OS/Arch: linux/amd64
Server:
Version: 1.13.0
API version: 1.25 (minimum version 1.12)
Go version: go1.7.3
Git commit: 49bf474
Built: Tue Jan 17 09:50:17 2017
OS/Arch: linux/amd64
Experimental: false |
@deviantony correct, "auto negotiation" works with a header that's sent by the daemon; since that header is added in docker 1.13, docker will only automatically fall back to version 1.12, which is the last version that did not set that header (see cli/command/cli.go#L169-L172) If you want the client to talk to an even older version of the daemon, you can manually force it to use this API version through the
|
@thaJeztah, regarding interoperability with servers older than 1.12 (the blog does not note that restriction by the way), even without the header, it appears that the client does know the server's API version (e.g. the error message displays it... Or could |
Thanks for noticing that; I'll see if I can find the right person to update the blog and clarify that
The client does not know the daemon version, but only prints the error; if you look at the error;
So the client tried to connect to the daemon through API 1.24, but the daemon did not support anything above 1.22. While this information could be used for negotiation, the current design is to use a dedicated API endpoint for that. Also note that
This was implemented for a short while, but led to incorrect issues where the original machine was a 1.12 daemon, but upgraded to 1.13, but the environment variable still forced the client to use the old API. |
Thanks @thaJeztah. So is the recommendation for using older than 1.12 servers to continue to use |
I think the recommendation would be to update those servers 😇 , but yes, using |
allow client to talk to an older server
Pull request moby/moby#27745 added support for the client to talk to older versions of the daemon. Various flags were added to docker 1.13 that are not compatible with older daemons. This PR adds annotations to those flags, so that they are automatically hidden if the daemon is older than docker 1.13 (API 1.25). Not all new flags affect the API (some are client-side only). The following PR's added new flags to docker 1.13 that affect the API; - moby/moby#23430 added `--cpu-rt-period`and `--cpu-rt-runtime` - moby/moby#27800 / moby/moby#25317 added `--group` / `--group-add` / `--group-rm` - moby/moby#27702 added `--network` to `docker build` - moby/moby#25962 added `--attachable` to `docker network create` - moby/moby#27998 added `--compose-file` to `docker stack deploy` - moby/moby#22566 added `--stop-timeout` to `docker run` and `docker create` - moby/moby#26061 added `--init` to `docker run` and `docker create` - moby/moby#26941 added `--init-path` to `docker run` and `docker create` - moby/moby#27958 added `--cpus` on `docker run` / `docker create` - moby/moby#27567 added `--dns`, `--dns-opt`, and `--dns-search` to `docker service create` - moby/moby#27596 added `--force` to `docker service update` - moby/moby#27857 added `--hostname` to `docker service create` - moby/moby#28031 added `--hosts`, `--host-add` / `--host-rm` to `docker service create` and `docker service update` - moby/moby#28076 added `--tty` on `docker service create` / `docker service update` - moby/moby#26421 added `--update-max-failure-ratio`, `--update-monitor` and `--rollback` on `docker service update` - moby/moby#27369 added `--health-cmd`, `--health-interval`, `--health-retries`, `--health-timeout` and `--no-healthcheck` options to `docker service create` and `docker service update` Signed-off-by: Sebastiaan van Stijn <[email protected]> Upstream-commit: 5d2722f83db9e301c6dcbe1c562c2051a52905db Component: cli
Client and server don't have the same version
error)client is newer than server
errorAPI-Version
header on all API callsPing()
always calls/_ping
, not/v1.xx/_ping
if
where flags/features are only supported in1.25
ping @dnephin