├── 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 |
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 |
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 |
2 |
3 |
4 |
5 |
Recipes
6 |
7 |
8 |
10 |
11 |
12 |
13 |
20 |
--------------------------------------------------------------------------------
/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 |
2 |
3 |
4 |
5 |
Recipes
6 |
7 |
8 |
10 |
11 |
12 |
13 |
20 |
--------------------------------------------------------------------------------
/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 | You need to enable JavaScript to run this app.
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 |
9 | Recipes
10 |
11 |
12 |
13 |
14 |
23 |
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 |
9 |
10 |
11 |
12 |
{user.name}
13 |
14 |
15 |
18 |
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 |
13 |
14 | {{range .recipes}}
15 |
16 |
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 | You need to enable JavaScript to run this app.
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 |
9 | Recipes
10 |
11 |
12 |
13 |
14 |
23 |
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 |
9 |
10 |
11 |
12 |
{user.name}
13 |
14 |
15 |
18 |
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"
--------------------------------------------------------------------------------