├── LICENSE ├── README.md ├── chapter01 ├── go.mod ├── go.sum └── main.go ├── chapter02 ├── go.mod ├── go.sum ├── main.go ├── recipes.json └── swagger.json ├── chapter03 ├── apache-benchmark.p ├── benchmark.png ├── go.mod ├── go.sum ├── handlers │ └── handler.go ├── main.go ├── models │ └── recipe.go ├── recipes.json ├── swagger.json ├── with-cache.data └── without-cache.data ├── chapter04 ├── api │ ├── apache-benchmark.p │ ├── benchmark.png │ ├── go.mod │ ├── go.sum │ ├── handlers │ │ ├── auth.go │ │ └── recipes.go │ ├── main.go │ ├── models │ │ ├── recipe.go │ │ └── user.go │ ├── recipes.json │ ├── swagger.json │ ├── with-cache.data │ └── without-cache.data ├── auth0 │ ├── go.mod │ ├── go.sum │ ├── handlers │ │ ├── auth.go │ │ └── recipes.go │ ├── main.go │ └── models │ │ ├── recipe.go │ │ └── user.go ├── cookies.txt ├── cookies │ ├── apache-benchmark.p │ ├── benchmark.png │ ├── go.mod │ ├── go.sum │ ├── handlers │ │ ├── auth.go │ │ └── recipes.go │ ├── main.go │ ├── models │ │ ├── recipe.go │ │ └── user.go │ ├── recipes.json │ ├── swagger.json │ ├── with-cache.data │ └── without-cache.data └── users │ └── main.go ├── chapter05 ├── .DS_Store ├── api │ ├── go.mod │ ├── go.sum │ ├── handlers │ │ ├── auth.go │ │ └── recipes.go │ ├── main.go │ ├── models │ │ ├── recipe.go │ │ └── user.go │ ├── recipes.json │ └── swagger.json ├── embed │ ├── 404.html │ ├── assets.go │ ├── assets │ │ ├── .DS_Store │ │ ├── css │ │ │ └── app.css │ │ └── images │ │ │ ├── 404.jpg │ │ │ ├── blueberry-crumb-bars.jpg │ │ │ ├── burger.jpg │ │ │ ├── crock-pot-roast.jpg │ │ │ ├── curried-chicken-lentils-and-rice.jpeg │ │ │ ├── curry-chicken-salad.jpg │ │ │ ├── logo.svg │ │ │ ├── oatmeal-cookies.jpg │ │ │ ├── pizza.jpg │ │ │ ├── roasted-asparagus.jpg │ │ │ ├── stuffed-cornsquash.jpg │ │ │ ├── tacos.jpg │ │ │ └── yorkshire_pudding.jpg │ ├── main.go │ ├── recipes.json │ └── templates │ │ ├── index.tmpl │ │ ├── navbar.tmpl │ │ └── recipe.tmpl ├── go-assets │ ├── 404.html │ ├── assets.go │ ├── assets │ │ ├── .DS_Store │ │ ├── css │ │ │ └── app.css │ │ └── images │ │ │ ├── 404.jpg │ │ │ ├── blueberry-crumb-bars.jpg │ │ │ ├── burger.jpg │ │ │ ├── crock-pot-roast.jpg │ │ │ ├── curried-chicken-lentils-and-rice.jpeg │ │ │ ├── curry-chicken-salad.jpg │ │ │ ├── logo.svg │ │ │ ├── oatmeal-cookies.jpg │ │ │ ├── pizza.jpg │ │ │ ├── roasted-asparagus.jpg │ │ │ ├── stuffed-cornsquash.jpg │ │ │ ├── tacos.jpg │ │ │ └── yorkshire_pudding.jpg │ ├── main.go │ ├── recipes.json │ └── templates │ │ ├── index.tmpl │ │ ├── navbar.tmpl │ │ └── recipe.tmpl └── recipes-web │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── Navbar.js │ ├── Profile.css │ ├── Profile.js │ ├── Recipe.css │ ├── Recipe.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js ├── chapter06 ├── api │ ├── Dockerfile │ ├── docker-compose.yml │ ├── go.mod │ ├── go.sum │ ├── handlers │ │ └── handler.go │ ├── main.go │ ├── models │ │ └── recipe.go │ ├── nginx.conf │ └── recipes.json ├── bulk.sh ├── caching │ ├── illustration.png │ └── main.go ├── consumer │ ├── Dockerfile │ ├── docker-compose.yml │ ├── go.mod │ ├── go.sum │ └── main.go ├── dashboard │ ├── Dockerfile │ ├── assets │ │ └── css │ │ │ └── app.css │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── templates │ │ └── index.tmpl ├── docker-compose.yml ├── nginx.conf ├── parser │ └── main.go ├── producer │ └── main.go ├── threads └── web-app │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── Navbar.js │ ├── Profile.css │ ├── Profile.js │ ├── Recipe.css │ ├── Recipe.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js ├── chapter07 ├── api-with-db │ ├── coverage.out │ ├── debug.test │ ├── docker-compose.yml │ ├── go.mod │ ├── go.sum │ ├── handlers │ │ ├── auth.go │ │ └── recipes.go │ ├── main.go │ ├── main_test.go │ ├── models │ │ ├── recipe.go │ │ └── user.go │ ├── recipes.json │ └── swagger.json ├── api-without-db │ ├── coverage.out │ ├── main.go │ ├── main_test.go │ └── recipes.json ├── basic │ ├── main.go │ └── main_test.go ├── postman.json └── testify │ ├── main.go │ └── main_test.go ├── chapter08 ├── docker-compose.ecs.yml ├── docker-compose.yml ├── ecs-params.yml ├── eks-admin-service-account.yaml ├── nginx.conf ├── nginx │ ├── Dockerfile │ └── nginx.conf ├── resources │ ├── api-deployment.yaml │ ├── dashboard-deployment.yaml │ ├── mongodb-deployment.yaml │ ├── mongodb-service.yaml │ ├── nginx-deployment.yaml │ ├── nginx-service.yaml │ ├── redis-deployment.yaml │ └── redis-service.yaml └── task-execution-assume-role.json ├── chapter09 ├── .circleci │ ├── config.ecs.yml │ ├── config.eks.yml │ ├── config.s3.yml │ └── config.yml ├── Dockerfile ├── README.md ├── docker-compose.yml ├── go.mod ├── go.sum ├── handlers │ └── handler.go ├── iam-policy.json ├── main.go ├── models │ └── recipe.go ├── recipes.json └── swagger.json └── chapter10 ├── Dockerfile ├── README.md ├── dashboard.json ├── debug.log ├── docker-compose.yml ├── filebeat.yml ├── go.mod ├── go.sum ├── handlers └── handler.go ├── logstash.conf ├── main.go ├── models └── recipe.go ├── prometheus.yml └── telegraf.conf /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Packt 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 | 2 | 3 | 4 | # Building Distributed Applications in Gin 5 | 6 | Useful Links 7 | 8 | This is the code repository for [Building Distributed Applications in Gin](https://www.packtpub.com/web-development/building-distributed-applications-in-gin?utm_source=github&utm_medium=repository&utm_campaign=9781786461629), published by Packt. 9 | 10 | **A hands-on guide for Go developers to build and deploy distributed web apps with the Gin framework.** 11 | 12 | ## What is this book about? 13 | Gin is a high-performance HTTP web framework used to build web applications and microservices in Go. This book is designed to teach you the ins and outs of the Gin framework with the help of practical examples. 14 | 15 | You’ll start by exploring the basics of the Gin framework, before progressing to build a real-world RESTful API. Along the way, you’ll learn how to write custom middleware and understand the routing mechanism, as well as how to bind user data and validate incoming HTTP requests. The book also demonstrates how to store and retrieve data at scale with a NoSQL database such as MongoDB, and how to implement a caching layer with Redis. Next, you’ll understand how to secure and test your API endpoints with authentication protocols such as OAuth 2 and JWT. Later chapters will guide you through rendering HTML templates on the server-side and building a frontend application with the React web framework to consume API responses. Finally, you’ll deploy your application on Amazon Web Services (AWS) and learn how to automate the deployment process with a CI/CD pipeline. 16 | 17 | By the end of this Gin book, you will be able to design, build, and deploy a production-ready distributed application from scratch using the Gin framework. 18 | 19 | This book covers the following exciting features: 20 | Build a production-ready REST API with the Gin framework 21 | Scale web applications with event-driven architecture 22 | Use NoSQL databases for data persistence 23 | Set up authentication middleware with JWT and Auth0 24 | Deploy a Gin-based RESTful API on AWS with Docker and Kubernetes 25 | Implement a CI/CD workflow for Gin web apps 26 | 27 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1801074852) today! 28 | 29 | https://www.packtpub.com/ 31 | 32 | ## Instructions and Navigations 33 | All of the code is organized into folders. For example, Chapter02. 34 | 35 | The code will look like the following: 36 | ``` 37 | module github.com/mlabouardy/hello-world 38 | go 1.15 39 | ``` 40 | 41 | **Following is what you need for this book:** 42 | This book is for Go developers who are comfortable with the Go language and seeking to learn REST API design and development with the Gin framework. Beginner-level knowledge of the Go programming language is required to make the most of this book. 43 | 44 | With the following software and hardware list you can run all code files present in the book (Chapter 1-10). 45 | ### Software and Hardware List 46 | | Chapter | Software required | OS required | 47 | | -------- | ------------------------------------ | ----------------------------------- | 48 | | 1 | Visual Studio Code 1.56.2 | Windows, Mac OS X, and Linux (Any) | 49 | | 2 | Go Swagger | Windows, Mac OS X, and Linux (Any) | 50 | | 3 | Postman | Windows, Mac OS X, and Linux (Any) | 51 | | 4 | cURL | Windows, Mac OS X, and Linux (Any) | 52 | | 5 | Docker CE | Windows, Mac OS X, and Linux (Any) | 53 | | 6 | Docker Compose | Windows, Mac OS X, and Linux (Any) | 54 | | 7 | AWS CLI | Windows, Mac OS X, and Linux (Any) | 55 | | 8 | Node.JS LTS | Windows, Mac OS X, and Linux (Any) | 56 | | 9 | React 17.0.1 | Windows, Mac OS X, and Linux (Any) | 57 | | 10 | Auth0, CircleCI, Synk, and AWS accounts | Windows, Mac OS X, and Linux (Any) | 58 | 59 | We also provide a PDF file that has color images of the screenshots/diagrams used in this book. [Click here to download it](https://static.packt-cdn.com/downloads/9781801074858_ColorImages.pdf). 60 | 61 | ### Related products 62 | * Building Cross-Platform GUI Applications with Fyne [[Packt]](https://www.packtpub.com/product/building-cross-platform-gui-applications-with-fyne/9781800563162?utm_source=github&utm_medium=repository&utm_campaign=9781800563162) [[Amazon]](https://www.amazon.com/dp/B08PKTNVBQ) 63 | 64 | * Hands-On High Performance with Go [[Packt]](https://www.packtpub.com/product/hands-on-high-performance-with-go/9781789805789?utm_source=github&utm_medium=repository&utm_campaign=9781789805789) [[Amazon]](https://www.amazon.com/dp/1789805783) 65 | 66 | ## Get to Know the Author 67 | **Mohamed Labouardy** is the CTO and co-founder of Crew, and a DevSecOps evangelist. He is the founder of Komiser, a regular conference speaker, and author of several books around Serverless and Distributed Applications. He is also a contributor to numerous open-source projects such as Jenkins, Docker, and Telegraf. You can find him on Twitter at @mlabouardy. 68 | 69 | ### Download a free PDF 70 | 71 | If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.
Simply click on the link to claim your free PDF.
72 |

https://packt.link/free-ebook/9781801074858

-------------------------------------------------------------------------------- /chapter01/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlabouardy/hello-world 2 | 3 | go 1.15 4 | 5 | require github.com/gin-gonic/gin v1.6.3 6 | -------------------------------------------------------------------------------- /chapter01/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 5 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 6 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 7 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 8 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 9 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 10 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 11 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 12 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 13 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 14 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 15 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 16 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 17 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 18 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 19 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 20 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 21 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 22 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 23 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 24 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 25 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 27 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 28 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 32 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 33 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 34 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 35 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 36 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 37 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 38 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 39 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 40 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 42 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 46 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 47 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 48 | -------------------------------------------------------------------------------- /chapter01/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func IndexHandler(c *gin.Context) { 10 | c.JSON(200, gin.H{ 11 | "message": "hello world", 12 | }) 13 | } 14 | 15 | func main() { 16 | router := gin.Default() 17 | router.GET("/", IndexHandler) 18 | http.ListenAndServe(":8080", router) 19 | } 20 | -------------------------------------------------------------------------------- /chapter02/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlabouardy/recipes-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/rs/xid v1.2.1 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /chapter02/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 4 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 5 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 6 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 7 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 8 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 9 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 10 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 11 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 12 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 13 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 14 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 15 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 16 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 17 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 18 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 19 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 20 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 21 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 22 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 23 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 24 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 25 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 26 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= 29 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 30 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 31 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 32 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 33 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 34 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 35 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 36 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 37 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 38 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 40 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 43 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 44 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 45 | -------------------------------------------------------------------------------- /chapter03/apache-benchmark.p: -------------------------------------------------------------------------------- 1 | set terminal png 2 | set output "benchmark.png" 3 | set title "Cache benchmark" 4 | set size 1,0.7 5 | set grid y 6 | set xlabel "request" 7 | set ylabel "response time (ms)" 8 | plot "with-cache.data" using 9 smooth sbezier with lines title "with cache", "without-cache.data" using 9 smooth sbezier with lines title "without cache" -------------------------------------------------------------------------------- /chapter03/benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter03/benchmark.png -------------------------------------------------------------------------------- /chapter03/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlabouardy/recipes-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/go-redis/redis v6.15.9+incompatible 8 | github.com/go-redis/redis/v8 v8.4.10 9 | go.mongodb.org/mongo-driver v1.4.5 10 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb 11 | ) 12 | -------------------------------------------------------------------------------- /chapter03/main.go: -------------------------------------------------------------------------------- 1 | // Recipes API 2 | // 3 | // This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin. 4 | // 5 | // Schemes: http 6 | // Host: localhost:8080 7 | // BasePath: / 8 | // Version: 1.0.0 9 | // Contact: Mohamed Labouardy https://labouardy.com 10 | // 11 | // Consumes: 12 | // - application/json 13 | // 14 | // Produces: 15 | // - application/json 16 | // swagger:meta 17 | package main 18 | 19 | import ( 20 | "context" 21 | "log" 22 | "os" 23 | 24 | "github.com/gin-gonic/gin" 25 | "github.com/go-redis/redis" 26 | handlers "github.com/mlabouardy/recipes-api/handlers" 27 | "go.mongodb.org/mongo-driver/mongo" 28 | "go.mongodb.org/mongo-driver/mongo/options" 29 | "go.mongodb.org/mongo-driver/mongo/readpref" 30 | ) 31 | 32 | var recipesHandler *handlers.RecipesHandler 33 | 34 | func init() { 35 | /*recipes = make([]Recipe, 0) 36 | file, _ := ioutil.ReadFile("recipes.json") 37 | _ = json.Unmarshal([]byte(file), &recipes) 38 | 39 | ctx = context.Background() 40 | client, err = mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 41 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 42 | log.Fatal(err) 43 | } 44 | log.Println("Connected to MongoDB") 45 | 46 | var listOfRecipes []interface{} 47 | for _, recipe := range recipes { 48 | listOfRecipes = append(listOfRecipes, recipe) 49 | } 50 | collection := client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 51 | insertManyResult, err := collection.InsertMany(ctx, listOfRecipes) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | log.Println("Inserted recipes: ", len(insertManyResult.InsertedIDs))*/ 56 | ctx := context.Background() 57 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 58 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 59 | log.Fatal(err) 60 | } 61 | log.Println("Connected to MongoDB") 62 | collection := client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 63 | 64 | redisClient := redis.NewClient(&redis.Options{ 65 | Addr: "localhost:6379", 66 | Password: "", 67 | DB: 0, 68 | }) 69 | 70 | status := redisClient.Ping() 71 | log.Println(status) 72 | 73 | recipesHandler = handlers.NewRecipesHandler(ctx, collection, redisClient) 74 | } 75 | 76 | func main() { 77 | router := gin.Default() 78 | router.POST("/recipes", recipesHandler.NewRecipeHandler) 79 | router.GET("/recipes", recipesHandler.ListRecipesHandler) 80 | router.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler) 81 | router.DELETE("/recipes/:id", recipesHandler.DeleteRecipeHandler) 82 | router.GET("/recipes/:id", recipesHandler.GetOneRecipeHandler) 83 | /* 84 | router.GET("/recipes/search", SearchRecipesHandler)*/ 85 | router.Run() 86 | } 87 | -------------------------------------------------------------------------------- /chapter03/models/recipe.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // swagger:parameters recipes newRecipe 10 | type Recipe struct { 11 | //swagger:ignore 12 | ID primitive.ObjectID `json:"id" bson:"_id"` 13 | Name string `json:"name" bson:"name"` 14 | Tags []string `json:"tags" bson:"tags"` 15 | Ingredients []string `json:"ingredients" bson:"ingredients"` 16 | Instructions []string `json:"instructions" bson:"instructions"` 17 | PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"` 18 | } 19 | -------------------------------------------------------------------------------- /chapter03/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumes": [ 3 | "application/json" 4 | ], 5 | "produces": [ 6 | "application/json" 7 | ], 8 | "schemes": [ 9 | "http" 10 | ], 11 | "swagger": "2.0", 12 | "info": { 13 | "description": "This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin.", 14 | "title": "Recipes API", 15 | "contact": { 16 | "name": "Mohamed Labouardy", 17 | "url": "https://labouardy.com", 18 | "email": "mohamed@labouardy.com" 19 | }, 20 | "version": "1.0.0" 21 | }, 22 | "host": "localhost:8080", 23 | "basePath": "/", 24 | "paths": { 25 | "/recipes": { 26 | "get": { 27 | "description": "Returns list of recipes", 28 | "produces": [ 29 | "application/json" 30 | ], 31 | "tags": [ 32 | "recipes" 33 | ], 34 | "operationId": "listRecipes", 35 | "responses": { 36 | "200": { 37 | "description": "Successful operation" 38 | } 39 | } 40 | }, 41 | "post": { 42 | "description": "Create a new recipe", 43 | "produces": [ 44 | "application/json" 45 | ], 46 | "tags": [ 47 | "recipes" 48 | ], 49 | "operationId": "newRecipe", 50 | "responses": { 51 | "200": { 52 | "description": "Successful operation" 53 | }, 54 | "400": { 55 | "description": "Invalid input" 56 | } 57 | } 58 | } 59 | }, 60 | "/recipes/search": { 61 | "get": { 62 | "description": "Search recipes based on tags", 63 | "produces": [ 64 | "application/json" 65 | ], 66 | "tags": [ 67 | "recipes" 68 | ], 69 | "operationId": "findRecipe", 70 | "parameters": [ 71 | { 72 | "type": "string", 73 | "description": "recipe tag", 74 | "name": "tag", 75 | "in": "query", 76 | "required": true 77 | } 78 | ], 79 | "responses": { 80 | "200": { 81 | "description": "Successful operation" 82 | } 83 | } 84 | } 85 | }, 86 | "/recipes/{id}": { 87 | "put": { 88 | "description": "Update an existing recipe", 89 | "produces": [ 90 | "application/json" 91 | ], 92 | "tags": [ 93 | "recipes" 94 | ], 95 | "operationId": "updateRecipe", 96 | "parameters": [ 97 | { 98 | "type": "string", 99 | "description": "ID of the recipe", 100 | "name": "id", 101 | "in": "path", 102 | "required": true 103 | } 104 | ], 105 | "responses": { 106 | "200": { 107 | "description": "Successful operation" 108 | }, 109 | "400": { 110 | "description": "Invalid input" 111 | }, 112 | "404": { 113 | "description": "Invalid recipe ID" 114 | } 115 | } 116 | }, 117 | "delete": { 118 | "description": "Delete an existing recipe", 119 | "produces": [ 120 | "application/json" 121 | ], 122 | "tags": [ 123 | "recipes" 124 | ], 125 | "operationId": "deleteRecipe", 126 | "parameters": [ 127 | { 128 | "type": "string", 129 | "description": "ID of the recipe", 130 | "name": "id", 131 | "in": "path", 132 | "required": true 133 | } 134 | ], 135 | "responses": { 136 | "200": { 137 | "description": "Successful operation" 138 | }, 139 | "404": { 140 | "description": "Invalid recipe ID" 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /chapter04/api/apache-benchmark.p: -------------------------------------------------------------------------------- 1 | set terminal png 2 | set output "benchmark.png" 3 | set title "Cache benchmark" 4 | set size 1,0.7 5 | set grid y 6 | set xlabel "request" 7 | set ylabel "response time (ms)" 8 | plot "with-cache.data" using 9 smooth sbezier with lines title "with cache", "without-cache.data" using 9 smooth sbezier with lines title "without cache" -------------------------------------------------------------------------------- /chapter04/api/benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter04/api/benchmark.png -------------------------------------------------------------------------------- /chapter04/api/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlabouardy/recipes-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gin-gonic/gin v1.6.3 8 | github.com/go-redis/redis v6.15.9+incompatible 9 | github.com/go-redis/redis/v8 v8.4.10 10 | go.mongodb.org/mongo-driver v1.4.5 11 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb 12 | ) 13 | -------------------------------------------------------------------------------- /chapter04/api/handlers/auth.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "crypto/sha256" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/dgrijalva/jwt-go" 10 | "github.com/gin-gonic/gin" 11 | "github.com/mlabouardy/recipes-api/models" 12 | "go.mongodb.org/mongo-driver/bson" 13 | "go.mongodb.org/mongo-driver/mongo" 14 | "golang.org/x/net/context" 15 | ) 16 | 17 | type AuthHandler struct { 18 | collection *mongo.Collection 19 | ctx context.Context 20 | } 21 | 22 | type Claims struct { 23 | Username string `json:"username"` 24 | jwt.StandardClaims 25 | } 26 | 27 | type JWTOutput struct { 28 | Token string `json:"token"` 29 | Expires time.Time `json:"expires"` 30 | } 31 | 32 | func NewAuthHandler(ctx context.Context, collection *mongo.Collection) *AuthHandler { 33 | return &AuthHandler{ 34 | collection: collection, 35 | ctx: ctx, 36 | } 37 | } 38 | 39 | func (handler *AuthHandler) SignInHandler(c *gin.Context) { 40 | var user models.User 41 | if err := c.ShouldBindJSON(&user); err != nil { 42 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 43 | return 44 | } 45 | 46 | h := sha256.New() 47 | 48 | cur := handler.collection.FindOne(handler.ctx, bson.M{ 49 | "username": user.Username, 50 | "password": string(h.Sum([]byte(user.Password))), 51 | }) 52 | if cur.Err() != nil { 53 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) 54 | return 55 | } 56 | 57 | expirationTime := time.Now().Add(10 * time.Minute) 58 | claims := &Claims{ 59 | Username: user.Username, 60 | StandardClaims: jwt.StandardClaims{ 61 | ExpiresAt: expirationTime.Unix(), 62 | }, 63 | } 64 | 65 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 66 | tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) 67 | if err != nil { 68 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 69 | return 70 | } 71 | 72 | jwtOutput := JWTOutput{ 73 | Token: tokenString, 74 | Expires: expirationTime, 75 | } 76 | c.JSON(http.StatusOK, jwtOutput) 77 | } 78 | 79 | func (handler *AuthHandler) RefreshHandler(c *gin.Context) { 80 | tokenValue := c.GetHeader("Authorization") 81 | claims := &Claims{} 82 | tkn, err := jwt.ParseWithClaims(tokenValue, claims, func(token *jwt.Token) (interface{}, error) { 83 | return []byte(os.Getenv("JWT_SECRET")), nil 84 | }) 85 | if err != nil { 86 | c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) 87 | return 88 | } 89 | if !tkn.Valid { 90 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) 91 | return 92 | } 93 | 94 | if time.Unix(claims.ExpiresAt, 0).Sub(time.Now()) > 30*time.Second { 95 | c.JSON(http.StatusBadRequest, gin.H{"error": "Token is not expired yet"}) 96 | return 97 | } 98 | 99 | expirationTime := time.Now().Add(5 * time.Minute) 100 | claims.ExpiresAt = expirationTime.Unix() 101 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 102 | tokenString, err := token.SignedString(os.Getenv("JWT_SECRET")) 103 | if err != nil { 104 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 105 | return 106 | } 107 | 108 | jwtOutput := JWTOutput{ 109 | Token: tokenString, 110 | Expires: expirationTime, 111 | } 112 | c.JSON(http.StatusOK, jwtOutput) 113 | } 114 | 115 | func (handler *AuthHandler) AuthMiddleware() gin.HandlerFunc { 116 | return func(c *gin.Context) { 117 | tokenValue := c.GetHeader("Authorization") 118 | claims := &Claims{} 119 | 120 | tkn, err := jwt.ParseWithClaims(tokenValue, claims, func(token *jwt.Token) (interface{}, error) { 121 | return []byte(os.Getenv("JWT_SECRET")), nil 122 | }) 123 | if err != nil { 124 | c.AbortWithStatus(http.StatusUnauthorized) 125 | } 126 | if !tkn.Valid { 127 | c.AbortWithStatus(http.StatusUnauthorized) 128 | } 129 | c.Next() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /chapter04/api/main.go: -------------------------------------------------------------------------------- 1 | // Recipes API 2 | // 3 | // This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin. 4 | // 5 | // Schemes: http 6 | // Host: localhost:8080 7 | // BasePath: / 8 | // Version: 1.0.0 9 | // Contact: Mohamed Labouardy https://labouardy.com 10 | // 11 | // Consumes: 12 | // - application/json 13 | // 14 | // Produces: 15 | // - application/json 16 | // swagger:meta 17 | package main 18 | 19 | import ( 20 | "context" 21 | "log" 22 | "os" 23 | 24 | "github.com/gin-gonic/gin" 25 | "github.com/go-redis/redis" 26 | handlers "github.com/mlabouardy/recipes-api/handlers" 27 | "go.mongodb.org/mongo-driver/mongo" 28 | "go.mongodb.org/mongo-driver/mongo/options" 29 | "go.mongodb.org/mongo-driver/mongo/readpref" 30 | ) 31 | 32 | var authHandler *handlers.AuthHandler 33 | var recipesHandler *handlers.RecipesHandler 34 | 35 | func init() { 36 | ctx := context.Background() 37 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 38 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 39 | log.Fatal(err) 40 | } 41 | log.Println("Connected to MongoDB") 42 | collectionRecipes := client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 43 | 44 | redisClient := redis.NewClient(&redis.Options{ 45 | Addr: "localhost:6379", 46 | Password: "", 47 | DB: 0, 48 | }) 49 | 50 | status := redisClient.Ping() 51 | log.Println(status) 52 | 53 | recipesHandler = handlers.NewRecipesHandler(ctx, collectionRecipes, redisClient) 54 | 55 | collectionUsers := client.Database(os.Getenv("MONGO_DATABASE")).Collection("users") 56 | authHandler = handlers.NewAuthHandler(ctx, collectionUsers) 57 | } 58 | 59 | func main() { 60 | router := gin.Default() 61 | 62 | router.GET("/recipes", recipesHandler.ListRecipesHandler) 63 | 64 | router.POST("/signin", authHandler.SignInHandler) 65 | router.POST("/refresh", authHandler.RefreshHandler) 66 | 67 | authorized := router.Group("/") 68 | authorized.Use(authHandler.AuthMiddleware()) 69 | { 70 | authorized.POST("/recipes", recipesHandler.NewRecipeHandler) 71 | authorized.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler) 72 | authorized.DELETE("/recipes/:id", recipesHandler.DeleteRecipeHandler) 73 | authorized.GET("/recipes/:id", recipesHandler.GetOneRecipeHandler) 74 | } 75 | 76 | router.Run() 77 | } 78 | -------------------------------------------------------------------------------- /chapter04/api/models/recipe.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // swagger:parameters recipes newRecipe 10 | type Recipe struct { 11 | //swagger:ignore 12 | ID primitive.ObjectID `json:"id" bson:"_id"` 13 | Name string `json:"name" bson:"name"` 14 | Tags []string `json:"tags" bson:"tags"` 15 | Ingredients []string `json:"ingredients" bson:"ingredients"` 16 | Instructions []string `json:"instructions" bson:"instructions"` 17 | PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"` 18 | } 19 | -------------------------------------------------------------------------------- /chapter04/api/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | Password string `json:"password"` 5 | Username string `json:"username"` 6 | } 7 | -------------------------------------------------------------------------------- /chapter04/api/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumes": [ 3 | "application/json" 4 | ], 5 | "produces": [ 6 | "application/json" 7 | ], 8 | "schemes": [ 9 | "http" 10 | ], 11 | "swagger": "2.0", 12 | "info": { 13 | "description": "This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin.", 14 | "title": "Recipes API", 15 | "contact": { 16 | "name": "Mohamed Labouardy", 17 | "url": "https://labouardy.com", 18 | "email": "mohamed@labouardy.com" 19 | }, 20 | "version": "1.0.0" 21 | }, 22 | "host": "localhost:8080", 23 | "basePath": "/", 24 | "paths": { 25 | "/recipes": { 26 | "get": { 27 | "description": "Returns list of recipes", 28 | "produces": [ 29 | "application/json" 30 | ], 31 | "tags": [ 32 | "recipes" 33 | ], 34 | "operationId": "listRecipes", 35 | "responses": { 36 | "200": { 37 | "description": "Successful operation" 38 | } 39 | } 40 | }, 41 | "post": { 42 | "description": "Create a new recipe", 43 | "produces": [ 44 | "application/json" 45 | ], 46 | "tags": [ 47 | "recipes" 48 | ], 49 | "operationId": "newRecipe", 50 | "responses": { 51 | "200": { 52 | "description": "Successful operation" 53 | }, 54 | "400": { 55 | "description": "Invalid input" 56 | } 57 | } 58 | } 59 | }, 60 | "/recipes/search": { 61 | "get": { 62 | "description": "Search recipes based on tags", 63 | "produces": [ 64 | "application/json" 65 | ], 66 | "tags": [ 67 | "recipes" 68 | ], 69 | "operationId": "findRecipe", 70 | "parameters": [ 71 | { 72 | "type": "string", 73 | "description": "recipe tag", 74 | "name": "tag", 75 | "in": "query", 76 | "required": true 77 | } 78 | ], 79 | "responses": { 80 | "200": { 81 | "description": "Successful operation" 82 | } 83 | } 84 | } 85 | }, 86 | "/recipes/{id}": { 87 | "put": { 88 | "description": "Update an existing recipe", 89 | "produces": [ 90 | "application/json" 91 | ], 92 | "tags": [ 93 | "recipes" 94 | ], 95 | "operationId": "updateRecipe", 96 | "parameters": [ 97 | { 98 | "type": "string", 99 | "description": "ID of the recipe", 100 | "name": "id", 101 | "in": "path", 102 | "required": true 103 | } 104 | ], 105 | "responses": { 106 | "200": { 107 | "description": "Successful operation" 108 | }, 109 | "400": { 110 | "description": "Invalid input" 111 | }, 112 | "404": { 113 | "description": "Invalid recipe ID" 114 | } 115 | } 116 | }, 117 | "delete": { 118 | "description": "Delete an existing recipe", 119 | "produces": [ 120 | "application/json" 121 | ], 122 | "tags": [ 123 | "recipes" 124 | ], 125 | "operationId": "deleteRecipe", 126 | "parameters": [ 127 | { 128 | "type": "string", 129 | "description": "ID of the recipe", 130 | "name": "id", 131 | "in": "path", 132 | "required": true 133 | } 134 | ], 135 | "responses": { 136 | "200": { 137 | "description": "Successful operation" 138 | }, 139 | "404": { 140 | "description": "Invalid recipe ID" 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /chapter04/auth0/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlabouardy/recipes-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/auth0-community/go-auth0 v1.0.0 7 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 8 | github.com/gin-contrib/sessions v0.0.3 9 | github.com/gin-gonic/autotls v0.0.3 // indirect 10 | github.com/gin-gonic/gin v1.6.3 11 | github.com/go-redis/redis v6.15.9+incompatible 12 | github.com/go-redis/redis/v8 v8.4.10 13 | github.com/rs/xid v1.2.1 14 | go.mongodb.org/mongo-driver v1.4.5 15 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb 16 | gopkg.in/square/go-jose.v2 v2.5.1 17 | ) 18 | -------------------------------------------------------------------------------- /chapter04/auth0/handlers/auth.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | 7 | "github.com/auth0-community/go-auth0" 8 | "github.com/gin-gonic/gin" 9 | "go.mongodb.org/mongo-driver/mongo" 10 | "golang.org/x/net/context" 11 | jose "gopkg.in/square/go-jose.v2" 12 | ) 13 | 14 | type AuthHandler struct { 15 | collection *mongo.Collection 16 | ctx context.Context 17 | } 18 | 19 | func NewAuthHandler(ctx context.Context, collection *mongo.Collection) *AuthHandler { 20 | return &AuthHandler{ 21 | collection: collection, 22 | ctx: ctx, 23 | } 24 | } 25 | 26 | func (handler *AuthHandler) AuthMiddleware() gin.HandlerFunc { 27 | return func(c *gin.Context) { 28 | var auth0Domain = "https://" + os.Getenv("AUTH0_DOMAIN") + "/" 29 | client := auth0.NewJWKClient(auth0.JWKClientOptions{URI: auth0Domain + ".well-known/jwks.json"}, nil) 30 | configuration := auth0.NewConfiguration(client, []string{os.Getenv("AUTH0_API_IDENTIFIER")}, auth0Domain, jose.RS256) 31 | validator := auth0.NewValidator(configuration, nil) 32 | 33 | _, err := validator.ValidateRequest(c.Request) 34 | 35 | if err != nil { 36 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid token"}) 37 | c.Abort() 38 | return 39 | } 40 | c.Next() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /chapter04/auth0/main.go: -------------------------------------------------------------------------------- 1 | // Recipes API 2 | // 3 | // This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin. 4 | // 5 | // Schemes: http 6 | // Host: api.recipes.io:8080 7 | // BasePath: / 8 | // Version: 1.0.0 9 | // Contact: Mohamed Labouardy https://labouardy.com 10 | // 11 | // Consumes: 12 | // - application/json 13 | // 14 | // Produces: 15 | // - application/json 16 | // swagger:meta 17 | package main 18 | 19 | import ( 20 | "context" 21 | "log" 22 | "os" 23 | 24 | "github.com/gin-gonic/gin" 25 | "github.com/go-redis/redis" 26 | handlers "github.com/mlabouardy/recipes-api/handlers" 27 | "go.mongodb.org/mongo-driver/mongo" 28 | "go.mongodb.org/mongo-driver/mongo/options" 29 | "go.mongodb.org/mongo-driver/mongo/readpref" 30 | ) 31 | 32 | var authHandler *handlers.AuthHandler 33 | var recipesHandler *handlers.RecipesHandler 34 | 35 | func init() { 36 | ctx := context.Background() 37 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 38 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 39 | log.Fatal(err) 40 | } 41 | log.Println("Connected to MongoDB") 42 | collectionRecipes := client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 43 | 44 | redisClient := redis.NewClient(&redis.Options{ 45 | Addr: "localhost:6379", 46 | Password: "", 47 | DB: 0, 48 | }) 49 | 50 | status := redisClient.Ping() 51 | log.Println(status) 52 | 53 | recipesHandler = handlers.NewRecipesHandler(ctx, collectionRecipes, redisClient) 54 | 55 | collectionUsers := client.Database(os.Getenv("MONGO_DATABASE")).Collection("users") 56 | authHandler = handlers.NewAuthHandler(ctx, collectionUsers) 57 | } 58 | 59 | func main() { 60 | router := gin.Default() 61 | 62 | router.GET("/recipes", recipesHandler.ListRecipesHandler) 63 | 64 | authorized := router.Group("/") 65 | authorized.Use(authHandler.AuthMiddleware()) 66 | { 67 | authorized.POST("/recipes", recipesHandler.NewRecipeHandler) 68 | authorized.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler) 69 | authorized.DELETE("/recipes/:id", recipesHandler.DeleteRecipeHandler) 70 | authorized.GET("/recipes/:id", recipesHandler.GetOneRecipeHandler) 71 | } 72 | 73 | router.Run() 74 | } 75 | -------------------------------------------------------------------------------- /chapter04/auth0/models/recipe.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // swagger:parameters recipes newRecipe 10 | type Recipe struct { 11 | //swagger:ignore 12 | ID primitive.ObjectID `json:"id" bson:"_id"` 13 | Name string `json:"name" bson:"name"` 14 | Tags []string `json:"tags" bson:"tags"` 15 | Ingredients []string `json:"ingredients" bson:"ingredients"` 16 | Instructions []string `json:"instructions" bson:"instructions"` 17 | PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"` 18 | } 19 | -------------------------------------------------------------------------------- /chapter04/auth0/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // swagger:parameters auth signIn 4 | type User struct { 5 | Password string `json:"password"` 6 | Username string `json:"username"` 7 | } 8 | -------------------------------------------------------------------------------- /chapter04/cookies.txt: -------------------------------------------------------------------------------- 1 | # Netscape HTTP Cookie File 2 | # https://curl.haxx.se/docs/http-cookies.html 3 | # This file was generated by libcurl! Edit at your own risk. 4 | 5 | localhost FALSE / FALSE 1615374150 recipes_api MTYxMjc4MjE1MHxOd3dBTkVSWlEwSlFNazAwU2xwTlVVOWFRazAzVERWRVREUlZXbFJCUVVsYU5rcE5TMUUyV0ZGV01rNUVWRVJGVVRSWlJsQktNa0U9fFNl3Yj9KqU3yMsnoNqqCobqbilmgx1oQdr-Zh9FWtS9 6 | -------------------------------------------------------------------------------- /chapter04/cookies/apache-benchmark.p: -------------------------------------------------------------------------------- 1 | set terminal png 2 | set output "benchmark.png" 3 | set title "Cache benchmark" 4 | set size 1,0.7 5 | set grid y 6 | set xlabel "request" 7 | set ylabel "response time (ms)" 8 | plot "with-cache.data" using 9 smooth sbezier with lines title "with cache", "without-cache.data" using 9 smooth sbezier with lines title "without cache" -------------------------------------------------------------------------------- /chapter04/cookies/benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter04/cookies/benchmark.png -------------------------------------------------------------------------------- /chapter04/cookies/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlabouardy/recipes-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gin-contrib/sessions v0.0.3 8 | github.com/gin-gonic/autotls v0.0.3 // indirect 9 | github.com/gin-gonic/gin v1.6.3 10 | github.com/go-redis/redis v6.15.9+incompatible 11 | github.com/go-redis/redis/v8 v8.4.10 12 | github.com/rs/xid v1.2.1 13 | go.mongodb.org/mongo-driver v1.4.5 14 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb 15 | ) 16 | -------------------------------------------------------------------------------- /chapter04/cookies/handlers/auth.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "crypto/sha256" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/dgrijalva/jwt-go" 9 | "github.com/gin-contrib/sessions" 10 | "github.com/gin-gonic/gin" 11 | "github.com/mlabouardy/recipes-api/models" 12 | "github.com/rs/xid" 13 | "go.mongodb.org/mongo-driver/bson" 14 | "go.mongodb.org/mongo-driver/mongo" 15 | "golang.org/x/net/context" 16 | ) 17 | 18 | type AuthHandler struct { 19 | collection *mongo.Collection 20 | ctx context.Context 21 | } 22 | 23 | type Claims struct { 24 | Username string `json:"username"` 25 | jwt.StandardClaims 26 | } 27 | 28 | type JWTOutput struct { 29 | Token string `json:"token"` 30 | Expires time.Time `json:"expires"` 31 | } 32 | 33 | func NewAuthHandler(ctx context.Context, collection *mongo.Collection) *AuthHandler { 34 | return &AuthHandler{ 35 | collection: collection, 36 | ctx: ctx, 37 | } 38 | } 39 | 40 | // swagger:operation POST /signin auth signIn 41 | // Login with username and password 42 | // --- 43 | // produces: 44 | // - application/json 45 | // responses: 46 | // '200': 47 | // description: Successful operation 48 | // '401': 49 | // description: Invalid credentials 50 | func (handler *AuthHandler) SignInHandler(c *gin.Context) { 51 | var user models.User 52 | if err := c.ShouldBindJSON(&user); err != nil { 53 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 54 | return 55 | } 56 | 57 | h := sha256.New() 58 | 59 | cur := handler.collection.FindOne(handler.ctx, bson.M{ 60 | "username": user.Username, 61 | "password": string(h.Sum([]byte(user.Password))), 62 | }) 63 | if cur.Err() != nil { 64 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) 65 | return 66 | } 67 | 68 | sessionToken := xid.New().String() 69 | session := sessions.Default(c) 70 | session.Set("username", user.Username) 71 | session.Set("token", sessionToken) 72 | session.Save() 73 | 74 | c.JSON(http.StatusOK, gin.H{"message": "User signed in"}) 75 | } 76 | 77 | // swagger:operation POST /refresh auth refresh 78 | // Refresh token 79 | // --- 80 | // produces: 81 | // - application/json 82 | // responses: 83 | // '200': 84 | // description: Successful operation 85 | // '401': 86 | // description: Invalid credentials 87 | func (handler *AuthHandler) RefreshHandler(c *gin.Context) { 88 | session := sessions.Default(c) 89 | sessionToken := session.Get("token") 90 | sessionUser := session.Get("username") 91 | if sessionToken == nil { 92 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid session cookie"}) 93 | return 94 | } 95 | 96 | sessionToken = xid.New().String() 97 | session.Set("username", sessionUser.(string)) 98 | session.Set("token", sessionToken) 99 | session.Save() 100 | 101 | c.JSON(http.StatusOK, gin.H{"message": "New session issued"}) 102 | } 103 | 104 | // swagger:operation POST /signout auth signOut 105 | // Signing out 106 | // --- 107 | // responses: 108 | // '200': 109 | // description: Successful operation 110 | func (handler *AuthHandler) SignOutHandler(c *gin.Context) { 111 | session := sessions.Default(c) 112 | session.Clear() 113 | session.Save() 114 | c.JSON(http.StatusOK, gin.H{"message": "Signed out..."}) 115 | } 116 | 117 | func (handler *AuthHandler) AuthMiddleware() gin.HandlerFunc { 118 | return func(c *gin.Context) { 119 | session := sessions.Default(c) 120 | sessionToken := session.Get("token") 121 | if sessionToken == nil { 122 | c.JSON(http.StatusForbidden, gin.H{ 123 | "message": "Not logged", 124 | }) 125 | c.Abort() 126 | } 127 | c.Next() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /chapter04/cookies/main.go: -------------------------------------------------------------------------------- 1 | // Recipes API 2 | // 3 | // This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin. 4 | // 5 | // Schemes: http 6 | // Host: api.recipes.io:8080 7 | // BasePath: / 8 | // Version: 1.0.0 9 | // Contact: Mohamed Labouardy https://labouardy.com 10 | // SecurityDefinitions: 11 | // api_key: 12 | // type: apiKey 13 | // name: Authorization 14 | // in: header 15 | // 16 | // Consumes: 17 | // - application/json 18 | // 19 | // Produces: 20 | // - application/json 21 | // swagger:meta 22 | package main 23 | 24 | import ( 25 | "context" 26 | "log" 27 | "os" 28 | 29 | "github.com/gin-contrib/sessions" 30 | redisStore "github.com/gin-contrib/sessions/redis" 31 | "github.com/gin-gonic/gin" 32 | "github.com/go-redis/redis" 33 | handlers "github.com/mlabouardy/recipes-api/handlers" 34 | "go.mongodb.org/mongo-driver/mongo" 35 | "go.mongodb.org/mongo-driver/mongo/options" 36 | "go.mongodb.org/mongo-driver/mongo/readpref" 37 | ) 38 | 39 | var authHandler *handlers.AuthHandler 40 | var recipesHandler *handlers.RecipesHandler 41 | 42 | func init() { 43 | ctx := context.Background() 44 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 45 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 46 | log.Fatal(err) 47 | } 48 | log.Println("Connected to MongoDB") 49 | collectionRecipes := client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 50 | 51 | redisClient := redis.NewClient(&redis.Options{ 52 | Addr: "localhost:6379", 53 | Password: "", 54 | DB: 0, 55 | }) 56 | 57 | status := redisClient.Ping() 58 | log.Println(status) 59 | 60 | recipesHandler = handlers.NewRecipesHandler(ctx, collectionRecipes, redisClient) 61 | 62 | collectionUsers := client.Database(os.Getenv("MONGO_DATABASE")).Collection("users") 63 | authHandler = handlers.NewAuthHandler(ctx, collectionUsers) 64 | } 65 | 66 | func main() { 67 | router := gin.Default() 68 | 69 | store, _ := redisStore.NewStore(10, "tcp", "localhost:6379", "", []byte("secret")) 70 | router.Use(sessions.Sessions("recipes_api", store)) 71 | 72 | router.GET("/recipes", recipesHandler.ListRecipesHandler) 73 | 74 | router.POST("/signin", authHandler.SignInHandler) 75 | router.POST("/refresh", authHandler.RefreshHandler) 76 | router.POST("/signout", authHandler.SignOutHandler) 77 | 78 | authorized := router.Group("/") 79 | authorized.Use(authHandler.AuthMiddleware()) 80 | { 81 | authorized.POST("/recipes", recipesHandler.NewRecipeHandler) 82 | authorized.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler) 83 | authorized.DELETE("/recipes/:id", recipesHandler.DeleteRecipeHandler) 84 | authorized.GET("/recipes/:id", recipesHandler.GetOneRecipeHandler) 85 | } 86 | 87 | router.Run() 88 | } 89 | -------------------------------------------------------------------------------- /chapter04/cookies/models/recipe.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // swagger:parameters recipes newRecipe 10 | type Recipe struct { 11 | //swagger:ignore 12 | ID primitive.ObjectID `json:"id" bson:"_id"` 13 | Name string `json:"name" bson:"name"` 14 | Tags []string `json:"tags" bson:"tags"` 15 | Ingredients []string `json:"ingredients" bson:"ingredients"` 16 | Instructions []string `json:"instructions" bson:"instructions"` 17 | PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"` 18 | } 19 | -------------------------------------------------------------------------------- /chapter04/cookies/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // swagger:parameters auth signIn 4 | type User struct { 5 | Password string `json:"password"` 6 | Username string `json:"username"` 7 | } 8 | -------------------------------------------------------------------------------- /chapter04/users/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "log" 7 | "os" 8 | 9 | "go.mongodb.org/mongo-driver/bson" 10 | "go.mongodb.org/mongo-driver/mongo" 11 | "go.mongodb.org/mongo-driver/mongo/options" 12 | "go.mongodb.org/mongo-driver/mongo/readpref" 13 | ) 14 | 15 | func main() { 16 | users := map[string]string{ 17 | "admin": "fCRmh4Q2J7Rseqkz", 18 | "packt": "RE4zfHB35VPtTkbT", 19 | "mlabouardy": "L3nSFRcZzNQ67bcc", 20 | } 21 | 22 | ctx := context.Background() 23 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 24 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | collection := client.Database(os.Getenv("MONGO_DATABASE")).Collection("users") 29 | h := sha256.New() 30 | 31 | for username, password := range users { 32 | collection.InsertOne(ctx, bson.M{ 33 | "username": username, 34 | "password": string(h.Sum([]byte(password))), 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /chapter05/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/.DS_Store -------------------------------------------------------------------------------- /chapter05/api/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlabouardy/recipes-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gin-contrib/cors v1.3.1 // indirect 8 | github.com/gin-gonic/gin v1.6.3 9 | github.com/go-redis/redis v6.15.9+incompatible 10 | github.com/go-redis/redis/v8 v8.4.10 11 | go.mongodb.org/mongo-driver v1.4.5 12 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb 13 | ) 14 | -------------------------------------------------------------------------------- /chapter05/api/handlers/auth.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "crypto/sha256" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/dgrijalva/jwt-go" 10 | "github.com/gin-gonic/gin" 11 | "github.com/mlabouardy/recipes-api/models" 12 | "go.mongodb.org/mongo-driver/bson" 13 | "go.mongodb.org/mongo-driver/mongo" 14 | "golang.org/x/net/context" 15 | ) 16 | 17 | type AuthHandler struct { 18 | collection *mongo.Collection 19 | ctx context.Context 20 | } 21 | 22 | type Claims struct { 23 | Username string `json:"username"` 24 | jwt.StandardClaims 25 | } 26 | 27 | type JWTOutput struct { 28 | Token string `json:"token"` 29 | Expires time.Time `json:"expires"` 30 | } 31 | 32 | func NewAuthHandler(ctx context.Context, collection *mongo.Collection) *AuthHandler { 33 | return &AuthHandler{ 34 | collection: collection, 35 | ctx: ctx, 36 | } 37 | } 38 | 39 | func (handler *AuthHandler) SignInHandler(c *gin.Context) { 40 | var user models.User 41 | if err := c.ShouldBindJSON(&user); err != nil { 42 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 43 | return 44 | } 45 | 46 | h := sha256.New() 47 | 48 | cur := handler.collection.FindOne(handler.ctx, bson.M{ 49 | "username": user.Username, 50 | "password": string(h.Sum([]byte(user.Password))), 51 | }) 52 | if cur.Err() != nil { 53 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) 54 | return 55 | } 56 | 57 | expirationTime := time.Now().Add(10 * time.Minute) 58 | claims := &Claims{ 59 | Username: user.Username, 60 | StandardClaims: jwt.StandardClaims{ 61 | ExpiresAt: expirationTime.Unix(), 62 | }, 63 | } 64 | 65 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 66 | tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) 67 | if err != nil { 68 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 69 | return 70 | } 71 | 72 | jwtOutput := JWTOutput{ 73 | Token: tokenString, 74 | Expires: expirationTime, 75 | } 76 | c.JSON(http.StatusOK, jwtOutput) 77 | } 78 | 79 | func (handler *AuthHandler) RefreshHandler(c *gin.Context) { 80 | tokenValue := c.GetHeader("Authorization") 81 | claims := &Claims{} 82 | tkn, err := jwt.ParseWithClaims(tokenValue, claims, func(token *jwt.Token) (interface{}, error) { 83 | return []byte(os.Getenv("JWT_SECRET")), nil 84 | }) 85 | if err != nil { 86 | c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) 87 | return 88 | } 89 | if !tkn.Valid { 90 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) 91 | return 92 | } 93 | 94 | if time.Unix(claims.ExpiresAt, 0).Sub(time.Now()) > 30*time.Second { 95 | c.JSON(http.StatusBadRequest, gin.H{"error": "Token is not expired yet"}) 96 | return 97 | } 98 | 99 | expirationTime := time.Now().Add(5 * time.Minute) 100 | claims.ExpiresAt = expirationTime.Unix() 101 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 102 | tokenString, err := token.SignedString(os.Getenv("JWT_SECRET")) 103 | if err != nil { 104 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 105 | return 106 | } 107 | 108 | jwtOutput := JWTOutput{ 109 | Token: tokenString, 110 | Expires: expirationTime, 111 | } 112 | c.JSON(http.StatusOK, jwtOutput) 113 | } 114 | 115 | func (handler *AuthHandler) AuthMiddleware() gin.HandlerFunc { 116 | return func(c *gin.Context) { 117 | tokenValue := c.GetHeader("Authorization") 118 | claims := &Claims{} 119 | 120 | tkn, err := jwt.ParseWithClaims(tokenValue, claims, func(token *jwt.Token) (interface{}, error) { 121 | return []byte(os.Getenv("JWT_SECRET")), nil 122 | }) 123 | if err != nil { 124 | c.AbortWithStatus(http.StatusUnauthorized) 125 | } 126 | if !tkn.Valid { 127 | c.AbortWithStatus(http.StatusUnauthorized) 128 | } 129 | c.Next() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /chapter05/api/main.go: -------------------------------------------------------------------------------- 1 | // Recipes API 2 | // 3 | // This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin. 4 | // 5 | // Schemes: http 6 | // Host: localhost:8080 7 | // BasePath: / 8 | // Version: 1.0.0 9 | // Contact: Mohamed Labouardy https://labouardy.com 10 | // 11 | // Consumes: 12 | // - application/json 13 | // 14 | // Produces: 15 | // - application/json 16 | // swagger:meta 17 | package main 18 | 19 | import ( 20 | "context" 21 | "log" 22 | "os" 23 | 24 | "github.com/gin-contrib/cors" 25 | "github.com/gin-gonic/gin" 26 | "github.com/go-redis/redis" 27 | handlers "github.com/mlabouardy/recipes-api/handlers" 28 | "go.mongodb.org/mongo-driver/mongo" 29 | "go.mongodb.org/mongo-driver/mongo/options" 30 | "go.mongodb.org/mongo-driver/mongo/readpref" 31 | ) 32 | 33 | var authHandler *handlers.AuthHandler 34 | var recipesHandler *handlers.RecipesHandler 35 | 36 | func init() { 37 | ctx := context.Background() 38 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 39 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 40 | log.Fatal(err) 41 | } 42 | log.Println("Connected to MongoDB") 43 | collectionRecipes := client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 44 | 45 | redisClient := redis.NewClient(&redis.Options{ 46 | Addr: "localhost:6379", 47 | Password: "", 48 | DB: 0, 49 | }) 50 | 51 | status := redisClient.Ping() 52 | log.Println(status) 53 | 54 | recipesHandler = handlers.NewRecipesHandler(ctx, collectionRecipes, redisClient) 55 | 56 | collectionUsers := client.Database(os.Getenv("MONGO_DATABASE")).Collection("users") 57 | authHandler = handlers.NewAuthHandler(ctx, collectionUsers) 58 | } 59 | 60 | func main() { 61 | router := gin.Default() 62 | router.Use(cors.Default()) 63 | 64 | router.GET("/recipes", recipesHandler.ListRecipesHandler) 65 | 66 | router.POST("/signin", authHandler.SignInHandler) 67 | router.POST("/refresh", authHandler.RefreshHandler) 68 | 69 | authorized := router.Group("/") 70 | authorized.Use(authHandler.AuthMiddleware()) 71 | { 72 | authorized.POST("/recipes", recipesHandler.NewRecipeHandler) 73 | authorized.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler) 74 | authorized.DELETE("/recipes/:id", recipesHandler.DeleteRecipeHandler) 75 | authorized.GET("/recipes/:id", recipesHandler.GetOneRecipeHandler) 76 | } 77 | 78 | router.Run() 79 | } 80 | -------------------------------------------------------------------------------- /chapter05/api/models/recipe.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // swagger:parameters recipes newRecipe 10 | type Recipe struct { 11 | //swagger:ignore 12 | ID primitive.ObjectID `json:"id" bson:"_id"` 13 | Name string `json:"name" bson:"name"` 14 | Tags []string `json:"tags" bson:"tags"` 15 | Ingredients []string `json:"ingredients" bson:"ingredients"` 16 | Instructions []string `json:"instructions" bson:"instructions"` 17 | PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"` 18 | } 19 | -------------------------------------------------------------------------------- /chapter05/api/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | Password string `json:"password"` 5 | Username string `json:"username"` 6 | } 7 | -------------------------------------------------------------------------------- /chapter05/api/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumes": [ 3 | "application/json" 4 | ], 5 | "produces": [ 6 | "application/json" 7 | ], 8 | "schemes": [ 9 | "http" 10 | ], 11 | "swagger": "2.0", 12 | "info": { 13 | "description": "This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin.", 14 | "title": "Recipes API", 15 | "contact": { 16 | "name": "Mohamed Labouardy", 17 | "url": "https://labouardy.com", 18 | "email": "mohamed@labouardy.com" 19 | }, 20 | "version": "1.0.0" 21 | }, 22 | "host": "localhost:8080", 23 | "basePath": "/", 24 | "paths": { 25 | "/recipes": { 26 | "get": { 27 | "description": "Returns list of recipes", 28 | "produces": [ 29 | "application/json" 30 | ], 31 | "tags": [ 32 | "recipes" 33 | ], 34 | "operationId": "listRecipes", 35 | "responses": { 36 | "200": { 37 | "description": "Successful operation" 38 | } 39 | } 40 | }, 41 | "post": { 42 | "description": "Create a new recipe", 43 | "produces": [ 44 | "application/json" 45 | ], 46 | "tags": [ 47 | "recipes" 48 | ], 49 | "operationId": "newRecipe", 50 | "responses": { 51 | "200": { 52 | "description": "Successful operation" 53 | }, 54 | "400": { 55 | "description": "Invalid input" 56 | } 57 | } 58 | } 59 | }, 60 | "/recipes/search": { 61 | "get": { 62 | "description": "Search recipes based on tags", 63 | "produces": [ 64 | "application/json" 65 | ], 66 | "tags": [ 67 | "recipes" 68 | ], 69 | "operationId": "findRecipe", 70 | "parameters": [ 71 | { 72 | "type": "string", 73 | "description": "recipe tag", 74 | "name": "tag", 75 | "in": "query", 76 | "required": true 77 | } 78 | ], 79 | "responses": { 80 | "200": { 81 | "description": "Successful operation" 82 | } 83 | } 84 | } 85 | }, 86 | "/recipes/{id}": { 87 | "put": { 88 | "description": "Update an existing recipe", 89 | "produces": [ 90 | "application/json" 91 | ], 92 | "tags": [ 93 | "recipes" 94 | ], 95 | "operationId": "updateRecipe", 96 | "parameters": [ 97 | { 98 | "type": "string", 99 | "description": "ID of the recipe", 100 | "name": "id", 101 | "in": "path", 102 | "required": true 103 | } 104 | ], 105 | "responses": { 106 | "200": { 107 | "description": "Successful operation" 108 | }, 109 | "400": { 110 | "description": "Invalid input" 111 | }, 112 | "404": { 113 | "description": "Invalid recipe ID" 114 | } 115 | } 116 | }, 117 | "delete": { 118 | "description": "Delete an existing recipe", 119 | "produces": [ 120 | "application/json" 121 | ], 122 | "tags": [ 123 | "recipes" 124 | ], 125 | "operationId": "deleteRecipe", 126 | "parameters": [ 127 | { 128 | "type": "string", 129 | "description": "ID of the recipe", 130 | "name": "id", 131 | "in": "path", 132 | "required": true 133 | } 134 | ], 135 | "responses": { 136 | "200": { 137 | "description": "Successful operation" 138 | }, 139 | "404": { 140 | "description": "Invalid recipe ID" 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /chapter05/embed/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Recipe not found 5 | 6 | 8 | 9 | 10 | 11 |
12 |

Recipe not found 😔

13 | 14 |
15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /chapter05/embed/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/.DS_Store -------------------------------------------------------------------------------- /chapter05/embed/assets/css/app.css: -------------------------------------------------------------------------------- 1 | .recipes { 2 | width: 100%; 3 | } 4 | 5 | .recipe { 6 | margin-top: 40px; 7 | } 8 | 9 | .list-steps { 10 | overflow: scroll; 11 | -webkit-overflow-scrolling: scroll; 12 | margin-top: 10px; 13 | } 14 | 15 | .list-steps li { 16 | font-size: 0.8rem; 17 | } 18 | 19 | .steps { 20 | max-height: 200px; 21 | overflow: scroll; 22 | -webkit-overflow-scrolling: scroll; 23 | margin-top: 10px; 24 | } 25 | 26 | .steps li { 27 | font-size: 0.8rem; 28 | } 29 | 30 | .card { 31 | margin-top: 20px; 32 | } 33 | 34 | .badge { 35 | margin-top: 3px; 36 | } 37 | 38 | .logo img { 39 | width: 30px; 40 | } 41 | 42 | .logo span { 43 | color: #36A88E; 44 | font-weight: bolder; 45 | font-style: italic; 46 | } 47 | 48 | .not-found { 49 | text-align: center; 50 | } 51 | 52 | .not-found h4 { 53 | margin-top: 30px; 54 | } -------------------------------------------------------------------------------- /chapter05/embed/assets/images/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/404.jpg -------------------------------------------------------------------------------- /chapter05/embed/assets/images/blueberry-crumb-bars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/blueberry-crumb-bars.jpg -------------------------------------------------------------------------------- /chapter05/embed/assets/images/burger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/burger.jpg -------------------------------------------------------------------------------- /chapter05/embed/assets/images/crock-pot-roast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/crock-pot-roast.jpg -------------------------------------------------------------------------------- /chapter05/embed/assets/images/curried-chicken-lentils-and-rice.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/curried-chicken-lentils-and-rice.jpeg -------------------------------------------------------------------------------- /chapter05/embed/assets/images/curry-chicken-salad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/curry-chicken-salad.jpg -------------------------------------------------------------------------------- /chapter05/embed/assets/images/oatmeal-cookies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/oatmeal-cookies.jpg -------------------------------------------------------------------------------- /chapter05/embed/assets/images/pizza.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/pizza.jpg -------------------------------------------------------------------------------- /chapter05/embed/assets/images/roasted-asparagus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/roasted-asparagus.jpg -------------------------------------------------------------------------------- /chapter05/embed/assets/images/stuffed-cornsquash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/stuffed-cornsquash.jpg -------------------------------------------------------------------------------- /chapter05/embed/assets/images/tacos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/tacos.jpg -------------------------------------------------------------------------------- /chapter05/embed/assets/images/yorkshire_pudding.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/embed/assets/images/yorkshire_pudding.jpg -------------------------------------------------------------------------------- /chapter05/embed/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | "html/template" 7 | "io/fs" 8 | "net/http" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type Recipe struct { 14 | ID string `json:"id"` 15 | Name string `json:"name"` 16 | Ingredients []Ingredient `json:"ingredients"` 17 | Steps []string `json:"steps"` 18 | Picture string `json:"imageURL"` 19 | } 20 | 21 | type Ingredient struct { 22 | Quantity string `json:"quantity"` 23 | Name string `json:"name"` 24 | Type string `json:"type"` 25 | } 26 | 27 | //go:embed assets/* templates/* 404.html recipes.json 28 | var f embed.FS 29 | var recipes []Recipe 30 | 31 | func IndexHandler(c *gin.Context) { 32 | c.HTML(http.StatusOK, "index.tmpl", gin.H{ 33 | "recipes": recipes, 34 | }) 35 | } 36 | 37 | func RecipeHandler(c *gin.Context) { 38 | for _, recipe := range recipes { 39 | if recipe.ID == c.Param("id") { 40 | c.HTML(http.StatusOK, "recipe.tmpl", gin.H{ 41 | "recipe": recipe, 42 | }) 43 | return 44 | } 45 | } 46 | c.File("404.html") 47 | } 48 | 49 | func init() { 50 | recipes = make([]Recipe, 0) 51 | data, _ := f.ReadFile("recipes.json") 52 | json.Unmarshal(data, &recipes) 53 | } 54 | 55 | func main() { 56 | templ := template.Must(template.New("").ParseFS(f, "templates/*.tmpl")) 57 | 58 | fsys, err := fs.Sub(f, "assets") 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | router := gin.Default() 64 | router.SetHTMLTemplate(templ) 65 | router.StaticFS("/assets", http.FS(fsys)) 66 | router.GET("/", IndexHandler) 67 | router.GET("/recipes/:id", RecipeHandler) 68 | router.Run() 69 | } 70 | -------------------------------------------------------------------------------- /chapter05/embed/templates/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Recipes 5 | 6 | 8 | 9 | 10 | 11 | {{template "navbar.tmpl"}} 12 | 13 |
14 |
15 | {{range .recipes}} 16 |
17 |
18 | ... 19 |
20 |
{{ .Name }}
21 | {{range $ingredient := .Ingredients}} 22 | 23 | {{$ingredient.Name}} 24 | 25 | {{end}} 26 |
    27 | {{range $step := .Steps}} 28 |
  • {{$step}}
  • 29 | {{end}} 30 |
31 | See recipe 32 |
33 |
34 |
35 | {{end}} 36 |
37 |
38 | 39 | 42 | 43 | -------------------------------------------------------------------------------- /chapter05/embed/templates/navbar.tmpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter05/embed/templates/recipe.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ .recipe.Name }} - Recipes 5 | 6 | 8 | 9 | 10 | 11 | {{template "navbar.tmpl"}} 12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |

{{ .recipe.Name }}

20 |
    21 |
  • Steps
  • 22 | {{range $step := .recipe.Steps }} 23 |
  • {{$step}}
  • 24 | {{end}} 25 |
26 |
27 |
28 |
29 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /chapter05/go-assets/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Recipe not found 5 | 6 | 8 | 9 | 10 | 11 |
12 |

Recipe not found 😔

13 | 14 |
15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /chapter05/go-assets/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/.DS_Store -------------------------------------------------------------------------------- /chapter05/go-assets/assets/css/app.css: -------------------------------------------------------------------------------- 1 | .recipes { 2 | width: 100%; 3 | } 4 | 5 | .recipe { 6 | margin-top: 40px; 7 | } 8 | 9 | .list-steps { 10 | overflow: scroll; 11 | -webkit-overflow-scrolling: scroll; 12 | margin-top: 10px; 13 | } 14 | 15 | .list-steps li { 16 | font-size: 0.8rem; 17 | } 18 | 19 | .steps { 20 | max-height: 200px; 21 | overflow: scroll; 22 | -webkit-overflow-scrolling: scroll; 23 | margin-top: 10px; 24 | } 25 | 26 | .steps li { 27 | font-size: 0.8rem; 28 | } 29 | 30 | .card { 31 | margin-top: 20px; 32 | } 33 | 34 | .badge { 35 | margin-top: 3px; 36 | } 37 | 38 | .logo img { 39 | width: 30px; 40 | } 41 | 42 | .logo span { 43 | color: #36A88E; 44 | font-weight: bolder; 45 | font-style: italic; 46 | } 47 | 48 | .not-found { 49 | text-align: center; 50 | } 51 | 52 | .not-found h4 { 53 | margin-top: 30px; 54 | } -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/404.jpg -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/blueberry-crumb-bars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/blueberry-crumb-bars.jpg -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/burger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/burger.jpg -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/crock-pot-roast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/crock-pot-roast.jpg -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/curried-chicken-lentils-and-rice.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/curried-chicken-lentils-and-rice.jpeg -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/curry-chicken-salad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/curry-chicken-salad.jpg -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/oatmeal-cookies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/oatmeal-cookies.jpg -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/pizza.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/pizza.jpg -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/roasted-asparagus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/roasted-asparagus.jpg -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/stuffed-cornsquash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/stuffed-cornsquash.jpg -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/tacos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/tacos.jpg -------------------------------------------------------------------------------- /chapter05/go-assets/assets/images/yorkshire_pudding.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/go-assets/assets/images/yorkshire_pudding.jpg -------------------------------------------------------------------------------- /chapter05/go-assets/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "html/template" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type Recipe struct { 14 | ID string `json:"id"` 15 | Name string `json:"name"` 16 | Ingredients []Ingredient `json:"ingredients"` 17 | Steps []string `json:"steps"` 18 | Picture string `json:"imageURL"` 19 | } 20 | 21 | type Ingredient struct { 22 | Quantity string `json:"quantity"` 23 | Name string `json:"name"` 24 | Type string `json:"type"` 25 | } 26 | 27 | var recipes []Recipe 28 | 29 | func IndexHandler(c *gin.Context) { 30 | c.HTML(http.StatusOK, "/templates/index.tmpl", gin.H{ 31 | "recipes": recipes, 32 | }) 33 | } 34 | 35 | func RecipeHandler(c *gin.Context) { 36 | for _, recipe := range recipes { 37 | if recipe.ID == c.Param("id") { 38 | c.HTML(http.StatusOK, "/templates/recipe.tmpl", gin.H{ 39 | "recipe": recipe, 40 | }) 41 | return 42 | } 43 | } 44 | c.File("404.html") 45 | } 46 | 47 | func init() { 48 | recipes = make([]Recipe, 0) 49 | json.Unmarshal(Assets.Files["/recipes.json"].Data, &recipes) 50 | } 51 | 52 | func loadTemplate() (*template.Template, error) { 53 | t := template.New("") 54 | for name, file := range Assets.Files { 55 | if file.IsDir() || !strings.HasSuffix(name, ".tmpl") { 56 | continue 57 | } 58 | h, err := ioutil.ReadAll(file) 59 | if err != nil { 60 | return nil, err 61 | } 62 | t, err = t.New(name).Parse(string(h)) 63 | if err != nil { 64 | return nil, err 65 | } 66 | } 67 | return t, nil 68 | } 69 | 70 | func StaticHandler(c *gin.Context) { 71 | filepath := c.Param("filepath") 72 | data := Assets.Files["/assets"+filepath].Data 73 | c.Writer.Write(data) 74 | } 75 | 76 | func main() { 77 | t, err := loadTemplate() 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | router := gin.Default() 83 | router.SetHTMLTemplate(t) 84 | router.GET("/assets/*filepath", StaticHandler) 85 | router.GET("/", IndexHandler) 86 | router.GET("/recipes/:id", RecipeHandler) 87 | router.Run() 88 | } 89 | -------------------------------------------------------------------------------- /chapter05/go-assets/templates/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Recipes 5 | 6 | 8 | 9 | 10 | 11 | {{template "/templates/navbar.tmpl"}} 12 | 13 |
14 |
15 | {{range .recipes}} 16 |
17 |
18 | ... 19 |
20 |
{{ .Name }}
21 | {{range $ingredient := .Ingredients}} 22 | 23 | {{$ingredient.Name}} 24 | 25 | {{end}} 26 |
    27 | {{range $step := .Steps}} 28 |
  • {{$step}}
  • 29 | {{end}} 30 |
31 | See recipe 32 |
33 |
34 |
35 | {{end}} 36 |
37 |
38 | 39 | 42 | 43 | -------------------------------------------------------------------------------- /chapter05/go-assets/templates/navbar.tmpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter05/go-assets/templates/recipe.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ .recipe.Name }} - Recipes 5 | 6 | 8 | 9 | 10 | 11 | {{template "/templates/navbar.tmpl"}} 12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |

{{ .recipe.Name }}

20 |
    21 |
  • Steps
  • 22 | {{range $step := .recipe.Steps }} 23 |
  • {{$step}}
  • 24 | {{end}} 25 |
26 |
27 |
28 |
29 | 30 | 33 | 34 | -------------------------------------------------------------------------------- /chapter05/recipes-web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /chapter05/recipes-web/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /chapter05/recipes-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recipes-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@auth0/auth0-react": "^1.3.0", 7 | "@testing-library/jest-dom": "^5.11.9", 8 | "@testing-library/react": "^11.2.5", 9 | "@testing-library/user-event": "^12.8.1", 10 | "bootstrap": "^4.6.0", 11 | "react": "^17.0.1", 12 | "react-dom": "^17.0.1", 13 | "react-scripts": "4.0.3", 14 | "web-vitals": "^1.1.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /chapter05/recipes-web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/recipes-web/public/favicon.ico -------------------------------------------------------------------------------- /chapter05/recipes-web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /chapter05/recipes-web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/recipes-web/public/logo192.png -------------------------------------------------------------------------------- /chapter05/recipes-web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter05/recipes-web/public/logo512.png -------------------------------------------------------------------------------- /chapter05/recipes-web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /chapter05/recipes-web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /chapter05/recipes-web/src/App.css: -------------------------------------------------------------------------------- 1 | .btn-recipe { 2 | margin: 20px; 3 | } -------------------------------------------------------------------------------- /chapter05/recipes-web/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import Recipe from './Recipe'; 4 | import Navbar from './Navbar'; 5 | 6 | class App extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | 10 | this.state = { 11 | recipes: [] 12 | } 13 | 14 | this.getRecipes(); 15 | } 16 | 17 | getRecipes() { 18 | fetch('http://localhost:8080/recipes') 19 | .then(response => response.json()) 20 | .then(data => this.setState({ recipes: data })); 21 | } 22 | 23 | render() { 24 | return (
25 | 26 | {this.state.recipes.map((recipe, index) => ( 27 | 28 | ))} 29 |
); 30 | } 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /chapter05/recipes-web/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /chapter05/recipes-web/src/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAuth0 } from "@auth0/auth0-react"; 3 | import Profile from './Profile'; 4 | 5 | const Navbar = () => { 6 | const { isAuthenticated, loginWithRedirect, logout, user } = useAuth0(); 7 | return ( 8 | 24 | ) 25 | } 26 | 27 | export default Navbar; -------------------------------------------------------------------------------- /chapter05/recipes-web/src/Profile.css: -------------------------------------------------------------------------------- 1 | .user img { 2 | width: 30px; 3 | margin-right: 5px; 4 | } 5 | 6 | .user span { 7 | font-size: 0.8rem; 8 | } -------------------------------------------------------------------------------- /chapter05/recipes-web/src/Profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAuth0 } from "@auth0/auth0-react"; 3 | import './Profile.css' 4 | 5 | const Profile = () => { 6 | const { user, logout } = useAuth0(); 7 | return ( 8 | 19 | ) 20 | } 21 | 22 | export default Profile; -------------------------------------------------------------------------------- /chapter05/recipes-web/src/Recipe.css: -------------------------------------------------------------------------------- 1 | .recipe { 2 | margin: 10px; 3 | } -------------------------------------------------------------------------------- /chapter05/recipes-web/src/Recipe.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Recipe.css'; 3 | 4 | class Recipe extends React.Component { 5 | render() { 6 | return ( 7 |
8 |

{this.props.recipe.name}

9 |
    10 | {this.props.recipe.ingredients.map((ingredient, index) => { 11 | return
  • {ingredient}
  • 12 | })} 13 |
14 |
15 | ) 16 | } 17 | } 18 | 19 | export default Recipe; -------------------------------------------------------------------------------- /chapter05/recipes-web/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /chapter05/recipes-web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import './index.css'; 5 | import 'bootstrap/dist/css/bootstrap.min.css'; 6 | import 'bootstrap/dist/js/bootstrap.min.js'; 7 | import { Auth0Provider } from "@auth0/auth0-react"; 8 | 9 | ReactDOM.render( 10 | 15 | 16 | , 17 | document.getElementById("root") 18 | ); 19 | -------------------------------------------------------------------------------- /chapter05/recipes-web/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter05/recipes-web/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /chapter05/recipes-web/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /chapter06/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | WORKDIR /go/src/github.com/api 3 | COPY . . 4 | RUN go mod download 5 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 6 | 7 | FROM alpine:latest 8 | RUN apk --no-cache add ca-certificates 9 | WORKDIR /root/ 10 | COPY --from=0 /go/src/github.com/api/app . 11 | CMD ["./app"] -------------------------------------------------------------------------------- /chapter06/api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | api: 5 | image: api 6 | environment: 7 | - MONGO_URI=mongodb://admin:password@mongodb:27017/test?authSource=admin&readPreference=primary&ssl=false 8 | - MONGO_DATABASE=demo 9 | - REDIS_URI=redis:6379 10 | networks: 11 | - api_network 12 | external_links: 13 | - mongodb 14 | - redis 15 | scale: 5 16 | 17 | dashboard: 18 | image: dashboard 19 | networks: 20 | - api_network 21 | 22 | redis: 23 | image: redis 24 | networks: 25 | - api_network 26 | ports: 27 | - 6379:6379 28 | 29 | mongodb: 30 | image: mongo:4.4.3 31 | networks: 32 | - api_network 33 | ports: 34 | - 27017:27017 35 | environment: 36 | - MONGO_INITDB_ROOT_USERNAME=admin 37 | - MONGO_INITDB_ROOT_PASSWORD=password 38 | 39 | nginx: 40 | image: nginx 41 | ports: 42 | - 80:80 43 | volumes: 44 | - $PWD/nginx.conf:/etc/nginx/nginx.conf 45 | depends_on: 46 | - api 47 | - dashboard 48 | networks: 49 | - api_network 50 | 51 | reddit-trending: 52 | image: web 53 | environment: 54 | - MONGO_URI=mongodb://admin:password@mongodb:27017/test?authSource=admin&readPreference=primary&ssl=false 55 | - MONGO_DATABASE=demo2 56 | networks: 57 | - api_network 58 | 59 | networks: 60 | api_network: -------------------------------------------------------------------------------- /chapter06/api/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlabouardy/recipes-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/go-redis/redis v6.15.9+incompatible 8 | github.com/go-redis/redis/v8 v8.4.10 9 | go.mongodb.org/mongo-driver v1.4.5 10 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb 11 | ) 12 | -------------------------------------------------------------------------------- /chapter06/api/main.go: -------------------------------------------------------------------------------- 1 | // Recipes API 2 | // 3 | // This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin. 4 | // 5 | // Schemes: http 6 | // Host: localhost:8080 7 | // BasePath: / 8 | // Version: 1.0.0 9 | // Contact: Mohamed Labouardy https://labouardy.com 10 | // 11 | // Consumes: 12 | // - application/json 13 | // 14 | // Produces: 15 | // - application/json 16 | // swagger:meta 17 | package main 18 | 19 | import ( 20 | "context" 21 | "log" 22 | "os" 23 | 24 | "github.com/gin-gonic/gin" 25 | "github.com/go-redis/redis" 26 | handlers "github.com/mlabouardy/recipes-api/handlers" 27 | "go.mongodb.org/mongo-driver/mongo" 28 | "go.mongodb.org/mongo-driver/mongo/options" 29 | "go.mongodb.org/mongo-driver/mongo/readpref" 30 | ) 31 | 32 | var recipesHandler *handlers.RecipesHandler 33 | 34 | func init() { 35 | ctx := context.Background() 36 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 37 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 38 | log.Fatal(err) 39 | } 40 | log.Println("Connected to MongoDB") 41 | collection := client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 42 | 43 | redisClient := redis.NewClient(&redis.Options{ 44 | Addr: os.Getenv("REDIS_URI"), 45 | Password: "", 46 | DB: 0, 47 | }) 48 | 49 | status := redisClient.Ping() 50 | log.Println(status) 51 | 52 | recipesHandler = handlers.NewRecipesHandler(ctx, collection, redisClient) 53 | } 54 | 55 | func main() { 56 | router := gin.Default() 57 | router.POST("/recipes", recipesHandler.NewRecipeHandler) 58 | router.GET("/recipes", recipesHandler.ListRecipesHandler) 59 | router.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler) 60 | router.DELETE("/recipes/:id", recipesHandler.DeleteRecipeHandler) 61 | router.GET("/recipes/:id", recipesHandler.GetOneRecipeHandler) 62 | router.Run() 63 | } 64 | -------------------------------------------------------------------------------- /chapter06/api/models/recipe.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // swagger:parameters recipes newRecipe 10 | type Recipe struct { 11 | //swagger:ignore 12 | ID primitive.ObjectID `json:"id" bson:"_id"` 13 | Name string `json:"name" bson:"name"` 14 | Tags []string `json:"tags" bson:"tags"` 15 | Ingredients []string `json:"ingredients" bson:"ingredients"` 16 | Instructions []string `json:"instructions" bson:"instructions"` 17 | PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"` 18 | } 19 | -------------------------------------------------------------------------------- /chapter06/api/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | map $sent_http_content_type $expires { 6 | default off; 7 | text/html epoch; 8 | text/css max; 9 | application/javascript max; 10 | ~image/ max; 11 | } 12 | 13 | http { 14 | server_tokens off; 15 | server { 16 | listen 80; 17 | expires $expires; 18 | 19 | location / { 20 | proxy_set_header X-Forwarded-For $remote_addr; 21 | proxy_set_header Host $http_host; 22 | proxy_pass http://dashboard:3000/; 23 | } 24 | 25 | location /reddit/ { 26 | proxy_set_header X-Forwarded-For $remote_addr; 27 | proxy_set_header Host $http_host; 28 | proxy_pass http://reddit-trending:3000/; 29 | } 30 | 31 | location /api/ { 32 | proxy_set_header X-Forwarded-For $remote_addr; 33 | proxy_set_header Host $http_host; 34 | proxy_pass http://api:8080/; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /chapter06/bulk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while IFS= read -r thread 4 | do 5 | printf "\n$thread\n" 6 | curl -X POST -H "Content-Type: application/json" -d '{"url":"'$thread'"}' http://localhost:5000/parse 7 | done < "threads" -------------------------------------------------------------------------------- /chapter06/caching/illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter06/caching/illustration.png -------------------------------------------------------------------------------- /chapter06/caching/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func IllustrationHandler(c *gin.Context) { 11 | c.Header("Etag", "illustration") 12 | c.Header("Cache-Control", "max-age=2592000") 13 | 14 | if match := c.GetHeader("If-None-Match"); match != "" { 15 | if strings.Contains(match, "illustration") { 16 | c.Writer.WriteHeader(http.StatusNotModified) 17 | return 18 | } 19 | } 20 | 21 | c.File("illustration.png") 22 | } 23 | 24 | func main() { 25 | router := gin.Default() 26 | router.GET("/illustration", IllustrationHandler) 27 | router.Run(":3000") 28 | } 29 | -------------------------------------------------------------------------------- /chapter06/consumer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | WORKDIR /go/src/github.com/worker 3 | COPY main.go go.mod go.sum . 4 | RUN go mod download 5 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 6 | 7 | FROM alpine:latest 8 | RUN apk --no-cache add ca-certificates 9 | WORKDIR /root/ 10 | COPY --from=0 /go/src/github.com/worker/app . 11 | CMD ["./app"] -------------------------------------------------------------------------------- /chapter06/consumer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | worker: 5 | image: worker 6 | environment: 7 | - MONGO_URI=mongodb://admin:password@mongodb:27017/test?authSource=admin&readPreference=primary&ssl=false 8 | - MONGO_DATABASE=demo2 9 | - RABBITMQ_URI=amqp://user:password@rabbitmq:5672 10 | - RABBITMQ_QUEUE=rss_urls 11 | networks: 12 | - app_network 13 | external_links: 14 | - mongodb 15 | - rabbitmq 16 | 17 | networks: 18 | app_network: 19 | external: true -------------------------------------------------------------------------------- /chapter06/consumer/go.mod: -------------------------------------------------------------------------------- 1 | module consumer 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/streadway/amqp v1.0.0 7 | go.mongodb.org/mongo-driver v1.5.0 8 | ) 9 | -------------------------------------------------------------------------------- /chapter06/consumer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "encoding/xml" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "os" 12 | 13 | "github.com/streadway/amqp" 14 | "go.mongodb.org/mongo-driver/bson" 15 | "go.mongodb.org/mongo-driver/mongo" 16 | "go.mongodb.org/mongo-driver/mongo/options" 17 | ) 18 | 19 | type Request struct { 20 | URL string `json:"url"` 21 | } 22 | 23 | type Feed struct { 24 | Entries []Entry `xml:"entry"` 25 | } 26 | 27 | type Entry struct { 28 | Link struct { 29 | Href string `xml:"href,attr"` 30 | } `xml:"link"` 31 | Thumbnail struct { 32 | URL string `xml:"url,attr"` 33 | } `xml:"thumbnail"` 34 | Title string `xml:"title"` 35 | } 36 | 37 | func GetFeedEntries(url string) ([]Entry, error) { 38 | httpClient := &http.Client{} 39 | req, err := http.NewRequest("GET", url, nil) 40 | if err != nil { 41 | return nil, err 42 | } 43 | req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36") 44 | 45 | resp, err := httpClient.Do(req) 46 | if err != nil { 47 | return nil, err 48 | } 49 | defer resp.Body.Close() 50 | 51 | byteValue, _ := ioutil.ReadAll(resp.Body) 52 | var feed Feed 53 | xml.Unmarshal(byteValue, &feed) 54 | 55 | return feed.Entries, nil 56 | } 57 | 58 | func main() { 59 | ctx := context.Background() 60 | mongoClient, _ := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 61 | defer mongoClient.Disconnect(ctx) 62 | 63 | amqpConnection, err := amqp.Dial(os.Getenv("RABBITMQ_URI")) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | defer amqpConnection.Close() 68 | 69 | channelAmqp, _ := amqpConnection.Channel() 70 | defer channelAmqp.Close() 71 | 72 | forever := make(chan bool) 73 | 74 | msgs, err := channelAmqp.Consume( 75 | os.Getenv("RABBITMQ_QUEUE"), 76 | "", 77 | true, 78 | false, 79 | false, 80 | false, 81 | nil, 82 | ) 83 | 84 | go func() { 85 | for d := range msgs { 86 | log.Printf("Received a message: %s", d.Body) 87 | 88 | var request Request 89 | json.Unmarshal(d.Body, &request) 90 | 91 | log.Println("RSS URL:", request.URL) 92 | 93 | entries, _ := GetFeedEntries(request.URL) 94 | 95 | fmt.Println(entries) 96 | 97 | collection := mongoClient.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 98 | fmt.Println(len(entries)) 99 | for _, entry := range entries { 100 | collection.InsertOne(ctx, bson.M{ 101 | "title": entry.Title, 102 | "thumbnail": entry.Thumbnail.URL, 103 | "url": entry.Link.Href, 104 | }) 105 | } 106 | } 107 | }() 108 | 109 | log.Printf(" [*] Waiting for messages. To exit press CTRL+C") 110 | <-forever 111 | } 112 | -------------------------------------------------------------------------------- /chapter06/dashboard/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | WORKDIR /go/src/github.com/api 3 | COPY . . 4 | RUN go mod download 5 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 6 | 7 | FROM alpine:latest 8 | RUN apk --no-cache add ca-certificates 9 | WORKDIR /root/ 10 | COPY --from=0 /go/src/github.com/api/app . 11 | COPY templates templates/ 12 | COPY assets assets/ 13 | CMD ["./app"] -------------------------------------------------------------------------------- /chapter06/dashboard/assets/css/app.css: -------------------------------------------------------------------------------- 1 | .thumbnail { 2 | width: 50px !important; 3 | } 4 | 5 | .title { 6 | font-size: 1rem !important; 7 | } 8 | 9 | .see_recipe { 10 | float: right !important; 11 | } 12 | 13 | .header { 14 | margin-top: 20px !important; 15 | margin-bottom: 20px !important; 16 | } -------------------------------------------------------------------------------- /chapter06/dashboard/go.mod: -------------------------------------------------------------------------------- 1 | module dashboard 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | go.mongodb.org/mongo-driver v1.5.0 8 | ) 9 | -------------------------------------------------------------------------------- /chapter06/dashboard/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/gin-gonic/gin" 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | "go.mongodb.org/mongo-driver/mongo/readpref" 14 | ) 15 | 16 | var collection *mongo.Collection 17 | var ctx context.Context 18 | 19 | type Recipe struct { 20 | Title string `json:"title" bson:"title"` 21 | Thumbnail string `json:"thumbnail" bson:"thumbnail"` 22 | URL string `json:"url" bson:"url"` 23 | } 24 | 25 | func IndexHandler(c *gin.Context) { 26 | cur, err := collection.Find(ctx, bson.M{}) 27 | if err != nil { 28 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 29 | return 30 | } 31 | defer cur.Close(ctx) 32 | 33 | recipes := make([]Recipe, 0) 34 | for cur.Next(ctx) { 35 | var recipe Recipe 36 | cur.Decode(&recipe) 37 | recipes = append(recipes, recipe) 38 | } 39 | 40 | c.HTML(http.StatusOK, "index.tmpl", gin.H{ 41 | "recipes": recipes, 42 | }) 43 | } 44 | 45 | func init() { 46 | ctx = context.Background() 47 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 48 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 49 | log.Fatal(err) 50 | } 51 | log.Println("Connected to MongoDB") 52 | collection = client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 53 | } 54 | 55 | func main() { 56 | router := gin.Default() 57 | router.LoadHTMLGlob("templates/*") 58 | router.Static("/assets", "./assets") 59 | router.GET("/dashboard", IndexHandler) 60 | router.Run(":3000") 61 | } 62 | -------------------------------------------------------------------------------- /chapter06/dashboard/templates/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Trending Recipes 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Trending recipes 😋

13 |
    14 | {{range .recipes}} 15 |
  • 16 |
    17 | 18 | {{ .Title }} 19 | See recipe 20 |
    21 |
  • 22 | {{end}} 23 |
24 | 25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /chapter06/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | api: 5 | image: api 6 | environment: 7 | - MONGO_URI=mongodb://admin:password@mongodb:27017/test?authSource=admin&readPreference=primary&ssl=false 8 | - MONGO_DATABASE=demo 9 | - REDIS_URI=redis:6379 10 | networks: 11 | - api_network 12 | external_links: 13 | - mongodb 14 | - redis 15 | scale: 5 16 | 17 | dashboard: 18 | image: dashboard 19 | networks: 20 | - api_network 21 | 22 | redis: 23 | image: redis 24 | networks: 25 | - api_network 26 | ports: 27 | - 6379:6379 28 | 29 | mongodb: 30 | image: mongo:4.4.3 31 | networks: 32 | - api_network 33 | ports: 34 | - 27017:27017 35 | environment: 36 | - MONGO_INITDB_ROOT_USERNAME=admin 37 | - MONGO_INITDB_ROOT_PASSWORD=password 38 | 39 | nginx: 40 | image: nginx 41 | ports: 42 | - 80:80 43 | volumes: 44 | - $PWD/nginx.conf:/etc/nginx/nginx.conf 45 | depends_on: 46 | - api 47 | - dashboard 48 | networks: 49 | - api_network 50 | 51 | networks: 52 | api_network: -------------------------------------------------------------------------------- /chapter06/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | server_tokens off; 7 | server { 8 | listen 80; 9 | 10 | location / { 11 | proxy_set_header X-Forwarded-For $remote_addr; 12 | proxy_set_header Host $http_host; 13 | proxy_pass http://dashboard:3000/; 14 | } 15 | 16 | location /api/ { 17 | proxy_set_header X-Forwarded-For $remote_addr; 18 | proxy_set_header Host $http_host; 19 | proxy_pass http://api:8080/; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /chapter06/parser/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/xml" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/gin-gonic/gin" 11 | "go.mongodb.org/mongo-driver/bson" 12 | "go.mongodb.org/mongo-driver/mongo" 13 | "go.mongodb.org/mongo-driver/mongo/options" 14 | ) 15 | 16 | var client *mongo.Client 17 | var ctx context.Context 18 | 19 | type Request struct { 20 | URL string `json:"url"` 21 | } 22 | 23 | type Feed struct { 24 | Entries []Entry `xml:"entry"` 25 | } 26 | 27 | type Entry struct { 28 | Link struct { 29 | Href string `xml:"href,attr"` 30 | } `xml:"link"` 31 | Thumbnail struct { 32 | URL string `xml:"url,attr"` 33 | } `xml:"thumbnail"` 34 | Title string `xml:"title"` 35 | } 36 | 37 | func GetFeedEntries(url string) ([]Entry, error) { 38 | httpClient := &http.Client{} 39 | req, err := http.NewRequest("GET", url, nil) 40 | if err != nil { 41 | return nil, err 42 | } 43 | req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36") 44 | 45 | resp, err := httpClient.Do(req) 46 | if err != nil { 47 | return nil, err 48 | } 49 | defer resp.Body.Close() 50 | 51 | byteValue, _ := ioutil.ReadAll(resp.Body) 52 | var feed Feed 53 | xml.Unmarshal(byteValue, &feed) 54 | 55 | return feed.Entries, nil 56 | } 57 | 58 | func ParserHandler(c *gin.Context) { 59 | var request Request 60 | if err := c.ShouldBindJSON(&request); err != nil { 61 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 62 | return 63 | } 64 | 65 | entries, err := GetFeedEntries(request.URL) 66 | if err != nil { 67 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Error while parsing the rss feed"}) 68 | return 69 | } 70 | 71 | collection := client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 72 | for _, entry := range entries[2:] { 73 | collection.InsertOne(ctx, bson.M{ 74 | "title": entry.Title, 75 | "thumbnail": entry.Thumbnail.URL, 76 | "url": entry.Link.Href, 77 | }) 78 | } 79 | 80 | c.JSON(http.StatusOK, entries) 81 | } 82 | 83 | func init() { 84 | ctx = context.Background() 85 | client, _ = mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 86 | } 87 | 88 | func main() { 89 | router := gin.Default() 90 | router.POST("/parse", ParserHandler) 91 | router.Run(":5000") 92 | } 93 | -------------------------------------------------------------------------------- /chapter06/producer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/streadway/amqp" 12 | ) 13 | 14 | var channelAmqp *amqp.Channel 15 | 16 | type Request struct { 17 | URL string `json:"url"` 18 | } 19 | 20 | func ParserHandler(c *gin.Context) { 21 | var request Request 22 | if err := c.ShouldBindJSON(&request); err != nil { 23 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 24 | return 25 | } 26 | 27 | data, _ := json.Marshal(request) 28 | err := channelAmqp.Publish( 29 | "", 30 | os.Getenv("RABBITMQ_QUEUE"), 31 | false, 32 | false, 33 | amqp.Publishing{ 34 | ContentType: "application/json", 35 | Body: []byte(data), 36 | }) 37 | if err != nil { 38 | fmt.Println(err) 39 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Error while publishing to RabbitMQ"}) 40 | return 41 | } 42 | 43 | c.JSON(http.StatusOK, map[string]string{"message": "success"}) 44 | } 45 | 46 | func init() { 47 | amqpConnection, err := amqp.Dial(os.Getenv("RABBITMQ_URI")) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | channelAmqp, _ = amqpConnection.Channel() 53 | } 54 | 55 | func main() { 56 | router := gin.Default() 57 | router.POST("/parse", ParserHandler) 58 | router.Run(":5000") 59 | } 60 | -------------------------------------------------------------------------------- /chapter06/threads: -------------------------------------------------------------------------------- 1 | https://www.reddit.com/r/recipes/.rss 2 | https://www.reddit.com/r/food/.rss 3 | https://www.reddit.com/r/Cooking/.rss 4 | https://www.reddit.com/r/IndianFood/.rss 5 | https://www.reddit.com/r/Baking/.rss 6 | https://www.reddit.com/r/vegan/.rss 7 | https://www.reddit.com/r/fastfood/.rss 8 | https://www.reddit.com/r/vegetarian/.rss 9 | https://www.reddit.com/r/cookingforbeginners/.rss 10 | https://www.reddit.com/r/MealPrepSunday/.rss 11 | https://www.reddit.com/r/EatCheapAndHealthy/.rss 12 | https://www.reddit.com/r/Cheap_Meals/.rss 13 | https://www.reddit.com/r/slowcooking/.rss 14 | https://www.reddit.com/r/AskCulinary/.rss 15 | https://www.reddit.com/r/fromscratch/.rss -------------------------------------------------------------------------------- /chapter06/web-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /chapter06/web-app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.15.1 2 | COPY package-lock.json . 3 | COPY package.json . 4 | RUN npm install 5 | COPY . . 6 | CMD npm start -------------------------------------------------------------------------------- /chapter06/web-app/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /chapter06/web-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recipes-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@auth0/auth0-react": "^1.3.0", 7 | "@testing-library/jest-dom": "^5.11.9", 8 | "@testing-library/react": "^11.2.5", 9 | "@testing-library/user-event": "^12.8.1", 10 | "bootstrap": "^4.6.0", 11 | "jquery": "^3.6.0", 12 | "popper.js": "^1.16.1", 13 | "react": "^17.0.1", 14 | "react-dom": "^17.0.1", 15 | "react-scripts": "4.0.3", 16 | "web-vitals": "^1.1.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /chapter06/web-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter06/web-app/public/favicon.ico -------------------------------------------------------------------------------- /chapter06/web-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /chapter06/web-app/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter06/web-app/public/logo192.png -------------------------------------------------------------------------------- /chapter06/web-app/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter06/web-app/public/logo512.png -------------------------------------------------------------------------------- /chapter06/web-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /chapter06/web-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /chapter06/web-app/src/App.css: -------------------------------------------------------------------------------- 1 | .btn-recipe { 2 | margin: 20px; 3 | } -------------------------------------------------------------------------------- /chapter06/web-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import Recipe from './Recipe'; 4 | import Navbar from './Navbar'; 5 | 6 | class App extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | 10 | this.state = { 11 | recipes: [] 12 | } 13 | 14 | this.getRecipes(); 15 | } 16 | 17 | getRecipes() { 18 | fetch('/api/recipes') 19 | .then(response => response.json()) 20 | .then(data => this.setState({ recipes: data })); 21 | } 22 | 23 | render() { 24 | return (
25 | 26 | {this.state.recipes.map((recipe, index) => ( 27 | 28 | ))} 29 |
); 30 | } 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /chapter06/web-app/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /chapter06/web-app/src/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAuth0 } from "@auth0/auth0-react"; 3 | import Profile from './Profile'; 4 | 5 | const Navbar = () => { 6 | const { isAuthenticated, loginWithRedirect, logout, user } = useAuth0(); 7 | return ( 8 | 24 | ) 25 | } 26 | 27 | export default Navbar; -------------------------------------------------------------------------------- /chapter06/web-app/src/Profile.css: -------------------------------------------------------------------------------- 1 | .user img { 2 | width: 30px; 3 | margin-right: 5px; 4 | } 5 | 6 | .user span { 7 | font-size: 0.8rem; 8 | } -------------------------------------------------------------------------------- /chapter06/web-app/src/Profile.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAuth0 } from "@auth0/auth0-react"; 3 | import './Profile.css' 4 | 5 | const Profile = () => { 6 | const { user, logout } = useAuth0(); 7 | return ( 8 | 19 | ) 20 | } 21 | 22 | export default Profile; -------------------------------------------------------------------------------- /chapter06/web-app/src/Recipe.css: -------------------------------------------------------------------------------- 1 | .recipe { 2 | margin: 10px; 3 | } -------------------------------------------------------------------------------- /chapter06/web-app/src/Recipe.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Recipe.css'; 3 | 4 | class Recipe extends React.Component { 5 | render() { 6 | return ( 7 |
8 |

{this.props.recipe.name}

9 |
    10 | {this.props.recipe.ingredients.map((ingredient, index) => { 11 | return
  • {ingredient}
  • 12 | })} 13 |
14 |
15 | ) 16 | } 17 | } 18 | 19 | export default Recipe; -------------------------------------------------------------------------------- /chapter06/web-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /chapter06/web-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import './index.css'; 5 | import 'bootstrap/dist/css/bootstrap.min.css'; 6 | import 'bootstrap/dist/js/bootstrap.min.js'; 7 | import { Auth0Provider } from "@auth0/auth0-react"; 8 | 9 | ReactDOM.render( 10 | 15 | 16 | , 17 | document.getElementById("root") 18 | ); 19 | -------------------------------------------------------------------------------- /chapter06/web-app/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter06/web-app/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /chapter06/web-app/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /chapter07/api-with-db/coverage.out: -------------------------------------------------------------------------------- 1 | mode: set 2 | github.com/mlabouardy/recipes-api/main.go:36.13,39.71 3 1 3 | github.com/mlabouardy/recipes-api/main.go:42.2,57.61 8 1 4 | github.com/mlabouardy/recipes-api/main.go:39.71,41.3 1 0 5 | github.com/mlabouardy/recipes-api/main.go:60.32,71.2 8 1 6 | github.com/mlabouardy/recipes-api/main.go:78.2,78.15 1 1 7 | github.com/mlabouardy/recipes-api/main.go:71.2,76.3 4 1 8 | github.com/mlabouardy/recipes-api/main.go:80.13,82.2 1 0 9 | -------------------------------------------------------------------------------- /chapter07/api-with-db/debug.test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Building-Distributed-Applications-in-Gin/2b4d2026c2020d4c0afadf866aa628b28a926f70/chapter07/api-with-db/debug.test -------------------------------------------------------------------------------- /chapter07/api-with-db/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | redis: 5 | image: redis 6 | ports: 7 | - 6379:6379 8 | 9 | mongodb: 10 | image: mongo:4.4.3 11 | ports: 12 | - 27017:27017 13 | environment: 14 | - MONGO_INITDB_ROOT_USERNAME=admin 15 | - MONGO_INITDB_ROOT_PASSWORD=password -------------------------------------------------------------------------------- /chapter07/api-with-db/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlabouardy/recipes-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gin-contrib/cors v1.3.1 8 | github.com/gin-gonic/gin v1.6.2 9 | github.com/go-redis/redis v6.15.9+incompatible 10 | github.com/go-redis/redis/v8 v8.4.10 11 | github.com/stretchr/testify v1.6.1 12 | go.mongodb.org/mongo-driver v1.4.5 13 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb 14 | ) 15 | -------------------------------------------------------------------------------- /chapter07/api-with-db/handlers/auth.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "crypto/sha256" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | "github.com/dgrijalva/jwt-go" 10 | "github.com/gin-gonic/gin" 11 | "github.com/mlabouardy/recipes-api/models" 12 | "go.mongodb.org/mongo-driver/bson" 13 | "go.mongodb.org/mongo-driver/mongo" 14 | "golang.org/x/net/context" 15 | ) 16 | 17 | type AuthHandler struct { 18 | collection *mongo.Collection 19 | ctx context.Context 20 | } 21 | 22 | type Claims struct { 23 | Username string `json:"username"` 24 | jwt.StandardClaims 25 | } 26 | 27 | type JWTOutput struct { 28 | Token string `json:"token"` 29 | Expires time.Time `json:"expires"` 30 | } 31 | 32 | func NewAuthHandler(ctx context.Context, collection *mongo.Collection) *AuthHandler { 33 | return &AuthHandler{ 34 | collection: collection, 35 | ctx: ctx, 36 | } 37 | } 38 | 39 | func (handler *AuthHandler) SignInHandler(c *gin.Context) { 40 | var user models.User 41 | if err := c.ShouldBindJSON(&user); err != nil { 42 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 43 | return 44 | } 45 | 46 | h := sha256.New() 47 | 48 | cur := handler.collection.FindOne(handler.ctx, bson.M{ 49 | "username": user.Username, 50 | "password": string(h.Sum([]byte(user.Password))), 51 | }) 52 | if cur.Err() != nil { 53 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) 54 | return 55 | } 56 | 57 | expirationTime := time.Now().Add(10 * time.Minute) 58 | claims := &Claims{ 59 | Username: user.Username, 60 | StandardClaims: jwt.StandardClaims{ 61 | ExpiresAt: expirationTime.Unix(), 62 | }, 63 | } 64 | 65 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 66 | tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) 67 | if err != nil { 68 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 69 | return 70 | } 71 | 72 | jwtOutput := JWTOutput{ 73 | Token: tokenString, 74 | Expires: expirationTime, 75 | } 76 | c.JSON(http.StatusOK, jwtOutput) 77 | } 78 | 79 | func (handler *AuthHandler) RefreshHandler(c *gin.Context) { 80 | tokenValue := c.GetHeader("Authorization") 81 | claims := &Claims{} 82 | tkn, err := jwt.ParseWithClaims(tokenValue, claims, func(token *jwt.Token) (interface{}, error) { 83 | return []byte(os.Getenv("JWT_SECRET")), nil 84 | }) 85 | if err != nil { 86 | c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) 87 | return 88 | } 89 | if !tkn.Valid { 90 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) 91 | return 92 | } 93 | 94 | if time.Unix(claims.ExpiresAt, 0).Sub(time.Now()) > 30*time.Second { 95 | c.JSON(http.StatusBadRequest, gin.H{"error": "Token is not expired yet"}) 96 | return 97 | } 98 | 99 | expirationTime := time.Now().Add(5 * time.Minute) 100 | claims.ExpiresAt = expirationTime.Unix() 101 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 102 | tokenString, err := token.SignedString(os.Getenv("JWT_SECRET")) 103 | if err != nil { 104 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 105 | return 106 | } 107 | 108 | jwtOutput := JWTOutput{ 109 | Token: tokenString, 110 | Expires: expirationTime, 111 | } 112 | c.JSON(http.StatusOK, jwtOutput) 113 | } 114 | 115 | func (handler *AuthHandler) AuthMiddleware() gin.HandlerFunc { 116 | return func(c *gin.Context) { 117 | tokenValue := c.GetHeader("Authorization") 118 | claims := &Claims{} 119 | 120 | tkn, err := jwt.ParseWithClaims(tokenValue, claims, func(token *jwt.Token) (interface{}, error) { 121 | return []byte(os.Getenv("JWT_SECRET")), nil 122 | }) 123 | if err != nil { 124 | c.AbortWithStatus(http.StatusUnauthorized) 125 | } 126 | if !tkn.Valid { 127 | c.AbortWithStatus(http.StatusUnauthorized) 128 | } 129 | c.Next() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /chapter07/api-with-db/main.go: -------------------------------------------------------------------------------- 1 | // Recipes API 2 | // 3 | // This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin. 4 | // 5 | // Schemes: http 6 | // Host: localhost:8080 7 | // BasePath: / 8 | // Version: 1.0.0 9 | // Contact: Mohamed Labouardy https://labouardy.com 10 | // 11 | // Consumes: 12 | // - application/json 13 | // 14 | // Produces: 15 | // - application/json 16 | // swagger:meta 17 | package main 18 | 19 | import ( 20 | "context" 21 | "log" 22 | "os" 23 | 24 | "github.com/gin-contrib/cors" 25 | "github.com/gin-gonic/gin" 26 | "github.com/go-redis/redis" 27 | handlers "github.com/mlabouardy/recipes-api/handlers" 28 | "go.mongodb.org/mongo-driver/mongo" 29 | "go.mongodb.org/mongo-driver/mongo/options" 30 | "go.mongodb.org/mongo-driver/mongo/readpref" 31 | ) 32 | 33 | var authHandler *handlers.AuthHandler 34 | var recipesHandler *handlers.RecipesHandler 35 | 36 | func init() { 37 | ctx := context.Background() 38 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 39 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 40 | log.Fatal(err) 41 | } 42 | log.Println("Connected to MongoDB") 43 | collectionRecipes := client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 44 | 45 | redisClient := redis.NewClient(&redis.Options{ 46 | Addr: "localhost:6379", 47 | Password: "", 48 | DB: 0, 49 | }) 50 | 51 | status := redisClient.Ping() 52 | log.Println(status) 53 | 54 | recipesHandler = handlers.NewRecipesHandler(ctx, collectionRecipes, redisClient) 55 | 56 | collectionUsers := client.Database(os.Getenv("MONGO_DATABASE")).Collection("users") 57 | authHandler = handlers.NewAuthHandler(ctx, collectionUsers) 58 | } 59 | 60 | func SetupServer() *gin.Engine { 61 | router := gin.Default() 62 | router.Use(cors.Default()) 63 | 64 | router.GET("/recipes", recipesHandler.ListRecipesHandler) 65 | 66 | router.POST("/signin", authHandler.SignInHandler) 67 | router.POST("/refresh", authHandler.RefreshHandler) 68 | 69 | authorized := router.Group("/") 70 | //authorized.Use(authHandler.AuthMiddleware()) 71 | //{ 72 | authorized.POST("/recipes", recipesHandler.NewRecipeHandler) 73 | authorized.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler) 74 | authorized.DELETE("/recipes/:id", recipesHandler.DeleteRecipeHandler) 75 | authorized.GET("/recipes/:id", recipesHandler.GetOneRecipeHandler) 76 | // } 77 | 78 | return router 79 | } 80 | func main() { 81 | SetupServer().Run() 82 | } 83 | -------------------------------------------------------------------------------- /chapter07/api-with-db/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/mlabouardy/recipes-api/models" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestListRecipesHandler(t *testing.T) { 17 | ts := httptest.NewServer(SetupServer()) 18 | defer ts.Close() 19 | 20 | resp, err := http.Get(fmt.Sprintf("%s/recipes", ts.URL)) 21 | defer resp.Body.Close() 22 | assert.Nil(t, err) 23 | assert.Equal(t, http.StatusOK, resp.StatusCode) 24 | data, _ := ioutil.ReadAll(resp.Body) 25 | 26 | var recipes []models.Recipe 27 | json.Unmarshal(data, &recipes) 28 | assert.Equal(t, len(recipes), 10) 29 | } 30 | 31 | func TestUpdateRecipeHandler(t *testing.T) { 32 | ts := httptest.NewServer(SetupServer()) 33 | defer ts.Close() 34 | 35 | recipe := Recipe{ 36 | ID: "c0283p3d0cvuglq85log", 37 | Name: "Oregano Marinated Chicken", 38 | } 39 | 40 | raw, _ := json.Marshal(recipe) 41 | resp, err := http.PUT(fmt.Sprintf("%s/recipes/%s", ts.URL, recipe.ID), bytes.NewBuffer(raw)) 42 | defer resp.Body.Close() 43 | assert.Nil(t, err) 44 | assert.Equal(t, http.StatusOK, resp.StatusCode) 45 | data, _ := ioutil.ReadAll(resp.Body) 46 | 47 | var payload map[string]string 48 | json.Unmarshal(data, &payload) 49 | 50 | assert.Equal(t, payload["message"], "Recipe has been updated") 51 | } 52 | 53 | func TestDeleteRecipeHandler(t *testing.T) { 54 | ts := httptest.NewServer(SetupServer()) 55 | defer ts.Close() 56 | 57 | resp, err := http.DELETE(fmt.Sprintf("%s/recipes/c0283p3d0cvuglq85log", ts.URL)) 58 | defer resp.Body.Close() 59 | assert.Nil(t, err) 60 | assert.Equal(t, http.StatusOK, resp.StatusCode) 61 | data, _ := ioutil.ReadAll(resp.Body) 62 | 63 | var payload map[string]string 64 | json.Unmarshal(data, &payload) 65 | 66 | assert.Equal(t, payload["message"], "Recipe has been deleted") 67 | } 68 | 69 | func TestFindRecipeHandler(t *testing.T) { 70 | ts := httptest.NewServer(SetupServer()) 71 | defer ts.Close() 72 | 73 | expectedRecipe := Recipe{ 74 | ID: "c0283p3d0cvuglq85log", 75 | Name: "Oregano Marinated Chicken", 76 | Tags: []string{"main", "chicken"}, 77 | } 78 | 79 | resp, err := http.GET(fmt.Sprintf("%s/recipes/c0283p3d0cvuglq85log", ts.URL)) 80 | defer resp.Body.Close() 81 | assert.Nil(t, err) 82 | assert.Equal(t, http.StatusOK, resp.StatusCode) 83 | data, _ := ioutil.ReadAll(resp.Body) 84 | 85 | var actualRecipe Recipe 86 | json.Unmarshal(data, &actualRecipe) 87 | 88 | assert.Equal(t, expectedRecipe.Name, actualRecipe.Name) 89 | assert.Equal(t, len(expectedRecipe.Tags), len(actualRecipe.Tags)) 90 | } 91 | -------------------------------------------------------------------------------- /chapter07/api-with-db/models/recipe.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // swagger:parameters recipes newRecipe 10 | type Recipe struct { 11 | //swagger:ignore 12 | ID primitive.ObjectID `json:"id" bson:"_id"` 13 | Name string `json:"name" bson:"name"` 14 | Tags []string `json:"tags" bson:"tags"` 15 | Ingredients []string `json:"ingredients" bson:"ingredients"` 16 | Instructions []string `json:"instructions" bson:"instructions"` 17 | PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"` 18 | } 19 | -------------------------------------------------------------------------------- /chapter07/api-with-db/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | Password string `json:"password"` 5 | Username string `json:"username"` 6 | } 7 | -------------------------------------------------------------------------------- /chapter07/api-with-db/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumes": [ 3 | "application/json" 4 | ], 5 | "produces": [ 6 | "application/json" 7 | ], 8 | "schemes": [ 9 | "http" 10 | ], 11 | "swagger": "2.0", 12 | "info": { 13 | "description": "This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin.", 14 | "title": "Recipes API", 15 | "contact": { 16 | "name": "Mohamed Labouardy", 17 | "url": "https://labouardy.com", 18 | "email": "mohamed@labouardy.com" 19 | }, 20 | "version": "1.0.0" 21 | }, 22 | "host": "localhost:8080", 23 | "basePath": "/", 24 | "paths": { 25 | "/recipes": { 26 | "get": { 27 | "description": "Returns list of recipes", 28 | "produces": [ 29 | "application/json" 30 | ], 31 | "tags": [ 32 | "recipes" 33 | ], 34 | "operationId": "listRecipes", 35 | "responses": { 36 | "200": { 37 | "description": "Successful operation" 38 | } 39 | } 40 | }, 41 | "post": { 42 | "description": "Create a new recipe", 43 | "produces": [ 44 | "application/json" 45 | ], 46 | "tags": [ 47 | "recipes" 48 | ], 49 | "operationId": "newRecipe", 50 | "responses": { 51 | "200": { 52 | "description": "Successful operation" 53 | }, 54 | "400": { 55 | "description": "Invalid input" 56 | } 57 | } 58 | } 59 | }, 60 | "/recipes/search": { 61 | "get": { 62 | "description": "Search recipes based on tags", 63 | "produces": [ 64 | "application/json" 65 | ], 66 | "tags": [ 67 | "recipes" 68 | ], 69 | "operationId": "findRecipe", 70 | "parameters": [ 71 | { 72 | "type": "string", 73 | "description": "recipe tag", 74 | "name": "tag", 75 | "in": "query", 76 | "required": true 77 | } 78 | ], 79 | "responses": { 80 | "200": { 81 | "description": "Successful operation" 82 | } 83 | } 84 | } 85 | }, 86 | "/recipes/{id}": { 87 | "put": { 88 | "description": "Update an existing recipe", 89 | "produces": [ 90 | "application/json" 91 | ], 92 | "tags": [ 93 | "recipes" 94 | ], 95 | "operationId": "updateRecipe", 96 | "parameters": [ 97 | { 98 | "type": "string", 99 | "description": "ID of the recipe", 100 | "name": "id", 101 | "in": "path", 102 | "required": true 103 | } 104 | ], 105 | "responses": { 106 | "200": { 107 | "description": "Successful operation" 108 | }, 109 | "400": { 110 | "description": "Invalid input" 111 | }, 112 | "404": { 113 | "description": "Invalid recipe ID" 114 | } 115 | } 116 | }, 117 | "delete": { 118 | "description": "Delete an existing recipe", 119 | "produces": [ 120 | "application/json" 121 | ], 122 | "tags": [ 123 | "recipes" 124 | ], 125 | "operationId": "deleteRecipe", 126 | "parameters": [ 127 | { 128 | "type": "string", 129 | "description": "ID of the recipe", 130 | "name": "id", 131 | "in": "path", 132 | "required": true 133 | } 134 | ], 135 | "responses": { 136 | "200": { 137 | "description": "Successful operation" 138 | }, 139 | "404": { 140 | "description": "Invalid recipe ID" 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /chapter07/api-without-db/coverage.out: -------------------------------------------------------------------------------- 1 | mode: set 2 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:52.41,54.2 1 1 3 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:66.39,68.50 2 1 4 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:73.2,78.31 4 1 5 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:68.50,71.3 2 0 6 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:99.42,102.50 3 1 7 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:106.2,109.36 3 1 8 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:115.2,115.17 1 1 9 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:120.2,122.31 2 1 10 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:102.50,105.3 2 0 11 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:109.36,110.26 1 1 12 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:110.26,112.4 1 1 13 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:115.17,118.3 2 1 14 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:141.42,145.36 3 0 15 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:151.2,151.17 1 0 16 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:156.2,158.68 2 0 17 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:145.36,146.26 1 0 18 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:146.26,148.4 1 0 19 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:151.17,154.3 2 0 20 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:175.43,179.36 3 0 21 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:191.2,191.38 1 0 22 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:179.36,181.37 2 0 23 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:186.3,186.12 1 0 24 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:181.37,182.33 1 0 25 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:182.33,184.5 1 0 26 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:186.12,188.4 1 0 27 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:210.39,212.36 2 0 28 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:218.2,218.65 1 0 29 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:212.36,213.26 1 0 30 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:213.26,215.4 1 0 31 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:221.13,225.2 3 1 32 | /Users/mlabouardy/github/Building-Distributed-Applications-in-Gin/chapter7/api-without-db/main.go:227.13,235.2 7 0 33 | -------------------------------------------------------------------------------- /chapter07/api-without-db/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func SetupRouter() *gin.Engine { 15 | router := gin.Default() 16 | return router 17 | } 18 | 19 | func TestListRecipesHandler(t *testing.T) { 20 | r := SetupRouter() 21 | r.GET("/recipes", ListRecipesHandler) 22 | req, _ := http.NewRequest("GET", "/recipes", nil) 23 | w := httptest.NewRecorder() 24 | r.ServeHTTP(w, req) 25 | 26 | var recipes []Recipe 27 | json.Unmarshal([]byte(w.Body.String()), &recipes) 28 | 29 | assert.Equal(t, http.StatusOK, w.Code) 30 | assert.Equal(t, 492, len(recipes)) 31 | } 32 | 33 | func TestNewRecipeHandler(t *testing.T) { 34 | r := SetupRouter() 35 | r.POST("/recipes", NewRecipeHandler) 36 | 37 | recipe := Recipe{ 38 | Name: "New York Pizza", 39 | } 40 | jsonValue, _ := json.Marshal(recipe) 41 | req, _ := http.NewRequest("POST", "/recipes", bytes.NewBuffer(jsonValue)) 42 | w := httptest.NewRecorder() 43 | r.ServeHTTP(w, req) 44 | 45 | assert.Equal(t, http.StatusOK, w.Code) 46 | } 47 | 48 | func TestUpdateRecipeHandler(t *testing.T) { 49 | r := SetupRouter() 50 | r.PUT("/recipes/:id", UpdateRecipeHandler) 51 | 52 | recipe := Recipe{ 53 | ID: "c0283p3d0cvuglq85lpg", 54 | Name: "Gnocchi", 55 | Ingredients: []string{ 56 | "5 large Idaho potatoes", 57 | "2 egges", 58 | "3/4 cup grated Parmesan", 59 | "3 1/2 cup all-purpose flour", 60 | }, 61 | } 62 | jsonValue, _ := json.Marshal(recipe) 63 | reqFound, _ := http.NewRequest("PUT", "/recipes/"+recipe.ID, bytes.NewBuffer(jsonValue)) 64 | w := httptest.NewRecorder() 65 | r.ServeHTTP(w, reqFound) 66 | 67 | assert.Equal(t, http.StatusOK, w.Code) 68 | 69 | reqNotFound, _ := http.NewRequest("PUT", "/recipes/1", bytes.NewBuffer(jsonValue)) 70 | w = httptest.NewRecorder() 71 | r.ServeHTTP(w, reqNotFound) 72 | 73 | assert.Equal(t, http.StatusNotFound, w.Code) 74 | } 75 | -------------------------------------------------------------------------------- /chapter07/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func IndexHandler(c *gin.Context) { 10 | c.JSON(http.StatusOK, gin.H{ 11 | "message": "hello world", 12 | }) 13 | } 14 | 15 | func SetupServer() *gin.Engine { 16 | r := gin.Default() 17 | r.GET("/", IndexHandler) 18 | return r 19 | } 20 | 21 | func main() { 22 | SetupServer().Run() 23 | } 24 | -------------------------------------------------------------------------------- /chapter07/basic/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func TestIndexHandler(t *testing.T) { 12 | mockUserResp := `{"message":"hello world"}` 13 | 14 | ts := httptest.NewServer(SetupServer()) 15 | defer ts.Close() 16 | 17 | resp, err := http.Get(fmt.Sprintf("%s/", ts.URL)) 18 | if err != nil { 19 | t.Fatalf("Expected no error, got %v", err) 20 | } 21 | defer resp.Body.Close() 22 | 23 | if resp.StatusCode != http.StatusOK { 24 | t.Fatalf("Expected status code 200, got %v", resp.StatusCode) 25 | } 26 | 27 | responseData, _ := ioutil.ReadAll(resp.Body) 28 | 29 | if string(responseData) != mockUserResp { 30 | t.Fatalf("Expected hello world message, got %v", responseData) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /chapter07/postman.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "285a8115-f24d-4040-ac90-b60cd164fb5b", 4 | "name": "Recipes API", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "List Recipes", 10 | "event": [ 11 | { 12 | "listen": "test", 13 | "script": { 14 | "exec": [ 15 | "pm.test(\"More than 10 recipes\", function () {", 16 | " var jsonData = pm.response.json();", 17 | " pm.expect(jsonData.length).to.equal(10)", 18 | "});", 19 | "", 20 | "pm.test(\"Gnocchi recipe\", function () {", 21 | " var jsonData = pm.response.json();", 22 | " var found = false;", 23 | " jsonData.forEach(recipe => {", 24 | " if (recipe.name == 'Gnocchi') {", 25 | " found = true;", 26 | " }", 27 | " })", 28 | " pm.expect(found).to.true", 29 | "});" 30 | ], 31 | "type": "text/javascript" 32 | } 33 | } 34 | ], 35 | "request": { 36 | "method": "GET", 37 | "header": [], 38 | "url": { 39 | "raw": "{{url}}/recipes", 40 | "host": [ 41 | "{{url}}" 42 | ], 43 | "path": [ 44 | "recipes" 45 | ] 46 | } 47 | }, 48 | "response": [] 49 | }, 50 | { 51 | "name": "New Recipe", 52 | "event": [ 53 | { 54 | "listen": "test", 55 | "script": { 56 | "exec": [ 57 | "pm.test(\"Status code is 200\", function () {", 58 | " pm.response.to.have.status(200);", 59 | "});", 60 | "", 61 | "pm.test(\"Recipe ID is not null\", function(){", 62 | " var id = pm.response.json().id;", 63 | " pm.expect(id).to.be.a(\"string\");", 64 | " pm.expect(id.length).to.eq(24);", 65 | "})" 66 | ], 67 | "type": "text/javascript" 68 | } 69 | } 70 | ], 71 | "request": { 72 | "method": "POST", 73 | "header": [], 74 | "body": { 75 | "mode": "raw", 76 | "raw": "{\n \"name\": \"New York Pizza\"\n}", 77 | "options": { 78 | "raw": { 79 | "language": "json" 80 | } 81 | } 82 | }, 83 | "url": { 84 | "raw": "{{url}}/recipes", 85 | "host": [ 86 | "{{url}}" 87 | ], 88 | "path": [ 89 | "recipes" 90 | ] 91 | } 92 | }, 93 | "response": [] 94 | } 95 | ], 96 | "auth": { 97 | "type": "basic", 98 | "basic": [ 99 | { 100 | "key": "password", 101 | "value": "password", 102 | "type": "string" 103 | }, 104 | { 105 | "key": "username", 106 | "value": "admin", 107 | "type": "string" 108 | } 109 | ] 110 | } 111 | } -------------------------------------------------------------------------------- /chapter07/testify/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func IndexHandler(c *gin.Context) { 10 | c.JSON(http.StatusOK, gin.H{ 11 | "message": "hello world", 12 | }) 13 | } 14 | 15 | func SetupServer() *gin.Engine { 16 | r := gin.Default() 17 | r.GET("/", IndexHandler) 18 | return r 19 | } 20 | 21 | func main() { 22 | SetupServer().Run() 23 | } 24 | -------------------------------------------------------------------------------- /chapter07/testify/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestIndexHandler(t *testing.T) { 14 | mockUserResp := `{"message":"hello world"}` 15 | 16 | ts := httptest.NewServer(SetupServer()) 17 | defer ts.Close() 18 | 19 | resp, err := http.Get(fmt.Sprintf("%s/", ts.URL)) 20 | defer resp.Body.Close() 21 | 22 | assert.Nil(t, err) 23 | assert.Equal(t, http.StatusOK, resp.StatusCode) 24 | 25 | responseData, _ := ioutil.ReadAll(resp.Body) 26 | assert.Equal(t, mockUserResp, string(responseData)) 27 | } 28 | -------------------------------------------------------------------------------- /chapter08/docker-compose.ecs.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | api: 5 | image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/recipes-api:latest 6 | environment: 7 | - MONGO_URI=mongodb://admin:password@mongodb:27017/test?authSource=admin&readPreference=primary&ssl=false 8 | - MONGO_DATABASE=demo 9 | - REDIS_URI=redis:6379 10 | logging: 11 | driver: awslogs 12 | options: 13 | awslogs-group: sandbox 14 | awslogs-region: eu-central-1 15 | awslogs-stream-prefix: api 16 | 17 | dashboard: 18 | image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/dashboard:latest 19 | logging: 20 | driver: awslogs 21 | options: 22 | awslogs-group: sandbox 23 | awslogs-region: eu-central-1 24 | awslogs-stream-prefix: dashboard 25 | 26 | redis: 27 | image: redis 28 | logging: 29 | driver: awslogs 30 | options: 31 | awslogs-group: sandbox 32 | awslogs-region: eu-central-1 33 | awslogs-stream-prefix: redis 34 | 35 | mongodb: 36 | image: mongo:4.4.3 37 | environment: 38 | - MONGO_INITDB_ROOT_USERNAME=admin 39 | - MONGO_INITDB_ROOT_PASSWORD=password 40 | logging: 41 | driver: awslogs 42 | options: 43 | awslogs-group: sandbox 44 | awslogs-region: eu-central-1 45 | awslogs-stream-prefix: mongodb 46 | 47 | nginx: 48 | image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/nginx:latest 49 | ports: 50 | - 80:80 51 | depends_on: 52 | - api 53 | - dashboard 54 | logging: 55 | driver: awslogs 56 | options: 57 | awslogs-group: sandbox 58 | awslogs-region: eu-central-1 59 | awslogs-stream-prefix: nginx -------------------------------------------------------------------------------- /chapter08/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | api: 5 | image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/recipes-api:latest 6 | environment: 7 | - MONGO_URI=mongodb://admin:password@mongodb:27017/test?authSource=admin&readPreference=primary&ssl=false 8 | - MONGO_DATABASE=demo 9 | - REDIS_URI=redis:6379 10 | external_links: 11 | - mongodb 12 | - redis 13 | 14 | dashboard: 15 | image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/dashboard:latest 16 | 17 | redis: 18 | image: redis 19 | ports: 20 | - 6379:6379 21 | 22 | mongodb: 23 | image: mongo:4.4.3 24 | ports: 25 | - 27017:27017 26 | environment: 27 | - MONGO_INITDB_ROOT_USERNAME=admin 28 | - MONGO_INITDB_ROOT_PASSWORD=password 29 | 30 | nginx: 31 | image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/nginx:latest 32 | ports: 33 | - 80:80 34 | depends_on: 35 | - api 36 | - dashboard 37 | -------------------------------------------------------------------------------- /chapter08/ecs-params.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | task_definition: 3 | task_execution_role: ecsTaskExecutionRole 4 | ecs_network_mode: awsvpc 5 | task_size: 6 | mem_limit: 2GB 7 | cpu_limit: 2vcpu 8 | services: 9 | nginx: 10 | essential: true 11 | run_params: 12 | network_configuration: 13 | awsvpc_configuration: 14 | subnets: 15 | - "subnet-ID" 16 | - "subnet-ID" 17 | security_groups: 18 | - "sg-ID" 19 | assign_public_ip: ENABLED 20 | -------------------------------------------------------------------------------- /chapter08/eks-admin-service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: eks-admin 5 | namespace: kube-system 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1beta1 8 | kind: ClusterRoleBinding 9 | metadata: 10 | name: eks-admin 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: ClusterRole 14 | name: cluster-admin 15 | subjects: 16 | - kind: ServiceAccount 17 | name: eks-admin 18 | namespace: kube-system 19 | -------------------------------------------------------------------------------- /chapter08/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | server_tokens off; 7 | server { 8 | listen 80; 9 | 10 | location / { 11 | proxy_set_header X-Forwarded-For $remote_addr; 12 | proxy_set_header Host $http_host; 13 | proxy_pass http://dashboard/; 14 | } 15 | 16 | location /api/ { 17 | proxy_set_header X-Forwarded-For $remote_addr; 18 | proxy_set_header Host $http_host; 19 | proxy_pass http://api:8080/; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /chapter08/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | COPY nginx.conf /etc/nginx/nginx.conf -------------------------------------------------------------------------------- /chapter08/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | http { 6 | server_tokens off; 7 | server { 8 | listen 80; 9 | 10 | location / { 11 | proxy_set_header X-Forwarded-For $remote_addr; 12 | proxy_set_header Host $http_host; 13 | proxy_pass http://dashboard:3000/; 14 | } 15 | 16 | location /api/ { 17 | proxy_set_header X-Forwarded-For $remote_addr; 18 | proxy_set_header Host $http_host; 19 | proxy_pass http://api:8080/; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /chapter08/resources/api-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | kompose.cmd: kompose convert 6 | kompose.version: 1.22.0 (955b78124) 7 | creationTimestamp: null 8 | labels: 9 | io.kompose.service: api 10 | name: api 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | io.kompose.service: api 16 | strategy: {} 17 | template: 18 | metadata: 19 | annotations: 20 | kompose.cmd: kompose convert 21 | kompose.version: 1.22.0 (955b78124) 22 | creationTimestamp: null 23 | labels: 24 | io.kompose.service: api 25 | spec: 26 | containers: 27 | - env: 28 | - name: MONGO_DATABASE 29 | value: demo 30 | - name: MONGO_URI 31 | value: mongodb://admin:password@mongodb:27017/test?authSource=admin&readPreference=primary&ssl=false 32 | - name: REDIS_URI 33 | value: redis:6379 34 | image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/recipes-api:latest 35 | name: api 36 | resources: {} 37 | restartPolicy: Always 38 | status: {} 39 | -------------------------------------------------------------------------------- /chapter08/resources/dashboard-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | kompose.cmd: kompose convert 6 | kompose.version: 1.22.0 (955b78124) 7 | creationTimestamp: null 8 | labels: 9 | io.kompose.service: dashboard 10 | name: dashboard 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | io.kompose.service: dashboard 16 | strategy: {} 17 | template: 18 | metadata: 19 | annotations: 20 | kompose.cmd: kompose convert 21 | kompose.version: 1.22.0 (955b78124) 22 | creationTimestamp: null 23 | labels: 24 | io.kompose.service: dashboard 25 | spec: 26 | containers: 27 | - image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/dashboard:latest 28 | name: dashboard 29 | resources: {} 30 | restartPolicy: Always 31 | status: {} 32 | -------------------------------------------------------------------------------- /chapter08/resources/mongodb-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | kompose.cmd: kompose convert 6 | kompose.version: 1.22.0 (955b78124) 7 | creationTimestamp: null 8 | labels: 9 | io.kompose.service: mongodb 10 | name: mongodb 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | io.kompose.service: mongodb 16 | strategy: {} 17 | template: 18 | metadata: 19 | annotations: 20 | kompose.cmd: kompose convert 21 | kompose.version: 1.22.0 (955b78124) 22 | creationTimestamp: null 23 | labels: 24 | io.kompose.service: mongodb 25 | spec: 26 | containers: 27 | - env: 28 | - name: MONGO_INITDB_ROOT_PASSWORD 29 | value: password 30 | - name: MONGO_INITDB_ROOT_USERNAME 31 | value: admin 32 | image: mongo:4.4.3 33 | name: mongodb 34 | ports: 35 | - containerPort: 27017 36 | resources: {} 37 | restartPolicy: Always 38 | status: {} 39 | -------------------------------------------------------------------------------- /chapter08/resources/mongodb-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | kompose.cmd: kompose convert 6 | kompose.version: 1.22.0 (955b78124) 7 | creationTimestamp: null 8 | labels: 9 | io.kompose.service: mongodb 10 | name: mongodb 11 | spec: 12 | ports: 13 | - name: "27017" 14 | port: 27017 15 | targetPort: 27017 16 | selector: 17 | io.kompose.service: mongodb 18 | status: 19 | loadBalancer: {} 20 | -------------------------------------------------------------------------------- /chapter08/resources/nginx-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | kompose.cmd: kompose convert 6 | kompose.version: 1.22.0 (955b78124) 7 | creationTimestamp: null 8 | labels: 9 | io.kompose.service: nginx 10 | name: nginx 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | io.kompose.service: nginx 16 | strategy: {} 17 | template: 18 | metadata: 19 | annotations: 20 | kompose.cmd: kompose convert 21 | kompose.version: 1.22.0 (955b78124) 22 | creationTimestamp: null 23 | labels: 24 | io.kompose.service: nginx 25 | spec: 26 | containers: 27 | - image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/nginx:latest 28 | name: nginx 29 | ports: 30 | - containerPort: 80 31 | resources: {} 32 | restartPolicy: Always 33 | status: {} 34 | -------------------------------------------------------------------------------- /chapter08/resources/nginx-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | kompose.cmd: kompose convert 6 | kompose.version: 1.22.0 (955b78124) 7 | creationTimestamp: null 8 | labels: 9 | io.kompose.service: nginx 10 | name: nginx 11 | spec: 12 | ports: 13 | - name: "80" 14 | port: 80 15 | targetPort: 80 16 | selector: 17 | io.kompose.service: nginx 18 | status: 19 | loadBalancer: {} 20 | -------------------------------------------------------------------------------- /chapter08/resources/redis-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | kompose.cmd: kompose convert 6 | kompose.version: 1.22.0 (955b78124) 7 | creationTimestamp: null 8 | labels: 9 | io.kompose.service: redis 10 | name: redis 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | io.kompose.service: redis 16 | strategy: {} 17 | template: 18 | metadata: 19 | annotations: 20 | kompose.cmd: kompose convert 21 | kompose.version: 1.22.0 (955b78124) 22 | creationTimestamp: null 23 | labels: 24 | io.kompose.service: redis 25 | spec: 26 | containers: 27 | - image: redis 28 | name: redis 29 | ports: 30 | - containerPort: 6379 31 | resources: {} 32 | restartPolicy: Always 33 | status: {} 34 | -------------------------------------------------------------------------------- /chapter08/resources/redis-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | kompose.cmd: kompose convert 6 | kompose.version: 1.22.0 (955b78124) 7 | creationTimestamp: null 8 | labels: 9 | io.kompose.service: redis 10 | name: redis 11 | spec: 12 | ports: 13 | - name: "6379" 14 | port: 6379 15 | targetPort: 6379 16 | selector: 17 | io.kompose.service: redis 18 | status: 19 | loadBalancer: {} 20 | -------------------------------------------------------------------------------- /chapter08/task-execution-assume-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "", 6 | "Effect": "Allow", 7 | "Principal": { 8 | "Service": "ecs-tasks.amazonaws.com" 9 | }, 10 | "Action": "sts:AssumeRole" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /chapter09/.circleci/config.ecs.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | aws-ecs: circleci/aws-ecs@0.0.11 5 | 6 | workflows: 7 | ci_cd: 8 | jobs: 9 | - test 10 | - build 11 | - aws-ecs/deploy-service-update: 12 | aws-region: AWS_DEFAULT_REGION 13 | family: 'demo' 14 | cluster-name: 'sandbox' 15 | container-image-name-updates: 'container=api,tag=0.1.${CIRCLE_BUILD_NUM}' -------------------------------------------------------------------------------- /chapter09/.circleci/config.eks.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | aws-eks: circleci/aws-eks@0.2.0 5 | kubernetes: circleci/kubernetes@0.3.0 6 | 7 | jobs: 8 | deploy: 9 | executor: aws-eks/python3 10 | steps: 11 | - checkout 12 | - aws-eks/update-kubeconfig-with-authenticator: 13 | cluster-name: sandbox 14 | install-kubectl: true 15 | aws-region: AWS_DEFAULT_REGION 16 | - kubernetes/create-or-update-resource: 17 | resource-file-path: "deployment/api.deployment.yaml" 18 | get-rollout-status: true 19 | resource-name: deployment/api 20 | - kubernetes/create-or-update-resource: 21 | resource-file-path: "deployment/api.service.yaml" 22 | 23 | workflows: 24 | ci_cd: 25 | jobs: 26 | - test 27 | - build 28 | - deploy -------------------------------------------------------------------------------- /chapter09/.circleci/config.s3.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | environment: 5 | docker: 6 | - image: node:lts 7 | working_directory: /dashboard 8 | 9 | jobs: 10 | build: 11 | executor: environment 12 | steps: 13 | - checkout 14 | - restore_cache: 15 | key: node-modules-{{checksum "package.json"}} 16 | - run: 17 | name: Install dependencies 18 | command: npm install 19 | - save_cache: 20 | key: node-modules-{{checksum "package.json"}} 21 | paths: 22 | - node_modules 23 | - run: 24 | name: Build artifact 25 | command: CI=false npm run build 26 | - persist_to_workspace: 27 | root: . 28 | paths: 29 | - build 30 | 31 | deploy: 32 | executor: environment 33 | steps: 34 | - attach_workspace: 35 | at: dist 36 | - run: 37 | name: Install AWS CLI 38 | command: | 39 | apt-get update 40 | apt-get install -y python3-pip 41 | pip3 install awscli 42 | - run: 43 | name: Push to S3 bucket 44 | command: | 45 | cd dist/build/dashboard/ 46 | aws configure set preview.cloudfront true 47 | aws s3 cp --recursive . s3://YOUR_S3_BUCKET/ --region YOUR_AWS_REGION 48 | workflows: 49 | ci_cd: 50 | jobs: 51 | - build 52 | - deploy: 53 | requires: 54 | - build 55 | filters: 56 | branches: 57 | only: 58 | - master -------------------------------------------------------------------------------- /chapter09/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | WORKDIR /go/src/github.com/api 3 | COPY . . 4 | RUN go mod download 5 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 6 | 7 | FROM alpine:latest 8 | ARG API_VERSION 9 | ENV API_VERSION=$API_VERSION 10 | RUN apk --no-cache add ca-certificates 11 | WORKDIR /root/ 12 | COPY --from=0 /go/src/github.com/api/app . 13 | CMD ["./app"] -------------------------------------------------------------------------------- /chapter09/README.md: -------------------------------------------------------------------------------- 1 | # Recipes API 2 | Go based Gin RESTful API -------------------------------------------------------------------------------- /chapter09/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | api: 5 | image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/recipes-api:develop 6 | environment: 7 | - MONGO_URI=mongodb://admin:password@mongodb:27017/test?authSource=admin&readPreference=primary&ssl=false 8 | - MONGO_DATABASE=demo 9 | - REDIS_URI=redis:6379 10 | external_links: 11 | - mongodb 12 | - redis 13 | 14 | dashboard: 15 | image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/dashboard:develop 16 | 17 | redis: 18 | image: redis 19 | ports: 20 | - 6379:6379 21 | 22 | mongodb: 23 | image: mongo:4.4.3 24 | ports: 25 | - 27017:27017 26 | environment: 27 | - MONGO_INITDB_ROOT_USERNAME=admin 28 | - MONGO_INITDB_ROOT_PASSWORD=password 29 | 30 | nginx: 31 | image: ID.dkr.ecr.eu-central-1.amazonaws.com/mlabouardy/nginx:develop 32 | ports: 33 | - 80:80 34 | depends_on: 35 | - api 36 | - dashboard -------------------------------------------------------------------------------- /chapter09/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlabouardy/recipes-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/go-redis/redis v6.15.9+incompatible 8 | github.com/go-redis/redis/v8 v8.4.10 9 | go.mongodb.org/mongo-driver v1.4.5 10 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb 11 | ) 12 | -------------------------------------------------------------------------------- /chapter09/iam-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2008-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "AllowPushPull", 6 | "Effect": "Allow", 7 | "Principal": { 8 | "AWS": [ 9 | "arn:aws:iam::account-id:user/push-pull-user-1", 10 | "arn:aws:iam::account-id:user/push-pull-user-2" 11 | ] 12 | }, 13 | "Action": [ 14 | "ecr:GetDownloadUrlForLayer", 15 | "ecr:BatchGetImage", 16 | "ecr:BatchCheckLayerAvailability", 17 | "ecr:PutImage", 18 | "ecr:InitiateLayerUpload", 19 | "ecr:UploadLayerPart", 20 | "ecr:CompleteLayerUpload" 21 | ] 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /chapter09/main.go: -------------------------------------------------------------------------------- 1 | // Recipes API 2 | // 3 | // This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin. 4 | // 5 | // Schemes: http 6 | // Host: localhost:8080 7 | // BasePath: / 8 | // Version: 1.0.0 9 | // Contact: Mohamed Labouardy https://labouardy.com 10 | // 11 | // Consumes: 12 | // - application/json 13 | // 14 | // Produces: 15 | // - application/json 16 | // swagger:meta 17 | package main 18 | 19 | import ( 20 | "context" 21 | "log" 22 | "net/http" 23 | "os" 24 | 25 | "github.com/gin-gonic/gin" 26 | "github.com/go-redis/redis" 27 | handlers "github.com/mlabouardy/recipes-api/handlers" 28 | "go.mongodb.org/mongo-driver/mongo" 29 | "go.mongodb.org/mongo-driver/mongo/options" 30 | "go.mongodb.org/mongo-driver/mongo/readpref" 31 | ) 32 | 33 | var recipesHandler *handlers.RecipesHandler 34 | 35 | func init() { 36 | ctx := context.Background() 37 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 38 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 39 | log.Fatal(err) 40 | } 41 | log.Println("Connected to MongoDB") 42 | collection := client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 43 | 44 | redisClient := redis.NewClient(&redis.Options{ 45 | Addr: os.Getenv("REDIS_URI"), 46 | Password: "", 47 | DB: 0, 48 | }) 49 | 50 | status := redisClient.Ping() 51 | log.Println(status) 52 | 53 | recipesHandler = handlers.NewRecipesHandler(ctx, collection, redisClient) 54 | } 55 | 56 | func VersionHandler(c *gin.Context) { 57 | c.JSON(http.StatusOK, gin.H{"version": os.Getenv("API_VERSION")}) 58 | } 59 | 60 | func main() { 61 | router := gin.Default() 62 | router.POST("/recipes", recipesHandler.NewRecipeHandler) 63 | router.GET("/recipes", recipesHandler.ListRecipesHandler) 64 | router.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler) 65 | router.GET("/version", VersionHandler) 66 | router.Run() 67 | } 68 | -------------------------------------------------------------------------------- /chapter09/models/recipe.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // swagger:parameters recipes newRecipe 10 | type Recipe struct { 11 | //swagger:ignore 12 | ID primitive.ObjectID `json:"id" bson:"_id"` 13 | Name string `json:"name" bson:"name"` 14 | Tags []string `json:"tags" bson:"tags"` 15 | Ingredients []string `json:"ingredients" bson:"ingredients"` 16 | Instructions []string `json:"instructions" bson:"instructions"` 17 | PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"` 18 | } 19 | -------------------------------------------------------------------------------- /chapter09/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "consumes": [ 3 | "application/json" 4 | ], 5 | "produces": [ 6 | "application/json" 7 | ], 8 | "schemes": [ 9 | "http" 10 | ], 11 | "swagger": "2.0", 12 | "info": { 13 | "description": "This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin.", 14 | "title": "Recipes API", 15 | "contact": { 16 | "name": "Mohamed Labouardy", 17 | "url": "https://labouardy.com", 18 | "email": "mohamed@labouardy.com" 19 | }, 20 | "version": "1.0.0" 21 | }, 22 | "host": "localhost:8080", 23 | "basePath": "/", 24 | "paths": { 25 | "/recipes": { 26 | "get": { 27 | "description": "Returns list of recipes", 28 | "produces": [ 29 | "application/json" 30 | ], 31 | "tags": [ 32 | "recipes" 33 | ], 34 | "operationId": "listRecipes", 35 | "responses": { 36 | "200": { 37 | "description": "Successful operation" 38 | } 39 | } 40 | }, 41 | "post": { 42 | "description": "Create a new recipe", 43 | "produces": [ 44 | "application/json" 45 | ], 46 | "tags": [ 47 | "recipes" 48 | ], 49 | "operationId": "newRecipe", 50 | "responses": { 51 | "200": { 52 | "description": "Successful operation" 53 | }, 54 | "400": { 55 | "description": "Invalid input" 56 | } 57 | } 58 | } 59 | }, 60 | "/recipes/search": { 61 | "get": { 62 | "description": "Search recipes based on tags", 63 | "produces": [ 64 | "application/json" 65 | ], 66 | "tags": [ 67 | "recipes" 68 | ], 69 | "operationId": "findRecipe", 70 | "parameters": [ 71 | { 72 | "type": "string", 73 | "description": "recipe tag", 74 | "name": "tag", 75 | "in": "query", 76 | "required": true 77 | } 78 | ], 79 | "responses": { 80 | "200": { 81 | "description": "Successful operation" 82 | } 83 | } 84 | } 85 | }, 86 | "/recipes/{id}": { 87 | "put": { 88 | "description": "Update an existing recipe", 89 | "produces": [ 90 | "application/json" 91 | ], 92 | "tags": [ 93 | "recipes" 94 | ], 95 | "operationId": "updateRecipe", 96 | "parameters": [ 97 | { 98 | "type": "string", 99 | "description": "ID of the recipe", 100 | "name": "id", 101 | "in": "path", 102 | "required": true 103 | } 104 | ], 105 | "responses": { 106 | "200": { 107 | "description": "Successful operation" 108 | }, 109 | "400": { 110 | "description": "Invalid input" 111 | }, 112 | "404": { 113 | "description": "Invalid recipe ID" 114 | } 115 | } 116 | }, 117 | "delete": { 118 | "description": "Delete an existing recipe", 119 | "produces": [ 120 | "application/json" 121 | ], 122 | "tags": [ 123 | "recipes" 124 | ], 125 | "operationId": "deleteRecipe", 126 | "parameters": [ 127 | { 128 | "type": "string", 129 | "description": "ID of the recipe", 130 | "name": "id", 131 | "in": "path", 132 | "required": true 133 | } 134 | ], 135 | "responses": { 136 | "200": { 137 | "description": "Successful operation" 138 | }, 139 | "404": { 140 | "description": "Invalid recipe ID" 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /chapter10/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | WORKDIR /go/src/github.com/api 3 | COPY . . 4 | RUN go mod download 5 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 6 | 7 | FROM alpine:latest 8 | ARG API_VERSION 9 | ENV API_VERSION=$API_VERSION 10 | RUN apk --no-cache add ca-certificates 11 | WORKDIR /root/ 12 | COPY --from=0 /go/src/github.com/api/app . 13 | CMD ["./app"] -------------------------------------------------------------------------------- /chapter10/README.md: -------------------------------------------------------------------------------- 1 | # Recipes API 2 | Go based Gin RESTful API -------------------------------------------------------------------------------- /chapter10/debug.log: -------------------------------------------------------------------------------- 1 | [GIN] 2021/05/13 - 22:10:56 | 200 | 4.378737ms | ::1 | GET "/recipes" 2 | [GIN] 2021/05/13 - 22:10:57 | 200 | 3.363734ms | ::1 | GET "/recipes" 3 | [GIN] 2021/05/13 - 22:10:58 | 200 | 1.44712ms | ::1 | GET "/recipes" 4 | [GIN] 2021/05/13 - 22:10:58 | 200 | 1.638844ms | ::1 | GET "/recipes" 5 | [GIN] 2021/05/13 - 22:10:58 | 200 | 1.399161ms | ::1 | GET "/recipes" 6 | -------------------------------------------------------------------------------- /chapter10/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | api: 5 | build: . 6 | environment: 7 | - MONGO_URI=mongodb://admin:password@mongodb:27017/test?authSource=admin&readPreference=primary&ssl=false 8 | - MONGO_DATABASE=demo 9 | - REDIS_URI=redis:6379 10 | - API_VERSION=1.0.0 11 | ports: 12 | - 8080:8080 13 | external_links: 14 | - mongodb 15 | - redis 16 | - logstash 17 | restart: always 18 | logging: 19 | driver: gelf 20 | options: 21 | gelf-address: "udp://127.0.0.1:12201" 22 | tag: "recipes-api" 23 | networks: 24 | - demo 25 | 26 | 27 | redis: 28 | image: redis 29 | restart: always 30 | networks: 31 | - demo 32 | 33 | mongodb: 34 | image: mongo:4.4.3 35 | environment: 36 | - MONGO_INITDB_ROOT_USERNAME=admin 37 | - MONGO_INITDB_ROOT_PASSWORD=password 38 | restart: always 39 | networks: 40 | - demo 41 | 42 | prometheus: 43 | image: prom/prometheus:v2.27.0 44 | volumes: 45 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 46 | ports: 47 | - 9090:9090 48 | restart: always 49 | networks: 50 | - demo 51 | 52 | grafana: 53 | image: grafana/grafana:7.5.6 54 | ports: 55 | - 3000:3000 56 | restart: always 57 | networks: 58 | - demo 59 | 60 | telegraf: 61 | image: telegraf:latest 62 | volumes: 63 | - ./telegraf.conf:/etc/telegraf/telegraf.conf 64 | - /var/run/docker.sock:/var/run/docker.sock 65 | networks: 66 | - demo 67 | 68 | logstash: 69 | image: docker.elastic.co/logstash/logstash:7.12.1 70 | command: logstash -f /etc/logstash/logstash.conf 71 | volumes: 72 | - ./logstash.conf:/etc/logstash/logstash.conf 73 | networks: 74 | - demo 75 | ports: 76 | - "5000:5000" 77 | - "12201:12201" 78 | - "12201:12201/udp" 79 | 80 | elasticsearch: 81 | image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1 82 | ports: 83 | - 9200:9200 84 | environment: 85 | - discovery.type=single-node 86 | networks: 87 | - demo 88 | restart: always 89 | 90 | kibana: 91 | image: docker.elastic.co/kibana/kibana:7.12.1 92 | ports: 93 | - 5601:5601 94 | environment: 95 | - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 96 | networks: 97 | - demo 98 | 99 | filebeat: 100 | image: docker.elastic.co/beats/filebeat:7.12.1 101 | volumes: 102 | - ./filebeat.yml:/usr/share/filebeat/filebeat.yml 103 | - ./debug.log:/var/log/api/debug.log 104 | networks: 105 | - demo 106 | 107 | networks: 108 | demo: 109 | driver: bridge -------------------------------------------------------------------------------- /chapter10/filebeat.yml: -------------------------------------------------------------------------------- 1 | filebeat.inputs: 2 | - type: log 3 | paths: 4 | - /var/log/api/debug.log 5 | 6 | output.elasticsearch: 7 | hosts: 'http://elasticsearch:9200' -------------------------------------------------------------------------------- /chapter10/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mlabouardy/recipes-api 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.6.3 7 | github.com/go-redis/redis v6.15.9+incompatible 8 | github.com/go-redis/redis/v8 v8.4.10 9 | github.com/prometheus/client_golang v1.10.0 // indirect 10 | go.mongodb.org/mongo-driver v1.4.5 11 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb 12 | ) 13 | -------------------------------------------------------------------------------- /chapter10/logstash.conf: -------------------------------------------------------------------------------- 1 | input { 2 | gelf { 3 | type => docker 4 | port => 12201 5 | } 6 | } 7 | 8 | filter { 9 | grok { 10 | match => {"message" => "%{DATE:date} - %{TIME:time} \| %{NUMBER:status} \| %{SPACE} %{NUMBER:requestDuration}%{GREEDYDATA:unit} \| %{SPACE} %{IP:clientIp} \| %{WORD:httpMethod} %{SPACE} %{QUOTEDSTRING:url}"} 11 | } 12 | } 13 | 14 | output { 15 | elasticsearch { 16 | hosts => "elasticsearch:9200" 17 | index => "containers-%{+YYYY.MM.dd}" 18 | } 19 | } -------------------------------------------------------------------------------- /chapter10/main.go: -------------------------------------------------------------------------------- 1 | // Recipes API 2 | // 3 | // This is a sample recipes API. You can find out more about the API at https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin. 4 | // 5 | // Schemes: http 6 | // Host: localhost:8080 7 | // BasePath: / 8 | // Version: 1.0.0 9 | // Contact: Mohamed Labouardy https://labouardy.com 10 | // 11 | // Consumes: 12 | // - application/json 13 | // 14 | // Produces: 15 | // - application/json 16 | // swagger:meta 17 | package main 18 | 19 | import ( 20 | "context" 21 | "io" 22 | "log" 23 | "net/http" 24 | "os" 25 | 26 | "github.com/gin-gonic/gin" 27 | "github.com/go-redis/redis" 28 | handlers "github.com/mlabouardy/recipes-api/handlers" 29 | "github.com/prometheus/client_golang/prometheus" 30 | "github.com/prometheus/client_golang/prometheus/promauto" 31 | "github.com/prometheus/client_golang/prometheus/promhttp" 32 | "go.mongodb.org/mongo-driver/mongo" 33 | "go.mongodb.org/mongo-driver/mongo/options" 34 | "go.mongodb.org/mongo-driver/mongo/readpref" 35 | ) 36 | 37 | var recipesHandler *handlers.RecipesHandler 38 | 39 | var totalRequests = prometheus.NewCounterVec( 40 | prometheus.CounterOpts{ 41 | Name: "http_requests_total", 42 | Help: "Number of incoming requests", 43 | }, 44 | []string{"path"}, 45 | ) 46 | 47 | var totalHTTPMethods = prometheus.NewCounterVec( 48 | prometheus.CounterOpts{ 49 | Name: "http_methods_total", 50 | Help: "Number of requests per HTTP method", 51 | }, 52 | []string{"method"}, 53 | ) 54 | 55 | var httpDuration = promauto.NewHistogramVec( 56 | prometheus.HistogramOpts{ 57 | Name: "http_response_time_seconds", 58 | Help: "Duration of HTTP requests", 59 | }, 60 | []string{"path"}, 61 | ) 62 | 63 | func init() { 64 | ctx := context.Background() 65 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(os.Getenv("MONGO_URI"))) 66 | if err = client.Ping(context.TODO(), readpref.Primary()); err != nil { 67 | log.Fatal(err) 68 | } 69 | log.Println("Connected to MongoDB") 70 | collection := client.Database(os.Getenv("MONGO_DATABASE")).Collection("recipes") 71 | 72 | redisClient := redis.NewClient(&redis.Options{ 73 | Addr: os.Getenv("REDIS_URI"), 74 | Password: "", 75 | DB: 0, 76 | }) 77 | 78 | status := redisClient.Ping() 79 | log.Println(status) 80 | 81 | recipesHandler = handlers.NewRecipesHandler(ctx, collection, redisClient) 82 | 83 | prometheus.Register(totalRequests) 84 | prometheus.Register(totalHTTPMethods) 85 | prometheus.Register(httpDuration) 86 | } 87 | 88 | func VersionHandler(c *gin.Context) { 89 | c.JSON(http.StatusOK, gin.H{"version": os.Getenv("API_VERSION")}) 90 | } 91 | 92 | func PrometheusMiddleware() gin.HandlerFunc { 93 | return func(c *gin.Context) { 94 | timer := prometheus.NewTimer(httpDuration.WithLabelValues(c.Request.URL.Path)) 95 | totalRequests.WithLabelValues(c.Request.URL.Path).Inc() 96 | totalHTTPMethods.WithLabelValues(c.Request.Method).Inc() 97 | c.Next() 98 | timer.ObserveDuration() 99 | } 100 | } 101 | 102 | func main() { 103 | gin.DisableConsoleColor() 104 | f, _ := os.Create("debug.log") 105 | gin.DefaultWriter = io.MultiWriter(f) 106 | 107 | router := gin.Default() 108 | router.Use(PrometheusMiddleware()) 109 | router.POST("/recipes", recipesHandler.NewRecipeHandler) 110 | router.GET("/recipes", recipesHandler.ListRecipesHandler) 111 | router.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler) 112 | router.GET("/version", VersionHandler) 113 | router.GET("/prometheus", gin.WrapH(promhttp.Handler())) 114 | router.Run() 115 | } 116 | -------------------------------------------------------------------------------- /chapter10/models/recipe.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/bson/primitive" 7 | ) 8 | 9 | // swagger:parameters recipes newRecipe 10 | type Recipe struct { 11 | //swagger:ignore 12 | ID primitive.ObjectID `json:"id" bson:"_id"` 13 | Name string `json:"name" bson:"name"` 14 | Tags []string `json:"tags" bson:"tags"` 15 | Ingredients []string `json:"ingredients" bson:"ingredients"` 16 | Instructions []string `json:"instructions" bson:"instructions"` 17 | PublishedAt time.Time `json:"publishedAt" bson:"publishedAt"` 18 | } 19 | -------------------------------------------------------------------------------- /chapter10/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | scrape_configs: 6 | - job_name: prometheus 7 | static_configs: 8 | - targets: ['localhost:9090'] 9 | - job_name: recipes-api 10 | metrics_path: /prometheus 11 | static_configs: 12 | - targets: 13 | - api:8080 14 | - job_name: telegraf 15 | scrape_interval: 15s 16 | static_configs: 17 | - targets: ['telegraf:9100'] -------------------------------------------------------------------------------- /chapter10/telegraf.conf: -------------------------------------------------------------------------------- 1 | [[inputs.cpu]] 2 | percpu = false 3 | totalcpu = true 4 | fieldpass = [ "usage*" ] 5 | 6 | [[inputs.disk]] 7 | fielddrop = [ "inodes*" ] 8 | mount_points=["/"] 9 | 10 | [[inputs.net]] 11 | interfaces = [ "eth0" ] 12 | fielddrop = [ "icmp*", "ip*", "tcp*", "udp*" ] 13 | 14 | [[inputs.mem]] 15 | 16 | [[inputs.swap]] 17 | 18 | [[inputs.system]] 19 | 20 | [[inputs.docker]] 21 | endpoint = "unix:///var/run/docker.sock" 22 | container_names = [] 23 | 24 | [[outputs.prometheus_client]] 25 | listen = "telegraf:9100" --------------------------------------------------------------------------------