diff --git a/config/galaxy.ini.sample b/config/galaxy.ini.sample index 5dadaaa29b4..787962d2f95 100644 --- a/config/galaxy.ini.sample +++ b/config/galaxy.ini.sample @@ -574,6 +574,12 @@ nglims_config_file = tool-data/nglims.yaml # nodejs to wrap connections in SSL). #dynamic_proxy_external_proxy=False +# Additionally, when the dynamic proxy is proxied by an upstream server, you'll +# want to specify a prefixed URL so both Galaxy and the proxy reside under the +# same path that your cookies are under. This will result in a url like +# https://FQDN/galaxy-prefix/gie_proxy for proxying +#dynamic_proxy_prefix=gie_proxy + # -- Logging and Debugging # If True, Galaxy will attempt to configure a simple root logger if a diff --git a/config/plugins/interactive_environments/ipython/templates/ipython.mako b/config/plugins/interactive_environments/ipython/templates/ipython.mako index 118d72baf2f..2e87c5ae221 100644 --- a/config/plugins/interactive_environments/ipython/templates/ipython.mako +++ b/config/plugins/interactive_environments/ipython/templates/ipython.mako @@ -33,18 +33,16 @@ if hda.datatype.__class__.__name__ != "Ipynb": else: shutil.copy( hda.file_name, empty_nb_path ) - -## General IE specific -# Access URLs for the notebook from within galaxy. -notebook_access_url = ie_request.url_template('${PROXY_URL}/ipython/${PORT}/notebooks/ipython_galaxy_notebook.ipynb') -notebook_login_url = ie_request.url_template('${PROXY_URL}/ipython/${PORT}/login?next=%2Fipython%2F${PORT}%2Ftree') - - # Add all environment variables collected from Galaxy's IE infrastructure ie_request.launch(env_override={ 'notebook_password': PASSWORD, }) +## General IE specific +# Access URLs for the notebook from within galaxy. +notebook_access_url = ie_request.url_template('${PROXY_URL}/ipython/notebooks/ipython_galaxy_notebook.ipynb') +notebook_login_url = ie_request.url_template('${PROXY_URL}/ipython/login?next=${PROXY_PREFIX}%2Fipython%2Ftree') + %> diff --git a/config/plugins/interactive_environments/rstudio/templates/rstudio.mako b/config/plugins/interactive_environments/rstudio/templates/rstudio.mako index 0fc4d2a4ba2..9d35421ba5a 100644 --- a/config/plugins/interactive_environments/rstudio/templates/rstudio.mako +++ b/config/plugins/interactive_environments/rstudio/templates/rstudio.mako @@ -12,14 +12,6 @@ temp_dir = ie_request.temp_dir PASSWORD = ie_request.notebook_pw USERNAME = "galaxy" -## General IE specific -# Access URLs for the notebook from within galaxy. -# TODO: Make this work without pointing directly to IE. Currently does not work -# through proxy. -notebook_pubkey_url = ie_request.url_template('${PROXY_URL}/rstudio/${PORT}/auth-public-key') -notebook_access_url = ie_request.url_template('${PROXY_URL}/rstudio/${PORT}/') -notebook_login_url = ie_request.url_template('${PROXY_URL}/rstudio/${PORT}/auth-do-sign-in') - # Did the user give us an RData file? if hda.datatype.__class__.__name__ == "RData": shutil.copy( hda.file_name, os.path.join(temp_dir, '.RData') ) @@ -27,8 +19,16 @@ if hda.datatype.__class__.__name__ == "RData": ie_request.launch(env_override={ 'notebook_username': USERNAME, 'notebook_password': PASSWORD, - 'cors_origin': ie_request.attr.proxy_url, }) + +## General IE specific +# Access URLs for the notebook from within galaxy. +# TODO: Make this work without pointing directly to IE. Currently does not work +# through proxy. +notebook_pubkey_url = ie_request.url_template('${PROXY_URL}/rstudio/auth-public-key') +notebook_access_url = ie_request.url_template('${PROXY_URL}/rstudio/') +notebook_login_url = ie_request.url_template('${PROXY_URL}/rstudio/auth-do-sign-in') + %> diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index 28acdc42262..286ca8b38ef 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -443,6 +443,7 @@ class Configuration( object ): self.dynamic_proxy_bind_port = int( kwargs.get( "dynamic_proxy_bind_port", "8800" ) ) self.dynamic_proxy_bind_ip = kwargs.get( "dynamic_proxy_bind_ip", "0.0.0.0" ) self.dynamic_proxy_external_proxy = string_as_bool( kwargs.get( "dynamic_proxy_external_proxy", "False" ) ) + self.dynamic_proxy_prefix = kwargs.get( "dynamic_proxy_prefix", "gie_proxy" ) # Default chunk size for chunkable datatypes -- 64k self.display_chunk_size = int( kwargs.get( 'display_chunk_size', 65536) ) diff --git a/lib/galaxy/web/base/interactive_environments.py b/lib/galaxy/web/base/interactive_environments.py index ce850f2d850..52c5886f569 100644 --- a/lib/galaxy/web/base/interactive_environments.py +++ b/lib/galaxy/web/base/interactive_environments.py @@ -1,6 +1,7 @@ import ConfigParser import os +import json import stat import random import tempfile @@ -40,11 +41,6 @@ class InteractiveEnviornmentRequest(object): self.load_deploy_config() self.attr.docker_hostname = self.attr.viz_config.get("docker", "docker_hostname") - self.attr.proxy_request = trans.app.proxy_manager.setup_proxy( - trans, host=self.attr.docker_hostname - ) - self.attr.proxy_url = self.attr.proxy_request[ 'proxy_url' ] - self.attr.PORT = self.attr.proxy_request[ 'proxied_port' ] # Generate per-request passwords the IE plugin can use to configure # the destination container. @@ -62,6 +58,14 @@ class InteractiveEnviornmentRequest(object): log.error( "Could not change permissions of tmpdir %s" % self.temp_dir ) # continue anyway + # This duplicates the logic in the proxy manager + if self.attr.galaxy_config.dynamic_proxy_external_proxy: + self.attr.proxy_prefix = '%s/%s' % ( + self.attr.galaxy_config.cookie_path, + self.attr.galaxy_config.dynamic_proxy_prefix) + else: + self.attr.proxy_prefix = '' + def load_deploy_config(self, default_dict={}): # For backwards compat, any new variables added to the base .ini file # will need to be recorded here. The ConfigParser doesn't provide a @@ -107,8 +111,10 @@ class InteractiveEnviornmentRequest(object): 'history_id': self.attr.history_id, 'api_key': api_key, 'remote_host': request.remote_addr, - 'docker_port': self.attr.PORT, + # DOCKER_PORT is NO LONGER AVAILABLE. All IEs must update. 'cors_origin': request.host_url, + 'user_email': self.trans.user.email, + 'proxy_prefix': self.attr.proxy_prefix, } if self.attr.viz_config.has_option("docker", "galaxy_url"): @@ -143,36 +149,27 @@ class InteractiveEnviornmentRequest(object): return "false" def url_template(self, url_template): + """Process a URL template + + There are several variables accessible to the user: + + - ${PROXY_URL} will be replaced with the dynamically create proxy's url + - ${PROXY_PREFIX} will be replaced with the prefix that may occur """ - Process a URL template - - There are several variables accessible to the user: - - - ${PROXY_URL} will be replaced with dynamically create proxy - - ${PORT} will be replaced with the port the docker image is attached to - """ - # Figure out our substitutions - # Next several lines for older style replacements (not used with Galaxy dynamic # proxy) - if self.attr.SSL_URLS: protocol = 'https' else: protocol = 'http' - if not self.attr.APACHE_URLS: - # If they are not using apache URLs, that implies there's a port attached to the host - # string, thus we replace just the first instance of host that we see. - url_template = url_template.replace('${HOST}', '${HOST}:${PORT}', 1) - url_template = url_template.replace('${PROTO}', protocol) \ .replace('${HOST}', self.attr.HOST) # Only the following replacements are used with Galaxy dynamic proxy # URLs url = url_template.replace('${PROXY_URL}', str(self.attr.proxy_url)) \ - .replace('${PORT}', str(self.attr.PORT)) + .replace('${PROXY_PREFIX}', str(self.attr.proxy_prefix.replace('/', '%2F'))) return url def volume(self, host_path, container_path, **kwds): @@ -193,13 +190,11 @@ class InteractiveEnviornmentRequest(object): # Then we format in the entire docker command in place of # {docker_args}, so as to let the admin not worry about which args are # getting passed - command = command.format(docker_args='run {command_inject} {environment} -d -p {port_ext}:{port_int} -v "{temp_dir}:/import/" {volume_str} {image}') + command = command.format(docker_args='run {command_inject} {environment} -d -P -v "{temp_dir}:/import/" {volume_str} {image}') # Once that's available, we format again with all of our arguments command = command.format( command_inject=self.attr.viz_config.get("docker", "command_inject"), environment=env_str, - port_ext=self.attr.PORT, - port_int=self.attr.docker_port, temp_dir=temp_dir, volume_str=volume_str, image=self.attr.viz_config.get("docker", "image") @@ -217,5 +212,81 @@ class InteractiveEnviornmentRequest(object): stdout, stderr = p.communicate() if p.returncode != 0 or len(stderr): log.error( "%s\n%s" % (stdout, stderr) ) + return None else: log.debug( "Container id: %s" % stdout) + port_mappings = self.get_proxied_ports(stdout) + if len(port_mappings) > 1: + log.warning("Don't know how to handle proxies to containers with multiple exposed ports. Arbitrarily choosing first") + elif len(port_mappings) == 0: + log.warning("No exposed ports to map! Images MUST EXPOSE") + return None + # Fetch the first port_mapping + (service, host_ip, host_port) = port_mappings[0] + + # Now we configure our proxy_requst object and we manually specify + # the port to map to and ensure the proxy is available. + self.attr.proxy_request = self.trans.app.proxy_manager.setup_proxy( + self.trans, + host=self.attr.docker_hostname, + port=host_port, + proxy_prefix=self.attr.proxy_prefix, + ) + # These variables then become available for use in templating URLs + self.attr.proxy_url = self.attr.proxy_request[ 'proxy_url' ] + # Commented out because it needs to be documented and visible that + # this variable was moved here. Usually would remove commented + # code, but again, needs to be clear where this went. Remove at a + # later time. + # + # PORT is no longer exposed internally. All requests are forced to + # go through the proxy we ship. + # self.attr.PORT = self.attr.proxy_request[ 'proxied_port' ] + + def get_proxied_ports(self, container_id): + """Run docker inspect on a container to figure out which ports were + mapped where. + + :type container_id: str + :param container_id: a docker container ID + + :returns: a list of triples containing (internal_port, external_ip, + external_port), of which the ports are probably the only + useful information. + + Someday code that calls this should be refactored whenever we get + containers with multiple ports working. + """ + command = self.attr.viz_config.get("docker", "command") + command = command.format(docker_args="inspect %s" % container_id) + log.info("Inspecting docker container {0} with command [{1}]".format( + container_id, + command + )) + + p = Popen(command, stdout=PIPE, stderr=PIPE, close_fds=True, shell=True) + stdout, stderr = p.communicate() + if p.returncode != 0 or len(stderr): + log.error( "%s\n%s" % (stdout, stderr) ) + return None + + inspect_data = json.loads(stdout) + # [{ + # "NetworkSettings" : { + # "Ports" : { + # "3306/tcp" : [ + # { + # "HostIp" : "127.0.0.1", + # "HostPort" : "3306" + # } + # ] + mappings = [] + port_mappings = inspect_data[0]['NetworkSettings']['Ports'] + for port_name in port_mappings: + for binding in port_mappings[port_name]: + mappings.append(( + port_name.replace('/tcp', '').replace('/udp', ''), + binding['HostIp'], + binding['HostPort'] + )) + return mappings diff --git a/lib/galaxy/web/proxy/__init__.py b/lib/galaxy/web/proxy/__init__.py index 30738ad6c11..a23b42a285c 100644 --- a/lib/galaxy/web/proxy/__init__.py +++ b/lib/galaxy/web/proxy/__init__.py @@ -17,7 +17,9 @@ SECURE_COOKIE = "galaxysession" class ProxyManager(object): def __init__( self, config ): - for option in [ "manage_dynamic_proxy", "dynamic_proxy_bind_port", "dynamic_proxy_bind_ip", "dynamic_proxy_debug", "dynamic_proxy_external_proxy" ]: + for option in ["manage_dynamic_proxy", "dynamic_proxy_bind_port", + "dynamic_proxy_bind_ip", "dynamic_proxy_debug", + "dynamic_proxy_external_proxy", "dynamic_proxy_prefix"]: setattr( self, option, getattr( config, option ) ) self.launch_by = "node" # TODO: Support docker if self.manage_dynamic_proxy: @@ -29,7 +31,7 @@ class ProxyManager(object): def shutdown( self ): self.lazy_process.shutdown() - def setup_proxy( self, trans, host=DEFAULT_PROXY_TO_HOST, port=None ): + def setup_proxy( self, trans, host=DEFAULT_PROXY_TO_HOST, port=None, proxy_prefix="" ): if self.manage_dynamic_proxy: log.info("Attempting to start dynamic proxy process") self.lazy_process.start_process() @@ -46,7 +48,7 @@ class ProxyManager(object): if not self.dynamic_proxy_external_proxy: proxy_url = '%s://%s:%d' % (scheme, host, self.dynamic_proxy_bind_port) else: - proxy_url = '%s://%s' % (scheme, host) + proxy_url = '%s://%s%s' % (scheme, host, proxy_prefix) return { 'proxy_url': proxy_url, 'proxied_port': proxy_requests.port,