diff --git a/.vscode/cspell-templates.txt b/.vscode/cspell-templates.txt index 3391a155733..6d280d7c2af 100644 --- a/.vscode/cspell-templates.txt +++ b/.vscode/cspell-templates.txt @@ -7,20 +7,29 @@ autouse azdo borderless browserslist +codegen codepaths Codespaces configfile +configurer containerapp CUSTOMHANDLER +databind +Dskip eastus envconfig ezfunc +fasterxml immer inprogress INPROGRESS Instrumentor mkdir +mvnw networkidle +noverify +openapigen +openapitools OPTARG prestart printenv @@ -30,6 +39,11 @@ Quickstart reactd ROLENAME Serializers +servlet +simpletodo +simpletodo +springdoc +springframework staticwebapp Swashbuckle tsbuildinfo diff --git a/.vscode/cspell.yaml b/.vscode/cspell.yaml index 0949b323930..8d5240534a7 100644 --- a/.vscode/cspell.yaml +++ b/.vscode/cspell.yaml @@ -11,6 +11,8 @@ ignorePaths: - eng # not required - ext # uses its own cspell config - "*.bicep" # no need to lint resource files + - mvnw # Maven wrapper asset used in Java templates -- external library code + - mvnw.cmd # Maven wrapper asset used in Java templates -- external library code dictionaryDefinitions: - name: templatesDictionary path: ./cspell-templates.txt diff --git a/cli/Dockerfile b/cli/Dockerfile index f701d4f0b37..46822f3d176 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -3,7 +3,7 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0 WORKDIR /app RUN export DEBIAN_FRONTEND=noninteractive \ - && wget -q https://packages.microsoft.com/config/ubuntu/19.04/packages-microsoft-prod.deb && dpkg -i packages-microsoft-prod.deb \ + && wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb && dpkg -i packages-microsoft-prod.deb \ && apt-get update && apt-get install -y --no-install-recommends apt-utils && apt-get install -y apt-transport-https ca-certificates curl unzip procps gnupg2 software-properties-common lsb-release \ # functions core tools && curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg \ @@ -30,7 +30,8 @@ RUN export DEBIAN_FRONTEND=noninteractive \ && python3 -m pip install --upgrade pip \ && echo 'alias python=python3' >> ~/.bashrc \ && echo 'alias pip=pip3' >> ~/.bashrc \ - && apt-get clean -y && rm -rf /var/lib/apt/lists/* \ + # java + && apt-get update && apt-get install -y msopenjdk-17 \ # terraform cli && apt-get update && apt-get install -y gnupg software-properties-common \ && wget -O- https://apt.releases.hashicorp.com/gpg | \ @@ -39,7 +40,9 @@ RUN export DEBIAN_FRONTEND=noninteractive \ && echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \ https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \ tee /etc/apt/sources.list.d/hashicorp.list \ - && apt update && apt-get install terraform + && apt update && apt-get install terraform \ + # Cleanup + && apt-get clean -y && rm -rf /var/lib/apt/lists/* # azd COPY cli/bin/azd-linux-amd64 /usr/bin/azd diff --git a/templates/common/.devcontainer/devcontainer.json/java/devcontainer.json b/templates/common/.devcontainer/devcontainer.json/java/devcontainer.json new file mode 100644 index 00000000000..c2b065aceac --- /dev/null +++ b/templates/common/.devcontainer/devcontainer.json/java/devcontainer.json @@ -0,0 +1,32 @@ +{ + "name": "Azure Developer CLI", + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "bullseye" + } + }, + "features": { + "github-cli": "2", + "azure-cli": "2.38", + "docker-from-docker": "20.10", + "java": "17.0", + "node": { + "version": "16", + "nodeGypDependencies": false + } + }, + "extensions": [ + "ms-azuretools.azure-dev", + "ms-azuretools.vscode-bicep", + "ms-azuretools.vscode-docker", + "ms-vscode.vscode-node-azure-pack", + "vscjava.vscode-java-pack" + ], + "forwardPorts": [ + 3000, + 3100 + ], + "postCreateCommand": "", + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/templates/common/infra/bicep/core/host/appservice-java.bicep b/templates/common/infra/bicep/core/host/appservice-java.bicep new file mode 100644 index 00000000000..d3630dfef9f --- /dev/null +++ b/templates/common/infra/bicep/core/host/appservice-java.bicep @@ -0,0 +1,35 @@ +param environmentName string +param location string = resourceGroup().location + +param allowedOrigins array = [] +param appCommandLine string = '' +param applicationInsightsName string +param appServicePlanId string +param appSettings object = {} +param keyVaultName string = '' +param linuxFxVersion string = 'JAVA|17-java17' +param managedIdentity bool = !(empty(keyVaultName)) +param scmDoBuildDuringDeployment bool = true +param serviceName string + +module appService 'appservice.bicep' = { + name: '${serviceName}-appservice-java' + params: { + environmentName: environmentName + location: location + allowedOrigins: allowedOrigins + appCommandLine: appCommandLine + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + appSettings: appSettings + keyVaultName: keyVaultName + linuxFxVersion: linuxFxVersion + managedIdentity: managedIdentity + scmDoBuildDuringDeployment: scmDoBuildDuringDeployment + serviceName: serviceName + } +} + +output identityPrincipalId string = appService.outputs.identityPrincipalId +output name string = appService.outputs.name +output uri string = appService.outputs.uri diff --git a/templates/common/infra/bicep/core/host/container-app.bicep b/templates/common/infra/bicep/core/host/container-app.bicep index fdfcf6777ad..cc0751d1a33 100644 --- a/templates/common/infra/bicep/core/host/container-app.bicep +++ b/templates/common/infra/bicep/core/host/container-app.bicep @@ -11,6 +11,12 @@ param managedIdentity bool = !(empty(keyVaultName)) param targetPort int = 80 param serviceName string +@description('CPU cores allocated to a single container instance, e.g. 0.5') +param containerCpuCoreCount string = '0.5' + +@description('Memory allocated to a single container instance, e.g. 1Gi') +param containerMemory string = '1.0Gi' + var abbrs = loadJsonContent('../../abbreviations.json') var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) var tags = { 'azd-env-name': environmentName } @@ -49,6 +55,10 @@ resource app 'Microsoft.App/containerApps@2022-03-01' = { image: imageName name: 'main' env: env + resources: { + cpu: json(containerCpuCoreCount) + memory: containerMemory + } } ] } diff --git a/templates/todo/api/java/.gitattributes b/templates/todo/api/java/.gitattributes new file mode 100644 index 00000000000..149df3f4afa --- /dev/null +++ b/templates/todo/api/java/.gitattributes @@ -0,0 +1 @@ +mvnw text eol=lf diff --git a/templates/todo/api/java/.gitignore b/templates/todo/api/java/.gitignore new file mode 100644 index 00000000000..d26f4fbbc7e --- /dev/null +++ b/templates/todo/api/java/.gitignore @@ -0,0 +1,35 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +applicationinsights.log \ No newline at end of file diff --git a/templates/todo/api/java/.mvn/wrapper/maven-wrapper.jar b/templates/todo/api/java/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000000..c1dd12f1764 Binary files /dev/null and b/templates/todo/api/java/.mvn/wrapper/maven-wrapper.jar differ diff --git a/templates/todo/api/java/.mvn/wrapper/maven-wrapper.properties b/templates/todo/api/java/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000000..db95c131dde --- /dev/null +++ b/templates/todo/api/java/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# 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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/templates/todo/api/java/.openapi-generator/FILES b/templates/todo/api/java/.openapi-generator/FILES new file mode 100644 index 00000000000..726c90a1f19 --- /dev/null +++ b/templates/todo/api/java/.openapi-generator/FILES @@ -0,0 +1,5 @@ +src/main/java/com/microsoft/azure/simpletodo/api/ApiUtil.java +src/main/java/com/microsoft/azure/simpletodo/api/ListsApi.java +src/main/java/com/microsoft/azure/simpletodo/model/TodoItem.java +src/main/java/com/microsoft/azure/simpletodo/model/TodoList.java +src/main/java/com/microsoft/azure/simpletodo/model/TodoState.java diff --git a/templates/todo/api/java/.openapi-generator/VERSION b/templates/todo/api/java/.openapi-generator/VERSION new file mode 100644 index 00000000000..6d54bbd7751 --- /dev/null +++ b/templates/todo/api/java/.openapi-generator/VERSION @@ -0,0 +1 @@ +6.0.1 \ No newline at end of file diff --git a/templates/todo/api/java/.openapi-generator/openapi.yaml-default.sha256 b/templates/todo/api/java/.openapi-generator/openapi.yaml-default.sha256 new file mode 100644 index 00000000000..4bf5f74fc3c --- /dev/null +++ b/templates/todo/api/java/.openapi-generator/openapi.yaml-default.sha256 @@ -0,0 +1 @@ +1e0347e24b739d303f3af0493d1e730e4ac47a27fdac1e8a151defcc745496f0 \ No newline at end of file diff --git a/templates/todo/api/java/Dockerfile b/templates/todo/api/java/Dockerfile new file mode 100644 index 00000000000..17a4231e68a --- /dev/null +++ b/templates/todo/api/java/Dockerfile @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/openjdk/jdk:17-mariner AS build + +WORKDIR /workspace/app +EXPOSE 3100 + +COPY mvnw . +COPY .mvn .mvn +COPY openapi.yaml openapi.yaml +COPY pom.xml . +COPY src src + +RUN ./mvnw package -DskipTests +RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar) + +FROM mcr.microsoft.com/openjdk/jdk:17-mariner + +ARG DEPENDENCY=/workspace/app/target/dependency +COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib +COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF +COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app + +ENTRYPOINT ["java","-noverify", "-XX:MaxRAMPercentage=70", "-XX:+UseParallelGC", "-XX:ActiveProcessorCount=2", "-cp","app:app/lib/*","com.microsoft.azure.simpletodo.SimpleTodoApplication"] \ No newline at end of file diff --git a/templates/todo/api/java/README.md b/templates/todo/api/java/README.md new file mode 100644 index 00000000000..8553362a55f --- /dev/null +++ b/templates/todo/api/java/README.md @@ -0,0 +1,39 @@ +# Java with Spring Boot REST API + +## Setup + +### Prerequisites + +- Java 17 or later + +### Local Environment + +#### Environment variables + +The following environment variables are available for configuration: + +- `AZURE_KEY_VAULT_ENDPOINT`. If set, other secret environment properties such as `AZURE_COSMOS_CONNECTION_STRING` are loaded from KeyVault. +- `AZURE_COSMOS_CONNECTION_STRING`. A direct override for specifying the Cosmos DB connection string (Mongo DB also supported). +- `APPLICATIONINSIGHTS_CONNECTION_STRING`. (Optional) Connection string of an Application Insights instance for telemetry to be logged. + +### Build & Compile + +Run `./mvnw package` to build & compile the application in the `target` directory. +`./mvnw package -DskipTests` may be used instead to skip start-up tests that will require app configuration defined. + +### Run the application locally + +Run `./mvnw spring-boot:run` to start the local development server. + +The REST API will be available at `http://localhost:3100`. + +### Build and run the Docker image + +```bash +docker build . -t java-todo@latest +docker run -e AZURE_COSMOS_CONNECTION_STRING=$AZURE_COSMOS_CONNECTION_STRING -p 3100:3100 -t java-todo@latest +``` + +### Regenerate API from OpenAPI spec + +Run `./mvnw -P openapigen compile` to regenerate the API model and interfaces from the OpenAPI spec. diff --git a/templates/todo/api/java/mvnw b/templates/todo/api/java/mvnw new file mode 100755 index 00000000000..5643201c7d8 --- /dev/null +++ b/templates/todo/api/java/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/templates/todo/api/java/mvnw.cmd b/templates/todo/api/java/mvnw.cmd new file mode 100644 index 00000000000..8a15b7f311f --- /dev/null +++ b/templates/todo/api/java/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/templates/todo/api/java/pom.xml b/templates/todo/api/java/pom.xml new file mode 100644 index 00000000000..f24a89c9654 --- /dev/null +++ b/templates/todo/api/java/pom.xml @@ -0,0 +1,146 @@ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.4 + + + + com.microsoft.azure.simpletodo + simple-todo + 0.0.1-SNAPSHOT + SimpleTodo + Simple Todo application + + 17 + ${java.version} + ${java.version} + 1.6.11 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.data + spring-data-commons + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springdoc + springdoc-openapi-ui + ${springdoc.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.openapitools + jackson-databind-nullable + 0.2.3 + + + + jakarta.validation + jakarta.validation-api + + + com.fasterxml.jackson.core + jackson-databind + + + + com.azure.spring + spring-cloud-azure-starter-keyvault-secrets + 4.3.0 + + + com.microsoft.azure + applicationinsights-runtime-attach + 3.3.1 + + + + src/main/java + + + src/main/resources + true + + + + + org.springframework.boot + spring-boot-maven-plugin + + + azure/azure-dev-todo-java + + + + + + repackage + + + + + + + + + + openapigen + + + + org.openapitools + openapi-generator-maven-plugin + 6.0.1 + + + + generate + + + ${project.basedir}/../common/openapi.yaml + spring + ${project.basedir} + com.microsoft.azure.simpletodo.api + com.microsoft.azure.simpletodo.model + ApiUtil.java + + src/main/java/ + false + true + true + + + + + + + + + + diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/SimpleTodoApplication.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/SimpleTodoApplication.java new file mode 100644 index 00000000000..18f6d0bcfd1 --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/SimpleTodoApplication.java @@ -0,0 +1,16 @@ +package com.microsoft.azure.simpletodo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import com.microsoft.applicationinsights.attach.ApplicationInsights; + +@SpringBootApplication +public class SimpleTodoApplication { + + public static void main(String[] args) { + ApplicationInsights.attach(); + + new SpringApplication(SimpleTodoApplication.class).run(args); + } +} diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/api/ApiUtil.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/api/ApiUtil.java new file mode 100644 index 00000000000..bcdbd2577c0 --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/api/ApiUtil.java @@ -0,0 +1,19 @@ +package com.microsoft.azure.simpletodo.api; + +import org.springframework.web.context.request.NativeWebRequest; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ApiUtil { + public static void setExampleResponse(NativeWebRequest req, String contentType, String example) { + try { + HttpServletResponse res = req.getNativeResponse(HttpServletResponse.class); + res.setCharacterEncoding("UTF-8"); + res.addHeader("Content-Type", contentType); + res.getWriter().print(example); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/api/ListsApi.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/api/ListsApi.java new file mode 100644 index 00000000000..dcc1795e412 --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/api/ListsApi.java @@ -0,0 +1,521 @@ +/** + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.0.1). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +package com.microsoft.azure.simpletodo.api; + +import java.math.BigDecimal; +import java.util.List; +import com.microsoft.azure.simpletodo.model.TodoItem; +import com.microsoft.azure.simpletodo.model.TodoList; +import com.microsoft.azure.simpletodo.model.TodoState; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.Valid; +import javax.validation.constraints.*; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.annotation.Generated; + +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen") +@Validated +@Tag(name = "lists", description = "the lists API") +public interface ListsApi { + + default Optional getRequest() { + return Optional.empty(); + } + + /** + * POST /lists/{listId}/items : Creates a new Todo item within a list + * + * @param listId The Todo list unique identifier (required) + * @param todoItem The Todo Item (optional) + * @return A Todo item result (status code 201) + * or Todo list not found (status code 404) + */ + @Operation( + operationId = "createItem", + summary = "Creates a new Todo item within a list", + tags = { "Items" }, + responses = { + @ApiResponse(responseCode = "201", description = "A Todo item result", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = TodoItem.class)) + }), + @ApiResponse(responseCode = "404", description = "Todo list not found") + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/lists/{listId}/items", + produces = { "application/json" }, + consumes = { "application/json" } + ) + default ResponseEntity createItem( + @Parameter(name = "listId", description = "The Todo list unique identifier", required = true) @PathVariable("listId") String listId, + @Parameter(name = "TodoItem", description = "The Todo Item") @Valid @RequestBody(required = false) TodoItem todoItem + ) { + getRequest().ifPresent(request -> { + for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { + if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { + String exampleString = "{ \"listId\" : \"listId\", \"dueDate\" : \"2000-01-23T04:56:07.000+00:00\", \"name\" : \"name\", \"description\" : \"description\", \"id\" : \"id\", \"completedDate\" : \"2000-01-23T04:56:07.000+00:00\" }"; + ApiUtil.setExampleResponse(request, "application/json", exampleString); + break; + } + } + }); + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + /** + * POST /lists : Creates a new Todo list + * + * @param todoList The Todo List (optional) + * @return A Todo list result (status code 201) + * or Invalid request schema (status code 400) + */ + @Operation( + operationId = "createList", + summary = "Creates a new Todo list", + tags = { "Lists" }, + responses = { + @ApiResponse(responseCode = "201", description = "A Todo list result", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = TodoList.class)) + }), + @ApiResponse(responseCode = "400", description = "Invalid request schema") + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/lists", + produces = { "application/json" }, + consumes = { "application/json" } + ) + default ResponseEntity createList( + @Parameter(name = "TodoList", description = "The Todo List") @Valid @RequestBody(required = false) TodoList todoList + ) { + getRequest().ifPresent(request -> { + for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { + if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { + String exampleString = "{ \"name\" : \"name\", \"description\" : \"description\", \"id\" : \"id\" }"; + ApiUtil.setExampleResponse(request, "application/json", exampleString); + break; + } + } + }); + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + /** + * DELETE /lists/{listId}/items/{itemId} : Deletes a Todo item by unique identifier + * + * @param listId The Todo list unique identifier (required) + * @param itemId The Todo list unique identifier (required) + * @return Todo item deleted successfully (status code 204) + * or Todo list or item not found (status code 404) + */ + @Operation( + operationId = "deleteItemById", + summary = "Deletes a Todo item by unique identifier", + tags = { "Items" }, + responses = { + @ApiResponse(responseCode = "204", description = "Todo item deleted successfully"), + @ApiResponse(responseCode = "404", description = "Todo list or item not found") + } + ) + @RequestMapping( + method = RequestMethod.DELETE, + value = "/lists/{listId}/items/{itemId}" + ) + default ResponseEntity deleteItemById( + @Parameter(name = "listId", description = "The Todo list unique identifier", required = true) @PathVariable("listId") String listId, + @Parameter(name = "itemId", description = "The Todo list unique identifier", required = true) @PathVariable("itemId") String itemId + ) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + /** + * DELETE /lists/{listId} : Deletes a Todo list by unique identifier + * + * @param listId The Todo list unique identifier (required) + * @return Todo list deleted successfully (status code 204) + * or Todo list not found (status code 404) + */ + @Operation( + operationId = "deleteListById", + summary = "Deletes a Todo list by unique identifier", + tags = { "Lists" }, + responses = { + @ApiResponse(responseCode = "204", description = "Todo list deleted successfully"), + @ApiResponse(responseCode = "404", description = "Todo list not found") + } + ) + @RequestMapping( + method = RequestMethod.DELETE, + value = "/lists/{listId}" + ) + default ResponseEntity deleteListById( + @Parameter(name = "listId", description = "The Todo list unique identifier", required = true) @PathVariable("listId") String listId + ) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + /** + * GET /lists/{listId}/items/{itemId} : Gets a Todo item by unique identifier + * + * @param listId The Todo list unique identifier (required) + * @param itemId The Todo list unique identifier (required) + * @return A Todo item result (status code 200) + * or Todo list or item not found (status code 404) + */ + @Operation( + operationId = "getItemById", + summary = "Gets a Todo item by unique identifier", + tags = { "Items" }, + responses = { + @ApiResponse(responseCode = "200", description = "A Todo item result", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = TodoItem.class)) + }), + @ApiResponse(responseCode = "404", description = "Todo list or item not found") + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/lists/{listId}/items/{itemId}", + produces = { "application/json" } + ) + default ResponseEntity getItemById( + @Parameter(name = "listId", description = "The Todo list unique identifier", required = true) @PathVariable("listId") String listId, + @Parameter(name = "itemId", description = "The Todo list unique identifier", required = true) @PathVariable("itemId") String itemId + ) { + getRequest().ifPresent(request -> { + for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { + if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { + String exampleString = "{ \"listId\" : \"listId\", \"dueDate\" : \"2000-01-23T04:56:07.000+00:00\", \"name\" : \"name\", \"description\" : \"description\", \"id\" : \"id\", \"completedDate\" : \"2000-01-23T04:56:07.000+00:00\" }"; + ApiUtil.setExampleResponse(request, "application/json", exampleString); + break; + } + } + }); + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + /** + * GET /lists/{listId}/items : Gets Todo items within the specified list + * + * @param listId The Todo list unique identifier (required) + * @param top The max number of items to returns in a result (optional) + * @param skip The number of items to skip within the results (optional) + * @return An array of Todo items (status code 200) + * or Todo list not found (status code 404) + */ + @Operation( + operationId = "getItemsByListId", + summary = "Gets Todo items within the specified list", + tags = { "Items" }, + responses = { + @ApiResponse(responseCode = "200", description = "An array of Todo items", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = TodoItem.class)) + }), + @ApiResponse(responseCode = "404", description = "Todo list not found") + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/lists/{listId}/items", + produces = { "application/json" } + ) + default ResponseEntity> getItemsByListId( + @Parameter(name = "listId", description = "The Todo list unique identifier", required = true) @PathVariable("listId") String listId, + @Parameter(name = "top", description = "The max number of items to returns in a result") @Valid @RequestParam(value = "top", required = false) BigDecimal top, + @Parameter(name = "skip", description = "The number of items to skip within the results") @Valid @RequestParam(value = "skip", required = false) BigDecimal skip + ) { + getRequest().ifPresent(request -> { + for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { + if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { + String exampleString = "{ \"listId\" : \"listId\", \"dueDate\" : \"2000-01-23T04:56:07.000+00:00\", \"name\" : \"name\", \"description\" : \"description\", \"id\" : \"id\", \"completedDate\" : \"2000-01-23T04:56:07.000+00:00\" }"; + ApiUtil.setExampleResponse(request, "application/json", exampleString); + break; + } + } + }); + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + /** + * GET /lists/{listId}/items/state/{state} : Gets a list of Todo items of a specific state + * + * @param listId The Todo list unique identifier (required) + * @param state The Todo item state (required) + * @param top The max number of items to returns in a result (optional) + * @param skip The number of items to skip within the results (optional) + * @return An array of Todo items (status code 200) + * or Todo list or item not found (status code 404) + */ + @Operation( + operationId = "getItemsByListIdAndState", + summary = "Gets a list of Todo items of a specific state", + tags = { "Items" }, + responses = { + @ApiResponse(responseCode = "200", description = "An array of Todo items", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = TodoItem.class)) + }), + @ApiResponse(responseCode = "404", description = "Todo list or item not found") + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/lists/{listId}/items/state/{state}", + produces = { "application/json" } + ) + default ResponseEntity> getItemsByListIdAndState( + @Parameter(name = "listId", description = "The Todo list unique identifier", required = true) @PathVariable("listId") String listId, + @Parameter(name = "state", description = "The Todo item state", required = true) @PathVariable("state") TodoState state, + @Parameter(name = "top", description = "The max number of items to returns in a result") @Valid @RequestParam(value = "top", required = false) BigDecimal top, + @Parameter(name = "skip", description = "The number of items to skip within the results") @Valid @RequestParam(value = "skip", required = false) BigDecimal skip + ) { + getRequest().ifPresent(request -> { + for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { + if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { + String exampleString = "{ \"listId\" : \"listId\", \"dueDate\" : \"2000-01-23T04:56:07.000+00:00\", \"name\" : \"name\", \"description\" : \"description\", \"id\" : \"id\", \"completedDate\" : \"2000-01-23T04:56:07.000+00:00\" }"; + ApiUtil.setExampleResponse(request, "application/json", exampleString); + break; + } + } + }); + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + /** + * GET /lists/{listId} : Gets a Todo list by unique identifier + * + * @param listId The Todo list unique identifier (required) + * @return A Todo list result (status code 200) + * or Todo list not found (status code 404) + */ + @Operation( + operationId = "getListById", + summary = "Gets a Todo list by unique identifier", + tags = { "Lists" }, + responses = { + @ApiResponse(responseCode = "200", description = "A Todo list result", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = TodoList.class)) + }), + @ApiResponse(responseCode = "404", description = "Todo list not found") + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/lists/{listId}", + produces = { "application/json" } + ) + default ResponseEntity getListById( + @Parameter(name = "listId", description = "The Todo list unique identifier", required = true) @PathVariable("listId") String listId + ) { + getRequest().ifPresent(request -> { + for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { + if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { + String exampleString = "{ \"name\" : \"name\", \"description\" : \"description\", \"id\" : \"id\" }"; + ApiUtil.setExampleResponse(request, "application/json", exampleString); + break; + } + } + }); + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + /** + * GET /lists : Gets an array of Todo lists + * + * @param top The max number of items to returns in a result (optional) + * @param skip The number of items to skip within the results (optional) + * @return An array of Todo lists (status code 200) + */ + @Operation( + operationId = "getLists", + summary = "Gets an array of Todo lists", + tags = { "Lists" }, + responses = { + @ApiResponse(responseCode = "200", description = "An array of Todo lists", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = TodoList.class)) + }) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/lists", + produces = { "application/json" } + ) + default ResponseEntity> getLists( + @Parameter(name = "top", description = "The max number of items to returns in a result") @Valid @RequestParam(value = "top", required = false) BigDecimal top, + @Parameter(name = "skip", description = "The number of items to skip within the results") @Valid @RequestParam(value = "skip", required = false) BigDecimal skip + ) { + getRequest().ifPresent(request -> { + for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { + if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { + String exampleString = "{ \"name\" : \"name\", \"description\" : \"description\", \"id\" : \"id\" }"; + ApiUtil.setExampleResponse(request, "application/json", exampleString); + break; + } + } + }); + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + /** + * PUT /lists/{listId}/items/{itemId} : Updates a Todo item by unique identifier + * + * @param listId The Todo list unique identifier (required) + * @param itemId The Todo list unique identifier (required) + * @param todoItem The Todo Item (optional) + * @return A Todo item result (status code 200) + * or Todo item is invalid (status code 400) + * or Todo list or item not found (status code 404) + */ + @Operation( + operationId = "updateItemById", + summary = "Updates a Todo item by unique identifier", + tags = { "Items" }, + responses = { + @ApiResponse(responseCode = "200", description = "A Todo item result", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = TodoItem.class)) + }), + @ApiResponse(responseCode = "400", description = "Todo item is invalid"), + @ApiResponse(responseCode = "404", description = "Todo list or item not found") + } + ) + @RequestMapping( + method = RequestMethod.PUT, + value = "/lists/{listId}/items/{itemId}", + produces = { "application/json" }, + consumes = { "application/json" } + ) + default ResponseEntity updateItemById( + @Parameter(name = "listId", description = "The Todo list unique identifier", required = true) @PathVariable("listId") String listId, + @Parameter(name = "itemId", description = "The Todo list unique identifier", required = true) @PathVariable("itemId") String itemId, + @Parameter(name = "TodoItem", description = "The Todo Item") @Valid @RequestBody(required = false) TodoItem todoItem + ) { + getRequest().ifPresent(request -> { + for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { + if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { + String exampleString = "{ \"listId\" : \"listId\", \"dueDate\" : \"2000-01-23T04:56:07.000+00:00\", \"name\" : \"name\", \"description\" : \"description\", \"id\" : \"id\", \"completedDate\" : \"2000-01-23T04:56:07.000+00:00\" }"; + ApiUtil.setExampleResponse(request, "application/json", exampleString); + break; + } + } + }); + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + /** + * PUT /lists/{listId}/items/state/{state} : Changes the state of the specified list items + * + * @param listId The Todo list unique identifier (required) + * @param state The Todo item state (required) + * @param requestBody (optional) + * @return Todo items updated (status code 204) + * or Update request is invalid (status code 400) + */ + @Operation( + operationId = "updateItemsStateByListId", + summary = "Changes the state of the specified list items", + tags = { "Items" }, + responses = { + @ApiResponse(responseCode = "204", description = "Todo items updated"), + @ApiResponse(responseCode = "400", description = "Update request is invalid") + } + ) + @RequestMapping( + method = RequestMethod.PUT, + value = "/lists/{listId}/items/state/{state}", + consumes = { "application/json" } + ) + default ResponseEntity updateItemsStateByListId( + @Parameter(name = "listId", description = "The Todo list unique identifier", required = true) @PathVariable("listId") String listId, + @Parameter(name = "state", description = "The Todo item state", required = true) @PathVariable("state") TodoState state, + @Parameter(name = "request_body", description = "") @Valid @RequestBody(required = false) List requestBody + ) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + /** + * PUT /lists/{listId} : Updates a Todo list by unique identifier + * + * @param listId The Todo list unique identifier (required) + * @param todoList The Todo List (optional) + * @return A Todo list result (status code 200) + * or Todo list is invalid (status code 400) + */ + @Operation( + operationId = "updateListById", + summary = "Updates a Todo list by unique identifier", + tags = { "Lists" }, + responses = { + @ApiResponse(responseCode = "200", description = "A Todo list result", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = TodoList.class)) + }), + @ApiResponse(responseCode = "400", description = "Todo list is invalid") + } + ) + @RequestMapping( + method = RequestMethod.PUT, + value = "/lists/{listId}", + produces = { "application/json" }, + consumes = { "application/json" } + ) + default ResponseEntity updateListById( + @Parameter(name = "listId", description = "The Todo list unique identifier", required = true) @PathVariable("listId") String listId, + @Parameter(name = "TodoList", description = "The Todo List") @Valid @RequestBody(required = false) TodoList todoList + ) { + getRequest().ifPresent(request -> { + for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { + if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) { + String exampleString = "{ \"name\" : \"name\", \"description\" : \"description\", \"id\" : \"id\" }"; + ApiUtil.setExampleResponse(request, "application/json", exampleString); + break; + } + } + }); + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + +} diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/configuration/MongoDBConfiguration.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/configuration/MongoDBConfiguration.java new file mode 100644 index 00000000000..a47d5eb92fe --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/configuration/MongoDBConfiguration.java @@ -0,0 +1,38 @@ +package com.microsoft.azure.simpletodo.configuration; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Date; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; + +@Configuration +public class MongoDBConfiguration { + + @Bean + public MongoCustomConversions mongoCustomConversions() { + return new MongoCustomConversions(Arrays.asList( + new OffsetDateTimeReadConverter(), + new OffsetDateTimeWriteConverter() + )); + } + + static class OffsetDateTimeWriteConverter implements Converter { + + @Override + public Date convert(OffsetDateTime source) { + return Date.from(source.toInstant().atZone(ZoneOffset.UTC).toInstant()); + } + } + + static class OffsetDateTimeReadConverter implements Converter { + + @Override + public OffsetDateTime convert(Date source) { + return source.toInstant().atOffset(ZoneOffset.UTC); + } + } +} diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/configuration/RFC3339DateFormat.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/configuration/RFC3339DateFormat.java new file mode 100644 index 00000000000..06754ffab41 --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/configuration/RFC3339DateFormat.java @@ -0,0 +1,38 @@ +package com.microsoft.azure.simpletodo.configuration; + +import com.fasterxml.jackson.databind.util.StdDateFormat; + +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.ParsePosition; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +public class RFC3339DateFormat extends DateFormat { + private static final long serialVersionUID = 1L; + private static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone("UTC"); + + private final StdDateFormat fmt = new StdDateFormat() + .withTimeZone(TIMEZONE_Z) + .withColonInTimeZone(true); + + public RFC3339DateFormat() { + this.calendar = new GregorianCalendar(); + } + + @Override + public Date parse(String source, ParsePosition pos) { + return fmt.parse(source, pos); + } + + @Override + public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { + return fmt.format(date, toAppendTo, fieldPosition); + } + + @Override + public Object clone() { + return this; + } +} diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/configuration/WebConfiguration.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/configuration/WebConfiguration.java new file mode 100644 index 00000000000..191fa0b6d82 --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/configuration/WebConfiguration.java @@ -0,0 +1,24 @@ +package com.microsoft.azure.simpletodo.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfiguration { + + @Bean + public WebMvcConfigurer webConfigurer() { + return new WebMvcConfigurer() { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("*") + .allowedHeaders("*"); + } + }; + } +} diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/model/TodoItem.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/model/TodoItem.java new file mode 100644 index 00000000000..92bc02fff1a --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/model/TodoItem.java @@ -0,0 +1,233 @@ +package com.microsoft.azure.simpletodo.model; + +import java.net.URI; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.microsoft.azure.simpletodo.model.TodoState; +import java.time.OffsetDateTime; +import org.springframework.format.annotation.DateTimeFormat; +import org.openapitools.jackson.nullable.JsonNullable; +import java.time.OffsetDateTime; +import javax.validation.Valid; +import javax.validation.constraints.*; +import io.swagger.v3.oas.annotations.media.Schema; + + +import java.util.*; +import javax.annotation.Generated; + +/** + * A task that needs to be completed + */ + +@Schema(name = "TodoItem", description = "A task that needs to be completed") +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen") +public class TodoItem { + + @JsonProperty("id") + private String id; + + @JsonProperty("listId") + private String listId; + + @JsonProperty("name") + private String name; + + @JsonProperty("description") + private String description; + + @JsonProperty("state") + private TodoState state; + + @JsonProperty("dueDate") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + private OffsetDateTime dueDate; + + @JsonProperty("completedDate") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + private OffsetDateTime completedDate; + + public TodoItem id(String id) { + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + + @Schema(name = "id", required = false) + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public TodoItem listId(String listId) { + this.listId = listId; + return this; + } + + /** + * Get listId + * @return listId + */ + @NotNull + @Schema(name = "listId", required = true) + public String getListId() { + return listId; + } + + public void setListId(String listId) { + this.listId = listId; + } + + public TodoItem name(String name) { + this.name = name; + return this; + } + + /** + * Get name + * @return name + */ + @NotNull + @Schema(name = "name", required = true) + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TodoItem description(String description) { + this.description = description; + return this; + } + + /** + * Get description + * @return description + */ + @NotNull + @Schema(name = "description", required = true) + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public TodoItem state(TodoState state) { + this.state = state; + return this; + } + + /** + * Get state + * @return state + */ + @Valid + @Schema(name = "state", required = false) + public TodoState getState() { + return state; + } + + public void setState(TodoState state) { + this.state = state; + } + + public TodoItem dueDate(OffsetDateTime dueDate) { + this.dueDate = dueDate; + return this; + } + + /** + * Get dueDate + * @return dueDate + */ + @Valid + @Schema(name = "dueDate", required = false) + public OffsetDateTime getDueDate() { + return dueDate; + } + + public void setDueDate(OffsetDateTime dueDate) { + this.dueDate = dueDate; + } + + public TodoItem completedDate(OffsetDateTime completedDate) { + this.completedDate = completedDate; + return this; + } + + /** + * Get completedDate + * @return completedDate + */ + @Valid + @Schema(name = "completedDate", required = false) + public OffsetDateTime getCompletedDate() { + return completedDate; + } + + public void setCompletedDate(OffsetDateTime completedDate) { + this.completedDate = completedDate; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TodoItem todoItem = (TodoItem) o; + return Objects.equals(this.id, todoItem.id) && + Objects.equals(this.listId, todoItem.listId) && + Objects.equals(this.name, todoItem.name) && + Objects.equals(this.description, todoItem.description) && + Objects.equals(this.state, todoItem.state) && + Objects.equals(this.dueDate, todoItem.dueDate) && + Objects.equals(this.completedDate, todoItem.completedDate); + } + + @Override + public int hashCode() { + return Objects.hash(id, listId, name, description, state, dueDate, completedDate); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class TodoItem {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" listId: ").append(toIndentedString(listId)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append(" state: ").append(toIndentedString(state)).append("\n"); + sb.append(" dueDate: ").append(toIndentedString(dueDate)).append("\n"); + sb.append(" completedDate: ").append(toIndentedString(completedDate)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/model/TodoList.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/model/TodoList.java new file mode 100644 index 00000000000..e667387d99b --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/model/TodoList.java @@ -0,0 +1,132 @@ +package com.microsoft.azure.simpletodo.model; + +import java.net.URI; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import org.openapitools.jackson.nullable.JsonNullable; +import java.time.OffsetDateTime; +import javax.validation.Valid; +import javax.validation.constraints.*; +import io.swagger.v3.oas.annotations.media.Schema; + + +import java.util.*; +import javax.annotation.Generated; + +/** + * A list of related Todo items + */ + +@Schema(name = "TodoList", description = " A list of related Todo items") +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen") +public class TodoList { + + @JsonProperty("id") + private String id; + + @JsonProperty("name") + private String name; + + @JsonProperty("description") + private String description; + + public TodoList id(String id) { + this.id = id; + return this; + } + + /** + * Get id + * @return id + */ + + @Schema(name = "id", required = false) + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public TodoList name(String name) { + this.name = name; + return this; + } + + /** + * Get name + * @return name + */ + @NotNull + @Schema(name = "name", required = true) + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TodoList description(String description) { + this.description = description; + return this; + } + + /** + * Get description + * @return description + */ + + @Schema(name = "description", required = false) + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TodoList todoList = (TodoList) o; + return Objects.equals(this.id, todoList.id) && + Objects.equals(this.name, todoList.name) && + Objects.equals(this.description, todoList.description); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, description); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class TodoList {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/model/TodoState.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/model/TodoState.java new file mode 100644 index 00000000000..773481722e2 --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/model/TodoState.java @@ -0,0 +1,58 @@ +package com.microsoft.azure.simpletodo.model; + +import java.net.URI; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonValue; +import org.openapitools.jackson.nullable.JsonNullable; +import java.time.OffsetDateTime; +import javax.validation.Valid; +import javax.validation.constraints.*; +import io.swagger.v3.oas.annotations.media.Schema; + + +import java.util.*; +import javax.annotation.Generated; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Gets or Sets TodoState + */ + +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen") +public enum TodoState { + + TODO("todo"), + + INPROGRESS("inprogress"), + + DONE("done"); + + private String value; + + TodoState(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static TodoState fromValue(String value) { + for (TodoState b : TodoState.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} + diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/repository/TodoItemRepository.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/repository/TodoItemRepository.java new file mode 100644 index 00000000000..b32b1320832 --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/repository/TodoItemRepository.java @@ -0,0 +1,29 @@ +package com.microsoft.azure.simpletodo.repository; + +import java.util.List; + +import org.springframework.data.mongodb.repository.Aggregation; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; + +import com.microsoft.azure.simpletodo.model.TodoItem; + +public interface TodoItemRepository extends MongoRepository { + + @Query("{ 'listId' : ?0 }") + List findTodoItemsByTodoList(String listId); + + @Aggregation(pipeline = { + "{ '$match': { 'listId' : ?0 } }", + "{ '$skip': ?1 }", + "{ '$limit': ?2 }", + }) + List findTodoItemsByTodoList(String listId, int skip, int limit); + + @Aggregation(pipeline = { + "{ '$match': { 'listId' : ?0, 'state' : ?1 } }", + "{ '$skip': ?2 }", + "{ '$limit': ?3 }", + }) + List findTodoItemsByTodoListAndState(String listId, String state, int skip, int limit); +} diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/repository/TodoListRepository.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/repository/TodoListRepository.java new file mode 100644 index 00000000000..5b9a7202ebb --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/repository/TodoListRepository.java @@ -0,0 +1,16 @@ +package com.microsoft.azure.simpletodo.repository; + +import com.microsoft.azure.simpletodo.model.TodoList; + +import java.util.List; + +import org.springframework.data.mongodb.repository.Aggregation; +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface TodoListRepository extends MongoRepository { + @Aggregation(pipeline = { + "{ '$skip': ?0 }", + "{ '$limit': ?1 }", + }) + List findAll(int skip, int limit); +} diff --git a/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/web/TodoListsController.java b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/web/TodoListsController.java new file mode 100644 index 00000000000..b4f2e10a69b --- /dev/null +++ b/templates/todo/api/java/src/main/java/com/microsoft/azure/simpletodo/web/TodoListsController.java @@ -0,0 +1,174 @@ +package com.microsoft.azure.simpletodo.web; + +import com.microsoft.azure.simpletodo.api.ListsApi; +import com.microsoft.azure.simpletodo.model.TodoItem; +import com.microsoft.azure.simpletodo.model.TodoList; +import com.microsoft.azure.simpletodo.model.TodoState; +import com.microsoft.azure.simpletodo.repository.TodoItemRepository; +import com.microsoft.azure.simpletodo.repository.TodoListRepository; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.math.BigDecimal; +import java.net.URI; +import java.util.List; +import java.util.Optional; + +@RestController +public class TodoListsController implements ListsApi { + + private final TodoListRepository todoListRepository; + + private final TodoItemRepository todoItemRepository; + + public TodoListsController(TodoListRepository todoListRepository, TodoItemRepository todoItemRepository) { + this.todoListRepository = todoListRepository; + this.todoItemRepository = todoItemRepository; + } + + @Override + public ResponseEntity createItem(String listId, TodoItem todoItem) { + Optional optionalTodoList = todoListRepository.findById(listId); + if (optionalTodoList.isPresent()) { + todoItem.setListId(listId); + TodoItem savedTodoItem = todoItemRepository.save(todoItem); + URI location = ServletUriComponentsBuilder + .fromCurrentRequest() + .path("/{id}") + .buildAndExpand(savedTodoItem.getId()) + .toUri(); + return ResponseEntity.created(location).body(savedTodoItem); + } else { + return ResponseEntity.notFound().build(); + } + } + + @Override + public ResponseEntity createList(TodoList todoList) { + TodoList savedTodoList = todoListRepository.save(todoList); + URI location = ServletUriComponentsBuilder + .fromCurrentRequest() + .path("/{id}") + .buildAndExpand(savedTodoList.getId()) + .toUri(); + return ResponseEntity.created(location).body(savedTodoList); + } + + @Override + public ResponseEntity deleteItemById(String listId, String itemId) { + Optional todoItem = getTodoItem(listId, itemId); + if (todoItem.isPresent()) { + todoItemRepository.deleteById(itemId); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @Override + public ResponseEntity deleteListById(String listId) { + Optional todoList = todoListRepository.findById(listId); + if (todoList.isPresent()) { + todoListRepository.deleteById(listId); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @Override + public ResponseEntity getItemById(String listId, String itemId) { + return getTodoItem(listId, itemId).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + } + + @Override + public ResponseEntity> getItemsByListId(String listId, BigDecimal top, BigDecimal skip) { + if (top == null) { + top = new BigDecimal(20); + } + if (skip == null) { + skip = new BigDecimal(0); + } + Optional todoList = todoListRepository.findById(listId); + if (todoList.isPresent()) { + return ResponseEntity.ok(todoItemRepository.findTodoItemsByTodoList(listId, skip.intValue(), top.intValue())); + } else { + return ResponseEntity.notFound().build(); + } + } + + @Override + public ResponseEntity> getItemsByListIdAndState(String listId, TodoState state, BigDecimal top, BigDecimal skip) { + if (top == null) { + top = new BigDecimal(20); + } + if (skip == null) { + skip = new BigDecimal(0); + } + return ResponseEntity.ok( + todoItemRepository + .findTodoItemsByTodoListAndState(listId, state.name(), skip.intValue(), top.intValue())); + } + + @Override + public ResponseEntity getListById(String listId) { + return todoListRepository.findById(listId).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + } + + @Override + public ResponseEntity> getLists(BigDecimal top, BigDecimal skip) { + if (top == null) { + top = new BigDecimal(20); + } + if (skip == null) { + skip = new BigDecimal(0); + } + return ResponseEntity.ok(todoListRepository.findAll(skip.intValue(), top.intValue())); + } + + @Override + public ResponseEntity updateItemById(String listId, String itemId, TodoItem todoItem) { + return getTodoItem(listId, itemId).map(t -> { + todoItemRepository.save(todoItem); + return ResponseEntity.ok(todoItem); + }).orElseGet(() -> ResponseEntity.notFound().build()); + } + + @Override + public ResponseEntity updateItemsStateByListId(String listId, TodoState state, List requestBody) { + for (TodoItem todoItem : todoItemRepository.findTodoItemsByTodoList(listId)) { + todoItem.state(state); + todoItemRepository.save(todoItem); + } + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + @Override + public ResponseEntity updateListById(String listId, TodoList todoList) { + return todoListRepository + .findById(listId) + .map(t -> ResponseEntity.ok(todoListRepository.save(t))) + .orElseGet(() -> ResponseEntity.badRequest().build()); + } + + private Optional getTodoItem(String listId, String itemId) { + Optional optionalTodoList = todoListRepository.findById(listId); + if (optionalTodoList.isEmpty()) { + return Optional.empty(); + } + Optional optionalTodoItem = todoItemRepository.findById(itemId); + if (optionalTodoItem.isPresent()) { + TodoItem todoItem = optionalTodoItem.get(); + if (todoItem.getListId().equals(listId)) { + return Optional.of(todoItem); + } else { + return Optional.empty(); + } + } else { + return Optional.empty(); + } + } +} diff --git a/templates/todo/api/java/src/main/resources/application.properties b/templates/todo/api/java/src/main/resources/application.properties new file mode 100644 index 00000000000..5fe37f08070 --- /dev/null +++ b/templates/todo/api/java/src/main/resources/application.properties @@ -0,0 +1,13 @@ +server.port=3100 + +spring.jackson.date-format=com.microsoft.azure.simpletodo.configuration.RFC3339DateFormat +spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false + +# Import KeyVault secrets as properties +spring.cloud.azure.keyvault.secret.property-sources[0].enabled=true +spring.cloud.azure.keyvault.secret.property-sources[0].endpoint=${AZURE_KEY_VAULT_ENDPOINT} + +spring.data.mongodb.uri=${AZURE_COSMOS_CONNECTION_STRING:#{null}} +spring.data.mongodb.database=todo + +springdoc.swagger-ui.use-root-path=true \ No newline at end of file diff --git a/templates/todo/api/java/src/main/resources/applicationinsights.json b/templates/todo/api/java/src/main/resources/applicationinsights.json new file mode 100644 index 00000000000..e4b08093903 --- /dev/null +++ b/templates/todo/api/java/src/main/resources/applicationinsights.json @@ -0,0 +1,6 @@ +{ + "connectionString": "", + "role": { + "name": "API" + } +} diff --git a/templates/todo/api/java/src/test/java/com/microsoft/azure/simpletodo/SimpleTodoApplicationTests.java b/templates/todo/api/java/src/test/java/com/microsoft/azure/simpletodo/SimpleTodoApplicationTests.java new file mode 100644 index 00000000000..7e373c79969 --- /dev/null +++ b/templates/todo/api/java/src/test/java/com/microsoft/azure/simpletodo/SimpleTodoApplicationTests.java @@ -0,0 +1,13 @@ +package com.microsoft.azure.simpletodo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SimpleTodoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/templates/todo/common/infra/bicep/app/api-appservice-java.bicep b/templates/todo/common/infra/bicep/app/api-appservice-java.bicep new file mode 100644 index 00000000000..dff0d97f770 --- /dev/null +++ b/templates/todo/common/infra/bicep/app/api-appservice-java.bicep @@ -0,0 +1,25 @@ +param environmentName string +param location string = resourceGroup().location + +param allowedOrigins array = [] +param applicationInsightsName string +param appServicePlanId string +param keyVaultName string +param serviceName string = 'api' + +module api '../../../../../common/infra/bicep/core/host/appservice-java.bicep' = { + name: '${serviceName}-appservice-java-module' + params: { + environmentName: environmentName + location: location + allowedOrigins: allowedOrigins + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + keyVaultName: keyVaultName + serviceName: serviceName + } +} + +output API_IDENTITY_PRINCIPAL_ID string = api.outputs.identityPrincipalId +output API_NAME string = api.outputs.name +output API_URI string = api.outputs.uri diff --git a/templates/todo/common/infra/bicep/app/api-container-app.bicep b/templates/todo/common/infra/bicep/app/api-container-app.bicep index b68df26a83a..8cd77a3bc2c 100644 --- a/templates/todo/common/infra/bicep/app/api-container-app.bicep +++ b/templates/todo/common/infra/bicep/app/api-container-app.bicep @@ -18,6 +18,8 @@ module api '../../../../../common/infra/bicep/core/host/container-app.bicep' = { location: location containerAppsEnvironmentName: containerAppsEnvironmentName containerRegistryName: containerRegistryName + containerCpuCoreCount: '1.0' + containerMemory: '2.0Gi' env: [ { name: 'AZURE_KEY_VAULT_ENDPOINT' diff --git a/templates/todo/projects/java-mongo-aca/.repo/bicep/azure.yaml b/templates/todo/projects/java-mongo-aca/.repo/bicep/azure.yaml new file mode 100644 index 00000000000..d3ddab6476f --- /dev/null +++ b/templates/todo/projects/java-mongo-aca/.repo/bicep/azure.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: todo-java-mongo-aca +metadata: + template: todo-java-mongo-aca@0.0.1-beta +services: + web: + project: ../../web/react-fluent + module: app/web + language: js + host: containerapp + api: + project: ../../api/java + module: app/api + language: java + host: containerapp diff --git a/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/main.bicep b/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/main.bicep new file mode 100644 index 00000000000..02e0c504abd --- /dev/null +++ b/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/main.bicep @@ -0,0 +1,52 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +param location string + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +@description('The image name for the api service') +param apiImageName string = '' + +@description('The image name for the web service') +param webImageName string = '' + +var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') +var tags = { 'azd-env-name': environmentName } + +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: '${abbrs.resourcesResourceGroups}${environmentName}' + location: location + tags: tags +} + +module resources 'resources.bicep' = { + name: 'resources' + scope: rg + params: { + environmentName: environmentName + location: location + principalId: principalId + apiImageName: apiImageName + webImageName: webImageName + } +} + +output APPLICATIONINSIGHTS_CONNECTION_STRING string = resources.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output AZURE_CONTAINER_REGISTRY_NAME string = resources.outputs.AZURE_CONTAINER_REGISTRY_NAME +output AZURE_COSMOS_CONNECTION_STRING_KEY string = resources.outputs.AZURE_COSMOS_CONNECTION_STRING_KEY +output AZURE_COSMOS_DATABASE_NAME string = resources.outputs.AZURE_COSMOS_DATABASE_NAME +output AZURE_KEY_VAULT_ENDPOINT string = resources.outputs.AZURE_KEY_VAULT_ENDPOINT +output AZURE_LOCATION string = location +output AZURE_TENANT string = tenant().tenantId +output REACT_APP_API_BASE_URL string = resources.outputs.API_URI +output REACT_APP_APPLICATIONINSIGHTS_CONNECTION_STRING string = resources.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING +output REACT_APP_WEB_BASE_URL string = resources.outputs.WEB_URI diff --git a/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/main.parameters.json b/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/main.parameters.json new file mode 100644 index 00000000000..d25cd8d8018 --- /dev/null +++ b/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/main.parameters.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + }, + "apiImageName": { + "value": "${SERVICE_API_IMAGE_NAME}" + }, + "webImageName": { + "value": "${SERVICE_WEB_IMAGE_NAME}" + } + } +} \ No newline at end of file diff --git a/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/resources.bicep b/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/resources.bicep new file mode 100644 index 00000000000..f46733256de --- /dev/null +++ b/templates/todo/projects/java-mongo-aca/.repo/bicep/infra/resources.bicep @@ -0,0 +1,81 @@ +param environmentName string +param location string = resourceGroup().location +param principalId string = '' +param apiImageName string = '' +param webImageName string = '' + +// Container apps host (including container registry) +module containerApps '../../../../../../common/infra/bicep/core/host/container-apps.bicep' = { + name: 'container-apps' + params: { + environmentName: environmentName + location: location + logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName + } +} + +// Web frontend +module web '../../../../../common/infra/bicep/app/web-container-app.bicep' = { + name: 'web' + params: { + environmentName: environmentName + location: location + imageName: webImageName + apiName: api.outputs.API_NAME + applicationInsightsName: monitoring.outputs.applicationInsightsName + containerAppsEnvironmentName: containerApps.outputs.containerAppsEnvironmentName + containerRegistryName: containerApps.outputs.containerRegistryName + } +} + +// Api backend +module api '../../../../../common/infra/bicep/app/api-container-app.bicep' = { + name: 'api' + params: { + environmentName: environmentName + location: location + imageName: apiImageName + applicationInsightsName: monitoring.outputs.applicationInsightsName + containerAppsEnvironmentName: containerApps.outputs.containerAppsEnvironmentName + containerRegistryName: containerApps.outputs.containerRegistryName + keyVaultName: keyVault.outputs.keyVaultName + } +} + +// Application database +module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { + name: 'cosmos' + params: { + environmentName: environmentName + location: location + keyVaultName: keyVault.outputs.keyVaultName + } +} + +// Store secrets in a keyvault +module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { + name: 'keyvault' + params: { + environmentName: environmentName + location: location + principalId: principalId + } +} + +// Monitor application with Azure Monitor +module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { + name: 'monitoring' + params: { + environmentName: environmentName + location: location + } +} + +output API_URI string = api.outputs.API_URI +output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerApps.outputs.containerRegistryEndpoint +output AZURE_CONTAINER_REGISTRY_NAME string = containerApps.outputs.containerRegistryName +output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.cosmosConnectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.cosmosDatabaseName +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.keyVaultEndpoint +output WEB_URI string = web.outputs.WEB_URI diff --git a/templates/todo/projects/java-mongo-aca/.repo/bicep/repo.yaml b/templates/todo/projects/java-mongo-aca/.repo/bicep/repo.yaml new file mode 100644 index 00000000000..b88c0e7acf0 --- /dev/null +++ b/templates/todo/projects/java-mongo-aca/.repo/bicep/repo.yaml @@ -0,0 +1,172 @@ +templateApi: 1.0.0 +metadata: + type: repo + name: todo-java-mongo-aca + description: ToDo Application with a Java API and Azure Cosmos DB API for MongoDB on Azure Container Apps + +repo: + includeProjectAssets: false + + remotes: + - name: azure-samples-main + url: git@github.com:Azure-Samples/todo-java-mongo-aca.git + - name: azure-samples-staging + url: git@github.com:Azure-Samples/todo-java-mongo-aca.git + branch: staging + + rewrite: + rules: + - from: ../../../../../../common/infra/bicep/core + to: ./core + patterns: + - "**/*.bicep" + + - from: ../../../../../common/infra/bicep/app + to: ./app + patterns: + - "**/*.bicep" + + - from: ../../../../../common/infra/bicep/core + to: ../core + patterns: + - "**/*.bicep" + + # app service modules + - from: ../../../../../../common/infra/bicep + to: ../ + patterns: + - "**/*.bicep" + ignore: + - "**/main.bicep" + + - from: ../../../../../common/infra/bicep + to: ../ + patterns: + - "**/*.bicep" + ignore: + - "**/main.bicep" + + # main.bicep + - from: ../../../../../../common/infra/bicep + to: ./ + patterns: + - "**/main.bicep" + + - from: ../../api/java + to: ./src/api + patterns: + - "**/azure.@(yml|yaml)" + + - from: ../../web/react-fluent + to: ./src/web + patterns: + - "**/azure.@(yml|yaml)" + + - from: web-container-app.bicep + to: web.bicep + patterns: + - "**/resources.bicep" + + - from: api-container-app.bicep + to: api.bicep + patterns: + - "**/resources.bicep" + + - from: cosmos-mongo-db.bicep + to: db.bicep + patterns: + - "**/resources.bicep" + + assets: + # Common assets + + # Infra + - from: ./infra/ + to: ./infra + + - from: ../../../../common/infra/bicep/app/web-container-app.bicep + to: ./infra/app/web.bicep + + - from: ../../../../common/infra/bicep/app/api-container-app.bicep + to: ./infra/app/api.bicep + + - from: ../../../../common/infra/bicep/app/web-container-app.parameters.json + to: ./infra/app/web.parameters.json + + - from: ../../../../common/infra/bicep/app/api-container-app.parameters.json + to: ./infra/app/api.parameters.json + + - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep + to: ./infra/app/db.bicep + + - from: ./../../ + to: ./ + ignore: + - ".repo/**/*" + - "repo.y[a]ml" + - "azure.y[a]ml" + + # openapi.yaml to root + - from: ../../../../api/common + to: ./ + patterns: + - openapi.yaml + + # openapi.yaml to api root + - from: ../../../../api/common + to: ./src/api + patterns: + - openapi.yaml + + # Templates common + - from: ../../../../../common + to: ./ + ignore: + - .github/**/* + - .devcontainer/**/* + - "infra/**/*" + + # Github workflows for bicep + - from: ../../../../../common/.github/workflows/bicep + to: ./.github/workflows + + # azd core modules + - from: ../../../../../common/infra/bicep + to: ./infra + + # .devcontainer common (devcontainer.json) + - from: ../../../../../common/.devcontainer/devcontainer.json/java + to: ./.devcontainer + + # .devcontainer common (Dockerfile) + - from: ../../../../../common/.devcontainer/Dockerfile/base + to: ./.devcontainer + + # Assets common + - from: ../../../../common/assets + to: ./assets + + # Tests common + - from: ../../../../common/tests + to: ./tests + + # Java API + - from: ../../../../api/java + to: ./src/api + ignore: + - "target/**/*" + - "**/*.log" + + # React Frontend + - from: ../../../../web/react-fluent + to: ./src/web + ignore: + - "build/**/*" + - "node_modules/**/*" + + - from: ./infra/ + to: ./infra + + # Azure.yml + - from: ./azure.yaml + to: ./azure.yaml diff --git a/templates/todo/projects/java-mongo-aca/.vscode/launch.json b/templates/todo/projects/java-mongo-aca/.vscode/launch.json new file mode 100644 index 00000000000..4127d5eeaee --- /dev/null +++ b/templates/todo/projects/java-mongo-aca/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug API", + "type": "java", + "request": "launch", + "preLaunchTask": "Build API", + "mainClass": "com.microsoft.azure.simpletodo.SimpleTodoApplication", + "args": [], + "cwd": "${workspaceFolder}/src/api", + "env": {}, + "envFile": "${input:dotEnvFilePath}" + }, + { + "name": "Debug Web", + "request": "launch", + "type": "msedge", + "webRoot": "${workspaceFolder}/src/web/src", + "url": "http://localhost:3000", + "sourceMapPathOverrides": { + "webpack:///src/*": "${webRoot}/*" + }, + } + ], + + "inputs": [ + { + "id": "dotEnvFilePath", + "type": "command", + "command": "azure-dev.commands.getDotEnvFilePath" + } + ] +} diff --git a/templates/todo/projects/java-mongo-aca/.vscode/tasks.json b/templates/todo/projects/java-mongo-aca/.vscode/tasks.json new file mode 100644 index 00000000000..59de83bc45c --- /dev/null +++ b/templates/todo/projects/java-mongo-aca/.vscode/tasks.json @@ -0,0 +1,90 @@ +{ + "version": "2.0.0", + "tasks": [ + + { + "label": "Start API", + "type": "dotenv", + "targetTasks": "API mvn spring-boot:run", + "file": "${input:dotEnvFilePath}" + }, + { + "label": "API mvn spring-boot:run", + "detail": "Helper task--use 'Start API' task to ensure environment is set up correctly", + "type": "shell", + "command": "./mvnw spring-boot:run", + "options": { + "cwd": "${workspaceFolder}/src/api/" + }, + "presentation": { + "panel": "dedicated", + }, + "problemMatcher": [] + }, + { + "label": "Build API", + "command": "./mvnw", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}/src/api/" + }, + "args": [ + "package", + "-DskipTests" + ], + "problemMatcher": [] + }, + { + "label": "Start Web", + "type": "dotenv", + "targetTasks": [ + "Restore Web", + "Web npm start" + ], + "file": "${input:dotEnvFilePath}" + }, + { + "label": "Restore Web", + "type": "shell", + "command": "azd restore --service web", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": [] + }, + { + "label": "Web npm start", + "detail": "Helper task--use 'Start Web' task to ensure environment is set up correctly", + "type": "shell", + "command": "npm run start", + "options": { + "cwd": "${workspaceFolder}/src/web/", + "env": { + "REACT_APP_API_BASE_URL": "http://localhost:3100", + "BROWSER": "none" + } + }, + "presentation": { + "panel": "dedicated", + }, + "problemMatcher": [] + }, + + { + "label": "Start API and Web", + "dependsOn":[ + "Start API", + "Start Web" + ], + "problemMatcher": [] + } + ], + + "inputs": [ + { + "id": "dotEnvFilePath", + "type": "command", + "command": "azure-dev.commands.getDotEnvFilePath" + } + ] +} diff --git a/templates/todo/projects/java-mongo-aca/README.md b/templates/todo/projects/java-mongo-aca/README.md new file mode 100644 index 00000000000..ba69cf45ae9 --- /dev/null +++ b/templates/todo/projects/java-mongo-aca/README.md @@ -0,0 +1,232 @@ +# ToDo Application with a Java API and Azure Cosmos DB API for MongoDB on Azure Container Apps + +[![Open in Remote - Containers](https://img.shields.io/static/v1?label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/todo-java-mongo-aca) + +A complete ToDo application that includes everything you need to build, deploy, and monitor an Azure solution. This application uses the Azure Developer CLI (azd) to get you up and running on Azure quickly, React.js for the Web application, Java for the API, Azure Cosmos DB API for MongoDB for storage, and Azure Monitor for monitoring and logging. It includes application code, tools, and pipelines that serve as a foundation from which you can build upon and customize when creating your own solutions. + +Let's jump in and get the ToDo app up and running in Azure. When you are finished, you will have a fully functional web app deployed on Azure. In later steps, you'll see how to setup a pipeline and monitor the application. + +Screenshot of deployed ToDo app + +Screenshot of the deployed ToDo app + +### Prerequisites + +The following prerequisites are required to use this application. Please ensure that you have them all installed locally. + +- [Azure Developer CLI](https://aka.ms/azure-dev/install) + - Windows: + ```powershell + powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/install-azd.ps1' | Invoke-Expression" + ``` + - Linux/MacOS: + ``` + curl -fsSL https://aka.ms/install-azd.sh | bash + ``` +- [Azure CLI (2.38.0+)](https://docs.microsoft.com/cli/azure/install-azure-cli) +- [Java 17 or later](https://jdk.java.net/) - for API backend +- [Node.js with npm (16.13.1+)](https://nodejs.org/) - for the Web frontend +- [Docker](https://docs.docker.com/get-docker/) +- [Git (2.36.1+)](https://git-scm.com/) + +### Quickstart + +The fastest way for you to get this application up and running on Azure is to use the `azd up` command. This single command will create and configure all necessary Azure resources - including access policies and roles for your account and service-to-service communication with Managed Identities. + +1. Open a terminal, create a new empty folder, and change into it. +1. Run the following command to initialize the project, provision Azure resources, and deploy the application code. + +```bash +azd up --template todo-java-mongo-aca +``` + +You will be prompted for the following information: + +- `Environment Name`: This will be used as a prefix for the resource group that will be created to hold all Azure resources. This name should be unique within your Azure subscription. +- `Azure Location`: The Azure location where your resources will be deployed. +- `Azure Subscription`: The Azure Subscription where your resources will be deployed. + +> NOTE: This template may only be used with the following Azure locations: +> +> - Australia East +> - Brazil South +> - Canada Central +> - Central US +> - East Asia +> - East US +> - East US 2 +> - Germany West Central +> - Japan East +> - Korea Central +> - North Central US +> - North Europe +> - South Central US +> - UK South +> - West Europe +> - West US +> +> If you attempt to use the template with an unsupported region, the provision step will fail. + +> NOTE: This may take a while to complete as it executes three commands: `azd init` (initializes environment), `azd provision` (provisions Azure resources), and `azd deploy` (deploys application code). You will see a progress indicator as it provisions and deploys your application. + +When `azd up` is complete it will output the following URLs: + +- Azure Portal link to view resources +- ToDo Web application frontend +- ToDo API application + +!["azd up output"](assets/urls.png) + +Click the web application URL to launch the ToDo app. Create a new collection and add some items. This will create monitoring activity in the application that you will be able to see later when you run `azd monitor`. + +> NOTE: +> +> - The `azd up` command will create Azure resources that will incur costs to your Azure subscription. You can clean up those resources manually via the Azure portal or with the `azd down` command. +> - You can call `azd up` as many times as you like to both provision and deploy your solution, but you only need to provide the `--template` parameter the first time you call it to get the code locally. Subsequent `azd up` calls do not require the template parameter. If you do provide the parameter, all your local source code will be overwritten if you agree to overwrite when prompted. +> - You can always create a new environment with `azd env new`. + +### Application Architecture + +This application utilizes the following Azure resources: + +- [**Azure Container Apps**](https://docs.microsoft.com/azure/container-apps/) to host the Web frontend and API backend +- [**Azure Cosmos DB API for MongoDB**](https://docs.microsoft.com/azure/cosmos-db/mongodb/mongodb-introduction) for storage +- [**Azure Monitor**](https://docs.microsoft.com/azure/azure-monitor/) for monitoring and logging +- [**Azure Key Vault**](https://docs.microsoft.com/azure/key-vault/) for securing secrets + +Here's a high level architecture diagram that illustrates these components. Notice that these are all contained within a single [resource group](https://docs.microsoft.com/azure/azure-resource-manager/management/manage-resource-groups-portal), that will be created for you when you create the resources. + +Application architecture diagram + +> This template provisions resources to an Azure subscription that you will select upon provisioning them. Please refer to the [Pricing calculator for Microsoft Azure](https://azure.microsoft.com/pricing/calculator/) and, if needed, update the included Azure resource definitions found in `infra/main.bicep` to suit your needs. + +### Application Code + +The repo is structured to follow the [Azure Developer CLI](https://aka.ms/azure-dev/overview) conventions including: + +- **Source Code**: All application source code is located in the `src` folder. +- **Infrastructure as Code**: All application "infrastructure as code" files are located in the `infra` folder. +- **Azure Developer Configuration**: An `azure.yaml` file located in the root that ties the application source code to the Azure services defined in your "infrastructure as code" files. +- **GitHub Actions**: A sample GitHub action file is located in the `.github/workflows` folder. +- **VS Code Configuration**: All VS Code configuration to run and debug the application is located in the `.vscode` folder. + +### Azure Subscription + +This template will create infrastructure and deploy code to Azure. If you don't have an Azure Subscription, you can sign up for a [free account here](https://azure.microsoft.com/free/). + +### Azure Developer CLI - VS Code Extension + +The Azure Developer experience includes an Azure Developer CLI VS Code Extension that mirrors all of the Azure Developer CLI commands into the `azure.yaml` context menu and command palette options. If you are a VS Code user, then we highly recommend installing this extension for the best experience. + +Here's how to install it: + +#### VS Code + +1. Click on the "Extensions" tab in VS Code +1. Search for "Azure Developer CLI" - authored by Microsoft +1. Click "Install" + +#### Marketplace + +1. Go to the [Azure Developer CLI - VS Code Extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azure-dev) page +1. Click "Install" + +Once the extension is installed, you can press `F1`, and type "Azure Developer CLI" to see all of your available options. You can also right click on your project's `azure.yaml` file for a list of commands. + +### Next Steps + +At this point, you have a complete application deployed on Azure. But there is much more that the Azure Developer CLI can do. These next steps will introduce you to additional commands that will make creating applications on Azure much easier. Using the Azure Developer CLI, you can setup your pipelines, monitor your application, test and debug locally. + +#### Set up a pipeline using `azd pipeline` + +This template includes a GitHub Actions pipeline configuration file that will deploy your application whenever code is pushed to the main branch. You can find that pipeline file here: `.github/workflows`. + +Setting up this pipeline requires you to give GitHub permission to deploy to Azure on your behalf, which is done via a Service Principal stored in a GitHub secret named `AZURE_CREDENTIALS`. The `azd pipeline config` command will automatically create a service principal for you. The command also helps to create a private GitHub repository and pushes code to the newly created repo. + +Before you call the `azd pipeline config` command, you'll need to install the following: + +- [GitHub CLI (2.3+)](https://github.com/cli/cli) + +Run the following command to set up a GitHub Action: + +```bash +azd pipeline config +``` + +> Support for Azure DevOps Pipelines is coming soon to `azd pipeline config`. In the meantime, you can follow the instructions found here: [.azdo/pipelines/README.md](./.azdo/pipelines/README.md) to set it up manually. + +#### Monitor the application using `azd monitor` + +To help with monitoring applications, the Azure Dev CLI provides a `monitor` command to help you get to the various Application Insights dashboards. + +- Run the following command to open the "Overview" dashboard: + + ```bash + azd monitor --overview + ``` + +- Live Metrics Dashboard + + Run the following command to open the "Live Metrics" dashboard: + + ```bash + azd monitor --live + ``` + +- Logs Dashboard + + Run the following command to open the "Logs" dashboard: + + ```bash + azd monitor --logs + ``` + +#### Run and Debug Locally + +The easiest way to run and debug is to leverage the Azure Developer CLI Visual Studio Code Extension. Refer to this [walk-through](https://aka.ms/azure-dev/vscode) for more details. + +#### Clean up resources + +When you are done, you can delete all the Azure resources created with this template by running the following command: + +```bash +azd down +``` + +### Additional azd commands + +The Azure Developer CLI includes many other commands to help with your Azure development experience. You can view these commands at the terminal by running `azd help`. You can also view the full list of commands on our [Azure Developer CLI command](https://aka.ms/azure-dev/ref) page. + +## Troubleshooting/Known issues + +Sometimes, things go awry. If you happen to run into issues, then please review our ["Known Issues"](https://aka.ms/azure-dev/knownissues) page for help. If you continue to have issues, then please file an issue in our main [Azure Dev](https://aka.ms/azure-dev/issues) repository. + +## Security + +### Roles + +This template creates a [managed identity](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) for your app inside your Azure Active Directory tenant, and it is used to authenticate your app with Azure and other services that support Azure AD authentication like Key Vault via access policies. You will see principalId referenced in the infrastructure as code files, that refers to the id of the currently logged in Azure CLI user, which will be granted access policies and permissions to run the application locally. To view your managed identity in the Azure Portal, follow these [steps](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-view-managed-identity-service-principal-portal). + +### Key Vault + +This template uses [Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/overview) to securely store your Cosmos DB connection string for the provisioned Cosmos DB account. Key Vault is a cloud service for securely storing and accessing secrets (API keys, passwords, certificates, cryptographic keys) and makes it simple to give other Azure services access to them. As you continue developing your solution, you may add as many secrets to your Key Vault as you require. + +## Uninstall + +To uninstall the Azure Developer CLI: + +Windows: + +``` +powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/uninstall-azd.ps1' | Invoke-Expression" +``` + +Linux/MacOS: + +``` +curl -fsSL https://aka.ms/uninstall-azd.sh | bash +``` + +## Reporting Issues and Feedback + +If you have any feature requests, issues, or areas for improvement, please [file an issue](https://aka.ms/azure-dev/issues). To keep up-to-date, ask questions, or share suggestions, join our [GitHub Discussions](https://aka.ms/azure-dev/discussions). You may also contact us via AzDevTeam@microsoft.com. diff --git a/templates/todo/projects/java-mongo-aca/assets/resources.png b/templates/todo/projects/java-mongo-aca/assets/resources.png new file mode 100644 index 00000000000..229d955e1e3 Binary files /dev/null and b/templates/todo/projects/java-mongo-aca/assets/resources.png differ diff --git a/templates/todo/projects/java-mongo/.repo/bicep/azure.yaml b/templates/todo/projects/java-mongo/.repo/bicep/azure.yaml new file mode 100644 index 00000000000..ba47ec2ad91 --- /dev/null +++ b/templates/todo/projects/java-mongo/.repo/bicep/azure.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: todo-java-mongo +metadata: + template: todo-java-mongo@0.0.1-beta +services: + web: + project: ../../web/react-fluent + dist: build + language: js + host: appservice + api: + project: ../../api/java + language: java + host: appservice diff --git a/templates/todo/projects/java-mongo/.repo/bicep/infra/main.bicep b/templates/todo/projects/java-mongo/.repo/bicep/infra/main.bicep new file mode 100644 index 00000000000..b010ed8d985 --- /dev/null +++ b/templates/todo/projects/java-mongo/.repo/bicep/infra/main.bicep @@ -0,0 +1,42 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +param location string + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +var abbrs = loadJsonContent('../../../../../../common/infra/bicep/abbreviations.json') +var tags = { 'azd-env-name': environmentName } + +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: '${abbrs.resourcesResourceGroups}${environmentName}' + location: location + tags: tags +} + +module resources 'resources.bicep' = { + name: 'resources' + scope: rg + params: { + environmentName: environmentName + location: location + principalId: principalId + } +} + +output APPLICATIONINSIGHTS_CONNECTION_STRING string = resources.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING +output AZURE_COSMOS_CONNECTION_STRING_KEY string = resources.outputs.AZURE_COSMOS_CONNECTION_STRING_KEY +output AZURE_COSMOS_DATABASE_NAME string = resources.outputs.AZURE_COSMOS_DATABASE_NAME +output AZURE_KEY_VAULT_ENDPOINT string = resources.outputs.AZURE_KEY_VAULT_ENDPOINT +output AZURE_LOCATION string = location +output AZURE_TENANT string = tenant().tenantId +output REACT_APP_API_BASE_URL string = resources.outputs.API_URI +output REACT_APP_APPLICATIONINSIGHTS_CONNECTION_STRING string = resources.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING +output REACT_APP_WEB_BASE_URL string = resources.outputs.WEB_URI diff --git a/templates/todo/projects/java-mongo/.repo/bicep/infra/main.parameters.json b/templates/todo/projects/java-mongo/.repo/bicep/infra/main.parameters.json new file mode 100644 index 00000000000..67ad8524c44 --- /dev/null +++ b/templates/todo/projects/java-mongo/.repo/bicep/infra/main.parameters.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + } + } +} \ No newline at end of file diff --git a/templates/todo/projects/java-mongo/.repo/bicep/infra/resources.bicep b/templates/todo/projects/java-mongo/.repo/bicep/infra/resources.bicep new file mode 100644 index 00000000000..612dd6ecb7c --- /dev/null +++ b/templates/todo/projects/java-mongo/.repo/bicep/infra/resources.bicep @@ -0,0 +1,84 @@ +param environmentName string +param location string = resourceGroup().location +param principalId string = '' + +// The application frontend +module web '../../../../../common/infra/bicep/app/web-appservice.bicep' = { + name: 'web' + params: { + environmentName: environmentName + location: location + applicationInsightsName: monitoring.outputs.applicationInsightsName + appServicePlanId: appServicePlan.outputs.appServicePlanId + } +} + +// The application backend +module api '../../../../../common/infra/bicep/app/api-appservice-java.bicep' = { + name: 'api' + params: { + environmentName: environmentName + location: location + applicationInsightsName: monitoring.outputs.applicationInsightsName + appServicePlanId: appServicePlan.outputs.appServicePlanId + keyVaultName: keyVault.outputs.keyVaultName + allowedOrigins: [ web.outputs.WEB_URI ] + } +} + +// The application database +module cosmos '../../../../../common/infra/bicep/app/cosmos-mongo-db.bicep' = { + name: 'cosmos' + params: { + environmentName: environmentName + location: location + keyVaultName: keyVault.outputs.keyVaultName + } +} + +// Configure api to use cosmos +module apiCosmosConfig '../../../../../../common/infra/bicep/core/host/appservice-config-cosmos.bicep' = { + name: 'api-cosmos-config' + params: { + appServiceName: api.outputs.API_NAME + cosmosDatabaseName: cosmos.outputs.cosmosDatabaseName + cosmosConnectionStringKey: cosmos.outputs.cosmosConnectionStringKey + cosmosEndpoint: cosmos.outputs.cosmosEndpoint + } +} + +// Create an App Service Plan to group applications under the same payment plan and SKU +module appServicePlan '../../../../../../common/infra/bicep/core/host/appserviceplan-sites.bicep' = { + name: 'appserviceplan' + params: { + environmentName: environmentName + location: location + } +} + +// Store secrets in a keyvault +module keyVault '../../../../../../common/infra/bicep/core/security/keyvault.bicep' = { + name: 'keyvault' + params: { + environmentName: environmentName + location: location + principalId: principalId + } +} + +// Monitor application with Azure Monitor +module monitoring '../../../../../../common/infra/bicep/core/monitor/monitoring.bicep' = { + name: 'monitoring' + params: { + environmentName: environmentName + location: location + } +} + +output API_URI string = api.outputs.API_URI +output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString +output AZURE_COSMOS_CONNECTION_STRING_KEY string = cosmos.outputs.cosmosConnectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = cosmos.outputs.cosmosDatabaseName +output AZURE_COSMOS_ENDPOINT string = cosmos.outputs.cosmosEndpoint +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.keyVaultEndpoint +output WEB_URI string = web.outputs.WEB_URI diff --git a/templates/todo/projects/java-mongo/.repo/bicep/repo.yaml b/templates/todo/projects/java-mongo/.repo/bicep/repo.yaml new file mode 100644 index 00000000000..ddcdc32c6f6 --- /dev/null +++ b/templates/todo/projects/java-mongo/.repo/bicep/repo.yaml @@ -0,0 +1,156 @@ +templateApi: 1.0.0 +metadata: + type: repo + name: todo-java-mongo + description: ToDo Application with a Java API and Azure Cosmos DB API for MongoDB + +repo: + includeProjectAssets: false + + remotes: + - name: azure-samples-main + url: git@github.com:Azure-Samples/todo-java-mongo.git + - name: azure-samples-staging + url: git@github.com:Azure-Samples/todo-java-mongo.git + branch: staging + + rewrite: + rules: + - from: ../../../../../../common/infra/bicep/core + to: ./core + patterns: + - "**/*.bicep" + + - from: ../../../../../common/infra/bicep/app + to: ./app + patterns: + - "**/*.bicep" + + - from: ../../../../../common/infra/bicep/core + to: ../core + patterns: + - "**/*.bicep" + + # app service modules + - from: ../../../../../../common/infra/bicep + to: ../ + patterns: + - "**/*.bicep" + ignore: + - "**/main.bicep" + + # main.bicep + - from: ../../../../../../common/infra/bicep + to: ./ + patterns: + - "**/main.bicep" + + - from: ../../api/java + to: ./src/api + patterns: + - "**/azure.@(yml|yaml)" + + - from: ../../web/react-fluent + to: ./src/web + patterns: + - "**/azure.@(yml|yaml)" + + - from: web-appservice.bicep + to: web.bicep + patterns: + - "**/resources.bicep" + + - from: api-appservice-java.bicep + to: api.bicep + patterns: + - "**/resources.bicep" + + - from: cosmos-mongo-db.bicep + to: db.bicep + patterns: + - "**/resources.bicep" + + assets: + # Common assets + + # Infra + - from: ./infra/ + to: ./infra + + - from: ../../../../common/infra/bicep/app/web-appservice.bicep + to: ./infra/app/web.bicep + + - from: ../../../../common/infra/bicep/app/api-appservice-java.bicep + to: ./infra/app/api.bicep + + - from: ../../../../common/infra/bicep/app/cosmos-mongo-db.bicep + to: ./infra/app/db.bicep + + - from: ./../../ + to: ./ + ignore: + - ".repo/**/*" + - "repo.y[a]ml" + - "azure.y[a]ml" + + # openapi.yaml to root + - from: ../../../../api/common + to: ./ + patterns: + - openapi.yaml + + # openapi.yaml to api root + - from: ../../../../api/common + to: ./src/api + patterns: + - openapi.yaml + + # Templates common + - from: ../../../../../common + to: ./ + ignore: + - .github/**/* + - .devcontainer/**/* + - "infra/**/*" + + # Github workflows for bicep + - from: ../../../../../common/.github/workflows/bicep + to: ./.github/workflows + + # azd core modules + - from: ../../../../../common/infra/bicep + to: ./infra + + # .devcontainer common (devcontainer.json) + - from: ../../../../../common/.devcontainer/devcontainer.json/java + to: ./.devcontainer + + # .devcontainer common (Dockerfile) + - from: ../../../../../common/.devcontainer/Dockerfile/base + to: ./.devcontainer + + # Assets common + - from: ../../../../common/assets + to: ./assets + + # Tests common + - from: ../../../../common/tests + to: ./tests + + # Java API + - from: ../../../../api/java + to: ./src/api + ignore: + - "target/**/*" + - "**/*.log" + + # React Frontend + - from: ../../../../web/react-fluent + to: ./src/web + ignore: + - "build/**/*" + - "node_modules/**/*" + + # Azure.yml + - from: ./azure.yaml + to: ./azure.yaml diff --git a/templates/todo/projects/java-mongo/.vscode/launch.json b/templates/todo/projects/java-mongo/.vscode/launch.json new file mode 100644 index 00000000000..4127d5eeaee --- /dev/null +++ b/templates/todo/projects/java-mongo/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug API", + "type": "java", + "request": "launch", + "preLaunchTask": "Build API", + "mainClass": "com.microsoft.azure.simpletodo.SimpleTodoApplication", + "args": [], + "cwd": "${workspaceFolder}/src/api", + "env": {}, + "envFile": "${input:dotEnvFilePath}" + }, + { + "name": "Debug Web", + "request": "launch", + "type": "msedge", + "webRoot": "${workspaceFolder}/src/web/src", + "url": "http://localhost:3000", + "sourceMapPathOverrides": { + "webpack:///src/*": "${webRoot}/*" + }, + } + ], + + "inputs": [ + { + "id": "dotEnvFilePath", + "type": "command", + "command": "azure-dev.commands.getDotEnvFilePath" + } + ] +} diff --git a/templates/todo/projects/java-mongo/.vscode/tasks.json b/templates/todo/projects/java-mongo/.vscode/tasks.json new file mode 100644 index 00000000000..59de83bc45c --- /dev/null +++ b/templates/todo/projects/java-mongo/.vscode/tasks.json @@ -0,0 +1,90 @@ +{ + "version": "2.0.0", + "tasks": [ + + { + "label": "Start API", + "type": "dotenv", + "targetTasks": "API mvn spring-boot:run", + "file": "${input:dotEnvFilePath}" + }, + { + "label": "API mvn spring-boot:run", + "detail": "Helper task--use 'Start API' task to ensure environment is set up correctly", + "type": "shell", + "command": "./mvnw spring-boot:run", + "options": { + "cwd": "${workspaceFolder}/src/api/" + }, + "presentation": { + "panel": "dedicated", + }, + "problemMatcher": [] + }, + { + "label": "Build API", + "command": "./mvnw", + "type": "shell", + "options": { + "cwd": "${workspaceFolder}/src/api/" + }, + "args": [ + "package", + "-DskipTests" + ], + "problemMatcher": [] + }, + { + "label": "Start Web", + "type": "dotenv", + "targetTasks": [ + "Restore Web", + "Web npm start" + ], + "file": "${input:dotEnvFilePath}" + }, + { + "label": "Restore Web", + "type": "shell", + "command": "azd restore --service web", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": [] + }, + { + "label": "Web npm start", + "detail": "Helper task--use 'Start Web' task to ensure environment is set up correctly", + "type": "shell", + "command": "npm run start", + "options": { + "cwd": "${workspaceFolder}/src/web/", + "env": { + "REACT_APP_API_BASE_URL": "http://localhost:3100", + "BROWSER": "none" + } + }, + "presentation": { + "panel": "dedicated", + }, + "problemMatcher": [] + }, + + { + "label": "Start API and Web", + "dependsOn":[ + "Start API", + "Start Web" + ], + "problemMatcher": [] + } + ], + + "inputs": [ + { + "id": "dotEnvFilePath", + "type": "command", + "command": "azure-dev.commands.getDotEnvFilePath" + } + ] +} diff --git a/templates/todo/projects/java-mongo/README.md b/templates/todo/projects/java-mongo/README.md new file mode 100644 index 00000000000..ffadd601cfa --- /dev/null +++ b/templates/todo/projects/java-mongo/README.md @@ -0,0 +1,210 @@ +# ToDo Application with a Java API and Azure Cosmos DB API for MongoDB on Azure App Service + +[![Open in Remote - Containers](https://img.shields.io/static/v1?label=Remote%20-%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/azure-samples/todo-java-mongo) + +A complete ToDo application that includes everything you need to build, deploy, and monitor an Azure solution. This application uses the Azure Developer CLI (azd) to get you up and running on Azure quickly, React.js for the Web application, Java for the API, Azure Cosmos DB API for MongoDB for storage, and Azure Monitor for monitoring and logging. It includes application code, tools, and pipelines that serve as a foundation from which you can build upon and customize when creating your own solutions. + +Let's jump in and get the ToDo app up and running in Azure. When you are finished, you will have a fully functional web app deployed on Azure. In later steps, you'll see how to setup a pipeline and monitor the application. + +Screenshot of deployed ToDo app + +Screenshot of the deployed ToDo app + +### Prerequisites + +The following prerequisites are required to use this application. Please ensure that you have them all installed locally. + +- [Azure Developer CLI](https://aka.ms/azure-dev/install) + - Windows: + ```powershell + powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/install-azd.ps1' | Invoke-Expression" + ``` + - Linux/MacOS: + ``` + curl -fsSL https://aka.ms/install-azd.sh | bash + ``` +- [Azure CLI (2.38.0+)](https://docs.microsoft.com/cli/azure/install-azure-cli) +- [Java 17 or later](https://jdk.java.net/) - for API backend +- [Node.js with npm (16.13.1+)](https://nodejs.org/) - for Web frontend +- [Git (2.36.1+)](https://git-scm.com/) + +### Quickstart + +The fastest way for you to get this application up and running on Azure is to use the `azd up` command. This single command will create and configure all necessary Azure resources - including access policies and roles for your account and service-to-service communication with Managed Identities. + +1. Open a terminal, create a new empty folder, and change into it. +1. Run the following command to initialize the project, provision Azure resources, and deploy the application code. + +```bash +azd up --template todo-java-mongo +``` + +You will be prompted for the following information: + +- `Environment Name`: This will be used as a prefix for the resource group that will be created to hold all Azure resources. This name should be unique within your Azure subscription. +- `Azure Location`: The Azure location where your resources will be deployed. +- `Azure Subscription`: The Azure Subscription where your resources will be deployed. + +> NOTE: This may take a while to complete as it executes three commands: `azd init` (initializes environment), `azd provision` (provisions Azure resources), and `azd deploy` (deploys application code). You will see a progress indicator as it provisions and deploys your application. + +When `azd up` is complete it will output the following URLs: + +- Azure Portal link to view resources +- ToDo Web application frontend +- ToDo API application + +!["azd up output"](assets/urls.png) + +Click the web application URL to launch the ToDo app. Create a new collection and add some items. This will create monitoring activity in the application that you will be able to see later when you run `azd monitor`. + +> NOTE: +> +> - The `azd up` command will create Azure resources that will incur costs to your Azure subscription. You can clean up those resources manually via the Azure portal or with the `azd down` command. +> - You can call `azd up` as many times as you like to both provision and deploy your solution, but you only need to provide the `--template` parameter the first time you call it to get the code locally. Subsequent `azd up` calls do not require the template parameter. If you do provide the parameter, all your local source code will be overwritten if you agree to overwrite when prompted. +> - You can always create a new environment with `azd env new`. + +### Application Architecture + +This application utilizes the following Azure resources: + +- [**Azure App Services**](https://docs.microsoft.com/azure/app-service/) to host the Web frontend and API backend +- [**Azure Cosmos DB API for MongoDB**](https://docs.microsoft.com/azure/cosmos-db/mongodb/mongodb-introduction) for storage +- [**Azure Monitor**](https://docs.microsoft.com/azure/azure-monitor/) for monitoring and logging +- [**Azure Key Vault**](https://docs.microsoft.com/azure/key-vault/) for securing secrets + +Here's a high level architecture diagram that illustrates these components. Notice that these are all contained within a single [resource group](https://docs.microsoft.com/azure/azure-resource-manager/management/manage-resource-groups-portal), that will be created for you when you create the resources. + +Application architecture diagram + +> This template provisions resources to an Azure subscription that you will select upon provisioning them. Please refer to the [Pricing calculator for Microsoft Azure](https://azure.microsoft.com/pricing/calculator/) and, if needed, update the included Azure resource definitions found in `infra/main.bicep` to suit your needs. + +### Application Code + +The repo is structured to follow the [Azure Developer CLI](https://aka.ms/azure-dev/overview) conventions including: + +- **Source Code**: All application source code is located in the `src` folder. +- **Infrastructure as Code**: All application "infrastructure as code" files are located in the `infra` folder. +- **Azure Developer Configuration**: An `azure.yaml` file located in the root that ties the application source code to the Azure services defined in your "infrastructure as code" files. +- **GitHub Actions**: A sample GitHub action file is located in the `.github/workflows` folder. +- **VS Code Configuration**: All VS Code configuration to run and debug the application is located in the `.vscode` folder. + +### Azure Subscription + +This template will create infrastructure and deploy code to Azure. If you don't have an Azure Subscription, you can sign up for a [free account here](https://azure.microsoft.com/free/). + +### Azure Developer CLI - VS Code Extension + +The Azure Developer experience includes an Azure Developer CLI VS Code Extension that mirrors all of the Azure Developer CLI commands into the `azure.yaml` context menu and command palette options. If you are a VS Code user, then we highly recommend installing this extension for the best experience. + +Here's how to install it: + +#### VS Code + +1. Click on the "Extensions" tab in VS Code +1. Search for "Azure Developer CLI" - authored by Microsoft +1. Click "Install" + +#### Marketplace + +1. Go to the [Azure Developer CLI - VS Code Extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azure-dev) page +1. Click "Install" + +Once the extension is installed, you can press `F1`, and type "Azure Developer CLI" to see all of your available options. You can also right click on your project's `azure.yaml` file for a list of commands. + +### Next Steps + +At this point, you have a complete application deployed on Azure. But there is much more that the Azure Developer CLI can do. These next steps will introduce you to additional commands that will make creating applications on Azure much easier. Using the Azure Developer CLI, you can setup your pipelines, monitor your application, test and debug locally. + +#### Set up a pipeline using `azd pipeline` + +This template includes a GitHub Actions pipeline configuration file that will deploy your application whenever code is pushed to the main branch. You can find that pipeline file here: `.github/workflows`. + +Setting up this pipeline requires you to give GitHub permission to deploy to Azure on your behalf, which is done via a Service Principal stored in a GitHub secret named `AZURE_CREDENTIALS`. The `azd pipeline config` command will automatically create a service principal for you. The command also helps to create a private GitHub repository and pushes code to the newly created repo. + +Before you call the `azd pipeline config` command, you'll need to install the following: + +- [GitHub CLI (2.3+)](https://github.com/cli/cli) + +Run the following command to set up a GitHub Action: + +```bash +azd pipeline config +``` + +> Support for Azure DevOps Pipelines is coming soon to `azd pipeline config`. In the meantime, you can follow the instructions found here: [.azdo/pipelines/README.md](./.azdo/pipelines/README.md) to set it up manually. + +#### Monitor the application using `azd monitor` + +To help with monitoring applications, the Azure Dev CLI provides a `monitor` command to help you get to the various Application Insights dashboards. + +- Run the following command to open the "Overview" dashboard: + + ```bash + azd monitor --overview + ``` + +- Live Metrics Dashboard + + Run the following command to open the "Live Metrics" dashboard: + + ```bash + azd monitor --live + ``` + +- Logs Dashboard + + Run the following command to open the "Logs" dashboard: + + ```bash + azd monitor --logs + ``` + +#### Run and Debug Locally + +The easiest way to run and debug is to leverage the Azure Developer CLI Visual Studio Code Extension. Refer to this [walk-through](https://aka.ms/azure-dev/vscode) for more details. + +#### Clean up resources + +When you are done, you can delete all the Azure resources created with this template by running the following command: + +```bash +azd down +``` + +### Additional azd commands + +The Azure Developer CLI includes many other commands to help with your Azure development experience. You can view these commands at the terminal by running `azd help`. You can also view the full list of commands on our [Azure Developer CLI command](https://aka.ms/azure-dev/ref) page. + +## Troubleshooting/Known issues + +Sometimes, things go awry. If you happen to run into issues, then please review our ["Known Issues"](https://aka.ms/azure-dev/knownissues) page for help. If you continue to have issues, then please file an issue in our main [Azure Dev](https://aka.ms/azure-dev/issues) repository. + +## Security + +### Roles + +This template creates a [managed identity](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) for your app inside your Azure Active Directory tenant, and it is used to authenticate your app with Azure and other services that support Azure AD authentication like Key Vault via access policies. You will see principalId referenced in the infrastructure as code files, that refers to the id of the currently logged in Azure CLI user, which will be granted access policies and permissions to run the application locally. To view your managed identity in the Azure Portal, follow these [steps](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-view-managed-identity-service-principal-portal). + +### Key Vault + +This template uses [Azure Key Vault](https://docs.microsoft.com/azure/key-vault/general/overview) to securely store your Cosmos DB connection string for the provisioned Cosmos DB account. Key Vault is a cloud service for securely storing and accessing secrets (API keys, passwords, certificates, cryptographic keys) and makes it simple to give other Azure services access to them. As you continue developing your solution, you may add as many secrets to your Key Vault as you require. + +## Uninstall + +To uninstall the Azure Developer CLI: + +Windows: + +``` +powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/uninstall-azd.ps1' | Invoke-Expression" +``` + +Linux/MacOS: + +``` +curl -fsSL https://aka.ms/uninstall-azd.sh | bash +``` + +## Reporting Issues and Feedback + +If you have any feature requests, issues, or areas for improvement, please [file an issue](https://aka.ms/azure-dev/issues). To keep up-to-date, ask questions, or share suggestions, join our [GitHub Discussions](https://aka.ms/azure-dev/discussions). You may also contact us via AzDevTeam@microsoft.com. diff --git a/templates/todo/projects/java-mongo/assets/resources.png b/templates/todo/projects/java-mongo/assets/resources.png new file mode 100644 index 00000000000..9997056c8d2 Binary files /dev/null and b/templates/todo/projects/java-mongo/assets/resources.png differ diff --git a/templates/todo/web/react-fluent/README.md b/templates/todo/web/react-fluent/README.md index b1ebbab5a1a..de82c44353c 100644 --- a/templates/todo/web/react-fluent/README.md +++ b/templates/todo/web/react-fluent/README.md @@ -13,6 +13,10 @@ Create a `.env` file within the base of the `reactd-fluent` folder with the foll In the project directory, you can run: +### `npm ci` + +Installs local pre-requisites. + ### `npm start` Runs the app in the development mode.