Skip to content

Commit a3b5460

Browse files
committedJun 29, 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 315f41d commit a3b5460

File tree

8 files changed

+227
-34
lines changed

8 files changed

+227
-34
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

+25-4
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, LandingNotFoundException,
13+
RevisionNotFoundException, TRANSPLANT_JOB_FAILED, TRANSPLANT_JOB_LANDED
1514
)
1615

1716

@@ -71,3 +70,25 @@ def get(landing_id):
7170
)
7271

7372
return landing.serialize(), 200
73+
74+
75+
def update(landing_id, data):
76+
"""Update landing on pingback from Transplant."""
77+
try:
78+
landing = Landing.query.filter_by(
79+
id=landing_id, request_id=data['request_id']
80+
).one()
81+
except NoResultFound:
82+
return problem(
83+
404,
84+
'Landing not found',
85+
'The requested Landing does not exist',
86+
type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404'
87+
)
88+
89+
landing.error = data.get('error_msg', '')
90+
landing.result = data.get('result', '')
91+
landing.status = TRANSPLANT_JOB_LANDED if data['landed'
92+
] else TRANSPLANT_JOB_FAILED
93+
landing.save()
94+
return landing.serialize(), 202

‎landoapi/models/landing.py

+28-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
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.models.storage import db
57
from landoapi.phabricator_client import PhabricatorClient
68
from landoapi.transplant_client import TransplantClient
79

10+
TRANSPLANT_JOB_STARTING = 'starting'
811
TRANSPLANT_JOB_STARTED = 'started'
9-
TRANSPLANT_JOB_FINISHED = 'finished'
12+
TRANSPLANT_JOB_LANDED = 'landed'
13+
TRANSPLANT_JOB_FAILED = 'failed'
1014

1115

1216
def _get_revision(revision_id, api_key=None):
@@ -42,35 +46,45 @@ class Landing(db.Model):
4246
request_id = db.Column(db.Integer, unique=True)
4347
revision_id = db.Column(db.String(30))
4448
status = db.Column(db.Integer)
49+
error = db.Column(db.String(128), default='')
50+
result = db.Column(db.String(128))
4551

4652
def __init__(
47-
self, request_id=None, revision_id=None, status=TRANSPLANT_JOB_STARTED
53+
self,
54+
request_id=None,
55+
revision_id=None,
56+
status=TRANSPLANT_JOB_STARTING
4857
):
4958
self.request_id = request_id
5059
self.revision_id = revision_id
5160
self.status = status
5261

5362
@classmethod
54-
def create(cls, revision_id, phabricator_api_key=None, save=True):
63+
def create(cls, revision_id, phabricator_api_key=None):
5564
""" Land revision and create a Transplant item in storage. """
5665
revision = _get_revision(revision_id, phabricator_api_key)
5766
if not revision:
5867
raise RevisionNotFoundException(revision_id)
5968

69+
# save landing to make sure we've got the callback
70+
landing = cls(
71+
revision_id=revision_id,
72+
)
73+
landing.save()
74+
6075
trans = TransplantClient()
76+
callback = '%s/landings/%s/update' % (
77+
os.environ.get('HOST_URL'), landing.id
78+
)
6179
request_id = trans.land(
62-
'ldap_username@example.com', revision['repo_url']
80+
'ldap_username@example.com', revision['repo_url'], callback
6381
)
6482
if not request_id:
6583
raise LandingNotCreatedException
6684

67-
landing = cls(
68-
revision_id=revision_id,
69-
request_id=request_id,
70-
status=TRANSPLANT_JOB_STARTED
71-
)
72-
if save:
73-
landing.save(create=True)
85+
landing.request_id = request_id
86+
landing.status = TRANSPLANT_JOB_STARTED
87+
landing.save(create=True)
7488

7589
return landing
7690

@@ -100,7 +114,9 @@ def serialize(self):
100114
'id': self.id,
101115
'revision_id': self.revision_id,
102116
'request_id': self.request_id,
103-
'status': self.status
117+
'status': self.status,
118+
'error_msg': self.error,
119+
'result': self.result
104120
}
105121

106122

‎landoapi/spec/swagger.yml

+63
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,61 @@ paths:
121121
schema:
122122
allOf:
123123
- $ref: '#/definitions/Error'
124+
/landings/{landing_id}/update:
125+
post:
126+
operationId: landoapi.api.landings.update
127+
description: |
128+
Sends request to transplant service and responds just the status code
129+
parameters:
130+
- name: data
131+
in: body
132+
description: |
133+
Retrieve status of the landing job
134+
required: true
135+
schema:
136+
type: object
137+
required:
138+
- request_id
139+
properties:
140+
request_id:
141+
type: integer
142+
tree:
143+
type: string
144+
rev:
145+
type: string
146+
patch:
147+
type: string
148+
destination:
149+
type: string
150+
trysyntax:
151+
type: string
152+
landed:
153+
type: boolean
154+
error_msg:
155+
type: string
156+
result:
157+
type: string
158+
- name: landing_id
159+
in: path
160+
type: string
161+
description: |
162+
The id of the landing to return
163+
required: true
164+
responses:
165+
201:
166+
description: OK
167+
schema:
168+
$ref: '#/definitions/Landing'
169+
404:
170+
description: Landing does not exist
171+
schema:
172+
allOf:
173+
- $ref: '#/definitions/Error'
174+
default:
175+
description: Unexpected error
176+
schema:
177+
allOf:
178+
- $ref: '#/definitions/Error'
124179
/landings/{landing_id}:
125180
get:
126181
description: |
@@ -168,6 +223,14 @@ definitions:
168223
type: integer
169224
description: |
170225
The id of the Revision
226+
result:
227+
type: string
228+
description: |
229+
revision (sha) of push if landed == true
230+
error:
231+
type: string
232+
description: |
233+
Error message if landing failed
171234
Revision:
172235
type: object
173236
properties:

‎landoapi/transplant_client.py

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

1919
@requests_mock.mock()
20-
def land(self, ldap_username, tree, request):
20+
def land(self, ldap_username, tree, pingback, request):
2121
""" Sends a push request to Transplant API to land a revision.
2222
2323
Returns request_id received from Transplant API.
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
}

‎tests/test_landings.py

+55-13
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import pytest
66

77
from landoapi.models.storage import db as _db
8-
from landoapi.models.landing import (Landing, TRANSPLANT_JOB_STARTED)
8+
from landoapi.models.landing import (
9+
Landing, TRANSPLANT_JOB_STARTED, TRANSPLANT_JOB_LANDED
10+
)
911

1012
from tests.canned_responses.phabricator.revisions import *
1113
from tests.canned_responses.lando_api.revisions import *
@@ -40,12 +42,7 @@ def test_landing_revision(db, client, phabfactory):
4042

4143
# test saved data
4244
landing = Landing.query.get(1)
43-
assert landing.serialize() == {
44-
'id': 1,
45-
'request_id': 1,
46-
'revision_id': 'D1',
47-
'status': TRANSPLANT_JOB_STARTED
48-
}
45+
assert landing.serialize() == CANNED_LANDING_1
4946

5047
response = client.post(
5148
'/landings?api_key=api-key',
@@ -60,12 +57,7 @@ def test_landing_revision(db, client, phabfactory):
6057

6158
# test saved data
6259
landing = Landing.query.get(2)
63-
assert landing.serialize() == {
64-
'id': 2,
65-
'request_id': 2,
66-
'revision_id': 'D1',
67-
'status': TRANSPLANT_JOB_STARTED
68-
}
60+
assert landing.serialize() == CANNED_LANDING_2
6961

7062

7163
def test_get_transplant_status(db, client):
@@ -112,3 +104,53 @@ def test_get_jobs(db, client):
112104
response = client.get('/landings?revision_id=D1&status=finished')
113105
assert response.status_code == 200
114106
assert len(response.json) == 1
107+
108+
109+
def test_update_landing(db, client):
110+
Landing(1, 'D1', 'started').save(True)
111+
112+
response = client.post(
113+
'/landings/1/update',
114+
data=json.dumps({
115+
'request_id': 1,
116+
'landed': True,
117+
'result': 'sha123'
118+
}),
119+
content_type='application/json'
120+
)
121+
122+
assert response.status_code == 202
123+
response = client.get('/landings/1')
124+
assert response.json['status'] == TRANSPLANT_JOB_LANDED
125+
126+
127+
def test_update_landing_bad_id(db, client):
128+
Landing(1, 'D1', 'started').save(True)
129+
130+
response = client.post(
131+
'/landings/2/update',
132+
data=json.dumps({
133+
'request_id': 1,
134+
'landed': True,
135+
'result': 'sha123'
136+
}),
137+
content_type='application/json'
138+
)
139+
140+
assert response.status_code == 404
141+
142+
143+
def test_update_landing_bad_request_id(db, client):
144+
Landing(1, 'D1', 'started').save(True)
145+
146+
response = client.post(
147+
'/landings/1/update',
148+
data=json.dumps({
149+
'request_id': 2,
150+
'landed': True,
151+
'result': 'sha123'
152+
}),
153+
content_type='application/json'
154+
)
155+
156+
assert response.status_code == 404

0 commit comments

Comments
 (0)
Failed to load comments.