Merge lp:~tribaal/landscape-charm/send-haproxy-error-in-relation into lp:~landscape/landscape-charm/trunk

Proposed by Chris Glass
Status: Merged
Approved by: Chris Glass
Approved revision: 251
Merged at revision: 242
Proposed branch: lp:~tribaal/landscape-charm/send-haproxy-error-in-relation
Merge into: lp:~landscape/landscape-charm/trunk
Diff against target: 335 lines (+115/-28)
7 files modified
README.md (+1/-1)
hooks/lib/relations/haproxy.py (+42/-3)
hooks/lib/relations/tests/test_haproxy.py (+30/-2)
hooks/lib/services.py (+4/-3)
hooks/lib/tests/offline_fixture.py (+21/-0)
hooks/lib/tests/test_services.py (+3/-1)
tests/01-begin.py (+14/-18)
To merge this branch: bzr merge lp:~tribaal/landscape-charm/send-haproxy-error-in-relation
Reviewer Review Type Date Requested Status
Free Ekanayaka (community) Approve
Данило Шеган (community) Approve
🤖 Landscape Builder test results Approve
Review via email: mp+254584@code.launchpad.net

Commit message

This branch adds errorfiles to the haproxy relation.

Description of the change

This branch adds errorfiles to the haproxy relation.

Unfortunately https://bugs.launchpad.net/juju-core/+bug/1437366 prevents us from enabling all of the errorfiles for now, but once fixed it should be pretty easy to enable (just uncommenting the errorfiles in the map introduced here).

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 241
Branch: lp:~tribaal/landscape-charm/send-haproxy-error-in-relation
Jenkins: https://ci.lscape.net/job/latch-test/364/

review: Approve (test results)
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Marking as needs information because I'd like to have your opinion on a testing approach based on dependency-injection rather than monkey-patching/mocking.

review: Needs Information
Revision history for this message
Данило Шеган (danilo) wrote :

I personally do not have a specific preference towards one approach or the other: neither solves the critical problem in that when the real interface changes, tests continue working because they are using stubs/mocks. However, we (well, Free, and then the rest of us) have already started using the dependency injection for the landscape-charm, and I like the consistency.

So, unless there's a particular reason not to use the same pattern, I'd prefer it if you did.

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

> I personally do not have a specific preference towards one approach or the
> other: neither solves the critical problem in that when the real interface
> changes, tests continue working because they are using stubs/mocks. However,
> we (well, Free, and then the rest of us) have already started using the
> dependency injection for the landscape-charm, and I like the consistency.

The difference with the proposed approached is that:

- You'll limit the interfaces that you mock/stub to the minimum, i.e. either to APIs that you don't control and are not easily unit-testable (either by nature of because of factoring problem) or to APIs that you do control but are not easily unit-testable by nature (e.g. they go over the network). In the code in this branch we're mocking our own APIs that we could just call directly, as it happens in the proposed approach: note that if that interface changes tests *will* break.
 So there might indeed be some parts that don't exercise the real interface, but they will be limited to the minimum, and in this particular branch there's no need for them.

- When your code grows and the call stack/layers become deeper you won't have mocking explosion.

Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 242
Branch: lp:~tribaal/landscape-charm/send-haproxy-error-in-relation
Jenkins: https://ci.lscape.net/job/latch-test/399/

review: Approve (test results)
Revision history for this message
Данило Шеган (danilo) wrote :

Other than the left-over debugging import and possibly better test setup, looks good. A few minor comments throughout (mostly me being anal about var names :), but they are up to you.

review: Approve
Revision history for this message
Chris Glass (tribaal) wrote :

Sorry, I should have switched to WIP before I pushed (I switched machines), that ipdb and other things could really have been spared to you :/

Anyhow, good comments, I'll fix them before switching back to needs review :)

Revision history for this message
Данило Шеган (danilo) wrote :

Heh, no worries, sorry for being too eager, I just didn't want you to wait too long for a review since the branch has been up for a while :)

Revision history for this message
Chris Glass (tribaal) wrote :

Should be good for another round of comments.

Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 245
Branch: lp:~tribaal/landscape-charm/send-haproxy-error-in-relation
Jenkins: https://ci.lscape.net/job/latch-test/405/

review: Approve (test results)
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Looks good, but marking as Needs fixing because there's some untested behavior and I'm a bit strong on using composition over inheritance when it makes sense (see the Mixin vs Fixture comment).

review: Needs Fixing
Revision history for this message
Free Ekanayaka (free.ekanayaka) :
Revision history for this message
Chris Glass (tribaal) :
246. By Chris Glass

First review comments addressed.

247. By Chris Glass

Removed leftover comments. Fixed tests.

Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :
Download full text (10.1 KiB)

Command: make ci-test
Result: Fail
Revno: 247
Branch: lp:~tribaal/landscape-charm/send-haproxy-error-in-relation
Jenkins: https://ci.lscape.net/job/latch-test/408/
-------------------------------------------
./dev/ubuntu-deps
Reading package lists...
Building dependency tree...
Reading state information...
software-properties-common is already the newest version.
The following package was automatically installed and is no longer required:
  os-prober
Use 'apt-get autoremove' to remove it.
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
gpg: keyring `/tmp/tmpubm_e59u/secring.gpg' created
gpg: keyring `/tmp/tmpubm_e59u/pubring.gpg' created
gpg: requesting key C8068B11 from hkp server keyserver.ubuntu.com
gpg: /tmp/tmpubm_e59u/trustdb.gpg: trustdb created
gpg: key C8068B11: public key "Launchpad Ensemble PPA" imported
gpg: Total number processed: 1
gpg: imported: 1 (RSA: 1)
OK
Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Get:2 http://security.ubuntu.com trusty-security Release [63.5 kB]
Ign http://ppa.launchpad.net trusty InRelease
Ign http://ppa.launchpad.net trusty InRelease
Hit http://ppa.launchpad.net trusty Release.gpg
Hit http://ppa.launchpad.net trusty Release.gpg
Hit http://ppa.launchpad.net trusty Release
Hit http://ppa.launchpad.net trusty Release
Get:3 http://security.ubuntu.com trusty-security/main Sources [76.1 kB]
Hit http://ppa.launchpad.net trusty/main amd64 Packages
Ign https://private-ppa.launchpad.net trusty InRelease
Ign https://private-ppa.launchpad.net trusty InRelease
Get:4 http://security.ubuntu.com trusty-security/universe Sources [19.5 kB]
Get:5 http://security.ubuntu.com trusty-security/main amd64 Packages [251 kB]
Get:6 http://security.ubuntu.com trusty-security/universe amd64 Packages [92.0 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Ign http://archive.ubuntu.com trusty InRelease
Hit http://ppa.launchpad.net trusty/main Translation-en
Hit https://private-ppa.launchpad.net trusty Release.gpg
Hit https://private-ppa.launchpad.net trusty Release.gpg
Hit http://ppa.launchpad.net trusty/main amd64 Packages
Hit https://private-ppa.launchpad.net trusty Release
Hit http://ppa.launchpad.net trusty/main Translation-en
Hit https://private-ppa.launchpad.net trusty Release
Hit https://private-ppa.launchpad.net trusty/main amd64 Packages
Hit https://private-ppa.launchpad.net trusty/main Translation-en
Hit https://private-ppa.launchpad.net trusty/main amd64 Packages
Ign http://archive.ubuntu.com trusty-updates InRelease
Ign https://private-ppa.launchpad.net trusty/main Translation-en_US
Ign https://private-ppa.launchpad.net trusty/main Translation-en
Hit http://archive.ubuntu.com trusty Release.gpg
Hit http://archive.ubuntu.com trusty-updates Release.gpg
Hit http://archive.ubuntu.com trusty Release
Hit http://archive.ubuntu.com trusty-updates Release
Hit http://archive.ubuntu.com trusty/main Sources
Hit http://archive.ubuntu.com trusty/universe Sources
Hit http://archive.ubuntu.com trusty/main amd64 Packages
Hit http://archive...

review: Needs Fixing (test results)
248. By Chris Glass

Composition over inheritance change.

Revision history for this message
Chris Glass (tribaal) wrote :

All comments should be addressed now.

Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 248
Branch: lp:~tribaal/landscape-charm/send-haproxy-error-in-relation
Jenkins: https://ci.lscape.net/job/latch-test/410/

review: Approve (test results)
Revision history for this message
Данило Шеган (danilo) wrote :

FWIW, still looks good to me :)

review: Approve
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Thanks for addressing everything. A few more little points, +1 with those addressed.

review: Approve
249. By Chris Glass

Fixed a couple of review points.

250. By Chris Glass

All comments addressed.

251. By Chris Glass

lint

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README.md'
2--- README.md 2015-03-23 19:27:59 +0000
3+++ README.md 2015-04-07 13:50:51 +0000
4@@ -90,5 +90,5 @@
5 them:
6
7 # Make sure your JUJU_ENV is *not* bootstrapped, and:
8- $ sudo apt-get install python-pyscopg2 python-mocker python-psutil
9+ $ sudo apt-get install python-psycopg2 python-mocker python-psutil
10 $ JUJU_ENV=<env> make integration-test
11
12=== modified file 'hooks/lib/relations/haproxy.py'
13--- hooks/lib/relations/haproxy.py 2015-03-04 12:16:19 +0000
14+++ hooks/lib/relations/haproxy.py 2015-04-07 13:50:51 +0000
15@@ -1,5 +1,9 @@
16+import base64
17+import os
18 import yaml
19
20+from lib.hook import HookError
21+
22 from charmhelpers.core import hookenv
23 from charmhelpers.core.services.helpers import RelationContext
24
25@@ -40,6 +44,19 @@
26 "fall 5",
27 "maxconn 50",
28 ]
29+ERRORFILES_MAP = {
30+ # Add 503 only for now since that's what the integration tests
31+ # check.
32+ "503": "unplanned-offline-haproxy.html",
33+ # TODO: Due to bug #1437366 the command line call to "relation-set"
34+ # will fail by reaching MAX_ARGS if too many errorfiles are set.
35+ # Until fixed let's set only one errorfile to assert it works.
36+ #"403": "unauthorized-haproxy.html",
37+ #"500": "exception-haproxy.html",
38+ #"502": "unplanned-offline-haproxy.html",
39+ #"504": "timeout-haproxy.html",
40+}
41+OFFLINE_FOLDER = "/opt/canonical/landscape/canonical/landscape/offline"
42
43
44 class HAProxyProvider(RelationContext):
45@@ -47,11 +64,11 @@
46
47 name = "website"
48 interface = "http"
49- required_keys = [
50- "services"]
51+ required_keys = ["services"]
52
53- def __init__(self, hookenv=hookenv):
54+ def __init__(self, hookenv=hookenv, offline_dir=OFFLINE_FOLDER):
55 self._hookenv = hookenv
56+ self._offline_dir = offline_dir
57 super(HAProxyProvider, self).__init__()
58
59 def provide_data(self):
60@@ -93,6 +110,7 @@
61 "service_host": "0.0.0.0",
62 "service_port": SERVICE_PORTS[name],
63 "service_options": SERVICE_OPTIONS[name],
64+ "errorfiles": self._get_error_files()
65 }
66
67 def _backend(self, name, servers):
68@@ -118,3 +136,24 @@
69 server_name = "landscape-%s-%s" % (name, unit_name.replace("/", "-"))
70 server_port = SERVER_PORTS[name]
71 return (server_name, server_ip, server_port, SERVER_OPTIONS)
72+
73+ def _get_error_files(self):
74+ """Return the errorfiles configuration."""
75+ result = []
76+
77+ for error_code, file_name in sorted(ERRORFILES_MAP.items()):
78+ content = None
79+ path = os.path.join(self._offline_dir, file_name)
80+
81+ try:
82+ with open(path, "r") as error_file:
83+ content = error_file.read()
84+ except IOError as error:
85+ raise HookError(
86+ "Could not read '%s' (%s)!" % (path, str(error)))
87+
88+ entry = {"http_status": error_code,
89+ "content": base64.b64encode(content)}
90+ result.append(entry)
91+
92+ return result
93
94=== modified file 'hooks/lib/relations/tests/test_haproxy.py'
95--- hooks/lib/relations/tests/test_haproxy.py 2015-03-04 07:47:03 +0000
96+++ hooks/lib/relations/tests/test_haproxy.py 2015-04-07 13:50:51 +0000
97@@ -1,7 +1,13 @@
98 import yaml
99
100+from base64 import b64encode
101+from fixtures import TempDir
102+
103+from lib.relations.haproxy import (
104+ HAProxyProvider, SERVER_OPTIONS, ERRORFILES_MAP)
105+from lib.hook import HookError
106 from lib.tests.helpers import HookenvTest
107-from lib.relations.haproxy import HAProxyProvider, SERVER_OPTIONS
108+from lib.tests.offline_fixture import OfflineDir
109
110
111 class HAProxyProviderTest(HookenvTest):
112@@ -22,8 +28,18 @@
113 The HAProxyProvider class feeds haproxy with the services that this
114 Landscape unit runs. By default all services are run.
115 """
116- relation = HAProxyProvider()
117+ offline_dir = self.useFixture(OfflineDir()).path
118+ relation = HAProxyProvider(offline_dir=offline_dir)
119+
120+ expected_errorfiles = []
121+
122+ for error_code, filename in sorted(ERRORFILES_MAP.items()):
123+ expected_content = b64encode("Fake %s" % filename)
124+ expected_errorfiles.append(
125+ {"http_status": error_code, "content": expected_content})
126+
127 data = relation.provide_data()
128+
129 services = yaml.safe_load(data["services"])
130 self.assertEqual([
131 {"service_name": "landscape-http",
132@@ -36,6 +52,7 @@
133 "acl ping path_beg -i /ping",
134 "redirect scheme https unless ping",
135 "use_backend landscape-ping if ping"],
136+ "errorfiles": expected_errorfiles,
137 "servers": [
138 ["landscape-appserver-landscape-server-0",
139 "1.2.3.4", 8080, SERVER_OPTIONS]],
140@@ -56,6 +73,7 @@
141 "acl api path_beg -i /api",
142 "use_backend landscape-message if message",
143 "use_backend landscape-api if api"],
144+ "errorfiles": expected_errorfiles,
145 "crts": ["DEFAULT"],
146 "servers": [
147 ["landscape-appserver-landscape-server-0",
148@@ -70,3 +88,13 @@
149 ["landscape-api-landscape-server-0",
150 "1.2.3.4", 9080, SERVER_OPTIONS]]}]}],
151 services)
152+
153+ def test_files_cannot_be_read(self):
154+ """
155+ In case a file specified in the errorfiles map cannot be read, the
156+ provide_data method raises a HookError.
157+ """
158+ offline_dir = self.useFixture(TempDir()).path
159+ provider = HAProxyProvider(offline_dir=offline_dir)
160+
161+ self.assertRaises(HookError, provider.provide_data)
162
163=== modified file 'hooks/lib/services.py'
164--- hooks/lib/services.py 2015-04-02 16:37:14 +0000
165+++ hooks/lib/services.py 2015-04-07 13:50:51 +0000
166@@ -9,7 +9,7 @@
167 from lib.hook import Hook
168 from lib.relations.postgresql import PostgreSQLRequirer
169 from lib.relations.rabbitmq import RabbitMQRequirer, RabbitMQProvider
170-from lib.relations.haproxy import HAProxyProvider
171+from lib.relations.haproxy import HAProxyProvider, OFFLINE_FOLDER
172 from lib.relations.landscape import (
173 LandscapeLeaderContext, LandscapeRequirer, LandscapeProvider)
174 from lib.callbacks.scripts import SchemaBootstrap, LSCtl
175@@ -26,11 +26,12 @@
176 proceed with the configuration if ready.
177 """
178 def __init__(self, hookenv=hookenv, cluster=cluster, host=host,
179- subprocess=subprocess):
180+ subprocess=subprocess, offline_dir=OFFLINE_FOLDER):
181 super(ServicesHook, self).__init__(hookenv=hookenv)
182 self._cluster = cluster
183 self._host = host
184 self._subprocess = subprocess
185+ self._offline_dir = offline_dir
186
187 def _run(self):
188 leader_context = None
189@@ -43,7 +44,7 @@
190 "ports": [],
191 "provided_data": [
192 LandscapeProvider(leader_context),
193- HAProxyProvider(),
194+ HAProxyProvider(offline_dir=self._offline_dir),
195 RabbitMQProvider(),
196 ],
197 "required_data": [
198
199=== added file 'hooks/lib/tests/offline_fixture.py'
200--- hooks/lib/tests/offline_fixture.py 1970-01-01 00:00:00 +0000
201+++ hooks/lib/tests/offline_fixture.py 2015-04-07 13:50:51 +0000
202@@ -0,0 +1,21 @@
203+import os
204+from fixtures import TempDir
205+
206+from lib.relations.haproxy import ERRORFILES_MAP
207+
208+
209+class OfflineDir(TempDir):
210+ """Temporary offline dir populated with sample data."""
211+
212+ rootdir = "" # This is expected to be set
213+
214+ def __init__(self, errorfiles_map=ERRORFILES_MAP):
215+ super(OfflineDir, self)
216+ self._errorfiles_map = errorfiles_map
217+
218+ def setUp(self):
219+ super(OfflineDir, self).setUp()
220+ for filename in self._errorfiles_map.itervalues():
221+ fake_content = "Fake %s" % filename
222+ with open(os.path.join(self.path, filename), "w") as fd:
223+ fd.write(fake_content)
224
225=== modified file 'hooks/lib/tests/test_services.py'
226--- hooks/lib/tests/test_services.py 2015-04-02 17:00:37 +0000
227+++ hooks/lib/tests/test_services.py 2015-04-07 13:50:51 +0000
228@@ -6,6 +6,7 @@
229 SAMPLE_DB_UNIT_DATA, SAMPLE_LEADER_CONTEXT_DATA, SAMPLE_AMQP_UNIT_DATA,
230 SAMPLE_CONFIG_OPENID_DATA)
231 from lib.services import ServicesHook, SERVICE_CONF, DEFAULT_FILE
232+from lib.tests.offline_fixture import OfflineDir
233
234
235 class ServicesHookTest(HookenvTest):
236@@ -17,9 +18,10 @@
237 self.cluster = ClusterStub()
238 self.host = HostStub()
239 self.subprocess = SubprocessStub()
240+ self.offline_dir = self.useFixture(OfflineDir()).path
241 self.hook = ServicesHook(
242 hookenv=self.hookenv, cluster=self.cluster, host=self.host,
243- subprocess=self.subprocess)
244+ subprocess=self.subprocess, offline_dir=self.offline_dir)
245
246 # XXX Monkey patch the templating API, charmhelpers doesn't sport
247 # any dependency injection here as well.
248
249=== modified file 'tests/01-begin.py'
250--- tests/01-begin.py 2015-03-26 09:03:58 +0000
251+++ tests/01-begin.py 2015-04-07 13:50:51 +0000
252@@ -378,7 +378,6 @@
253 self.assertNotEqual(len(schema["store_password"]), 0)
254
255
256-@unittest.skip("no unavailable pages yet")
257 class LandscapeErrorPagesTests(BaseLandscapeTests):
258
259 @classmethod
260@@ -386,14 +385,8 @@
261 """Prepares juju_status and other attributes that many tests use."""
262 cls.juju_status = juju_status()
263 cls.frontend = find_address(cls.juju_status, "haproxy")
264- cls.app_unit = find_landscape_unit_with_service(
265- cls.juju_status, "appserver")
266- cls.msg_unit = find_landscape_unit_with_service(
267- cls.juju_status, "msgserver")
268- cls.ping_unit = find_landscape_unit_with_service(
269- cls.juju_status, "pingserver")
270- cls.async_unit = find_landscape_unit_with_service(
271- cls.juju_status, "async-frontend")
272+ cls.landscape_units = get_landscape_units(cls.juju_status)
273+ cls.first_unit = cls.landscape_units[0]
274
275 def run_command_on_unit(self, cmd, unit):
276 output = check_output(["juju", "ssh", unit, cmd], stderr=PIPE)
277@@ -409,46 +402,49 @@
278
279 def test_app_unavailable_page(self):
280 """
281- Verify that the frontend shows the styled unavailable page for app.
282+ Verify that the frontend shows the styled unavailable page.
283 """
284 self.addCleanup(self.start_server, "landscape-appserver",
285- self.app_unit)
286- self.stop_server("landscape-appserver", self.app_unit)
287+ self.first_unit)
288+ self.stop_server("landscape-appserver", self.first_unit)
289 good_content = "please phone us"
290 url = "https://{}/".format(self.frontend)
291 check_url(url, good_content)
292
293+ @unittest.skip
294 def test_msg_unavailable_page(self):
295 """
296 Verify that the frontend shows the unstyled unavailable page for msg.
297 """
298 self.addCleanup(self.start_server, "landscape-msgserver",
299- self.msg_unit)
300- self.stop_server("landscape-msgserver", self.msg_unit)
301+ self.first_unit)
302+ self.stop_server("landscape-msgserver", self.first_unit)
303 good_content = ["503 Service Unavailable",
304 "No server is available to handle this request."]
305 url = "https://{}/message-system".format(self.frontend)
306 check_url(url, good_content)
307
308+ @unittest.skip
309 def test_ping_unavailable_page(self):
310 """
311 Verify that the frontend shows the unstyled unavailable page for ping.
312 """
313 self.addCleanup(self.start_server, "landscape-pingserver",
314- self.ping_unit)
315- self.stop_server("landscape-pingserver", self.ping_unit)
316+ self.first_unit)
317+ self.stop_server("landscape-pingserver", self.first_unit)
318 good_content = ["503 Service Unavailable",
319 "No server is available to handle this request."]
320 url = "http://{}/ping".format(self.frontend)
321 check_url(url, good_content)
322
323+ @unittest.skip
324 def test_async_unavailable_page(self):
325 """
326 Verify that the frontend shows the unstyled unavailable page for async.
327 """
328 self.addCleanup(self.start_server, "landscape-async-frontend",
329- self.async_unit)
330- self.stop_server("landscape-async-frontend", self.async_unit)
331+ self.first_unit)
332+ self.stop_server("landscape-async-frontend", self.first_unit)
333 good_content = ["503 Service Unavailable",
334 "No server is available to handle this request."]
335 url = "https://{}/ajax".format(self.frontend)

Subscribers

People subscribed via source and target branches