├── .github ├── dependabot.yml └── workflows │ └── github-actions-build.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── Dockerfile ├── LICENSE ├── README.md ├── docs ├── argo.md └── k8s-istio.md ├── mvnw ├── mvnw.cmd ├── operations ├── argo │ ├── k8s-bluegreen-rollout-demo.yaml │ └── k8s-canary-rollout-demo.yaml ├── azure │ └── template │ │ ├── parameters.json │ │ └── template.json ├── istio │ ├── attendees-gateway.yaml │ ├── delayed-gateway.yaml │ └── half-broken-gateway.yaml └── k8s │ ├── attendees-deployment.yaml │ ├── grpc-service.yaml │ └── http-service.yaml ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── masteringapi │ │ └── attendees │ │ ├── AttendeesApplication.java │ │ ├── controller │ │ └── AttendeesController.java │ │ ├── grpc │ │ └── AttendeesServiceImpl.java │ │ ├── model │ │ ├── Attendee.java │ │ ├── AttendeeNotFoundException.java │ │ └── AttendeeResponse.java │ │ └── service │ │ └── AttendeeStore.java ├── proto │ └── attendees.proto └── resources │ └── application.properties └── test └── java └── com └── masteringapi └── attendees ├── TestAttendeesApplicationShould.java ├── grpc └── TestAttendeesServiceImplShould.java └── service └── TestAttendeeStoreShould.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "docker" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/github-actions-build.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: 4 | - pull_request 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up JDK 17 13 | uses: actions/setup-java@v3 14 | with: 15 | java-version: '17' 16 | distribution: 'adopt' 17 | - name: Build with Maven 18 | run: mvn --batch-mode --update-snapshots package -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masteringapi/attendees/0232ac9c6133d60010ee53e5d2b3a61c21ec81a5/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.9-eclipse-temurin-17 AS builder 2 | COPY src /usr/src/app/src 3 | COPY pom.xml /usr/src/app 4 | RUN mvn -f /usr/src/app/pom.xml clean package 5 | 6 | FROM eclipse-temurin:20 7 | RUN mkdir /opt/app 8 | COPY --from=builder /usr/src/app/target/attendees-0.0.1-SNAPSHOT.jar /opt/app/app.jar 9 | EXPOSE 8080 10 | EXPOSE 9090 11 | ENTRYPOINT [ "java", "-jar", "/opt/app/app.jar" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 masteringapi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mastering API Architecture Attendees 2 | 3 | GitHub: https://github.com/masteringapi/attendees 4 | 5 | Welcome to the primary repository accompanyting the book [Mastering API Architecture](https://www.oreilly.com/library/view/mastering-api-architecture/9781492090625/). 6 | We welcome all feedback as you are working through the book, if you would like to see anything additional please raise an issue. 7 | 8 | ## What is in this Repo? 9 | 10 | In this repo you will find Chapter 1's Attendees API, built in Java Spring. 11 | Using Apache Maven you can build the project or run the docker image (detail below) to experiment with 12 | 13 | * On port 8080 a HTTP service 14 | * On port 9090 a gRPC service 15 | 16 | ### Building and Running using Maven 17 | 18 | * In order to run in your IDE or via Maven, you need to generate the gRPC classes using `mvnw package` 19 | * `mvn spring-boot:run` 20 | 21 | ### Building using Docker 22 | 23 | The [Dockerfile](/Dockerfile) contains a multistage build, this will both compile the java code and create an image. 24 | You can simply run `docker build -t /attendees .` 25 | Once the build has completed run using `docker run -p 8080:8080 -p 9090:9090 /attendees` 26 | 27 | ### Running using DockerHub 28 | 29 | You can run the latest example directly from our [DockerHub](https://hub.docker.com/r/masteringapi/attendees) using: 30 | `docker run -p 8080:8080 -p 9090:9090 masteringapi/attendees`. 31 | 32 | ## Querying the Attendees Service 33 | 34 | ### REST 35 | 36 | You can find the OpenAPI Specification documented on the live instance: http://localhost:8080/swagger-ui.html. 37 | Clicking Try It Out will hit the endpoint. 38 | 39 | ### gRPC 40 | 41 | Using [gRPC UI](https://github.com/fullstorydev/grpcui) you can use the reflection service and invoke endpoints: 42 | 43 | ```bash 44 | grpcui --plaintext localhost:9090 45 | ``` 46 | 47 | ## Next steps 48 | 49 | * [Deploying with Kubernetes and Istio](/docs/k8s-istio.md) 50 | 51 | _More examples coming soon, please see the issues for the upcoming sections and raise an issue if something is missing that would be helpful for you._ -------------------------------------------------------------------------------- /docs/argo.md: -------------------------------------------------------------------------------- 1 | Follow instructions here: https://argoproj.github.io/argo-rollouts/installation/ 2 | 3 | kubectl create -f k8s-rollout-demo.yaml 4 | 5 | ```shell 6 | kubectl argo rollouts set image rollouts-demo attendees=jpgough/attendees:v2 7 | kubectl argo rollouts promote rollouts-demo 8 | 9 | #Terminate 10 | kubectl argo rollouts abort rollouts-demo 11 | ``` 12 | 13 | kubectl argo rollouts dashboard -------------------------------------------------------------------------------- /docs/k8s-istio.md: -------------------------------------------------------------------------------- 1 | # Deploying to Kubernetes and Istio 2 | 3 | In this tutorial we will deploy the attendees service to both Kubernetes and Istio. 4 | This will allow you to explore the deployment and figure out how APIs work in this environment. 5 | This is not intended to be a complete introduction to Kubernetes or Istio. 6 | 7 | ## Pre-reqs 8 | 9 | Required Tools: 10 | 11 | - [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) 12 | - [istioctl](https://istio.io/latest/docs/setup/getting-started/#download) 13 | 14 | Optional Tools: 15 | 16 | If you want to deploy an out-of-the-box setup with Azure you will need the following along with an Azure account: 17 | 18 | - [az](https://docs.microsoft.com/en-us/cli/azure/) 19 | 20 | ## Optional: Deploying a Kubernetes Cluster on Azure 21 | 22 | Running the following commands from the root of the project will set up a Kubernetes cluster with a single node in Azure. 23 | * The first command will create a resource group in UK South. 24 | * The second command will use the [templates](/operations/azure/template) to configure a Kubernetes cluster in the resource group. 25 | * The final step will merge the context of your application into `kubectl` allowing you to run commands using the CLI against the cluster. 26 | 27 | ``` 28 | az group create --name mastering-api --location "UK South" 29 | az deployment group create --name MasteringAPI --resource-group mastering-api --template-file operations/azure/template/template.json --parameters operations/azure/template/parameters.json 30 | 31 | #Wait for k8s cluster to create 32 | az aks get-credentials --resource-group mastering-api --name mastering-api 33 | ``` 34 | 35 | When you have finished with the demonstration, you can clean up everything with the following command: 36 | 37 | `az group delete --resource-group mastering-api` 38 | 39 | ## Step 1: Deploying Attendees to Kubernetes 40 | 41 | ### Deployment 42 | 43 | The [attendees-deployment.yaml](/operations/k8s/attendees-deployment.yaml) describes the [deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) of the attendees service. 44 | By default, this configuration will use the [image on DockerHub](https://hub.docker.com/repository/docker/masteringapi/attendees). 45 | Note that we have both ports 8080 and 9090 open for the REST and gRPC service respectively. 46 | 47 | `kubectl apply -f operations/k8s/attendees-deployment.yaml` 48 | 49 | We can validate that this has deployed successfully by running `kubectl get pods`, which should show something like: 50 | 51 | ``` 52 | NAME READY STATUS RESTARTS AGE 53 | attendees-api-7b4d4cb7cb-rhdss 1/1 Running 0 27s 54 | ``` 55 | 56 | Feel free to play around with updating the number of replicas or possibly killing the pod. 57 | Pods are ephemeral, meaning that we should use service to locate our deployments at runtime. 58 | 59 | ### Services 60 | 61 | A [Kubernetes Service](https://kubernetes.io/docs/concepts/services-networking/service/) allows you to specify discovery of pods at runtime and can be used directly for DNS routing in the cluster. 62 | It is also possible to set the type of the service to be `LoadBalancer`, creating an IP address external to the cluster. 63 | The two services in the examples are: 64 | 65 | - [HTTP Service](/operations/k8s/http-service.yaml) 66 | - [gRPC Service](/operations/k8s/grpc-service.yaml) 67 | 68 | ``` 69 | kubectl apply -f operations/k8s/grpc-service.yaml 70 | kubectl apply -f operations/k8s/http-service.yaml 71 | ``` 72 | 73 | You can find where the IP address of the services are located (note that external IP addresses can take a minute or two to allocate). 74 | `kubectl get svc` 75 | 76 | ``` 77 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 78 | attendees-grpc LoadBalancer 10.0.84.235 20.108.87.2 80:32632/TCP 13s 79 | attendees-http LoadBalancer 10.0.139.185 20.108.87.17 80:30105/TCP 3s 80 | ``` 81 | 82 | In the above example you can now query this API directly by hitting the HTTP URL from a browser, e.g. `http://20.108.87.17/swagger-ui/index.html` 83 | 84 | Using [gRPC UI](https://github.com/fullstorydev/grpcui) you can use the reflection service and invoke endpoints: 85 | 86 | ``` 87 | grpcui --plaintext 20.108.87.2:80 88 | ``` 89 | 90 | ### Deploy Istio 91 | 92 | ```shell 93 | kubectl create namespace istio-system 94 | istioctl install 95 | kubectl label namespace default istio-injection=enabled 96 | ``` 97 | 98 | Istio will now be installed to your cluster. 99 | If you rollout our deployment, you will see Istio start to take over some of the standard k8s control. 100 | 101 | ``` 102 | $kubectl get pods 103 | NAME READY STATUS RESTARTS AGE 104 | attendees-api-7b4d4cb7cb-rhdss 1/1 Running 0 18m 105 | 106 | $kubectl rollout restart deployment attendees-api 107 | deployment.apps/attendees-api restarted 108 | 109 | $kubectl get pods 110 | NAME READY STATUS RESTARTS AGE 111 | attendees-api-57df7445f6-mwxbk 2/2 Running 0 9s 112 | attendees-api-7b4d4cb7cb-rhdss 1/1 Terminating 0 18m 113 | ``` 114 | 115 | You can see that the new pod has 2/2 containers running, the new container running in the pod is the Istio sidecar. 116 | Let's make a few changes so that we can see Istio's traffic management impact, for simplicity we will just look at HTTP for now. 117 | 118 | ### Take over with Istio 119 | 120 | In order to take full advantage of Istio, our first job is to expose the attendees service on the Istio Ingress Gateway. 121 | You can do this by applying the [attendees-gateway.yaml](/operations/istio/attendees-gateway.yaml). 122 | 123 | `kubectl apply -f operations/istio/attendees-gateway.yaml` 124 | 125 | Once this has deployed you can execute the following commands to run a request against the /attendees endpoint. 126 | 127 | ``` 128 | export INGRESS_NAME=istio-ingressgateway 129 | export INGRESS_NS=istio-system 130 | export INGRESS_HOST=$(kubectl -n "$INGRESS_NS" get service "$INGRESS_NAME" -o jsonpath='{.status.loadBalancer.ingress[0].ip}') 131 | export INGRESS_PORT=$(kubectl -n "$INGRESS_NS" get service "$INGRESS_NAME" -o jsonpath='{.spec.ports[?(@.name=="http2")].port}') 132 | 133 | curl "http://$INGRESS_HOST:$INGRESS_PORT/attendees" 134 | ``` 135 | 136 | #### Fault Injection 137 | 138 | One useful feature of service mesh is the fine grain ability to control traffic. 139 | In this next demo we will introduce a 7s delay to all requests on the gateway. 140 | You can see the configuration in the [delayed-gateway.yaml](/operations/istio/delayed-gateway.yaml). 141 | 142 | `kubectl apply -f operations/istio/delayed-gateway.yaml` 143 | 144 | We can try another example where 50% (approximately) of requests will fail with a 500 status. 145 | You can see the configuration in the [half-broken.yaml](/operations/istio/half-broken-gateway.yaml). 146 | 147 | `kubectl apply -f operations/istio/half-broken-gateway.yaml` 148 | 149 | ### Carry on Exploring 150 | 151 | At this point you have a fully configured, setup cluster capable of running attendees on Kubernetes and Istio. 152 | You can try out some more of the mesh features and possibly try releasing a new version of the attendees service and routing between the two. 153 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.1.1 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "`uname`" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=`java-config --jre-home` 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && 89 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="`which javac`" 94 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=`which readlink` 97 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 98 | if $darwin ; then 99 | javaHome="`dirname \"$javaExecutable\"`" 100 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 101 | else 102 | javaExecutable="`readlink -f \"$javaExecutable\"`" 103 | fi 104 | javaHome="`dirname \"$javaExecutable\"`" 105 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="`\\unset -f command; \\command -v java`" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=`cd "$wdir/.."; pwd` 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir"; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | echo "$(tr -s '\n' ' ' < "$1")" 164 | fi 165 | } 166 | 167 | BASE_DIR=$(find_maven_basedir "$(dirname $0)") 168 | if [ -z "$BASE_DIR" ]; then 169 | exit 1; 170 | fi 171 | 172 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | echo $MAVEN_PROJECTBASEDIR 175 | fi 176 | 177 | ########################################################################################## 178 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 179 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 180 | ########################################################################################## 181 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 182 | if [ "$MVNW_VERBOSE" = true ]; then 183 | echo "Found .mvn/wrapper/maven-wrapper.jar" 184 | fi 185 | else 186 | if [ "$MVNW_VERBOSE" = true ]; then 187 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 188 | fi 189 | if [ -n "$MVNW_REPOURL" ]; then 190 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 191 | else 192 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 193 | fi 194 | while IFS="=" read key value; do 195 | case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; 196 | esac 197 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 198 | if [ "$MVNW_VERBOSE" = true ]; then 199 | echo "Downloading from: $wrapperUrl" 200 | fi 201 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 202 | if $cygwin; then 203 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 204 | fi 205 | 206 | if command -v wget > /dev/null; then 207 | QUIET="--quiet" 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found wget ... using wget" 210 | QUIET="" 211 | fi 212 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 213 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" 214 | else 215 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" 216 | fi 217 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 218 | elif command -v curl > /dev/null; then 219 | QUIET="--silent" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Found curl ... using curl" 222 | QUIET="" 223 | fi 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L 228 | fi 229 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 230 | else 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Falling back to using Java to download" 233 | fi 234 | javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 235 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" 236 | # For Cygwin, switch paths to Windows format before running javac 237 | if $cygwin; then 238 | javaSource=`cygpath --path --windows "$javaSource"` 239 | javaClass=`cygpath --path --windows "$javaClass"` 240 | fi 241 | if [ -e "$javaSource" ]; then 242 | if [ ! -e "$javaClass" ]; then 243 | if [ "$MVNW_VERBOSE" = true ]; then 244 | echo " - Compiling MavenWrapperDownloader.java ..." 245 | fi 246 | # Compiling the Java class 247 | ("$JAVA_HOME/bin/javac" "$javaSource") 248 | fi 249 | if [ -e "$javaClass" ]; then 250 | # Running the downloader 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo " - Running MavenWrapperDownloader.java ..." 253 | fi 254 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 255 | fi 256 | fi 257 | fi 258 | fi 259 | ########################################################################################## 260 | # End of extension 261 | ########################################################################################## 262 | 263 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 264 | 265 | # For Cygwin, switch paths to Windows format before running java 266 | if $cygwin; then 267 | [ -n "$JAVA_HOME" ] && 268 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 269 | [ -n "$CLASSPATH" ] && 270 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 271 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 272 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 273 | fi 274 | 275 | # Provide a "standardized" way to retrieve the CLI args that will 276 | # work with both Windows and non-Windows executions. 277 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 278 | export MAVEN_CMD_LINE_ARGS 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | $MAVEN_DEBUG_OPTS \ 285 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 286 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 287 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 288 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /operations/argo/k8s-bluegreen-rollout-demo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Rollout 3 | metadata: 4 | name: bluegreen-demo 5 | spec: 6 | replicas: 2 7 | revisionHistoryLimit: 2 8 | selector: 9 | matchLabels: 10 | app: attendees-api 11 | template: 12 | metadata: 13 | labels: 14 | app: attendees-api 15 | spec: 16 | containers: 17 | - name: attendees 18 | image: jpgough/attendees:v1 19 | ports: 20 | - name: http 21 | containerPort: 8080 22 | protocol: TCP 23 | resources: 24 | requests: 25 | memory: 32Mi 26 | cpu: 5m 27 | strategy: 28 | blueGreen: 29 | activeService: rollout-bluegreen-active 30 | previewService: rollout-bluegreen-preview 31 | autoPromotionEnabled: false -------------------------------------------------------------------------------- /operations/argo/k8s-canary-rollout-demo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Rollout 3 | metadata: 4 | name: rollouts-demo 5 | spec: 6 | replicas: 5 7 | strategy: 8 | canary: 9 | steps: 10 | - setWeight: 20 11 | - pause: {} 12 | - setWeight: 40 13 | - pause: {duration: 10} 14 | - setWeight: 60 15 | - pause: {duration: 10} 16 | - setWeight: 80 17 | - pause: {duration: 10} 18 | revisionHistoryLimit: 2 19 | selector: 20 | matchLabels: 21 | app: attendees-api 22 | template: 23 | metadata: 24 | labels: 25 | app: attendees-api 26 | spec: 27 | containers: 28 | - name: attendees 29 | image: jpgough/attendees:v1 30 | ports: 31 | - name: http 32 | containerPort: 8080 33 | protocol: TCP 34 | resources: 35 | requests: 36 | memory: 32Mi 37 | cpu: 5m -------------------------------------------------------------------------------- /operations/azure/template/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourceName": { 6 | "value": "mastering-api" 7 | }, 8 | "location": { 9 | "value": "uksouth" 10 | }, 11 | "dnsPrefix": { 12 | "value": "mastering-api-dns" 13 | }, 14 | "kubernetesVersion": { 15 | "value": "1.25.11" 16 | }, 17 | "networkPlugin": { 18 | "value": "kubenet" 19 | }, 20 | "enableRBAC": { 21 | "value": true 22 | }, 23 | "enablePrivateCluster": { 24 | "value": false 25 | }, 26 | "enableHttpApplicationRouting": { 27 | "value": false 28 | }, 29 | "enableAzurePolicy": { 30 | "value": false 31 | }, 32 | "enableSecretStoreCSIDriver": { 33 | "value": false 34 | }, 35 | "vmssNodePool": { 36 | "value": true 37 | }, 38 | "networkPolicy": { 39 | "value": "calico" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /operations/azure/template/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "resourceName": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "The name of the Managed Cluster resource." 9 | } 10 | }, 11 | "location": { 12 | "type": "string", 13 | "metadata": { 14 | "description": "The location of AKS resource." 15 | } 16 | }, 17 | "dnsPrefix": { 18 | "type": "string", 19 | "metadata": { 20 | "description": "Optional DNS prefix to use with hosted Kubernetes API server FQDN." 21 | } 22 | }, 23 | "osDiskSizeGB": { 24 | "type": "int", 25 | "defaultValue": 0, 26 | "metadata": { 27 | "description": "Disk size (in GiB) to provision for each of the agent pool nodes. This value ranges from 0 to 1023. Specifying 0 will apply the default disk size for that agentVMSize." 28 | }, 29 | "minValue": 0, 30 | "maxValue": 1023 31 | }, 32 | "kubernetesVersion": { 33 | "type": "string", 34 | "defaultValue": "1.7.7", 35 | "metadata": { 36 | "description": "The version of Kubernetes." 37 | } 38 | }, 39 | "networkPlugin": { 40 | "type": "string", 41 | "allowedValues": [ 42 | "azure", 43 | "kubenet" 44 | ], 45 | "metadata": { 46 | "description": "Network plugin used for building Kubernetes network." 47 | } 48 | }, 49 | "enableRBAC": { 50 | "type": "bool", 51 | "defaultValue": true, 52 | "metadata": { 53 | "description": "Boolean flag to turn on and off of RBAC." 54 | } 55 | }, 56 | "vmssNodePool": { 57 | "type": "bool", 58 | "defaultValue": false, 59 | "metadata": { 60 | "description": "Boolean flag to turn on and off of virtual machine scale sets" 61 | } 62 | }, 63 | "windowsProfile": { 64 | "type": "bool", 65 | "defaultValue": false, 66 | "metadata": { 67 | "description": "Boolean flag to turn on and off of virtual machine scale sets" 68 | } 69 | }, 70 | "enablePrivateCluster": { 71 | "type": "bool", 72 | "defaultValue": false, 73 | "metadata": { 74 | "description": "Enable private network access to the Kubernetes cluster." 75 | } 76 | }, 77 | "enableHttpApplicationRouting": { 78 | "type": "bool", 79 | "defaultValue": true, 80 | "metadata": { 81 | "description": "Boolean flag to turn on and off http application routing." 82 | } 83 | }, 84 | "enableAzurePolicy": { 85 | "type": "bool", 86 | "defaultValue": false, 87 | "metadata": { 88 | "description": "Boolean flag to turn on and off Azure Policy addon." 89 | } 90 | }, 91 | "enableSecretStoreCSIDriver": { 92 | "type": "bool", 93 | "defaultValue": false, 94 | "metadata": { 95 | "description": "Boolean flag to turn on and off secret store CSI driver." 96 | } 97 | }, 98 | "networkPolicy": { 99 | "type": "string", 100 | "metadata": { 101 | "description": "Network policy used for building Kubernetes network." 102 | } 103 | } 104 | }, 105 | "resources": [ 106 | { 107 | "apiVersion": "2021-07-01", 108 | "dependsOn": [], 109 | "type": "Microsoft.ContainerService/managedClusters", 110 | "location": "[parameters('location')]", 111 | "name": "[parameters('resourceName')]", 112 | "properties": { 113 | "kubernetesVersion": "[parameters('kubernetesVersion')]", 114 | "enableRBAC": "[parameters('enableRBAC')]", 115 | "dnsPrefix": "[parameters('dnsPrefix')]", 116 | "agentPoolProfiles": [ 117 | { 118 | "name": "agentpool", 119 | "osDiskSizeGB": "[parameters('osDiskSizeGB')]", 120 | "count": 2, 121 | "enableAutoScaling": false, 122 | "vmSize": "Standard_D2as_v4", 123 | "osType": "Linux", 124 | "storageProfile": "ManagedDisks", 125 | "type": "VirtualMachineScaleSets", 126 | "mode": "System", 127 | "maxPods": 110, 128 | "availabilityZones": [], 129 | "enableNodePublicIP": false, 130 | "tags": {} 131 | } 132 | ], 133 | "networkProfile": { 134 | "loadBalancerSku": "standard", 135 | "networkPlugin": "[parameters('networkPlugin')]", 136 | "networkPolicy": "[parameters('networkPolicy')]" 137 | }, 138 | "apiServerAccessProfile": { 139 | "enablePrivateCluster": "[parameters('enablePrivateCluster')]" 140 | }, 141 | "addonProfiles": { 142 | "httpApplicationRouting": { 143 | "enabled": "[parameters('enableHttpApplicationRouting')]" 144 | }, 145 | "azurepolicy": { 146 | "enabled": "[parameters('enableAzurePolicy')]" 147 | }, 148 | "azureKeyvaultSecretsProvider": { 149 | "enabled": "[parameters('enableSecretStoreCSIDriver')]" 150 | } 151 | } 152 | }, 153 | "tags": {}, 154 | "sku": { 155 | "name": "Basic", 156 | "tier": "Free" 157 | }, 158 | "identity": { 159 | "type": "SystemAssigned" 160 | } 161 | } 162 | ], 163 | "outputs": { 164 | "controlPlaneFQDN": { 165 | "type": "string", 166 | "value": "[reference(concat('Microsoft.ContainerService/managedClusters/', parameters('resourceName'))).fqdn]" 167 | } 168 | } 169 | } -------------------------------------------------------------------------------- /operations/istio/attendees-gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: Gateway 3 | metadata: 4 | name: attendees-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway # use Istio default gateway implementation 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" 15 | --- 16 | apiVersion: networking.istio.io/v1alpha3 17 | kind: VirtualService 18 | metadata: 19 | name: attendees 20 | spec: 21 | hosts: 22 | - "*" 23 | gateways: 24 | - attendees-gateway 25 | http: 26 | - match: 27 | - uri: 28 | prefix: /attendees 29 | route: 30 | - destination: 31 | port: 32 | number: 80 33 | host: attendees-http -------------------------------------------------------------------------------- /operations/istio/delayed-gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: Gateway 3 | metadata: 4 | name: attendees-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway # use Istio default gateway implementation 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" 15 | --- 16 | apiVersion: networking.istio.io/v1alpha3 17 | kind: VirtualService 18 | metadata: 19 | name: attendees 20 | spec: 21 | hosts: 22 | - "*" 23 | gateways: 24 | - attendees-gateway 25 | http: 26 | - fault: 27 | delay: 28 | fixedDelay: 7s 29 | percentage: 30 | value: 100 31 | match: 32 | - uri: 33 | prefix: /attendees 34 | route: 35 | - destination: 36 | port: 37 | number: 80 38 | host: attendees-http -------------------------------------------------------------------------------- /operations/istio/half-broken-gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: Gateway 3 | metadata: 4 | name: attendees-gateway 5 | spec: 6 | selector: 7 | istio: ingressgateway # use Istio default gateway implementation 8 | servers: 9 | - port: 10 | number: 80 11 | name: http 12 | protocol: HTTP 13 | hosts: 14 | - "*" 15 | --- 16 | apiVersion: networking.istio.io/v1alpha3 17 | kind: VirtualService 18 | metadata: 19 | name: attendees 20 | spec: 21 | hosts: 22 | - "*" 23 | gateways: 24 | - attendees-gateway 25 | http: 26 | - fault: 27 | abort: 28 | httpStatus: 500 29 | percentage: 30 | value: 50 31 | match: 32 | - uri: 33 | prefix: /attendees 34 | route: 35 | - destination: 36 | port: 37 | number: 80 38 | host: attendees-http -------------------------------------------------------------------------------- /operations/k8s/attendees-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: attendees-api 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: attendees-api 10 | template: 11 | metadata: 12 | labels: 13 | app: attendees-api 14 | spec: 15 | containers: 16 | - name: attendees 17 | image: masteringapi/attendees 18 | ports: 19 | - containerPort: 8080 20 | - containerPort: 9090 -------------------------------------------------------------------------------- /operations/k8s/grpc-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: attendees-grpc 5 | spec: 6 | type: LoadBalancer 7 | selector: 8 | app: attendees-api 9 | ports: 10 | - protocol: TCP 11 | port: 80 12 | targetPort: 9090 -------------------------------------------------------------------------------- /operations/k8s/http-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: attendees-http 5 | spec: 6 | type: LoadBalancer 7 | selector: 8 | app: attendees-api 9 | ports: 10 | - protocol: TCP 11 | port: 80 12 | targetPort: 8080 13 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.7.6 9 | 10 | 11 | com.masteringapi 12 | attendees 13 | 0.0.1-SNAPSHOT 14 | attendees 15 | Attendees Service for Mastering API 16 | 17 | 18 | 17 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-test 30 | test 31 | 32 | 33 | org.junit.vintage 34 | junit-vintage-engine 35 | 36 | 37 | 38 | 39 | org.springdoc 40 | springdoc-openapi-ui 41 | 1.7.0 42 | 43 | 44 | net.devh 45 | grpc-server-spring-boot-starter 46 | 2.14.0.RELEASE 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-actuator 51 | 52 | 53 | jakarta.validation 54 | jakarta.validation-api 55 | 2.0.2 56 | 57 | 58 | javax.annotation 59 | javax.annotation-api 60 | 1.3.2 61 | 62 | 63 | io.grpc 64 | grpc-testing 65 | 1.56.1 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | kr.motd.maven 75 | os-maven-plugin 76 | 1.7.1 77 | 78 | 79 | 80 | 81 | org.springframework.boot 82 | spring-boot-maven-plugin 83 | 84 | 85 | org.xolstice.maven.plugins 86 | protobuf-maven-plugin 87 | 0.6.1 88 | 89 | 90 | com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier} 91 | 92 | grpc-java 93 | 94 | io.grpc:protoc-gen-grpc-java:1.36.0:exe:${os.detected.classifier} 95 | 96 | 97 | 98 | 99 | 100 | compile 101 | compile-custom 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/main/java/com/masteringapi/attendees/AttendeesApplication.java: -------------------------------------------------------------------------------- 1 | package com.masteringapi.attendees; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | 7 | @SpringBootApplication 8 | public class AttendeesApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication.run(AttendeesApplication.class, args); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/masteringapi/attendees/controller/AttendeesController.java: -------------------------------------------------------------------------------- 1 | package com.masteringapi.attendees.controller; 2 | 3 | import com.masteringapi.attendees.model.Attendee; 4 | import com.masteringapi.attendees.model.AttendeeNotFoundException; 5 | import com.masteringapi.attendees.model.AttendeeResponse; 6 | import com.masteringapi.attendees.service.AttendeeStore; 7 | import io.swagger.v3.oas.annotations.Operation; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.validation.annotation.Validated; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import java.net.URI; 14 | import java.util.ArrayList; 15 | 16 | @RestController 17 | public class AttendeesController { 18 | private AttendeeStore store; 19 | 20 | public AttendeesController(@Autowired AttendeeStore store) { 21 | this.store = store; 22 | } 23 | 24 | @GetMapping("/attendees") 25 | @ResponseBody 26 | @Operation(summary = "Retrieve a list of all attendees registered in the system") 27 | public AttendeeResponse getAttendees() { 28 | return new AttendeeResponse(new ArrayList<>(store.getAttendees())); 29 | } 30 | 31 | @GetMapping("/attendees/{id}") 32 | @ResponseBody 33 | @Operation(summary = "Retrieve a specific attendee registered in the system") 34 | public ResponseEntity getAttendee(@PathVariable(value = "id") Integer id) { 35 | try { 36 | return ResponseEntity.ok(this.store.getAttendee(id)); 37 | } catch(AttendeeNotFoundException ex) { 38 | return ResponseEntity.notFound().build(); 39 | } 40 | } 41 | 42 | @PostMapping("attendees") 43 | @ResponseBody 44 | @Operation(summary = "Create a new attendee") 45 | public ResponseEntity addAttendee(@Validated @RequestBody Attendee attendee) { 46 | int id = this.store.addAttendee(attendee); 47 | URI uri = URI.create("/attendees/" + id); 48 | return ResponseEntity.created(uri).build(); 49 | } 50 | 51 | @DeleteMapping("/attendees/{id}") 52 | @ResponseBody 53 | @Operation(summary = "Delete a specific attendee") 54 | public ResponseEntity deleteAttendee(@PathVariable(value = "id") Integer id) { 55 | try { 56 | this.store.removeAttendee(id); 57 | return ResponseEntity.ok().build(); 58 | } catch(AttendeeNotFoundException ex) { 59 | return ResponseEntity.notFound().build(); 60 | } 61 | } 62 | 63 | @PutMapping("attendees/{id}") 64 | @ResponseBody 65 | @Operation(summary = "Update an attendees details registered in the system") 66 | public ResponseEntity updateAttendee(@PathVariable(value = "id") Integer id, @RequestBody Attendee attendee) { 67 | try { 68 | this.store.updateAttendee(id, attendee); 69 | return ResponseEntity.noContent().build(); 70 | } catch(AttendeeNotFoundException ex) { 71 | return ResponseEntity.notFound().build(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/masteringapi/attendees/grpc/AttendeesServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.masteringapi.attendees.grpc; 2 | 3 | import com.google.rpc.Code; 4 | import com.google.rpc.Status; 5 | import com.masteringapi.attendees.grpc.server.*; 6 | import com.masteringapi.attendees.model.AttendeeNotFoundException; 7 | import com.masteringapi.attendees.service.AttendeeStore; 8 | import io.grpc.protobuf.StatusProto; 9 | import io.grpc.stub.StreamObserver; 10 | import net.devh.boot.grpc.server.service.GrpcService; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | 15 | @GrpcService 16 | public class AttendeesServiceImpl extends AttendeesServiceGrpc.AttendeesServiceImplBase { 17 | 18 | private final Logger logger = LoggerFactory.getLogger(AttendeesServiceImpl.class); 19 | 20 | private final AttendeeStore store; 21 | 22 | public AttendeesServiceImpl(@Autowired AttendeeStore store) { 23 | this.store = store; 24 | } 25 | 26 | @Override 27 | public void getAttendees(GetAttendeesRequest request, StreamObserver responseObserver) { 28 | GetAttendeesResponse.Builder responseBuilder = com.masteringapi.attendees.grpc.server.GetAttendeesResponse.newBuilder(); 29 | 30 | for(com.masteringapi.attendees.model.Attendee attendee: store.getAttendees()) { 31 | Attendee grpcAttendee = Attendee.newBuilder() 32 | .setId(attendee.getId()) 33 | .setGivenName(attendee.getGivenName()) 34 | .setSurname(attendee.getSurname()) 35 | .setEmail(attendee.getEmail()) 36 | .build(); 37 | responseBuilder.addAttendees(grpcAttendee); 38 | } 39 | 40 | responseObserver.onNext(responseBuilder.build()); 41 | responseObserver.onCompleted(); 42 | } 43 | 44 | @Override 45 | public void createAttendee(CreateAttendeeRequest request, StreamObserver responseObserver) { 46 | CreateAttendeeResponse.Builder responseBuilder = CreateAttendeeResponse.newBuilder(); 47 | 48 | int id = this.store.addAttendee(new com.masteringapi.attendees.model.Attendee(request.getAttendee())); 49 | 50 | Attendee attendee = Attendee.newBuilder().mergeFrom(request.getAttendee()) 51 | .setId(id) 52 | .build(); 53 | responseBuilder.setAttendee(attendee); 54 | responseObserver.onNext(responseBuilder.build()); 55 | responseObserver.onCompleted(); 56 | } 57 | 58 | @Override 59 | public void getAttendee(GetAttendeeRequest request, StreamObserver responseObserver) { 60 | GetAttendeeResponse.Builder responseBuilder = GetAttendeeResponse.newBuilder(); 61 | 62 | try { 63 | com.masteringapi.attendees.model.Attendee attendee = this.store.getAttendee(request.getId()); 64 | Attendee grpcAttendee = Attendee.newBuilder() 65 | .setId(attendee.getId()) 66 | .setGivenName(attendee.getGivenName()) 67 | .setSurname(attendee.getSurname()) 68 | .setEmail(attendee.getEmail()) 69 | .build(); 70 | responseBuilder.setAttendee(grpcAttendee); 71 | responseObserver.onNext(responseBuilder.build()); 72 | responseObserver.onCompleted(); 73 | } catch (AttendeeNotFoundException e) { 74 | Status status = Status.newBuilder().setCode(Code.NOT_FOUND.getNumber()) 75 | .setMessage("Attendee Not Found") 76 | .build(); 77 | responseObserver.onError(StatusProto.toStatusRuntimeException(status)); 78 | logger.error("Could not find attendee", e); 79 | } 80 | } 81 | 82 | @Override 83 | public void deleteAttendee(DeleteAttendeeRequest request, StreamObserver responseObserver) { 84 | DeleteAttendeeResponse.Builder responseBuilder = DeleteAttendeeResponse.newBuilder(); 85 | 86 | try { 87 | this.store.removeAttendee(request.getId()); 88 | responseObserver.onNext(responseBuilder.build()); 89 | responseObserver.onCompleted(); 90 | } catch (AttendeeNotFoundException e) { 91 | Status status = Status.newBuilder().setCode(Code.NOT_FOUND.getNumber()) 92 | .setMessage("Attendee Not Found") 93 | .build(); 94 | responseObserver.onError(StatusProto.toStatusRuntimeException(status)); 95 | logger.error("Could not find attendee to delete", e); 96 | } 97 | } 98 | 99 | @Override 100 | public void updateAttendee(UpdateAttendeeRequest request, StreamObserver responseObserver) { 101 | UpdateAttendeeResponse.Builder responseBuilder = UpdateAttendeeResponse.newBuilder(); 102 | 103 | try { 104 | this.store.updateAttendee(request.getAttendee().getId(), 105 | new com.masteringapi.attendees.model.Attendee(request.getAttendee())); 106 | 107 | responseBuilder.setAttendee(request.getAttendee()); 108 | responseObserver.onNext(responseBuilder.build()); 109 | responseObserver.onCompleted(); 110 | } catch (AttendeeNotFoundException e) { 111 | Status status = Status.newBuilder().setCode(Code.NOT_FOUND.getNumber()) 112 | .setMessage("Attendee Not Found") 113 | .build(); 114 | responseObserver.onError(StatusProto.toStatusRuntimeException(status)); 115 | logger.error("Unable to update attendee", e); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/masteringapi/attendees/model/Attendee.java: -------------------------------------------------------------------------------- 1 | package com.masteringapi.attendees.model; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import javax.validation.constraints.Size; 5 | import java.util.Objects; 6 | 7 | public class Attendee { 8 | private Integer id; 9 | 10 | public Attendee() { 11 | 12 | } 13 | 14 | public Attendee(com.masteringapi.attendees.grpc.server.Attendee attendee) { 15 | this.surname = attendee.getSurname(); 16 | this.givenName = attendee.getGivenName(); 17 | this.email = attendee.getEmail(); 18 | } 19 | 20 | @NotNull 21 | @Size(max=35) 22 | private String givenName; 23 | 24 | @NotNull 25 | @Size(max=35) 26 | private String surname; 27 | 28 | @NotNull 29 | @Size(max=254) 30 | private String email; 31 | 32 | public Integer getId() { 33 | return id; 34 | } 35 | 36 | public void setId(Integer id) { 37 | this.id = id; 38 | } 39 | 40 | public String getGivenName() { 41 | return givenName; 42 | } 43 | 44 | public void setGivenName(String givenName) { 45 | this.givenName = givenName; 46 | } 47 | 48 | public String getSurname() { 49 | return surname; 50 | } 51 | 52 | public void setSurname(String surname) { 53 | this.surname = surname; 54 | } 55 | 56 | public String getEmail() { 57 | return email; 58 | } 59 | 60 | public void setEmail(String email) { 61 | this.email = email; 62 | } 63 | 64 | @Override 65 | public boolean equals(Object o) { 66 | if (this == o) return true; 67 | if (o == null || getClass() != o.getClass()) return false; 68 | Attendee attendee = (Attendee) o; 69 | return Objects.equals(id, attendee.id); 70 | } 71 | 72 | @Override 73 | public int hashCode() { 74 | return Objects.hash(id); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/masteringapi/attendees/model/AttendeeNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.masteringapi.attendees.model; 2 | 3 | public class AttendeeNotFoundException extends Exception { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/masteringapi/attendees/model/AttendeeResponse.java: -------------------------------------------------------------------------------- 1 | package com.masteringapi.attendees.model; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * The AttendeeResponse is a wrapper response allowing the API to conform to the Microsoft API Guidelines. 7 | * This allows for forward compatibility if later we introduce pagination/filtering to the service. 8 | */ 9 | public class AttendeeResponse { 10 | 11 | private List value; 12 | 13 | public AttendeeResponse(List attendees) { 14 | this.value = attendees; 15 | } 16 | 17 | //Used by the marshaller 18 | private AttendeeResponse() {} 19 | 20 | public List getValue() { 21 | return this.value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/masteringapi/attendees/service/AttendeeStore.java: -------------------------------------------------------------------------------- 1 | package com.masteringapi.attendees.service; 2 | 3 | import com.masteringapi.attendees.model.Attendee; 4 | import com.masteringapi.attendees.model.AttendeeNotFoundException; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.*; 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | @Component 11 | public class AttendeeStore { 12 | private final Map attendees = new LinkedHashMap<>(); 13 | 14 | private final AtomicInteger counter = new AtomicInteger(0); 15 | 16 | public AttendeeStore() { 17 | //Setup some mock data 18 | Attendee attendee = new Attendee(); 19 | attendee.setId(counter.incrementAndGet()); 20 | attendee.setGivenName("Jim"); 21 | attendee.setSurname("Gough"); 22 | attendee.setEmail("gough@mail.com"); 23 | this.attendees.put(1, attendee); 24 | attendee = new Attendee(); 25 | attendee.setId(counter.incrementAndGet()); 26 | attendee.setGivenName("Matt"); 27 | attendee.setSurname("Auburn"); 28 | attendee.setEmail("auburn@mail.com"); 29 | this.attendees.put(2, attendee); 30 | attendee = new Attendee(); 31 | attendee.setId(counter.incrementAndGet()); 32 | attendee.setGivenName("Daniel"); 33 | attendee.setSurname("Bryant"); 34 | attendee.setEmail("bryant@mail.com"); 35 | this.attendees.put(3, attendee); 36 | } 37 | 38 | public Collection getAttendees() { 39 | return this.attendees.values(); 40 | } 41 | 42 | public Attendee getAttendee(Integer id) throws AttendeeNotFoundException { 43 | if(this.attendees.containsKey(id)) { 44 | return this.attendees.get(id); 45 | } else { 46 | throw new AttendeeNotFoundException(); 47 | } 48 | } 49 | 50 | public int addAttendee(Attendee attendee) { 51 | int id = counter.incrementAndGet(); 52 | attendee.setId(id); 53 | this.attendees.put(id, attendee); 54 | return this.attendees.size(); 55 | } 56 | 57 | public void removeAttendee(Integer id) throws AttendeeNotFoundException { 58 | if(this.attendees.containsKey(id)) { 59 | this.attendees.remove(id); 60 | } else { 61 | throw new AttendeeNotFoundException(); 62 | } 63 | } 64 | 65 | public void updateAttendee(Integer id, Attendee attendee) throws AttendeeNotFoundException { 66 | if(this.attendees.containsKey(id)) { 67 | attendee.setId(id); 68 | this.attendees.put(id, attendee); 69 | } else { 70 | throw new AttendeeNotFoundException(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/proto/attendees.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option java_multiple_files = true; 3 | package com.masteringapi.attendees.grpc.server; 4 | 5 | message GetAttendeesRequest { 6 | } 7 | 8 | message Attendee { 9 | int32 id = 1; 10 | string givenName = 2; 11 | string surname = 3; 12 | string email = 4; 13 | 14 | } 15 | 16 | message GetAttendeesResponse { 17 | repeated Attendee attendees = 1; 18 | } 19 | 20 | message CreateAttendeeRequest { 21 | Attendee attendee = 1; 22 | } 23 | 24 | message CreateAttendeeResponse { 25 | Attendee attendee = 1; 26 | } 27 | 28 | message GetAttendeeRequest { 29 | int32 id = 1; 30 | } 31 | message GetAttendeeResponse { 32 | Attendee attendee = 1; 33 | } 34 | 35 | message DeleteAttendeeRequest { 36 | int32 id = 1; 37 | } 38 | 39 | message DeleteAttendeeResponse { 40 | 41 | } 42 | 43 | message UpdateAttendeeRequest { 44 | Attendee attendee = 1; 45 | } 46 | 47 | message UpdateAttendeeResponse { 48 | Attendee attendee = 1; 49 | } 50 | 51 | service AttendeesService { 52 | rpc getAttendees(GetAttendeesRequest) returns (GetAttendeesResponse) {} 53 | rpc createAttendee(CreateAttendeeRequest) returns (CreateAttendeeResponse) {} 54 | rpc getAttendee(GetAttendeeRequest) returns (GetAttendeeResponse) {} 55 | rpc deleteAttendee(DeleteAttendeeRequest) returns (DeleteAttendeeResponse) {} 56 | rpc updateAttendee(UpdateAttendeeRequest) returns (UpdateAttendeeResponse) {} 57 | } 58 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/test/java/com/masteringapi/attendees/TestAttendeesApplicationShould.java: -------------------------------------------------------------------------------- 1 | package com.masteringapi.attendees; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class TestAttendeesApplicationShould { 8 | 9 | @Test 10 | void load_spring_context() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/masteringapi/attendees/grpc/TestAttendeesServiceImplShould.java: -------------------------------------------------------------------------------- 1 | package com.masteringapi.attendees.grpc; 2 | 3 | import com.masteringapi.attendees.grpc.server.*; 4 | import com.masteringapi.attendees.model.Attendee; 5 | import com.masteringapi.attendees.model.AttendeeNotFoundException; 6 | import com.masteringapi.attendees.service.AttendeeStore; 7 | import io.grpc.internal.testing.StreamRecorder; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.ArgumentMatchers; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import static org.hamcrest.MatcherAssert.assertThat; 19 | import static org.hamcrest.Matchers.*; 20 | import static org.mockito.ArgumentMatchers.anyInt; 21 | import static org.mockito.Mockito.*; 22 | 23 | @ExtendWith(MockitoExtension.class) 24 | public class TestAttendeesServiceImplShould { 25 | 26 | @Mock 27 | private AttendeeStore store; 28 | 29 | @InjectMocks 30 | private AttendeesServiceImpl attendeesService; 31 | 32 | @Test 33 | void return_empty_list_when_no_attendees_in_store() { 34 | when(store.getAttendees()).thenReturn(new ArrayList<>()); 35 | GetAttendeesRequest request = GetAttendeesRequest.newBuilder().build(); 36 | StreamRecorder responseObserver = StreamRecorder.create(); 37 | 38 | this.attendeesService.getAttendees(request, responseObserver); 39 | assertThat(responseObserver.getError(), equalTo(null)); 40 | List responses = responseObserver.getValues(); 41 | assertThat(responses.size(), equalTo(1)); 42 | assertThat(responses.get(0).getAttendeesList().size(), equalTo(0)); 43 | } 44 | 45 | @Test 46 | void return_attendee_when_in_store() { 47 | List attendees = new ArrayList<>(); 48 | attendees.add(testAttendee()); 49 | GetAttendeesRequest request = GetAttendeesRequest.newBuilder().build(); 50 | when(store.getAttendees()).thenReturn(attendees); 51 | StreamRecorder responseObserver = StreamRecorder.create(); 52 | 53 | this.attendeesService.getAttendees(request, responseObserver); 54 | assertThat(responseObserver.getError(), equalTo(null)); 55 | List responses = responseObserver.getValues(); 56 | assertThat(responses.size(), equalTo(1)); 57 | assertThat(responses.get(0).getAttendeesList().size(), equalTo(1)); 58 | assertThat(responses.get(0).getAttendeesList().get(0).getEmail(), equalTo("jim@gough")); 59 | } 60 | 61 | @Test 62 | void throw_an_error_when_attendee_does_not_exist() throws AttendeeNotFoundException { 63 | GetAttendeeRequest request = GetAttendeeRequest.newBuilder().setId(1).build(); 64 | when(store.getAttendee(anyInt())).thenThrow(new AttendeeNotFoundException()); 65 | StreamRecorder responseObserver = StreamRecorder.create(); 66 | 67 | this.attendeesService.getAttendee(request, responseObserver); 68 | assertThat(responseObserver.getError(), is(notNullValue())); 69 | assertThat(responseObserver.getError().getMessage(), is("NOT_FOUND: Attendee Not Found")); 70 | } 71 | 72 | @Test 73 | void return_attendee_for_given_id() throws AttendeeNotFoundException { 74 | GetAttendeeRequest request = GetAttendeeRequest.newBuilder().setId(1).build(); 75 | when(store.getAttendee(anyInt())).thenReturn(testAttendee()); 76 | StreamRecorder responseObserver = StreamRecorder.create(); 77 | 78 | this.attendeesService.getAttendee(request, responseObserver); 79 | assertThat(responseObserver.getError(), equalTo(null)); 80 | List responses = responseObserver.getValues(); 81 | assertThat(responses.get(0).getAttendee().getEmail(), equalTo("jim@gough")); 82 | assertThat(responses.get(0).getAttendee().getId(), equalTo(1)); 83 | } 84 | 85 | @Test 86 | void create_an_attendee() { 87 | CreateAttendeeRequest createAttendeeRequest = CreateAttendeeRequest.newBuilder() 88 | .setAttendee(testGrpcAttendee()).build(); 89 | StreamRecorder responseObserver = StreamRecorder.create(); 90 | 91 | this.attendeesService.createAttendee(createAttendeeRequest, responseObserver); 92 | assertThat(responseObserver.getError(), equalTo(null)); 93 | List responses = responseObserver.getValues(); 94 | assertThat(responses.get(0).getAttendee().getEmail(), equalTo("jim@gough")); 95 | verify(store).addAttendee(ArgumentMatchers.isA(Attendee.class)); 96 | } 97 | 98 | @Test 99 | void error_when_deleting_a_non_existing_attendee() throws AttendeeNotFoundException { 100 | DeleteAttendeeRequest deleteAttendeeRequest = DeleteAttendeeRequest.newBuilder().setId(1).build(); 101 | StreamRecorder responseObserver = StreamRecorder.create(); 102 | doThrow(new AttendeeNotFoundException()).when(store).removeAttendee(isA(Integer.class)); 103 | 104 | this.attendeesService.deleteAttendee(deleteAttendeeRequest, responseObserver); 105 | assertThat(responseObserver.getError(), is(notNullValue())); 106 | assertThat(responseObserver.getError().getMessage(), is("NOT_FOUND: Attendee Not Found")); 107 | } 108 | 109 | @Test 110 | void delete_a_known_attendee() throws AttendeeNotFoundException { 111 | DeleteAttendeeRequest deleteAttendeeRequest = DeleteAttendeeRequest.newBuilder().setId(1).build(); 112 | StreamRecorder responseObserver = StreamRecorder.create(); 113 | 114 | this.attendeesService.deleteAttendee(deleteAttendeeRequest, responseObserver); 115 | assertThat(responseObserver.getError(), equalTo(null)); 116 | verify(store).removeAttendee(ArgumentMatchers.isA(Integer.class)); 117 | } 118 | 119 | @Test 120 | void error_when_updating_a_missing_attendee() throws AttendeeNotFoundException { 121 | UpdateAttendeeRequest updateAttendeeRequest = UpdateAttendeeRequest.newBuilder().setAttendee(testGrpcAttendee()).build(); 122 | StreamRecorder responseObserver = StreamRecorder.create(); 123 | doThrow(new AttendeeNotFoundException()).when(store).updateAttendee( isA(Integer.class), isA(Attendee.class)); 124 | 125 | this.attendeesService.updateAttendee(updateAttendeeRequest, responseObserver); 126 | assertThat(responseObserver.getError(), is(notNullValue())); 127 | assertThat(responseObserver.getError().getMessage(), is("NOT_FOUND: Attendee Not Found")); 128 | } 129 | 130 | @Test 131 | void update_a_given_attendee() throws AttendeeNotFoundException { 132 | UpdateAttendeeRequest updateAttendeeRequest = UpdateAttendeeRequest.newBuilder().setAttendee(testGrpcAttendee()).build(); 133 | StreamRecorder responseObserver = StreamRecorder.create(); 134 | 135 | this.attendeesService.updateAttendee(updateAttendeeRequest, responseObserver); 136 | assertThat(responseObserver.getError(), equalTo(null)); 137 | verify(store).updateAttendee(ArgumentMatchers.isA(Integer.class), ArgumentMatchers.isA(Attendee.class)); 138 | } 139 | 140 | private com.masteringapi.attendees.grpc.server.Attendee testGrpcAttendee() { 141 | return com.masteringapi.attendees.grpc.server.Attendee.newBuilder() 142 | .setId(1) 143 | .setEmail("jim@gough") 144 | .setSurname("Gough") 145 | .setGivenName("Jim") 146 | .build(); 147 | } 148 | 149 | private Attendee testAttendee() { 150 | Attendee attendee = new Attendee(); 151 | attendee.setSurname("Gough"); 152 | attendee.setId(1); 153 | attendee.setGivenName("Jim"); 154 | attendee.setEmail("jim@gough"); 155 | return attendee; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/test/java/com/masteringapi/attendees/service/TestAttendeeStoreShould.java: -------------------------------------------------------------------------------- 1 | package com.masteringapi.attendees.service; 2 | 3 | import com.masteringapi.attendees.model.Attendee; 4 | import com.masteringapi.attendees.model.AttendeeNotFoundException; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | import static org.hamcrest.Matchers.equalTo; 10 | import static org.junit.jupiter.api.Assertions.assertThrows; 11 | 12 | public class TestAttendeeStoreShould { 13 | 14 | private AttendeeStore attendeeStore; 15 | 16 | @BeforeEach 17 | void before() { 18 | this.attendeeStore = new AttendeeStore(); 19 | } 20 | @Test 21 | void initializes_with_sample_data() { 22 | assertThat(this.attendeeStore.getAttendees().size(), equalTo(3)); 23 | } 24 | 25 | @Test 26 | void raises_exception_for_out_of_range_id() { 27 | assertThrows(AttendeeNotFoundException.class, () -> this.attendeeStore.getAttendee(5)); 28 | } 29 | 30 | @Test 31 | void returns_valid_attendee_with_in_range_id() throws AttendeeNotFoundException { 32 | Attendee attendee = this.attendeeStore.getAttendee(1); 33 | assertThat(attendee.getId(), equalTo(1)); 34 | assertThat(attendee.getSurname(), equalTo("Gough")); 35 | } 36 | 37 | @Test 38 | void add_a_new_attendee_to_store() throws AttendeeNotFoundException { 39 | Attendee attendee = new Attendee(); 40 | //Should be replaced with 4 41 | attendee.setId(100); 42 | attendee.setEmail("test@mail.com"); 43 | attendee.setGivenName("Test"); 44 | attendee.setSurname("Surname"); 45 | int id = this.attendeeStore.addAttendee(attendee); 46 | assertThat(id, equalTo(4)); 47 | Attendee storedAttendee = this.attendeeStore.getAttendee(4); 48 | assertThat(storedAttendee.getId(), equalTo(4)); 49 | assertThat(storedAttendee.getEmail(), equalTo("test@mail.com")); 50 | } 51 | 52 | @Test 53 | void remove_an_attendee_throws_when_out_of_bounds() { 54 | assertThrows(AttendeeNotFoundException.class, () -> this.attendeeStore.removeAttendee(5)); 55 | } 56 | 57 | @Test 58 | void remove_a_valid_attendee() throws AttendeeNotFoundException { 59 | this.attendeeStore.removeAttendee(3); 60 | assertThat(this.attendeeStore.getAttendees().size(), equalTo(2)); 61 | } 62 | 63 | @Test 64 | void update_an_attendee_throws_when_out_of_bounds() { 65 | assertThrows(AttendeeNotFoundException.class, () -> this.attendeeStore.updateAttendee(5, null)); 66 | } 67 | 68 | @Test 69 | void update_an_attendee_with_new_values() throws AttendeeNotFoundException { 70 | Attendee attendee = new Attendee(); 71 | attendee.setId(10); 72 | attendee.setEmail("test@mail.com"); 73 | attendee.setGivenName("Test"); 74 | attendee.setSurname("User"); 75 | this.attendeeStore.updateAttendee(1, attendee); 76 | assertThat(1, equalTo(this.attendeeStore.getAttendee(1).getId())); 77 | assertThat("test@mail.com", equalTo(this.attendeeStore.getAttendee(1).getEmail())); 78 | } 79 | } 80 | --------------------------------------------------------------------------------