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
1=== modified file 'revision'
2--- revision 2014-03-17 12:45:59 +0000
3+++ revision 2014-04-09 16:48:27 +0000
4@@ -1,1 +1,1 @@
5-109
6+110
7
8=== modified file 'server/guiserver/__init__.py'
9--- server/guiserver/__init__.py 2014-01-27 15:12:32 +0000
10+++ server/guiserver/__init__.py 2014-04-09 16:48:27 +0000
11@@ -37,7 +37,7 @@
12 which originally made the request.
13 """
14
15-VERSION = (0, 3, 0)
16+VERSION = (0, 4, 0)
17
18
19 def get_version():
20
21=== modified file 'server/guiserver/apps.py'
22--- server/guiserver/apps.py 2014-02-04 17:23:57 +0000
23+++ server/guiserver/apps.py 2014-04-09 16:48:27 +0000
24@@ -56,13 +56,17 @@
25 # The tokens collection for authentication token requests.
26 'tokens': tokens,
27 }
28+ juju_proxy_handler_options = {
29+ 'target_url': utils.ws_to_http(options.apiurl),
30+ 'charmworld_url': options.charmworldurl,
31+ }
32 server_handlers.extend([
33 # Handle WebSocket connections.
34 (r'^/ws$', handlers.WebSocketHandler, websocket_handler_options),
35 # Handle connections to the juju-core HTTPS server.
36 # The juju-core HTTPS and WebSocket servers share the same URL.
37- (r'^/juju-core/(.*)', handlers.ProxyHandler,
38- {'target_url': utils.ws_to_http(options.apiurl)}),
39+ (r'^/juju-core/(.*)', handlers.JujuProxyHandler,
40+ juju_proxy_handler_options),
41 ])
42 if options.testsroot:
43 params = {'path': options.testsroot, 'default_filename': 'index.html'}
44
45=== modified file 'server/guiserver/handlers.py'
46--- server/guiserver/handlers.py 2014-01-29 10:21:03 +0000
47+++ server/guiserver/handlers.py 2014-04-09 16:48:27 +0000
48@@ -20,6 +20,7 @@
49 import logging
50 import os
51 import time
52+import urlparse
53
54 from tornado import (
55 escape,
56@@ -47,6 +48,10 @@
57 )
58
59
60+# Define the path to the fallback charm icon hosted by charmworld.
61+DEFAULT_CHARM_ICON_PATH = '/static/img/charm_160.svg'
62+
63+
64 class WebSocketHandler(websocket.WebSocketHandler):
65 """WebSocket handler supporting secure WebSockets.
66
67@@ -225,12 +230,14 @@
68 class ProxyHandler(web.RequestHandler):
69 """An HTTP(S) proxy from the server to the given target URL."""
70
71- def initialize(self, target_url):
72+ def initialize(self, target_url, validate_cert=True):
73 """Initialize the proxy.
74
75- Receive the target URL where to redirect to.
76+ Receive the target URL where to redirect to, and a flag indicating
77+ whether to validate remote server certificates.
78 """
79 self.target_url = target_url
80+ self.validate_cert = validate_cert
81
82 @gen.coroutine
83 def get(self, path):
84@@ -241,12 +248,23 @@
85 The response will then be sent back to the client.
86 """
87 url = join_url(self.target_url, path, self.request.query)
88- # Server certificates are not validated: we use this function to
89- # connect to juju-core, and we would need to obtain ca-certificates
90- # from it. Unfortunately we don't have that information, and for this
91- # reason we skip validation for both WebSocket and HTTPS connections.
92- # This is not ideal but currently is our best option.
93- request = clone_request(self.request, url, validate_cert=False)
94+ response = yield self.send_request(url)
95+ if response is not None:
96+ self.send_response(response)
97+
98+ # Handle POST requests the same way GET requests are handled.
99+ post = get
100+
101+ @gen.coroutine
102+ def send_request(self, url):
103+ """Send an asynchronous request to the given URL.
104+
105+ Return the server response.
106+ If an error occurs in the communication, return None and call
107+ self._send_error with the given error.
108+ """
109+ request = clone_request(
110+ self.request, url, validate_cert=self.validate_cert)
111 client = httpclient.AsyncHTTPClient()
112 try:
113 response = yield client.fetch(request)
114@@ -254,13 +272,9 @@
115 response = getattr(err, 'response', None)
116 if not response:
117 self._send_error(url, err)
118- return
119- self._send_response(response)
120-
121- # Handle POST requests the same way GET ones are handled.
122- post = get
123-
124- def _send_response(self, response):
125+ raise gen.Return(response)
126+
127+ def send_response(self, response):
128 """Prepare and send the response to the client."""
129 self.set_status(response.code)
130 set_header = self.set_header
131@@ -279,6 +293,55 @@
132 self.write('Internal server error:\n{}'.format(msg))
133
134
135+class JujuProxyHandler(ProxyHandler):
136+ """A specialized proxy handler used for the juju-core HTTP API."""
137+
138+ def initialize(self, target_url, charmworld_url):
139+ """Initialize the proxy.
140+
141+ Receive the target URL where to redirect to, and the charmworld URL
142+ used to retrieve the default charm icon.
143+ """
144+ # Server certificates are not validated: we use this handler to connect
145+ # to juju-core, and we would need to obtain ca-certificates from it.
146+ # Unfortunately we don't have that information, and for this reason we
147+ # skip validation for both WebSocket and HTTPS connections. This is not
148+ # ideal but currently is our best option.
149+ super(JujuProxyHandler, self).initialize(
150+ target_url, validate_cert=False)
151+ self.default_charm_icon_url = urlparse.urljoin(
152+ charmworld_url, DEFAULT_CHARM_ICON_PATH)
153+
154+ @gen.coroutine
155+ def get(self, path):
156+ """Handle GET requests.
157+ See the ProxyHandler.get method.
158+
159+ Override to handle the case when a charm icon is not found.
160+ """
161+ url = join_url(self.target_url, path, self.request.query)
162+ response = yield self.send_request(url)
163+ if response is not None:
164+ if response.code == 404 and self._charm_icon_requested(path):
165+ # This is a request for a charm icon file, and the icon is not
166+ # found: redirect to the fallback icon hosted on charmworld.
167+ self.redirect(self.default_charm_icon_url)
168+ else:
169+ # Return the response to the client as usual.
170+ self.send_response(response)
171+
172+ def _charm_icon_requested(self, path):
173+ """Return True if the current request is for a charm icon."""
174+ return (
175+ # The request is for a local charm.
176+ path == 'charms' and
177+ # The charm URL is specified.
178+ self.get_argument('url', None) and
179+ # The icon file is requested.
180+ self.get_argument('file', None) == 'icon.svg'
181+ )
182+
183+
184 class InfoHandler(web.RequestHandler):
185 """Return information about the GUI server."""
186
187
188=== modified file 'server/guiserver/tests/test_handlers.py'
189--- server/guiserver/tests/test_handlers.py 2014-01-28 15:48:59 +0000
190+++ server/guiserver/tests/test_handlers.py 2014-04-09 16:48:27 +0000
191@@ -517,6 +517,7 @@
192 'Server': 'Apache/2.4.1 (Unix)',
193 'WWW-Authenticate': 'Basic',
194 }
195+ expected_validate_cert = True
196
197 def get_app(self):
198 # Set up an application exposing the proxy handler.
199@@ -570,8 +571,6 @@
200 self.assertEqual(self.target_url + '/remote-path/', remote_request.url)
201 self.assert_include_headers(
202 self.request_headers, remote_request.headers)
203- # Certificates are automatically accepted.
204- self.assertFalse(remote_request.validate_cert)
205
206 def test_post_request(self):
207 # POST requests are properly sent to the target URL.
208@@ -595,6 +594,19 @@
209 # Also the body is propagated.
210 self.assertEqual('original body', remote_request.body)
211
212+ def test_validate_certificates(self):
213+ # Server certificates are properly handled.
214+ remote_response = helpers.make_response(200)
215+ with self.patch_http_client(remote_response) as mock_client:
216+ self.fetch('/base/remote-path/', headers=self.request_headers)
217+ mock_fetch = mock_client().fetch
218+ # The client's fetch method has been used to fetch the remote resource.
219+ mock_fetch = mock_client().fetch
220+ self.assertEqual(1, mock_fetch.call_count)
221+ remote_request = mock_fetch.call_args[0][0]
222+ self.assertEqual(
223+ self.expected_validate_cert, remote_request.validate_cert)
224+
225 def test_remote_path(self):
226 # The corresponding path on the remote server is properly generated.
227 remote_response = helpers.make_response(200)
228@@ -617,6 +629,15 @@
229 self.assertEqual('try harder', response.body)
230 self.assertEqual('Bad Request', response.reason)
231
232+ def test_not_found_response(self):
233+ # 404 responses are returned to the original client.
234+ remote_response = helpers.make_response(404, body='try later')
235+ with self.patch_http_client(remote_response):
236+ response = self.fetch('/base/remote-path/')
237+ self.assertEqual(404, response.code)
238+ self.assertEqual('try later', response.body)
239+ self.assertEqual('Not Found', response.reason)
240+
241 def test_internal_server_error(self):
242 # A 500 error is returned if an HTTP error occurs during the remote
243 # request/response process.
244@@ -632,6 +653,43 @@
245 self.assertEqual('Internal Server Error', response.reason)
246
247
248+class TestJujuProxyHandler(TestProxyHandler):
249+
250+ charmworld_url = 'https://charmworld.example.com'
251+ expected_validate_cert = False
252+
253+ def get_app(self):
254+ # Set up an application exposing the proxy handler.
255+ options = {
256+ 'target_url': self.target_url,
257+ 'charmworld_url': self.charmworld_url,
258+ }
259+ return web.Application([
260+ (r'^/base/(.*)', handlers.JujuProxyHandler, options)])
261+
262+ def test_default_charm_icon(self):
263+ # If a charm icon is not found, a GET request redirects to the fallback
264+ # icon available on charmworld.
265+ remote_response = helpers.make_response(404)
266+ path = '/base/charms?url=local:trusty/django=42&file=icon.svg'
267+ with self.patch_http_client(remote_response):
268+ response = self.fetch(path, follow_redirects=False)
269+ self.assertEqual(302, response.code)
270+ self.assertEqual(
271+ self.charmworld_url + handlers.DEFAULT_CHARM_ICON_PATH,
272+ response.headers['location'])
273+
274+ def test_charm_file_not_found(self):
275+ # If a charm file is not found and it is not the icon, a 404 is
276+ # correctly returned to the original client.
277+ remote_response = helpers.make_response(404)
278+ path = '/base/charms?url=local:trusty/django=42&file=readme.rst'
279+ with self.patch_http_client(remote_response):
280+ response = self.fetch(path)
281+ self.assertEqual(404, response.code)
282+ self.assertEqual('Not Found', response.reason)
283+
284+
285 class TestInfoHandler(LogTrapTestCase, AsyncHTTPTestCase):
286
287 def get_app(self):

Subscribers

People subscribed via source and target branches