Skip to content

Commit bc6b655

Browse files
committedJul 5, 2017
Add pingback support (bug 1377305)
Added POST API endpoint `/landings/{landing_id}/update`. `landing_id` (from path) and `request_id` (from data) have to correspond with database entry, otherwise 404 is raised. Modified the schema by adding error and result.
1 parent 233c414 commit bc6b655

File tree

8 files changed

+270
-64
lines changed

8 files changed

+270
-64
lines changed
 

‎docker-compose.yml

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ services:
1717
- PHABRICATOR_UNPRIVILEGED_API_KEY=api-123456789
1818
- TRANSPLANT_URL=https://stub.transplant.example.com
1919
- DATABASE_URL=sqlite:////db/sqlite.db
20+
- HOST_URL=https://lando-api.test
2021
volumes:
2122
- ./:/app
2223
- ./.db/:/db/

‎landoapi/api/landings.py

+48-7
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
"""
88
from connexion import problem
99
from flask import request
10+
from sqlalchemy.orm.exc import NoResultFound
1011
from landoapi.models.landing import (
11-
Landing,
12-
LandingNotCreatedException,
13-
LandingNotFoundException,
14-
RevisionNotFoundException,
12+
Landing, LandingNotCreatedException, RevisionNotFoundException,
13+
TRANSPLANT_JOB_FAILED, TRANSPLANT_JOB_LANDED
1514
)
1615

1716

@@ -60,9 +59,8 @@ def get_list(revision_id=None, status=None):
6059
def get(landing_id):
6160
""" API endpoint at /landings/{landing_id} to return stored Landing.
6261
"""
63-
try:
64-
landing = Landing.get(landing_id)
65-
except LandingNotFoundException:
62+
landing = Landing.query.get(landing_id)
63+
if not landing:
6664
return problem(
6765
404,
6866
'Landing not found',
@@ -71,3 +69,46 @@ def get(landing_id):
7169
)
7270

7371
return landing.serialize(), 200
72+
73+
74+
def update(landing_id, data):
75+
"""Update landing on pingback from Transplant.
76+
77+
data contains following fields:
78+
request_id: integer
79+
id of the landing request in Transplant
80+
tree: string
81+
tree name as per treestatus
82+
rev: string
83+
matching phabricator revision identifier
84+
destination: string
85+
full url of destination repo
86+
trysyntax: string
87+
change will be pushed to try or empty string
88+
landed: boolean;
89+
true when operation was successful
90+
error_msg: string
91+
error message if landed == false
92+
empty string if landed == true
93+
result: string
94+
revision (sha) of push if landed == true
95+
empty string if landed == false
96+
"""
97+
try:
98+
landing = Landing.query.filter_by(
99+
id=landing_id, request_id=data['request_id']
100+
).one()
101+
except NoResultFound:
102+
return problem(
103+
404,
104+
'Landing not found',
105+
'The requested Landing does not exist',
106+
type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404'
107+
)
108+
109+
landing.error = data.get('error_msg', '')
110+
landing.result = data.get('result', '')
111+
landing.status = TRANSPLANT_JOB_LANDED if data['landed'
112+
] else TRANSPLANT_JOB_FAILED
113+
landing.save()
114+
return {}, 202

‎landoapi/models/landing.py

+29-28
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
# This Source Code Form is subject to the terms of the Mozilla Public
22
# License, v. 2.0. If a copy of the MPL was not distributed with this
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
import os
5+
46
from landoapi.hgexportbuilder import build_patch_for_revision
57
from landoapi.models.storage import db
68
from landoapi.phabricator_client import PhabricatorClient
79
from landoapi.transplant_client import TransplantClient
810

11+
TRANSPLANT_JOB_STARTING = 'pending'
912
TRANSPLANT_JOB_STARTED = 'started'
10-
TRANSPLANT_JOB_FINISHED = 'finished'
13+
TRANSPLANT_JOB_LANDED = 'landed'
14+
TRANSPLANT_JOB_FAILED = 'failed'
1115

1216

1317
class Landing(db.Model):
@@ -17,16 +21,21 @@ class Landing(db.Model):
1721
request_id = db.Column(db.Integer, unique=True)
1822
revision_id = db.Column(db.String(30))
1923
status = db.Column(db.Integer)
24+
error = db.Column(db.String(128), default='')
25+
result = db.Column(db.String(128))
2026

2127
def __init__(
22-
self, request_id=None, revision_id=None, status=TRANSPLANT_JOB_STARTED
28+
self,
29+
request_id=None,
30+
revision_id=None,
31+
status=TRANSPLANT_JOB_STARTING
2332
):
2433
self.request_id = request_id
2534
self.revision_id = revision_id
2635
self.status = status
2736

2837
@classmethod
29-
def create(cls, revision_id, phabricator_api_key=None, save=True):
38+
def create(cls, revision_id, phabricator_api_key=None):
3039
""" Land revision and create a Transplant item in storage. """
3140
phab = PhabricatorClient(phabricator_api_key)
3241
revision = phab.get_revision(id=revision_id)
@@ -40,39 +49,34 @@ def create(cls, revision_id, phabricator_api_key=None, save=True):
4049

4150
repo = phab.get_revision_repo(revision)
4251

52+
# save landing to make sure we've got the callback
53+
landing = cls(
54+
revision_id=revision_id,
55+
).save()
56+
4357
trans = TransplantClient()
58+
callback = '%s/landings/%s/update' % (
59+
os.getenv('HOST_URL'), landing.id
60+
)
4461
# The LDAP username used here has to be the username of the patch
4562
# pusher.
4663
# FIXME: what value do we use here?
4764
# FIXME: This client, or the person who pushed the 'Land it!' button?
4865
request_id = trans.land(
49-
'ldap_username@example.com', hgpatch, repo['uri']
66+
'ldap_username@example.com', hgpatch, repo['uri'], callback
5067
)
5168
if not request_id:
5269
raise LandingNotCreatedException
5370

54-
landing = cls(
55-
revision_id=revision_id,
56-
request_id=request_id,
57-
status=TRANSPLANT_JOB_STARTED
58-
)
59-
if save:
60-
landing.save(create=True)
71+
landing.request_id = request_id
72+
landing.status = TRANSPLANT_JOB_STARTED
73+
landing.save()
6174

6275
return landing
6376

64-
@classmethod
65-
def get(cls, landing_id):
66-
""" Get Landing object from storage. """
67-
landing = cls.query.get(landing_id)
68-
if not landing:
69-
raise LandingNotFoundException()
70-
71-
return landing
72-
73-
def save(self, create=False):
77+
def save(self):
7478
""" Save objects in storage. """
75-
if create:
79+
if not self.id:
7680
db.session.add(self)
7781

7882
db.session.commit()
@@ -87,7 +91,9 @@ def serialize(self):
8791
'id': self.id,
8892
'revision_id': self.revision_id,
8993
'request_id': self.request_id,
90-
'status': self.status
94+
'status': self.status,
95+
'error_msg': self.error,
96+
'result': self.result
9197
}
9298

9399

@@ -96,11 +102,6 @@ class LandingNotCreatedException(Exception):
96102
pass
97103

98104

99-
class LandingNotFoundException(Exception):
100-
""" No specific Landing was found in database. """
101-
pass
102-
103-
104105
class RevisionNotFoundException(Exception):
105106
""" Phabricator returned 404 for a given revision id. """
106107

‎landoapi/spec/swagger.yml

+69
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ paths:
111111
responses:
112112
202:
113113
description: OK
114+
schema:
115+
type: object
116+
properties:
117+
id:
118+
type: integer
119+
description: |
120+
A newly created Landing id
114121
404:
115122
description: Revision does not exist
116123
schema:
@@ -121,6 +128,60 @@ paths:
121128
schema:
122129
allOf:
123130
- $ref: '#/definitions/Error'
131+
/landings/{landing_id}/update:
132+
post:
133+
operationId: landoapi.api.landings.update
134+
description: |
135+
Sends request to transplant service and responds with just the
136+
status code
137+
parameters:
138+
- name: data
139+
in: body
140+
description: |
141+
Retrieve status of the landing job
142+
required: true
143+
schema:
144+
type: object
145+
required:
146+
- request_id
147+
properties:
148+
request_id:
149+
type: integer
150+
tree:
151+
type: string
152+
rev:
153+
type: string
154+
patch:
155+
type: string
156+
destination:
157+
type: string
158+
trysyntax:
159+
type: string
160+
landed:
161+
type: boolean
162+
error_msg:
163+
type: string
164+
result:
165+
type: string
166+
- name: landing_id
167+
in: path
168+
type: string
169+
description: |
170+
The id of the landing to return
171+
required: true
172+
responses:
173+
202:
174+
description: OK
175+
404:
176+
description: Landing does not exist
177+
schema:
178+
allOf:
179+
- $ref: '#/definitions/Error'
180+
default:
181+
description: Unexpected error
182+
schema:
183+
allOf:
184+
- $ref: '#/definitions/Error'
124185
/landings/{landing_id}:
125186
get:
126187
description: |
@@ -168,6 +229,14 @@ definitions:
168229
type: integer
169230
description: |
170231
The id of the Revision
232+
result:
233+
type: string
234+
description: |
235+
revision (sha) of push if landed == true
236+
error:
237+
type: string
238+
description: |
239+
Error message if landing failed
171240
Revision:
172241
type: object
173242
properties:

‎landoapi/transplant_client.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ def __init__(self):
1717
self.api_url = os.getenv('TRANSPLANT_URL')
1818

1919
@requests_mock.mock()
20-
def land(self, ldap_username, hgpatch, tree, request):
20+
def land(self, ldap_username, hgpatch, tree, pingback, request):
2121
""" Sends a push request to Transplant API to land a revision.
2222
2323
Returns request_id received from Transplant API.
2424
"""
2525
# get the number of Landing objects to create the unique request_id
2626
sql = text('SELECT COUNT(*) FROM landings')
2727
result = db.session.execute(sql).fetchone()
28-
request_id = result[0] + 1
28+
request_id = result[0]
2929

3030
# Connect to stubbed Transplant service
3131
request.post(
@@ -44,7 +44,7 @@ def land(self, ldap_username, hgpatch, tree, request):
4444
'destination': 'destination',
4545
'push_bookmark': 'push_bookmark',
4646
'commit_descriptions': 'commit_descriptions',
47-
'pingback_url': 'http://pingback.url/'
47+
'pingback_url': pingback
4848
}
4949
)
5050

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Add error message and result to Landing
2+
3+
Revision ID: 67151bb74080
4+
Revises: ae9dd729e66c
5+
Create Date: 2017-06-29 22:16:56.874034
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
# revision identifiers, used by Alembic.
12+
revision = '67151bb74080'
13+
down_revision = 'ae9dd729e66c'
14+
branch_labels = ()
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.add_column(
21+
'landings', sa.Column('error', sa.String(length=128), nullable=True)
22+
)
23+
op.add_column(
24+
'landings', sa.Column('result', sa.String(length=128), nullable=True)
25+
)
26+
# ### end Alembic commands ###
27+
28+
29+
def downgrade():
30+
# ### commands auto generated by Alembic - please adjust! ###
31+
op.drop_column('landings', 'result')
32+
op.drop_column('landings', 'error')
33+
# ### end Alembic commands ###

‎tests/canned_responses/lando_api/landings.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,40 @@
77
'id': 1,
88
'request_id': 1,
99
'revision_id': 'D1',
10-
'status': 'started'
10+
'status': 'started',
11+
'error_msg': '',
12+
'result': None,
1113
}, {
1214
'id': 2,
1315
'request_id': 2,
1416
'revision_id': 'D1',
15-
'status': 'finished'
17+
'status': 'finished',
18+
'error_msg': '',
19+
'result': None,
1620
}, {
1721
'id': 4,
1822
'request_id': 4,
1923
'revision_id': 'D1',
20-
'status': 'started'
24+
'status': 'started',
25+
'error_msg': '',
26+
'result': None,
2127
}
2228
]
2329

2430
CANNED_LANDING_1 = {
2531
'id': 1,
2632
'status': 'started',
2733
'request_id': 1,
28-
'revision_id': 'D1'
34+
'revision_id': 'D1',
35+
'error_msg': '',
36+
'result': None,
37+
}
38+
39+
CANNED_LANDING_2 = {
40+
'id': 2,
41+
'status': 'started',
42+
'request_id': 2,
43+
'revision_id': 'D1',
44+
'error_msg': '',
45+
'result': None,
2946
}

0 commit comments

Comments
 (0)
Failed to load comments.