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

Che server should connect to ws-agent on internal URL (#2030) #2837

Closed
wants to merge 2 commits into from

Conversation

amisevsk
Copy link
Contributor

@amisevsk amisevsk commented Oct 19, 2016

What does this PR do?

Enables che-server to use the internal address of wsagent containers when it is running
in docker through setting an environment variable.

When CHE_DOCKER_USE_INTERNAL_CONTAINER_ADDRESS is set to "true", the
ServerProperties objects returned as part of DockerInstanceRuntimeInfo#getServers()
will use the internal address of the container as obtained from docker inspect.

If the environment variable is not set, then behaviour is unchanged.

This is a WIP so comments / suggestions are greatly appreciated.

What issues does this PR fix or reference?

Che server should connect to ws-agent on internal URL (#2030)

Previous behavior

Value used for internal address could cause issues contacting wsagent if firewall interfered.

New behavior

Che-server can now optionally communicate with wsagent directly.

PR type

  • Minor change = no change to existing features or docs
  • Major change = changes existing features or docs

Minor change checklist

  • New API required?
  • API updated
  • Tests updated
  • Tests passed

Signed-off-by: Angel Misevski amisevsk@redhat.com

@codenvy-ci
Copy link

Can one of the admins verify this patch?

@TylerJewell TylerJewell added this to the 5.0.0-M7 milestone Oct 19, 2016
@TylerJewell TylerJewell added kind/enhancement A feature request - must adhere to the feature request template. team/enterprise labels Oct 19, 2016
@TylerJewell
Copy link

This improvement addresses issues on certain linux systems where Che does not work with iptables or firewalld. By connecting over the internal address, communication flow will not go through the firewall which blocks access.

@TylerJewell
Copy link

-[ ] Requires updates to the Networking docs for Eclipse Che page to explain the scenario.

@benoitf
Copy link
Contributor

benoitf commented Oct 19, 2016

ci-build

@codenvy-ci
Copy link

Build # 747 - FAILED

Please check console output at https://ci.codenvycorp.com/job/che-pullrequests-build/747/ to view the results.

@amisevsk
Copy link
Contributor Author

I've rebased this PR, and dropped a commit that fixed an issue that has been since fixed in master (see d74b24f).

Copy link

@garagatyi garagatyi left a comment

Choose a reason for hiding this comment

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

Please add fixes in accordance to comments

* @return true if {@code CHE_DOCKER_USE_INTERNAL_CONTAINER_ADDRESS} is "true", false otherwise.
*/
protected boolean useInternalContainerAddresses() {
String useInternalContainerAddresses = System.getenv(CHE_DOCKER_USE_INTERNAL_CONTAINER_ADDRESS);

Choose a reason for hiding this comment

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

In Che we use system variables in a different way. We declare property from DI container and it should use environment variable if it is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, could you clarify this point? Do you mean that it is preferable to control this behaviour through a property defined in che.properties?

Choose a reason for hiding this comment

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

Exactly! And it can also be configured using env variable that match rules of CHE of conversion env variable into property.
The idea is that we use only named injection in Java code and CheBootstrap handles injection of all properties and environment variables and has everything needed to improve code readability.
If you want to know more about that, please, ask.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you, that makes a lot of sense! I've updated the PR to use a property (currently called che.docker.ip.useinternaladdress -- open to suggestions).

A few questions regarding the conversion of env vars to properties though:

  1. in CheBootstrap, I see that env vars are converted by lowercasing them and replacing underscores with periods. However, some of the properties have underscores in their name, which makes it impossible to set them with environment variables, and prevents the new property here from having underscores (che.docker.ip.use_internal_address would make more sense).

  2. In LocalDockerInstanceRuntimeInfo, it looks like this convention is ignored: the methods externalHostnameWithPrecedence and internalHostnameWithPrecedence use environment variables to override their behavior -- is this intentional or should it be fixed?

Choose a reason for hiding this comment

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

  1. I submitted a PR to fix that. I'm going to merge it soon. Configuring of named variable with underscore in name doesn't work if it was set using environment variables #2454
  2. Probably it was implemented before we merged feature with automatic conversion. Or we missed it in PR review.
    I believe that it should be fixed. @amisevsk If you want you can fix it also.
    @TylerJewell should env variables be renamed to match properties names or it is better to try to use new property aliases feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@garagatyi Thanks for pointing me to that PR -- I've renamed the property to che.docker.ip.use_internal_address for readability. This has the side effect of not being able to override the property with an environment variable until your change is merged.

I've modified LocalDockerInstanceRuntimeInfo to not use environment variables anymore and updated the docs. Previously, the properties che.docker.ip and che.docker.ip.external were overridden through both the environment variable CHE_DOCKER_IP/CHE_DOCKER_IP_EXTERNAL but also through CHE_DOCKER_MACHINE_HOST and CHE_DOCKER_MACHINE_HOST_EXTERNAL. With the change only the former has an effect.

@@ -310,16 +316,33 @@ public String projectsRoot() {
protected Map<String, ServerImpl> getServersWithFilledPorts(final String externalHostame, final String internalHostname, final Map<String, List<PortBinding>> exposedPorts) {
final HashMap<String, ServerImpl> servers = new LinkedHashMap<>();

boolean useMappedPorts = true;
if (useInternalContainerAddresses()) {

Choose a reason for hiding this comment

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

It is better to do this inside of constructor to keep main code clean

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved.

boolean useMappedPorts = true;
if (useInternalContainerAddresses()) {
if (info.getNetworkSettings() != null && internalHostname.equals(info.getNetworkSettings().getIpAddress())) {
useMappedPorts = false;

Choose a reason for hiding this comment

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

Can you elaborate why we should care about internalHostname field if usage of IP address provided by Docker is set by configuration?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was meant mostly as a sanity check, although even as that I'm not sure it's handled as well as it could be. internalHostname is still the address used to communicate with wsagent, and I was concerned that it may be possible to create an instance of DockerInstanceRuntimeInfo with internalHostname not matching info.getNetworkSettings().getIpAddress(), in which case contacting wsagent would fail even if internalHostname is valid.

If you feel that it's not a concern, I will remove it.

Choose a reason for hiding this comment

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

My point that if someone configured property/env variable that enables useInternalContainerAddresses mode then he definitely want to use it. And If it does match internalHostname then this mode doesn't make sense.
So I treat this mode as an alternative to default mode with set internalHostname.
Please correct me if I misunderstood the goal of this contribution.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, that was the goal. I'll remove this check and rename the variable for clarity.

if (useMappedPorts) {
internalHostnameAndPort = internalHostname + ":" + externalPort;
} else {
String internalPort = portEntry.getKey().split("/")[0];

Choose a reason for hiding this comment

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

It is better to use portProtocol variable here instead of portEntry.getKey()

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.

@bmicklea bmicklea modified the milestones: 5.0.0-M8, 5.0.0-M7 Nov 1, 2016
@amisevsk amisevsk force-pushed the CHE-2030-s branch 2 times, most recently from 2504e74 to a0edb9c Compare November 2, 2016 21:31
Add ability to make che-server use internal address of
workspaces when che-server is running in docker through
property `che.docker.ip.useinternaladdress` or
environment variable CHE_DOCKER_IP_USEINTERNALADDRESS.

When property is set to "true", internal address is set
to the internal address of the relevant wsagent docker
container. Otherwise, behavior is unchanged.

Signed-off-by: Angel Misevski <amisevsk@redhat.com>
String internalHostnameAndPort;
if (useInternalAddress) {
String internalPort = portProtocol.split("/")[0];
internalHostnameAndPort = internalHostname + ":" + internalPort;

Choose a reason for hiding this comment

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

In that case it is supposed that browser will connect to ephemeral port or exposed port? I'm asking because as far as I remember it should be exposed port, but I'm not sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To my understanding, the browser will use the exposed port; the internalHostname and internalPort should be used only be che-server.

Choose a reason for hiding this comment

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

Do you really mean exposed port or ephemeral port which is mapped to exposed port?

@l0rd Maybe you can help to understand how this workflow is supposed to work? I know you explain such things very clear.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's make an example. wsagent is available on ports:

  • 4401 on docker0 network (let's call it the exposed port)
  • 32801 on host network (let's call it the ephemeral port).

Browsers use address: externalHostname:32801
Che server used address internalHostname:32801 (before this PR)

After this PR Che server should use address containerIP:4401

The difference is between ephemeral and exposed ports but also between internalHostname and containerIP:

  • internalHostname= the hostname of the host where docker is running and in most cases is identical to externalHostname
  • containerIP= is the IP address of the workspace container, it's an IP of the docker0 network (you can get this IP address using docker inspect -f '{{ .NetworkSettings.IPAddress }}' <containerID>).

Looking to the code of this PR it seems to me internalHostname is still used by the Che server. containerIP should be used instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

@amisevsk there is a typo in variable externalHostame. You haven't introduced that (that was probably me 😊) but it would be cool if you could fix it => externalHostname

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@l0rd I've fixed the typo now. Could you elaborate more on the issue of containerIP vs internalHostname?

@garagatyi Sorry for the confusion -- I meant exposed port in the sense of "exposed to the outside world", e.g. Docker would map the container at 172.17.0.2 with port 4401 (docker0 network) to 172.17.0.1 (address of docker0 network) with port 32771. The browser should contact the container at 172.17.0.1:32771 while the che-server should use 172.17.0.2:4401. I had my definition of exposed and ephemeral backwards.

Copy link
Contributor

Choose a reason for hiding this comment

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

@amisevsk I understand now that internaHostname can be either containerIP or internalHostname here. The logic to set the right is in class LocalDockerInstanceRuntimeInfo.

Choose a reason for hiding this comment

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

Ok, now it is clearer for me. Thanks @l0rd and @amisevsk. Mario explains such things in a really clear way, as always. 😄

* <p>Value of external hostname can be retrieved from property ${code machine.docker.local_node_host.external} or
* from environment variable {@code CHE_DOCKER_MACHINE_HOST_EXTERNAL}.<br>
* Environment variables have higher priority.
* <p>If environment variable {@code CHE_DOCKER_IP_USE__INTERNAL__ADDRESS} or

Choose a reason for hiding this comment

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

Javadocs should use properties for explanation of usage. Whereas dependency container can inject it in several ways.

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've removed references to environment variables in the javadoc.

@@ -142,6 +142,12 @@ che.docker.ip=NULL
# This is unusual, but happens for example in Docker for Mac when containers are in a VM.
che.docker.ip.external=NULL

# If true, then uses the internal address and port of workspace Docker containers (i.e. within the Docker
# Docker network) instead of the address and port provided by the Docker daemon. May be necessary if the
Copy link
Contributor

Choose a reason for hiding this comment

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

I find confusing the sentence "instead of the address and port provided by the Docker daemon". In fact the Docker daemon never provides the address of the container as far as I know. It does maps the ports of a container (that are exposed to the docker0 network) to some corresponding ephemeral ports exposed to the host network. I would rather change it to "instead of the docker host address and the port provided by the Docker daemon".

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've updated the sentence.

String internalHostnameAndPort;
if (useInternalAddress) {
String internalPort = portProtocol.split("/")[0];
internalHostnameAndPort = internalHostname + ":" + internalPort;
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's make an example. wsagent is available on ports:

  • 4401 on docker0 network (let's call it the exposed port)
  • 32801 on host network (let's call it the ephemeral port).

Browsers use address: externalHostname:32801
Che server used address internalHostname:32801 (before this PR)

After this PR Che server should use address containerIP:4401

The difference is between ephemeral and exposed ports but also between internalHostname and containerIP:

  • internalHostname= the hostname of the host where docker is running and in most cases is identical to externalHostname
  • containerIP= is the IP address of the workspace container, it's an IP of the docker0 network (you can get this IP address using docker inspect -f '{{ .NetworkSettings.IPAddress }}' <containerID>).

Looking to the code of this PR it seems to me internalHostname is still used by the Che server. containerIP should be used instead.

String internalHostnameAndPort;
if (useInternalAddress) {
String internalPort = portProtocol.split("/")[0];
internalHostnameAndPort = internalHostname + ":" + internalPort;
Copy link
Contributor

Choose a reason for hiding this comment

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

@amisevsk there is a typo in variable externalHostame. You haven't introduced that (that was probably me 😊) but it would be cool if you could fix it => externalHostname

if (useInternalAddress) {
String containerHostName = null;
if (networkSettings != null) {
containerHostName = networkSettings.getIpAddress();
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this IP address the IP of the Che server container? What we need is instead the IP addresses of the workspace containers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As far as I can tell, the ContainerInfo (which provides NetworkSettings) injected into the LocalDockerInstanceRuntimeInfo constructor is obtained from DockerConnector.inspectContainer(), where the container inspect is the workspace container. See DockerInstance.java.

Copy link
Contributor

Choose a reason for hiding this comment

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

@amisevsk you are right. Sorry about that :-)

Signed-off-by: Angel Misevski <amisevsk@redhat.com>
@l0rd
Copy link
Contributor

l0rd commented Nov 16, 2016

LGTM good job @amisevsk!

@garagatyi
Copy link

@l0rd @amisevsk Honestly existing code is already over-complicated. And this PR makes what is happening here even less clear. I'll suggest a way how to refactor this code below. Please share your thoughts about this idea. Do you think that this approach is clearer and more extensible than current one?

We can refactor this code in such a way:
DockerInstanceRuntimeInfo accepts:

  • servers internal host (nullable, can be IP)
  • container JSON data object
  • provider of new class which represents host&port evaluation strategy

... other things not related to this topic
it doesn't accept:

  • servers external host, since it is always set to null for now - looks like it is useless now

HostPortEvaluationStrategyProvider (name just for example, feel free to use another name) uses property that defines chosen strategy impl and set of strategies impls and return needed HostPortEvaluationStrategy implementation.
HostPortEvaluationStrategyProvider accepts internal host and container JSON and pass it to HostPortEvaluationStrategy impl.
DockerInstanceRuntimeInfo calls HostPortEvaluationStrategy#getServer(String exposedPortProtocol).
HostPortEvaluationStrategy impl forms external/internal addresses using its own logic.
In that case each vendor may bind his own implementation of HostPortEvaluationStrategy and set it as current one for Che assembly.

My description looks a bit complicated when I read it, but it is not really complex. So please ask me for an explanation if this algorithm is not clear enough to you.

@l0rd
Copy link
Contributor

l0rd commented Nov 16, 2016

@garagatyi I like your approach and I agree that current code is overcomplicated. Just a couple of comments:

  • Why do you think that servers external host is useless? It should be used when using docker4mac or with K8s/OpenShift etc..
  • Would it be ok to do the refactoring as a separate PR? That would make the refactoring PR easier to review.

@garagatyi
Copy link

garagatyi commented Nov 16, 2016

Why do you think that servers external host is useless? It should be used when using docker4mac or with K8s/OpenShift etc..

I mean it is useless in DockerInstanceRuntimeInfo constructor. Later I would want to refactor DockerInstance code again and remove DockerNode because it looks odd to me. I think we can move internal/external address setting into suggested HostPortEvaluationStrategy.
WDYT?

Would it be ok to do the refactoring as a separate PR? That would make the refactoring PR easier to review

Do you suggest to merge this PR and make another one with refactoring or just open another one with the refactoring? If first then we will have to pass 2 QA cycles - for 1st and 2nd PRs.

@bmicklea bmicklea added the status/code-review This issue has a pull request posted for it and is awaiting code review completion by the community. label Nov 16, 2016
@l0rd
Copy link
Contributor

l0rd commented Nov 17, 2016

I mean it is useless in DockerInstanceRuntimeInfo constructor. Later I would want to refactor DockerInstance code again and remove DockerNode because it looks odd to me. I think we can move internal/external address setting into suggested HostPortEvaluationStrategy.
WDYT?

Ok I see what you mean. It makes sense. And we should move internalhost parameter to HostPortEvaluationStrategy too.

Do you suggest to merge this PR and make another one with refactoring or just open another one with the refactoring? If first then we will have to pass 2 QA cycles - for 1st and 2nd PRs.

I guess that's not ideal because QA cycles are manual and take time. That's ok I will talk with @amisevsk to see how to handle this.

@amisevsk
Copy link
Contributor Author

@garagatyi I agree that the current setup is probably more complicated than it should be, and would be happy to do the refactor you suggest.

For my understanding: Currently, DockerInstanceRuntimeInfo takes internal/external hostname, the ContainerInfo json, and Sets of ServerConf. When DockerInstanceRuntimeInfo#getServers() is called, DockerInstanceRuntimeInfo

  1. gets port mappings and labels from ContainerInfo json
  2. calls DockerInstanceRuntimeInfo#getServersWithFilledPorts(), which
    1. parses the ports json object from (1)
    2. for each port mapping (e.g. 22/tcp=[PortBinding{hostIp='0.0.0.0', hostPort='32898'}]) gets the protocol (22/tcp) and the external port (32898)
    3. adds an entry in a Map<String, ServerImpl> which pairs the desired port (32898) with internal and external hostname (from the constructor)
  3. The Map from (2) is passed to DockerInstanceRuntimeInfo#addRefAndUrlToServers() which updates each ServerImpl, adding ref (e.g 22/tcp -> ssh) and relevant parts of ServerPropertiesImpl (e.g. path, internal URL)
  4. The updated Map from (3) is passed to DockerInstanceRuntimeInfo#AddDefaultReferenceForServersWithoutReference(), which puts a default value for ref if it is not set.

The result of (4) is what is returned by getServers().

If I understand you correctly, you're suggesting that steps 2-4 are moved into the HostPortEvaluationStrategy impl, and DockerInstanceRuntimeInfo#getServers() instead iterates over the port mappings obtained in (1), calling HostPortEvaluationStrategy#getServer(String portProtocol) and adding the returned ServerImpl to the map it returns.

That makes sense to me. A few things I'm still not clear on though. It's maybe important to note that with the set up I'm running, the constructor for DockerInstanceRuntimeInfo is invoked from LocalDockerInstanceRuntimeInfo.

  1. How do we define HostPortEvaluationStrategies? Currently, the modes of function defined by properties seem to be

    • Default: use InternalHostname for internal and external address, and external ports
    • If containerExternalHostname is provided, use that for external address
    • (My changes) If property che.docker.ip.use_internal_address is set, ephemeral ports should be used on internal addresses instead. Additionally, we assume that containerInternalHostname is set correctly so that a route can be established.

    This gets muddy quickly; these don't really represent different strategies for evaluating address and ports, so much as they are two options applied on top of default. This means potentially 4 different strategies, since the two options don't affect one another. Or is there one default strategy that takes these options into account, with the potential for more strategies in the future?

  2. I'm not sure I totally understand the role of LocalDockerInstanceRuntimeInfo, as it seems to just call the DockerInstanceRuntimeInfo constructor with the parameters we want.
    It is where the options listed above actually take effect, since

    • Its constructor is injected with che.docker.ip.external, which it uses as containerExternalHostname in DockerInstanceRuntimeInfo
    • With my changes above, it's also resposible for setting containerInternalHostname correctly

    This kind of obfuscates the effects of these properties and I think adds to the confusion. Would the refactor get rid of this class, and move this functionality into HostPortEvaluationStrategy? I like this idea since with my changes, the property has to be injected twice -- in LocalDockerInstanceRuntimeInfo to set containerInternalHostname correctly, and in DockerInstanceRuntimeInfo to cause it to use ephemeral ports.

Sorry for the novel, I want to make sure I understand.

@garagatyi
Copy link

If I understand you correctly, you're suggesting...

Yes, It is what I'm thinking about.

How do we define HostPortEvaluationStrategies? ...

Yes, I suppose it should be a set of different strategies. They can be simple enough and don't need to override some default values. Apparently we will have some default strategy. I assume that we can set property with some strategy as a default and allow to override strategy with properties instead of changing assembly by binding needed strategy in Guice module. If vendor wants to implement new strategy it can contribute it into Che or bind it in customized assembly.

I'm not sure I totally understand the role of LocalDockerInstanceRuntimeInfo ...

Yes, I believe we should get rid of LocalDockerInstanceRuntimeInfo

@amisevsk
Copy link
Contributor Author

amisevsk commented Dec 5, 2016

I've opened a new pull request which includes the refactor suggested above: #3282

@amisevsk
Copy link
Contributor Author

Closed; issue will be solved in #3282

@amisevsk amisevsk closed this Dec 15, 2016
@benoitf benoitf removed the status/code-review This issue has a pull request posted for it and is awaiting code review completion by the community. label Nov 2, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/enhancement A feature request - must adhere to the feature request template.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants