diff --git a/.travis.yml b/.travis.yml index 267cc9bf97ae883cb8b32972e647d4b718e177ea..843cb4bffb8f3f1c9f743b029d74f3c062bed56a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,14 +7,14 @@ jdk: install: true env: - - GROUP=weaveworksdemos COMMIT=$TRAVIS_COMMIT TAG=$TRAVIS_TAG; + - GROUP=weaveworksdemos COMMIT=$TRAVIS_COMMIT TAG=$TRAVIS_TAG REPO=cart; script: - set -e - - ./scripts/build.sh; + - travis_wait ./scripts/build.sh; - ./test/test.sh unit.py - ./test/test.sh component.py -# - ./test/test.sh container.py --tag $TAG + - ./test/test.sh container.py --tag $TAG after_success: - set -e; @@ -25,3 +25,20 @@ after_success: fi; - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS; - ./scripts/push.sh + +before_install: +- openssl aes-256-cbc -K $encrypted_71d9c8a3a58a_key -iv $encrypted_71d9c8a3a58a_iv + -in cart_deploy_rsa.enc -out cart_deploy_rsa -d +before_deploy: + - eval "$(ssh-agent -s)" + - chmod 600 $TRAVIS_BUILD_DIR/${REPO}_deploy_rsa + - ssh-add $TRAVIS_BUILD_DIR/${REPO}_deploy_rsa +addons: + ssh_known_hosts: $BASTION +deploy: + provider: script + skip_cleanup: true + # The deploy.sh file actually points to the deploy file on the bastion. Not one in the repo. + script: ssh -o StrictHostKeyChecking=no $BASTION_USER@$BASTION ./deploy.sh ${REPO} $COMMIT + on: + branch: master diff --git a/README.md b/README.md index dd11a28949af1ea5e39b1232a702416440ab42e7..77f40e4ae299390f9e9e3edefabef6d773b868c8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [](https://travis-ci.org/microservices-demo/carts) [](https://coveralls.io/github/microservices-demo/carts?branch=master) +[](http://microbadger.com/images/weaveworksdemos/cart "Get your own image badge on microbadger.com") # cart A microservices-demo service that provides shopping carts for users. diff --git a/api-spec/cart.json b/api-spec/cart.json new file mode 100644 index 0000000000000000000000000000000000000000..856eb3248c0079fa8d08b7e7b4e8e4d36ccfcbdd --- /dev/null +++ b/api-spec/cart.json @@ -0,0 +1,206 @@ +{ + "swagger": "2.0", + "info": { + "version": "", + "title": "Carts and items", + "description": "Carts and items resources", + "license": { + "name": "MIT", + "url": "http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT" + } + }, + "host": "carts", + "basePath": "/", + "securityDefinitions": {}, + "schemes": [ + "http" + ], + "consumes": [ + "application/json;charset=UTF-8", + "text/plain" + ], + "produces": [ + "application/json;charset=UTF-8", + "text/plain" + + ], + "paths": { + "/carts/{customerId}": { + "get": { + "description": "", + "operationId": "Get cart", + "produces": [ + "application/json;charset=UTF-8" + ], + "parameters": [ + { + "name": "customerId", + "in": "path", + "required": true, + "type": "string", + "x-example": "1" + } + ], + "responses": { + "200": { + "description": "Returns cart", + "schema": { + "$ref": "#/definitions/Getcartresponse" + } + } + } + }, + "delete": { + "description": "", + "operationId": "Delete cart", + "produces": [ + "application/json;charset=UTF-8" + ], + "parameters": [ + { + "name": "customerId", + "in": "path", + "required": true, + "type": "string", + "x-example": "1" + } + ], + "responses": { + "202": { + "description": "" + } + } + } + }, + "/carts/{customerId}/items": { + "post": { + "description": "", + "operationId": "Add an item to the cart", + "produces": [ + "application/json;charset=UTF-8" + ], + "parameters": [ + { + "name": "customerId", + "in": "path", + "required": true, + "type": "string", + "x-example": "579f21ae98684924944651bf" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CartItem", + "example": { + "itemId":"819e1fbf-8b7e-4f6d-811f-693534916a8b", + "quantity": 20, + "unitPrice" : 99.0 + } + } + } + ], + "responses": { + "201": { + "description": "", + "schema": { + "$ref": "#/definitions/CartItem" + } + } + } + }, + "patch": { + "description": "Update an item", + "operationId": "Update item", + "produces": [ + "application/json;charset=UTF-8" + ], + "parameters": [ + { + "name": "customerId", + "in": "path", + "required": true, + "type": "string", + "x-example": "579f21ae98684924944651bf" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "" + } + } + } + }, + "/carts/{customerId}/items/{itemId}": { + "delete": { + "description": "Delete cart item", + "operationId": "delete", + + "parameters": [ + { + "name": "itemId", + "in": "path", + "required": true, + "type": "string", + "x-example": "819e1fbf-8b7e-4f6d-811f-693534916a8b" + }, + { + "name": "customerId", + "in": "path", + "required": true, + "type": "string", + "x-example": "579f21ae98684924944651bf" + } + ], + "responses": { + "202": { + "description": "Delete response" + } + } + } + } + }, + "definitions": { + "Getcartresponse": { + "title": "Get cart response", + "type": "object", + "properties": { + "customerId": { + "type": "string" + } + }, + "required": [ + "customerId" + ] + }, + "CartItem": { + "title": "Cart item", + "type": "object", + "properties": { + "itemId": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "unitPrice": { + "type": "number" + } + }, + "required": [ + "itemId", + "quantity", + "unitPrice" + ] + } + } +} diff --git a/api-spec/hooks.js b/api-spec/hooks.js new file mode 100644 index 0000000000000000000000000000000000000000..879c9d1ed37f54bf955de834a6785fe50674bb1b --- /dev/null +++ b/api-spec/hooks.js @@ -0,0 +1,93 @@ +const hooks = require('hooks'); +const {MongoClient} = require('mongodb'); +const ObjectID = require('mongodb').ObjectID; + +let db; + +const address = [ + {"_id":ObjectID("579f21ae98684924944651bd"),"_class":"works.weave.socks.accounts.entities.Address","number":"69","street":"Wilson Street","city":"Hartlepool","postcode":"TS26 8JU","country":"United Kingdom"}, + {"_id":ObjectID("579f21ae98684924944651c0"),"_class":"works.weave.socks.accounts.entities.Address","number":"122","street":"Radstone WayNet","city":"Northampton","postcode":"NN2 8NT","country":"United Kingdom"}, + {"_id":ObjectID("579f21ae98684924944651c3"),"_class":"works.weave.socks.accounts.entities.Address","number":"3","street":"Radstone Way","city":"Northampton","postcode":"NN2 8NT","country":"United Kingdom"} +]; + + +const card = [ + {"_id":ObjectID("579f21ae98684924944651be"),"_class":"works.weave.socks.accounts.entities.Card","longNum":"8575776807334952","expires":"08/19","ccv":"014"}, + {"_id":ObjectID("579f21ae98684924944651c1"),"_class":"works.weave.socks.accounts.entities.Card","longNum":"8918468841895184","expires":"08/19","ccv":"597"}, + {"_id":ObjectID("579f21ae98684924944651c4"),"_class":"works.weave.socks.accounts.entities.Card","longNum":"6426429851404909","expires":"08/19","ccv":"381"} +]; + +const cart = [ + {"_id":ObjectID("579f21de98689ebf2bf1cd2f"),"_class":"works.weave.socks.cart.entities.Cart","customerId":"579f21ae98684924944651bf","items":[{"$ref":"item","$id":ObjectID("579f227698689ebf2bf1cd31")},{"$ref":"item","$id":ObjectID("579f22ac98689ebf2bf1cd32")}]}, + {"_id":ObjectID("579f21e298689ebf2bf1cd30"),"_class":"works.weave.socks.cart.entities.Cart","customerId":"579f21ae98684924944651bfaa","items":[]} +]; + + +const item = [ + {"_id":ObjectID("579f227698689ebf2bf1cd31"),"_class":"works.weave.socks.cart.entities.Item","itemId":"819e1fbf-8b7e-4f6d-811f-693534916a8b","quantity":20,"unitPrice":99.0} +]; + + +const customer = [ + {"_id":"579f21ae98684924944651bf","_class":"works.weave.socks.accounts.entities.Customer","firstName":"Eve","lastName":"Berger","username":"Eve_Berger","addresses":[{"$ref":"address","$id":ObjectID("579f21ae98684924944651bd")}],"cards":[{"$ref":"card","$id":ObjectID("579f21ae98684924944651be")}] + }, + {"_id":"579f21ae98684924944651c2","_class":"works.weave.socks.accounts.entities.Customer","firstName":"User","lastName":"Name","username":"user","addresses":[{"$ref":"address","$id":ObjectID("579f21ae98684924944651c0")}],"cards":[{"$ref":"card","$id":ObjectID("579f21ae98684924944651c1")}]}, + {"_id":"579f21ae98684924944651c5","_class":"works.weave.socks.accounts.entities.Customer","firstName":"User1","lastName":"Name1","username":"user1","addresses":[{"$ref":"address","$id":ObjectID("579f21ae98684924944651c3")}],"cards":[{"$ref":"card","$id":ObjectID("579f21ae98684924944651c4")}]} +]; + + +// Setup database connection before Dredd starts testing +hooks.beforeAll((transactions, done) => { + var MongoEndpoint = process.env.MONGO_ENDPOINT || 'mongodb://localhost:32769/data'; + MongoClient.connect(MongoEndpoint, function(err, conn) { + if (err) { + console.error(err); + } + db = conn; + done(err); + }); +}); + +hooks.beforeEach((transaction, done) => { + db.dropDatabase(function (err, res) { + var promisesToKeep = [ + db.collection('customer').insertMany(customer), + db.collection('card').insertMany(card), + db.collection('cart').insertMany(cart), + db.collection('address').insertMany(address), + db.collection('item').insertMany(item) + ]; + Promise.all(promisesToKeep).then(function(vls) { + done(); + }, function(vls) { + done(); + }); + }) + +}); + + +hooks.before("/carts/{customerId}/items > POST", function(transaction, done) { + transaction.request.headers['Content-Type'] = 'application/json'; + transaction.request.body = JSON.stringify( + { + "itemId":"819e1fbf-8b7e-4f6d-811f-693534916a8b", + "quantity": 20, + "unitPrice" : 99.0 + } + ); + + done(); +}); + +// TODO: Can't make POST and PUT work, skipping for now + +// hooks.before("/carts/{customerId}/items > POST", function(transaction, done) { +// transaction.skip = true; +// done(); +// }); + +hooks.before("/carts/{customerId}/items > PATCH", function(transaction, done) { + transaction.skip = true; + done(); +}); diff --git a/cart_deploy_rsa.enc b/cart_deploy_rsa.enc new file mode 100644 index 0000000000000000000000000000000000000000..bef5caa09c2722048e643d526e70862c2306be6d Binary files /dev/null and b/cart_deploy_rsa.enc differ diff --git a/scripts/build.sh b/scripts/build.sh index 1edf51470767ece80dd02560b7ac43892806d0d1..6e7cdb1f4cb78f828a7fccf8d45bbf9c202bb8f0 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -24,7 +24,7 @@ else fi CODE_DIR=$(cd $SCRIPT_DIR/..; pwd) echo $CODE_DIR -$DOCKER_CMD run --rm -v $HOME/.m2:/root/.m2 -v $CODE_DIR:/usr/src/mymaven -w /usr/src/mymaven maven:3.2-jdk-8 mvn -DskipTests package +$DOCKER_CMD run --rm -v $HOME/.m2:/root/.m2 -v $CODE_DIR:/usr/src/mymaven -w /usr/src/mymaven maven:3.2-jdk-8 mvn -q -DskipTests package cp $CODE_DIR/target/*.jar $CODE_DIR/docker/cart diff --git a/test/container.py b/test/container.py index 426fb3f58cb2488766c8ab6a947097d82bdc7a90..dd8638b8b984ceac36c3a6b1306a2c8654232a18 100644 --- a/test/container.py +++ b/test/container.py @@ -1,22 +1,22 @@ import argparse import sys import unittest +import os +from util.Api import Api from time import sleep -from util.Api import Api from util.Docker import Docker from util.Dredd import Dredd - class CartContainerTest(unittest.TestCase): TAG = "latest" + COMMIT = "" container_name = Docker().random_container_name('cart') mongo_container_name = Docker().random_container_name('cart-db') - def __init__(self, methodName='runTest'): super(CartContainerTest, self).__init__(methodName) self.ip = "" - + def setUp(self): Docker().start_container(container_name=self.mongo_container_name, image="mongo", host="cart-db") command = ['docker', 'run', @@ -25,7 +25,7 @@ class CartContainerTest(unittest.TestCase): '-h', 'cart', '--link', CartContainerTest.mongo_container_name, - 'weaveworksdemos/cart:' + self.TAG] + 'weaveworksdemos/cart:' + self.COMMIT] Docker().execute(command) self.ip = Docker().get_container_ip(CartContainerTest.container_name) @@ -34,26 +34,34 @@ class CartContainerTest(unittest.TestCase): Docker().kill_and_remove(CartContainerTest.mongo_container_name) def test_api_validated(self): - limit = 60 - while Api().noResponse('http://' + self.ip + ':80/carts/579f21ae98684924944651bf'): + limit = 30 + while Api().noResponse('http://' + self.ip + ':80/carts/'): if limit == 0: self.fail("Couldn't get the API running") limit = limit - 1 sleep(1) - - out = Dredd().test_against_endpoint("carts/carts.json", CartContainerTest.container_name, "http://cart/", - "mongodb://cart-db:27017/data", self.mongo_container_name) + + out = Dredd().test_against_endpoint( + "cart", "http://cart/", + links=[self.mongo_container_name, self.container_name], + env=[("MONGO_ENDPOINT", "mongodb://cart-db:27017/data")], + dump_streams=True) self.assertGreater(out.find("0 failing"), -1) self.assertGreater(out.find("0 errors"), -1) print(out) - if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('--tag', default="latest", help='The tag of the image to use. (default: latest)') + default_tag = "latest" + parser.add_argument('--tag', default=default_tag, help='The tag of the image to use. (default: latest)') parser.add_argument('unittest_args', nargs='*') args = parser.parse_args() CartContainerTest.TAG = args.tag + + if CartContainerTest.TAG == "": + CartContainerTest.TAG = default_tag + + CartContainerTest.COMMIT = os.environ["COMMIT"] # Now set the sys.argv to the unittest_args (leaving sys.argv[0] alone) sys.argv[1:] = args.unittest_args unittest.main() diff --git a/test/test.sh b/test/test.sh index f84b64db1f9aa5c0f69022cf5a03c110657eba85..13adbeec19ae60dc4ba63cce5964362b2fa6d474 100755 --- a/test/test.sh +++ b/test/test.sh @@ -21,14 +21,16 @@ echo "Testing $1" CODE_DIR=$(cd $SCRIPT_DIR/..; pwd) echo "$@" $DOCKER_CMD run \ - --rm \ - --name test \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v $CODE_DIR:$CODE_DIR -w $CODE_DIR \ - -e COVERALLS_TOKEN=$COVERALLS_TOKEN \ - -e TRAVIS_JOB_ID=$TRAVIS_JOB_ID \ - -e TRAVIS_BRANCH=$TRAVIS_BRANCH \ - -e TRAVIS_PULL_REQUEST=$TRAVIS_PULL_REQUEST \ - -e TRAVIS=$TRAVIS \ - test-container \ - sh -c export PYTHONPATH=\$PYTHONPATH:\$PWD/test ; python test/"$@" + --rm \ + --name test \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v $CODE_DIR:$CODE_DIR -w $CODE_DIR \ + -e COVERALLS_TOKEN=$COVERALLS_TOKEN \ + -e TRAVIS_JOB_ID=$TRAVIS_JOB_ID \ + -e TRAVIS_BRANCH=$TRAVIS_BRANCH \ + -e TRAVIS_PULL_REQUEST=$TRAVIS_PULL_REQUEST \ + -e TRAVIS=$TRAVIS \ + -e TAG=$TAG \ + -e COMMIT=$COMMIT \ + test-container \ + sh -c "export PYTHONPATH=\$PYTHONPATH:\$PWD/test ; python test/$@" diff --git a/test/unit.py b/test/unit.py index 0b8acc59711c178b41e6e09706e01119e994c28a..4c9aeac01cf549ae61f2f2246171c80cc45d222b 100644 --- a/test/unit.py +++ b/test/unit.py @@ -11,7 +11,7 @@ class JavaServices(unittest.TestCase): code_dir = script_dir + "/.." home = expanduser("~") command = ['docker', 'run', '--rm', '-v', home + '/.m2:/root/.m2', '-v', code_dir + ':/usr/src/mymaven', '-w', - '/usr/src/mymaven', 'maven:3.2-jdk-8', 'mvn', 'test'] + '/usr/src/mymaven', 'maven:3.2-jdk-8', 'mvn', '-q', 'test'] print(Docker().execute(command)) diff --git a/test/util/Api.py b/test/util/Api.py index 34953e00bea565ea97ca0a9b6b5300010bfc4ef2..64468d83dd636157eb91af0653fd4eb20180bb31 100644 --- a/test/util/Api.py +++ b/test/util/Api.py @@ -1,10 +1,9 @@ import requests - class Api: def noResponse(self, url): try: r = requests.get(url, timeout=5) except requests.exceptions.ConnectionError: return True - return r.status_code > 299 + return False diff --git a/test/util/Docker.py b/test/util/Docker.py index c739f03d7110823bb66fd5b70fa57eeaa7a925f2..7711ba0f7922d1bb6fce35cb03755bc28e62c946 100644 --- a/test/util/Docker.py +++ b/test/util/Docker.py @@ -1,18 +1,22 @@ import re -from random import random from subprocess import Popen, PIPE - +from random import random # From http://blog.bordage.pro/avoid-docker-py/ class Docker: def kill_and_remove(self, ctr_name): command = ['docker', 'rm', '-f', ctr_name] - self.execute(command) + try: + self.execute(command) + return True + except RuntimeError as e: + print(e) + return False def random_container_name(self, prefix): retstr = prefix + '-' for i in range(5): - retstr += chr(int(round(random() * (122 - 97) + 97))) + retstr += chr(int(round(random() * (122-97) + 97))) return retstr def get_container_ip(self, ctr_name): @@ -21,17 +25,13 @@ class Docker: ctr_name] return re.sub(r'[^0-9.]*', '', self.execute(command)) - def execute(self, command): + def execute(self, command, dump_streams=False): print("Running: " + ' '.join(command)) p = Popen(command, stdout=PIPE, stderr=PIPE) - out = p.stdout.read() - stderr = p.stderr.read() - if p.wait() != 0: - p.stdout.close() - p.stderr.close() - raise RuntimeError(str(stderr.decode('utf-8'))) - p.stdout.close() - p.stderr.close() + out, err = p.communicate() + if dump_streams == True: + print(out.decode('utf-8')) + print(err.decode('utf-8')) return str(out.decode('utf-8')) def start_container(self, container_name="", image="", cmd="", host=""): diff --git a/test/util/Dredd.py b/test/util/Dredd.py index 03b0f899a4758e1e33d9fedb833aaf407963c716..fae932e692cb7943ec4b0807bba2e3d24f0528a9 100644 --- a/test/util/Dredd.py +++ b/test/util/Dredd.py @@ -1,25 +1,30 @@ from util.Docker import Docker - +from util.Api import Api +import os +import unittest class Dredd: - image = 'weaveworksdemos/openapi' + image = 'weaveworksdemos/openapi:snapshot' container_name = '' - - def test_against_endpoint(self, json_spec, endpoint_container_name, api_endpoint, mongo_endpoint_url, - mongo_container_name): + def test_against_endpoint(self, service, api_endpoint, links=[], env=[], dump_streams=False): self.container_name = Docker().random_container_name('openapi') command = ['docker', 'run', '-h', 'openapi', '--name', self.container_name, - '--link', mongo_container_name, - '--link', endpoint_container_name, - '--env', "MONGO_ENDPOINT={0}".format(mongo_endpoint_url), - Dredd.image, - "/usr/src/app/{0}".format(json_spec), - api_endpoint, - "-f", - "/usr/src/app/hooks.js" - ] - out = Docker().execute(command) + '-v', "{0}:{1}".format(os.getcwd() + "/api-spec/", "/tmp/specs/")] + + if links != []: + [command.extend(["--link", x]) for x in links] + + if env != []: + [command.extend(["--env", "{}={}".format(x[0], x[1])]) for x in env] + + command.extend([Dredd.image, + "/tmp/specs/{0}.json".format(service), + api_endpoint, + "-f", + "/tmp/specs/hooks.js".format(service)]) + out = Docker().execute(command, dump_streams=dump_streams) + Docker().kill_and_remove(self.container_name) return out