This commit is contained in:
ZhijunZhao 2017-06-04 23:39:48 -07:00 коммит произвёл Zhijun Zhao
Коммит 2eb4b9e63b
89 изменённых файлов: 6097 добавлений и 0 удалений

2
.deployment Normal file
Просмотреть файл

@ -0,0 +1,2 @@
[config]
command = ./deployment/function/deploy.cmd

31
.gitignore поставляемый Normal file
Просмотреть файл

@ -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/

44
Jenkinsfile поставляемый Normal file
Просмотреть файл

@ -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)
}
}

21
LICENSE Normal file
Просмотреть файл

@ -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

399
README.md Normal file
Просмотреть файл

@ -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 `<servers>` collection in the the *settings.xml* file, this will enable Maven to use a private registry; for example:
```xml
<servers>
<server>
<id>${env.ACR_NAME}</id>
<username>${env.ACR_USERNAME}</username>
<password>${env.ACR_PASSWORD}</password>
<configuration>
<email>john_doe@contoso.com</email>
</configuration>
</server>
</servers>
```
c. Save and close your *settings.xml* file.
<a name="create-the-initial-build"></a>
### 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 "<your-azure-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 ###
<!--
> **NOTE**: Detailed notes will be included here at a later date.
-->
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=<your-new-relic-license-key>
export WEBAPP_NEW_RELIC_APP_NAME=<app-name-in-new-relic>
```
- 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=<your-overops-sk>
```
- 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}
```
<!--
> **NOTE**: Add a screenshot of the Jenkins dashboard here
> **NOTE**: Add the steps to login with the Jenkins CLI
-->
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
<h1 class="cover-heading">Welcome to My Cool Movie DB on Azure!!!</h1>
```
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
<!-- The Spring Boot app starts up using secrets stored in Azure Key Vault. App uses Spring Cloud Vault to access secrets inside Azure Key Vault, a "Secret Backend" -->
- 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
<!-- The Spring Boot app starts up using secrets stored in Azure Key Vault. Uses Spring Cloud Vault to access secrets from Azure Key Vault, a "Secret Backend" -->
- 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.

5
data-app/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,5 @@
target/
.settings/
logs/
.classpath
.project

208
data-app/pom.xml Normal file
Просмотреть файл

@ -0,0 +1,208 @@
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.azure.java.samples.moviedb</groupId>
<artifactId>data-app</artifactId>
<packaging>jar</packaging>
<version>0.1.0-SNAPSHOT</version>
<parent>
<groupId>com.microsoft.azure.java.samples.moviedb</groupId>
<artifactId>movie-db-java-on-azure</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<docker.image.prefix>${env.ACR_LOGIN_SERVER}</docker.image.prefix>
<newrelic.version>3.37.0</newrelic.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<!-- JPA Data (We are going to use Repositories, Entities, Hibernate, etc...) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Use MySQL Connector-J -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.newrelic.agent.java</groupId>
<artifactId>newrelic-java</artifactId>
<version>${newrelic.version}</version>
<scope>provided</scope>
<type>zip</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>unpack-zip</id>
<phase>package</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>com.newrelic.agent.java</groupId>
<artifactId>newrelic-java</artifactId>
<version>${newrelic.version}</version>
<type>zip</type>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}</outputDirectory>
<destFileName>newrelic</destFileName>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/newrelic</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.5.3.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.4.11</version>
<configuration>
<serverId>${env.ACR_NAME}</serverId>
<registryUrl>https://${env.ACR_LOGIN_SERVER}</registryUrl>
<dockerDirectory>src/main/docker/base</dockerDirectory>
<imageName>${docker.image.prefix}/${project.artifactId}</imageName>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
<executions>
<execution>
<id>with-new-relic</id>
<goals>
<goal>build</goal>
</goals>
<configuration>
<dockerDirectory>src/main/docker/new-relic</dockerDirectory>
<imageName>${docker.image.prefix}/${project.artifactId}-w-new-relic</imageName>
<buildArgs>
<NEW_RELIC_LICENSE_KEY>${env.NEW_RELIC_LICENSE_KEY}</NEW_RELIC_LICENSE_KEY>
<NEW_RELIC_APP_NAME>${env.DATAAPP_NEW_RELIC_APP_NAME}</NEW_RELIC_APP_NAME>
</buildArgs>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}/newrelic</directory>
<include>newrelic.jar</include>
</resource>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}/newrelic</directory>
<include>newrelic.yml</include>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>with-overops</id>
<goals>
<goal>build</goal>
</goals>
<configuration>
<dockerDirectory>src/main/docker/overops</dockerDirectory>
<imageName>${docker.image.prefix}/${project.artifactId}-w-overops</imageName>
<buildArgs>
<OVEROPSSK>${env.OVEROPSSK}</OVEROPSSK>
</buildArgs>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>all</id>
<goals>
<goal>build</goal>
</goals>
<configuration>
<dockerDirectory>src/main/docker/all</dockerDirectory>
<imageName>${docker.image.prefix}/${project.artifactId}-w-all</imageName>
<buildArgs>
<NEW_RELIC_LICENSE_KEY>${env.NEW_RELIC_LICENSE_KEY}</NEW_RELIC_LICENSE_KEY>
<NEW_RELIC_APP_NAME>${env.DATAAPP_NEW_RELIC_APP_NAME}</NEW_RELIC_APP_NAME>
<OVEROPSSK>${env.OVEROPSSK}</OVEROPSSK>
</buildArgs>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}/newrelic</directory>
<include>newrelic.jar</include>
</resource>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}/newrelic</directory>
<include>newrelic.yml</include>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Просмотреть файл

@ -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

Просмотреть файл

@ -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" ]

Просмотреть файл

@ -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" ]

Просмотреть файл

@ -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

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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<Movie, Long> {
/**
* Provides find movie by id API.
*
* @param id movie id
* @return movie object
*/
Movie findOne(@Param("id") Long id);
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -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.', '');

Просмотреть файл

@ -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)
);

Просмотреть файл

@ -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));
}
}

35
database/data/data.sql Normal file
Просмотреть файл

@ -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.");

89
database/pom.xml Normal file
Просмотреть файл

@ -0,0 +1,89 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License. See License.txt in the project root for
license information.
Run "SET MYSQL_SERVER_ENDPOINT=jdbc:mysql://localhost:3306/"
Run "SET MYSQL_USERNAME=root"
Run "SET MYSQL_PASSWORD=YourSecretPassword"
Run "mvn sql:execute" to populate the database.
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.azure.java.samples.moviedb</groupId>
<artifactId>database-deployment</artifactId>
<version>0.1.0-SNAPSHOT</version>
<parent>
<groupId>com.microsoft.azure.java.samples.moviedb</groupId>
<artifactId>movie-db-java-on-azure</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>sql-maven-plugin</artifactId>
<version>1.5</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
</dependencies>
<configuration>
<driver>com.mysql.cj.jdbc.Driver</driver>
<url>${env.MYSQL_SERVER_ENDPOINT}</url>
<username>${env.MYSQL_USERNAME}</username>
<password>${env.MYSQL_PASSWORD}</password>
</configuration>
<executions>
<execution>
<id>default-cli</id>
<goals>
<goal>execute</goal>
</goals>
<configuration>
<autocommit>true</autocommit>
<srcFiles>
<srcFile>./schema/DDL.sql</srcFile>
<srcFile>./data/data.sql</srcFile>
</srcFiles>
</configuration>
</execution>
<execution>
<id>create-database</id>
<goals>
<goal>execute</goal>
</goals>
<configuration>
<autocommit>true</autocommit>
<srcFiles>
<srcFile>./schema/DDL.sql</srcFile>
</srcFiles>
</configuration>
</execution>
<execution>
<id>populate-data</id>
<goals>
<goal>execute</goal>
</goals>
<configuration>
<autocommit>true</autocommit>
<srcFiles>
<srcFile>./data/data.sql</srcFile>
</srcFiles>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

13
database/schema/DDL.sql Normal file
Просмотреть файл

@ -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`)
);

158
deployment/README.md Normal file
Просмотреть файл

@ -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/<your-github-id>/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": "<your-github-id>"
},
{
"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": "<some-random-suffix>"
}
]
```
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 "<your-azure-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=<your-mysql-admin-password>
export JENKINS_PASSWORD=<your-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`.
<!--
**NOTE**: Follow the steps in the root-level README.md file instead of using the following steps.
1. Run the following command:
```bash
source provision.sh
```
1. Wait for about 16 minutes till all resources are created.
The IP of Jenkins server will be displayed at the end of the output.
During the installation, there might be prompt for your credential for elevated permission to install `kubectl`.
1. Go to the Jenkins server and login with username `jenkins` and password set in step 13.
1. Because our Repo is private right now, you will have to setup credentials to allow Jenkins enlist your repo. Click the pipeline job and configure it.
1. At the pipeline tab, add a new credential with your GitHub account and your personal access token.
Refer to [GitHub document](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) for creating your personal access token.
1. After configuration is saved, click "build now" to trigger the first deployment of web-app and data-app.
1. When deployment is done, go to Azure Portal to find the URL of web-app traffic manager.
Open the URL in browser, then you will see the home page of the web-app.
For more information about using GitHub with Jenkins, see [How to Start Working with the GitHub Plugin for Jenkins](https://www.blazemeter.com/blog/how-start-working-github-plugin-jenkins) for details on how to enable Jenkins triggers every time changes are pushed to GitHub.
-->

Просмотреть файл

@ -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"
}
}
}

Просмотреть файл

@ -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"
}
}
]
}
]
}

Просмотреть файл

@ -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": {}
}

Просмотреть файл

@ -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
}
}
]
}

Просмотреть файл

@ -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"
}
}
}
]
}

70
deployment/arm/redis.json Normal file
Просмотреть файл

@ -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')]"
}
}
}
]
}

Просмотреть файл

@ -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')]"
}
}
}
]
}

118
deployment/config.json Normal file
Просмотреть файл

@ -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": ""
}
]
}

Просмотреть файл

@ -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"

53
deployment/deprovision.sh Executable file
Просмотреть файл

@ -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

55
deployment/dev_setup.sh Normal file
Просмотреть файл

@ -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}

Просмотреть файл

@ -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.

Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -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<PodVolume>()
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<PodEnvVar>()
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')
}

Просмотреть файл

@ -0,0 +1,16 @@
<?xml version='1.0' encoding='UTF-8'?>
<scriptApproval plugin="script-security@1.27">
<approvedScriptHashes>
</approvedScriptHashes>
<approvedSignatures>
<string>method hudson.plugins.git.BranchSpec getName</string>
<string>method hudson.plugins.git.GitSCM getBranches</string>
<string>new groovy.json.JsonSlurperClassic</string>
<string>method groovy.json.JsonSlurperClassic parseText java.lang.String</string>
</approvedSignatures>
<aclApprovedSignatures/>
<approvedClasspathEntries/>
<pendingScripts/>
<pendingSignatures/>
<pendingClasspathEntries/>
</scriptApproval>

Просмотреть файл

@ -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

Просмотреть файл

@ -0,0 +1,265 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<!--
| This is the configuration file for Maven. It can be specified at two levels:
|
| 1. User Level. This settings.xml file provides configuration for a single user,
| and is normally provided in ${user.home}/.m2/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -s /path/to/user/settings.xml
|
| 2. Global Level. This settings.xml file provides configuration for all Maven
| users on a machine (assuming they're all using the same Maven
| installation). It's normally provided in
| ${maven.home}/conf/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -gs /path/to/global/settings.xml
|
| The sections in this sample file are intended to give you a running start at
| getting the most out of your Maven installation. Where appropriate, the default
| values (values used when the setting is not specified) are provided.
|
|-->
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<!-- localRepository
| The path to the local repository maven will use to store artifacts.
|
| Default: ${user.home}/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->
<!-- interactiveMode
| This will determine whether maven prompts you when it needs input. If set to false,
| maven will use a sensible default value, perhaps based on some other setting, for
| the parameter in question.
|
| Default: true
<interactiveMode>true</interactiveMode>
-->
<!-- offline
| Determines whether maven should attempt to connect to the network when executing a build.
| This will have an effect on artifact downloads, artifact deployment, and others.
|
| Default: false
<offline>false</offline>
-->
<!-- pluginGroups
| This is a list of additional group identifiers that will be searched when resolving plugins by their prefix, i.e.
| when invoking a command line like "mvn prefix:goal". Maven will automatically add the group identifiers
| "org.apache.maven.plugins" and "org.codehaus.mojo" if these are not already contained in the list.
|-->
<pluginGroups>
<!-- pluginGroup
| Specifies a further group identifier to use for plugin lookup.
<pluginGroup>com.your.plugins</pluginGroup>
-->
</pluginGroups>
<!-- proxies
| This is a list of proxies which can be used on this machine to connect to the network.
| Unless otherwise specified (by system property or command-line switch), the first proxy
| specification in this list marked as active will be used.
|-->
<proxies>
<!-- proxy
| Specification for one proxy, to be used in connecting to the network.
|
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol>
<username>proxyuser</username>
<password>proxypass</password>
<host>proxy.host.net</host>
<port>80</port>
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>
-->
</proxies>
<!-- servers
| This is a list of authentication profiles, keyed by the server-id used within the system.
| Authentication profiles can be used whenever maven must make a connection to a remote server.
|-->
<servers>
<!-- server
| Specifies the authentication information to use when connecting to a particular server, identified by
| a unique name within the system (referred to by the 'id' attribute below).
|
| NOTE: You should either specify username/password OR privateKey/passphrase, since these pairings are
| used together.
|
<server>
<id>deploymentRepo</id>
<username>repouser</username>
<password>repopwd</password>
</server>
-->
<!-- Another sample, using keys to authenticate.
<server>
<id>siteServer</id>
<privateKey>/path/to/private/key</privateKey>
<passphrase>optional; leave empty if not used.</passphrase>
</server>
-->
<server>
<id>${env.ACR_NAME}</id>
<username>${env.ACR_USERNAME}</username>
<password>${env.ACR_PASSWORD}</password>
<configuration>
<email>foo@foo.bar</email>
</configuration>
</server>
</servers>
<!-- mirrors
| This is a list of mirrors to be used in downloading artifacts from remote repositories.
|
| It works like this: a POM may declare a repository to use in resolving certain artifacts.
| However, this repository may have problems with heavy traffic at times, so people have mirrored
| it to several places.
|
| That repository definition will have a unique id, so we can create a mirror reference for that
| repository, to be used as an alternate download site. The mirror site will be the preferred
| server for that repository.
|-->
<mirrors>
<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->
</mirrors>
<!-- profiles
| This is a list of profiles which can be activated in a variety of ways, and which can modify
| the build process. Profiles provided in the settings.xml are intended to provide local machine-
| specific paths and repository locations which allow the build to work in the local environment.
|
| For example, if you have an integration testing plugin - like cactus - that needs to know where
| your Tomcat instance is installed, you can provide a variable here such that the variable is
| dereferenced during the build process to configure the cactus plugin.
|
| As noted above, profiles can be activated in a variety of ways. One way - the activeProfiles
| section of this document (settings.xml) - will be discussed later. Another way essentially
| relies on the detection of a system property, either matching a particular value for the property,
| or merely testing its existence. Profiles can also be activated by JDK version prefix, where a
| value of '1.4' might activate a profile when the build is executed on a JDK version of '1.4.2_07'.
| Finally, the list of active profiles can be specified directly from the command line.
|
| NOTE: For profiles defined in the settings.xml, you are restricted to specifying only artifact
| repositories, plugin repositories, and free-form properties to be used as configuration
| variables for plugins in the POM.
|
|-->
<profiles>
<!-- profile
| Specifies a set of introductions to the build process, to be activated using one or more of the
| mechanisms described above. For inheritance purposes, and to activate profiles via <activatedProfiles/>
| or the command line, profiles have to have an ID that is unique.
|
| An encouraged best practice for profile identification is to use a consistent naming convention
| for profiles, such as 'env-dev', 'env-test', 'env-production', 'user-jdcasey', 'user-brett', etc.
| This will make it more intuitive to understand what the set of introduced profiles is attempting
| to accomplish, particularly when you only have a list of profile id's for debug.
|
| This profile example uses the JDK version to trigger activation, and provides a JDK-specific repo.
<profile>
<id>jdk-1.4</id>
<activation>
<jdk>1.4</jdk>
</activation>
<repositories>
<repository>
<id>jdk14</id>
<name>Repository for JDK 1.4 builds</name>
<url>http://www.myhost.com/maven/jdk14</url>
<layout>default</layout>
<snapshotPolicy>always</snapshotPolicy>
</repository>
</repositories>
</profile>
-->
<!--
| Here is another profile, activated by the system property 'target-env' with a value of 'dev',
| which provides a specific path to the Tomcat instance. To use this, your plugin configuration
| might hypothetically look like:
|
| ...
| <plugin>
| <groupId>org.myco.myplugins</groupId>
| <artifactId>myplugin</artifactId>
|
| <configuration>
| <tomcatLocation>${tomcatPath}</tomcatLocation>
| </configuration>
| </plugin>
| ...
|
| NOTE: If you just wanted to inject this configuration whenever someone set 'target-env' to
| anything, you could just leave off the <value/> inside the activation-property.
|
<profile>
<id>env-dev</id>
<activation>
<property>
<name>target-env</name>
<value>dev</value>
</property>
</activation>
<properties>
<tomcatPath>/path/to/tomcat/instance</tomcatPath>
</properties>
</profile>
-->
</profiles>
<!-- activeProfiles
| List of profiles that are active for all builds.
|
<activeProfiles>
<activeProfile>alwaysActiveProfile</activeProfile>
<activeProfile>anotherAlwaysActiveProfile</activeProfile>
</activeProfiles>
-->
</settings>

Просмотреть файл

@ -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"

767
deployment/lib.sh Normal file
Просмотреть файл

@ -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 '********************************************************************************'
}

84
deployment/provision.sh Normal file
Просмотреть файл

@ -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'

13
deployment/social.md Normal file
Просмотреть файл

@ -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 <https://developers.facebook.com/>.
* 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.

Просмотреть файл

@ -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);
});
};

Просмотреть файл

@ -0,0 +1,10 @@
{
"name": "resize-image",
"version": "0.0.1",
"private": true,
"main": "index.js",
"author": "Microsoft Corp.",
"dependencies": {
"jimp": "0.2.27"
}
}

Двоичные данные
media/movie-app-layout.jpg Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 149 KiB

66
pom.xml Normal file
Просмотреть файл

@ -0,0 +1,66 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License. See License.txt in the project root for
license information.
-->
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.azure.java.samples.moviedb</groupId>
<artifactId>movie-db-java-on-azure</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Microsoft Azure Movie Database</name>
<description>This package contains the parent module of Microsoft Azure Movie Database.</description>
<url>https://github.com/Microsoft/movie-db-java-on-azure</url>
<licenses>
<license>
<name>The MIT License (MIT)</name>
<url>http://opensource.org/licenses/MIT</url>
<distribution>repo</distribution>
</license>
</licenses>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<legal>
<![CDATA[[INFO] Any downloads listed may be third party software. Microsoft grants you no rights for third party software.]]></legal>
</properties>
<developers>
<developer>
<id>microsoft</id>
<name>Microsoft</name>
</developer>
</developers>
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.4.1</version>
</dependency>
</dependencies>
<modules>
<module>./database</module>
<module>./web-app</module>
<module>./data-app</module>
</modules>
</project>

5
web-app/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,5 @@
target/
.settings/
logs/
.classpath
.project

247
web-app/pom.xml Normal file
Просмотреть файл

@ -0,0 +1,247 @@
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.microsoft.azure.java.samples.moviedb</groupId>
<artifactId>web-app</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>com.microsoft.azure.java.samples.moviedb</groupId>
<artifactId>movie-db-java-on-azure</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<properties>
<java.version>1.8</java.version>
<docker.image.prefix>${env.ACR_LOGIN_SERVER}</docker.image.prefix>
<newrelic.version>3.37.0</newrelic.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.test.skip>true</maven.test.skip>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-storage</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>com.newrelic.agent.java</groupId>
<artifactId>newrelic-java</artifactId>
<version>${newrelic.version}</version>
<scope>provided</scope>
<type>zip</type>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>unpack-zip</id>
<phase>package</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>com.newrelic.agent.java</groupId>
<artifactId>newrelic-java</artifactId>
<version>${newrelic.version}</version>
<type>zip</type>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}</outputDirectory>
<destFileName>newrelic</destFileName>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/newrelic</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.5.3.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.4.11</version>
<configuration>
<serverId>${env.ACR_NAME}</serverId>
<registryUrl>https://${env.ACR_LOGIN_SERVER}</registryUrl>
<dockerDirectory>src/main/docker/base</dockerDirectory>
<imageName>${docker.image.prefix}/${project.artifactId}</imageName>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
<executions>
<execution>
<id>with-new-relic</id>
<goals>
<goal>build</goal>
</goals>
<configuration>
<dockerDirectory>src/main/docker/new-relic</dockerDirectory>
<imageName>${docker.image.prefix}/${project.artifactId}-w-new-relic</imageName>
<buildArgs>
<NEW_RELIC_LICENSE_KEY>${env.NEW_RELIC_LICENSE_KEY}</NEW_RELIC_LICENSE_KEY>
<NEW_RELIC_APP_NAME>${env.WEBAPP_NEW_RELIC_APP_NAME}</NEW_RELIC_APP_NAME>
</buildArgs>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}/newrelic</directory>
<include>newrelic.jar</include>
</resource>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}/newrelic</directory>
<include>newrelic.yml</include>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>with-overops</id>
<goals>
<goal>build</goal>
</goals>
<configuration>
<dockerDirectory>src/main/docker/overops</dockerDirectory>
<imageName>${docker.image.prefix}/${project.artifactId}-w-overops</imageName>
<buildArgs>
<OVEROPSSK>${env.OVEROPSSK}</OVEROPSSK>
</buildArgs>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>all</id>
<goals>
<goal>build</goal>
</goals>
<configuration>
<dockerDirectory>src/main/docker/all</dockerDirectory>
<imageName>${docker.image.prefix}/${project.artifactId}-w-all</imageName>
<buildArgs>
<NEW_RELIC_LICENSE_KEY>${env.NEW_RELIC_LICENSE_KEY}</NEW_RELIC_LICENSE_KEY>
<NEW_RELIC_APP_NAME>${env.WEBAPP_NEW_RELIC_APP_NAME}</NEW_RELIC_APP_NAME>
<OVEROPSSK>${env.OVEROPSSK}</OVEROPSSK>
</buildArgs>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}/newrelic</directory>
<include>newrelic.jar</include>
</resource>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}/newrelic</directory>
<include>newrelic.yml</include>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Просмотреть файл

@ -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

Просмотреть файл

@ -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" ]

Просмотреть файл

@ -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" ]

Просмотреть файл

@ -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

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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";
}
}

Просмотреть файл

@ -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<Movie> 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);
}
}

Просмотреть файл

@ -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());
}
}
}

Просмотреть файл

@ -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();
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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();
}
}

Просмотреть файл

@ -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<Movie> movies;
/**
* Get movie list.
*
* @return movie list
*/
public List<Movie> getMovies() {
return movies;
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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;
}
}
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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."
}
]
}

Просмотреть файл

@ -0,0 +1,5 @@
# development environment variables
moviedb.webapp.dataAppUri=http://localhost:8090/
# Disable cache for local environment
spring.cache.type=none

Просмотреть файл

@ -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}

Просмотреть файл

@ -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

Просмотреть файл

@ -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}

Просмотреть файл

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<!-- Activates scanning of @Autowired -->
<context:annotation-config/>
<!-- Activates scanning of @Repository and @Service -->
<context:component-scan base-package="com.microsoft.azure.java.samples.moviedb.web"/>
</beans>

Просмотреть файл

@ -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;
}
}

Просмотреть файл

@ -0,0 +1,50 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Movie DB on Azure</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous"/>
<!-- Optional theme -->
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
crossorigin="anonymous"/>
<link rel="stylesheet"
href="cover.css"/>
</head>
<body>
<div class="site-wrapper">
<div class="site-wrapper-inner">
<div class="cover-container">
<div class="inner cover">
<h1 class="cover-heading">Welcome to Movie DB on Azure</h1>
<p class="lead">
<a class="btn btn-lg btn-success" href="/movies">Top Rated Movies</a>
</p>
</div>
<div class="mastfoot">
<div class="inner">
<p>
Hosted by
<image src="thumbnail.png"/>
with <span style="font-size: 150%; color: red;">&hearts;</span>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Latest compiled and minified JavaScript -->
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-1.12.4.min.js"
crossorigin="anonymous"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
</body>
</html>

Двоичные данные
web-app/src/main/resources/static/original.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 22 KiB

Двоичные данные
web-app/src/main/resources/static/thumbnail.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.8 KiB

Просмотреть файл

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8"/>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous"/>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
crossorigin="anonymous"/>
</head>
<body>
<nav th:if="${@environment.getProperty('facebook.client.client-id')} != ''" class="navbar navbar-default" data-th-fragment="header">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="#">Movie DB on Azure</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li sec:authorize="isAuthenticated()">
<p class="navbar-text">Signed in as <span th:text="${userInfo.getDisplayName()}">username</span></p>
</li>
<li sec:authorize="isAuthenticated()">
<form action="/logout" th:action="@{/logout}" method="post">
<input class="btn btn-primary btn-small navbar-btn" type="submit" value="Logout"/>
</form>
</li>
<li sec:authorize="isAnonymous()">
<div class="btn-nav"><a class="btn btn-primary btn-small navbar-btn" href="/login">Login with Facebook</a>
</div>
</li>
</ul>
</div>
</div>
</nav>
</body>
</html>

Просмотреть файл

@ -0,0 +1,50 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<title>Movie DB on Azure</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous"/>
<!-- Optional theme -->
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
crossorigin="anonymous"/>
<link rel="stylesheet"
href="cover.css"/>
</head>
<body>
<div class="site-wrapper">
<div class="site-wrapper-inner">
<div class="cover-container">
<div class="inner cover">
<h1 class="cover-heading">Welcome to Movie DB on Azure</h1>
<p class="lead">
<a class="btn btn-lg btn-success" href="/movies">Top Rated Movies</a>
</p>
</div>
<div class="mastfoot">
<div class="inner">
<p>
Hosted by
<image src="thumbnail.png"/>
with <span style="font-size: 150%; color: red;">&hearts;</span>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Latest compiled and minified JavaScript -->
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-1.12.4.min.js"
crossorigin="anonymous"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Movie DB on Azure</title>
</head>
<body>
<h1>Login Error</h1>
<p>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</p>
</body>
</html>

Просмотреть файл

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="${movie.name}">Movie</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta th:name="_csrf" th:content="${_csrf.token}"/>
<meta th:name="_csrf_header" th:content="${_csrf.headerName}"/>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous"/>
<!-- Optional theme -->
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
crossorigin="anonymous"/>
</head>
<body>
<div class="container">
<div th:replace="fragments/header :: header">...</div>
<div class="jumbotron">
<script th:inline="javascript">
function updateMovieDescription() {
document.getElementById('description').setAttribute('contenteditable', 'false');
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "/movies/" + [[${movie.id}]], true);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
xhttp.setRequestHeader(header, token);
xhttp.send("description=" + document.getElementById('description').innerHTML);
}
</script>
<h1 th:text="${movie.name}"></h1>
<p th:if="${userInfo.getIsAllowedToUpdateMovieDB()}" id="description" th:text="${movie.description}"
onblur="updateMovieDescription()"
ondblclick="document.getElementById('description').setAttribute('contenteditable', 'true');"></p>
<p th:unless="${userInfo.getIsAllowedToUpdateMovieDB()}" id="description" th:text="${movie.description}"></p>
</div>
<div th:if="${movie.imageFullPathUri}">
<image class="img-responsive" th:src="${movie.imageFullPathUri}"/>
</div>
<div th:unless="${movie.imageFullPathUri}">
<image class="img-responsive" src="../original.png"/>
</div>
<div th:if="${userInfo.getIsAllowedToUpdateMovieDB()}">
<script>
function validateImageInput() {
var fileName = document.getElementById('file').files[0].name;
document.getElementById('submit').disabled = !fileName.match(/.(jpg|jpeg|png|gif)$/i);
document.getElementById('path').value = fileName;
}
</script>
<form method="POST" enctype="multipart/form-data" action="/upload">
<input type="text" th:value="${movie.id}" style="display: none"
name="id"/>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<table>
<tr>
<td><input type="button" value="Choose New Image"
onclick="document.getElementById('file').click();"
class="btn btn-success"/></td>
<td><input type="file" name="file" style="display: none;"
id="file" accept="image/*" onchange="validateImageInput();"/>
</td>
<td><input type="text" id="path" value="New Image..."
readonly="readonly"/></td>
<td><input type="submit" id="submit" value="Update"
class="btn btn-success" disabled="true"/></td>
</tr>
</table>
</form>
</div>
</div>
<!-- Latest compiled and minified JavaScript -->
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-1.12.4.min.js"
crossorigin="anonymous"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Movie doesn't exist</title>
</head>
<body>
<h1>The requested movie doesn't exist. It's also possible that the data app is down.</h1>
</body>
</html>

Просмотреть файл

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Top Rated Movies</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous"/>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
crossorigin="anonymous"/>
</head>
<body>
<div class="container">
<div th:replace="fragments/header :: header">...</div>
<div class="page-header">
<h1 class="text-center">Top Rated Movies</h1>
</div>
<div>
<div>
<table class="table table-hover">
<tr>
<th></th>
<th>Rank &#38; Title</th>
<th>Rating</th>
</tr>
<tr th:each="movie : ${movies}">
<td th:if="${movie.thumbnailFullPathUri}">
<object
th:data="${movie.thumbnailFullPathUri}" type="image/png">
<img src="../thumbnail.png"/>
</object>
</td>
<td th:unless="${movie.thumbnailFullPathUri}"><img
src="../thumbnail.png"/></td>
<td><span><a th:text="${movie.id + '. '}"></a></span><span><a
th:href="@{'/movies/' + ${movie.id}}" th:text="${movie.name}"></a></span></td>
<td th:text="${movie.rating}">Rating ...</td>
</tr>
</table>
</div>
<div class="text-center">
<a class="btn btn-success" th:href="${prev}" th:if="${hasprev}">Prev</a>
<a th:text="${number}"></a> <a class="btn btn-success"
th:href="${next}" th:if="${hasnext}">Next</a>
</div>
</div>
</div>
<!-- Latest compiled and minified JavaScript -->
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-1.12.4.min.js"
crossorigin="anonymous"></script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
</body>
</html>

Просмотреть файл

@ -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"));
}
}