Merge lp:~frankban/charms/precise/juju-gui/local-releases into lp:~juju-gui/charms/precise/juju-gui/trunk

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 115
Proposed branch: lp:~frankban/charms/precise/juju-gui/local-releases
Merge into: lp:~juju-gui/charms/precise/juju-gui/trunk
Diff against target: 780 lines (+414/-66)
7 files modified
HACKING.md (+14/-0)
README.md (+36/-5)
config.yaml (+21/-13)
hooks/utils.py (+91/-22)
revision (+1/-1)
tests/20-functional.test (+22/-11)
tests/test_utils.py (+229/-14)
To merge this branch: bzr merge lp:~frankban/charms/precise/juju-gui/local-releases
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+189896@code.launchpad.net

Description of the change

Introduce local GUI releases.

Implemented the new "local" juju-gui-source option.
By default the GUI release is now retrieved from
the charm itself, so that in the deployment process
the charm can avoid connecting to Launchpad.

This should result in the charm being able to be
deployed behind a firewall without the traditional
workarounds. Note that this is not yet demonstrated
and must be QAed in a real firewalled environment.

The process tries is like the following:
- if juju-gui-source is "local", the last tarball
  in the releases dir is installed;
- if juju-gui-source is a stable or development version,
  the charm checks if that version is present in the
  local repository before downloading it from Launchpad.
- the rest of the options should still work as usual
  (but some QA could help, e.g. deploying from a branch).

Also updated the charm documentation and added some
missing tests.

Tests: `make unittest` from the branch root
(I am currently running the functional tests).

QA (assuming 0.10.1 is the latest GUI release):
- `juju bootstrap --debug`;
- `make deploy`;
- check the logs: no PPAs are used, the local release
  is installed;
- `juju set juju-gui juju-gui-source=0.10.0`;
- check the logs: the release is downloaded from
  Launchpad;
- `juju set juju-gui juju-gui-source=0.10.1`;
- check the logs: the charm successfully find
  the 0.10.1 release in the local repository and
  avoids downloading it from Launchpad.
- keep trying to break the charm in all the ways
  you can imagine...

Thank you.

https://codereview.appspot.com/14545044/

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

Reviewers: mp+189896_code.launchpad.net,

Message:
Please take a look.

Description:
Introduce local GUI releases.

Implemented the new "local" juju-gui-source option.
By default the GUI release is now retrieved from
the charm itself, so that in the deployment process
the charm can avoid connecting to Launchpad.

This should result in the charm being able to be
deployed behind a firewall without the traditional
workarounds. Note that this is not yet demonstrated
and must be QAed in a real firewalled environment.

The process tries is like the following:
- if juju-gui-source is "local", the last tarball
   in the releases dir is installed;
- if juju-gui-source is a stable or development version,
   the charm checks if that version is present in the
   local repository before downloading it from Launchpad.
- the rest of the options should still work as usual
   (but some QA could help, e.g. deploying from a branch).

Also updated the charm documentation and added some
missing tests.

Tests: `make unittest` from the branch root
(I am currently running the functional tests).

QA (assuming 0.10.1 is the latest GUI release):
- `juju bootstrap --debug`;
- `make deploy`;
- check the logs: no PPAs are used, the local release
   is installed;
- `juju set juju-gui juju-gui-source=0.10.0`;
- check the logs: the release is downloaded from
   Launchpad;
- `juju set juju-gui juju-gui-source=0.10.1`;
- check the logs: the charm successfully find
   the 0.10.1 release in the local repository and
   avoids downloading it from Launchpad.
- keep trying to break the charm in all the ways
   you can imagine...

Thank you.

https://code.launchpad.net/~frankban/charms/precise/juju-gui/local-releases/+merge/189896

(do not edit description out of merge proposal)

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

Affected files (+395, -57 lines):
   M HACKING.md
   M README.md
   A [revision details]
   M config.yaml
   M hooks/utils.py
   A releases/juju-gui-0.10.1.tgz
   M revision
   M tests/20-functional.test
   M tests/test_utils.py

Revision history for this message
Madison Scott-Clary (makyo) wrote :
Revision history for this message
Gary Poster (gary) wrote :
Download full text (4.4 KiB)

LGTM with many trivials and one slightly-more-than-trivial-but-not-much.
  Relying on Makyo's QA. Great, thank you!

Gary

https://codereview.appspot.com/14545044/diff/1/config.yaml
File config.yaml (right):

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode20
config.yaml:20: Where to install Juju GUI from. Possible values are:
Possible values are the following.

[changes below will largely be punctuation and capitalization, shifting
semicolons to periods and capitalizing the start of new sentences.]

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode21
config.yaml:21: - 'local' (default): the latest local release will be
deployed. Releases
Capitalize "The"

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode22
config.yaml:22: are stored in the releases directory of this charm;
...charm; -> ...charm.

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode23
config.yaml:23: - 'stable' the latest stable release will be deployed;
The latest release from the "stable" series will be deployed.

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode24
config.yaml:24: - 'trunk': the latest trunk release will be deployed;
The latest release from the "trunk" series will be deployed. Please
note that this is not a build of the current Juju GUI trunk. For that
functionality, use "lp:juju-gui", as described below.

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode25
config.yaml:25: - a stable version (e.g '0.1.0'): the specified stable
version will be
The...

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode28
config.yaml:28: release will be downloaded from Launchpad;
...Launchpad.

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode29
config.yaml:29: - a trunk version (e.g '0.1.0+build.1'): the specified
trunk version
The...

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode32
config.yaml:32: release will be downloaded from Launchpad;
...Launchpad.

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode33
config.yaml:33: - a Bazaar branch (e.g. 'lp:juju-gui'): a release will
be created and
A release...

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode36
config.yaml:36: revision, e.g. 'lp:juju-gui:42' will checkout revno 42;
...revno 42.

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode37
config.yaml:37: - a 'url:' prefixed url (ex: url:http://...) of a
specific location
- a 'url:' prefixed url: The release found at the given URL (ex:
url:http://... or url:file://...) will be deployed.

https://codereview.appspot.com/14545044/diff/1/hooks/utils.py
File hooks/utils.py (right):

https://codereview.appspot.com/14545044/diff/1/hooks/utils.py#newcode165
hooks/utils.py:165: \.tgz # File extension.
would you mind changing this to

     \.(tgz|xz)

per https://bugs.launchpad.net/juju-gui/+bug/1218888 ?

...No, never mind. We can do that in a separate branch, later. It
would be good to decrease the charm size, now that releases will be
included, but not something we have to tackle right now. Stop me! :-)

https://codereview.appspot.com/14545044/diff/1/hooks/utils.py#newcode...

Read more...

127. By Francesco Banconi

Changes as per review.

Revision history for this message
Francesco Banconi (frankban) wrote :
Download full text (6.8 KiB)

*** Submitted:

Introduce local GUI releases.

Implemented the new "local" juju-gui-source option.
By default the GUI release is now retrieved from
the charm itself, so that in the deployment process
the charm can avoid connecting to Launchpad.

This should result in the charm being able to be
deployed behind a firewall without the traditional
workarounds. Note that this is not yet demonstrated
and must be QAed in a real firewalled environment.

The process tries is like the following:
- if juju-gui-source is "local", the last tarball
   in the releases dir is installed;
- if juju-gui-source is a stable or development version,
   the charm checks if that version is present in the
   local repository before downloading it from Launchpad.
- the rest of the options should still work as usual
   (but some QA could help, e.g. deploying from a branch).

Also updated the charm documentation and added some
missing tests.

Tests: `make unittest` from the branch root
(I am currently running the functional tests).

QA (assuming 0.10.1 is the latest GUI release):
- `juju bootstrap --debug`;
- `make deploy`;
- check the logs: no PPAs are used, the local release
   is installed;
- `juju set juju-gui juju-gui-source=0.10.0`;
- check the logs: the release is downloaded from
   Launchpad;
- `juju set juju-gui juju-gui-source=0.10.1`;
- check the logs: the charm successfully find
   the 0.10.1 release in the local repository and
   avoids downloading it from Launchpad.
- keep trying to break the charm in all the ways
   you can imagine...

Thank you.

R=matthew.scott, gary.poster
CC=
https://codereview.appspot.com/14545044

https://codereview.appspot.com/14545044/diff/1/config.yaml
File config.yaml (right):

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode20
config.yaml:20: Where to install Juju GUI from. Possible values are:
On 2013/10/08 18:29:31, gary.poster wrote:
> Possible values are the following.

> [changes below will largely be punctuation and capitalization,
shifting
> semicolons to periods and capitalizing the start of new sentences.]

Done.

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode21
config.yaml:21: - 'local' (default): the latest local release will be
deployed. Releases
On 2013/10/08 18:29:31, gary.poster wrote:
> Capitalize "The"

Done.

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode22
config.yaml:22: are stored in the releases directory of this charm;
On 2013/10/08 18:29:31, gary.poster wrote:
> ...charm; -> ...charm.

Done.

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode23
config.yaml:23: - 'stable' the latest stable release will be deployed;
On 2013/10/08 18:29:31, gary.poster wrote:
> The latest release from the "stable" series will be deployed.

Done.

https://codereview.appspot.com/14545044/diff/1/config.yaml#newcode24
config.yaml:24: - 'trunk': the latest trunk release will be deployed;
On 2013/10/08 18:29:31, gary.poster wrote:
> The latest release from the "trunk" series will be deployed. Please
note that
> this is not a build of the current Juju GUI trunk. For that
functionality, use
> "lp:juju-gui", as described below.

Good clarification! Done.

h...

Read more...

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

Thank you both for the reviews!

https://codereview.appspot.com/14545044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'HACKING.md'
2--- HACKING.md 2013-08-28 14:47:13 +0000
3+++ HACKING.md 2013-10-09 08:56:52 +0000
4@@ -161,3 +161,17 @@
5
6 All of this is described in more detail on the Juju site: this is an
7 introduction to the process.
8+
9+## Upgrading the local releases repository ##
10+
11+The charm, in the default juju-gui-source configuration ("local"), deploys the
12+GUI from the local releases repository. The repository is the `releases`
13+directory located in the branch source, and contains GUI releases as tarball
14+files. When a new GUI version is released, we want to also update the local
15+repository. To do that, just copy the GUI tarball file to the `releases`
16+directory, and commit a new revision of the charm.
17+It is safe to remove the older files, and it is even recommended: too many
18+releases can slow down the deployment process, especially when deploying from
19+the local charm.
20+Note that at least one release must be always present in the repository,
21+otherwise the deployment process will fail.
22
23=== modified file 'README.md'
24--- README.md 2013-08-02 14:19:13 +0000
25+++ README.md 2013-10-09 08:56:52 +0000
26@@ -18,7 +18,7 @@
27
28 ## Demo and Staging Servers ##
29
30-The Juju GUI runs the Juju Charm Store on
31+The Juju GUI runs the Juju Charm Store on
32 [jujucharms.com](http://jujucharms.com). From there, you can browse charms,
33 try the GUI, and build an example environment to export for use elsewhere.
34
35@@ -78,10 +78,29 @@
36
37 ### Deploying behind a firewall ###
38
39-While we may change this in the future to make firewall deploys simpler, this
40-charm pulls the latest release from the Juju GUI page on Launchpad. If you
41-are behind a firewall, however, this may not be available to you. In this case,
42-you can configure the charm to pull the GUI release from a location you specify.
43+When using the default options the charm uses the network connection only for
44+installing Deb packages from the default Ubuntu repositories. For this reason
45+the charm can be deployed behind a firewall in the usual way:
46+
47+ juju deploy juju-gui
48+
49+There are situations and customizations in which the charm needs to connect to
50+Launchpad:
51+
52+- juju-gui-source is set to "stable" or "trunk": in this cases the charm pulls
53+ the latest stable or development release from Launchpad;
54+- juju-gui-source is set to a branch (e.g. "lp:juju-gui"): in this case the
55+ charm retrieves a checkout of the specified branch from Launchpad, and adds
56+ an external Launchpad PPA to install build dependencies;
57+- juju-gui-source is set to a specific version number not available in the
58+ local store (i.e. in the releases directory of the deployed charm): in this
59+ case the release is downloaded from Launchpad;
60+- builtin-server is set to false: in this case the charm adds an external
61+ Launchpad PPA to install the legacy server dependencies.
62+
63+If, for any reason, you need to use the legacy server, it is still possible to
64+deploy behind a firewall configuring the charm to pull the GUI release from a
65+location you specify.
66
67 For both Juju Core and PyJuju, you must simply do the following steps. Note
68 that PyJuju must do these steps, plus another set described further below.
69@@ -117,6 +136,18 @@
70
71 3. Retry as described in the step 3 above (`juju resolved --retry juju-gui/0`).
72
73+### Upgrading the charm behind a firewall ###
74+
75+When a new version of Juju GUI is released, the charm is updated to include the
76+new release in the local releases repository. Assuming the new version is
77+1.0.1, after upgrading the charm, it is possible to also upgrade to the newer
78+Juju GUI release by running the following:
79+
80+ juju set juju-gui-source=1.0.1
81+
82+In this case the new version will be found in the local repository and
83+therefore the charm will not attempt to connect to Launchpad.
84+
85 ### Deploying to a chosen machine ###
86
87 The instructions above cause you to use a separate machine to work with the
88
89=== modified file 'config.yaml'
90--- config.yaml 2013-10-07 13:26:52 +0000
91+++ config.yaml 2013-10-09 08:56:52 +0000
92@@ -17,21 +17,29 @@
93 options:
94 juju-gui-source:
95 description: |
96- Where to install Juju GUI from. Possible values are:
97- - 'stable' (default): the latest stable release will be deployed;
98- - 'trunk': the latest trunk release will be deployed;
99- - a stable version (e.g '0.1.0'): the specified stable version will be
100- deployed;
101- - a trunk version (e.g '0.1.0+build.1'): the specified trunk version
102- will be deployed;
103- - a Bazaar branch (e.g. 'lp:juju-gui'): a release will be created and
104- deployed from the specified Bazaar branch. 'http://' prefixed branches
105+ Where to install Juju GUI from. Possible values the following.
106+ - 'local' (default): The latest local release will be deployed. Releases
107+ are stored in the releases directory of this charm.
108+ - 'stable': The latest release from the "stable" series will be deployed.
109+ - 'trunk': The latest release from the "trunk" series will be deployed.
110+ Please note that this is not a build of the current Juju GUI trunk.
111+ For that functionality, use "lp:juju-gui", as described below.
112+ - a stable version (e.g '0.1.0'): The specified stable version will be
113+ deployed. A suitable release is looked up in the local releases
114+ repository (see the "local" choice above). If not found locally, the
115+ release will be downloaded from Launchpad.
116+ - a trunk version (e.g '0.1.0+build.1'): The specified trunk version
117+ will be deployed. A suitable release is looked up in the local releases
118+ repository (see the "local" choice above). If not found locally, the
119+ release will be downloaded from Launchpad.
120+ - a Bazaar branch (e.g. 'lp:juju-gui'): A release will be created and
121+ deployed from the specified Bazaar branch. "http://"" prefixed branches
122 work as well. It is also possible to include the specific branch
123- revision, e.g. 'lp:juju-gui:42' will checkout revno 42.
124- - a 'url:' prefixed url (ex: url:http://...) of a specific location
125- to pull a release from.
126+ revision, e.g. "lp:juju-gui:42" will checkout revno 42.
127+ - a "url:"" prefixed url: The release found at the given URL
128+ (ex: url:http://... or url:file://...) will be deployed.
129 type: string
130- default: stable
131+ default: local
132 juju-gui-debug:
133 description: |
134 Run Juju GUI in debug mode, serving the uncompressed GUI source files.
135
136=== modified file 'hooks/utils.py'
137--- hooks/utils.py 2013-10-07 14:17:11 +0000
138+++ hooks/utils.py 2013-10-09 08:56:52 +0000
139@@ -27,14 +27,16 @@
140 'WEB_PORT',
141 'cmd_log',
142 'compute_build_dir',
143+ 'download_release',
144 'fetch_api',
145 'fetch_gui_from_branch',
146 'fetch_gui_release',
147 'find_missing_packages',
148 'first_path_in_dir',
149 'get_api_address',
150+ 'get_launchpad_release',
151 'get_npm_cache_archive_url',
152- 'get_release_file_url',
153+ 'get_release_file_path',
154 'get_zookeeper_address',
155 'install_missing_packages',
156 'legacy_juju',
157@@ -60,6 +62,7 @@
158 ]
159
160 from contextlib import contextmanager
161+from distutils.version import LooseVersion
162 import errno
163 import json
164 import os
165@@ -68,7 +71,7 @@
166 import shutil
167 from subprocess import CalledProcessError
168 import tempfile
169-from urlparse import urlparse
170+import urlparse
171
172 import apt
173 import tempita
174@@ -110,6 +113,7 @@
175 CONFIG_DIR = os.path.join(CURRENT_DIR, 'config')
176 JUJU_AGENT_DIR = os.path.join(BASE_DIR, 'juju')
177 JUJU_GUI_DIR = os.path.join(BASE_DIR, 'juju-gui')
178+RELEASES_DIR = os.path.join(CURRENT_DIR, 'releases')
179 # Builtin server dependencies. The order of these requirements is important.
180 SERVER_DEPENDENCIES = (
181 'futures-2.1.4.tar.gz',
182@@ -153,6 +157,14 @@
183 (?::(\d+))? # Optional branch revision.
184 $ # End of line.
185 """, re.VERBOSE)
186+release_expression = re.compile(r"""
187+ juju-gui- # Juju GUI prefix.
188+ (
189+ \d+\.\d+\.\d+ # Major, minor, and patch version numbers.
190+ (?:\+build\.\d+)? # Optional bzr revno for development releases.
191+ )
192+ \.tgz # File extension.
193+""", re.VERBOSE)
194
195 results_log = None
196
197@@ -200,14 +212,15 @@
198 return item
199
200
201-def get_release_file_url(project, series_name, release_version):
202- """Return the URL of the release file hosted in Launchpad.
203+def get_launchpad_release(project, series_name, release_version):
204+ """Return the URL and the name of the release file hosted in Launchpad.
205
206 The returned URL points to a release file for the given project, series
207 name and release version.
208 The argument *project* is a project object as returned by launchpadlib.
209 The arguments *series_name* and *release_version* are strings. If
210- *release_version* is None, the URL of the latest release will be returned.
211+ *release_version* is None, the URL and file name of the latest release will
212+ be returned.
213 """
214 series = _get_by_attr(project.series, 'name', series_name)
215 if series is None:
216@@ -223,8 +236,10 @@
217 releases = [release]
218 for release in releases:
219 for file_ in release.files:
220- if str(file_).endswith('.tgz'):
221- return file_.file_link
222+ file_url = str(file_)
223+ if file_url.endswith('.tgz'):
224+ filename = os.path.split(urlparse.urlsplit(file_url).path)[1]
225+ return file_.file_link, filename
226 raise ValueError('%r: file not found' % release_version)
227
228
229@@ -264,6 +279,7 @@
230
231 Return a tuple of two elements representing info on how to deploy Juju GUI.
232 Examples:
233+ - ('local', None): latest local release;
234 - ('stable', None): latest stable release;
235 - ('stable', '0.1.0'): stable release v0.1.0;
236 - ('trunk', None): latest trunk release;
237@@ -276,12 +292,12 @@
238 if source.startswith('url:'):
239 source = source[4:]
240 # Support file paths, including relative paths.
241- if urlparse(source).scheme == '':
242+ if urlparse.urlparse(source).scheme == '':
243 if not source.startswith('/'):
244 source = os.path.join(os.path.abspath(CURRENT_DIR), source)
245 source = "file://%s" % source
246 return 'url', source
247- if source in ('stable', 'trunk'):
248+ if source in ('local', 'stable', 'trunk'):
249 return source, None
250 match = bzr_url_expression.match(source)
251 if match is not None:
252@@ -596,7 +612,7 @@
253 launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production')
254 project = launchpad.projects['juju-gui']
255 # Find the URL of the most recently created NPM cache archive.
256- npm_cache_url = get_release_file_url(project, 'npm-cache', None)
257+ npm_cache_url, _ = get_launchpad_release(project, 'npm-cache', None)
258 return npm_cache_url
259
260
261@@ -641,21 +657,74 @@
262 os.path.join(juju_gui_source_dir, 'releases'))
263
264
265+def download_release(url, filename):
266+ """Download a Juju GUI release from the given URL.
267+
268+ Save the resulting file as filename in the local releases repository.
269+ Return the full path of the saved file.
270+ """
271+ destination = os.path.join(RELEASES_DIR, filename)
272+ log('Downloading release file: {} --> {}.'.format(url, destination))
273+ cmd_log(run('curl', '-L', '-o', destination, url))
274+ return destination
275+
276+
277+def get_release_file_path(version=None):
278+ """Return the local path of the release file with the given version.
279+
280+ If version is None, return the path of the last release.
281+ Raise a ValueError if no releases are found in the local repository.
282+ """
283+ version_path_map = {}
284+ # Collect the locally stored releases.
285+ for filename in os.listdir(RELEASES_DIR):
286+ match = release_expression.match(filename)
287+ if match is not None:
288+ release_version = match.groups()[0]
289+ release_path = os.path.join(RELEASES_DIR, filename)
290+ version_path_map[release_version] = release_path
291+ # We expect the charm to include at least one release file.
292+ if not version_path_map:
293+ raise ValueError('Error: no releases found in the charm.')
294+ if version is None:
295+ # Return the path of the last release.
296+ last_version = sorted(version_path_map.keys(), key=LooseVersion)[-1]
297+ return version_path_map[last_version]
298+ # Return the path of the release with the requested version, or None if
299+ # the release is not found.
300+ return version_path_map.get(version)
301+
302+
303 def fetch_gui_release(origin, version):
304- """Retrieve a Juju GUI release."""
305+ """Retrieve a Juju GUI release. Return the release tarball local path.
306+
307+ The release file can be retrieved from:
308+ - an arbitrary URL (if origin is "url");
309+ - the local releases repository (if origin is "local" or if a release
310+ version is specified and the corresponding file is present locally);
311+ - Launchpad (in all the other cases).
312+ """
313 log('Retrieving Juju GUI release.')
314 if origin == 'url':
315- file_url = version
316- else:
317- # Retrieve a release from Launchpad.
318- launchpad = Launchpad.login_anonymously(
319- 'Juju GUI charm', 'production')
320- project = launchpad.projects['juju-gui']
321- file_url = get_release_file_url(project, origin, version)
322- log('Downloading release file from %s.' % file_url)
323- release_tarball = os.path.join(CURRENT_DIR, 'release.tgz')
324- cmd_log(run('curl', '-L', '-o', release_tarball, file_url))
325- return release_tarball
326+ return download_release(version, 'url-release.tgz')
327+ if origin == 'local':
328+ path = get_release_file_path()
329+ log('Using a local release: {}'.format(path))
330+ return path
331+ # Handle "stable" and "trunk" origins.
332+ if version is not None:
333+ # If the user specified a version, before attempting to download the
334+ # requested release from Launchpad, check if that version is already
335+ # stored locally.
336+ path = get_release_file_path(version)
337+ if path is not None:
338+ log('Using a local release: {}'.format(path))
339+ return path
340+ # Retrieve a release from Launchpad.
341+ launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production')
342+ project = launchpad.projects['juju-gui']
343+ url, filename = get_launchpad_release(project, origin, version)
344+ return download_release(url, filename)
345
346
347 def fetch_api(juju_api_branch):
348
349=== added directory 'releases'
350=== added file 'releases/juju-gui-0.10.1.tgz'
351Binary files releases/juju-gui-0.10.1.tgz 1970-01-01 00:00:00 +0000 and releases/juju-gui-0.10.1.tgz 2013-10-09 08:56:52 +0000 differ
352=== modified file 'revision'
353--- revision 2013-10-07 14:18:05 +0000
354+++ revision 2013-10-09 08:56:52 +0000
355@@ -1,1 +1,1 @@
356-89
357+90
358
359=== modified file 'tests/20-functional.test'
360--- tests/20-functional.test 2013-10-07 16:17:20 +0000
361+++ tests/20-functional.test 2013-10-09 08:56:52 +0000
362@@ -168,14 +168,25 @@
363 def tearDown(self):
364 juju_destroy_service(self.charm)
365
366- def test_api_agent(self):
367- # Ensure the Juju GUI and API agent services are correctly set up.
368+ def test_local_release(self):
369+ # Ensure the Juju GUI and API agent services are correctly set up when
370+ # deploying the local release.
371 unit_info = self.juju_deploy(self.charm)
372 hostname = unit_info['public-address']
373 self.navigate_to(hostname)
374 self.handle_browser_warning()
375 self.assertEnvironmentIsConnected()
376
377+ def test_stable_release(self):
378+ # Ensure the Juju GUI and API agent services are correctly set up when
379+ # deploying the stable release.
380+ options = {'juju-gui-source': 'stable'}
381+ unit_info = self.juju_deploy(self.charm, options=options)
382+ hostname = unit_info['public-address']
383+ self.navigate_to(hostname)
384+ self.handle_browser_warning()
385+ self.assertEnvironmentIsConnected()
386+
387 @unittest.skipUnless(is_legacy_juju, 'staging only works in pyJuju')
388 def test_staging(self):
389 # Ensure the Juju GUI and improv services are correctly set up.
390@@ -189,8 +200,7 @@
391
392 def test_sandbox(self):
393 # The GUI is correctly deployed and set up in sandbox mode.
394- unit_info = self.juju_deploy(
395- self.charm, options={'builtin-server': 'true', 'sandbox': 'true'})
396+ unit_info = self.juju_deploy(self.charm, options={'sandbox': 'true'})
397 hostname = unit_info['public-address']
398 self.navigate_to(hostname)
399 self.handle_browser_warning()
400@@ -198,15 +208,16 @@
401
402 def test_branch_source(self):
403 # Ensure the Juju GUI is correctly deployed from a Bazaar branch.
404- unit_info = self.juju_deploy(
405- self.charm, options={'juju-gui-source': JUJU_GUI_TEST_BRANCH})
406+ options = {'juju-gui-source': JUJU_GUI_TEST_BRANCH}
407+ unit_info = self.juju_deploy(self.charm, options=options)
408 hostname = unit_info['public-address']
409 self.navigate_to(hostname)
410 self.handle_browser_warning()
411 self.assertEnvironmentIsConnected()
412
413- def test_cache_headers(self):
414- # Make sure the correct cache headers are sent.
415+ def test_legacy_server(self):
416+ # The legacy apache + haproxy server configuration works correctly.
417+ # Also make sure the correct cache headers are sent.
418 options = {
419 'builtin-server': False,
420 'juju-gui-source': JUJU_GUI_TEST_BRANCH,
421@@ -236,8 +247,8 @@
422
423 def test_nrpe_check_available(self):
424 # Make sure the check-app-access.sh script's ADDRESS is available.
425- unit_info = self.juju_deploy(
426- self.charm, options={'juju-gui-source': JUJU_GUI_TEST_BRANCH})
427+ options = {'juju-gui-source': JUJU_GUI_TEST_BRANCH}
428+ unit_info = self.juju_deploy(self.charm, options=options)
429 hostname = unit_info['public-address']
430 conn = httplib.HTTPSConnection(hostname)
431 # This request matches the ADDRESS var in the script.
432@@ -252,7 +263,7 @@
433 def setUpClass(cls):
434 # Deploy the charm. The resulting service is used by all the tests
435 # in this test case.
436- unit_info = juju_deploy(cls.charm, options={'builtin-server': 'true'})
437+ unit_info = juju_deploy(cls.charm)
438 cls.hostname = unit_info['public-address']
439 # The counter is used to produce API request identifiers.
440 cls.counter = itertools.count()
441
442=== modified file 'tests/test_utils.py'
443--- tests/test_utils.py 2013-10-07 14:17:11 +0000
444+++ tests/test_utils.py 2013-10-09 08:56:52 +0000
445@@ -39,16 +39,19 @@
446 _get_by_attr,
447 cmd_log,
448 compute_build_dir,
449+ download_release,
450+ fetch_gui_release,
451 first_path_in_dir,
452 get_api_address,
453- get_release_file_url,
454+ get_launchpad_release,
455+ get_npm_cache_archive_url,
456+ get_release_file_path,
457 get_zookeeper_address,
458+ install_builtin_server,
459+ install_missing_packages,
460 legacy_juju,
461 log_hook,
462 parse_source,
463- get_npm_cache_archive_url,
464- install_builtin_server,
465- install_missing_packages,
466 remove_apache_setup,
467 remove_haproxy_setup,
468 render_to_file,
469@@ -94,6 +97,108 @@
470 AttrDict().myattr
471
472
473+@mock.patch('utils.run')
474+@mock.patch('utils.log')
475+@mock.patch('utils.cmd_log', mock.Mock())
476+class TestDownloadRelease(unittest.TestCase):
477+
478+ def test_download(self, mock_log, mock_run):
479+ # A release is properly downloaded using curl.
480+ url = 'http://download.example.com/release.tgz'
481+ filename = 'local-release.tgz'
482+ destination = download_release(url, filename)
483+ expected_destination = os.path.join(os.getcwd(), 'releases', filename)
484+ self.assertEqual(expected_destination, destination)
485+ expected_log = 'Downloading release file: {} --> {}.'.format(
486+ url, expected_destination)
487+ mock_log.assert_called_once_with(expected_log)
488+ mock_run.assert_called_once_with(
489+ 'curl', '-L', '-o', expected_destination, url)
490+
491+
492+@mock.patch('utils.log', mock.Mock())
493+class TestFetchGuiRelease(unittest.TestCase):
494+
495+ release_path = '/my/release.tgz'
496+
497+ @contextmanager
498+ def patch_launchpad(self, origin, version):
499+ """Mock the functions used to download a release from Launchpad.
500+
501+ Ensure all the functions are called correctly.
502+ """
503+ url = 'http://launchpad.example.com/release.tgz/file'
504+ filename = 'release.tgz'
505+ patch_launchpad = mock.patch('utils.Launchpad')
506+ patch_get_launchpad_release = mock.patch(
507+ 'utils.get_launchpad_release',
508+ mock.Mock(return_value=(url, filename)),
509+ )
510+ patch_download_release = mock.patch(
511+ 'utils.download_release',
512+ mock.Mock(return_value=self.release_path),
513+ )
514+ with patch_launchpad as mock_launchpad:
515+ with patch_get_launchpad_release as mock_get_launchpad_release:
516+ with patch_download_release as mock_download_release:
517+ yield
518+ login = mock_launchpad.login_anonymously
519+ login.assert_called_once_with('Juju GUI charm', 'production')
520+ mock_get_launchpad_release.assert_called_once_with(
521+ login().projects['juju-gui'], origin, version)
522+ mock_download_release.assert_called_once_with(url, filename)
523+
524+ @mock.patch('utils.download_release')
525+ def test_url(self, mock_download_release):
526+ # The release is retrieved from an URL.
527+ mock_download_release.return_value = self.release_path
528+ url = 'http://download.example.com/release.tgz'
529+ path = fetch_gui_release('url', url)
530+ self.assertEqual(self.release_path, path)
531+ mock_download_release.assert_called_once_with(url, 'url-release.tgz')
532+
533+ @mock.patch('utils.get_release_file_path')
534+ def test_local(self, mock_get_release_file_path):
535+ # The last local release is requested.
536+ mock_get_release_file_path.return_value = self.release_path
537+ path = fetch_gui_release('local', None)
538+ self.assertEqual(self.release_path, path)
539+ mock_get_release_file_path.assert_called_once_with()
540+
541+ @mock.patch('utils.get_release_file_path')
542+ def test_version_found(self, mock_get_release_file_path):
543+ # A release version is specified and found locally.
544+ mock_get_release_file_path.return_value = self.release_path
545+ path = fetch_gui_release('stable', '0.1.42')
546+ self.assertEqual(self.release_path, path)
547+ mock_get_release_file_path.assert_called_once_with('0.1.42')
548+
549+ @mock.patch('utils.get_release_file_path')
550+ def test_version_not_found(self, mock_get_release_file_path):
551+ # A release version is specified but not found locally.
552+ mock_get_release_file_path.return_value = None
553+ with self.patch_launchpad('stable', '0.1.42'):
554+ path = fetch_gui_release('stable', '0.1.42')
555+ self.assertEqual(self.release_path, path)
556+ mock_get_release_file_path.assert_called_once_with('0.1.42')
557+
558+ @mock.patch('utils.get_release_file_path')
559+ def test_stable(self, mock_get_release_file_path):
560+ # The last stable release is requested.
561+ with self.patch_launchpad('stable', None):
562+ path = fetch_gui_release('stable', None)
563+ self.assertEqual(self.release_path, path)
564+ self.assertFalse(mock_get_release_file_path.called)
565+
566+ @mock.patch('utils.get_release_file_path')
567+ def test_trunk(self, mock_get_release_file_path):
568+ # The last development release is requested.
569+ with self.patch_launchpad('trunk', None):
570+ path = fetch_gui_release('trunk', None)
571+ self.assertEqual(self.release_path, path)
572+ self.assertFalse(mock_get_release_file_path.called)
573+
574+
575 class TestFirstPathInDir(unittest.TestCase):
576
577 def setUp(self):
578@@ -184,6 +289,105 @@
579 self.assertRaises(IOError, get_api_address, unit_dir)
580
581
582+class TestGetReleaseFilePath(unittest.TestCase):
583+
584+ def setUp(self):
585+ self.playground = tempfile.mkdtemp()
586+ self.addCleanup(shutil.rmtree, self.playground)
587+
588+ def mock_releases_dir(self):
589+ """Mock the releases directory."""
590+ return mock.patch('utils.RELEASES_DIR', self.playground)
591+
592+ def assert_path(self, filename, path):
593+ """Ensure the absolute path of filename equals the given path."""
594+ expected = os.path.join(self.playground, filename)
595+ self.assertEqual(expected, path)
596+
597+ @contextmanager
598+ def assert_error(self):
599+ """Ensure the code executed in the context block raises a ValueError.
600+
601+ Also check the error message.
602+ """
603+ with self.assertRaises(ValueError) as context_manager:
604+ yield
605+ error = str(context_manager.exception)
606+ self.assertEqual('Error: no releases found in the charm.', error)
607+
608+ def add(self, filename):
609+ """Create a release file in the playground directory."""
610+ path = os.path.join(self.playground, filename)
611+ open(path, 'w').close()
612+
613+ def test_last_release(self):
614+ # The last release is correctly retrieved.
615+ self.add('juju-gui-0.12.1.tgz')
616+ self.add('juju-gui-1.2.3.tgz')
617+ self.add('juju-gui-2.0.0+build.42.tgz')
618+ self.add('juju-gui-2.0.1.tgz')
619+ with self.mock_releases_dir():
620+ path = get_release_file_path()
621+ self.assert_path('juju-gui-2.0.1.tgz', path)
622+
623+ def test_ordering(self):
624+ # Release versions are correctly ordered.
625+ self.add('juju-gui-0.12.1.tgz')
626+ self.add('juju-gui-0.9.1.tgz')
627+ with self.mock_releases_dir():
628+ path = get_release_file_path()
629+ self.assert_path('juju-gui-0.12.1.tgz', path)
630+
631+ def test_no_releases(self):
632+ # A ValueError is raised if no releases are found.
633+ with self.mock_releases_dir():
634+ with self.assert_error():
635+ get_release_file_path()
636+
637+ def test_no_releases_with_files(self):
638+ # A ValueError is raised if no releases are found.
639+ # Extraneous files are ignored while looking for releases.
640+ self.add('jujugui-1.2.3.tgz') # Wrong prefix.
641+ self.add('juju-gui-1.2.tgz') # Missing patch version number.
642+ self.add('juju-gui-1.2.3.bz2') # Wrong file extension.
643+ self.add('juju-gui-1.2.3.4.tgz') # Wrong version.
644+ self.add('juju-gui-1.2.3.build.42.tgz') # Missing "+" separator.
645+ self.add('juju-gui-1.2.3+built.42.tgz') # Typo.
646+ self.add('juju-gui-1.2.3+build.42.47.tgz') # Invalid bzr revno.
647+ self.add('juju-gui-1.2.3+build.42.bz2') # Wrong file extension again.
648+ with self.mock_releases_dir():
649+ with self.assert_error():
650+ print get_release_file_path()
651+
652+ def test_stable_version(self):
653+ # A specific stable version is correctly retrieved.
654+ self.add('juju-gui-1.2.3.tgz')
655+ self.add('juju-gui-2.0.1+build.42.tgz')
656+ self.add('juju-gui-2.0.1.tgz')
657+ self.add('juju-gui-3.2.1.tgz')
658+ with self.mock_releases_dir():
659+ path = get_release_file_path('2.0.1')
660+ self.assert_path('juju-gui-2.0.1.tgz', path)
661+
662+ def test_development_version(self):
663+ # A specific development version is correctly retrieved.
664+ self.add('juju-gui-1.2.3+build.4247.tgz')
665+ self.add('juju-gui-2.42.47+build.4247.tgz')
666+ self.add('juju-gui-2.42.47.tgz')
667+ self.add('juju-gui-3.42.47+build.4247.tgz')
668+ with self.mock_releases_dir():
669+ path = get_release_file_path('2.42.47+build.4247')
670+ self.assert_path('juju-gui-2.42.47+build.4247.tgz', path)
671+
672+ def test_version_not_found(self):
673+ # None is returned if the requested version is not found.
674+ self.add('juju-gui-1.2.3.tgz')
675+ self.add('juju-GUI-1.42.47.tgz') # This is not a valid release.
676+ with self.mock_releases_dir():
677+ path = get_release_file_path('1.42.47')
678+ self.assertIsNone(path)
679+
680+
681 class TestLegacyJuju(unittest.TestCase):
682
683 def setUp(self):
684@@ -261,7 +465,7 @@
685 return self.file_link
686
687
688-class TestGetReleaseFileUrl(unittest.TestCase):
689+class TestGetLaunchpadRelease(unittest.TestCase):
690
691 project = AttrDict(
692 series=(
693@@ -308,43 +512,48 @@
694
695 def test_latest_stable_release(self):
696 # Ensure the correct URL is returned for the latest stable release.
697- url = get_release_file_url(self.project, 'stable', None)
698+ url, name = get_launchpad_release(self.project, 'stable', None)
699 self.assertEqual('http://example.com/0.1.1.tgz', url)
700+ self.assertEqual('0.1.1.tgz', name)
701
702 def test_latest_trunk_release(self):
703 # Ensure the correct URL is returned for the latest trunk release.
704- url = get_release_file_url(self.project, 'trunk', None)
705+ url, name = get_launchpad_release(self.project, 'trunk', None)
706 self.assertEqual('http://example.com/0.1.1+build.1.tgz', url)
707+ self.assertEqual('0.1.1+build.1.tgz', name)
708
709 def test_specific_stable_release(self):
710 # Ensure the correct URL is returned for a specific version of the
711 # stable release.
712- url = get_release_file_url(self.project, 'stable', '0.1.0')
713+ url, name = get_launchpad_release(self.project, 'stable', '0.1.0')
714 self.assertEqual('http://example.com/0.1.0.tgz', url)
715+ self.assertEqual('0.1.0.tgz', name)
716
717 def test_specific_trunk_release(self):
718 # Ensure the correct URL is returned for a specific version of the
719 # trunk release.
720- url = get_release_file_url(self.project, 'trunk', '0.1.0+build.1')
721+ url, name = get_launchpad_release(
722+ self.project, 'trunk', '0.1.0+build.1')
723 self.assertEqual('http://example.com/0.1.0+build.1.tgz', url)
724+ self.assertEqual('0.1.0+build.1.tgz', name)
725
726 def test_series_not_found(self):
727 # A ValueError is raised if the series cannot be found.
728 with self.assertRaises(ValueError) as cm:
729- get_release_file_url(self.project, 'unstable', None)
730+ get_launchpad_release(self.project, 'unstable', None)
731 self.assertIn('series not found', str(cm.exception))
732
733 def test_no_releases(self):
734 # A ValueError is raised if the series does not contain releases.
735 project = AttrDict(series=[AttrDict(name='stable', releases=[])])
736 with self.assertRaises(ValueError) as cm:
737- get_release_file_url(project, 'stable', None)
738+ get_launchpad_release(project, 'stable', None)
739 self.assertIn('series does not contain releases', str(cm.exception))
740
741 def test_release_not_found(self):
742 # A ValueError is raised if the release cannot be found.
743 with self.assertRaises(ValueError) as cm:
744- get_release_file_url(self.project, 'stable', '2.0')
745+ get_launchpad_release(self.project, 'stable', '2.0')
746 self.assertIn('release not found', str(cm.exception))
747
748 def test_file_not_found(self):
749@@ -358,7 +567,7 @@
750 ],
751 )
752 with self.assertRaises(ValueError) as cm:
753- get_release_file_url(project, 'stable', None)
754+ get_launchpad_release(project, 'stable', None)
755 self.assertIn('file not found', str(cm.exception))
756
757 def test_file_not_found_in_latest_release(self):
758@@ -378,8 +587,9 @@
759 ),
760 ],
761 )
762- url = get_release_file_url(project, 'stable', None)
763+ url, name = get_launchpad_release(project, 'stable', None)
764 self.assertEqual('http://example.com/0.1.0.tgz', url)
765+ self.assertEqual('0.1.0.tgz', name)
766
767
768 class TestGetZookeeperAddress(unittest.TestCase):
769@@ -452,6 +662,11 @@
770 # Restore the original utils.CURRENT_DIR.
771 utils.CURRENT_DIR = self.original_current_dir
772
773+ def test_latest_local_release(self):
774+ # Ensure the latest local release is correctly parsed.
775+ expected = ('local', None)
776+ self.assertTupleEqual(expected, parse_source('local'))
777+
778 def test_latest_stable_release(self):
779 # Ensure the latest stable release is correctly parsed.
780 expected = ('stable', None)

Subscribers

People subscribed via source and target branches