Merge lp:~tribaal/landscape-charm/send-haproxy-error-in-relation into lp:~landscape/landscape-charm/trunk
- send-haproxy-error-in-relation
- Merge into trunk
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 | ||||
Related bugs: |
|
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:/
🤖 Landscape Builder (landscape-builder) wrote : | # |
Free Ekanayaka (free.ekanayaka) wrote : | # |
Marking as needs information because I'd like to have your opinion on a testing approach based on dependency-
Данило Шеган (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.
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.
🤖 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:/
Данило Шеган (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.
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 :)
Данило Шеган (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 :)
Chris Glass (tribaal) wrote : | # |
Should be good for another round of comments.
🤖 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:/
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).
Free Ekanayaka (free.ekanayaka) : | # |
Chris Glass (tribaal) : | # |
- 246. By Chris Glass
-
First review comments addressed.
- 247. By Chris Glass
-
Removed leftover comments. Fixed tests.
🤖 Landscape Builder (landscape-builder) wrote : | # |
Command: make ci-test
Result: Fail
Revno: 247
Branch: lp:~tribaal/landscape-charm/send-haproxy-error-in-relation
Jenkins: https:/
-------
./dev/ubuntu-deps
Reading package lists...
Building dependency tree...
Reading state information...
software-
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_
gpg: keyring `/tmp/tmpubm_
gpg: requesting key C8068B11 from hkp server keyserver.
gpg: /tmp/tmpubm_
gpg: key C8068B11: public key "Launchpad Ensemble PPA" imported
gpg: Total number processed: 1
gpg: imported: 1 (RSA: 1)
OK
Ign http://
Get:1 http://
Get:2 http://
Ign http://
Ign http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:3 http://
Hit http://
Ign https:/
Ign https:/
Get:4 http://
Get:5 http://
Get:6 http://
Hit http://
Hit http://
Ign http://
Hit http://
Hit https:/
Hit https:/
Hit http://
Hit https:/
Hit http://
Hit https:/
Hit https:/
Hit https:/
Hit https:/
Ign http://
Ign https:/
Ign https:/
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
- 248. By Chris Glass
-
Composition over inheritance change.
Chris Glass (tribaal) wrote : | # |
All comments should be addressed now.
🤖 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:/
Данило Шеган (danilo) wrote : | # |
FWIW, still looks good to me :)
Free Ekanayaka (free.ekanayaka) wrote : | # |
Thanks for addressing everything. A few more little points, +1 with those addressed.
- 249. By Chris Glass
-
Fixed a couple of review points.
- 250. By Chris Glass
-
All comments addressed.
- 251. By Chris Glass
-
lint
Preview Diff
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) |
Command: make ci-test /ci.lscape. net/job/ latch-test/ 364/
Result: Success
Revno: 241
Branch: lp:~tribaal/landscape-charm/send-haproxy-error-in-relation
Jenkins: https:/