diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..83db606d --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,85 @@ +version: 2 +jobs: + test: + docker: + - image: circleci/node:8.9.4 + - image: circleci/postgres:9.6.2-alpine + environment: + - POSTGRES_USER: circle_test + - POSTGRES_DB: circle_test + - image: elasticsearch:2.3 + environment: + DB_MASTER_URL: postgres://circle_test:@127.0.0.1:5432/circle_test + AUTH_SECRET: secret + AUTH_DOMAIN: topcoder-dev.com + LOG_LEVEL: debug + APP_VERSION: v4 + steps: + - checkout + - restore_cache: + key: test-node-modules-{{ checksum "package.json" }} + - run: npm install + - save_cache: + key: test-node-modules-{{ checksum "package.json" }} + paths: + - node_modules + - run: npm run lint + - run: npm run test + - run: npm run build + - persist_to_workspace: + root: . + paths: + - dist + deployDev: + docker: + - image: docker:17.06.1-ce-git + steps: + - checkout + - setup_remote_docker + - run: + name: Installation of build dependencies. + command: apk add --no-cache bash + - attach_workspace: + at: ./workspace + - run: + name: Installing AWS client + command: | + apk add --no-cache jq py-pip sudo + sudo pip install awscli --upgrade + - run: ./build.sh DEV + - run: ./deploy.sh DEV + deployProd: + docker: + - image: docker:17.06.1-ce-git + steps: + - checkout + - setup_remote_docker + - run: + name: Installation of build dependencies. + command: apk add --no-cache bash + - attach_workspace: + at: ./workspace + - run: + name: Installing AWS client + command: | + apk add --no-cache jq py-pip sudo + sudo pip install awscli --upgrade + - run: ./build.sh PROD + - run: ./deploy.sh PROD +workflows: + version: 2 + build: + jobs: + - test + - deployDev: + requires: + - test + filters: + branches: + only: 'dev' + - deployProd: + requires: + - test + filters: + branches: + only: 'master' diff --git a/.ebextensions/01-environment-variables.config b/.ebextensions/01-environment-variables.config index ded02ed4..8d8d7ffc 100644 --- a/.ebextensions/01-environment-variables.config +++ b/.ebextensions/01-environment-variables.config @@ -32,9 +32,6 @@ option_settings: - namespace: aws:elasticbeanstalk:application:environment option_name: DIRECT_PROJECT_SERVICE_ENDPOINT value: TBD - - namespace: aws:elasticbeanstalk:application:environment - option_name: TOPIC_SERVICE_ENDPOINT - value: TBD - namespace: aws:elasticbeanstalk:application:environment option_name: FILE_SERVICE_ENDPOINT value: TBD @@ -53,27 +50,6 @@ option_settings: - namespace: aws:elasticbeanstalk:application:environment option_name: AWS_SECRET_ACCESS_KEY value: TBD - - namespace: aws:elasticbeanstalk:application:environment - option_name: SALESFORCE_WEB_TO_LEAD_URL - value: https://www.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8 - - namespace: aws:elasticbeanstalk:application:environment - option_name: SALESFORCE_ORG_ID - value: TBD - - namespace: aws:elasticbeanstalk:application:environment - option_name: SALESFORCE_LEAD_PROJECT_NAME - value: TBD - - namespace: aws:elasticbeanstalk:application:environment - option_name: SALESFORCE_LEAD_PROJECT_DESC - value: TBD - - namespace: aws:elasticbeanstalk:application:environment - option_name: SALESFORCE_LEAD_PROJECT_LINK - value: TBD - - namespace: aws:elasticbeanstalk:application:environment - option_name: SALESFORCE_LEAD_PROJECT_ID - value: TBD - namespace: aws:elasticbeanstalk:application:environment option_name: CONNECT_PROJECTS_URL value: TBD - - namespace: aws:elasticbeanstalk:application:environment - option_name: USER_SERVICE_URL - value: TBD diff --git a/Dockerfile b/Dockerfile index 2a7fe789..a59f1239 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM node:6.9.4 -LABEL version="1.0" +FROM node:8.2.1 +LABEL version="1.2" LABEL description="Projects microservice" RUN apt-get update && \ diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..6e97ae07 --- /dev/null +++ b/build.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# more bash-friendly output for jq +JQ="jq --raw-output --exit-status" + +ENV=$1 +AWS_REGION=$(eval "echo \$${ENV}_AWS_REGION") +ACCOUNT_ID=$(eval "echo \$${ENV}_AWS_ACCOUNT_ID") +AWS_REPOSITORY=$(eval "echo \$${ENV}_AWS_REPOSITORY") + +build() { + docker build -t $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$AWS_REPOSITORY:$CIRCLE_SHA1 . +} + +build \ No newline at end of file diff --git a/circle.yml b/circle.yml deleted file mode 100644 index ddc060f3..00000000 --- a/circle.yml +++ /dev/null @@ -1,55 +0,0 @@ -machine: - - node: - version: v6.9.4 - environment: - DB_MASTER_URL: postgres://ubuntu:@127.0.0.1:5432/circle_test - #RABBITMQ_URL: amqp://localhost:5672 - AUTH_SECRET: secret - AUTH_DOMAIN: topcoder-dev.com - LOG_LEVEL: debug - APP_VERSION: v4 - -dependencies: - pre: - - pip install awsebcli - override: - - npm install - - wget https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/2.3.5/elasticsearch-2.3.5.tar.gz - - tar -xvf elasticsearch-2.3.5.tar.gz - - elasticsearch-2.3.5/bin/elasticsearch: {background: true} - # Make sure that Elasticsearch is up before running tests: - - sleep 10 && wget --waitretry=5 --retry-connrefused -v http://127.0.0.1:9200/ - - -deployment: - development: - branch: [dev, 'feature/admin-endpoints'] - commands: - - ./ebs_deploy.sh tc-project-service DEV $CIRCLE_BUILD_NUM - - - production: - branch: master - commands: - - ./ebs_deploy.sh tc-project-service PROD $CIRCLE_BUILD_NUM - - # tag: /v[0-9]+(\.[0-9]+)*/ - # owner: appirio-tech - # commands: - # - ./ebs_deploy.sh tc-project-service PROD $CIRCLE_TAG - -general: - artifacts: - - ./coverage - -notify: - webhooks: - # slack - product-dev - - url: https://hooks.slack.com/services/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 - -experimental: - notify: - branches: - only: - - master diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 59ff0ddd..07c7936f 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -1,5 +1,6 @@ { - "authSecret": "AUTH_SECRET", + "apiVersion": "API_VERSION", + "AUTH_SECRET": "AUTH_SECRET", "logLevel": "LOG_LEVEL", "version": "APP_VERSION", "captureLogs": "CAPTURE_LOGS", @@ -17,28 +18,22 @@ "directProjectServiceTimeout": "DIRECT_PROJECT_SERVICE_TIMEOUt", "fileServiceEndpoint": "FILE_SERVICE_ENDPOINT", "identityServiceEndpoint": "IDENTITY_SERVICE_ENDPOINT", - "topicServiceEndpoint": "TOPIC_SERVICE_ENDPOINT", "memberServiceEndpoint": "MEMBER_SERVICE_ENDPOINT", "systemUserClientId": "SYSTEM_USER_CLIENT_ID", "systemUserClientSecret": "SYSTEM_USER_CLIENT_SECRET", - "userServiceUrl": "USER_SERVICE_URL", "connectProjectsUrl": "CONNECT_PROJECTS_URL", - "salesforceLead" : { - "webToLeadUrl": "SALESFORCE_WEB_TO_LEAD_URL", - "orgId" : "SALESFORCE_ORG_ID", - "projectNameFieldId": "SALESFORCE_LEAD_PROJECT_NAME", - "projectDescFieldId": "SALESFORCE_LEAD_PROJECT_DESC", - "projectLinkFieldId": "SALESFORCE_LEAD_PROJECT_LINK", - "projectIdFieldId" : "SALESFORCE_LEAD_PROJECT_ID" - }, "dbConfig": { "masterUrl": "DB_MASTER_URL", "maxPoolSize": "DB_MAX_POOL_SIZE", "minPoolSize": "DB_MIN_POOL_SIZE" }, - "analyticsKey": "ANALYTICS_KEY", - "validIssuers": "VALID_ISSUERS", + "analyticsKey": "SEGMENT_ANALYTICS_KEY", + "VALID_ISSUERS": "VALID_ISSUERS", "jwksUri": "JWKS_URI", "busApiUrl": "BUS_API_URL", - "busApiToken": "BUS_API_TOKEN" + "AUTH0_URL" : "AUTH0_URL", + "AUTH0_CLIENT_ID": "AUTH0_CLIENT_ID", + "AUTH0_CLIENT_SECRET": "AUTH0_CLIENT_SECRET", + "AUTH0_AUDIENCE": "AUTH0_AUDIENCE", + "TOKEN_CACHE_TIME" : "TOKEN_CACHE_TIME" } diff --git a/config/default.json b/config/default.json index dd753d8e..1db936d4 100644 --- a/config/default.json +++ b/config/default.json @@ -1,6 +1,6 @@ { - "authSecret": "secret", - "authDomain": "topcoder-dev.com", + "apiVersion": "v4", + "AUTH_SECRET": "secret", "logLevel": "info", "version": "v4", "captureLogs": "false", @@ -9,7 +9,6 @@ "pubsubQueueName": "project.service", "pubsubExchangeName": "projects", "fileServiceEndpoint": "", - "topicServiceEndpoint": "", "identityServiceEndpoint": "", "memberServiceEndpoint": "", "directProjectServiceEndpoint": "", @@ -25,16 +24,7 @@ }, "systemUserClientId": "", "systemUserClientSecret": "", - "userServiceUrl": "", "connectProjectUrl":"", - "salesforceLead" : { - "webToLeadUrl": "https://www.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8", - "orgId": "", - "projectNameFieldId": "", - "projectDescFieldId": "", - "projectLinkFieldId": "", - "projectIdFieldId" : "" - }, "dbConfig": { "masterUrl": "", "maxPoolSize": 50, @@ -42,8 +32,12 @@ "idleTimeout": 1000 }, "analyticsKey": "", - "validIssuers": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", - "jwksUri": "", + "VALID_ISSUERS": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]", "busApiUrl": "http://api.topcoder-dev.com", - "busApiToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoicHJvamVjdC1zZXJ2aWNlIiwiaWF0IjoxNTEyNzQ3MDgyLCJleHAiOjE1MjEzODcwODJ9.PHuNcFDaotGAL8RhQXQMdpL8yOKXxjB5DbBIodmt7RE" + "HEALTH_CHECK_URL": "_health", + "AUTH0_CLIENT_ID": "", + "AUTH0_CLIENT_SECRET": "", + "AUTH0_AUDIENCE": "", + "AUTH0_URL": "", + "TOKEN_CACHE_TIME": "" } diff --git a/config/development.json b/config/development.json index b7de350a..7e8ce29d 100644 --- a/config/development.json +++ b/config/development.json @@ -1,5 +1,4 @@ { - "authDomain": "topcoder-dev.com", "pubsubQueueName": "dev.project.service", "pubsubExchangeName": "dev.projects", "attachmentsS3Bucket": "topcoder-dev-media" diff --git a/config/sample.local.js b/config/sample.local.js index 47dbc0bd..058f9089 100644 --- a/config/sample.local.js +++ b/config/sample.local.js @@ -12,18 +12,9 @@ if (process.env.NODE_ENV === 'test') { logentriesToken: '', rabbitmqURL: 'amqp://dockerhost:5672', fileServiceEndpoint: 'https://api.topcoder-dev.com/v3/files/', - topicServiceEndpoint: 'https://api.topcoder-dev.com/v4/topics/', directProjectServiceEndpoint: 'https://api.topcoder-dev.com/v3/direct', connectProjectsUrl: 'https://connect.topcoder-dev.com/projects/', memberServiceEndpoint: 'http://dockerhost:3001/members', - salesforceLead: { - webToLeadUrl: 'https://www.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8', - orgId: '00D2C0000000dO6', - projectNameFieldId: 'title', - projectDescFieldId: 'description', - projectLinkFieldId: 'URL', - projectIdFieldId: '00N2C000000Vxxx', - }, dbConfig: { masterUrl: 'postgres://coder:mysecretpassword@dockerhost:54321/projectsdb', maxPoolSize: 50, diff --git a/config/test.json b/config/test.json index 2b045431..26d22a7a 100644 --- a/config/test.json +++ b/config/test.json @@ -1,6 +1,5 @@ { - "authSecret": "secret", - "authDomain": "topcoder-dev.com", + "AUTH_SECRET": "secret", "logLevel": "debug", "captureLogs": "false", "logentriesToken": "", diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 00000000..9f95a6e2 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,265 @@ +#!/usr/bin/env bash + +# more bash-friendly output for jq +JQ="jq --raw-output --exit-status" + +ENV=$1 +COUNTER_LIMIT=20 +ACCOUNT_ID=$(eval "echo \$${ENV}_AWS_ACCOUNT_ID") +AWS_REGION=$(eval "echo \$${ENV}_AWS_REGION") +AWS_ECS_CONTAINER_NAME="tc-project-service" +AWS_REPOSITORY=$(eval "echo \$${ENV}_AWS_REPOSITORY") +AWS_ECS_CLUSTER=$(eval "echo \$${ENV}_AWS_ECS_CLUSTER") +AWS_ECS_SERVICE=$(eval "echo \$${ENV}_AWS_ECS_SERVICE") +AUTH_DOMAIN=$(eval "echo \$${ENV}_AUTH_DOMAIN") +AUTH_SECRET=$(eval "echo \$${ENV}_AUTH_SECRET") +VALID_ISSUERS=$(eval "echo \$${ENV}_VALID_ISSUERS") +PORT=3000 +family="tc-project-service" + +# configures aws cli for further usage +configure_aws_cli() { + export AWS_ACCESS_KEY_ID=$(eval "echo \$${ENV}_AWS_ACCESS_KEY_ID") + export AWS_SECRET_ACCESS_KEY=$(eval "echo \$${ENV}_AWS_SECRET_ACCESS_KEY") + aws --version + aws configure set default.region $AWS_REGION + aws configure set default.output json +} + +# deploys the app to the ecs cluster +deploy_cluster() { + + make_task_def + register_definition + if [[ $(aws ecs update-service --cluster $AWS_ECS_CLUSTER --service $AWS_ECS_SERVICE --task-definition $revision | \ + $JQ '.service.taskDefinition') != $revision ]]; then + echo "Error updating service." + return 1 + fi + + echo "Deployed!" + return 0 +} + +make_task_def(){ + task_template='{ + "family": "%s", + "requiresCompatibilities": ["EC2", "FARGATE"], + "networkMode": "awsvpc", + "executionRoleArn": "arn:aws:iam::%s:role/ecsTaskExecutionRole", + "cpu": "1024", + "memory": "2048", + "containerDefinitions": [ + { + "name": "%s", + "image": "%s.dkr.ecr.%s.amazonaws.com/%s:%s", + "essential": true, + "memory": 200, + "cpu": 10, + "environment": [ + { + "name": "NODE_ENV", + "value": "%s" + }, + { + "name": "LOG_LEVEL", + "value": "%s" + }, + { + "name": "CAPTURE_LOGS", + "value": "%s" + }, + { + "name": "LOGENTRIES_TOKEN", + "value": "%s" + }, + { + "name": "API_VERSION", + "value": "%s" + }, + { + "name": "AWS_REGION", + "value": "%s" + }, + { + "name": "AWS_ACCESS_KEY_ID", + "value": "%s" + }, + { + "name": "AWS_SECRET_ACCESS_KEY", + "value": "%s" + }, + { + "name": "AUTH_DOMAIN", + "value": "%s" + }, + { + "name": "AUTH_SECRET", + "value": "%s" + }, + { + "name": "VALID_ISSUERS", + "value": "%s" + }, + { + "name": "DB_MASTER_URL", + "value": "%s" + }, + { + "name": "MEMBER_SERVICE_ENDPOINT", + "value": "%s" + }, + { + "name": "IDENTITY_SERVICE_ENDPOINT", + "value": "%s" + }, + { + "name": "BUS_API_URL", + "value": "%s" + }, + { + "name": "SYSTEM_USER_CLIENT_ID", + "value": "%s" + }, + { + "name": "SYSTEM_USER_CLIENT_SECRET", + "value": "%s" + }, + { + "name": "PROJECTS_ES_URL", + "value": "%s" + }, + { + "name": "PROJECTS_ES_INDEX_NAME", + "value": "%s" + }, + { + "name": "RABBITMQ_URL", + "value": "%s" + }, + { + "name": "DIRECT_PROJECT_SERVICE_ENDPOINT", + "value": "%s" + }, + { + "name": "FILE_SERVICE_ENDPOINT", + "value": "%s" + }, + { + "name": "CONNECT_PROJECTS_URL", + "value": "%s" + }, + { + "name": "SEGMENT_ANALYTICS_KEY", + "value": "%s" + }, + { + "name": "AUTH0_URL", + "value": "%s" + }, + { + "name": "AUTH0_AUDIENCE", + "value": "%s" + }, + { + "name": "AUTH0_CLIENT_ID", + "value": "%s" + }, + { + "name": "AUTH0_CLIENT_SECRET", + "value": "%s" + }, + { + "name": "TOKEN_CACHE_TIME", + "value": "%s" + } + ], + "portMappings": [ + { + "hostPort": %s, + "protocol": "tcp", + "containerPort": %s + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/aws/ecs/%s", + "awslogs-region": "%s", + "awslogs-stream-prefix": "%s" + } + } + } + ]}' + API_VERSION=$(eval "echo \$${ENV}_API_VERSION") + DB_MASTER_URL=$(eval "echo \$${ENV}_DB_MASTER_URL") + MEMBER_SERVICE_ENDPOINT=$(eval "echo \$${ENV}_MEMBER_SERVICE_ENDPOINT") + IDENTITY_SERVICE_ENDPOINT=$(eval "echo \$${ENV}_IDENTITY_SERVICE_ENDPOINT") + BUS_API_URL=$(eval "echo \$${ENV}_BUS_API_URL") + SYSTEM_USER_CLIENT_ID=$(eval "echo \$${ENV}_SYSTEM_USER_CLIENT_ID") + SYSTEM_USER_CLIENT_SECRET=$(eval "echo \$${ENV}_SYSTEM_USER_CLIENT_SECRET") + CAPTURE_LOGS=$(eval "echo \$${ENV}_CAPTURE_LOGS") + LOGENTRIES_TOKEN=$(eval "echo \$${ENV}_LOGENTRIES_TOKEN") + LOG_LEVEL=$(eval "echo \$${ENV}_LOG_LEVEL") + PROJECTS_ES_URL=$(eval "echo \$${ENV}_PROJECTS_ES_URL") + PROJECTS_ES_INDEX_NAME=$(eval "echo \$${ENV}_PROJECTS_ES_INDEX_NAME") + RABBITMQ_URL=$(eval "echo \$${ENV}_RABBITMQ_URL") + DIRECT_PROJECT_SERVICE_ENDPOINT=$(eval "echo \$${ENV}_DIRECT_PROJECT_SERVICE_ENDPOINT") + FILE_SERVICE_ENDPOINT=$(eval "echo \$${ENV}_FILE_SERVICE_ENDPOINT") + CONNECT_PROJECTS_URL=$(eval "echo \$${ENV}_CONNECT_PROJECTS_URL") + SEGMENT_ANALYTICS_KEY=$(eval "echo \$${ENV}_SEGMENT_ANALYTICS_KEY") + if [ "$ENV" = "PROD" ]; then + NODE_ENV=production + elif [ "$ENV" = "DEV" ]; then + NODE_ENV=development + fi + echo "NODE_ENV" + echo $NODE_ENV + + AUTH0_URL=$(eval "echo \$${ENV}_AUTH0_URL") + AUTH0_AUDIENCE=$(eval "echo \$${ENV}_AUTH0_AUDIENCE") + AUTH0_CLIENT_ID=$(eval "echo \$${ENV}_AUTH0_CLIENT_ID") + AUTH0_CLIENT_SECRET=$(eval "echo \$${ENV}_AUTH0_CLIENT_SECRET") + TOKEN_CACHE_TIME=$(eval "echo \$${ENV}_TOKEN_CACHE_TIME") + + + task_def=$(printf "$task_template" $family $ACCOUNT_ID $AWS_ECS_CONTAINER_NAME $ACCOUNT_ID $AWS_REGION $AWS_REPOSITORY $CIRCLE_SHA1 $NODE_ENV $LOG_LEVEL $CAPTURE_LOGS $LOGENTRIES_TOKEN $API_VERSION $AWS_REGION $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY $AUTH_DOMAIN $AUTH_SECRET $VALID_ISSUERS $DB_MASTER_URL $MEMBER_SERVICE_ENDPOINT $IDENTITY_SERVICE_ENDPOINT $BUS_API_URL $SYSTEM_USER_CLIENT_ID $SYSTEM_USER_CLIENT_SECRET $PROJECTS_ES_URL $PROJECTS_ES_INDEX_NAME $RABBITMQ_URL $DIRECT_PROJECT_SERVICE_ENDPOINT $FILE_SERVICE_ENDPOINT $CONNECT_PROJECTS_URL $SEGMENT_ANALYTICS_KEY "$AUTH0_URL" "$AUTH0_AUDIENCE" $AUTH0_CLIENT_ID "$AUTH0_CLIENT_SECRET" $TOKEN_CACHE_TIME $PORT $PORT $AWS_ECS_CLUSTER $AWS_REGION $NODE_ENV) +} + +push_ecr_image(){ + eval $(aws ecr get-login --region $AWS_REGION --no-include-email) + docker push $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/$AWS_REPOSITORY:$CIRCLE_SHA1 +} + +register_definition() { + if revision=$(aws ecs register-task-definition --cli-input-json "$task_def" 2> /dev/null | $JQ '.taskDefinition.taskDefinitionArn'); then + echo "Revision: $revision" + else + echo "Failed to register task definition" + return 1 + fi +} + +check_service_status() { + counter=0 + sleep 60 + servicestatus=`aws ecs describe-services --service $AWS_ECS_SERVICE --cluster $AWS_ECS_CLUSTER | $JQ '.services[].events[0].message'` + while [[ $servicestatus != *"steady state"* ]] + do + echo "Current event message : $servicestatus" + echo "Waiting for 30 seconds to check the service status...." + sleep 30 + servicestatus=`aws ecs describe-services --service $AWS_ECS_SERVICE --cluster $AWS_ECS_CLUSTER | $JQ '.services[].events[0].message'` + counter=`expr $counter + 1` + if [[ $counter -gt $COUNTER_LIMIT ]] ; then + echo "Service does not reach steady state within 10 minutes. Please check" + exit 1 + fi + done + echo "$servicestatus" +} + +configure_aws_cli +push_ecr_image +deploy_cluster +check_service_status diff --git a/ebs_deploy.sh b/ebs_deploy.sh deleted file mode 100755 index 94e9af47..00000000 --- a/ebs_deploy.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -SERVICE=$1 -ENV=$2 -TAG_SUFFIX=$3 -TAG="$ENV.$TAG_SUFFIX" -ENV_LOWER=`echo "$ENV" | awk '{print tolower($0)}'` - -echo "Deploying to Elasticbeanstalk" -echo "############################" -export AWS_ACCESS_KEY_ID=$(eval "echo \$${ENV}_AWS_ACCESS_KEY_ID") -export AWS_SECRET_ACCESS_KEY=$(eval "echo \$${ENV}_AWS_SECRET_ACCESS_KEY") - -# eb deploy -# eb init -r us-east-1 $SERVICE -EB_OUTPUT="$(eb deploy tc-project-api-v4-${ENV_LOWER} -l $TAG -r us-east-1)" - -echo $EB_OUTPUT -if echo $EB_OUTPUT | grep -iq error; then - exit 1 -fi -exit 0 diff --git a/package.json b/package.json index b6234466..7d8dd65e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "prestart": "npm run -s build", "start": "node dist", "start:dev": "NODE_ENV=development PORT=8001 nodemon -w src --exec \"babel-node src --presets es2015\" | ./node_modules/.bin/bunyan", - "test": "NODE_ENV=test npm run lint && NODE_ENV=test npm run sync:es && NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- --compilers js:babel-core/register $(find src -path '*spec.js*')", + "test": "NODE_ENV=test npm run lint && NODE_ENV=test npm run sync:es && NODE_ENV=test npm run sync:db && NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- --timeout 5000 --compilers js:babel-core/register $(find src -path '*spec.js*')", "test:watch": "NODE_ENV=test ./node_modules/.bin/mocha -w --compilers js:babel-core/register $(find src -path '*spec.js*')", "seed": "babel-node src/tests/seed.js --presets es2015" }, @@ -55,7 +55,7 @@ "pg": "^4.5.5", "pg-native": "^1.10.0", "sequelize": "^3.23.0", - "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.2", + "tc-core-library-js": "appirio-tech/tc-core-library-js.git#v2.3", "traverse": "^0.6.6", "urlencode": "^1.1.0" }, diff --git a/src/app.js b/src/app.js index dbb693ca..864fb0e6 100644 --- a/src/app.js +++ b/src/app.js @@ -4,6 +4,7 @@ import _ from 'lodash'; import bodyParser from 'body-parser'; import expressSanitizer from 'express-sanitizer'; import config from 'config'; +import cors from 'cors'; import coreLib from 'tc-core-library-js'; import expressRequestId from 'express-request-id'; import router from './routes'; @@ -63,6 +64,20 @@ const logger = coreLib.logger({ app.use(coreLib.middleware.logger(null, logger)); app.logger = logger; +// ======================= +// CORS ================ +// ======================= +// const whitelist = [`*.${domain}`]; +// const corsOptions = { +// origin: (origin, callback) => { +// const originIsWhitelisted = whitelist.indexOf(origin) !== -1; +// callback(null, originIsWhitelisted); +// }, +// }; +// app.use(cors(corsOptions)); +// app.options('*', cors()); +app.use(cors()); + // ======================= // Database ========= // ======================= diff --git a/src/constants.js b/src/constants.js index e642eb66..39b0b398 100644 --- a/src/constants.js +++ b/src/constants.js @@ -77,3 +77,7 @@ export const BUS_API_EVENT = { export const REGEX = { URL: /^(http(s?):\/\/)?(www\.)?[a-zA-Z0-9\.\-\_]+(\.[a-zA-Z]{2,15})+(\:[0-9]{2,5})?(\/[a-zA-Z0-9\_\-\s\.\/\?\%\#\&\=;]*)?$/, // eslint-disable-line }; + +export const TOKEN_SCOPES = { + CONNECT_PROJECT_ADMIN: 'all:connect_project', +}; diff --git a/src/routes/index.js b/src/routes/index.js index 16018aef..47a51502 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,10 +1,13 @@ import _ from 'lodash'; +import config from 'config'; import validate from 'express-validation'; import { Router } from 'express'; const router = Router(); +const apiVersion = config.apiVersion; + validate.options({ status: 422, flatten: true, @@ -12,7 +15,7 @@ validate.options({ }); // health check -router.get('/_health', (req, res) => { +router.get(`/${apiVersion}/projects/health`, (req, res) => { // TODO more checks res.status(200).send({ message: 'All-is-well', @@ -23,7 +26,7 @@ router.get('/_health', (req, res) => { // All project service endpoints need authentication const jwtAuth = require('tc-core-library-js').middleware.jwtAuthenticator; -router.all('/v4/projects*', jwtAuth()); +router.all(RegExp(`\\/${apiVersion}\\/projects(?!\\/health).*`), jwtAuth()); // Register all the routes router.route('/v4/projects') diff --git a/src/routes/projects/create.js b/src/routes/projects/create.js index 684ef859..393ffbf6 100644 --- a/src/routes/projects/create.js +++ b/src/routes/projects/create.js @@ -118,13 +118,16 @@ module.exports = [ if (newProject.billingAccountId) { body.billingAccountId = newProject.billingAccountId; } + req.log.debug('creating project history for project %d', newProject.id); // add to project history models.ProjectHistory.create({ projectId: _newProject.id, status: PROJECT_STATUS.DRAFT, cancelReason: null, updatedBy: req.authUser.userId, - }); + }).then(() => req.log.debug('project history created for project %d', newProject.id)) + .catch(() => req.log.error('project history failed for project %d', newProject.id)); + req.log.debug('creating direct project for project %d', newProject.id); return directProject.createDirectProject(req, body) .then((resp) => { newProject.directProjectId = resp.data.result.content.projectId; @@ -139,17 +142,18 @@ module.exports = [ }); // return Promise.resolve(); }) - .then(() => { newProject = newProject.get({ plain: true }); // remove utm details & deletedAt field newProject = _.omit(newProject, ['deletedAt', 'utm']); // add an empty attachments array newProject.attachments = []; + req.log.debug('Sending event to RabbitMQ bus for project %d', newProject.id); req.app.services.pubsub.publish(EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED, newProject, { correlationId: req.id }, ); + req.log.debug('Sending event to Kafka bus for project %d', newProject.id); // emit event req.app.emit(EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED, { req, project: newProject }); res.status(201).json(util.wrapResponse(req.id, newProject, 1, 201)); diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js index f1cd2c9a..d2ffa319 100755 --- a/src/routes/projects/list.js +++ b/src/routes/projects/list.js @@ -133,6 +133,23 @@ const parseElasticSearchCriteria = (criteria, fields, order) => { }, }, }, + { + nested: { + path: 'details', + query: { + nested: { + path: 'details.utm', + query: { + query_string: { + query: `*${keyword}*`, + analyze_wildcard: true, + fields: ['details.utm.code'], + }, + }, + }, + }, + }, + }, ], }, }; diff --git a/src/services/busApi.js b/src/services/busApi.js index ee297e9e..5639d25a 100644 --- a/src/services/busApi.js +++ b/src/services/busApi.js @@ -2,7 +2,9 @@ import config from 'config'; const Promise = require('bluebird'); const axios = require('axios'); +const tcCoreLibAuth = require('tc-core-library-js').auth; +const m2m = tcCoreLibAuth.m2m(config); let client = null; @@ -10,25 +12,28 @@ let client = null; * Get Http client to bus api * @return {Object} Http Client to bus api */ -function getClient() { +async function getClient() { if (client) return client; const apiBusUrl = config.get('busApiUrl'); - const apiBusToken = config.get('busApiToken'); + try { + const token = await m2m.getMachineToken(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_SECRET); + client = axios.create({ baseURL: apiBusUrl }); - client = axios.create({ baseURL: apiBusUrl }); + // Alter defaults after instance has been created + client.defaults.headers.common.Authorization = `Bearer ${token}`; - // Alter defaults after instance has been created - client.defaults.headers.common.Authorization = `Bearer ${apiBusToken}`; + // Add a response interceptor + client.interceptors.response.use(function (res) { // eslint-disable-line + return res; + }, function (error) { // eslint-disable-line + // Ingore response errors + return Promise.resolve(); + }); - // Add a response interceptor - client.interceptors.response.use(function (res) { // eslint-disable-line - return res; - }, function (error) { // eslint-disable-line - // Ingore response errors - return Promise.resolve(); - }); - - return client; + return client; + } catch (err) { + return Promise.reject(`Bus api calling - Error in genearting m2m token : ${err.message}`); + } } /** @@ -42,39 +47,42 @@ function getClient() { function createEvent(type, message, logger) { const body = JSON.stringify(message); logger.debug(`Sending message: ${JSON.stringify(message)}`); - return getClient().post('/v5/bus/events', { - type, - message: body, - }) - .then((resp) => { - logger.debug('Sent event to bus-api'); - logger.debug(`Sent event to bus-api [data]: ${resp.data}`); - logger.debug(`Sent event to bus-api [status]: ${resp.status}`); - }) - .catch((error) => { - logger.debug('Error sending event to bus-api'); - if (error.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - logger.debug(error.response.data); - logger.debug(error.response.status); - logger.debug(error.response.headers); - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - logger.debug(error.request); - } else { - // Something happened in setting up the request that triggered an Error - logger.debug(error.message); - } - logger.debug(error.config); - - Promise.resolve(); // eslint-disable-line + return getClient().then((busClient) => { + logger.debug('calling bus-api'); + busClient.post('/bus/events', { + type, + message: body, + }) + .then((resp) => { + logger.debug('Sent event to bus-api'); + logger.debug(`Sent event to bus-api [data]: ${resp.data}`); + logger.debug(`Sent event to bus-api [status]: ${resp.status}`); + }) + .catch((error) => { + logger.debug('Error sending event to bus-api'); + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + logger.debug(error.response.data); + logger.debug(error.response.status); + logger.debug(error.response.headers); + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + logger.debug(error.request); + } else { + // Something happened in setting up the request that triggered an Error + logger.debug(error.message); + } + logger.debug(error.config); + Promise.resolve(); // eslint-disable-line + }); + }).catch((errMessage) => { + logger.debug(errMessage); }); } - module.exports = { createEvent, }; diff --git a/src/services/index.js b/src/services/index.js index c7b9f37a..a1d84c12 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -18,6 +18,7 @@ module.exports = (fapp, logger) => { if (process.env.NODE_ENV.toLowerCase() === 'test') { require('../tests/serviceMocks')(app); // eslint-disable-line global-require } else { + logger.info('initializing RabbitMQ service'); // RabbitMQ Initialization app.services.pubsub = new RabbitMQService(logger); diff --git a/src/services/topicService.js b/src/services/topicService.js deleted file mode 100644 index 204f07bf..00000000 --- a/src/services/topicService.js +++ /dev/null @@ -1,53 +0,0 @@ -import config from 'config'; -import util from '../util'; -/** - * Service methods to handle creating topics - */ - -/** - * Build custom http client for request - * @param {Object} req request - * @returns {Promise} custom http client - * @private - */ -function getHttpClient(req) { - const httpClient = util.getHttpClient(req); - httpClient.defaults.headers.common.Authorization = req.headers.authorization; - httpClient.defaults.baseURL = config.get('topicServiceEndpoint'); - httpClient.defaults.timeout = 30000; - httpClient.interceptors.response.use((resp) => { - // req.log.debug('resp: ', JSON.stringify(resp.data, null, 2)) - if (resp.status !== 200 || resp.data.result.status !== 200) { - // req.log.error('error resp: ', JSON.stringify(resp.data, null, 2)) - return Promise.reject(new Error(resp.data.result.content.message)); - } - return Promise.resolve(resp); - }); - return httpClient; -} - - -/** - * Create topics in topic service - * @param {Object} req request object - * @param {integer} projectId project id - * @param {String} title title of the post - * @param {String} message message to be posted - * @param {String} tag tag, defaults to PRIMARY - * @return {Promise} returned Promise - */ -function createTopic(req, projectId, title, message, tag = 'PRIMARY') { - return getHttpClient(req) - .post('', { - reference: 'project', - referenceId: projectId.toString(), - tag, - title, - body: message, - }); -} - - -export default { - createTopic, -}; diff --git a/src/util.js b/src/util.js index 6371f726..0afbdfb0 100644 --- a/src/util.js +++ b/src/util.js @@ -17,7 +17,7 @@ import urlencode from 'urlencode'; import elasticsearch from 'elasticsearch'; import Promise from 'bluebird'; import AWS from 'aws-sdk'; -import { ADMIN_ROLES } from './constants'; +import { ADMIN_ROLES, TOKEN_SCOPES } from './constants'; const exec = require('child_process').exec; const models = require('./models').default; @@ -70,6 +70,12 @@ _.assignIn(util, { * @return {boolean} true/false */ hasRole: (req, role) => { + const isMachineToken = _.get(req, 'authUser.isMachine', false); + const tokenScopes = _.get(req, 'authUser.scopes', []); + if (isMachineToken) { + if (_.indexOf(tokenScopes, TOKEN_SCOPES.CONNECT_PROJECT_ADMIN) >= 0) return true; + return false; + } let roles = _.get(req, 'authUser.roles', []); roles = roles.map(s => s.toLowerCase()); return _.indexOf(roles, role.toLowerCase()) >= 0; @@ -81,6 +87,12 @@ _.assignIn(util, { * @return {boolean} true/false */ hasRoles: (req, roles) => { + const isMachineToken = _.get(req, 'authUser.isMachine', false); + const tokenScopes = _.get(req, 'authUser.scopes', []); + if (isMachineToken) { + if (_.indexOf(tokenScopes, TOKEN_SCOPES.CONNECT_PROJECT_ADMIN) >= 0) return true; + return false; + } let authRoles = _.get(req, 'authUser.roles', []); authRoles = authRoles.map(s => s.toLowerCase()); return _.intersection(authRoles, roles.map(r => r.toLowerCase())).length > 0; @@ -101,6 +113,12 @@ _.assignIn(util, { * @return {boolean} true/false */ hasAdminRole: (req) => { + const isMachineToken = _.get(req, 'authUser.isMachine', false); + const tokenScopes = _.get(req, 'authUser.scopes', []); + if (isMachineToken) { + if (_.indexOf(tokenScopes, TOKEN_SCOPES.CONNECT_PROJECT_ADMIN) >= 0) return true; + return false; + } let roles = _.get(req, 'authUser.roles', []); roles = roles.map(s => s.toLowerCase()); return _.intersection(roles, ADMIN_ROLES.map(r => r.toLowerCase())).length > 0; @@ -264,7 +282,7 @@ _.assignIn(util, { httpClient.defaults.headers.common.Accept = 'application/json'; httpClient.defaults.headers.common['Content-Type'] = 'application/json'; httpClient.defaults.headers.common.Authorization = `Bearer ${jwtToken}`; - return httpClient.get(`${config.userServiceUrl}/${userId}`).then((response) => { + return httpClient.get(`${config.identityServiceEndpoint}users/${userId}`).then((response) => { if (response.data && response.data.result && response.data.result.status === 200 && response.data.result.content) { return response.data.result.content;