Merge ~cjwatson/launchpad-layers:db-layer into launchpad-layers:main
- Git
- lp:~cjwatson/launchpad-layers
- db-layer
- Merge into main
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merge reported by: | Colin Watson |
Merged at revision: | 42a4b4c4f62936b1d050c775e84f7364dfb5efc0 |
Proposed branch: | ~cjwatson/launchpad-layers:db-layer |
Merge into: | launchpad-layers:main |
Prerequisite: | ~cjwatson/launchpad-layers:payload-layer |
Diff against target: |
557 lines (+219/-170) 14 files modified
launchpad-base/config.yaml (+0/-11) launchpad-base/layer.yaml (+0/-1) launchpad-base/lib/charms/launchpad/base.py (+0/-110) launchpad-base/metadata.yaml (+0/-2) launchpad-base/reactive/launchpad-base.py (+3/-34) launchpad-base/templates/launchpad-base-lazr.conf (+0/-4) launchpad-db/config.yaml (+12/-0) launchpad-db/layer.yaml (+4/-0) launchpad-db/lib/charms/launchpad/db.py (+121/-0) launchpad-db/metadata.yaml (+3/-0) launchpad-db/reactive/launchpad-db.py (+57/-0) launchpad-db/templates/launchpad-db-lazr.conf (+19/-0) launchpad-payload/lib/charms/launchpad/payload.py (+0/-4) launchpad-payload/reactive/launchpad-payload.py (+0/-4) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Clinton Fung | Approve | ||
Guruprasad | Approve | ||
Review via email: mp+442155@code.launchpad.net |
Commit message
Split out a launchpad-db layer
Description of the change
`launchpad-
This will require applications to change: they will need to import some functions from `charms.
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/launchpad-base/config.yaml b/launchpad-base/config.yaml |
2 | index b804392..3263531 100644 |
3 | --- a/launchpad-base/config.yaml |
4 | +++ b/launchpad-base/config.yaml |
5 | @@ -43,17 +43,6 @@ options: |
6 | type: string |
7 | description: URL of file used to control whether cron scripts run. |
8 | default: "file:cronscripts.ini" |
9 | - databases: |
10 | - type: string |
11 | - description: > |
12 | - YAML-encoded information about database relations, overriding the |
13 | - defaults. For example, setting this to "db: {name: launchpad_prod}" |
14 | - changes the name of the database used by the "db" relation. |
15 | - default: "" |
16 | - db_statement_timeout: |
17 | - type: int |
18 | - description: SQL statement timeout in milliseconds. |
19 | - default: |
20 | default_batch_size: |
21 | type: int |
22 | description: The default size used in a batched listing of results. |
23 | diff --git a/launchpad-base/layer.yaml b/launchpad-base/layer.yaml |
24 | index 5abc6ac..ed3e8dc 100644 |
25 | --- a/launchpad-base/layer.yaml |
26 | +++ b/launchpad-base/layer.yaml |
27 | @@ -1,6 +1,5 @@ |
28 | includes: |
29 | - layer:basic |
30 | - - layer:ols-pg |
31 | - interface:rabbitmq |
32 | - layer:launchpad-payload |
33 | repo: https://git.launchpad.net/launchpad-layers |
34 | diff --git a/launchpad-base/lib/charms/launchpad/base.py b/launchpad-base/lib/charms/launchpad/base.py |
35 | index 2d441de..043cd4f 100644 |
36 | --- a/launchpad-base/lib/charms/launchpad/base.py |
37 | +++ b/launchpad-base/lib/charms/launchpad/base.py |
38 | @@ -1,22 +1,16 @@ |
39 | # Copyright 2022 Canonical Ltd. This software is licensed under the |
40 | # GNU Affero General Public License version 3 (see the file LICENSE). |
41 | |
42 | -import grp |
43 | import os.path |
44 | -import pwd |
45 | -import re |
46 | import subprocess |
47 | -from dataclasses import dataclass |
48 | from email.utils import parseaddr |
49 | from urllib.parse import urlparse |
50 | |
51 | from charmhelpers.core import hookenv, host, templating |
52 | from charms.launchpad.payload import config_file_path |
53 | from charms.launchpad.payload import get_service_config as get_payload_config |
54 | -from charms.launchpad.payload import home_dir |
55 | from charms.reactive import endpoint_from_flag |
56 | from ols import base |
57 | -from psycopg2.extensions import make_dsn, parse_dsn |
58 | |
59 | |
60 | def oopses_dir(): |
61 | @@ -136,107 +130,3 @@ def configure_rsync(config, template, name): |
62 | templating.render(template, rsync_path, config, perms=0o644) |
63 | elif os.path.exists(rsync_path): |
64 | os.unlink(rsync_path) |
65 | - |
66 | - |
67 | -@dataclass |
68 | -class PgPassLine: |
69 | - hostname: str |
70 | - port: str |
71 | - database: str |
72 | - username: str |
73 | - password: str |
74 | - |
75 | - |
76 | -def update_pgpass(dsn): |
77 | - # See https://www.postgresql.org/docs/current/libpq-pgpass.html. |
78 | - |
79 | - def unescape(entry): |
80 | - return re.sub(r"\\(.)", r"\1", entry) |
81 | - |
82 | - def escape(entry): |
83 | - return re.sub(r"([:\\])", r"\\\1", entry) |
84 | - |
85 | - parsed_dsn = parse_dsn(dsn) |
86 | - pgpass_path = os.path.join(home_dir(), ".pgpass") |
87 | - pgpass = [] |
88 | - try: |
89 | - with open(pgpass_path) as f: |
90 | - for line in f: |
91 | - if line.startswith("#"): |
92 | - continue |
93 | - match = re.match( |
94 | - r""" |
95 | - ^ |
96 | - (?P<hostname>(?:[^:\\]|\\.)*): |
97 | - (?P<port>(?:[^:\\]|\\.)*): |
98 | - (?P<database>(?:[^:\\]|\\.)*): |
99 | - (?P<username>(?:[^:\\]|\\.)*): |
100 | - (?P<password>(?:[^:\\]|\\.)*) |
101 | - $ |
102 | - """, |
103 | - line.rstrip("\n"), |
104 | - flags=re.X, |
105 | - ) |
106 | - if match is None: |
107 | - continue |
108 | - pgpass.append( |
109 | - PgPassLine( |
110 | - hostname=unescape(match.group("hostname")), |
111 | - port=unescape(match.group("port")), |
112 | - database=unescape(match.group("database")), |
113 | - username=unescape(match.group("username")), |
114 | - password=unescape(match.group("password")), |
115 | - ) |
116 | - ) |
117 | - except OSError: |
118 | - pass |
119 | - |
120 | - modified = False |
121 | - for line in pgpass: |
122 | - if ( |
123 | - line.hostname in ("*", parsed_dsn["host"]) |
124 | - and line.port in ("*", parsed_dsn["port"]) |
125 | - and line.database in ("*", parsed_dsn["dbname"]) |
126 | - and line.username in ("*", parsed_dsn["user"]) |
127 | - ): |
128 | - if line.password != parsed_dsn["password"]: |
129 | - line.password = parsed_dsn["password"] |
130 | - modified = True |
131 | - break |
132 | - else: |
133 | - pgpass.append( |
134 | - PgPassLine( |
135 | - hostname=parsed_dsn["host"], |
136 | - port=parsed_dsn["port"], |
137 | - database=parsed_dsn["dbname"], |
138 | - username=parsed_dsn["user"], |
139 | - password=parsed_dsn["password"], |
140 | - ) |
141 | - ) |
142 | - modified = True |
143 | - |
144 | - if modified: |
145 | - uid = pwd.getpwnam(base.user()).pw_uid |
146 | - gid = grp.getgrnam(base.user()).gr_gid |
147 | - with open(pgpass_path, "w") as f: |
148 | - for line in pgpass: |
149 | - print( |
150 | - ":".join( |
151 | - [ |
152 | - escape(line.hostname), |
153 | - escape(line.port), |
154 | - escape(line.database), |
155 | - escape(line.username), |
156 | - escape(line.password), |
157 | - ] |
158 | - ), |
159 | - file=f, |
160 | - ) |
161 | - os.fchown(f.fileno(), uid, gid) |
162 | - os.fchmod(f.fileno(), 0o600) |
163 | - |
164 | - |
165 | -def strip_dsn_authentication(dsn): |
166 | - parsed_dsn = parse_dsn(dsn) |
167 | - parsed_dsn.pop("password", None) |
168 | - return make_dsn(**parsed_dsn) |
169 | diff --git a/launchpad-base/metadata.yaml b/launchpad-base/metadata.yaml |
170 | index eafdcc0..f2480ad 100644 |
171 | --- a/launchpad-base/metadata.yaml |
172 | +++ b/launchpad-base/metadata.yaml |
173 | @@ -1,5 +1,3 @@ |
174 | requires: |
175 | - db: |
176 | - interface: pgsql |
177 | rabbitmq: |
178 | interface: rabbitmq |
179 | diff --git a/launchpad-base/reactive/launchpad-base.py b/launchpad-base/reactive/launchpad-base.py |
180 | index 186e7df..d8e3376 100644 |
181 | --- a/launchpad-base/reactive/launchpad-base.py |
182 | +++ b/launchpad-base/reactive/launchpad-base.py |
183 | @@ -7,8 +7,6 @@ from charms.launchpad.base import ( |
184 | configure_rsync, |
185 | ensure_lp_directories, |
186 | get_service_config, |
187 | - strip_dsn_authentication, |
188 | - update_pgpass, |
189 | ) |
190 | from charms.launchpad.payload import configure_lazr |
191 | from charms.reactive import ( |
192 | @@ -22,7 +20,7 @@ from charms.reactive import ( |
193 | when_not, |
194 | when_not_all, |
195 | ) |
196 | -from ols import base, postgres |
197 | +from ols import base |
198 | |
199 | |
200 | @when("rabbitmq.connected") |
201 | @@ -59,30 +57,15 @@ def rabbitmq_unavailable(): |
202 | clear_flag("launchpad.rabbitmq.available") |
203 | |
204 | |
205 | -@when( |
206 | - "launchpad.payload.configured", |
207 | - "db.master.available", |
208 | - "launchpad.rabbitmq.available", |
209 | -) |
210 | +@when("launchpad.payload.configured", "launchpad.rabbitmq.available") |
211 | @when_not("launchpad.base.configured") |
212 | def configure(): |
213 | - db = endpoint_from_flag("db.master.available") |
214 | rabbitmq = endpoint_from_flag("rabbitmq.available") |
215 | # Interactive use shouldn't be frequent, but it's still needed |
216 | # sometimes, so make it less annoying. |
217 | change_shell(base.user(), "/bin/bash") |
218 | ensure_lp_directories() |
219 | config = get_service_config() |
220 | - db_primary, db_standby = postgres.get_db_uris(db) |
221 | - # XXX cjwatson 2022-09-23: Mangle the connection strings into forms |
222 | - # Launchpad understands. In the long term it would be better to have |
223 | - # Launchpad be able to consume unmodified connection strings. |
224 | - for dsn in [db_primary] + db_standby: |
225 | - update_pgpass(dsn) |
226 | - config["db_primary"] = strip_dsn_authentication(db_primary) |
227 | - config["db_standby"] = ",".join( |
228 | - strip_dsn_authentication(dsn) for dsn in db_standby |
229 | - ) |
230 | config["rabbitmq_broker_urls"] = sorted(get_rabbitmq_uris(rabbitmq)) |
231 | configure_lazr( |
232 | config, |
233 | @@ -102,30 +85,16 @@ def configure(): |
234 | |
235 | |
236 | @when("launchpad.base.configured") |
237 | -@when_not_all( |
238 | - "launchpad.payload.configured", |
239 | - "db.master.available", |
240 | - "launchpad.rabbitmq.available", |
241 | -) |
242 | +@when_not_all("launchpad.payload.configured", "launchpad.rabbitmq.available") |
243 | def deconfigure(): |
244 | clear_flag("launchpad.base.configured") |
245 | - clear_flag("service.configured") |
246 | |
247 | |
248 | @when("config.changed") |
249 | def config_changed(): |
250 | clear_flag("launchpad.base.configured") |
251 | - clear_flag("service.configured") |
252 | - |
253 | - |
254 | -@when("db.database.changed", "launchpad.base.configured") |
255 | -def db_changed(): |
256 | - clear_flag("launchpad.base.configured") |
257 | - clear_flag("service.configured") |
258 | - clear_flag("db.database.changed") |
259 | |
260 | |
261 | @hook("{requires:rabbitmq}-relation-changed") |
262 | def rabbitmq_relation_changed(*args): |
263 | clear_flag("launchpad.base.configured") |
264 | - clear_flag("service.configured") |
265 | diff --git a/launchpad-base/templates/launchpad-base-lazr.conf b/launchpad-base/templates/launchpad-base-lazr.conf |
266 | index b9707cb..a73f081 100644 |
267 | --- a/launchpad-base/templates/launchpad-base-lazr.conf |
268 | +++ b/launchpad-base/templates/launchpad-base-lazr.conf |
269 | @@ -44,10 +44,6 @@ git_ssh_root: git+ssh://{{ domain_git }}/ |
270 | {%- endif %} |
271 | |
272 | [database] |
273 | -{{- opt("db_statement_timeout", db_statement_timeout) }} |
274 | -rw_main_primary: {{ db_primary }} |
275 | -rw_main_standby: {{ db_standby or db_primary }} |
276 | -set_role_after_connecting: True |
277 | {{- opt("soft_request_timeout", soft_request_timeout) }} |
278 | |
279 | [error_reports] |
280 | diff --git a/launchpad-db/config.yaml b/launchpad-db/config.yaml |
281 | new file mode 100644 |
282 | index 0000000..cd088e2 |
283 | --- /dev/null |
284 | +++ b/launchpad-db/config.yaml |
285 | @@ -0,0 +1,12 @@ |
286 | +options: |
287 | + databases: |
288 | + type: string |
289 | + description: > |
290 | + YAML-encoded information about database relations, overriding the |
291 | + defaults. For example, setting this to "db: {name: launchpad_prod}" |
292 | + changes the name of the database used by the "db" relation. |
293 | + default: "" |
294 | + db_statement_timeout: |
295 | + type: int |
296 | + description: SQL statement timeout in milliseconds. |
297 | + default: |
298 | diff --git a/launchpad-db/layer.yaml b/launchpad-db/layer.yaml |
299 | new file mode 100644 |
300 | index 0000000..3ea5d6a |
301 | --- /dev/null |
302 | +++ b/launchpad-db/layer.yaml |
303 | @@ -0,0 +1,4 @@ |
304 | +includes: |
305 | + - layer:launchpad-base |
306 | + - layer:ols-pg |
307 | +repo: https://git.launchpad.net/launchpad-layers |
308 | diff --git a/launchpad-db/lib/charms/launchpad/db.py b/launchpad-db/lib/charms/launchpad/db.py |
309 | new file mode 100644 |
310 | index 0000000..e1d1520 |
311 | --- /dev/null |
312 | +++ b/launchpad-db/lib/charms/launchpad/db.py |
313 | @@ -0,0 +1,121 @@ |
314 | +# Copyright 2022 Canonical Ltd. This software is licensed under the |
315 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
316 | + |
317 | +import grp |
318 | +import os.path |
319 | +import pwd |
320 | +import re |
321 | +from dataclasses import dataclass |
322 | + |
323 | +from charms.launchpad.base import lazr_config_files as base_config_files |
324 | +from charms.launchpad.payload import config_file_path, home_dir |
325 | +from ols import base |
326 | +from psycopg2.extensions import make_dsn, parse_dsn |
327 | + |
328 | + |
329 | +def lazr_config_files(): |
330 | + return base_config_files() + [config_file_path("launchpad-db-lazr.conf")] |
331 | + |
332 | + |
333 | +@dataclass |
334 | +class PgPassLine: |
335 | + hostname: str |
336 | + port: str |
337 | + database: str |
338 | + username: str |
339 | + password: str |
340 | + |
341 | + |
342 | +def update_pgpass(dsn): |
343 | + # See https://www.postgresql.org/docs/current/libpq-pgpass.html. |
344 | + |
345 | + def unescape(entry): |
346 | + return re.sub(r"\\(.)", r"\1", entry) |
347 | + |
348 | + def escape(entry): |
349 | + return re.sub(r"([:\\])", r"\\\1", entry) |
350 | + |
351 | + parsed_dsn = parse_dsn(dsn) |
352 | + pgpass_path = os.path.join(home_dir(), ".pgpass") |
353 | + pgpass = [] |
354 | + try: |
355 | + with open(pgpass_path) as f: |
356 | + for line in f: |
357 | + if line.startswith("#"): |
358 | + continue |
359 | + match = re.match( |
360 | + r""" |
361 | + ^ |
362 | + (?P<hostname>(?:[^:\\]|\\.)*): |
363 | + (?P<port>(?:[^:\\]|\\.)*): |
364 | + (?P<database>(?:[^:\\]|\\.)*): |
365 | + (?P<username>(?:[^:\\]|\\.)*): |
366 | + (?P<password>(?:[^:\\]|\\.)*) |
367 | + $ |
368 | + """, |
369 | + line.rstrip("\n"), |
370 | + flags=re.X, |
371 | + ) |
372 | + if match is None: |
373 | + continue |
374 | + pgpass.append( |
375 | + PgPassLine( |
376 | + hostname=unescape(match.group("hostname")), |
377 | + port=unescape(match.group("port")), |
378 | + database=unescape(match.group("database")), |
379 | + username=unescape(match.group("username")), |
380 | + password=unescape(match.group("password")), |
381 | + ) |
382 | + ) |
383 | + except OSError: |
384 | + pass |
385 | + |
386 | + modified = False |
387 | + for line in pgpass: |
388 | + if ( |
389 | + line.hostname in ("*", parsed_dsn["host"]) |
390 | + and line.port in ("*", parsed_dsn["port"]) |
391 | + and line.database in ("*", parsed_dsn["dbname"]) |
392 | + and line.username in ("*", parsed_dsn["user"]) |
393 | + ): |
394 | + if line.password != parsed_dsn["password"]: |
395 | + line.password = parsed_dsn["password"] |
396 | + modified = True |
397 | + break |
398 | + else: |
399 | + pgpass.append( |
400 | + PgPassLine( |
401 | + hostname=parsed_dsn["host"], |
402 | + port=parsed_dsn["port"], |
403 | + database=parsed_dsn["dbname"], |
404 | + username=parsed_dsn["user"], |
405 | + password=parsed_dsn["password"], |
406 | + ) |
407 | + ) |
408 | + modified = True |
409 | + |
410 | + if modified: |
411 | + uid = pwd.getpwnam(base.user()).pw_uid |
412 | + gid = grp.getgrnam(base.user()).gr_gid |
413 | + with open(pgpass_path, "w") as f: |
414 | + for line in pgpass: |
415 | + print( |
416 | + ":".join( |
417 | + [ |
418 | + escape(line.hostname), |
419 | + escape(line.port), |
420 | + escape(line.database), |
421 | + escape(line.username), |
422 | + escape(line.password), |
423 | + ] |
424 | + ), |
425 | + file=f, |
426 | + ) |
427 | + os.fchown(f.fileno(), uid, gid) |
428 | + os.fchmod(f.fileno(), 0o600) |
429 | + |
430 | + |
431 | +def strip_dsn_authentication(dsn): |
432 | + parsed_dsn = parse_dsn(dsn) |
433 | + parsed_dsn.pop("password", None) |
434 | + return make_dsn(**parsed_dsn) |
435 | diff --git a/launchpad-db/metadata.yaml b/launchpad-db/metadata.yaml |
436 | new file mode 100644 |
437 | index 0000000..6de4a93 |
438 | --- /dev/null |
439 | +++ b/launchpad-db/metadata.yaml |
440 | @@ -0,0 +1,3 @@ |
441 | +requires: |
442 | + db: |
443 | + interface: pgsql |
444 | diff --git a/launchpad-db/reactive/launchpad-db.py b/launchpad-db/reactive/launchpad-db.py |
445 | new file mode 100644 |
446 | index 0000000..4d8c7a8 |
447 | --- /dev/null |
448 | +++ b/launchpad-db/reactive/launchpad-db.py |
449 | @@ -0,0 +1,57 @@ |
450 | +# Copyright 2022 Canonical Ltd. This software is licensed under the |
451 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
452 | + |
453 | +from charms.launchpad.base import get_service_config |
454 | +from charms.launchpad.db import strip_dsn_authentication, update_pgpass |
455 | +from charms.launchpad.payload import configure_lazr |
456 | +from charms.reactive import ( |
457 | + clear_flag, |
458 | + endpoint_from_flag, |
459 | + hook, |
460 | + set_flag, |
461 | + when, |
462 | + when_not, |
463 | + when_not_all, |
464 | +) |
465 | +from ols import postgres |
466 | + |
467 | + |
468 | +@when("launchpad.base.configured", "db.master.available") |
469 | +@when_not("launchpad.db.configured") |
470 | +def configure(): |
471 | + db = endpoint_from_flag("db.master.available") |
472 | + config = get_service_config() |
473 | + db_primary, db_standby = postgres.get_db_uris(db) |
474 | + # XXX cjwatson 2022-09-23: Mangle the connection strings into forms |
475 | + # Launchpad understands. In the long term it would be better to have |
476 | + # Launchpad be able to consume unmodified connection strings. |
477 | + for dsn in [db_primary] + db_standby: |
478 | + update_pgpass(dsn) |
479 | + config["db_primary"] = strip_dsn_authentication(db_primary) |
480 | + config["db_standby"] = ",".join( |
481 | + strip_dsn_authentication(dsn) for dsn in db_standby |
482 | + ) |
483 | + configure_lazr(config, "launchpad-db-lazr.conf", "launchpad-db-lazr.conf") |
484 | + set_flag("launchpad.db.configured") |
485 | + |
486 | + |
487 | +@when("launchpad.db.configured") |
488 | +@when_not_all("launchpad.base.configured", "db.master.available") |
489 | +def deconfigure(): |
490 | + clear_flag("launchpad.db.configured") |
491 | + |
492 | + |
493 | +@when("config.changed") |
494 | +def config_changed(): |
495 | + clear_flag("launchpad.db.configured") |
496 | + |
497 | + |
498 | +@when("db.database.changed", "launchpad.db.configured") |
499 | +def db_changed(): |
500 | + clear_flag("launchpad.db.configured") |
501 | + clear_flag("db.database.changed") |
502 | + |
503 | + |
504 | +@hook("{requires:rabbitmq}-relation-changed") |
505 | +def rabbitmq_relation_changed(*args): |
506 | + clear_flag("launchpad.db.configured") |
507 | diff --git a/launchpad-db/templates/launchpad-db-lazr.conf b/launchpad-db/templates/launchpad-db-lazr.conf |
508 | new file mode 100644 |
509 | index 0000000..e6f4f80 |
510 | --- /dev/null |
511 | +++ b/launchpad-db/templates/launchpad-db-lazr.conf |
512 | @@ -0,0 +1,19 @@ |
513 | +# Public configuration data. The contents of this file may be freely shared |
514 | +# with developers if needed for debugging. |
515 | + |
516 | +# A schema's sections, keys, and values are automatically inherited, except |
517 | +# for '.optional' sections. Update this config to override key values. |
518 | +# Values are strings, except for numbers that look like ints. The tokens |
519 | +# true, false, and none are treated as True, False, and None. |
520 | + |
521 | +{% from "macros.j2" import opt -%} |
522 | + |
523 | +[meta] |
524 | +extends: launchpad-base-lazr.conf |
525 | + |
526 | +[database] |
527 | +{{- opt("db_statement_timeout", db_statement_timeout) }} |
528 | +rw_main_primary: {{ db_primary }} |
529 | +rw_main_standby: {{ db_standby or db_primary }} |
530 | +set_role_after_connecting: True |
531 | + |
532 | diff --git a/launchpad-payload/lib/charms/launchpad/payload.py b/launchpad-payload/lib/charms/launchpad/payload.py |
533 | index a633ca4..d6669f8 100644 |
534 | --- a/launchpad-payload/lib/charms/launchpad/payload.py |
535 | +++ b/launchpad-payload/lib/charms/launchpad/payload.py |
536 | @@ -1,8 +1,4 @@ |
537 | -<<<<<<< launchpad-payload/lib/charms/launchpad/payload.py |
538 | # Copyright 2022 Canonical Ltd. This software is licensed under the |
539 | -======= |
540 | -# Copyright 2022-2023 Canonical Ltd. This software is licensed under the |
541 | ->>>>>>> launchpad-payload/lib/charms/launchpad/payload.py |
542 | # GNU Affero General Public License version 3 (see the file LICENSE). |
543 | |
544 | import os.path |
545 | diff --git a/launchpad-payload/reactive/launchpad-payload.py b/launchpad-payload/reactive/launchpad-payload.py |
546 | index 479aeab..13cbd2d 100644 |
547 | --- a/launchpad-payload/reactive/launchpad-payload.py |
548 | +++ b/launchpad-payload/reactive/launchpad-payload.py |
549 | @@ -1,8 +1,4 @@ |
550 | -<<<<<<< launchpad-payload/reactive/launchpad-payload.py |
551 | # Copyright 2022 Canonical Ltd. This software is licensed under the |
552 | -======= |
553 | -# Copyright 2022-2023 Canonical Ltd. This software is licensed under the |
554 | ->>>>>>> launchpad-payload/reactive/launchpad-payload.py |
555 | # GNU Affero General Public License version 3 (see the file LICENSE). |
556 | |
557 | import subprocess |
LGTM 👍🏼