Skip to content

Commit 2ffc8ed

Browse files
ErikWitternrshriram
authored andcommitted
API-enabled BookInfo sample application (#693)
* revised source code of microservices so they communicate in JSON and expose an API * make API accessible via ingress controller * added swagger definition of new API * removed HTML status pages of microservices; aligned their health endpoints to return JSON * do not display anything when ratings data is not present to address comment by @frankbu on pull request #693
1 parent 103b3dd commit 2ffc8ed

8 files changed

Lines changed: 559 additions & 292 deletions

File tree

samples/apps/bookinfo/bookinfo.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,12 @@ spec:
215215
backend:
216216
serviceName: productpage
217217
servicePort: 9080
218+
- path: /api/v1/products
219+
backend:
220+
serviceName: productpage
221+
servicePort: 9080
222+
- path: /api/v1/products/.*
223+
backend:
224+
serviceName: productpage
225+
servicePort: 9080
218226
---

samples/apps/bookinfo/src/details/details.rb

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# limitations under the License.
1616

1717
require 'webrick'
18+
require 'json'
1819

1920
if ARGV.length < 1 then
2021
puts "usage: #{$PROGRAM_NAME} port"
@@ -27,57 +28,39 @@
2728

2829
trap 'INT' do server.shutdown end
2930

30-
details_resp = '
31-
<h4 class="text-center text-primary">Book Details</h4>
32-
<dl>
33-
<dt>Paperback:</dt>200 pages
34-
<dt>Publisher:</dt> PublisherA
35-
<dt>Language:</dt>English
36-
<dt>ISBN-10:</dt>1234567890
37-
<dt>ISBN-13:</dt>123-1234567980
38-
</dl>
39-
'
40-
4131
server.mount_proc '/health' do |req, res|
4232
res.status = 200
43-
res.body = 'Details is healthy'
44-
res['Content-Type'] = 'text/html'
33+
res.body = {'status' => 'Details is healthy'}.to_json
34+
res['Content-Type'] = 'application/json'
4535
end
4636

4737
server.mount_proc '/details' do |req, res|
48-
res.body = details_resp
49-
res['Content-Type'] = 'text/html'
38+
pathParts = req.path.split('/')
39+
begin
40+
id = Integer(pathParts[-1])
41+
details = get_book_details(id)
42+
res.body = details.to_json
43+
res['Content-Type'] = 'application/json'
44+
rescue
45+
res.body = {'error' => 'please provide numeric product id'}.to_json
46+
res['Content-Type'] = 'application/json'
47+
res.status = 400
48+
end
5049
end
5150

52-
server.mount_proc '/' do |req, res|
53-
res.body = '
54-
<html>
55-
<head>
56-
<meta charset="utf-8">
57-
<meta http-equiv="X-UA-Compatible" content="IE=edge">
58-
<meta name="viewport" content="width=device-width, initial-scale=1">
59-
60-
<!-- Latest compiled and minified CSS -->
61-
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
62-
63-
<!-- Optional theme -->
64-
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
65-
66-
<!-- Latest compiled and minified JavaScript -->
67-
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
68-
69-
<!-- Latest compiled and minified JavaScript -->
70-
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
71-
72-
</head>
73-
<title>Book details service</title>
74-
<body>
75-
<p><h2>Hello! This is the book details service. My content is</h2></p>
76-
<div>%s</div>
77-
</body>
78-
</html>
79-
' % [details_resp]
80-
res['Content-Type'] = 'text/html'
51+
# TODO: provide details on different books.
52+
def get_book_details(id)
53+
return {
54+
'id' => id,
55+
'author': 'William Shakespeare',
56+
'year': 1595,
57+
'type' => 'paperback',
58+
'pages' => 200,
59+
'publisher' => 'PublisherA',
60+
'language' => 'English',
61+
'ISBN-10' => '1234567890',
62+
'ISBN-13' => '123-1234567890'
63+
}
8164
end
8265

8366
server.start

samples/apps/bookinfo/src/productpage/productpage.py

Lines changed: 93 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ def getForwardHeaders(request):
9898

9999
return headers
100100

101+
102+
# The UI:
101103
@app.route('/')
102104
@app.route('/index.html')
103105
def index():
@@ -109,66 +111,138 @@ def index():
109111

110112
return render_template('index.html', serviceTable=table)
111113

114+
112115
@app.route('/health')
113116
def health():
114117
return 'Product page is healthy'
115118

119+
116120
@app.route('/login', methods=['POST'])
117121
def login():
118122
user = request.values.get('username')
119123
response = app.make_response(redirect(request.referrer))
120124
response.set_cookie('user', user)
121125
return response
122126

127+
123128
@app.route('/logout', methods=['GET'])
124129
def logout():
125130
response = app.make_response(redirect(request.referrer))
126131
response.set_cookie('user', '', expires=0)
127132
return response
128133

134+
129135
@app.route('/productpage')
130136
def front():
137+
product_id = 0 # TODO: replace default value
131138
headers = getForwardHeaders(request)
132139
user = request.cookies.get("user", "")
133-
bookdetails = getDetails(headers)
134-
bookreviews = getReviews(headers)
135-
return render_template('productpage.html', details=bookdetails, reviews=bookreviews, user=user)
140+
product = getProduct(product_id)
141+
(detailsStatus, details) = getProductDetails(product_id, headers)
142+
(reviewsStatus, reviews) = getProductReviews(product_id, headers)
143+
return render_template(
144+
'productpage.html',
145+
detailsStatus=detailsStatus,
146+
reviewsStatus=reviewsStatus,
147+
product=product,
148+
details=details,
149+
reviews=reviews,
150+
user=user)
151+
152+
153+
# The API:
154+
@app.route('/api/v1/products')
155+
def productsRoute():
156+
return json.dumps(getProducts()), 200, {'Content-Type': 'application/json'}
157+
158+
159+
@app.route('/api/v1/products/<product_id>')
160+
def productRoute(product_id):
161+
headers = getForwardHeaders(request)
162+
(status, details) = getProductDetails(product_id, headers)
163+
return json.dumps(details), status, {'Content-Type': 'application/json'}
136164

137-
def getReviews(headers):
138-
for i in range(2):
139-
try:
140-
res = requests.get(reviews['name']+"/"+reviews['endpoint'], headers=headers, timeout=3.0)
141-
except:
142-
res = None
143165

144-
if res and res.status_code == 200:
145-
return res.text
166+
@app.route('/api/v1/products/<product_id>/reviews')
167+
def reviewsRoute(product_id):
168+
headers = getForwardHeaders(request)
169+
(status, reviews) = getProductReviews(product_id, headers)
170+
return json.dumps(reviews), status, {'Content-Type': 'application/json'}
171+
172+
173+
@app.route('/api/v1/products/<product_id>/ratings')
174+
def ratingsRoute(product_id):
175+
headers = getForwardHeaders(request)
176+
(status, ratings) = getProductRatings(product_id, headers)
177+
return json.dumps(ratings), status, {'Content-Type': 'application/json'}
178+
179+
180+
181+
# Data providers:
182+
def getProducts():
183+
return [
184+
{
185+
'id': 0,
186+
'title': 'The Comedy of Errors',
187+
'descriptionHtml': '<a href="https://en.wikipedia.org/wiki/The_Comedy_of_Errors">Wikipedia Summary</a>: The Comedy of Errors is one of <b>William Shakespeare\'s</b> early plays. It is his shortest and one of his most farcical comedies, with a major part of the humour coming from slapstick and mistaken identity, in addition to puns and word play.'
188+
}
189+
]
190+
146191

147-
return """<h3>Sorry, product reviews are currently unavailable for this book.</h3>"""
192+
def getProduct(product_id):
193+
products = getProducts()
194+
if product_id + 1 > len(products):
195+
return None
196+
else:
197+
return products[product_id]
148198

149199

150-
def getDetails(headers):
200+
def getProductDetails(product_id, headers):
151201
try:
152-
res = requests.get(details['name']+"/"+details['endpoint'], headers=headers, timeout=1.0)
202+
url = details['name'] + "/" + details['endpoint'] + "/" + str(product_id)
203+
res = requests.get(url, headers=headers, timeout=3.0)
153204
except:
154205
res = None
206+
if res and res.status_code == 200:
207+
return (200, res.json())
208+
else:
209+
status = (res.status_code if res != None and res.status_code else 500)
210+
return (status, {'error': 'Sorry, product details are currently unavailable for this book.'})
211+
155212

213+
def getProductReviews(product_id, headers):
214+
try:
215+
url = reviews['name'] + "/" + reviews['endpoint'] + "/" + str(product_id)
216+
res = requests.get(url, headers=headers, timeout=3.0)
217+
except:
218+
res = None
156219
if res and res.status_code == 200:
157-
return res.text
220+
return (200, res.json())
158221
else:
159-
return """<h3>Sorry, product details are currently unavailable for this book.</h3>"""
222+
status = (res.status_code if res != None and res.status_code else 500)
223+
return (status, {'error': 'Sorry, product reviews are currently unavailable for this book.'})
160224

161225

162-
class Writer(object):
226+
def getProductRatings(product_id, headers):
227+
try:
228+
url = ratings['name'] + "/" + ratings['endpoint'] + "/" + str(product_id)
229+
res = requests.get(url, headers=headers, timeout=3.0)
230+
except:
231+
res = None
232+
if res and res.status_code == 200:
233+
return (200, res.json())
234+
else:
235+
status = (res.status_code if res != None and res.status_code else 500)
236+
return (status, {'error': 'Sorry, product ratings are currently unavailable for this book.'})
163237

238+
class Writer(object):
164239
def __init__(self, filename):
165240
self.file = open(filename,'w')
166241

167242
def write(self, data):
168243
self.file.write(data)
169244
self.file.flush()
170245

171-
172246
if __name__ == '__main__':
173247
if len(sys.argv) < 2:
174248
print "usage: %s port" % (sys.argv[0])
@@ -177,5 +251,6 @@ def write(self, data):
177251
p = int(sys.argv[1])
178252
sys.stderr = Writer('stderr.log')
179253
sys.stdout = Writer('stdout.log')
254+
print "start at port %s" % (p)
180255
app.run(host='0.0.0.0', port=p, debug = True, threaded=True)
181256

samples/apps/bookinfo/src/productpage/templates/productpage.html

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -92,29 +92,63 @@ <h4 class="modal-title">Please sign in</h4>
9292
</div>
9393

9494
<div class="container-fluid">
95-
<div class="row">
96-
<div class="col-md-12">
97-
<h3 class="text-center text-primary">The Comedy of Errors</h3>
98-
<p> <a href="https://en.wikipedia.org/wiki/The_Comedy_of_Errors">Wikipedia
99-
Summary</a>: The Comedy of Errors is one of <b>William
100-
Shakespeare's</b> early plays. It is his shortest and one of his
101-
most farcical comedies, with a major part of the humour coming
102-
from slapstick and mistaken identity, in addition to puns and word
103-
play.</p>
104-
</div>
105-
</div>
95+
<div class="row">
96+
<div class="col-md-12">
97+
<h3 class="text-center text-primary">{{ product.title }}</h3>
98+
{% autoescape false %}
99+
<p>Summary: {{ product.descriptionHtml }}</p>
100+
{% endautoescape %}
101+
</div>
102+
</div>
106103

107-
<div class="row">
108-
<div class="col-md-6">
109-
{% autoescape false %}
110-
{{ details }}
111-
{% endautoescape %}
112-
</div>
113-
<div class="col-md-6">
114-
{% autoescape false %}
115-
{{ reviews }}
116-
{% endautoescape %}
117-
</div>
118-
</div>
104+
<div class="row">
105+
<div class="col-md-6">
106+
{% if detailsStatus == 200: %}
107+
<h4 class="text-center text-primary">Book Details</h4>
108+
<dl>
109+
<dt>Type:</dt>{{ details.type }}
110+
<dt>Pages:</dt>{{ details.pages }}
111+
<dt>Publisher:</dt>{{ details.publisher }}
112+
<dt>Language:</dt>{{ details.language }}
113+
<dt>ISBN-10:</dt>{{ details['ISBN-10'] }}
114+
<dt>ISBN-13:</dt>{{ details['ISBN-13'] }}
115+
</dl>
116+
{% else %}
117+
<h4 class="text-center text-primary">Error fetching product details!</h4>
118+
{% if details: %}
119+
<p>{{ details.error }}</p>
120+
{% endif %}
121+
{% endif %}
122+
</div>
123+
124+
<div class="col-md-6">
125+
{% if reviewsStatus == 200: %}
126+
<h4 class="text-center text-primary">Book Reviews</h4>
127+
{% for review in reviews.reviews %}
128+
<blockquote>
129+
<p>{{ review.text }}</p>
130+
<small>{{ review.reviewer }}</small>
131+
{% if review.rating: %}
132+
<font color="{{ review.rating.color }}">
133+
<!-- full stars: -->
134+
{% for n in range(review.rating.stars) %}
135+
<span class="glyphicon glyphicon-star"></span>
136+
{% endfor %}
137+
<!-- empty stars: -->
138+
{% for n in range(5 - review.rating.stars) %}
139+
<span class="glyphicon glyphicon-star-empty"></span>
140+
{% endfor %}
141+
</font>
142+
{% endif %}
143+
</blockquote>
144+
{% endfor %}
145+
{% else %}
146+
<h4 class="text-center text-primary">Error fetching product reviews!</h4>
147+
{% if reviews: %}
148+
<p>{{ reviews.error }}</p>
149+
{% endif %}
150+
{% endif %}
151+
</div>
152+
</div>
119153
</div>
120154
{% endblock %}

samples/apps/bookinfo/src/ratings/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
},
55
"dependencies": {
66
"httpdispatcher": "1.0.0",
7-
"mysql": "2.13.0",
87
"mongodb": "^2.2.31"
98
}
109
}

0 commit comments

Comments
 (0)