commit 2eb4b9e63b81ed186368fc2c9d00a22e2a72ce75 Author: ZhijunZhao Date: Sun Jun 4 23:39:48 2017 -0700 Initial commit diff --git a/.deployment b/.deployment new file mode 100644 index 0000000..cf9fee1 --- /dev/null +++ b/.deployment @@ -0,0 +1,2 @@ +[config] +command = ./deployment/function/deploy.cmd \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..234499a --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Maven Outputs +target/ + +# Gradle Outputs +.gradle/ +build/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Kubernetes +**/.kube + +# IDE +.vscode/ +.idea/ +**/*.iml +.springbean + +# Spring logs +logs/ diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..cc5203e --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,44 @@ +node { + stage('Checkout Source') { + checkout scm + } + + def azureUtil = load './deployment/jenkins/azureutil.groovy' + def branchName = scm.branches[0].name.split('/').last() + def targetEnv = (branchName in ['test','prod']) ? branchName : 'dev' + + stage('Prepare') { + echo "Target environment is: ${targetEnv}" + azureUtil.prepareEnv(targetEnv) + } + + stage('Build') { + sh("cd data-app; mvn compile; cd ..") + sh("cd web-app; mvn compile; cd ..") + } + + stage('Test') { + sh("cd data-app; mvn test; cd ..") + sh("cd web-app; mvn test; cd ..") + } + + stage('Publish Docker Image') { + withEnv(["ACR_NAME=${azureUtil.acrName}", "ACR_LOGIN_SERVER=${azureUtil.acrLoginServer}", "ACR_USERNAME=${azureUtil.acrUsername}", "ACR_PASSWORD=${azureUtil.acrPassword}"]) { + sh("cd data-app; mvn package docker:build -DpushImage -DskipTests; cd ..") + sh("cd web-app; mvn package docker:build -DpushImage -DskipTests; cd ..") + } + } + + stage('Deploy') { + // Deploy function + azureUtil.deployFunction() + + // Deploy data app + azureUtil.deployDataApp(targetEnv, azureUtil.config.EAST_US_GROUP) + azureUtil.deployDataApp(targetEnv, azureUtil.config.WEST_EUROPE_GROUP) + + // Deploy web app + azureUtil.deployWebApp(azureUtil.config.EAST_US_GROUP) + azureUtil.deployWebApp(azureUtil.config.WEST_EUROPE_GROUP) + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4b1ad51 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/README.md b/README.md new file mode 100644 index 0000000..63ecce3 --- /dev/null +++ b/README.md @@ -0,0 +1,399 @@ +# Movie Database App using Java on Azure # + +The purpose of this sample application is to illustrate a modern Java app in the cloud; the result of this project will be to create a movie database simliar to IMDB. + +## Requirements ## + +In order to create and deploy this sample application, you need to have the following: + +An Azure subscription; if you don't already have an Azure subscription, you can activate your [MSDN subscriber benefits](https://azure.microsoft.com/pricing/member-offers/msdn-benefits-details/) or sign up for a [free Azure account](https://azure.microsoft.com/pricing/free-trial/). + +In addition, you will need all of the following components before you go through the steps in this README: + +| [Azure CLI](http://docs.microsoft.com/cli/azure/overview) | [Java 8](http://java.oracle.com/) | [Maven 3](http://maven.apache.org/) | [Git](https://github.com/) | [Docker](https://www.docker.com/) | + +**NOTE**: There are additional requirements in the *~/deployment/README.md* file which are required in order to setup your development environment; other required components will be installed automatically by the provisioning scripts. + +## Overview ## + +In the following sections, you will create a development sandbox environment on Azure which uses the following components: + +- web apps in Linux containers on [Azure App Service (AAS)](https://azure.microsoft.com/en-us/services/app-service/) + +- Data apps in Kubernetes clusters in the [Azure Container Service (ACS)](https://azure.microsoft.com/en-us/services/container-service/) + +- [Azure Container Registry (ACR)](https://azure.microsoft.com/en-us/services/container-registry/) for container images + +- [Azure Database for MySQL](https://azure.microsoft.com/en-us/services/mysql/) for data + +- [Azure Storage](https://azure.microsoft.com/en-us/services/storage/) for media contents + +The following diagram illustrates the full topology for this sample application enviroment: + + ![](./media/movie-app-layout.jpg) + +In this basic layout, the following design decisions have been implemented: + +- Internet-facing web apps are running in Linux containers on AAS, which can run across multiple regions worldwide. + +- For better performace, this enviroment uses the following: + + - [Azure Traffic Manager](https://azure.microsoft.com/en-us/services/traffic-manager/) to route requests for better performance and availability. + + - [Azure Redis Cache](https://azure.microsoft.com/en-us/services/cache/) for high throughput and low-latency. + +- Container images for the web apps are built using Docker and pushed to a managed private Docker registry in ACR, and deployed to Linux containers on AAS. + +- The web apps communicate with the data apps running in Kubernetes clusters in ACS. + +- Data apps are REST API apps which store and read data from Azure Database for MySQL, which is a fully managed database as a service; data apps store images into and read images from Azure Storage. + +- Another traffic manager is deployed as a load balancer in the front of data apps for routing requests for better performance and availability. + +**Note**: For now, Node.js is being used instead of Java in this sample application for the Azure functions; this will be updated in the near future. + +## Create and Deploy the Sample Application ## + +### Download and customize the sample for your development environment ### + +1. Follow the steps in the *[~/deployment/README.md](deployment/README.md)* file of the sample project to clone the project repo and set up your development environment. + +1. Navigate to the configuration directory for your Maven installation; for example: */usr/local/maven/3.5.0/libexec/conf/* or *%ProgramFiles%\apache-maven\3.5.0\conf*: + + a. Open the *settings.xml* file with a text editor. + + b. Add parameterized settings for ACR access settings to the `` collection in the the *settings.xml* file, this will enable Maven to use a private registry; for example: + + ```xml + + + ${env.ACR_NAME} + ${env.ACR_USERNAME} + ${env.ACR_PASSWORD} + + john_doe@contoso.com + + + + ``` + + c. Save and close your *settings.xml* file. + + +### Create the initial build ### + +1. Open a command prompt and navigate to the *~/deployment/* folder of your local repo. + +1. Login to your Azure account and specify which subscription to use: + + ```shell + az login + az account set --subscription "" + ``` + + **NOTE**: You can use either a subscription name or id when specifying which subscription to use; to obtain a list of your subscriptions, type `az account list`. + +1. Build an initial layout on Azure using an ARM template from using one of the following methods: + + ```shell + source provision.sh + ``` + > **NOTE**: On Windows, run all shell scripts in Git Bash. + + The provisioning script will take a long time to process. + + +### Deploy the internal-facing data app into a Kubernetes cluster in ACS ### + + +1. Open a command prompt and navigate to the folder which contains the data app, which is located in the "*~/data-app/*" folder of your repo; this is a Spring Boot app which: + + * Stores and reads data from Azure Database for MySQL using Spring JDBC + + * Stores images into and reads images from Azure Storage. + +1. Build and dockerize the data app, and push the container into ACR: + + ```shell + mvn package docker:build -DpushImage + ``` + +1. Deploy the data app to a Kubernetes cluster in ACS using the Kubernetes CLI: + + ```shell + kubectl create secret docker-registry ${ACR_LOGIN_SERVER} \ + --docker-server=${ACR_LOGIN_SERVER} \ + --docker-username=${ACR_USERNAME} \ + --docker-password=${ACR_PASSWORD} \ + --docker-email=john_doe@contoso.com \ + --namespace=${TARGET_ENV} \ + --save-config + + envsubst < ../deployment/data-app/deploy.yaml | kubectl apply --namespace=${TARGET_ENV} -f - + ``` + +1. Run below command to watch the creation process of your service object in Kubernetes. + Wait until column `EXTERNAL-IP` has a valid IP address, which means your data app is accessible from internet now. + + ```shell + kubectl get svc --namespace=${TARGET_ENV} --watch + ``` + +1. Navigate to the *~/deployment/* folder of your local repo and run the following command: + + ```shell + cd ../deployment + source dev_setup.sh + ``` + + **NOTE**: Microsoft is currently developing a Maven plugin to deploy to a Kubernetes cluster in Azure Container Service, so in the future you will be able to use `mvn deploy`. + +### Deploy the Internet-facing web app into Linux containers in AAS ### + +1. Open the Internet-facing web app, which is located in the "*~/web-app/*" folder of your repo. + + * This is also a Spring Boot app which talks to the data app that we just deployed. + + * The web app can also use Azure Redis Cache using Spring Data Redis. + +1. Build and dockerize the web app, and push the container into ACR: + + ```shell + mvn package docker:build -DpushImage + ``` + +1. Use Azure CLI to deploy the web app to a Linux container in Azure App Service: + + ```shell + az webapp config container set -g ${EAST_US_GROUP} \ + -n ${EAST_US_WEBAPP_NAME} \ + --docker-custom-image-name ${ACR_LOGIN_SERVER}/web-app \ + --docker-registry-server-url http://${ACR_LOGIN_SERVER} \ + --docker-registry-server-user ${ACR_USERNAME} \ + --docker-registry-server-password ${ACR_PASSWORD} + + az webapp config set -g ${EAST_US_GROUP} \ + -n ${EAST_US_WEBAPP_NAME} \ + --linux-fx-version "DOCKER|${ACR_LOGIN_SERVER}/web-app" + + az webapp config appsettings set -g ${EAST_US_GROUP} \ + -n ${EAST_US_WEBAPP_NAME} \ + --settings DATA_API_URL=${DATA_API_URL} \ + PORT=${WEB_APP_CONTAINER_PORT} \ + WEB_APP_CONTAINER_PORT=${WEB_APP_CONTAINER_PORT} \ + STORAGE_CONNECTION_STRING=${STORAGE_CONNECTION_STRING} \ + ORIGINAL_IMAGE_CONTAINER=${ORIGINAL_IMAGE_CONTAINER} \ + THUMBNAIL_IMAGE_CONTAINER=${THUMBNAIL_IMAGE_CONTAINER} \ + REDIS_HOST=${REDIS_HOST} REDIS_PASSWORD=${REDIS_PASSWORD} + + az webapp restart -g ${EAST_US_GROUP} -n ${EAST_US_WEBAPP_NAME} + ``` + + **NOTE**: Microsoft is currently developing a Maven plugin to accomplish these steps, So in the future you will be able to use `mvn deploy`. + + +### Test and diagnose your sample deployment ### + +Test and diagnose using one of the following two methods: + + * Open website in a web browser + ``` + open http://${EAST_US_WEBAPP_NAME}.azurewebsites.net/ + ``` + + * Run the following command in a console window: + ```shell + curl http://${EAST_US_WEBAPP_NAME}.azurewebsites.net/index + ``` + +### OPTIONAL: Enable monitoring and diagnostics using third party services ### + + + +You can optionally enable *New Relic* and *OverOps* in both web app and data app. +Enable monitoring and diagnostics using the following: + + +* **New Relic** + - Open a console and navigate to `~/web-app`. + - Setup below environment variables + ```shell + export NEW_RELIC_LICENSE_KEY= + export WEBAPP_NEW_RELIC_APP_NAME= + ``` + - Run below command to build an image with New Relic and push to ACR. + The image name is *web-app-w-new-relic*. + ```shell + mvn package docker:build@with-new-relic -DpushImage + ``` + - Run below command to deploy web app to AAS. + ```shell + az webapp config container set -g ${EAST_US_GROUP} \ + -n ${EAST_US_WEBAPP_NAME} \ + --docker-custom-image-name ${ACR_LOGIN_SERVER}/web-app-w-new-relic \ + --docker-registry-server-url http://${ACR_LOGIN_SERVER} \ + --docker-registry-server-user ${ACR_USERNAME} \ + --docker-registry-server-password ${ACR_PASSWORD} + + az webapp config set -g ${EAST_US_GROUP} \ + -n ${EAST_US_WEBAPP_NAME} \ + --linux-fx-version "DOCKER|${ACR_LOGIN_SERVER}/web-app-w-new-relic" + + az webapp restart -g ${EAST_US_GROUP} -n ${EAST_US_WEBAPP_NAME} + ``` + - Go to your account portal in New Relic to see real-time monitor data. + +* **OverOps** + - Open a console and navigate to `~/web-app`. + - Setup below environment variables + ```shell + export OVEROPSSK= + ``` + - Run below command to build an image with OverOps and push to ACR. + The image name is *web-app-w-overops*. + ```shell + mvn package docker:build@with-overops -DpushImage + ``` + - Run below command to deploy web app to AAS. + ```shell + az webapp config container set -g ${EAST_US_GROUP} \ + -n ${EAST_US_WEBAPP_NAME} \ + --docker-custom-image-name ${ACR_LOGIN_SERVER}/web-app-w-overops \ + --docker-registry-server-url http://${ACR_LOGIN_SERVER} \ + --docker-registry-server-user ${ACR_USERNAME} \ + --docker-registry-server-password ${ACR_PASSWORD} + + az webapp config set -g ${EAST_US_GROUP} \ + -n ${EAST_US_WEBAPP_NAME} \ + --linux-fx-version "DOCKER|${ACR_LOGIN_SERVER}/web-app-w-overops" + + az webapp restart -g ${EAST_US_GROUP} -n ${EAST_US_WEBAPP_NAME} + ``` + - Go to your account portal in OverOps to see real-time diagnostic data. + +* **App Dynamics** + +* **Dynatrace** + + +### Automate continuous integration and continuous deployment (CI/CD) using Jenkins ### + +1. Use an existing Jenkins instance, setup continuous delivery - build and configure pipeline using: + + a. A pipeline config file from the cloned repo + + b. The forked repo + + As part of the [initial build](#create-the-initial-build), a Jenkins cluster with pipelines is setup in a Kubernetes cluster in Azure Container Service. You can see that at: + + ```shell + http://${JENKINS_URL} + ``` + + +1. Jenkins Dashboard: build and deploy development, test and production releases for these environments: + + ```shell + java -jar jenkins-cli.jar -s \ + http://${JENKINS_URL}/ \ + build 'movie-db-pipeline-for-dev' -f -v + ``` + + **NOTE**: Job *movie-db-pipeline-for-dev* is for `dev` environment you created in previous section. + If you want to have `test` and `prod` environments, please create them using below commands. + ```shell + cd ./deployment + source provision.sh --env test + source provision.sh --env prod + ``` + Then you can build the `test` and `prod` environments using similar steps; e.g. `build 'movie-db-pipeline-for-test' -f -v`. + + +### Continue to develop apps and rapidly deploy them ### + +The steps in this section will walk you through the steps to make a simple change to your web app and see those changes reflected on the hom page when you browse to your web app. + +1. Using IntelliJ change the name of the web app in the *~/web-app/src/main/resources/static/index.html* page; for example: + + ```html +

Welcome to My Cool Movie DB on Azure!!!

+ ``` + +1. Using IntelliJ and Maven, build, deploy and test the web app: + + ```shell + mvn spring-boot:run + curl http://localhost:8080/ + ``` + +1. Using IntelliJ and Git, push changes to the forked repo when you are satisfied with the changes: + + ```shell + git push origin master + ``` + +1. Watch Jenkins building and deploying dev and test releases on the Jenkins Dashboard (triggered by GitHub) + + Go to `http://${JENKINS_URL}` + +1. Trigger a new build of job `movie-db-pipeline-for-dev` to deploy your latest changes to `dev` environment. + +1. Once these steps have been completed, you should see your updated title on the home page of your web app. + +### Scale apps ### + +1. Scale out Internet facing web apps + + ```shell + az appservice plan update --number-of-workers 6 \ + --name ${EAST_US_WEBAPP_PLAN} \ + --resource-group ${EAST_US_GROUP} + + az appservice plan update --number-of-workers 6 \ + --name ${WEST_EUROPE_WEBAPP_PLAN} \ + --resource-group ${WEST_EUROPE_GROUP} + ``` + +## Sample Application Summary ## + +In review, this sample application utilized all of the following design concepts and technologies. + +### Web App Design ### + +- It is a Spring Boot app with an embedded Tomcat server +- It can run in Linux Containers in Azure App Service + +- App uses Azure Redis Cache and accesses it using Spring Data Redis + +> **NOTE**: In the future the app secrets will be stored in an Azure Key Vault. + +### Data App Design ### + +- It is a Spring Boot app with an embedded Tomcat server +- It can run in Kubernetes clusters in Azure Container Service + +- App uses Azure Redis Cache and accesses it using Spring Data Redis +- App stores and fetches images into and from Azure Storage +- App stores and reads app data into and from SQL MySQL-as-a-service using Spring JDBC + +> **NOTE**: In the future the app secrets will be stored in an Azure Key Vault. + +### Functions Design ### + +- For now, Node.js is being used instead of Java in this sample application for the Azure functions; this will be updated in the near future +- They are used for independent micro computations +- They are used for linking two disconnected units in a workflow + +## Contributing ## + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/data-app/.gitignore b/data-app/.gitignore new file mode 100644 index 0000000..2e5cbfa --- /dev/null +++ b/data-app/.gitignore @@ -0,0 +1,5 @@ +target/ +.settings/ +logs/ +.classpath +.project diff --git a/data-app/pom.xml b/data-app/pom.xml new file mode 100644 index 0000000..d89135c --- /dev/null +++ b/data-app/pom.xml @@ -0,0 +1,208 @@ + + + 4.0.0 + com.microsoft.azure.java.samples.moviedb + data-app + jar + 0.1.0-SNAPSHOT + + com.microsoft.azure.java.samples.moviedb + movie-db-java-on-azure + 0.1.0-SNAPSHOT + + + 1.8 + 1.8 + 1.8 + ${env.ACR_LOGIN_SERVER} + 3.37.0 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-rest + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.hsqldb + hsqldb + runtime + + + + mysql + mysql-connector-java + + + org.springframework.boot + spring-boot-starter-test + test + + + com.newrelic.agent.java + newrelic-java + ${newrelic.version} + provided + zip + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.6 + + + unpack-zip + package + + unpack + + + + + com.newrelic.agent.java + newrelic-java + ${newrelic.version} + zip + true + ${project.build.directory} + newrelic + + + ${project.build.directory}/newrelic + + + + + + org.springframework.boot + spring-boot-maven-plugin + 1.5.3.RELEASE + + + + repackage + + + + + + com.spotify + docker-maven-plugin + 0.4.11 + + ${env.ACR_NAME} + https://${env.ACR_LOGIN_SERVER} + src/main/docker/base + ${docker.image.prefix}/${project.artifactId} + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + + + + with-new-relic + + build + + + src/main/docker/new-relic + ${docker.image.prefix}/${project.artifactId}-w-new-relic + + ${env.NEW_RELIC_LICENSE_KEY} + ${env.DATAAPP_NEW_RELIC_APP_NAME} + + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + / + ${project.build.directory}/newrelic + newrelic.jar + + + / + ${project.build.directory}/newrelic + newrelic.yml + + + + + + with-overops + + build + + + src/main/docker/overops + ${docker.image.prefix}/${project.artifactId}-w-overops + + ${env.OVEROPSSK} + + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + + + + all + + build + + + src/main/docker/all + ${docker.image.prefix}/${project.artifactId}-w-all + + ${env.NEW_RELIC_LICENSE_KEY} + ${env.DATAAPP_NEW_RELIC_APP_NAME} + ${env.OVEROPSSK} + + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + / + ${project.build.directory}/newrelic + newrelic.jar + + + / + ${project.build.directory}/newrelic + newrelic.yml + + + + + + + + + \ No newline at end of file diff --git a/data-app/src/main/docker/all/Dockerfile b/data-app/src/main/docker/all/Dockerfile new file mode 100644 index 0000000..b248975 --- /dev/null +++ b/data-app/src/main/docker/all/Dockerfile @@ -0,0 +1,23 @@ +FROM centos:7 + +RUN yum install -y java-1.8.0-openjdk.x86_64 +ARG OVEROPSSK="" + +# Takipi installation +RUN curl -Ls /dev/null http://get.takipi.com/takipi-t4c-installer | \ + bash /dev/stdin -i --sk=${OVEROPSSK} + +VOLUME /tmp +ADD data-app-0.1.0-SNAPSHOT.jar app.jar +RUN sh -c 'touch /app.jar' +ADD newrelic.jar newrelic.jar +RUN sh -c 'touch /newrelic.jar' +ADD newrelic.yml newrelic.yml +RUN sh -c 'touch /newrelic.jar' +ARG NEW_RELIC_APP_NAME="" +ENV NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME} +ARG NEW_RELIC_LICENSE_KEY="" +ENV NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY} + +# Connecting the Takipi agent to a Java process +CMD java -javaagent:/newrelic.jar -agentlib:TakipiAgent -jar /app.jar diff --git a/data-app/src/main/docker/base/Dockerfile b/data-app/src/main/docker/base/Dockerfile new file mode 100644 index 0000000..90392f4 --- /dev/null +++ b/data-app/src/main/docker/base/Dockerfile @@ -0,0 +1,6 @@ +FROM frolvlad/alpine-oraclejdk8:slim +VOLUME /tmp +ADD data-app-0.1.0-SNAPSHOT.jar app.jar +RUN sh -c 'touch /app.jar' +ENV JAVA_OPTS="" +ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ] diff --git a/data-app/src/main/docker/new-relic/Dockerfile b/data-app/src/main/docker/new-relic/Dockerfile new file mode 100644 index 0000000..b281b12 --- /dev/null +++ b/data-app/src/main/docker/new-relic/Dockerfile @@ -0,0 +1,14 @@ +FROM frolvlad/alpine-oraclejdk8:slim +VOLUME /tmp +ADD data-app-0.1.0-SNAPSHOT.jar app.jar +RUN sh -c 'touch /app.jar' +ADD newrelic.jar newrelic.jar +RUN sh -c 'touch /newrelic.jar' +ADD newrelic.yml newrelic.yml +RUN sh -c 'touch /newrelic.jar' +ARG NEW_RELIC_APP_NAME="" +ENV NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME} +ARG NEW_RELIC_LICENSE_KEY="" +ENV NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY} +ENV JAVA_OPTS="" +ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -javaagent:/newrelic.jar -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ] diff --git a/data-app/src/main/docker/overops/Dockerfile b/data-app/src/main/docker/overops/Dockerfile new file mode 100644 index 0000000..bd5bf15 --- /dev/null +++ b/data-app/src/main/docker/overops/Dockerfile @@ -0,0 +1,15 @@ +FROM centos:7 + +RUN yum install -y java-1.8.0-openjdk.x86_64 +ARG OVEROPSSK="" + +# Takipi installation +RUN curl -Ls /dev/null http://get.takipi.com/takipi-t4c-installer | \ + bash /dev/stdin -i --sk=${OVEROPSSK} + +VOLUME /tmp +ADD data-app-0.1.0-SNAPSHOT.jar app.jar +RUN sh -c 'touch /app.jar' + +# Connecting the Takipi agent to a Java process +CMD java -agentlib:TakipiAgent -jar /app.jar diff --git a/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/Application.java b/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/Application.java new file mode 100644 index 0000000..307dfd2 --- /dev/null +++ b/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/Application.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.java.samples.moviedb.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Entry point of spring boot application. + */ +@SpringBootApplication +public class Application { + /** + * Main entry point. + * + * @param args the parameters + */ + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/DataRepositoryConfiguration.java b/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/DataRepositoryConfiguration.java new file mode 100644 index 0000000..9cfa4fa --- /dev/null +++ b/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/DataRepositoryConfiguration.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.java.samples.moviedb.api; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurerAdapter; + +/** + * Configure Spring Data REST. + */ +@Configuration +public class DataRepositoryConfiguration extends RepositoryRestConfigurerAdapter { + @Override + public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { + config.exposeIdsFor(Movie.class); + } +} diff --git a/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/Movie.java b/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/Movie.java new file mode 100644 index 0000000..621f327 --- /dev/null +++ b/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/Movie.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.java.samples.moviedb.api; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * Movie entity corresponds to `movies` table. + */ +@Entity +@Table(name = "movies") +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class Movie { + @Id + private Long id; + private String name; + private String description; + private Double rating; + private String imageUri; + + /** + * Get movie id. + * + * @return movie id + */ + public Long getId() { + return this.id; + } + + /** + * Set movie id. + * + * @param id movie id + */ + public void setId(Long id) { + this.id = id; + } + + /** + * Get movie name. + * + * @return movie name + */ + public String getName() { + return this.name; + } + + /** + * Set movie name. + * + * @param name movie name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Get movie description. + * + * @return movie description + */ + public String getDescription() { + return this.description; + } + + /** + * Set movie description. + * + * @param description movie description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Get movie rating. + * + * @return movie rating + */ + public Double getRating() { + return this.rating; + } + + /** + * Set movie rating. + * + * @param rating movie rating + */ + public void setRating(Double rating) { + this.rating = rating; + } + + /** + * Get image uri. + * + * @return image uri + */ + public String getImageUri() { + return this.imageUri; + } + + /** + * Set image uri. + * + * @param imageUri image uri + */ + public void setImageUri(String imageUri) { + this.imageUri = imageUri; + } +} diff --git a/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/MovieRepository.java b/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/MovieRepository.java new file mode 100644 index 0000000..edc70b5 --- /dev/null +++ b/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/MovieRepository.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.java.samples.moviedb.api; + +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +/** + * Movie repository against 'movies' table. + */ +@RepositoryRestResource(collectionResourceRel = "movies", path = "movies") +public interface MovieRepository extends PagingAndSortingRepository { + /** + * Provides find movie by id API. + * + * @param id movie id + * @return movie object + */ + Movie findOne(@Param("id") Long id); +} diff --git a/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/package-info.java b/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/package-info.java new file mode 100644 index 0000000..704f08f --- /dev/null +++ b/data-app/src/main/java/com/microsoft/azure/java/samples/moviedb/api/package-info.java @@ -0,0 +1,10 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +/** + * The package provides REST API for accessing movie db. + */ +package com.microsoft.azure.java.samples.moviedb.api; \ No newline at end of file diff --git a/data-app/src/main/resources/application.properties b/data-app/src/main/resources/application.properties new file mode 100644 index 0000000..fe0acd4 --- /dev/null +++ b/data-app/src/main/resources/application.properties @@ -0,0 +1,13 @@ +spring.datasource.initialize=false +spring.data.rest.basePath=/api/v1 +server.port=${DATA_APP_CONTAINER_PORT} + +spring.datasource.url=${MYSQL_ENDPOINT} +spring.datasource.username=${MYSQL_USERNAME} +spring.datasource.password=${MYSQL_PASSWORD} + +logging.level.root=WARN +logging.level.com.microsoft.azure.java.samples.moviedb=DEBUG +logging.level.org.springframework.data=INFO +logging.level.org.hibernate=ERROR +logging.file=logs/application.log \ No newline at end of file diff --git a/data-app/src/main/resources/application.test.properties b/data-app/src/main/resources/application.test.properties new file mode 100644 index 0000000..12da332 --- /dev/null +++ b/data-app/src/main/resources/application.test.properties @@ -0,0 +1,4 @@ +spring.datasource.initialize=true +spring.jpa.hibernate.ddl-auto="" +spring.data.rest.basePath=/api/v1 +spring.datasource.url=jdbc:hsqldb:mem:testdb;sql.syntax_mys=true \ No newline at end of file diff --git a/data-app/src/main/resources/data.sql b/data-app/src/main/resources/data.sql new file mode 100644 index 0000000..ab48581 --- /dev/null +++ b/data-app/src/main/resources/data.sql @@ -0,0 +1,35 @@ +INSERT INTO movies VALUES (1, 'Inception (2010)', 9.7, 'This is the description.', ''); +INSERT INTO movies VALUES (2, 'Goodfellas (1990)', 9.7, 'This is the description.', ''); +INSERT INTO movies VALUES (3, 'The Usual Suspects (1995)', 9.6, 'This is the description.', ''); +INSERT INTO movies VALUES (4, 'The Matrix (1999)', 8.8, 'This is the description.', ''); +INSERT INTO movies VALUES (5, 'Saving Private Ryan (1998)', 8.5, 'This is the description.', ''); +INSERT INTO movies VALUES (6, 'Cera una volta il West (1968)', 8.5, 'This is the description.', ''); +INSERT INTO movies VALUES (7, 'American History X (1998)', 8.5, 'This is the description.', ''); +INSERT INTO movies VALUES (8, 'Shichinin no samurai (1954)', 8.5, 'This is the description.', ''); +INSERT INTO movies VALUES (9, 'Star Wars (1977)', 8.4, 'This is the description.', ''); +INSERT INTO movies VALUES (10, 'Sen to Chihiro no kamikakushi (2001)', 8.4, 'This is the description.', ''); +INSERT INTO movies VALUES (11, 'The Departed (2006)', 8.4, 'This is the description.', ''); +INSERT INTO movies VALUES (12, 'Rear Window (1954)', 8.4, 'This is the description.', ''); +INSERT INTO movies VALUES (13, 'The Pianist (2002)', 8.4, 'This is the description.', ''); +INSERT INTO movies VALUES (14, 'The Dark Knight (2008)', 8.4, 'This is the description.', ''); +INSERT INTO movies VALUES (15, '12 Angry Men (1957)', 8.4, 'This is the description.', ''); +INSERT INTO movies VALUES (16, 'Modern Times (1936)', 8.1, 'This is the description.', ''); +INSERT INTO movies VALUES (17, 'Schindler s List (1993)', 8.1, 'This is the description.', ''); +INSERT INTO movies VALUES (18, 'The Silence of the Lambs (1991)', 8.1, 'This is the description.', ''); +INSERT INTO movies VALUES (19, 'The Green Mile (1999)', 8.1, 'This is the description.', ''); +INSERT INTO movies VALUES (20, 'Raiders of the Lost Ark (1981)', 8.1, 'This is the description.', ''); +INSERT INTO movies VALUES (21, 'Pulp Fiction (1994)', 8.1, 'This is the description.', ''); +INSERT INTO movies VALUES (22, 'Star Wars: Episode V - The Empire Strikes Back (1980)', 7.7, 'This is the description.', ''); +INSERT INTO movies VALUES (23, 'Forrest Gump (1994)', 7.7, 'This is the description.', ''); +INSERT INTO movies VALUES (24, 'Back to the Future (1985)', 7.5, 'This is the description.', ''); +INSERT INTO movies VALUES (25, 'Gladiator (2000)', 7.5, 'This is the description.', ''); +INSERT INTO movies VALUES (26, 'Memento (2000)', 7.5, 'This is the description.', ''); +INSERT INTO movies VALUES (27, 'Apocalypse Now (1979)', 6.5, 'This is the description.', ''); +INSERT INTO movies VALUES (28, 'It s a Wonderful Life (1946)', 6.5, 'This is the description.', ''); +INSERT INTO movies VALUES (29, 'Interstellar (2014)', 6.5, 'This is the description.', ''); +INSERT INTO movies VALUES (30, 'City Lights (1931)', 5.5, 'This is the description.', ''); +INSERT INTO movies VALUES (31, 'Cidade de Deus (2002)', 5.4, 'This is the description.', ''); +INSERT INTO movies VALUES (32, 'The Godfather (1972)', 5.2, 'This is the description.', ''); +INSERT INTO movies VALUES (33, 'The Godfather: Part II (1974)', 5.0, 'This is the description.', ''); +INSERT INTO movies VALUES (34, 'Casablanca (1942)', 4.5, 'This is the description.', ''); +INSERT INTO movies VALUES (35, 'Whiplash (2014)', 4.5, 'This is the description.', ''); \ No newline at end of file diff --git a/data-app/src/main/resources/schema.sql b/data-app/src/main/resources/schema.sql new file mode 100644 index 0000000..2441a7b --- /dev/null +++ b/data-app/src/main/resources/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE movies ( + id INTEGER NOT NULL, + name VARCHAR(60) NOT NULL, + rating FLOAT DEFAULT NULL, + description TEXT NOT NULL, + image_uri TEXT DEFAULT NULL, + PRIMARY KEY (id) +); \ No newline at end of file diff --git a/data-app/src/test/java/com/microsoft/azure/java/samples/moviedb/api/HttpRequestTest.java b/data-app/src/test/java/com/microsoft/azure/java/samples/moviedb/api/HttpRequestTest.java new file mode 100644 index 0000000..fc06964 --- /dev/null +++ b/data-app/src/test/java/com/microsoft/azure/java/samples/moviedb/api/HttpRequestTest.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.java.samples.moviedb.api; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.client.RestTemplate; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +@TestPropertySource(locations = "classpath:application.test.properties") +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class HttpRequestTest { + + private static final String FIRST_MOVIE_PATH = "/api/v1/movies/1"; + @LocalServerPort + private int port; + @Autowired + private RestTemplateBuilder builder; + private RestTemplate restTemplate; + + @Before + public void setup() throws Exception { + restTemplate = builder.rootUri("http://localhost:" + port).build(); + restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory()); + } + + @Test + public void pingApiV1Uri() throws Exception { + String result = this.restTemplate.getForObject("/api/v1", + String.class); + assertTrue(result.contains("movies")); + } + + @Test + public void getMovie() throws Exception { + Movie firstMovie = this.restTemplate.getForObject(FIRST_MOVIE_PATH, Movie.class); + assertThat(firstMovie.getId(), is(1L)); + assertThat(firstMovie.getName(), is("Inception (2010)")); + assertThat(firstMovie.getDescription(), is("This is the description.")); + assertThat(firstMovie.getRating(), is(9.7)); + assertNull(firstMovie.getImageUri()); + } + + @Test + public void patchMovieRating() throws Exception { + Movie movie = new Movie(); + movie.setRating(0.0); + this.restTemplate.patchForObject(FIRST_MOVIE_PATH, new HttpEntity<>(movie), Void.class); + + Movie patchedMovie = this.restTemplate.getForObject(FIRST_MOVIE_PATH, Movie.class); + assertThat(patchedMovie.getRating(), is(0.0)); + + movie.setRating(9.7); + this.restTemplate.patchForObject(FIRST_MOVIE_PATH, new HttpEntity<>(movie), Void.class); + + Movie restoredMovie = this.restTemplate.getForObject(FIRST_MOVIE_PATH, Movie.class); + assertThat(restoredMovie.getRating(), is(9.7)); + } +} \ No newline at end of file diff --git a/database/data/data.sql b/database/data/data.sql new file mode 100644 index 0000000..61f072a --- /dev/null +++ b/database/data/data.sql @@ -0,0 +1,35 @@ +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (1, "Inception (2010)", 9.7, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (2, "Goodfellas (1990)", 9.7, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (3, "The Usual Suspects (1995)", 9.6, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (4, "The Matrix (1999)", 8.8, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (5, "Saving Private Ryan (1998)", 8.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (6, "C'era una volta il West (1968)", 8.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (7, "American History X (1998)", 8.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (8, "Shichinin no samurai (1954)", 8.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (9, "Star Wars (1977)", 8.4, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (10, "Sen to Chihiro no kamikakushi (2001)", 8.4, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (11, "The Departed (2006)", 8.4, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (12, "Rear Window (1954)", 8.4, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (13, "The Pianist (2002)", 8.4, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (14, "The Dark Knight (2008)", 8.4, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (15, "12 Angry Men (1957)", 8.4, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (16, "Modern Times (1936)", 8.1, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (17, "Schindler's List (1993)", 8.1, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (18, "The Silence of the Lambs (1991)", 8.1, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (19, "The Green Mile (1999)", 8.1, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (20, "Raiders of the Lost Ark (1981)", 8.1, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (21, "Pulp Fiction (1994)", 8.1, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (22, "Star Wars: Episode V - The Empire Strikes Back (1980)", 7.7, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (23, "Forrest Gump (1994)", 7.7, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (24, "Back to the Future (1985)", 7.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (25, "Gladiator (2000)", 7.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (26, "Memento (2000)", 7.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (27, "Apocalypse Now (1979)", 6.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (28, "It's a Wonderful Life (1946)", 6.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (29, "Interstellar (2014)", 6.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (30, "City Lights (1931)", 5.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (31, "Cidade de Deus (2002)", 5.4, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (32, "The Godfather (1972)", 5.2, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (33, "The Godfather: Part II (1974)", 5.0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (34, "Casablanca (1942)", 4.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); +INSERT INTO moviedb.movies (id, name, rating, description) VALUES (35, "Whiplash (2014)", 4.5, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."); \ No newline at end of file diff --git a/database/pom.xml b/database/pom.xml new file mode 100644 index 0000000..186ea14 --- /dev/null +++ b/database/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + com.microsoft.azure.java.samples.moviedb + database-deployment + 0.1.0-SNAPSHOT + + + com.microsoft.azure.java.samples.moviedb + movie-db-java-on-azure + 0.1.0-SNAPSHOT + + + + + org.codehaus.mojo + sql-maven-plugin + 1.5 + + + + mysql + mysql-connector-java + 6.0.6 + + + + + com.mysql.cj.jdbc.Driver + ${env.MYSQL_SERVER_ENDPOINT} + ${env.MYSQL_USERNAME} + ${env.MYSQL_PASSWORD} + + + + + default-cli + + execute + + + true + + ./schema/DDL.sql + ./data/data.sql + + + + + + create-database + + execute + + + true + + ./schema/DDL.sql + + + + + + populate-data + + execute + + + true + + ./data/data.sql + + + + + + + + diff --git a/database/schema/DDL.sql b/database/schema/DDL.sql new file mode 100644 index 0000000..668db14 --- /dev/null +++ b/database/schema/DDL.sql @@ -0,0 +1,13 @@ +DROP DATABASE IF EXISTS moviedb; +CREATE DATABASE moviedb; + +USE moviedb; + +CREATE TABLE `movies` ( + `id` BIGINT(20) unsigned NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + `description` TEXT NOT NULL, + `rating` FLOAT DEFAULT NULL, + `image_uri` TEXT DEFAULT NULL, + PRIMARY KEY (`id`) +); \ No newline at end of file diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 0000000..dfcaaf0 --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,158 @@ +# Development Environment Setup + +The following instructions contain the required steps to install the necessary utilities and setup your development environment in order to run the sample application which is detailed in the parent [README](../README.md) file. + +## STEP 1 - Install the required developer utilities for your operating system ## + +### If you are using a MacOS system ### + +1. Install the **Azure CLI** using the instructions at https://docs.microsoft.com/en-us/cli/azure/install-azure-cli: + +1. Install **kubectl** by running below command: + + ```shell + sudo az acs kubernetes install-cli + ``` + +1. Install **Homebrew** from https://brew.sh/ + +1. Install **jq** using Homebrew: + + ```shell + brew install jq + ``` + +1. Install **gettext** using Homebrew: + + ```shell + brew install gettext + brew link --force gettext + ``` + +1. Install **maven** using Homebrew: + + ```shell + brew install maven + ``` + +### If you are using a Windows system ### + +1. Install the **Azure CLI** using the instructions at https://docs.microsoft.com/en-us/cli/azure/install-azure-cli. + +1. Install **kubectl** by running below Azure CLI command with administrator privilege: + + ```shell + az acs kubernetes install-cli + ``` + +1. Install [Chocolatey](https://chocolatey.org/). + +1. Install **[Maven](http://maven.apache.org/)** using Chocolatey: + + ```shell + choco install Maven + ``` + +1. Install **jq** using Chocolatey: + + ```shell + choco install jq + ``` + +## STEP 2 - Clone the sample application and customize it for your environment ## + +1. Open https://github.com/Microsoft/movie-db-java-on-azure in a web browser and create a private fork of the sample application. + +1. Open a console window and clone your forked repo on your local system: + + ```shell + git clone https://github.com//movie-db-java-on-azure + ``` + +1. Open the `~/deployment/config.json` in a text editor: + + a. Locate the following configuration settings, and modify the `value` of the GITHUB_REPO_OWNER key/value pair with your GitHub account name: + + ```json + "repo": [ + { + "comment": "GitHub account name.", + "key": "GITHUB_REPO_OWNER", + "value": "" + }, + { + "comment": "GitHub repository name.", + "key": "GITHUB_REPO_NAME", + "value": "movie-db-java-on-azure" + } + ], + ``` + + b. Locate the following configuration settings, and modify the `value` of the GROUP_SUFFIX key/value pair with a unique ID; for example "123456": + + ```json + "optional": [ + { + "comment": "Optional resource group suffix to avoid naming conflict.", + "key": "GROUP_SUFFIX", + "value": "" + } + ] + ``` + + c. Save and close the `~/deployment/config.json` file. + Suggest to commit these changes to GitHub so that it won't be lost. + +1. Login to your Azure account and specify which subscription to use: + + ```shell + az login + az account set --subscription "" + ``` + + **NOTE**: You can use either a subscription name or id when specifying which subscription to use; to obtain a list of your subscriptions, type `az account list`. + +1. Setup environment variables for passwords. + ```shell + export MYSQL_PASSWORD= + export JENKINS_PASSWORD= + ``` + + `MYSQL_PASSWORD` will be used to create a MySQL database in Azure. It has to satisfy certain complexity. + Default MySQL admin username is `AzureUser`. You can change it in `~/deployment/config.json`. + + `JENKINS_PASSWORD` will be used to deploy a Jenkins cluster in ACS. Jenkins admin username is `jenkins`. + + \ No newline at end of file diff --git a/deployment/arm/container-registry.json b/deployment/arm/container-registry.json new file mode 100644 index 0000000..ff1fd0c --- /dev/null +++ b/deployment/arm/container-registry.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "acrName": { + "type": "string", + "defaultValue": "[concat(uniqueString(resourceGroup().id), 'acr')]", + "minLength": 5, + "maxLength": 50, + "metadata": { + "description": "Name of your Azure Container Registry" + } + }, + "acrAdminUserEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable admin user that have push / pull permission to the registry." + } + }, + "acrStorageType": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_ZRS", + "Standard_GRS" + ], + "metadata": { + "description": "Type of the storage account for container registry." + } + } + }, + "variables": { + "acrStorageName": "[concat(uniqueString(resourceGroup().id), 'acr')]", + "acrStorageId": "[resourceId('Microsoft.Storage/storageAccounts', variables('acrStorageName'))]" + }, + "resources": [ + { + "name": "[variables('acrStorageName')]", + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2015-06-15", + "location": "[resourceGroup().location]", + "comments": "This storage account is used by Container Registry for storing its datas.", + "dependsOn": [], + "tags": { + "displayName": "ACR Image's storage", + "container.registry": "[parameters('acrName')]" + }, + "properties": { + "accountType": "[parameters('acrStorageType')]" + } + }, + { + "name": "[parameters('acrName')]", + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2016-06-27-preview", + "location": "[resourceGroup().location]", + "comments": "Container registry for storing docker images", + "dependsOn": [ + "[variables('acrStorageId')]" + ], + "tags": { + "displayName": "Container Registry", + "container.registry": "[parameters('acrName')]" + }, + "properties": { + "adminUserEnabled": "[parameters('acrAdminUserEnabled')]", + "storageAccount": { + "accessKey": "[listKeys(variables('acrStorageId'),'2015-06-15').key1]", + "name": "[variables('acrStorageName')]" + } + } + } + ], + "outputs": { + "acrLoginServer": { + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries',parameters('acrName')),'2016-06-27-preview').loginServer]", + "type": "string" + } + } +} \ No newline at end of file diff --git a/deployment/arm/database.json b/deployment/arm/database.json new file mode 100644 index 0000000..ff0f4b7 --- /dev/null +++ b/deployment/arm/database.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "administratorLogin": { + "type": "string" + }, + "administratorLoginPassword": { + "type": "securestring" + }, + "location": { + "type": "string", + "defaultValue": "westus" + } + }, + "variables": { + "serverName": "[concat(uniqueString(resourceGroup().id), '-database')]", + "mysqlApiVersion": "2016-02-01-privatepreview" + }, + "resources": [ + { + "type": "Microsoft.DBforMySQL/servers", + "apiVersion": "[variables('mysqlApiVersion')]", + "name": "[variables('serverName')]", + "location": "[parameters('location')]", + "kind": "", + "properties": { + "version": "5.6", + "administratorLogin": "[parameters('administratorLogin')]", + "administratorLoginPassword": "[parameters('administratorLoginPassword')]", + "storageMB": 51200, + "sslEnforcement": "Disabled" + }, + "sku": { + "name": "MYSQLS2M50", + "tier": "Basic", + "capacity": 50, + "size": 51200, + "family": "SkuFamily" + }, + "resources": [ + { + "apiVersion": "[variables('mysqlApiVersion')]", + "type": "firewallRules", + "name": "all", + "dependsOn": [ + "[resourceId('Microsoft.DBforMySQL/servers', variables('serverName'))]" + ], + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "255.255.255.255" + } + } + ] + } + ] +} diff --git a/deployment/arm/function-app.json b/deployment/arm/function-app.json new file mode 100644 index 0000000..b16f343 --- /dev/null +++ b/deployment/arm/function-app.json @@ -0,0 +1,171 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "inputContainerName": { + "type": "string", + "defaultValue": "images-original", + "metadata": { + "description": "Name of the input blob container for function app." + } + }, + "outputContainerName": { + "type": "string", + "defaultValue": "images-thumbnail", + "metadata": { + "description": "Name of the output blob container for function app." + } + }, + "storageType": { + "type": "string", + "allowedValues": [ + "Standard_LRS", + "Standard_ZRS", + "Standard_GRS" + ], + "defaultValue": "Standard_LRS", + "metadata": { + "description": "Type of the storage account that will store data." + } + } + }, + "variables": { + "location": "[resourceGroup().location]", + "storageVersion": "2015-06-15", + "appServiceApiVersion": "2015-08-01", + "functionStorageName": "[concat(uniqueString(resourceGroup().id), 'func')]", + "functionAppPlanName": "[concat(uniqueString(resourceGroup().id), '-func-app-plan')]", + "functionAppName": "[concat(uniqueString(resourceGroup().id), '-func-app')]", + "resizeImageFunction": "ResizeImage", + "imageStorageName": "[concat(uniqueString(resourceGroup().id), 'image')]", + "repoUrl": "https://github.com/microsoft/movie-db-java-on-azure/", + "branch": "master" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('functionStorageName')]", + "apiVersion": "[variables('storageVersion')]", + "location": "[variables('location')]", + "properties": { + "accountType": "[parameters('storageType')]" + }, + "tags": { + "displayName": "Function App Storage" + } + }, + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('imageStorageName')]", + "apiVersion": "[variables('storageVersion')]", + "location": "[variables('location')]", + "properties": { + "accountType": "[parameters('storageType')]" + }, + "tags": { + "displayName": "Image Storage" + } + }, + { + "type": "Microsoft.Web/serverfarms", + "name": "[variables('functionAppPlanName')]", + "kind": "app", + "apiVersion": "[variables('appServiceApiVersion')]", + "location": "[variables('location')]", + "sku": { + "name": "B1", + "tier": "Basic", + "size": "B1", + "family": "B", + "capacity": 1 + }, + "properties": { + "name": "[variables('functionAppPlanName')]", + "numberOfWorkers": 1 + }, + "dependsOn": [] + }, + { + "type": "Microsoft.Web/sites", + "name": "[variables('functionAppName')]", + "kind": "functionapp", + "apiVersion": "[variables('appServiceApiVersion')]", + "location": "[variables('location')]", + "properties": { + "name": "[variables('functionAppName')]", + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('functionAppPlanName'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('functionStorageName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('imageStorageName'))]", + "[resourceId('Microsoft.Web/serverfarms', variables('functionAppPlanName'))]" + ], + "resources": [ + { + "type": "config", + "name": "appsettings", + "apiVersion": "[variables('appServiceApiVersion')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]" + ], + "properties": { + "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('functionStorageName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('functionStorageName')), variables('storageVersion')).key1,';')]", + "AzureWebJobsDashboard": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('functionStorageName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('functionStorageName')), variables('storageVersion')).key1,';')]", + "FUNCTIONS_EXTENSION_VERSION": "latest", + "WEBSITE_NODE_DEFAULT_VERSION": "6.5.0", + "STORAGE_CONNECTION_STRING": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('imageStorageName'),';AccountKey=',listkeys(resourceId('Microsoft.Storage/storageAccounts', variables('imageStorageName')), variables('storageVersion')).key1,';')]" + } + }, + { + "type": "sourcecontrols", + "name": "web", + "apiVersion": "[variables('appServiceApiVersion')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('functionAppName'))]", + "[resourceId('Microsoft.Web/sites/config', variables('functionAppName'), 'appsettings')]" + ], + "properties": { + "RepoUrl": "[variables('repoUrl')]", + "branch": "[variables('branch')]", + "IsManualIntegration": true + } + }, + { + "type": "functions", + "name": "[variables('resizeImageFunction')]", + "apiVersion": "[variables('appServiceApiVersion')]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('functionAppName'))]", + "[resourceId('Microsoft.Web/Sites/config', variables('functionAppName'), 'appsettings')]", + "[resourceId('Microsoft.Web/Sites/sourcecontrols', variables('functionAppName'), 'web')]" + ], + "properties": { + "config": { + "bindings": [ + { + "type": "blobTrigger", + "name": "inputBlob", + "dataType": "binary", + "path": "[concat(parameters('inputContainerName'), '/{name}')]", + "connection": "STORAGE_CONNECTION_STRING", + "direction": "in" + }, + { + "type": "blob", + "name": "outputBlob", + "path": "[concat(parameters('outputContainerName'), '/{name}')]", + "connection": "STORAGE_CONNECTION_STRING", + "direction": "out" + } + ] + } + } + } + ], + "tags": { + "displayName": "Function App" + } + } + ], + "outputs": {} +} diff --git a/deployment/arm/linux-webapp.json b/deployment/arm/linux-webapp.json new file mode 100644 index 0000000..ae7b3d2 --- /dev/null +++ b/deployment/arm/linux-webapp.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "type": "string", + "defaultValue": "[concat(uniqueString(resourceGroup().id), '-web-app')]" + }, + "location": { + "type": "string", + "defaultValue": "West US" + } + }, + "variables": { + "linuxAppPlanName": "[concat(uniqueString(resourceGroup().id), '-linux-app-plan')]" + }, + "resources": [ + { + "type": "Microsoft.Web/sites", + "kind": "app", + "name": "[parameters('name')]", + "apiVersion": "2016-03-01", + "location": "[parameters('location')]", + "properties": { + "name": "[parameters('name')]", + "serverFarmId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourcegroups/', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', variables('linuxAppPlanName'))]", + "hostingEnvironment": "" + }, + "tags": { + "[concat('hidden-related:', '/subscriptions/', subscription().subscriptionId, '/resourcegroups/', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', variables('linuxAppPlanName'))]": "empty" + }, + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', variables('linuxAppPlanName'))]" + ] + }, + { + "type": "Microsoft.Web/serverfarms", + "kind": "linux", + "name": "[variables('linuxAppPlanName')]", + "apiVersion": "2016-09-01", + "sku": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "location": "[parameters('location')]", + "properties": { + "name": "[variables('linuxAppPlanName')]", + "numberOfWorkers": "1", + "workerSizeId": "0", + "reserved": true + } + } + ] +} \ No newline at end of file diff --git a/deployment/arm/master.json b/deployment/arm/master.json new file mode 100644 index 0000000..fb0b9bd --- /dev/null +++ b/deployment/arm/master.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "administratorLogin": { + "type": "string" + }, + "administratorLoginPassword": { + "type": "securestring" + } + }, + "variables": {}, + "resources": [ + { + "apiVersion": "2015-01-01", + "name": "container-registry", + "type": "Microsoft.Resources/deployments", + "properties": { + "mode": "incremental", + "templateLink": { + "uri": "https://javaonazure.blob.core.windows.net/deployment/container-registry.json", + "contentVersion": "1.0.0.0" + } + } + }, + { + "apiVersion": "2015-01-01", + "name": "database", + "type": "Microsoft.Resources/deployments", + "properties": { + "mode": "incremental", + "templateLink": { + "uri": "https://javaonazure.blob.core.windows.net/deployment/database.json", + "contentVersion": "1.0.0.0" + }, + "parameters": { + "administratorLogin": { + "value": "[parameters('administratorLogin')]" + }, + "administratorLoginPassword": { + "value": "[parameters('administratorLoginPassword')]" + } + } + } + }, + { + "apiVersion": "2015-01-01", + "name": "redis", + "type": "Microsoft.Resources/deployments", + "properties": { + "mode": "incremental", + "templateLink": { + "uri": "https://javaonazure.blob.core.windows.net/deployment/redis.json", + "contentVersion": "1.0.0.0" + } + } + }, + { + "apiVersion": "2015-01-01", + "name": "function", + "type": "Microsoft.Resources/deployments", + "properties": { + "mode": "incremental", + "templateLink": { + "uri": "https://javaonazure.blob.core.windows.net/deployment/function-app.json", + "contentVersion": "1.0.0.0" + } + } + }, + { + "apiVersion": "2015-01-01", + "name": "traffic-manager", + "type": "Microsoft.Resources/deployments", + "properties": { + "mode": "incremental", + "templateLink": { + "uri": "https://javaonazure.blob.core.windows.net/deployment/traffic-manager.json", + "contentVersion": "1.0.0.0" + } + } + } + ] +} diff --git a/deployment/arm/redis.json b/deployment/arm/redis.json new file mode 100644 index 0000000..61d4b9e --- /dev/null +++ b/deployment/arm/redis.json @@ -0,0 +1,70 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", + "contentVersion": "1.0.0.0", + "parameters": { + "redisCacheName": { + "type": "string", + "defaultValue": "[concat(uniqueString(resourceGroup().id), '-redis')]", + "metadata": { + "description": "The name of the Azure Redis Cache to create." + } + }, + "redisCacheSKU": { + "type": "string", + "allowedValues": [ + "Basic", + "Standard" + ], + "defaultValue": "Basic", + "metadata": { + "description": "The pricing tier of the new Azure Redis Cache." + } + }, + "redisCacheFamily": { + "type": "string", + "defaultValue": "C", + "metadata": { + "description": "The family for the sku." + } + }, + "redisCacheCapacity": { + "type": "int", + "allowedValues": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6 + ], + "defaultValue": 0, + "metadata": { + "description": "The size of the new Azure Redis Cache instance. " + } + }, + "enableNonSslPort": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "A boolean value that indicates whether to allow access via non-SSL ports." + } + } + }, + "resources": [ + { + "apiVersion": "2015-08-01", + "name": "[parameters('redisCacheName')]", + "type": "Microsoft.Cache/Redis", + "location": "[resourceGroup().location]", + "properties": { + "enableNonSslPort": "[parameters('enableNonSslPort')]", + "sku": { + "capacity": "[parameters('redisCacheCapacity')]", + "family": "[parameters('redisCacheFamily')]", + "name": "[parameters('redisCacheSKU')]" + } + } + } + ] +} diff --git a/deployment/arm/traffic-manager.json b/deployment/arm/traffic-manager.json new file mode 100644 index 0000000..a5e81cb --- /dev/null +++ b/deployment/arm/traffic-manager.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "data-app-port": { + "type": "string", + "defaultValue": "80" + }, + "data-app-path": { + "type": "string", + "defaultValue": "/" + }, + "web-app-port": { + "type": "string", + "defaultValue": "80" + }, + "web-app-path": { + "type": "string", + "defaultValue": "/" + } + }, + "resources": [ + { + "apiVersion": "2017-03-01", + "type": "Microsoft.Network/trafficmanagerprofiles", + "name": "[concat(uniqueString(resourceGroup().id), '-data-app-trafficmanager')]", + "location": "global", + "properties": { + "trafficRoutingMethod": "Performance", + "dnsConfig": { + "relativeName": "[concat(uniqueString(resourceGroup().id), '-data-app-trafficmanager')]", + "ttl": "300" + }, + "monitorConfig": { + "protocol": "http", + "port": "[parameters('data-app-port')]", + "path": "[parameters('data-app-path')]" + } + } + }, + { + "apiVersion": "2017-03-01", + "type": "Microsoft.Network/trafficmanagerprofiles", + "name": "[concat(uniqueString(resourceGroup().id), '-web-app-trafficmanager')]", + "location": "global", + "properties": { + "trafficRoutingMethod": "Performance", + "dnsConfig": { + "relativeName": "[concat(uniqueString(resourceGroup().id), '-web-app-trafficmanager')]", + "ttl": "300" + }, + "monitorConfig": { + "protocol": "http", + "port": "[parameters('web-app-port')]", + "path": "[parameters('web-app-path')]" + } + } + } + ] +} diff --git a/deployment/config.json b/deployment/config.json new file mode 100644 index 0000000..3226e06 --- /dev/null +++ b/deployment/config.json @@ -0,0 +1,118 @@ +{ + "repo": [ + { + "comment": "GitHub account name.", + "key": "GITHUB_REPO_OWNER", + "value": "microsoft" + }, + { + "comment": "GitHub repository name.", + "key": "GITHUB_REPO_NAME", + "value": "movie-db-java-on-azure" + } + ], + + "env": [ + { + "comment": "Target cloud environment.", + "key": "TARGET_ENV", + "value": "dev" + } + ], + + "resourceGroup": [ + { + "comment": "Resource group name for Jenkins.", + "key": "JENKINS_GROUP", + "value": "JenkinsGroup" + }, + { + "comment": "Resource group name for non-regional resources.", + "key": "COMMON_GROUP", + "value": "CommonGroup" + }, + { + "comment": "Resource group name for East US region.", + "key": "EAST_US_GROUP", + "value": "EastUSGroup" + }, + { + "comment": "Resource group name for West Europe region.", + "key": "WEST_EUROPE_GROUP", + "value": "WestEuropeGroup" + } + ], + + "location": [ + { + "comment": "East US region.", + "key": "EAST_US", + "value": "eastus" + }, + { + "comment": "West Europe region.", + "key": "WEST_EUROPE", + "value": "westeurope" + } + ], + + "mySql": [ + { + "comment": "Admin username for MySQL server.", + "key": "MYSQL_ADMIN_USERNAME", + "value": "AzureUser" + } + ], + + "webApp": [ + { + "comment": "Port number that web app listens at.", + "key": "WEB_APP_CONTAINER_PORT", + "value": "8080" + }, + { + "comment": "Original image container name in Azure storage.", + "key": "ORIGINAL_IMAGE_CONTAINER", + "value": "images-original" + }, + { + "comment": "Thumbnail image container name in Azure storage.", + "key": "THUMBNAIL_IMAGE_CONTAINER", + "value": "images-thumbnail" + } + ], + + "dataApp": [ + { + "comment": "Port number that data app listens at.", + "key": "DATA_APP_CONTAINER_PORT", + "value": "8090" + } + ], + + "misc": [ + { + "comment": "Azure container service name.", + "key": "ACS_NAME", + "value": "acs" + } + ], + + "optional": [ + { + "comment": "Optional facebook app id. Should be paired with FACEBOOK_APP_SECRET.", + "key": "FACEBOOK_APP_ID", + "value": "" + }, + { + "comment": "Optional facebook app secret. Should be paired with FACEBOOK_APP_ID.", + "key": "FACEBOOK_APP_SECRET", + "value": "" + }, + { + "comment": "Optional resource group suffix to avoid naming conflict.", + "key": "GROUP_SUFFIX", + "value": "" + } + ] +} diff --git a/deployment/data-app/deploy.yaml b/deployment/data-app/deploy.yaml new file mode 100644 index 0000000..04414cb --- /dev/null +++ b/deployment/data-app/deploy.yaml @@ -0,0 +1,82 @@ +--- +apiVersion: "v1" +kind: "List" +items: + + - apiVersion: "v1" + kind: "Namespace" + metadata: + name: ${TARGET_ENV} + labels: + name: ${TARGET_ENV} + + - apiVersion: "extensions/v1beta1" + kind: "Deployment" + metadata: + name: "data-app" + namespace: ${TARGET_ENV} + labels: + name: "data-app" + spec: + replicas: 1 + template: + metadata: + labels: + name: "data-app" + spec: + containers: + - name: "data-app" + image: "${ACR_LOGIN_SERVER}/data-app:latest" + ports: + - containerPort: ${DATA_APP_CONTAINER_PORT} + resources: + limits: + cpu: 1 + memory: 4Gi + requests: + cpu: 1 + memory: 2Gi + env: + - name: MYSQL_USERNAME + valueFrom: + secretKeyRef: + name: my-secrets + key: mysqlUsername + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: my-secrets + key: mysqlPassword + - name: MYSQL_ENDPOINT + valueFrom: + secretKeyRef: + name: my-secrets + key: mysqlEndpoint + - name: DATA_APP_CONTAINER_PORT + value: "${DATA_APP_CONTAINER_PORT}" + livenessProbe: + httpGet: + path: /api/v1 + port: ${DATA_APP_CONTAINER_PORT} + initialDelaySeconds: 60 + timeoutSeconds: 5 + imagePullSecrets: + - name: ${ACR_LOGIN_SERVER} + securityContext: + fsGroup: 1000 + + - apiVersion: "v1" + kind: "Service" + metadata: + name: "data-app" + namespace: ${TARGET_ENV} + spec: + type: "LoadBalancer" + selector: + name: "data-app" + ports: + - + name: "http" + port: 80 + targetPort: ${DATA_APP_CONTAINER_PORT} + protocol: "TCP" diff --git a/deployment/deprovision.sh b/deployment/deprovision.sh new file mode 100755 index 0000000..e889cd8 --- /dev/null +++ b/deployment/deprovision.sh @@ -0,0 +1,53 @@ +#! /bin/bash +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# +# Utility script to de-provision Azure resources + +# Source library +source lib.sh + +# Check required tools. Exit if requirements aren't satisfied. +check_required_tools +[[ $? -ne 0 ]] && exit 1 + +# Load config.json and export environment variables +load_config + +export TEARDOWN_NO_WAIT=true +# Parse command line arguments +parse_teardown_args "$@" +[[ $? -ne 0 ]] && exit 1 + +# Prefix resource group names with target environment +c_group=${TARGET_ENV}${COMMON_GROUP}${GROUP_SUFFIX} +e_us_group=${TARGET_ENV}${EAST_US_GROUP}${GROUP_SUFFIX} +w_eu_group=${TARGET_ENV}${WEST_EUROPE_GROUP}${GROUP_SUFFIX} +jenkins_group=${JENKINS_GROUP}${GROUP_SUFFIX} + +# Delete resource groups in parallel +log_info "Start deleting resource group ${c_group}..." +az group delete -y -n ${c_group} --no-wait + +log_info "\nStart deleting resource group ${e_us_group}..." +az group delete -y -n ${e_us_group} --no-wait + +log_info "\nStart deleting resource group ${w_eu_group}..." +az group delete -y -n ${w_eu_group} --no-wait + +log_info "\nStart deleting resource group ${jenkins_group}..." +az group delete -y -n ${jenkins_group} --no-wait + +# Wait for completion if called with '--wait' +if [ "${TEARDOWN_NO_WAIT}" != "true" ]; then + log_info "\nWait for delete completion..." + + az group wait -n ${c_group} --deleted + az group wait -n ${e_us_group} --deleted + az group wait -n ${w_eu_group} --deleted + az group wait -n ${jenkins_group} --deleted + + log_info "\nAll deleted." +fi diff --git a/deployment/dev_setup.sh b/deployment/dev_setup.sh new file mode 100644 index 0000000..b790222 --- /dev/null +++ b/deployment/dev_setup.sh @@ -0,0 +1,55 @@ +#! /bin/bash +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# +# Setup environment variables for development + +# Source library +source lib.sh + +# Check whether script is running in a sub-shell +if [ "${BASH_SOURCE}" == "$0" ]; then + log_error '"dev_setup.sh" should be sourced. Run "source dev_setup.sh" or ". dev_setup.sh"' + exit 1 +fi + +# Check required tools. Exit if requirements aren't satisfied. +check_required_tools +[[ $? -ne 0 ]] && return 1 + +# Load config.json and export environment variables +load_config + +# Parse command line arguments +parse_args "$@" +[[ $? -ne 0 ]] && return 1 + +# Check required environment variables +check_required_env_vars +[[ $? -ne 0 ]] && return 1 + +# Hard code target env to 'dev' +export TARGET_ENV=dev + +# Prefix resource group name with 'dev' +export COMMON_GROUP=${TARGET_ENV}${COMMON_GROUP}${GROUP_SUFFIX} +export EAST_US_GROUP=${TARGET_ENV}${EAST_US_GROUP}${GROUP_SUFFIX} +export WEST_EUROPE_GROUP=${TARGET_ENV}${WEST_EUROPE_GROUP}${GROUP_SUFFIX} +export JENKINS_GROUP=${JENKINS_GROUP}${GROUP_SUFFIX} + +export_acr_details ${COMMON_GROUP} + +export_database_details ${COMMON_GROUP} + +export_redis_details ${COMMON_GROUP} + +export_image_storage ${COMMON_GROUP} + +export_webapp_details ${EAST_US_GROUP} EAST_US +export_webapp_details ${WEST_EUROPE_GROUP} WEST_EUROPE + +export_jenkins_url ${JENKINS_GROUP} ${ACS_NAME} + +export_data_api_url ${TARGET_ENV} ${EAST_US_GROUP} diff --git a/deployment/function/deploy.cmd b/deployment/function/deploy.cmd new file mode 100644 index 0000000..c6d9a21 --- /dev/null +++ b/deployment/function/deploy.cmd @@ -0,0 +1,119 @@ +@if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off + +:: ---------------------- +:: KUDU Deployment Script with Functions Package restore +:: Version: 1.0.6 +:: ---------------------- + +:: Prerequisites +:: ------------- + +:: Verify node.js installed +where node 2>nul >nul +IF %ERRORLEVEL% NEQ 0 ( + echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment. + goto error +) + +:: Setup +:: ----- + +setlocal enabledelayedexpansion + +SET ARTIFACTS=%~dp0%..\..\..\artifacts + +IF NOT DEFINED DEPLOYMENT_SOURCE ( + SET DEPLOYMENT_SOURCE=%~dp0%..\.. +) + +IF NOT DEFINED DEPLOYMENT_TARGET ( + SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot +) + +IF NOT DEFINED NEXT_MANIFEST_PATH ( + SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest + + IF NOT DEFINED PREVIOUS_MANIFEST_PATH ( + SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest + ) +) + +IF NOT DEFINED KUDU_SYNC_CMD ( + :: Install kudu sync + echo Installing Kudu Sync + call npm install kudusync -g --silent + IF !ERRORLEVEL! NEQ 0 goto error + + :: Locally just running "kuduSync" would also work + SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd +) + +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +:: Deployment +:: ---------- + +echo Handling Basic Web Site deployment. + +:: KuduSync +IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" ( + call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_SOURCE%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.deployment;.gitignore;jenkins-cli.jar;Jenkinsfile;LICENSE;pom.xml;README.md;data-app;database;deployment;media;web-app;" + IF !ERRORLEVEL! NEQ 0 goto error +) + + +echo Handling Function deployment. + +cd "%DEPLOYMENT_TARGET%" + +:: copy function app files from functions folder, then remove functions folder +robocopy /S function\ResizeImage .\ResizeImage +rd /S /Q function + +:: npm packages install +echo "Looking for Node package.json files to install..." +FOR /F %%d in ('DIR /a:d /B') DO ( + echo cd %%d + cd %%d + + IF EXIST "package.json" ( + Echo Installing packages for %%d + call npm install + ) + + cd .. +) + +:: NuGet package restore +echo "Restoring function packages" + +FOR /F %%d in ('DIR "Project.json" /S /B') DO ( + call nuget restore %%d -PackagesDirectory %home%\data\Functions\packages\nuget +) + + +:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: +goto end + +:: Execute command routine that will echo out when error +:ExecuteCmd +setlocal +set _CMD_=%* +call %_CMD_% +if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_% +exit /b %ERRORLEVEL% + +:error +endlocal +echo An error has occurred during web site deployment. +call :exitSetErrorLevel +call :exitFromFunction 2>nul + +:exitSetErrorLevel +exit /b 1 + +:exitFromFunction +() + +:end +endlocal +echo Finished successfully. \ No newline at end of file diff --git a/deployment/jenkins/azureutil.groovy b/deployment/jenkins/azureutil.groovy new file mode 100644 index 0000000..043e233 --- /dev/null +++ b/deployment/jenkins/azureutil.groovy @@ -0,0 +1,170 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + + +@NonCPS +def jsonParse(def json) { + new groovy.json.JsonSlurperClassic().parseText(json) +} + +def prepareEnv(String targetEnv) { + /** + * Parse config.json + */ + def rawConfig = jsonParse(readFile('./deployment/config.json')) + this.config = [:] + for (category in rawConfig) { + for (item in category.value) { + this.config.put(item.key, item.value) + } + } + + if (env.GROUP_SUFFIX != null) { + this.config['GROUP_SUFFIX'] = "${env.GROUP_SUFFIX}" + } + + this.config['COMMON_GROUP'] = "${targetEnv}${config.COMMON_GROUP}${config.GROUP_SUFFIX}" + this.config['EAST_US_GROUP'] = "${targetEnv}${config.EAST_US_GROUP}${config.GROUP_SUFFIX}" + this.config['WEST_EUROPE_GROUP'] = "${targetEnv}${config.WEST_EUROPE_GROUP}${config.GROUP_SUFFIX}" + + /** + * Azure CLI login + */ + sh ''' + client_id=$(cat /etc/kubernetes/azure.json | python -c "import sys, json; print json.load(sys.stdin)['aadClientId']") + client_secret=$(cat /etc/kubernetes/azure.json | python -c "import sys, json; print json.load(sys.stdin)['aadClientSecret']") + tenant_id=$(cat /etc/kubernetes/azure.json | python -c "import sys, json; print json.load(sys.stdin)['tenantId']") + az login --service-principal -u ${client_id} -p ${client_secret} --tenant ${tenant_id} + ''' + + this.acrName = sh( + script: "az acr list -g ${config.COMMON_GROUP} --query [0].name | tr -d '\"'", + returnStdout: true + ).trim() + + if (this.acrName.length() == 0) { + error('Azure Container Registry not found.') + } + + this.acrLoginServer = sh( + script: "az acr show -g ${config.COMMON_GROUP} -n ${acrName} --query loginServer | tr -d '\"'", + returnStdout: true + ).trim() + this.acrUsername = sh( + script: "az acr credential show -g ${config.COMMON_GROUP} -n ${acrName} --query username | tr -d '\"'", + returnStdout: true + ).trim() + this.acrPassword = sh( + script: "az acr credential show -g ${config.COMMON_GROUP} -n ${acrName} --query passwords[0].value | tr -d '\"'", + returnStdout: true + ).trim() +} + + +def deployFunction() { + sh """ + # Storage connection for images + storage_name=\$(az storage account list -g ${config.COMMON_GROUP} --query [2].name | tr -d '"') + storage_conn_str=\$(az storage account show-connection-string -g ${config.COMMON_GROUP} -n \${storage_name} --query connectionString | tr -d '"') + + function_id=\$(az functionapp list -g ${config.COMMON_GROUP} --query [0].id | tr -d '"') + az functionapp config appsettings set --ids \${function_id} --settings STORAGE_CONNECTION_STRING=\${storage_conn_str} + az functionapp deployment source sync --ids \${function_id} + """ +} + +def deployWebApp(String resGroup) { + sh """ + data_api_endpoint=\$(az network traffic-manager profile list -g ${config.COMMON_GROUP} --query [0].dnsConfig.fqdn | tr -d '"') + webapp_id=\$(az resource list -g ${resGroup} --resource-type Microsoft.Web/sites --query [0].id | tr -d '"') + + # Storage connection for images + storage_name=\$(az storage account list -g ${config.COMMON_GROUP} --query [2].name | tr -d '"') + storage_conn_str=\$(az storage account show-connection-string -g ${config.COMMON_GROUP} -n \${storage_name} --query connectionString | tr -d '"') + + # Redis credentials + redis_name=\$(az redis list -g ${config.COMMON_GROUP} --query [0].name | tr -d '"') + redis_host=\$(az redis show -g ${config.COMMON_GROUP} -n \${redis_name} --query hostName | tr -d '"') + redis_password=\$(az redis list-keys -g ${config.COMMON_GROUP} -n \${redis_name} --query primaryKey | tr -d '"') + + az webapp config container set --ids \${webapp_id} \\ + --docker-custom-image-name ${acrLoginServer}/web-app \\ + --docker-registry-server-url http://${acrLoginServer} \\ + --docker-registry-server-user ${acrUsername} \\ + --docker-registry-server-password ${acrPassword} + az webapp config set --ids \${webapp_id} --linux-fx-version "DOCKER|${acrLoginServer}/web-app" + az webapp config appsettings set --ids \${webapp_id} \\ + --settings DATA_API_URL=\${data_api_endpoint} \\ + PORT=${config.WEB_APP_CONTAINER_PORT} \\ + WEB_APP_CONTAINER_PORT=${config.WEB_APP_CONTAINER_PORT} \\ + STORAGE_CONNECTION_STRING=\${storage_conn_str} \\ + ORIGINAL_IMAGE_CONTAINER=${config.ORIGINAL_IMAGE_CONTAINER} \\ + THUMBNAIL_IMAGE_CONTAINER=${config.THUMBNAIL_IMAGE_CONTAINER} \\ + REDIS_HOST=\${redis_host} \\ + REDIS_PASSWORD=\${redis_password} + az webapp restart --ids \${webapp_id} + + # Add web-app endpoint to traffic manager + traffic_manager_name=\$(az resource list -g ${config.COMMON_GROUP} --resource-type Microsoft.Network/trafficManagerProfiles --query [1].name | tr -d '"') + if [ -z "\$(az network traffic-manager endpoint show -g ${config.COMMON_GROUP} --profile-name \${traffic_manager_name} -n web-app-${resGroup} --type azureEndpoints --query id)" ]; then + az network traffic-manager endpoint create -g ${config.COMMON_GROUP} --profile-name \${traffic_manager_name} \\ + -n web-app-${resGroup} --type azureEndpoints --target-resource-id \${webapp_id} + fi + """ +} + +def deployDataApp(String targetEnv, String resGroup) { + sh """ + # Change context to target Kubernetes cluster + context_name=\$(az acs list -g ${resGroup} --query [0].masterProfile.dnsPrefix | tr '[:upper:]' '[:lower:]' | tr -d '"') + kubectl config use-context \${context_name} + + # Create private container registry if not exist + if [ -z "\$(kubectl get ns ${targetEnv} --ignore-not-found)" ]; then + kubectl create ns ${targetEnv} --save-config + fi + secret_exist=\$(kubectl get secret ${acrLoginServer} --namespace=${targetEnv} --ignore-not-found) + if [ -z "\${secret_exist}" ]; then + kubectl create secret docker-registry ${acrLoginServer} --namespace=${targetEnv} \\ + --docker-server=${acrLoginServer} \\ + --docker-username=${acrUsername} \\ + --docker-password=${acrPassword} \\ + --docker-email=foo@foo.bar \\ + --save-config + fi + + # Deploy data app + export ACR_LOGIN_SERVER=${acrLoginServer} + export DATA_APP_CONTAINER_PORT=${config.DATA_APP_CONTAINER_PORT} + export TARGET_ENV=${targetEnv} + envsubst < ./deployment/data-app/deploy.yaml | kubectl apply --namespace=${targetEnv} -f - + + # Wait until external IP is created for data app + data_app_ip=\$(kubectl get svc data-app -o jsonpath={.status.loadBalancer.ingress[0].ip} --ignore-not-found --namespace=${targetEnv}) + while [ -z "\${data_app_ip}" ] + do + sleep 5 + data_app_ip=\$(kubectl get svc data-app -o jsonpath={.status.loadBalancer.ingress[0].ip} --ignore-not-found --namespace=${targetEnv}) + done + + # Update DNS name of public ip resource for data app + ip_name=\$(az network public-ip list -g ${resGroup} --query "[?ipAddress=='\${data_app_ip}']" | python -c "import sys, json; print json.load(sys.stdin)[0]['name']") + ip_resource_guid=\$(az network public-ip show -n \${ip_name} -g ${resGroup} --query resourceGuid | tr -d '"') + az network public-ip update -g ${resGroup} -n \${ip_name} --dns-name data-app-\${ip_resource_guid} --allocation-method Static + + # Add data app to traffic manager + ip_resource_id=\$(az network public-ip show -g ${resGroup} -n \${ip_name} --query id | tr -d '"') + traffic_manager_name=\$(az resource list -g ${config.COMMON_GROUP} --resource-type Microsoft.Network/trafficManagerProfiles --query [0].name | tr -d '"') + endpoint_name=\$(az network traffic-manager endpoint show -g ${config.COMMON_GROUP} -n data-app-${resGroup} --profile-name \${traffic_manager_name} --type azureEndpoints | tr -d '"') + if [ -z "\${endpoint_name}" ]; then + az network traffic-manager endpoint create -g ${config.COMMON_GROUP} --profile-name \${traffic_manager_name} -n data-app-${resGroup} --type azureEndpoints --target-resource-id \${ip_resource_id} + else + az network traffic-manager endpoint update -g ${config.COMMON_GROUP} --profile-name \${traffic_manager_name} -n data-app-${resGroup} --type azureEndpoints --target-resource-id \${ip_resource_id} + fi + """ +} + +return this diff --git a/deployment/jenkins/docker-master/Dockerfile b/deployment/jenkins/docker-master/Dockerfile new file mode 100644 index 0000000..bd4d9bb --- /dev/null +++ b/deployment/jenkins/docker-master/Dockerfile @@ -0,0 +1,36 @@ +FROM jenkins + +USER root + +ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false + +# Copy scripts to Jenkins image +COPY scriptApproval.xml /usr/share/jenkins/ref/ +COPY init.groovy /usr/share/jenkins/ref/ + +# Install suggested plugins +RUN /usr/local/bin/install-plugins.sh \ + cloudbees-folder \ + antisamy-markup-formatter \ + build-timeout \ + credentials-binding \ + timestamper \ + ws-cleanup \ + ant \ + gradle \ + workflow-job:2.10 \ + workflow-multibranch:2.10 \ + workflow-aggregator \ + github-organization-folder \ + pipeline-stage-view \ + git \ + subversion \ + ssh-slaves \ + matrix-auth \ + pam-auth \ + ldap \ + email-ext \ + mailer \ + kubernetes \ + job-dsl \ + groovy diff --git a/deployment/jenkins/docker-master/init.groovy b/deployment/jenkins/docker-master/init.groovy new file mode 100644 index 0000000..8d88dbb --- /dev/null +++ b/deployment/jenkins/docker-master/init.groovy @@ -0,0 +1,151 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +import hudson.model.* +import jenkins.model.* +import hudson.security.FullControlOnceLoggedInAuthorizationStrategy +import hudson.security.HudsonPrivateSecurityRealm +import hudson.security.HudsonPrivateSecurityRealm.Details +import hudson.tasks.Mailer.UserProperty +import javaposse.jobdsl.dsl.DslScriptLoader +import javaposse.jobdsl.plugin.JenkinsJobManagement +import org.csanchez.jenkins.plugins.kubernetes.* +import org.csanchez.jenkins.plugins.kubernetes.volumes.* +import com.cloudbees.plugins.credentials.* +import com.cloudbees.plugins.credentials.common.* +import com.cloudbees.plugins.credentials.domains.* +import com.cloudbees.plugins.credentials.impl.* + +/** + * Set up security for the Jenkins instance with below configuration. + * Security Realm: Jenkins' own user database and do not allow sign up + * Authorization: Logged-in users can do anything and read access is disabled for anonymous user + */ +void setupSecurity() { + def instance = Jenkins.getInstance() + def strategy = new FullControlOnceLoggedInAuthorizationStrategy() + strategy.setAllowAnonymousRead(false) + def realm = new HudsonPrivateSecurityRealm(false, false, null) + instance.setAuthorizationStrategy(strategy) + instance.setSecurityRealm(realm) + instance.save() +} + +/** + * Create user + */ +void createUser(String user_name, String password = '', String full_name = '', String email = 'jenkins@foo.bar') { + def user = User.get(user_name) + user.setFullName(full_name) + def email_param = new UserProperty(email) + user.addProperty(email_param) + def pw_param = Details.fromPlainPassword(password) + user.addProperty(pw_param) + user.save() +} + +/** + * Create new pipelines + */ +void createPipeline(String githubRepo, String envName, String branchName) { + def workspace = new File('.') + def jobManagement = new JenkinsJobManagement(System.out, [:], workspace) + try { + new DslScriptLoader(jobManagement).runScript(""" + pipelineJob("movie-db-pipeline-for-${envName}") { + description "Pipeline for ${envName} environment." + scm { + git("${githubRepo}","*/${branchName}") + } + triggers { + githubPush() + } + definition { + cpsScm { + scm { + github("${githubRepo}","**/${branchName}") + } + scriptPath('Jenkinsfile') + } + } + } + """) + } catch (Exception e) { + println("Fail to create/update pipeline for ${envName} environment.") + println(e.toString()) + println(e.getMessage()) + println(e.getStackTrace()) + } +} + +/** + * Set number of executors in Jenkins + */ +void setExecutorNum(int num) { + def instance = Jenkins.getInstance() + instance.setNumExecutors(num) + instance.save() +} + +void addKubeCredential(String credentialId) { + def kubeCredential = new ServiceAccountCredential(CredentialsScope.GLOBAL, credentialId, 'Kubernetes service account') + SystemCredentialsProvider.getInstance().getStore().addCredentials(Domain.global(), kubeCredential) +} + +/** + * Configure Kubernetes plugin + */ +void configureKubernetes() { + def instance = Jenkins.getInstance() + def env = System.getenv() + KubernetesCloud kube = new KubernetesCloud( + 'jenkins-slave', + null, + "https://${env.KUBERNETES_SERVICE_HOST}", + "jenkins", + "http://${env.JENKINS_SERVICE_HOST}", + '5', 0, 0, 5) + kube.setSkipTlsVerify(true) + this.addKubeCredential('kube') + kube.setCredentialsId('kube') + + def volumes = new ArrayList() + volumes.add(new HostPathVolume ('/etc/kubernetes', '/etc/kubernetes')) + volumes.add(new HostPathVolume ('/var/run/docker.sock', '/var/run/docker.sock')) + volumes.add(new SecretVolume ('/home/jenkins/.kube', 'kube-config')) + + def envVars = new ArrayList() + envVars.add(new PodEnvVar('GROUP_SUFFIX', "${env.GROUP_SUFFIX}")) + + def pod = new PodTemplate('jnlp', 'kevinzha/jenkins-k8s-s', volumes) + pod.setEnvVars(envVars) + pod.setLabel('jenkins-slave-docker') + pod.setRemoteFs('/home/jenkins') + pod.setIdleMinutes(5) + pod.setPrivileged(true) + pod.setCommand('') + pod.setArgs('') + + kube.addTemplate(pod) + instance.clouds.replace(kube) + instance.save() +} + +Thread.start { + def env = System.getenv() + // Create or update pipelines + String githubRepo = "${env.GITHUB_REPO_OWNER}/${env.GITHUB_REPO_NAME}" + this.createPipeline(githubRepo, 'dev', 'master') + this.createPipeline(githubRepo, 'test', 'test') + this.createPipeline(githubRepo, 'prod', 'prod') + // Configure Kubernetes plugin + this.configureKubernetes() + // Set number of executor to 0 so that slave agents will be created for each build + this.setExecutorNum(0) + // Setup security + this.setupSecurity() + this.createUser('jenkins', "${env.JENKINS_PASSWORD}", 'jenkins') +} \ No newline at end of file diff --git a/deployment/jenkins/docker-master/scriptApproval.xml b/deployment/jenkins/docker-master/scriptApproval.xml new file mode 100644 index 0000000..81c7d5e --- /dev/null +++ b/deployment/jenkins/docker-master/scriptApproval.xml @@ -0,0 +1,16 @@ + + + + + + method hudson.plugins.git.BranchSpec getName + method hudson.plugins.git.GitSCM getBranches + new groovy.json.JsonSlurperClassic + method groovy.json.JsonSlurperClassic parseText java.lang.String + + + + + + + diff --git a/deployment/jenkins/docker-slave/Dockerfile b/deployment/jenkins/docker-slave/Dockerfile new file mode 100644 index 0000000..501d971 --- /dev/null +++ b/deployment/jenkins/docker-slave/Dockerfile @@ -0,0 +1,44 @@ +FROM jenkinsci/jnlp-slave + +USER root + +# Install apt-utils +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + apt-utils \ + curl + +# Install maven 3.3.9 +ENV MAVEN_VERSION 3.3.9 +RUN curl -fsSL http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz | tar xzf - -C /usr/share \ + && mv /usr/share/apache-maven-$MAVEN_VERSION /usr/share/maven \ + && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn +ENV MAVEN_HOME /usr/share/maven +ADD maven-settings.xml $MAVEN_HOME/conf/settings.xml + +# Install docker +RUN apt-get install -y \ + apt-transport-https \ + ca-certificates \ + software-properties-common +RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - +RUN add-apt-repository \ + "deb [arch=amd64] https://download.docker.com/linux/debian \ + $(lsb_release -cs) \ + stable" +RUN apt-get update && \ + apt-get install -y docker-ce + +# Install kubectl +RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl +RUN chmod +x ./kubectl +RUN mv ./kubectl /usr/local/bin/kubectl + +# Install azure cli +RUN echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ wheezy main" | tee /etc/apt/sources.list.d/azure-cli.list +RUN apt-key adv --keyserver packages.microsoft.com --recv-keys 417A0893 +RUN apt-get update && \ + apt-get install -y libssl-dev libffi-dev python-dev build-essential apt-transport-https azure-cli + +# Install envsubst +RUN apt-get install -y gettext diff --git a/deployment/jenkins/docker-slave/maven-settings.xml b/deployment/jenkins/docker-slave/maven-settings.xml new file mode 100644 index 0000000..6f79256 --- /dev/null +++ b/deployment/jenkins/docker-slave/maven-settings.xml @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${env.ACR_NAME} + ${env.ACR_USERNAME} + ${env.ACR_PASSWORD} + + foo@foo.bar + + + + + + + + + + + + + + + + + + diff --git a/deployment/jenkins/jenkins-master.yaml b/deployment/jenkins/jenkins-master.yaml new file mode 100644 index 0000000..6cc2ec1 --- /dev/null +++ b/deployment/jenkins/jenkins-master.yaml @@ -0,0 +1,138 @@ +--- +apiVersion: "v1" +kind: "List" +items: + + - apiVersion: "v1" + kind: "Namespace" + metadata: + name: "jenkins" + labels: + name: "jenkins" + + - apiVersion: "v1" + kind: "PersistentVolumeClaim" + metadata: + name: "jenkins" + namespace: "jenkins" + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + + - apiVersion: "extensions/v1beta1" + kind: "Deployment" + metadata: + name: "jenkins" + namespace: "jenkins" + labels: + name: "jenkins" + spec: + replicas: 1 + template: + metadata: + labels: + name: "jenkins" + spec: + containers: + - name: "jenkins" + image: "kevinzha/jenkins-k8s-m" + ports: + - containerPort: 8080 + - containerPort: 50000 + resources: + limits: + cpu: 1 + memory: 4Gi + requests: + cpu: 1 + memory: 2Gi + env: + - name: CPU_REQUEST + valueFrom: + resourceFieldRef: + resource: requests.cpu + - name: CPU_LIMIT + valueFrom: + resourceFieldRef: + resource: limits.cpu + - name: MEM_REQUEST + valueFrom: + resourceFieldRef: + resource: requests.memory + divisor: "1Mi" + - name: MEM_LIMIT + valueFrom: + resourceFieldRef: + resource: limits.memory + divisor: "1Mi" + - name: GITHUB_REPO_OWNER + valueFrom: + configMapKeyRef: + name: my-config + key: githubRepoOwner + - name: GITHUB_REPO_NAME + valueFrom: + configMapKeyRef: + name: my-config + key: githubRepoName + - name: JENKINS_PASSWORD + valueFrom: + secretKeyRef: + name: my-secrets + key: jenkinsPassword + - name: GROUP_SUFFIX + valueFrom: + configMapKeyRef: + name: my-config + key: groupSuffix + volumeMounts: + - name: "kube" + mountPath: "/root/.kube" + - name: "kube-azure" + mountPath: "/etc/kubernetes" + - name: "docker" + mountPath: "/var/run/docker.sock" + - name: "jenkins-home" + mountPath: "/var/jenkins_home" + readinessProbe: + httpGet: + path: /login + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 5 + securityContext: + fsGroup: 1000 + volumes: + - name: "kube" + hostPath: + path: "/root/.kube" + - name: "kube-azure" + hostPath: + path: "/etc/kubernetes" + - name: "docker" + hostPath: + path: "/var/run/docker.sock" + - name: "jenkins-home" + persistentVolumeClaim: + claimName: "jenkins" + + - apiVersion: "v1" + kind: "Service" + metadata: + name: "jenkins" + namespace: "jenkins" + spec: + type: "LoadBalancer" + selector: + name: "jenkins" + ports: + - name: "http" + port: 80 + targetPort: 8080 + protocol: "TCP" + - name: "slave" + port: 50000 + protocol: "TCP" diff --git a/deployment/lib.sh b/deployment/lib.sh new file mode 100644 index 0000000..201577c --- /dev/null +++ b/deployment/lib.sh @@ -0,0 +1,767 @@ +#! /bin/bash +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# +# Bash function library + +############################################################################## +# Show help message +# Globals: +# None +# Arguments: +# None +# Returns: +# None +############################################################################## +function show_help() +{ + echo " +Usage: + source [shell script] [options] + . [shell script] [options] + +Options: + --mysql-password [value] Password for MySQL database to be created. + --jenkins-password [value] Password for 'jenkins' account to be created in Jenkins cluster. + --env [value] Optional. Target environment. + Allow values: dev, test, prod. Default is 'dev'. + --group-suffix [value] Optional. Resource group suffix to avoid conflict. Default is empty. + --github-owner [value] Optional. GitHub repository owner. Typically it is your GitHub account name. +" +} + +############################################################################## +# Show help message +# Globals: +# None +# Arguments: +# None +# Returns: +# None +############################################################################## +function show_teardown_help() +{ + echo " +Deprovision.sh will delete all Azure resources created by provision.sh. + +Usage: + bash teardown.sh [options] + ./teardown.sh [options] + +Options: + --env [value] Optional. Target environment to provision. + Allow values: dev, test, prod. Default is 'dev'. + --group-suffix [value] Optional. Suffix of provisioned resource groups. Default is empty. + --wait Optional. If this options is specified, script will wait until all resource groups are deleted successfully. + By default, script will initiate the deletion of resource groups and exit immediately. +" +} + +############################################################################## +# Parse arguments and setup environment variables +# Globals: +# TARGET_ENV +# MYSQL_PASSWORD +# JENKINS_PASSWORD +# GROUP_SUFFIX +# GITHUB_REPO_OWNER +# Arguments: +# All args from command line +# Returns: +# None +############################################################################## +function parse_args() +{ + while :; do + case "$(echo $1 | tr '[:upper:]' '[:lower:]')" in + -h|-\?|--help) + show_help + return 1 + ;; + --env) + if [ -n "$2" ]; then + case "$(echo $2 | tr '[:upper:]' '[:lower:]')" in + dev) export TARGET_ENV=dev ;; + test) export TARGET_ENV=test ;; + prod) export TARGET_ENV=prod ;; + *) + log_error "Invalid argument for option \"--env\": $2" + return 1 + ;; + esac + shift + else + log_error "\"--env\" requires an argument." + return 1 + fi + ;; + --mysql-password) + if [ -n "$2" ]; then + export MYSQL_PASSWORD=$2 + shift + else + log_error "\"--mysql-password\" requires an argument." + return 1 + fi + ;; + --jenkins-password) + if [ -n "$2" ]; then + export JENKINS_PASSWORD=$2 + shift + else + log_error "\"--jenkins-password\" requires an argument." + return 1 + fi + ;; + --group-suffix) + if [ -n "$2" ]; then + export GROUP_SUFFIX=$2 + shift + else + log_error "\"--group-suffix\" requires an argument." + return 1 + fi + ;; + --github-owner) + if [ -n "$2" ]; then + export GITHUB_REPO_OWNER=$2 + shift + else + log_error "\"--github-owner\" requires an argument." + return 1 + fi + ;; + -?*) + log_warning "Unknown option \"$1\"" + ;; + *) + break + esac + + shift + done +} + +############################################################################## +# Parse arguments and setup environment variables +# Globals: +# TARGET_ENV +# GROUP_SUFFIX +# Arguments: +# All args from command line +# Returns: +# None +############################################################################## +function parse_teardown_args() +{ + while :; do + case "$(echo $1 | tr '[:upper:]' '[:lower:]')" in + -h|-\?|--help) + show_teardown_help + return 1 + ;; + --env) + if [ -n "$2" ]; then + case "$(echo $2 | tr '[:upper:]' '[:lower:]')" in + dev) export TARGET_ENV=dev ;; + test) export TARGET_ENV=test ;; + prod) export TARGET_ENV=prod ;; + *) + log_error "Invalid argument for option \"--env\": $2" + return 1 + ;; + esac + shift + else + log_error "\"--env\" requires an argument." + return 1 + fi + ;; + --group-suffix) + if [ -n "$2" ]; then + export GROUP_SUFFIX=$2 + shift + else + log_error "\"--group-suffix\" requires an argument." + return 1 + fi + ;; + --wait) + export TEARDOWN_NO_WAIT=false + ;; + -?*) + log_warning "Unknown option \"$1\"" + ;; + *) + break + esac + + shift + done +} + +############################################################################## +# Check whether tool is installed +# Globals: +# None +# Arguments: +# tool_name +# test_command +# Returns: +# None +############################################################################## +function check_tool() +{ + local tool_name=$1 + local test_command=$2 + ${test_command} > /dev/null 2>&1 + if [ $? != 0 ]; then + log_error "\"${tool_name}\" not found. Please install \"${tool_name}\" before running this script." + return 1 + fi +} + +############################################################################## +# Check whether all required tools are installed +# Globals: +# None +# Arguments: +# None +# Returns: +# None +############################################################################## +function check_required_tools() +{ + check_tool 'Java SDK' 'javac -version' + [[ $? -ne 0 ]] && return 1 + + check_tool 'Maven' 'mvn --version' + [[ $? -ne 0 ]] && return 1 + + check_tool 'Azure CLI 2.0' 'az --version' + [[ $? -ne 0 ]] && return 1 + + check_tool 'docker' 'docker --version' + [[ $? -ne 0 ]] && return 1 + + check_tool 'jq' 'jq -h' + [[ $? -ne 0 ]] && return 1 + + check_tool 'gettext' 'envsubst -h' + [[ $? -ne 0 ]] && return 1 + + check_tool 'kubectl' 'kubectl' + [[ $? -ne 0 ]] && return 1 + + return 0 +} + +############################################################################## +# Check whether all required environment variables are set up +# Globals: +# None +# Arguments: +# None +# Returns: +# None +############################################################################## +function check_required_env_vars() +{ + if [ -z "${MYSQL_PASSWORD}" ]; then + log_error 'Environment variable MYSQL_PASSWORD not found. Either setup environment variable MYSQL_PASSWORD or pass it with "--mysql-password" option.' + return 1 + fi + + if [ -z "${JENKINS_PASSWORD}" ]; then + log_error 'Environment variable JENKINS_PASSWORD not found. Either setup environment variables JENKINS_PASSWORD or pass it with "--jenkins-password" option.' + return 1 + fi +} + +############################################################################## +# Read all key-value pairs from config.json and export as environment variables +# Globals: +# Environment variables set in config.json +# Arguments: +# None +# Returns: +# None +############################################################################## +function load_config() +{ + local keys=( $(jq -r '.[] | values[] | select(.value != "").key' config.json) ) + local values=( $(jq -r '.[] | values[] | select(.value != "").value' config.json) ) + + local total=${#keys[*]} + for ((i=0; i < $((total)); i++)) + do + #On Windows, there seems to be special line ending characters. Remove them. + local key=${keys[$i]} + key=${key//$'\n'/} + key=${key//$'\r'/} + local value=${values[$i]} + value=${value//$'\n'/} + value=${value//$'\r'/} + export ${key}=${value} + done +} + +############################################################################## +# Create shared resources with master ARM template +# Globals: +# None +# Arguments: +# resource_group +# username: Admin login username for MySQL server +# password: Admin login password for MySQL server +# Returns: +# None +############################################################################## +function create_shared_resources() +{ + local resource_group=$1 + local username=$2 + local password=$3 + az group deployment create -g ${resource_group} --template-file ./arm/master.json \ + --parameters "{\"administratorLogin\": {\"value\": \"${username}\"},\"administratorLoginPassword\": {\"value\": \"${password}\"}}" \ + --no-wait +} + +############################################################################## +# Wait for the completion of ARM template deployment +# Globals: +# None +# Arguments: +# resource_group +# deployment_name +# Returns: +# None +############################################################################## +function wait_till_deployment_created() +{ + local resource_group=$1 + local deployment_name=$2 + az group deployment wait -g ${resource_group} -n ${deployment_name} --created + if [ $? != 0 ]; then + log_error "Something is wrong when provisioning resources in resource group \"${resource_group}\". Please check out logs in Azure Portal." + return 1 + fi +} + +############################################################################## +# Create linux container web app +# Globals: +# None +# Arguments: +# resource_group +# location +# Returns: +# None +############################################################################## +function create_webapp() +{ + local resource_group=$1 + local location=$2 + az group deployment create -g ${resource_group} --template-file ./arm/linux-webapp.json \ + --parameters "{\"location\": {\"value\": \"${location}\"}}" \ + --query "{id:id,name:name,provisioningState:properties.provisioningState,resourceGroup:resourceGroup}" +} + +############################################################################## +# Create Kubernetes cluster without waiting if it doesn't exist +# Globals: +# None +# Arguments: +# resource_group +# acs_name +# Returns: +# None +############################################################################## +function create_kubernetes() +{ + local resource_group=$1 + local acs_name=$2 + if [ -z "$(az acs show -g ${resource_group} -n ${acs_name})" ]; then + az acs create --orchestrator-type=kubernetes -g ${resource_group} -n ${acs_name} \ + --generate-ssh-keys --agent-count 1 --no-wait + fi +} + +############################################################################## +# Wait for the completion of Kubernetes cluster creation +# Globals: +# None +# Arguments: +# resource_group +# acs_name +# Returns: +# None +############################################################################## +function wait_till_kubernetes_created() { + local resource_group=$1 + local acs_name=$2 + az acs wait -g ${resource_group} -n ${acs_name} --created + if [ $? != 0 ]; then + log_error "Something is wrong when provisioning Kubernetes in resource group \"${resource_group}\". Please check out logs in Azure Portal." + return 1 + fi +} + +############################################################################## +# Create ConfigMap and Secrets in Kubernetes cluster +# Globals: +# TARGET_ENV +# MYSQL_ENDPOINT +# MYSQL_USERNAME +# MYSQL_PASSWORD +# Arguments: +# resource_group +# acs_name +# Returns: +# None +############################################################################## +function create_secrets_in_kubernetes() { + local resource_group=$1 + local acs_name=$2 + + az acs kubernetes get-credentials -g ${resource_group} -n ${acs_name} + + if [ -z "$(kubectl get ns ${TARGET_ENV} --ignore-not-found)" ]; then + kubectl create ns ${TARGET_ENV} --save-config + fi + kubectl config set-context $(kubectl config current-context) --namespace=${TARGET_ENV} + + if [ -n "$(kubectl get secret my-secrets --ignore-not-found)" ]; then + kubectl delete secret my-secrets + fi + kubectl create secret generic my-secrets --type=string --save-config \ + --namespace=${TARGET_ENV} \ + --from-literal=mysqlEndpoint=${MYSQL_ENDPOINT} \ + --from-literal=mysqlUsername=${MYSQL_USERNAME} \ + --from-literal=mysqlPassword=${MYSQL_PASSWORD} +} + +############################################################################## +# Deploy Jenkins if it doesn't exist +# Globals: +# GITHUB_REPO_OWNER +# GITHUB_REPO_NAME +# JENKINS_PASSWORD +# GROUP_SUFFIX +# Arguments: +# resource_group +# acs_name +# Returns: +# None +############################################################################## +function deploy_jenkins() +{ + create_secrets_in_jenkins_kubernetes $1 $2 + + if [ -z "$(kubectl get deploy jenkins --ignore-not-found --namespace=jenkins)" ]; then + kubectl apply -f ./jenkins/jenkins-master.yaml + fi + + # Check existence of Jenkins service + check_jenkins_readiness +} + +############################################################################## +# Create secrets in Kubernetes for Jenkins +# Globals: +# None +# Arguments: +# resource_group +# acs_name +# Returns: +# None +############################################################################## +function create_secrets_in_jenkins_kubernetes() { + local resource_group=$1 + local acs_name=$2 + + az acs kubernetes get-credentials -g ${resource_group} -n ${acs_name} + + if [ -z "$(kubectl get ns jenkins --ignore-not-found)" ]; then + kubectl create ns jenkins --save-config + fi + kubectl config set-context $(kubectl config current-context) --namespace=jenkins + + if [ -n "$(kubectl get secret my-secrets --ignore-not-found)" ]; then + kubectl delete secret my-secrets + fi + kubectl create secret generic my-secrets --from-literal=jenkinsPassword=${JENKINS_PASSWORD} --save-config + + if [ -n "$(kubectl get secret kube-config --ignore-not-found)" ]; then + kubectl delete secret kube-config + fi + kubectl create secret generic kube-config --from-file=config=${HOME}/.kube/config + + if [ -n "$(kubectl get configMap my-config --ignore-not-found)" ]; then + kubectl delete configmap my-config + fi + kubectl create configmap my-config --save-config \ + --from-literal=githubRepoOwner=${GITHUB_REPO_OWNER} \ + --from-literal=githubRepoName=${GITHUB_REPO_NAME} \ + --from-literal=groupSuffix=${GROUP_SUFFIX} +} + +############################################################################## +# Check whether Jenkins is ready for access +# Globals: +# None +# Arguments: +# None +# Returns: +# None +############################################################################## +function check_jenkins_readiness() +{ + jenkins_ip=$(kubectl get svc -o jsonpath={.items[*].status.loadBalancer.ingress[0].ip}) + while [ -z "${jenkins_ip}" ] + do + sleep 5 + jenkins_ip=$(kubectl get svc -o jsonpath={.items[*].status.loadBalancer.ingress[0].ip}) + done + echo Jenkins is ready at http://${jenkins_ip}/. +} + +############################################################################## +# Export Jenkins URL as environment variables +# Globals: +# JENKINS_URL +# Arguments: +# resource_group +# acs_name +# Returns: +# None +############################################################################## +function export_jenkins_url() +{ + local resource_group=$1 + local acs_name=$2 + az acs kubernetes get-credentials -g ${resource_group} -n ${acs_name} + export JENKINS_URL=$(kubectl get svc -o jsonpath={.items[*].status.loadBalancer.ingress[0].ip} --namespace=jenkins) +} + +############################################################################## +# Export Azure Container Registry information as environment variables +# Globals: +# ACR_NAME +# ACR_USERNAME +# ACR_PASSWORD +# ACR_LOGIN_SERVER +# Arguments: +# resource_group +# Returns: +# None +############################################################################## +function export_acr_details() +{ + local resource_group=$1 + export ACR_NAME=$(az acr list -g ${resource_group} --query [0].name | tr -d '"') + if [ -z "${ACR_NAME}" ]; then + echo No Azure Container Registry found. Exit... + exit 1 + fi + export ACR_USERNAME=$(az acr credential show -g ${resource_group} -n ${ACR_NAME} --query username | tr -d '"') + export ACR_PASSWORD=$(az acr credential show -g ${resource_group} -n ${ACR_NAME} --query passwords[0].value | tr -d '"') + export ACR_LOGIN_SERVER=$(az acr show -g ${resource_group} -n ${ACR_NAME} --query loginServer | tr -d '"') +} + +############################################################################## +# Export MySQL server information as environment variables +# Globals: +# MYSQL_USERNAME +# MYSQL_SERVER_ENDPOINT +# MYSQL_ENDPOINT +# Arguments: +# resource_group +# Returns: +# None +############################################################################## +function export_database_details() +{ + local resource_group=$1 + local server_name=$(az mysql server list -g ${resource_group} --query [0].name | tr -d '"') + local username=$(az mysql server show -g ${resource_group} -n ${server_name} --query administratorLogin | tr -d '"') + local endpoint=$(az mysql server show -g ${resource_group} -n ${server_name} --query fullyQualifiedDomainName | tr -d '"') + local database_name=$(az mysql db list -g ${resource_group} --server-name ${server_name} --query [0].name | tr -d '"') + + export MYSQL_USERNAME=${username}@${server_name} + export MYSQL_SERVER_ENDPOINT=jdbc:mysql://${endpoint}:3306/?serverTimezone=UTC + export MYSQL_ENDPOINT=jdbc:mysql://${endpoint}:3306/${database_name}?serverTimezone=UTC +} + +############################################################################## +# Populate initial data set to MySQL database +# Globals: +# None +# Arguments: +# resource_group +# Returns: +# None +############################################################################## +function init_database() +{ + local resource_group=$1 + export_database_details ${resource_group} + cd ../database; mvn sql:execute; cd ../deployment +} + +############################################################################## +# Export Redis server information as environment variables +# Globals: +# REDIS_HOST +# REDIS_PASSWORD +# Arguments: +# resource_group +# Returns: +# None +############################################################################## +function export_redis_details() +{ + local resource_group=$1 + local redis_name=$(az redis list -g ${resource_group} --query [0].name | tr -d '"') + + export REDIS_HOST=$(az redis show -g ${resource_group} -n ${redis_name} --query hostName | tr -d '"') + export REDIS_PASSWORD=$(az redis list-keys -g ${resource_group} -n ${redis_name} --query primaryKey | tr -d '"') +} + +############################################################################## +# Export image storage account information as environment variables +# Globals: +# STORAGE_CONNECTION_STRING +# Arguments: +# resource_group +# Returns: +# None +############################################################################## +function export_image_storage() +{ + local resource_group=$1 + storage_name=$(az storage account list -g ${resource_group} --query [2].name | tr -d '"') + export STORAGE_CONNECTION_STRING=$(az storage account show-connection-string -g ${resource_group} -n ${storage_name} --query connectionString | tr -d '"') +} + +############################################################################## +# Export web-app information as environment variables +# Globals: +# WEBAPP_NAME +# WEBAPP_PLAN +# Arguments: +# resource_group +# Returns: +# None +############################################################################## +function export_webapp_details() +{ + local resource_group=$1 + local prefix=$2 + export ${prefix}_WEBAPP_NAME=$(az resource list -g ${resource_group} --resource-type Microsoft.Web/sites --query [0].name | tr -d '"') + export ${prefix}_WEBAPP_PLAN=$(az appservice plan list -g ${resource_group} --query [0].name | tr -d '"') +} + +############################################################################## +# Export data-app IP as environment variables +# Globals: +# DATA_API_URL +# Arguments: +# namespace +# resource_group +# Returns: +# None +############################################################################## +function export_data_api_url() +{ + local namespace=$1 + local resource_group=$2 + local k8_context=$(az acs list -g ${resource_group} --query [0].masterProfile.dnsPrefix | tr '[:upper:]' '[:lower:]' | tr -d '"') + kubectl config use-context ${k8_context} > /dev/null + export DATA_API_URL=$(kubectl get services -o jsonpath={.items[*].status.loadBalancer.ingress[0].ip} --namespace=${namespace}) +} + +############################################################################## +# Print string in specified color +# Globals: +# None +# Arguments: +# color +# info +# Returns: +# None +############################################################################## +function log_with_color() +{ + local color=$1 + local no_color='\033[0m' + local info=$2 + echo -e "${color}${info}${no_color}" +} + +############################################################################## +# Print information string in green color +# Globals: +# None +# Arguments: +# info +# Returns: +# None +############################################################################## +function log_info() +{ + local info=$1 + local green_color='\033[0;32m' + log_with_color "${green_color}" "${info}" +} + +############################################################################## +# Print warning string in yellow color +# Globals: +# None +# Arguments: +# info +# Returns: +# None +############################################################################## +function log_warning() +{ + local info=$1 + local yellow_color='\033[0;33m' + log_with_color "${yellow_color}" "[Warning] ${info}" +} + +############################################################################## +# Print error string in green color +# Globals: +# None +# Arguments: +# info +# Returns: +# None +############################################################################## +function log_error() +{ + local info=$1 + local red_color='\033[0;31m' + log_with_color "${red_color}" "[Error] ${info}" +} + +############################################################################## +# Print activity banner +# Globals: +# None +# Arguments: +# info +# Returns: +# None +############################################################################## +function print_banner() +{ + local info=$1 + log_info '********************************************************************************' + log_info "* ${info}" + log_info '********************************************************************************' +} diff --git a/deployment/provision.sh b/deployment/provision.sh new file mode 100644 index 0000000..b8bc269 --- /dev/null +++ b/deployment/provision.sh @@ -0,0 +1,84 @@ +#! /bin/bash +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# +# +# Provision resources in Azure + +# Source library +source lib.sh + +# Check whether script is running in a sub-shell +if [ "${BASH_SOURCE}" == "$0" ]; then + log_error '"provision.sh" should be sourced. Use ". provision.sh --help" for detailed information.' + exit 1 +fi + +# Check required tools. Exit if requirements aren't satisfied. +check_required_tools +[[ $? -ne 0 ]] && return 1 + +# Load config.json and export environment variables +load_config + +# Parse command line arguments +parse_args "$@" +[[ $? -ne 0 ]] && return 1 + +# Check required environment variables +check_required_env_vars +[[ $? -ne 0 ]] && return 1 + +# Prefix resource group names with target environment +c_group=${TARGET_ENV}${COMMON_GROUP}${GROUP_SUFFIX} +e_us_group=${TARGET_ENV}${EAST_US_GROUP}${GROUP_SUFFIX} +w_eu_group=${TARGET_ENV}${WEST_EUROPE_GROUP}${GROUP_SUFFIX} +jenkins_group=${JENKINS_GROUP}${GROUP_SUFFIX} + +print_banner 'Start provisioning shared resources...' +az group create -n ${c_group} -l ${EAST_US} +create_shared_resources ${c_group} ${MYSQL_ADMIN_USERNAME} ${MYSQL_PASSWORD} + +print_banner 'Start provisioning resources in West US region...' +az group create -n ${e_us_group} -l ${EAST_US} +create_webapp ${e_us_group} westus +create_kubernetes ${e_us_group} ${ACS_NAME} + +print_banner 'Start provisioning resources in North Europe region...' +az group create -n ${w_eu_group} -l ${WEST_EUROPE} +create_webapp ${w_eu_group} westeurope +create_kubernetes ${w_eu_group} ${ACS_NAME} + +print_banner 'Start provisioning Kubernetes cluster for Jenkins if not exist...' +az group create -n ${jenkins_group} -l ${EAST_US} +create_kubernetes ${jenkins_group} ${ACS_NAME} + +print_banner 'Wait until resource provisioning is finished...' +wait_till_kubernetes_created ${e_us_group} ${ACS_NAME} +[[ $? -ne 0 ]] && return 1 + +wait_till_kubernetes_created ${w_eu_group} ${ACS_NAME} +[[ $? -ne 0 ]] && return 1 + +wait_till_kubernetes_created ${jenkins_group} ${ACS_NAME} +[[ $? -ne 0 ]] && return 1 + +wait_till_deployment_created ${c_group} master +[[ $? -ne 0 ]] && return 1 + +print_banner 'Populating database...' +init_database ${c_group} + +print_banner 'Creating secrets and config map in Kubernetes...' +export_database_details ${c_group} +create_secrets_in_kubernetes ${e_us_group} ${ACS_NAME} +create_secrets_in_kubernetes ${w_eu_group} ${ACS_NAME} + +print_banner 'Deploy Jenkins cluster if not exist...' +deploy_jenkins ${jenkins_group} ${ACS_NAME} + +# Set up environment variables for local dev environment +source dev_setup.sh "$@" + +print_banner 'Provision completed' diff --git a/deployment/social.md b/deployment/social.md new file mode 100644 index 0000000..f6a589b --- /dev/null +++ b/deployment/social.md @@ -0,0 +1,13 @@ +#Configuring Social Login + +This application supports integrating with Facebook login using Spring Security. Once enabled, updating movie information would be allowed only for the authenticated users. + +Follow these instructions to enable Facebook login integration during the initial deployment. + +1. Register a new Facebook app at . + * Make a note of its App ID and App Secret. + * Enable Web OAuth Facebook login. + * Provide a temporary Valid OAuth redirect URIs value, ie http://localhost:8080/. _You will need to update this later once you know the actual web app url after the deployment completes._ +2. Edit [\deployment\config.json](config.json) to provide these values for the FACEBOOK_APP_ID and FACEBOOK_APP_SECRET environment variables. +3. Deploy using instructions provided in the [\deployment\readme.md](readme.md) +4. Update the Facebook app created in (1) with the actual value for the redirect URI. \ No newline at end of file diff --git a/function/ResizeImage/index.js b/function/ResizeImage/index.js new file mode 100644 index 0000000..48885d7 --- /dev/null +++ b/function/ResizeImage/index.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +var Jimp = require("jimp"); + +module.exports = (context) => { + + context.log('Starting...'); + + // Read image with Jimp + Jimp.read(context.bindings.inputBlob).then((image) => { + + context.log('Processing...'); + + // Resize image + image + .resize(60, Jimp.AUTO) + .getBuffer(Jimp.MIME_JPEG, (error, stream) => { + if (error) { + context.log('There was an error processing the image.'); + context.done(error); + } else { + context.log('Node.JS blob trigger function resized ' + context.bindingData.name + ' to ' + image.bitmap.width + 'x' + image.bitmap.height); + context.bindings.outputBlob = stream; + context.done(); + } + }); + + }).catch(function (error) { + context.log(error); + context.done(error); + }); + +}; diff --git a/function/ResizeImage/package.json b/function/ResizeImage/package.json new file mode 100644 index 0000000..98d7e72 --- /dev/null +++ b/function/ResizeImage/package.json @@ -0,0 +1,10 @@ +{ + "name": "resize-image", + "version": "0.0.1", + "private": true, + "main": "index.js", + "author": "Microsoft Corp.", + "dependencies": { + "jimp": "0.2.27" + } +} \ No newline at end of file diff --git a/media/movie-app-layout.jpg b/media/movie-app-layout.jpg new file mode 100644 index 0000000..e5b8cca Binary files /dev/null and b/media/movie-app-layout.jpg differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7b1acc7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + com.microsoft.azure.java.samples.moviedb + movie-db-java-on-azure + 0.1.0-SNAPSHOT + pom + + Microsoft Azure Movie Database + This package contains the parent module of Microsoft Azure Movie Database. + https://github.com/Microsoft/movie-db-java-on-azure + + + + The MIT License (MIT) + http://opensource.org/licenses/MIT + repo + + + + + + UTF-8 + + + + + + + microsoft + Microsoft + + + + + + + + org.springframework.boot + spring-boot-dependencies + 1.5.3.RELEASE + pom + import + + + + + + + org.apache.httpcomponents + httpclient + 4.4.1 + + + + + ./database + ./web-app + ./data-app + + diff --git a/web-app/.gitignore b/web-app/.gitignore new file mode 100644 index 0000000..2e5cbfa --- /dev/null +++ b/web-app/.gitignore @@ -0,0 +1,5 @@ +target/ +.settings/ +logs/ +.classpath +.project diff --git a/web-app/pom.xml b/web-app/pom.xml new file mode 100644 index 0000000..417a666 --- /dev/null +++ b/web-app/pom.xml @@ -0,0 +1,247 @@ + + + 4.0.0 + com.microsoft.azure.java.samples.moviedb + web-app + 0.1.0-SNAPSHOT + jar + + com.microsoft.azure.java.samples.moviedb + movie-db-java-on-azure + 0.1.0-SNAPSHOT + + + 1.8 + ${env.ACR_LOGIN_SERVER} + 3.37.0 + 1.8 + 1.8 + true + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security.oauth + spring-security-oauth2 + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity4 + true + + + org.springframework.boot + spring-boot-devtools + true + + + com.fasterxml.jackson.core + jackson-databind + + + org.json + json + + + com.microsoft.azure + azure-storage + 5.0.0 + + + com.newrelic.agent.java + newrelic-java + ${newrelic.version} + provided + zip + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-data-redis + + + commons-io + commons-io + 2.3 + + + org.springframework + spring-test + 4.3.7.RELEASE + + + org.mockito + mockito-core + test + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.6 + + + unpack-zip + package + + unpack + + + + + com.newrelic.agent.java + newrelic-java + ${newrelic.version} + zip + true + ${project.build.directory} + newrelic + + + ${project.build.directory}/newrelic + + + + + + org.springframework.boot + spring-boot-maven-plugin + 1.5.3.RELEASE + + + + repackage + + + + + + com.spotify + docker-maven-plugin + 0.4.11 + + ${env.ACR_NAME} + https://${env.ACR_LOGIN_SERVER} + src/main/docker/base + ${docker.image.prefix}/${project.artifactId} + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + + + + with-new-relic + + build + + + src/main/docker/new-relic + ${docker.image.prefix}/${project.artifactId}-w-new-relic + + ${env.NEW_RELIC_LICENSE_KEY} + ${env.WEBAPP_NEW_RELIC_APP_NAME} + + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + / + ${project.build.directory}/newrelic + newrelic.jar + + + / + ${project.build.directory}/newrelic + newrelic.yml + + + + + + with-overops + + build + + + src/main/docker/overops + ${docker.image.prefix}/${project.artifactId}-w-overops + + ${env.OVEROPSSK} + + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + + + + all + + build + + + src/main/docker/all + ${docker.image.prefix}/${project.artifactId}-w-all + + ${env.NEW_RELIC_LICENSE_KEY} + ${env.WEBAPP_NEW_RELIC_APP_NAME} + ${env.OVEROPSSK} + + + + / + ${project.build.directory} + ${project.build.finalName}.jar + + + / + ${project.build.directory}/newrelic + newrelic.jar + + + / + ${project.build.directory}/newrelic + newrelic.yml + + + + + + + + + \ No newline at end of file diff --git a/web-app/src/main/docker/all/Dockerfile b/web-app/src/main/docker/all/Dockerfile new file mode 100644 index 0000000..46cf00c --- /dev/null +++ b/web-app/src/main/docker/all/Dockerfile @@ -0,0 +1,23 @@ +FROM centos:7 + +RUN yum install -y java-1.8.0-openjdk.x86_64 +ARG OVEROPSSK="" + +# Takipi installation +RUN curl -Ls /dev/null http://get.takipi.com/takipi-t4c-installer | \ + bash /dev/stdin -i --sk=${OVEROPSSK} + +VOLUME /tmp +ADD web-app-0.1.0-SNAPSHOT.jar app.jar +RUN sh -c 'touch /app.jar' +ADD newrelic.jar newrelic.jar +RUN sh -c 'touch /newrelic.jar' +ADD newrelic.yml newrelic.yml +RUN sh -c 'touch /newrelic.yml' +ARG NEW_RELIC_APP_NAME="" +ENV NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME} +ARG NEW_RELIC_LICENSE_KEY="" +ENV NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY} + +# Connecting the Takipi agent to a Java process +CMD java -javaagent:/newrelic.jar -agentlib:TakipiAgent -jar /app.jar diff --git a/web-app/src/main/docker/base/Dockerfile b/web-app/src/main/docker/base/Dockerfile new file mode 100644 index 0000000..e647497 --- /dev/null +++ b/web-app/src/main/docker/base/Dockerfile @@ -0,0 +1,6 @@ +FROM frolvlad/alpine-oraclejdk8:slim +VOLUME /tmp +ADD web-app-0.1.0-SNAPSHOT.jar app.jar +RUN sh -c 'touch /app.jar' +ENV JAVA_OPTS="" +ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ] diff --git a/web-app/src/main/docker/new-relic/Dockerfile b/web-app/src/main/docker/new-relic/Dockerfile new file mode 100644 index 0000000..fc6b9ad --- /dev/null +++ b/web-app/src/main/docker/new-relic/Dockerfile @@ -0,0 +1,14 @@ +FROM frolvlad/alpine-oraclejdk8:slim +VOLUME /tmp +ADD web-app-0.1.0-SNAPSHOT.jar app.jar +RUN sh -c 'touch /app.jar' +ADD newrelic.jar newrelic.jar +RUN sh -c 'touch /newrelic.jar' +ADD newrelic.yml newrelic.yml +RUN sh -c 'touch /newrelic.yml' +ARG NEW_RELIC_APP_NAME="" +ENV NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME} +ARG NEW_RELIC_LICENSE_KEY="" +ENV NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY} +ENV JAVA_OPTS="" +ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -javaagent:/newrelic.jar -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ] diff --git a/web-app/src/main/docker/overops/Dockerfile b/web-app/src/main/docker/overops/Dockerfile new file mode 100644 index 0000000..d1618ec --- /dev/null +++ b/web-app/src/main/docker/overops/Dockerfile @@ -0,0 +1,15 @@ +FROM centos:7 + +RUN yum install -y java-1.8.0-openjdk.x86_64 +ARG OVEROPSSK="" + +# Takipi installation +RUN curl -Ls /dev/null http://get.takipi.com/takipi-t4c-installer | \ + bash /dev/stdin -i --sk=${OVEROPSSK} + +VOLUME /tmp +ADD web-app-0.1.0-SNAPSHOT.jar app.jar +RUN sh -c 'touch /app.jar' + +# Connecting the Takipi agent to a Java process +CMD java -agentlib:TakipiAgent -jar /app.jar diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/GlobalControllerAdvice.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/GlobalControllerAdvice.java new file mode 100644 index 0000000..35032dc --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/GlobalControllerAdvice.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.java.samples.moviedb.web; + +import com.microsoft.azure.java.samples.moviedb.web.pojo.UserInfo; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; + +import java.util.LinkedHashMap; + + +@ControllerAdvice +public class GlobalControllerAdvice { + + @Value("${facebook.client.client-id}") + String facebookAppId; + + @ModelAttribute + public void globalAttributes(Model model) { + Boolean isAllowedToUpdateMovieDB = true; + String displayName = "Anonymous"; + + if (facebookAppId != null && !facebookAppId.isEmpty()) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth instanceof OAuth2Authentication){ + displayName = (String)((LinkedHashMap) ((OAuth2Authentication) auth).getUserAuthentication().getDetails()).get("name"); + isAllowedToUpdateMovieDB = auth.isAuthenticated(); + } + else { + isAllowedToUpdateMovieDB = false; + } + } + + UserInfo userInfo = new UserInfo(displayName, isAllowedToUpdateMovieDB); + model.addAttribute("userInfo", userInfo); + } +} diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/MainController.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/MainController.java new file mode 100644 index 0000000..f1348dc --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/MainController.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.java.samples.moviedb.web; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import java.security.Principal; + +@Controller +public class MainController { + + @GetMapping("/") + public String root() { + return "redirect:/index"; + } + + @GetMapping("/index") + public String index(Principal principal, Model model) { + return "index"; + } + + @GetMapping("/login") + public String login(Principal principal, Model model) { + return "login"; + } +} diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/MovieController.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/MovieController.java new file mode 100644 index 0000000..af11429 --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/MovieController.java @@ -0,0 +1,171 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + + +package com.microsoft.azure.java.samples.moviedb.web; + +import com.microsoft.azure.java.samples.moviedb.web.pojo.Movie; +import com.microsoft.azure.java.samples.moviedb.web.pojo.MoviesResponse; +import com.microsoft.azure.java.samples.moviedb.web.pojo.PageInfo; +import com.microsoft.azure.java.samples.moviedb.web.util.AzureStorageUploader; +import org.apache.commons.io.FilenameUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; + +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.List; + +/** + * Controller that handles HTTP requests and returns corresponding views. + */ +@Controller +public class MovieController { + private static final Logger logger = LoggerFactory.getLogger(MovieController.class); + + @Autowired + private MovieRepository movieRepository; + + @Autowired + private AzureStorageUploader azureStorageUploader; + + /** + * Get movie info by movie id. + * + * @param id movie id + * @param model spring model + * @return movie detail page + */ + @RequestMapping(value = "/movies/{id}", method = RequestMethod.GET) + public String getMovieById(@PathVariable Long id, Model model) { + Movie movie = movieRepository.getMovie(Long.toString(id)); + if (movie != null) { + if (movie.getImageUri() != null) { + movie.setImageFullPathUri(azureStorageUploader.getAzureStorageBaseUri() + movie.getImageUri()); + } + + model.addAttribute("movie", movie); + return "moviedetail"; + } else { + return "moviedetailerror"; + } + } + + /** + * Update movie description by movie id. + * + * @param id movie id + * @param description movie description + * @return updated movie detail page + */ + @RequestMapping(value = "/movies/{id}", method = RequestMethod.POST) + public String updateMovieDescription(@PathVariable Long id, @RequestParam("description") String description) { + logger.debug("Update movie description"); + + Movie movie = new Movie(); + movie.setDescription(description); + + movieRepository.patchMovie(Long.toString(id), movie); + + return "redirect:/movies/" + id; + } + + /** + * Get one page of movies. + * + * @param page page number + * @param model spring model + * @return movie list page + */ + @RequestMapping(value = "/movies", method = RequestMethod.GET) + public String getMovieList(@RequestParam(value = "page", required = false, defaultValue = "0") Long page, + Model model) { + MoviesResponse moviesResponse = movieRepository.getMovies(Long.toString(page)); + if (moviesResponse != null) { + setupMovieList(moviesResponse, model); + } + + model.addAttribute("page", page); + + return "moviespage"; + } + + /** + * Upload image file to Azure blob and save its relative path to database. + * + * @param file image file + * @param id movie id + * @return updated movie detail page + */ + @RequestMapping(value = "/upload", method = RequestMethod.POST) + public String updateMovieImage(@RequestParam("file") MultipartFile file, @RequestParam("id") Long id) { + logger.debug(file.getOriginalFilename()); + + String newName = id + "." + FilenameUtils.getExtension(file.getOriginalFilename()); + String imageUri = this.azureStorageUploader.uploadToAzureStorage(file, newName.toLowerCase()); + + if (imageUri != null) { + Timestamp timestamp = new Timestamp(Calendar.getInstance().getTime().getTime()); + String timestampQuery = "?timestamp=" + timestamp.toString(); + + Movie movie = new Movie(); + movie.setImageUri(imageUri.toLowerCase() + timestampQuery); + movieRepository.patchMovie(Long.toString(id), movie); + } + + return "redirect:/movies/" + id; + } + + private void setupMovieList(MoviesResponse moviesResponse, Model model) { + setupMovieListPageInfo(moviesResponse, model); + setupMovieListThumbnail(moviesResponse, model); + } + + private void setupMovieListPageInfo(MoviesResponse moviesResponse, Model model) { + final String movielistPath = "/movies?page="; + PageInfo page = moviesResponse.getPage(); + if (page != null) { + Integer number = page.getNumber(); + Integer prev = number - 1; + Integer next = number + 1; + model.addAttribute("number", next); + String prevPath, nextPath; + if (prev >= 0) { + prevPath = movielistPath + String.valueOf(prev); + model.addAttribute("prev", prevPath); + model.addAttribute("hasprev", true); + } + if (next < page.getTotalPages()) { + nextPath = movielistPath + String.valueOf(next); + model.addAttribute("next", nextPath); + model.addAttribute("hasnext", true); + } + } + } + + private void setupMovieListThumbnail(MoviesResponse moviesResponse, Model model) { + List movies = moviesResponse.getMovieList().getMovies(); + for (Movie movie : movies) { + if (movie.getImageUri() != null) { + String thumbnailUri = movie.getImageUri().replace( + azureStorageUploader.getOriginalImageContainer().toLowerCase(), + azureStorageUploader.getThumbnailImageContainer().toLowerCase()); + + movie.setThumbnailFullPathUri(azureStorageUploader.getAzureStorageBaseUri() + thumbnailUri); + } + } + + model.addAttribute("movies", movies); + } +} diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/MovieRepository.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/MovieRepository.java new file mode 100644 index 0000000..7448cf6 --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/MovieRepository.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + + +package com.microsoft.azure.java.samples.moviedb.web; + +import com.microsoft.azure.java.samples.moviedb.web.pojo.Movie; +import com.microsoft.azure.java.samples.moviedb.web.pojo.MoviesResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +/** + * Wrapper for sending rest api request to data app with redis cache support. + */ +@Component +public class MovieRepository { + private static final String PATH_MOVIE_SEARCH_BY_ID = "/movies/"; + private static final String PATH_MOVIE_SEARCH_BY_PAGE = "/movies?page="; + private static final Logger logger = LoggerFactory.getLogger(MovieRepository.class); + private final RestTemplate restTemplate; + + /** + * Construct rest template with data app uri. + * + * @param builder rest template builder + * @param dataAppUri data app uri from application.properties + */ + public MovieRepository(RestTemplateBuilder builder, @Value("${moviedb.webapp.dataAppUri}") String dataAppUri) { + logger.debug("data app:" + dataAppUri); + + String trimmedURL = dataAppUri.trim().toLowerCase(); + String dataAppApiUrl; + if (trimmedURL.startsWith("http://") || trimmedURL.startsWith("https://")) { + dataAppApiUrl = trimmedURL + "/api/v1"; + } else { + dataAppApiUrl = "http://" + trimmedURL + "/api/v1"; + } + + logger.debug("data app api root url: " + dataAppApiUrl); + restTemplate = builder.rootUri(dataAppApiUrl).build(); + restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory()); + } + + /** + * Get movie list by page number. + * + * @param page page number + * @return response object that contains movie list and page info + */ + public MoviesResponse getMovies(String page) { + String requestPath = PATH_MOVIE_SEARCH_BY_PAGE + page; + logger.debug(requestPath); + try { + return this.restTemplate.getForObject(requestPath, MoviesResponse.class); + } catch (Exception e) { + e.printStackTrace(); + logger.error("Error requesting movies: " + e.getMessage()); + } + return null; + } + + /** + * Get movie by movie id. + * + * @param id movie id + * @return movie object + */ + @Cacheable(cacheNames = "movie", key = "#id") + public Movie getMovie(String id) { + String requestPath = PATH_MOVIE_SEARCH_BY_ID + id; + logger.debug(requestPath); + try { + return this.restTemplate.getForObject(requestPath, Movie.class); + } catch (Exception e) { + e.printStackTrace(); + logger.error("Error requesting movie: " + e.getMessage()); + } + return null; + } + + /** + * Patch movie by movie id. + * + * @param id movie id + * @param movie movie object + */ + @CacheEvict(cacheNames = "movie", key = "#id") + public void patchMovie(String id, Movie movie) { + try { + this.restTemplate.patchForObject("/movies/" + id, new HttpEntity<>(movie), Void.class); + } catch (Exception e) { + e.printStackTrace(); + logger.error("Error patching movie: " + e.getMessage()); + } + } +} diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/SecurityConfig.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/SecurityConfig.java new file mode 100644 index 0000000..eb2616e --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/SecurityConfig.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.java.samples.moviedb.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; +import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.client.OAuth2ClientContext; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter; +import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter; +import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; + +import javax.servlet.Filter; + +@EnableWebSecurity +@EnableOAuth2Client +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + OAuth2ClientContext oauth2ClientContext; + + @Override + protected void configure(HttpSecurity http) throws Exception { + boolean usingFacebookAuthentication = facebook().getClientId() != null && !facebook().getClientId().isEmpty(); + if (usingFacebookAuthentication) { + // @formatter:off + http.antMatcher("/**").authorizeRequests().antMatchers("/**").permitAll().anyRequest() + .authenticated().and().exceptionHandling() + .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")).and().logout() + .logoutSuccessUrl("/").permitAll().and().csrf() + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and() + .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class); + // @formatter:on + } + else{ + http.antMatcher("/**").authorizeRequests().anyRequest().permitAll(); + } + } + + private Filter ssoFilter() { + OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter("/login"); + OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext); + facebookFilter.setRestTemplate(facebookTemplate); + UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId()); + tokenServices.setRestTemplate(facebookTemplate); + facebookFilter.setTokenServices(tokenServices); + //TODO: this seems to break redirect to the referrer from facebook.com + SavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + authenticationSuccessHandler.setUseReferer(true); + facebookFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler); + return facebookFilter; + } + + @Bean + public FilterRegistrationBean oauth2ClientFilterRegistration( + OAuth2ClientContextFilter filter) { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(filter); + registration.setOrder(-100); + return registration; + } + + @Bean + @ConfigurationProperties("facebook.client") + public AuthorizationCodeResourceDetails facebook() { + return new AuthorizationCodeResourceDetails(); + } + + @Bean + @ConfigurationProperties("facebook.resource") + public ResourceServerProperties facebookResource() { + return new ResourceServerProperties(); + } +} diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/WebApplication.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/WebApplication.java new file mode 100644 index 0000000..e877b16 --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/WebApplication.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + + +package com.microsoft.azure.java.samples.moviedb.web; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; + +/** + * Entry point of spring boot web application. + */ +@SpringBootApplication +@EnableCaching +public class WebApplication { + + /** + * main entry point. + * + * @param args the parameters + */ + public static void main(String[] args) { + SpringApplication.run(WebApplication.class, args); + } +} \ No newline at end of file diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/package-info.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/package-info.java new file mode 100644 index 0000000..e4ca190 --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/package-info.java @@ -0,0 +1,10 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +/** + * The movie db web app. + */ +package com.microsoft.azure.java.samples.moviedb.web; \ No newline at end of file diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/Movie.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/Movie.java new file mode 100644 index 0000000..3362da0 --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/Movie.java @@ -0,0 +1,167 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + + +package com.microsoft.azure.java.samples.moviedb.web.pojo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.io.Serializable; + +/** + * Movie class that contains all movie properties. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class Movie implements Serializable { + private Long id; + private String name; + private String description; + private Double rating; + private String imageUri; + private String imageFullPathUri; + private String thumbnailFullPathUri; + + /** + * Get movie id. + * + * @return movie id + */ + public Long getId() { + return this.id; + } + + /** + * Set movie id. + * + * @param id movie id + */ + public void setId(Long id) { + this.id = id; + } + + /** + * Get movie name. + * + * @return movie name + */ + public String getName() { + return name; + } + + /** + * Set movie name. + * + * @param name movie name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Get movie description. + * + * @return movie description + */ + public String getDescription() { + return description; + } + + /** + * Set movie description. + * + * @param description movie description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Get movie rating. + * + * @return movie rating + */ + public Double getRating() { + return rating; + } + + /** + * Set movie rating. + * + * @param rating movie rating + */ + public void setRating(Double rating) { + this.rating = rating; + } + + /** + * Get image uri. + * + * @return image uri + */ + public String getImageUri() { + return this.imageUri; + } + + /** + * Set image uri. + * + * @param imageUri image uri + */ + public void setImageUri(String imageUri) { + this.imageUri = imageUri; + } + + /** + * Get full path image uri. + * + * @return full path image uri. + */ + public String getImageFullPathUri() { + return this.imageFullPathUri; + } + + /** + * Set full path image uri. + * + * @param imageFullPathUri full path image uri + */ + public void setImageFullPathUri(String imageFullPathUri) { + this.imageFullPathUri = imageFullPathUri; + } + + /** + * Get full path thumbnail uri. + * + * @return full path thumbnail uri + */ + public String getThumbnailFullPathUri() { + return this.thumbnailFullPathUri; + } + + /** + * Set full path thumbnail uri. + * + * @param thumbnailFullPathUri full path thumbnail uri + */ + public void setThumbnailFullPathUri(String thumbnailFullPathUri) { + this.thumbnailFullPathUri = thumbnailFullPathUri; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("Movie: {"); + builder.append("id: ").append(id != null ? id : "").append(","); + builder.append("name: ").append(name != null ? name : "").append(","); + builder.append("description: ").append(description != null ? description : "").append(","); + builder.append("rating: ").append(rating != null ? rating : "").append(","); + builder.append("imageFullPathUri: ").append(imageFullPathUri != null ? imageFullPathUri : "").append(","); + builder.append("thumbnailFullPathUri: ").append(thumbnailFullPathUri != null ? thumbnailFullPathUri : ""); + builder.append("}"); + return builder.toString(); + } +} \ No newline at end of file diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/MovieList.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/MovieList.java new file mode 100644 index 0000000..3896c21 --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/MovieList.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + + +package com.microsoft.azure.java.samples.moviedb.web.pojo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.io.Serializable; +import java.util.List; + +/** + * Class that contains a list of movies. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class MovieList implements Serializable { + private List movies; + + /** + * Get movie list. + * + * @return movie list + */ + public List getMovies() { + return movies; + } +} diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/MoviesResponse.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/MoviesResponse.java new file mode 100644 index 0000000..33bb40d --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/MoviesResponse.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + + +package com.microsoft.azure.java.samples.moviedb.web.pojo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; + +/** + * Class corresponds to JSON response of get movies rest api. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class MoviesResponse implements Serializable { + @JsonProperty("_embedded") + private MovieList movieList; + private PageInfo page; + + /** + * Get movie list. + * + * @return movie list + */ + @JsonProperty("_embedded") + public MovieList getMovieList() { + return movieList; + } + + /** + * Get page information. + * + * @return page information + */ + public PageInfo getPage() { + return this.page; + } +} diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/PageInfo.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/PageInfo.java new file mode 100644 index 0000000..b0977c3 --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/PageInfo.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + + +package com.microsoft.azure.java.samples.moviedb.web.pojo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.io.Serializable; + +/** + * Definition for page info. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class PageInfo implements Serializable { + private Integer size; + private Integer totalElements; + private Integer totalPages; + private Integer number; + + /** + * Get page size. + * + * @return page size. + */ + public Integer getSize() { + return this.size; + } + + /** + * Get the number of all elements. + * + * @return number of all elements + */ + public Integer getTotalElements() { + return this.totalElements; + } + + /** + * Get total number of pages. + * + * @return number of pages + */ + public Integer getTotalPages() { + return this.totalPages; + } + + /** + * Get page number. + * + * @return page number + */ + public Integer getNumber() { + return this.number; + } +} diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/UserInfo.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/UserInfo.java new file mode 100644 index 0000000..7a99b8b --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/UserInfo.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.java.samples.moviedb.web.pojo; + + +public class UserInfo { + private String displayName; + private Boolean isAllowedToUpdateMovieDB; + + public UserInfo(String displayName, Boolean isAllowedToUpdateMovieDB) { + this.displayName = displayName; + this.isAllowedToUpdateMovieDB = isAllowedToUpdateMovieDB; + } + + public String getDisplayName() { + return this.displayName; + } + + public Boolean getIsAllowedToUpdateMovieDB() { + return this.isAllowedToUpdateMovieDB; + } +} diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/package-info.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/package-info.java new file mode 100644 index 0000000..737ae89 --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/pojo/package-info.java @@ -0,0 +1,10 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +/** + * Holds all Java object definition. + */ +package com.microsoft.azure.java.samples.moviedb.web.pojo; \ No newline at end of file diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/util/AzureStorageUploader.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/util/AzureStorageUploader.java new file mode 100644 index 0000000..c08d459 --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/util/AzureStorageUploader.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + + +package com.microsoft.azure.java.samples.moviedb.web.util; + +import com.microsoft.azure.storage.CloudStorageAccount; +import com.microsoft.azure.storage.blob.BlobContainerPermissions; +import com.microsoft.azure.storage.blob.BlobContainerPublicAccessType; +import com.microsoft.azure.storage.blob.CloudBlobClient; +import com.microsoft.azure.storage.blob.CloudBlobContainer; +import com.microsoft.azure.storage.blob.CloudBlockBlob; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.net.URISyntaxException; +import java.security.InvalidKeyException; + +/** + * Helper class that provides function to upload image to Azure storage. + */ +@Component +public class AzureStorageUploader { + private static final Logger logger = LoggerFactory.getLogger(AzureStorageUploader.class); + + private final String storageConnectionString; + private final String originalImageContainer; + private final String thumbnailImageContainer; + + private String azureStorageBaseUri; + + /** + * Constructor that accepts settings from property file. + * + * @param storageConnectionString Azure storage connection string + * @param originalImageContainer storage container name for original images + * @param thumbnailImageContainer storage container name for thumbnail images + */ + public AzureStorageUploader(@Value("${moviedb.webapp.storageconnectionstring}") String storageConnectionString, + @Value("${moviedb.webapp.originalImageContainer}") String originalImageContainer, + @Value("${moviedb.webapp.thumbnailImageContainer}") String thumbnailImageContainer) { + logger.debug(storageConnectionString); + logger.debug(originalImageContainer); + logger.debug(thumbnailImageContainer); + + this.storageConnectionString = storageConnectionString; + this.originalImageContainer = (originalImageContainer == null || originalImageContainer.isEmpty()) + ? "images-original" : originalImageContainer; + this.thumbnailImageContainer = (thumbnailImageContainer == null || thumbnailImageContainer.isEmpty()) + ? "images-thumbnail" : thumbnailImageContainer; + } + + /** + * Get container name of original images. + * + * @return container name + */ + public String getOriginalImageContainer() { + return originalImageContainer; + } + + /** + * Get container name of thumbnail images. + * + * @return container name + */ + public String getThumbnailImageContainer() { + return thumbnailImageContainer; + } + + /** + * Get the base URI of Azure storage. + * + * @return base URI string + */ + public String getAzureStorageBaseUri() { + if (azureStorageBaseUri == null) { + CloudStorageAccount storageAccount; + try { + storageAccount = CloudStorageAccount.parse(this.storageConnectionString); + azureStorageBaseUri = "https://" + storageAccount.createCloudBlobClient().getEndpoint().getHost(); + } catch (InvalidKeyException e) { + e.printStackTrace(); + logger.error("InvalidKeyException: " + e.getMessage()); + } catch (URISyntaxException e) { + e.printStackTrace(); + logger.error("URISyntaxException: " + e.getMessage()); + } + } + + return azureStorageBaseUri; + } + + /** + * Upload image file to Azure storage with specified name. + * + * @param file image file object + * @param fileName specified file name + * @return relative path of the created image blob + */ + public String uploadToAzureStorage(MultipartFile file, String fileName) { + String uri = null; + + try { + CloudStorageAccount storageAccount = CloudStorageAccount.parse(this.storageConnectionString); + CloudBlobClient blobClient = storageAccount.createCloudBlobClient(); + + setupContainer(blobClient, this.thumbnailImageContainer); + CloudBlobContainer originalImageContainer = setupContainer(blobClient, this.originalImageContainer); + + if (originalImageContainer != null) { + CloudBlockBlob blob = originalImageContainer.getBlockBlobReference(fileName); + blob.upload(file.getInputStream(), file.getSize()); + + uri = blob.getUri().getPath(); + } + } catch (Exception e) { + e.printStackTrace(); + logger.error("Error uploading image: " + e.getMessage()); + } + + return uri; + } + + private CloudBlobContainer setupContainer(CloudBlobClient blobClient, String containerName) { + try { + CloudBlobContainer container = blobClient.getContainerReference(containerName); + if (!container.exists()) { + container.createIfNotExists(); + BlobContainerPermissions containerPermissions = new BlobContainerPermissions(); + containerPermissions.setPublicAccess(BlobContainerPublicAccessType.CONTAINER); + container.uploadPermissions(containerPermissions); + } + + return container; + } catch (Exception e) { + e.printStackTrace(); + logger.error("Error setting up container: " + e.getMessage()); + return null; + } + } +} diff --git a/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/util/package-info.java b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/util/package-info.java new file mode 100644 index 0000000..ecdb15c --- /dev/null +++ b/web-app/src/main/java/com/microsoft/azure/java/samples/moviedb/web/util/package-info.java @@ -0,0 +1,10 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +/** + * utility package. + */ +package com.microsoft.azure.java.samples.moviedb.web.util; \ No newline at end of file diff --git a/web-app/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/web-app/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..f59b9f2 --- /dev/null +++ b/web-app/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,24 @@ +{ + "properties": [ + { + "name": "moviedb.webapp.dataAppUri", + "type": "java.lang.String", + "description": "The api endpoint for data-app." + }, + { + "name": "moviedb.webapp.storageconnectionstring", + "type": "java.lang.String", + "description": "Storage connection string for storing uploaded images." + }, + { + "name": "moviedb.webapp.originalImageContainer", + "type": "java.lang.String", + "description": "Container name for original images." + }, + { + "name": "moviedb.webapp.thumbnailImageContainer", + "type": "java.lang.String", + "description": "Container name for thumbnail images." + } + ] +} \ No newline at end of file diff --git a/web-app/src/main/resources/application-dev.properties b/web-app/src/main/resources/application-dev.properties new file mode 100644 index 0000000..ded0836 --- /dev/null +++ b/web-app/src/main/resources/application-dev.properties @@ -0,0 +1,5 @@ +# development environment variables +moviedb.webapp.dataAppUri=http://localhost:8090/ + +# Disable cache for local environment +spring.cache.type=none \ No newline at end of file diff --git a/web-app/src/main/resources/application-prod.properties b/web-app/src/main/resources/application-prod.properties new file mode 100644 index 0000000..5080aff --- /dev/null +++ b/web-app/src/main/resources/application-prod.properties @@ -0,0 +1,6 @@ +# production environment variables +moviedb.webapp.dataAppUri=${DATA_API_URL} + +# Redis Cache +spring.redis.host=${REDIS_HOST} +spring.redis.password=${REDIS_PASSWORD} \ No newline at end of file diff --git a/web-app/src/main/resources/application.properties b/web-app/src/main/resources/application.properties new file mode 100644 index 0000000..9e7038b --- /dev/null +++ b/web-app/src/main/resources/application.properties @@ -0,0 +1,26 @@ +#spring.profiles.active +spring.profiles.active=prod + +server.port=${WEB_APP_CONTAINER_PORT} + +logging.level.root=WARN +logging.level.com.microsoft.azure.java.samples.moviedb=DEBUG +logging.level.org.springframework.web=INFO +logging.level.org.hibernate=ERROR +logging.file=logs/application.log + +moviedb.webapp.storageconnectionstring=${STORAGE_CONNECTION_STRING} +moviedb.webapp.originalImageContainer=${ORIGINAL_IMAGE_CONTAINER} +moviedb.webapp.thumbnailImageContainer=${THUMBNAIL_IMAGE_CONTAINER} + +spring.http.multipart.max-file-size=20MB +spring.http.multipart.max-request-size=20MB + +facebook.client.client-id=${FACEBOOK_APP_ID:} +facebook.client.client-secret=${FACEBOOK_APP_SECRET:} +facebook.client.access-token-uri=https://graph.facebook.com/oauth/access_token +facebook.client.user-authorization-uri=https://www.facebook.com/dialog/oauth +facebook.client.token-name=oauth_token +facebook.client.authentication-scheme=query +facebook.client.client-authentication-scheme=form +facebook.resource.user-info-uri=https://graph.facebook.com/me \ No newline at end of file diff --git a/web-app/src/main/resources/application.test.properties b/web-app/src/main/resources/application.test.properties new file mode 100644 index 0000000..0f0687c --- /dev/null +++ b/web-app/src/main/resources/application.test.properties @@ -0,0 +1,19 @@ +logging.level.root=WARN +logging.level.com.microsoft.azure.java.samples.moviedb=DEBUG +logging.level.org.springframework.web=INFO +logging.level.org.hibernate=ERROR +logging.file=logs/application.log + +moviedb.webapp.storageconnectionstring=${STORAGE_CONNECTION_STRING} +moviedb.webapp.originalImageContainer=${ORIGINAL_IMAGE_CONTAINER} +moviedb.webapp.thumbnailImageContainer=${THUMBNAIL_IMAGE_CONTAINER} + +spring.http.multipart.max-file-size=20MB +spring.http.multipart.max-request-size=20MB + +# production environment variables +moviedb.webapp.dataAppUri=${DATA_API_URL} + +# Redis Cache +spring.redis.host=${REDIS_HOST} +spring.redis.password=${REDIS_PASSWORD} \ No newline at end of file diff --git a/web-app/src/main/resources/applicationContext-test.xml b/web-app/src/main/resources/applicationContext-test.xml new file mode 100644 index 0000000..1cf75d4 --- /dev/null +++ b/web-app/src/main/resources/applicationContext-test.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/web-app/src/main/resources/static/cover.css b/web-app/src/main/resources/static/cover.css new file mode 100644 index 0000000..cb7e84e --- /dev/null +++ b/web-app/src/main/resources/static/cover.css @@ -0,0 +1,147 @@ +/* + * Globals + */ + +/* Links */ +a, +a:focus, +a:hover { + color: #fff; +} + +/* + * Base structure + */ + +html, +body { + height: 100%; + /*background-color: #555;*/ +} + +body { + /*color: #fff;*/ + text-align: center; + text-shadow: 0 1px 3px rgba(0, 0, 0, .5); +} + +/* Extra markup and styles for table-esque vertical and horizontal centering */ +.site-wrapper { + display: table; + width: 100%; + height: 100%; /* For at least Firefox */ + min-height: 100%; + /* + -webkit-box-shadow: inset 0 0 100px rgba(0,0,0,.5); + box-shadow: inset 0 0 100px rgba(0,0,0,.5); + */ +} + +.site-wrapper-inner { + display: table-cell; + vertical-align: top; +} + +.cover-container { + margin-right: auto; + margin-left: auto; + margin-bottom: 150px; +} + +/* Padding for spacing */ +.inner { + padding: 30px; +} + +/* + * Header + */ +.masthead-nav > li { + display: inline-block; +} + +.masthead-nav > li + li { + margin-left: 20px; +} + +.masthead-nav > li > a { + padding-right: 0; + padding-left: 0; + font-size: 16px; + font-weight: bold; + color: #fff; /* IE8 proofing */ + color: rgba(255, 255, 255, .75); + border-bottom: 2px solid transparent; +} + +.masthead-nav > li > a:hover, +.masthead-nav > li > a:focus { + background-color: transparent; + border-bottom-color: #a9a9a9; + border-bottom-color: rgba(255, 255, 255, .25); +} + +.masthead-nav > .active > a, +.masthead-nav > .active > a:hover, +.masthead-nav > .active > a:focus { + color: #fff; + border-bottom-color: #fff; +} + +/* + * Cover + */ + +.cover { + padding: 0 20px; +} + +.cover .btn-lg { + padding: 10px 20px; + font-weight: bold; +} + +.cover h1 { + font-size: 45px; + margin-bottom: 150px; +} + +/* + * Footer + */ + +.mastfoot { + color: #999; /* IE8 proofing */ + /*color: rgba(255,255,255,.5);*/ + font-size: 16px; +} + +/* + * Affix and center + */ + +@media (min-width: 768px) { + /* Pull out the header and footer */ + .mastfoot { + position: fixed; + bottom: 0; + } + + /* Start the vertical centering */ + .site-wrapper-inner { + vertical-align: middle; + } + + /* Handle the widths */ + .mastfoot, + .cover-container { + width: 100%; /* Must be percentage or pixels for horizontal alignment */ + } +} + +@media (min-width: 992px) { + .mastfoot, + .cover-container { + width: 700px; + } +} diff --git a/web-app/src/main/resources/static/index.html b/web-app/src/main/resources/static/index.html new file mode 100644 index 0000000..fa8e620 --- /dev/null +++ b/web-app/src/main/resources/static/index.html @@ -0,0 +1,50 @@ + + + + Movie DB on Azure + + + + + + + + + +
+
+
+
+

Welcome to Movie DB on Azure

+

+ Top Rated Movies +

+
+
+
+

+ Hosted by + + with +

+
+
+
+
+
+ + + + + diff --git a/web-app/src/main/resources/static/original.png b/web-app/src/main/resources/static/original.png new file mode 100644 index 0000000..564416b Binary files /dev/null and b/web-app/src/main/resources/static/original.png differ diff --git a/web-app/src/main/resources/static/thumbnail.png b/web-app/src/main/resources/static/thumbnail.png new file mode 100644 index 0000000..a269cfa Binary files /dev/null and b/web-app/src/main/resources/static/thumbnail.png differ diff --git a/web-app/src/main/resources/templates/fragments/header.html b/web-app/src/main/resources/templates/fragments/header.html new file mode 100644 index 0000000..ad20e92 --- /dev/null +++ b/web-app/src/main/resources/templates/fragments/header.html @@ -0,0 +1,39 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/web-app/src/main/resources/templates/index.html b/web-app/src/main/resources/templates/index.html new file mode 100644 index 0000000..dcc4832 --- /dev/null +++ b/web-app/src/main/resources/templates/index.html @@ -0,0 +1,50 @@ + + + + Movie DB on Azure + + + + + + + + + +
+
+
+
+

Welcome to Movie DB on Azure

+

+ Top Rated Movies +

+
+
+
+

+ Hosted by + + with +

+
+
+
+
+
+ + + + + diff --git a/web-app/src/main/resources/templates/login.html b/web-app/src/main/resources/templates/login.html new file mode 100644 index 0000000..078372e --- /dev/null +++ b/web-app/src/main/resources/templates/login.html @@ -0,0 +1,11 @@ + + + + + Movie DB on Azure + + +

Login Error

+

Reaching this page most likely means that you haven't configured Facebook authentication per instructions provided in the readme.md located in the root of the Github repository

+ + \ No newline at end of file diff --git a/web-app/src/main/resources/templates/moviedetail.html b/web-app/src/main/resources/templates/moviedetail.html new file mode 100644 index 0000000..a22f0b0 --- /dev/null +++ b/web-app/src/main/resources/templates/moviedetail.html @@ -0,0 +1,94 @@ + + + + + Movie + + + + + + + + + + + + +
+
...
+ +
+ + +

+

+

+
+
+ +
+
+ +
+
+ +
+ + + + + + + + + +
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/web-app/src/main/resources/templates/moviedetailerror.html b/web-app/src/main/resources/templates/moviedetailerror.html new file mode 100644 index 0000000..914cf41 --- /dev/null +++ b/web-app/src/main/resources/templates/moviedetailerror.html @@ -0,0 +1,10 @@ + + + + + Movie doesn't exist + + +

The requested movie doesn't exist. It's also possible that the data app is down.

+ + \ No newline at end of file diff --git a/web-app/src/main/resources/templates/moviespage.html b/web-app/src/main/resources/templates/moviespage.html new file mode 100644 index 0000000..487154e --- /dev/null +++ b/web-app/src/main/resources/templates/moviespage.html @@ -0,0 +1,62 @@ + + + + Top Rated Movies + + + + + + +
+
...
+ + +
+
+ + + + + + + + + + + + +
Rank & TitleRating
+ + + + Rating ...
+
+
+ Prev + Next +
+
+
+ + + + + + \ No newline at end of file diff --git a/web-app/src/test/java/com/microsoft/azure/java/samples/moviedb/web/ControllerTest.java b/web-app/src/test/java/com/microsoft/azure/java/samples/moviedb/web/ControllerTest.java new file mode 100644 index 0000000..2bb823e --- /dev/null +++ b/web-app/src/test/java/com/microsoft/azure/java/samples/moviedb/web/ControllerTest.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for + * license information. + */ + +package com.microsoft.azure.java.samples.moviedb.web; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@TestPropertySource(locations = "classpath:application.test.properties") +@ContextConfiguration(locations = {"classpath:/applicationContext-test.xml"}) +public class ControllerTest extends AbstractJUnit4SpringContextTests { + @Autowired + private MovieController movieController; + private MockMvc mockMvc; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + this.mockMvc = MockMvcBuilders.standaloneSetup(movieController).build(); + } + + @Test + public void testGetMovie() throws Exception { + this.mockMvc.perform(get("/movies/1")) + .andExpect(status().isOk()) + .andExpect(model().attributeExists("movie")); + } + + @Test + public void testGetMovies() throws Exception { + this.mockMvc.perform(get("/movies")) + .andExpect(status().isOk()) + .andExpect(model().attributeExists("movies")); + } + +} \ No newline at end of file