Merge lp:~frankban/charms/precise/juju-gui/support-default-charm-icon into lp:~juju-gui/charms/precise/juju-gui/trunk

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 176
Proposed branch: lp:~frankban/charms/precise/juju-gui/support-default-charm-icon
Merge into: lp:~juju-gui/charms/precise/juju-gui/trunk
Diff against target: 287 lines (+146/-21)
5 files modified
revision (+1/-1)
server/guiserver/__init__.py (+1/-1)
server/guiserver/apps.py (+6/-2)
server/guiserver/handlers.py (+78/-15)
server/guiserver/tests/test_handlers.py (+60/-2)
To merge this branch: bzr merge lp:~frankban/charms/precise/juju-gui/support-default-charm-icon
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+214985@code.launchpad.net

Description of the change

Make the GUI server redirect to the default icon.

Create a specialized proxy handler for handling
Juju HTTP API requests. In this subclass, handle
the case a request is for a local charm icon
that cannot be found on the Juju server.

Tests: `make unittest`.

QA:
- `juju bootstrap`;
- from the branch root, run `make deploy`;
- wait for the GUI service to be ready;
- switch to the trunk branch:
  `juju set juju-gui juju-gui-source=develop`
- wait for the GUI to be ready;
- deploy local charms including an icon:
  you should see the icons are correctly displayed in the
  service blocks and inspector header;
- deploy a local charm not including an icon:
  you should see the fallback icon displayed both in
  the service block and the inspector;
- destroy the environment, done.

https://codereview.appspot.com/86100043/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

Reviewers: mp+214985_code.launchpad.net,

Message:
Please take a look.

Description:
Make the GUI server redirect to the default icon.

Create a specialized proxy handler for handling
Juju HTTP API requests. In this subclass, handle
the case a request is for a local charm icon
that cannot be found on the Juju server.

Tests: `make unittest`.

QA:
- `juju bootstrap`;
- from the branch root, run `make deploy`;
- wait for the GUI service to be ready;
- switch to the trunk branch:
   `juju set juju-gui juju-gui-source=develop`
- wait for the GUI to be ready;
- deploy local charms including an icon:
   you should see the icons are correctly displayed in the
   service blocks and inspector header;
- deploy a local charm not including an icon:
   you should see the fallback icon displayed both in
   the service block and the inspector;
- destroy the environment, done.

https://code.launchpad.net/~frankban/charms/precise/juju-gui/support-default-charm-icon/+merge/214985

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/86100043/

Affected files (+148, -21 lines):
   A [revision details]
   M revision
   M server/guiserver/__init__.py
   M server/guiserver/apps.py
   M server/guiserver/handlers.py
   M server/guiserver/tests/test_handlers.py

Revision history for this message
Brad Crittenden (bac) wrote :

Code LGTM

https://codereview.appspot.com/86100043/diff/1/server/guiserver/handlers.py
File server/guiserver/handlers.py (right):

https://codereview.appspot.com/86100043/diff/1/server/guiserver/handlers.py#newcode255
server/guiserver/handlers.py:255: # Handle POST requests the same way
GET ones are handled.
s/ones/requests/ -- just reads better.

https://codereview.appspot.com/86100043/diff/1/server/guiserver/handlers.py#newcode320
server/guiserver/handlers.py:320: Override to handle the case a when a
charm icon is not found.
s/a when/when/ or /for when/?

https://codereview.appspot.com/86100043/

180. By Francesco Banconi

Changes as per review.

Revision history for this message
Francesco Banconi (frankban) wrote :

Please take a look.

https://codereview.appspot.com/86100043/diff/1/server/guiserver/handlers.py
File server/guiserver/handlers.py (right):

https://codereview.appspot.com/86100043/diff/1/server/guiserver/handlers.py#newcode255
server/guiserver/handlers.py:255: # Handle POST requests the same way
GET ones are handled.
On 2014/04/09 16:32:42, bac wrote:
> s/ones/requests/ -- just reads better.

Done.

https://codereview.appspot.com/86100043/diff/1/server/guiserver/handlers.py#newcode320
server/guiserver/handlers.py:320: Override to handle the case a when a
charm icon is not found.
On 2014/04/09 16:32:42, bac wrote:
> s/a when/when/ or /for when/?

Done.

https://codereview.appspot.com/86100043/

Revision history for this message
Brad Crittenden (bac) wrote :
Revision history for this message
Francesco Banconi (frankban) wrote :

*** Submitted:

Make the GUI server redirect to the default icon.

Create a specialized proxy handler for handling
Juju HTTP API requests. In this subclass, handle
the case a request is for a local charm icon
that cannot be found on the Juju server.

Tests: `make unittest`.

QA:
- `juju bootstrap`;
- from the branch root, run `make deploy`;
- wait for the GUI service to be ready;
- switch to the trunk branch:
   `juju set juju-gui juju-gui-source=develop`
- wait for the GUI to be ready;
- deploy local charms including an icon:
   you should see the icons are correctly displayed in the
   service blocks and inspector header;
- deploy a local charm not including an icon:
   you should see the fallback icon displayed both in
   the service block and the inspector;
- destroy the environment, done.

R=bac
CC=
https://codereview.appspot.com/86100043

https://codereview.appspot.com/86100043/

Revision history for this message
Francesco Banconi (frankban) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'revision'
--- revision 2014-03-17 12:45:59 +0000
+++ revision 2014-04-09 16:48:27 +0000
@@ -1,1 +1,1 @@
11091110
22
=== modified file 'server/guiserver/__init__.py'
--- server/guiserver/__init__.py 2014-01-27 15:12:32 +0000
+++ server/guiserver/__init__.py 2014-04-09 16:48:27 +0000
@@ -37,7 +37,7 @@
37which originally made the request.37which originally made the request.
38"""38"""
3939
40VERSION = (0, 3, 0)40VERSION = (0, 4, 0)
4141
4242
43def get_version():43def get_version():
4444
=== modified file 'server/guiserver/apps.py'
--- server/guiserver/apps.py 2014-02-04 17:23:57 +0000
+++ server/guiserver/apps.py 2014-04-09 16:48:27 +0000
@@ -56,13 +56,17 @@
56 # The tokens collection for authentication token requests.56 # The tokens collection for authentication token requests.
57 'tokens': tokens,57 'tokens': tokens,
58 }58 }
59 juju_proxy_handler_options = {
60 'target_url': utils.ws_to_http(options.apiurl),
61 'charmworld_url': options.charmworldurl,
62 }
59 server_handlers.extend([63 server_handlers.extend([
60 # Handle WebSocket connections.64 # Handle WebSocket connections.
61 (r'^/ws$', handlers.WebSocketHandler, websocket_handler_options),65 (r'^/ws$', handlers.WebSocketHandler, websocket_handler_options),
62 # Handle connections to the juju-core HTTPS server.66 # Handle connections to the juju-core HTTPS server.
63 # The juju-core HTTPS and WebSocket servers share the same URL.67 # The juju-core HTTPS and WebSocket servers share the same URL.
64 (r'^/juju-core/(.*)', handlers.ProxyHandler,68 (r'^/juju-core/(.*)', handlers.JujuProxyHandler,
65 {'target_url': utils.ws_to_http(options.apiurl)}),69 juju_proxy_handler_options),
66 ])70 ])
67 if options.testsroot:71 if options.testsroot:
68 params = {'path': options.testsroot, 'default_filename': 'index.html'}72 params = {'path': options.testsroot, 'default_filename': 'index.html'}
6973
=== modified file 'server/guiserver/handlers.py'
--- server/guiserver/handlers.py 2014-01-29 10:21:03 +0000
+++ server/guiserver/handlers.py 2014-04-09 16:48:27 +0000
@@ -20,6 +20,7 @@
20import logging20import logging
21import os21import os
22import time22import time
23import urlparse
2324
24from tornado import (25from tornado import (
25 escape,26 escape,
@@ -47,6 +48,10 @@
47)48)
4849
4950
51# Define the path to the fallback charm icon hosted by charmworld.
52DEFAULT_CHARM_ICON_PATH = '/static/img/charm_160.svg'
53
54
50class WebSocketHandler(websocket.WebSocketHandler):55class WebSocketHandler(websocket.WebSocketHandler):
51 """WebSocket handler supporting secure WebSockets.56 """WebSocket handler supporting secure WebSockets.
5257
@@ -225,12 +230,14 @@
225class ProxyHandler(web.RequestHandler):230class ProxyHandler(web.RequestHandler):
226 """An HTTP(S) proxy from the server to the given target URL."""231 """An HTTP(S) proxy from the server to the given target URL."""
227232
228 def initialize(self, target_url):233 def initialize(self, target_url, validate_cert=True):
229 """Initialize the proxy.234 """Initialize the proxy.
230235
231 Receive the target URL where to redirect to.236 Receive the target URL where to redirect to, and a flag indicating
237 whether to validate remote server certificates.
232 """238 """
233 self.target_url = target_url239 self.target_url = target_url
240 self.validate_cert = validate_cert
234241
235 @gen.coroutine242 @gen.coroutine
236 def get(self, path):243 def get(self, path):
@@ -241,12 +248,23 @@
241 The response will then be sent back to the client.248 The response will then be sent back to the client.
242 """249 """
243 url = join_url(self.target_url, path, self.request.query)250 url = join_url(self.target_url, path, self.request.query)
244 # Server certificates are not validated: we use this function to251 response = yield self.send_request(url)
245 # connect to juju-core, and we would need to obtain ca-certificates252 if response is not None:
246 # from it. Unfortunately we don't have that information, and for this253 self.send_response(response)
247 # reason we skip validation for both WebSocket and HTTPS connections.254
248 # This is not ideal but currently is our best option.255 # Handle POST requests the same way GET requests are handled.
249 request = clone_request(self.request, url, validate_cert=False)256 post = get
257
258 @gen.coroutine
259 def send_request(self, url):
260 """Send an asynchronous request to the given URL.
261
262 Return the server response.
263 If an error occurs in the communication, return None and call
264 self._send_error with the given error.
265 """
266 request = clone_request(
267 self.request, url, validate_cert=self.validate_cert)
250 client = httpclient.AsyncHTTPClient()268 client = httpclient.AsyncHTTPClient()
251 try:269 try:
252 response = yield client.fetch(request)270 response = yield client.fetch(request)
@@ -254,13 +272,9 @@
254 response = getattr(err, 'response', None)272 response = getattr(err, 'response', None)
255 if not response:273 if not response:
256 self._send_error(url, err)274 self._send_error(url, err)
257 return275 raise gen.Return(response)
258 self._send_response(response)276
259277 def send_response(self, response):
260 # Handle POST requests the same way GET ones are handled.
261 post = get
262
263 def _send_response(self, response):
264 """Prepare and send the response to the client."""278 """Prepare and send the response to the client."""
265 self.set_status(response.code)279 self.set_status(response.code)
266 set_header = self.set_header280 set_header = self.set_header
@@ -279,6 +293,55 @@
279 self.write('Internal server error:\n{}'.format(msg))293 self.write('Internal server error:\n{}'.format(msg))
280294
281295
296class JujuProxyHandler(ProxyHandler):
297 """A specialized proxy handler used for the juju-core HTTP API."""
298
299 def initialize(self, target_url, charmworld_url):
300 """Initialize the proxy.
301
302 Receive the target URL where to redirect to, and the charmworld URL
303 used to retrieve the default charm icon.
304 """
305 # Server certificates are not validated: we use this handler to connect
306 # to juju-core, and we would need to obtain ca-certificates from it.
307 # Unfortunately we don't have that information, and for this reason we
308 # skip validation for both WebSocket and HTTPS connections. This is not
309 # ideal but currently is our best option.
310 super(JujuProxyHandler, self).initialize(
311 target_url, validate_cert=False)
312 self.default_charm_icon_url = urlparse.urljoin(
313 charmworld_url, DEFAULT_CHARM_ICON_PATH)
314
315 @gen.coroutine
316 def get(self, path):
317 """Handle GET requests.
318 See the ProxyHandler.get method.
319
320 Override to handle the case when a charm icon is not found.
321 """
322 url = join_url(self.target_url, path, self.request.query)
323 response = yield self.send_request(url)
324 if response is not None:
325 if response.code == 404 and self._charm_icon_requested(path):
326 # This is a request for a charm icon file, and the icon is not
327 # found: redirect to the fallback icon hosted on charmworld.
328 self.redirect(self.default_charm_icon_url)
329 else:
330 # Return the response to the client as usual.
331 self.send_response(response)
332
333 def _charm_icon_requested(self, path):
334 """Return True if the current request is for a charm icon."""
335 return (
336 # The request is for a local charm.
337 path == 'charms' and
338 # The charm URL is specified.
339 self.get_argument('url', None) and
340 # The icon file is requested.
341 self.get_argument('file', None) == 'icon.svg'
342 )
343
344
282class InfoHandler(web.RequestHandler):345class InfoHandler(web.RequestHandler):
283 """Return information about the GUI server."""346 """Return information about the GUI server."""
284347
285348
=== modified file 'server/guiserver/tests/test_handlers.py'
--- server/guiserver/tests/test_handlers.py 2014-01-28 15:48:59 +0000
+++ server/guiserver/tests/test_handlers.py 2014-04-09 16:48:27 +0000
@@ -517,6 +517,7 @@
517 'Server': 'Apache/2.4.1 (Unix)',517 'Server': 'Apache/2.4.1 (Unix)',
518 'WWW-Authenticate': 'Basic',518 'WWW-Authenticate': 'Basic',
519 }519 }
520 expected_validate_cert = True
520521
521 def get_app(self):522 def get_app(self):
522 # Set up an application exposing the proxy handler.523 # Set up an application exposing the proxy handler.
@@ -570,8 +571,6 @@
570 self.assertEqual(self.target_url + '/remote-path/', remote_request.url)571 self.assertEqual(self.target_url + '/remote-path/', remote_request.url)
571 self.assert_include_headers(572 self.assert_include_headers(
572 self.request_headers, remote_request.headers)573 self.request_headers, remote_request.headers)
573 # Certificates are automatically accepted.
574 self.assertFalse(remote_request.validate_cert)
575574
576 def test_post_request(self):575 def test_post_request(self):
577 # POST requests are properly sent to the target URL.576 # POST requests are properly sent to the target URL.
@@ -595,6 +594,19 @@
595 # Also the body is propagated.594 # Also the body is propagated.
596 self.assertEqual('original body', remote_request.body)595 self.assertEqual('original body', remote_request.body)
597596
597 def test_validate_certificates(self):
598 # Server certificates are properly handled.
599 remote_response = helpers.make_response(200)
600 with self.patch_http_client(remote_response) as mock_client:
601 self.fetch('/base/remote-path/', headers=self.request_headers)
602 mock_fetch = mock_client().fetch
603 # The client's fetch method has been used to fetch the remote resource.
604 mock_fetch = mock_client().fetch
605 self.assertEqual(1, mock_fetch.call_count)
606 remote_request = mock_fetch.call_args[0][0]
607 self.assertEqual(
608 self.expected_validate_cert, remote_request.validate_cert)
609
598 def test_remote_path(self):610 def test_remote_path(self):
599 # The corresponding path on the remote server is properly generated.611 # The corresponding path on the remote server is properly generated.
600 remote_response = helpers.make_response(200)612 remote_response = helpers.make_response(200)
@@ -617,6 +629,15 @@
617 self.assertEqual('try harder', response.body)629 self.assertEqual('try harder', response.body)
618 self.assertEqual('Bad Request', response.reason)630 self.assertEqual('Bad Request', response.reason)
619631
632 def test_not_found_response(self):
633 # 404 responses are returned to the original client.
634 remote_response = helpers.make_response(404, body='try later')
635 with self.patch_http_client(remote_response):
636 response = self.fetch('/base/remote-path/')
637 self.assertEqual(404, response.code)
638 self.assertEqual('try later', response.body)
639 self.assertEqual('Not Found', response.reason)
640
620 def test_internal_server_error(self):641 def test_internal_server_error(self):
621 # A 500 error is returned if an HTTP error occurs during the remote642 # A 500 error is returned if an HTTP error occurs during the remote
622 # request/response process.643 # request/response process.
@@ -632,6 +653,43 @@
632 self.assertEqual('Internal Server Error', response.reason)653 self.assertEqual('Internal Server Error', response.reason)
633654
634655
656class TestJujuProxyHandler(TestProxyHandler):
657
658 charmworld_url = 'https://charmworld.example.com'
659 expected_validate_cert = False
660
661 def get_app(self):
662 # Set up an application exposing the proxy handler.
663 options = {
664 'target_url': self.target_url,
665 'charmworld_url': self.charmworld_url,
666 }
667 return web.Application([
668 (r'^/base/(.*)', handlers.JujuProxyHandler, options)])
669
670 def test_default_charm_icon(self):
671 # If a charm icon is not found, a GET request redirects to the fallback
672 # icon available on charmworld.
673 remote_response = helpers.make_response(404)
674 path = '/base/charms?url=local:trusty/django=42&file=icon.svg'
675 with self.patch_http_client(remote_response):
676 response = self.fetch(path, follow_redirects=False)
677 self.assertEqual(302, response.code)
678 self.assertEqual(
679 self.charmworld_url + handlers.DEFAULT_CHARM_ICON_PATH,
680 response.headers['location'])
681
682 def test_charm_file_not_found(self):
683 # If a charm file is not found and it is not the icon, a 404 is
684 # correctly returned to the original client.
685 remote_response = helpers.make_response(404)
686 path = '/base/charms?url=local:trusty/django=42&file=readme.rst'
687 with self.patch_http_client(remote_response):
688 response = self.fetch(path)
689 self.assertEqual(404, response.code)
690 self.assertEqual('Not Found', response.reason)
691
692
635class TestInfoHandler(LogTrapTestCase, AsyncHTTPTestCase):693class TestInfoHandler(LogTrapTestCase, AsyncHTTPTestCase):
636694
637 def get_app(self):695 def get_app(self):

Subscribers

People subscribed via source and target branches