├── .DS_Store ├── .env ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Jenkinsfile ├── LICENSE.md ├── Makefile ├── README.md ├── authservice ├── Makefile ├── README.md ├── data │ └── user_data.go ├── jwt │ ├── jwt.go │ └── jwt_test.go ├── middleware │ └── middleware.go ├── signin.go ├── signup.go └── swagger.yaml ├── clientclaims ├── README.md ├── claimstatus.go ├── claimstatusdir │ └── abc@example_com.jpeg ├── clientclaims.go └── saveimgdir │ └── download.jpeg ├── couponservice ├── README.md ├── couponregionclient │ └── main.go ├── couponstream.go ├── deployments │ ├── go-micro-deployment.yaml │ ├── go-micro-service.yaml │ ├── redis-deployment.yaml │ └── redis-service.yaml └── store │ └── coupon.go ├── functional_tests └── transformer_test.go ├── go.mod ├── go.sum ├── main.go ├── monitormodule ├── README.md ├── config.yaml └── monitor.go ├── ordertransformerservice ├── .DS_Store ├── README.md ├── json_store │ ├── orders_APAC.json │ ├── orders_EU.json │ ├── orders_NA.json │ └── orders_SA.json ├── region_rules │ ├── directive_APAC.yaml │ ├── directive_EU.yaml │ ├── directive_NA.yaml │ └── directive_SA.yaml ├── store │ ├── orders.go │ └── rules.go ├── transformer.go └── transformer_test.go └── productservice ├── README.md ├── productservice.go └── store └── product.go /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shadowshot-x/micro-product-go/0ac7419b0399e3023901b3a2e04020503b4cbf5e/.DS_Store -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | JWT_SECRET = S0m3_R4n90m_sss 2 | MYSQL_SECRET = "root:password@tcp(127.0.0.1:3306)/products?charset=utf8&parseTime=True" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Go ### 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | ### Go Patch ### 19 | /vendor/ 20 | /Godeps/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Citizen Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of Micro Product Go is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in Micro Product Go to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open [Source/Culture/Tech] Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open [source/culture/tech] citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people's personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone's consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Weapons Policy 47 | 48 | No weapons will be allowed at Micro Product Go events, community spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons include but are not limited to guns, explosives (including fireworks), and large knives such as those used for hunting or display, as well as any other item used for the purpose of causing injury or harm to others. Anyone seen in possession of one of these items will be asked to leave immediately, and will only be allowed to return without the weapon. Community members are further expected to comply with all state and local laws on this matter. 49 | 50 | ## 6. Consequences of Unacceptable Behavior 51 | 52 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 53 | 54 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 55 | 56 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 57 | 58 | ## 7. Reporting Guidelines 59 | 60 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. ujjwal26599@gmail.com. 61 | 62 | 63 | 64 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 65 | 66 | ## 8. Addressing Grievances 67 | 68 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 69 | 70 | 71 | 72 | ## 9. Scope 73 | 74 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business. 75 | 76 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 77 | 78 | ## 10. Contact info 79 | 80 | ujjwal26599@gmail.com 81 | 82 | ## 11. License and attribution 83 | 84 | The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 85 | 86 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 87 | 88 | _Revision 2.3. Posted 6 March 2017._ 89 | 90 | _Revision 2.2. Posted 4 February 2016._ 91 | 92 | _Revision 2.1. Posted 23 June 2014._ 93 | 94 | _Revision 2.0, adopted by the [Stumptown Syndicate](http://stumptownsyndicate.org) board on 10 January 2013. Posted 17 March 2013._ 95 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to Product-Micro contributing guide 2 | 3 | Thank you for investing your time in contributing to my project! 4 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 5 | 6 | ## New contributor guide 7 | 8 | See the [README](README.md) to get an overview of the project. Here are some helpful resources to get you comfortable with open source contribution: 9 | 10 | - [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github) 11 | - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git) 12 | - [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) 13 | - [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) 14 | 15 | 16 | ## Getting started 17 | 18 | See [the introduction authentication microservice in golang](https://medium.com/@ujjwal26599/build-an-authentication-microservice-in-golang-from-the-scratch-3b452fa876c0) to understand the Authentication Microservice. 19 | 20 | Before making changes, see what [types of contributions](/contributing/types-of-contributions.md) we accept. Some of them don't require writing even a single line of code :sparkles:. 21 | 22 | ### Issues 23 | 24 | #### Create a new issue 25 | 26 | If you spot a problem with the code or logic, please search if the issue already exists. If a related issue doesn't exist, you can open a new issue. 27 | 28 | #### Solve an issue 29 | 30 | Scan through our existing issues to find one that interests you. You can narrow down the search using `labels` as filters. This repository is participating in HacktoberFest. Your Approved Pull Request will contribute to the fest. 31 | 32 | ### Make Changes 33 | 34 | #### Make changes in the UI 35 | 36 | Click **Make a contribution** at the bottom of any docs page to make small changes such as a typo, sentence fix, or a broken link. This takes you to the `.md` file where you can make your changes and [create a pull request](#pull-request) for a review. 37 | 38 | 39 | 40 | ### Pull Request 41 | 42 | This Repository is taking part in HacktoberFest. If your PR gets approved, it will be reflected on your HacktoberFest DashBoard. 43 | 44 | When you're done making the changes, open a pull request, often referred to as a PR. 45 | - Fill out the "Ready for review" template so we can review your PR. This template helps reviewers understand your changes and the purpose of your pull request. 46 | - Don't forget to link PR to issue if you are solving one. 47 | - Once you submit your PR, a I will review your proposal. I may ask a few basic questions or request for additional information. Make sure the Pull Request you make for a new feature are followed by creation of an Issue. 48 | - As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations). 49 | - If you run into any merge issues, checkout this [git tutorial](https://lab.github.com/githubtraining/managing-merge-conflicts) to help you resolve merge conflicts and other issues. 50 | 51 | ### Your PR is merged! 52 | 53 | Once your PR is merged, your contributions will be publicly visible on the project -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM golang:1.16-alpine 3 | 4 | WORKDIR /server 5 | 6 | COPY go.mod ./ 7 | COPY go.sum ./ 8 | 9 | RUN go mod download 10 | COPY ./ ./ 11 | 12 | RUN go build -o /product-go-micro 13 | 14 | EXPOSE 9090 15 | 16 | CMD [ "/product-go-micro" ] -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | 3 | agent any 4 | 5 | tools { 6 | go 'go1.14' 7 | } 8 | environment { 9 | GO114MODULE = 'on' 10 | CGO_ENABLED = 0 11 | GOPATH = "${JENKINS_HOME}/jobs/${JOB_NAME}/builds/${BUILD_ID}" 12 | } 13 | 14 | stages { 15 | stage("unit-test") { 16 | steps { 17 | echo 'UNIT TEST EXECUTION STARTED' 18 | sh 'make unit-tests' 19 | } 20 | } 21 | stage("functional-test") { 22 | steps { 23 | echo 'FUNCTIONAL TEST EXECUTION STARTED' 24 | sh 'make functional-tests' 25 | } 26 | } 27 | stage("build") { 28 | steps { 29 | echo 'BUILD EXECUTION STARTED' 30 | sh 'go version' 31 | sh 'go get ./...' 32 | sh 'docker build . -t shadowshotx/product-go-micro' 33 | } 34 | } 35 | stage('Docker Push') { 36 | agent any 37 | steps { 38 | withCredentials([usernamePassword(credentialsId: 'dockerhub', passwordVariable: 'dockerhubPassword', usernameVariable: 'dockerhubUser')]) { 39 | sh "docker login -u ${env.dockerhubUser} -p ${env.dockerhubPassword}" 40 | sh 'docker push shadowshotx/product-go-micro' 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ujjwal Sharma 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | go run main.go 3 | 4 | unit-tests: 5 | go test ./... 6 | 7 | functional-tests: 8 | go test ./functional_tests/transformer_test.go 9 | 10 | build: 11 | docker build . -t shadowshotx/product-go-micro 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Server Project Best Practices 2 | 3 | ### PS. Each Directory is a microservice and has its own Readme and each directory uses some new tech. So, do check it out. 4 | 5 | ## Dependency Injection :- 6 | In simple words, we want our functions and packages to receive the objects they depend on ie. We dont want to declare new instances inside packages to have control over them. For Eg :- Using Structs to declare the private methods and save logger as a variables. The methods can access the value of logger using `g.logger` in their domain. 7 | 8 | ## Handle Timeouts :- 9 | This is to prevent DOS attacks. Dont make requests make infinitely if your server crashes. 10 | 11 | ## Graceful Shutdown :- 12 | Wait until current requests are handled and then shutdown the server. We use signal interrupts for this with channels 13 | 14 | ## Using JSON Encoder :- 15 | Sometimes it is better to use Encoder than json.Marshal as we dont have to use additional buffer. This will matter when there are a lot of concurrent go routines being processed. It is also a bit faster. 16 | 17 | ## Validation of Data is very Important :- 18 | Middleware ensures Connectivity between 2 or more types applications or components. You can write your validation code in middleware to make sure data is validated and then only goes to your handlers 19 | 20 | ## Running using Docker Image 21 | `docker build . -t shadowshotx/product-go-micro` 22 | 23 | `docker run --network host -d ` 24 | 25 | ## Using CORS 26 | Cross Origin Resource Sharing. Good security measure to protect the websites from malicious calls. It defines origins allowed to talk to the API. If source is not allowed, we reject the request.If we need to pass authentication headers like cookies the Origin Source should NOT be *. Like in this case where authentication using JWT is happening. 27 | 28 | ## File Handling 29 | To Handle files usign a Golang server, we should not store them on our disk but on some cloud storage facility like S3. We can use our code to retrieve and send them. Golang's HTTP Fileserver helps to deal with sending the files from server. It provides a Handler for this. 30 | 31 | To deal with very large files to make the data transfer efficient, we use GZipping. Time to compress and decompress is less than time taken for the unzipped file to be transferred. 32 | -------------------------------------------------------------------------------- /authservice/Makefile: -------------------------------------------------------------------------------- 1 | swagger: 2 | GO11MODULE=off swagger generate spec -o ./swagger.yaml --scan-models -------------------------------------------------------------------------------- /authservice/README.md: -------------------------------------------------------------------------------- 1 | # Authentication Microservice. \[REST\] 2 | ## JWT 3 | I will create my own JWT for authentication. This requires a key. H256 algo will be used to generate the signature. 4 | 5 | [Link for the Article](https://mattermost.com/blog/how-to-build-an-authentication-microservice-in-golang-from-scratch/) 6 | 7 | JWT has the following components 8 | 1. Header \[base64 encoded\] 9 | 2. Payload \[base64 encoded\] 10 | 3. Signature \[combination of header and payload hashed using a private key\] 11 | 12 | ## Swagger 13 | This will be used for Documenting the API 14 | 15 | ## Schema for the User 16 | 1. Email 17 | 2. Full Name 18 | 3. Password hash 19 | 4. Username 20 | 5. CreateDate 21 | 22 | ## Things Learnt\[Covered in Article\] :- 23 | 1. Using Gorilla MUX for Routing and Subroutes 24 | 2. Implementing my own JWT Logic 25 | 3. Creation of Modules and handling module specific data 26 | 4. Writing Handlers for Sign In and Sign Up 27 | 5. Creating Middleware 28 | 6. Containerize the Application using Docker 29 | 30 | ## Running the Hashed Command 31 | 32 | `curl http://localhost:9090/auth/signin --header 'Email:abc@gmail.com' --header 'Passwordhash:hashedme1'` 33 | 34 | `curl http://localhost:9090/auth/signup --request POST --header 'Email:newuser@example.com' --header 'Passwordhash:hashedme1' --header 'Username:user77' --header 'Fullname:test user'` 35 | -------------------------------------------------------------------------------- /authservice/data/user_data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // User struct 4 | type user struct { 5 | email string 6 | username string 7 | passwordhash string 8 | fullname string 9 | createDate string 10 | role int 11 | } 12 | 13 | // currently this is acting as our database 14 | var userList = []user{ 15 | { 16 | email: "abc@gmail.com", 17 | username: "abc12", 18 | passwordhash: "hashedme1", 19 | fullname: "abc def", 20 | createDate: "1631600786", 21 | role: 1, 22 | }, 23 | { 24 | email: "chekme@example.com", 25 | username: "checkme34", 26 | passwordhash: "hashedme2", 27 | fullname: "check me", 28 | createDate: "1631600837", 29 | role: 0, 30 | }, 31 | } 32 | 33 | // based on the email id provided, finds the user object 34 | // can be seen as the main constructor to start validation 35 | func GetUserObject(email string) (user, bool) { 36 | //needs to be replaces using Database 37 | for _, user := range userList { 38 | if user.email == email { 39 | return user, true 40 | } 41 | } 42 | return user{}, false 43 | } 44 | 45 | // checks if the password hash is valid 46 | func (u *user) ValidatePasswordHash(pswdhash string) bool { 47 | return u.passwordhash == pswdhash 48 | } 49 | 50 | // this simply adds the user to the list 51 | func AddUserObject(email string, username string, passwordhash string, fullname string, role int) bool { 52 | // declare the new user object 53 | newUser := user{ 54 | email: email, 55 | passwordhash: passwordhash, 56 | username: username, 57 | fullname: fullname, 58 | role: role, 59 | } 60 | // check if a user already exists 61 | for _, ele := range userList { 62 | if ele.email == email || ele.username == username { 63 | return false 64 | } 65 | } 66 | userList = append(userList, newUser) 67 | return true 68 | } 69 | -------------------------------------------------------------------------------- /authservice/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "os" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | CORRUPT_TOKEN = "Corrupt Token" 17 | INVALID_TOKEN = "Invalid Token" 18 | EXPIRED_TOKEN = "Expired Token" 19 | ) 20 | 21 | // claims are attributes. 22 | // Aud - audience 23 | // Iss - issuer 24 | // Exp - expiration of the Token 25 | type ClaimsMap struct { 26 | Aud string 27 | Iss string 28 | Exp string 29 | } 30 | 31 | // GetSecret fetches the value for the JWT_SECRET from the environment variable 32 | func GetSecret() string { 33 | return os.Getenv("JWT_SECRET") 34 | } 35 | 36 | // Function for generating the tokens. 37 | func GenerateToken(header string, payload ClaimsMap, secret string) (string, error) { 38 | // create a new hash of type sha256. We pass the secret key to it 39 | // sha256 is a symmetric cryptographic algorithm 40 | h := hmac.New(sha256.New, []byte(secret)) 41 | 42 | // We base encode the header which is a normal string 43 | header64 := base64.StdEncoding.EncodeToString([]byte(header)) 44 | // We then Marshal the payload which is a map. This converts it to a string of JSON. 45 | // Now we base encode this string 46 | payloadstr, err := json.Marshal(payload) 47 | if err != nil { 48 | return string(payloadstr), fmt.Errorf("Error generating token when encoding payload to string: %w", err) 49 | } 50 | payload64 := base64.StdEncoding.EncodeToString(payloadstr) 51 | 52 | // Now add the encoded string. 53 | message := header64 + "." + payload64 54 | 55 | // We have the unsigned message ready. This is simply concat of header and payload 56 | unsignedStr := header + string(payloadstr) 57 | 58 | // we write this to the SHA256 to hash it. We can use this to generate the signature now 59 | h.Write([]byte(unsignedStr)) 60 | signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) 61 | 62 | //Finally we have the token 63 | tokenStr := message + "." + signature 64 | return tokenStr, nil 65 | } 66 | 67 | // This helps in validating the token 68 | func ValidateToken(token string, secret string) error { 69 | // JWT has 3 parts separated by '.' 70 | splitToken := strings.Split(token, ".") 71 | // if length is not 3, we know that the token is corrupt 72 | if len(splitToken) != 3 { 73 | return errors.New(CORRUPT_TOKEN) 74 | } 75 | 76 | // decode the header and payload back to strings 77 | header, err := base64.StdEncoding.DecodeString(splitToken[0]) 78 | if err != nil { 79 | return err 80 | } 81 | payload, err := base64.StdEncoding.DecodeString(splitToken[1]) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | //again create the signature 87 | unsignedStr := string(header) + string(payload) 88 | h := hmac.New(sha256.New, []byte(secret)) 89 | h.Write([]byte(unsignedStr)) 90 | 91 | signature := base64.StdEncoding.EncodeToString(h.Sum(nil)) 92 | 93 | // if both the signature dont match, this means token is wrong 94 | if signature != splitToken[2] { 95 | return errors.New(INVALID_TOKEN) 96 | } 97 | 98 | //Unmarshal payload into ClaimsMap struct 99 | var payloadMap ClaimsMap 100 | json.Unmarshal(payload, &payloadMap) 101 | 102 | //Check if token is expired 103 | if payloadMap.Exp < fmt.Sprint(time.Now().Unix()) { 104 | return errors.New(EXPIRED_TOKEN) 105 | } 106 | 107 | // This means the token matches 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /authservice/jwt/jwt_test.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestTokenValidation(t *testing.T) { 10 | 11 | secret := GetSecret() 12 | longExpiryClaims := ClaimsMap{ 13 | Aud: "frontend.knowsearch.ml", 14 | Iss: "knowsearch.ml", 15 | Exp: fmt.Sprint(time.Now().Add(time.Minute * 60).Unix()), 16 | } 17 | longExpiryToken, err := GenerateToken("HS256", longExpiryClaims, secret) 18 | if err != nil { 19 | t.Error("Token generation failed") 20 | } 21 | //Token with long expiry date must not be expired at this time 22 | if EXPIRED_TOKEN == fmt.Sprint(ValidateToken(longExpiryToken, secret)) { 23 | t.Error("Token must not be expired") 24 | } 25 | 26 | //Corrupt token i.e without 3 sections must throw 'Token is corrupt' on validation 27 | corruptTokenString := "randomcorrupttokenstring" 28 | if CORRUPT_TOKEN != fmt.Sprint(ValidateToken(corruptTokenString, secret)) { 29 | t.Error("Should throw 'Token is corrupt' for corrupt tokens") 30 | } 31 | 32 | //Invalid token i.e signature mismatched token must throw 'Invalid Token' on validation 33 | invalidTokenString := longExpiryToken + "randomsignaturesuffix" 34 | if INVALID_TOKEN != fmt.Sprint(ValidateToken(invalidTokenString, secret)) { 35 | t.Error("Should throw 'Invalid Token' for invalid tokens") 36 | } 37 | 38 | shortExpiryClaims := ClaimsMap{ 39 | Aud: "frontend.knowsearch.ml", 40 | Iss: "knowsearch.ml", 41 | Exp: fmt.Sprint(time.Now().Unix()), 42 | } 43 | shortExpiryToken, err := GenerateToken("HS256", shortExpiryClaims, secret) 44 | if err != nil { 45 | t.Error("Token generation failed") 46 | } 47 | //Sleep for 5 seconds to ensure token is expired 48 | time.Sleep(5 * time.Second) 49 | 50 | //Expired token must throw 'Token Expired' on validation 51 | if EXPIRED_TOKEN != fmt.Sprint(ValidateToken(shortExpiryToken, secret)) { 52 | t.Error("Failed to detect expired token") 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /authservice/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/shadowshot-x/micro-product-go/authservice/jwt" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | // TokenMiddleware is the token validation route handler 12 | type TokenMiddleware struct { 13 | logger *zap.Logger 14 | } 15 | 16 | // NewTokenMiddleware returns a frsh Token controller 17 | func NewTokenMiddleware(logger *zap.Logger) *TokenMiddleware { 18 | return &TokenMiddleware{ 19 | logger: logger, 20 | } 21 | } 22 | 23 | // Middleware itself returns a function that is a Handler. it is executed for each request. 24 | // We want all our routes for REST to be authenticated. So, we validate the token 25 | func (ctrl *TokenMiddleware) TokenValidationMiddleware(next http.Handler) http.Handler { 26 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 27 | // check if token is present 28 | if _, ok := r.Header["Token"]; !ok { 29 | ctrl.logger.Warn("Token was not found in the header") 30 | rw.WriteHeader(http.StatusUnauthorized) 31 | rw.Write([]byte("Token Missing")) 32 | return 33 | } 34 | token := r.Header["Token"][0] 35 | 36 | secret := jwt.GetSecret() 37 | if secret == "" { 38 | ctrl.logger.Error("Empty JWT secret") 39 | rw.WriteHeader(http.StatusInternalServerError) 40 | rw.Write([]byte("Internal Server Error")) 41 | return 42 | } 43 | 44 | err := jwt.ValidateToken(token, secret) 45 | if err != nil { 46 | errInString := fmt.Sprint(err) 47 | ctrl.logger.Error(errInString, zap.String("token", token)) 48 | if errInString == jwt.CORRUPT_TOKEN || errInString == jwt.INVALID_TOKEN || errInString == jwt.EXPIRED_TOKEN { 49 | rw.WriteHeader(http.StatusUnauthorized) 50 | } else { 51 | rw.WriteHeader(http.StatusInternalServerError) 52 | } 53 | rw.Write([]byte(errInString)) 54 | return 55 | } 56 | // rw.WriteHeader(http.StatusOK) 57 | // rw.Write([]byte("Authorized Token")) 58 | 59 | // this calls the next function. If not included, the router wont entertain any requests 60 | next.ServeHTTP(rw, r) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /authservice/signin.go: -------------------------------------------------------------------------------- 1 | // Package Authentication of Product API 2 | // 3 | // Documentation for Authentication of Product API 4 | // 5 | // Schemes : http 6 | // BasePath : /auth 7 | // Version : 1.0.0 8 | // 9 | // Consumes: 10 | // - application/json 11 | // 12 | // Produces: 13 | // - application/json 14 | // swagger:meta 15 | package authservice 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "net/http" 21 | "time" 22 | 23 | "github.com/prometheus/client_golang/prometheus" 24 | "github.com/prometheus/client_golang/prometheus/promauto" 25 | "github.com/shadowshot-x/micro-product-go/authservice/data" 26 | "github.com/shadowshot-x/micro-product-go/authservice/jwt" 27 | "go.uber.org/zap" 28 | ) 29 | 30 | var ( 31 | signinRequests = promauto.NewCounter(prometheus.CounterOpts{ 32 | Name: "signin_total", 33 | Help: "Total number of signup requests", 34 | }) 35 | signinSuccess = promauto.NewCounter(prometheus.CounterOpts{ 36 | Name: "signin_success", 37 | Help: "Successful signup requests", 38 | }) 39 | signinFail = promauto.NewCounter(prometheus.CounterOpts{ 40 | Name: "signin_fail", 41 | Help: "Failed signup requests", 42 | }) 43 | signinError = promauto.NewCounter(prometheus.CounterOpts{ 44 | Name: "signin_error", 45 | Help: "Erroneous signup requests", 46 | }) 47 | ) 48 | 49 | // SigninController is the Signin route handler 50 | type SigninController struct { 51 | logger *zap.Logger 52 | promSigninTotal prometheus.Counter 53 | promSigninSuccess prometheus.Counter 54 | promSigninFail prometheus.Counter 55 | promSigninError prometheus.Counter 56 | } 57 | 58 | // NewSigninController returns a frsh Signin controller 59 | func NewSigninController(logger *zap.Logger) *SigninController { 60 | return &SigninController{ 61 | logger: logger, 62 | promSigninTotal: signinRequests, 63 | promSigninSuccess: signinSuccess, 64 | promSigninFail: signinFail, 65 | promSigninError: signinError, 66 | } 67 | } 68 | 69 | // we need this function to be private 70 | func getSignedToken() (string, error) { 71 | // we make a JWT Token here with signing method of ES256 and claims. 72 | // claims are attributes. 73 | // Aud - audience 74 | // Iss - issuer 75 | // Exp - expiration of the Token 76 | // token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 77 | // "Aud": "frontend.knowsearch.ml", 78 | // "Iss": "knowsearch.ml", 79 | // "Exp": string(time.Now().Add(time.Minute * 1).Unix()), 80 | // }) 81 | claimsMap := jwt.ClaimsMap{ 82 | Aud: "frontend.knowsearch.ml", 83 | Iss: "knowsearch.ml", 84 | Exp: fmt.Sprint(time.Now().Add(time.Minute * 1).Unix()), 85 | } 86 | 87 | secret := jwt.GetSecret() 88 | if secret == "" { 89 | return "", errors.New("empty JWT secret") 90 | } 91 | 92 | header := "HS256" 93 | tokenString, err := jwt.GenerateToken(header, claimsMap, secret) 94 | if err != nil { 95 | return tokenString, err 96 | } 97 | return tokenString, nil 98 | } 99 | 100 | // searches the user in the database. 101 | func validateUser(email string, passwordHash string) (bool, error) { 102 | usr, exists := data.GetUserObject(email) 103 | if !exists { 104 | return false, errors.New("user does not exist") 105 | } 106 | passwordCheck := usr.ValidatePasswordHash(passwordHash) 107 | 108 | if !passwordCheck { 109 | return false, nil 110 | } 111 | return true, nil 112 | } 113 | 114 | // This will be supplied to the MUX router. It will be called when signin request is sent 115 | // if user not found or not validates, returns the Unauthorized error 116 | // if found, returns the JWT back. [How to return this?] 117 | func (ctrl *SigninController) SigninHandler(rw http.ResponseWriter, r *http.Request) { 118 | // increment total singin requests 119 | ctrl.promSigninTotal.Inc() 120 | 121 | // validate the request first. 122 | if _, ok := r.Header["Email"]; !ok { 123 | ctrl.logger.Warn("Email was not found in the header") 124 | rw.WriteHeader(http.StatusBadRequest) 125 | rw.Write([]byte("Email Missing")) 126 | ctrl.promSigninFail.Inc() 127 | return 128 | } 129 | if _, ok := r.Header["Passwordhash"]; !ok { 130 | ctrl.logger.Warn("Passwordhash was not found in the header") 131 | rw.WriteHeader(http.StatusBadRequest) 132 | rw.Write([]byte("Passwordhash Missing")) 133 | ctrl.promSigninFail.Inc() 134 | return 135 | } 136 | // lets see if the user exists 137 | valid, err := validateUser(r.Header["Email"][0], r.Header["Passwordhash"][0]) 138 | if err != nil { 139 | // this means either the user does not exist 140 | ctrl.logger.Warn("User does not exist", zap.String("email", r.Header["Email"][0])) 141 | rw.WriteHeader(http.StatusUnauthorized) 142 | rw.Write([]byte("User Does not Exist")) 143 | ctrl.promSigninFail.Inc() 144 | return 145 | } 146 | 147 | if !valid { 148 | // this means the password is wrong 149 | ctrl.logger.Warn("Password is wrong", zap.String("email", r.Header["Email"][0])) 150 | rw.WriteHeader(http.StatusUnauthorized) 151 | rw.Write([]byte("Incorrect Password")) 152 | ctrl.promSigninFail.Inc() 153 | return 154 | } 155 | tokenString, err := getSignedToken() 156 | if err != nil { 157 | ctrl.logger.Error("unable to sign the token", zap.Error(err)) 158 | rw.WriteHeader(http.StatusInternalServerError) 159 | rw.Write([]byte("Internal Server Error")) 160 | ctrl.promSigninError.Inc() 161 | return 162 | } 163 | ctrl.logger.Info("Token sign", zap.String("token", tokenString), zap.String("email", r.Header["Email"][0])) 164 | 165 | rw.WriteHeader(http.StatusOK) 166 | rw.Write([]byte(tokenString)) 167 | ctrl.promSigninSuccess.Inc() 168 | } 169 | -------------------------------------------------------------------------------- /authservice/signup.go: -------------------------------------------------------------------------------- 1 | package authservice 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/prometheus/client_golang/prometheus/promauto" 8 | "github.com/shadowshot-x/micro-product-go/authservice/data" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | var ( 13 | singupRequests = promauto.NewCounter(prometheus.CounterOpts{ 14 | Name: "signup_total", 15 | Help: "Total number of signup requests", 16 | }) 17 | signupSuccess = promauto.NewCounter(prometheus.CounterOpts{ 18 | Name: "signup_success", 19 | Help: "Successful signup requests", 20 | }) 21 | signupFail = promauto.NewCounter(prometheus.CounterOpts{ 22 | Name: "signup_fail", 23 | Help: "Failed signup requests", 24 | }) 25 | ) 26 | 27 | // SignupController is the Signup route handler 28 | type SignupController struct { 29 | logger *zap.Logger 30 | promSignupTotal prometheus.Counter 31 | promSignupSuccess prometheus.Counter 32 | promSignupFail prometheus.Counter 33 | } 34 | 35 | // NewSignupController returns a frsh Signup controller 36 | func NewSignupController(logger *zap.Logger) *SignupController { 37 | return &SignupController{ 38 | logger: logger, 39 | promSignupTotal: singupRequests, 40 | promSignupSuccess: signupSuccess, 41 | promSignupFail: signupFail, 42 | } 43 | } 44 | 45 | // adds the user to the database of users 46 | func (ctrl *SignupController) SignupHandler(rw http.ResponseWriter, r *http.Request) { 47 | // we increment the signup request counter 48 | ctrl.promSignupTotal.Inc() 49 | 50 | // extra error handling should be done at server side to prevent malicious attacks 51 | if _, ok := r.Header["Email"]; !ok { 52 | ctrl.logger.Warn("Email was not found in the header") 53 | rw.WriteHeader(http.StatusBadRequest) 54 | rw.Write([]byte("Email Missing")) 55 | ctrl.promSignupFail.Inc() 56 | return 57 | } 58 | if _, ok := r.Header["Username"]; !ok { 59 | ctrl.logger.Warn("Username was not found in the header") 60 | rw.WriteHeader(http.StatusBadRequest) 61 | rw.Write([]byte("Username Missing")) 62 | ctrl.promSignupFail.Inc() 63 | return 64 | } 65 | if _, ok := r.Header["Passwordhash"]; !ok { 66 | ctrl.logger.Warn("Passwordhash was not found in the header") 67 | rw.WriteHeader(http.StatusBadRequest) 68 | rw.Write([]byte("Passwordhash Missing")) 69 | ctrl.promSignupFail.Inc() 70 | return 71 | } 72 | if _, ok := r.Header["Fullname"]; !ok { 73 | ctrl.logger.Warn("Fullname was not found in the header") 74 | rw.WriteHeader(http.StatusBadRequest) 75 | rw.Write([]byte("Fullname Missing")) 76 | ctrl.promSignupFail.Inc() 77 | return 78 | } 79 | 80 | // validate and then add the user 81 | check := data.AddUserObject(r.Header["Email"][0], r.Header["Username"][0], r.Header["Passwordhash"][0], 82 | r.Header["Fullname"][0], 0) 83 | // if false means username already exists 84 | if !check { 85 | ctrl.logger.Warn("User already exists", zap.String("email", r.Header["Email"][0]), zap.String("username", r.Header["Username"][0])) 86 | rw.WriteHeader(http.StatusConflict) 87 | rw.Write([]byte("Email or Username already exists")) 88 | ctrl.promSignupFail.Inc() 89 | return 90 | } 91 | ctrl.logger.Info("User created", zap.String("email", r.Header["Email"][0]), zap.String("username", r.Header["Username"][0])) 92 | rw.WriteHeader(http.StatusOK) 93 | rw.Write([]byte("User Created")) 94 | // this will mean the request was successfully added 95 | ctrl.promSignupSuccess.Inc() 96 | } 97 | -------------------------------------------------------------------------------- /authservice/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: /auth 2 | consumes: 3 | - application/json 4 | info: 5 | description: Documentation for Authentication of Product API 6 | title: of Product API 7 | version: 1.0.0 8 | paths: {} 9 | produces: 10 | - application/json 11 | schemes: 12 | - http 13 | swagger: "2.0" 14 | -------------------------------------------------------------------------------- /clientclaims/README.md: -------------------------------------------------------------------------------- 1 | # File Handling Microservice 2 | 3 | ## MultiPart File Uploading 4 | We are using multi part HTTP Requests which means each request can contain multiple key value pairs for file uploading. Currently we are storing the files in our server itself but a good practice is to rent out a cloud storage and write the logic in handler to send files to this. There are many mechanisms for this including Data Pipelines with Firehose in AWS. I have worked with S3 and it works the best for file upload and is a cheap option. 5 | 6 | ## GZipped File Downloading 7 | We are using the gzipped file downloads to send to the user. We want the data transfer to be as efficient as possible and zipping helps to ease the load on data transfer latency. Sending the files in this way requires the users to process the Gzipped Files at their end. 8 | 9 | ## Things Learnt 10 | 1. Handling Files in Golang 11 | 2. MultiPart HTTP Form Requests 12 | 3. Gzipping the File and sending to the Client 13 | 4. File Handling using OS Module 14 | 15 | ## Running the File Upload Service 16 | 17 | `curl -v -F file=@/home/ujjwal/Downloads/download.jpeg --header 'Token:SFMyNTY=.eyJhdWQiOiJmcm9udGVuZC5rbm93c2VhcmNoLm1sIiwiZXhwIjoiMTYzMjk4NzQwNiIsImlzcyI6Imtub3dzZWFyY2gubWwifQ==./4hTLnjLW1tt5tdHAq6hph1R7IGm5uJWehheZrMu24M=' localhost:9090/claims/upload ` 18 | 19 | ## Running the File Download Service 20 | 21 | `curl --header 'Token:SFMyNTY=.eyJhdWQiOiJmcm9udGVuZC5rbm93c2VhcmNoLm1sIiwiZXhwIjoiMTYzMjk4NzQwNiIsImlzcyI6Imtub3dzZWFyY2gubWwifQ==./4hTLnjLW1tt5tdHAq6hph1R7IGm5uJWehheZrMu24M=' --header 'Email:abc@example.com' localhost:9090/claims/download -o file.png` -------------------------------------------------------------------------------- /clientclaims/claimstatus.go: -------------------------------------------------------------------------------- 1 | package clientclaims 2 | 3 | import ( 4 | "compress/gzip" 5 | "io/ioutil" 6 | "net/http" 7 | "strings" 8 | 9 | "go.uber.org/zap" 10 | ) 11 | 12 | // DownloadController is the Download route handler 13 | type DownloadController struct { 14 | logger *zap.Logger 15 | } 16 | 17 | // NewDownloadController returns a frsh Download controller 18 | func NewDownloadController(logger *zap.Logger) *DownloadController { 19 | return &DownloadController{ 20 | logger: logger, 21 | } 22 | } 23 | 24 | func (ctrl *DownloadController) DownloadFile(rw http.ResponseWriter, r *http.Request) { 25 | if _, ok := r.Header["Email"]; !ok { 26 | ctrl.logger.Warn("Email was not found in the header") 27 | rw.WriteHeader(http.StatusBadRequest) 28 | rw.Write([]byte("Email Missing")) 29 | return 30 | } 31 | 32 | fileName := strings.Replace(r.Header["Email"][0], ".", "_", -1) 33 | files, err := ioutil.ReadDir("./clientclaims/claimstatusdir/") 34 | if err != nil { 35 | ctrl.logger.Error("Unable to read the claim directory", zap.Error(err)) 36 | rw.WriteHeader(http.StatusInternalServerError) 37 | rw.Write([]byte("Unable to read the claim directory")) 38 | return 39 | } 40 | 41 | for _, claim := range files { 42 | if strings.Contains(claim.Name(), fileName) { 43 | writer := gzip.NewWriter(rw) 44 | writer.Name = "claim-status.jpg" 45 | writer.Comment = "Status of your Claim from the Accounting department" 46 | 47 | fileContent, err := ioutil.ReadFile(claim.Name()) 48 | if err != nil { 49 | ctrl.logger.Error("Unable to read file", zap.String("file", claim.Name()), zap.Error(err)) 50 | rw.WriteHeader(http.StatusInternalServerError) 51 | rw.Write([]byte("Unable to read the claim file")) 52 | 53 | err := closeGzipStream(writer) 54 | if err != nil { 55 | ctrl.logger.Error("Unable to close gzip stream", zap.String("file", claim.Name()), zap.Error(err)) 56 | rw.WriteHeader(http.StatusInternalServerError) 57 | rw.Write([]byte("Unable to read the claim file")) 58 | return 59 | } 60 | return 61 | } 62 | _, err = writer.Write(fileContent) 63 | if err != nil { 64 | ctrl.logger.Error("Unable to write the Gzipped File", zap.String("file", claim.Name()), zap.Error(err)) 65 | rw.WriteHeader(http.StatusInternalServerError) 66 | rw.Write([]byte("Error writing the Gzipped File")) 67 | 68 | err := closeGzipStream(writer) 69 | if err != nil { 70 | ctrl.logger.Error("Unable to close the gzip stream", zap.String("file", claim.Name()), zap.Error(err)) 71 | rw.WriteHeader(http.StatusInternalServerError) 72 | rw.Write([]byte("Unable to read the claim file")) 73 | return 74 | } 75 | return 76 | } 77 | err = closeGzipStream(writer) 78 | if err != nil { 79 | ctrl.logger.Error("Unable to close the gzip stream", zap.String("file", claim.Name()), zap.Error(err)) 80 | rw.WriteHeader(http.StatusInternalServerError) 81 | rw.Write([]byte("Unable to read the claim file")) 82 | return 83 | } 84 | ctrl.logger.Info("File Gzipped and Downloaded", zap.String("file", claim.Name())) 85 | rw.WriteHeader(http.StatusOK) 86 | rw.Write([]byte("File Gzipped and Downloaded")) 87 | } 88 | } 89 | ctrl.logger.Warn("Claim not yet generated", zap.Int("nbFile", len(files))) 90 | rw.WriteHeader(http.StatusLocked) 91 | rw.Write([]byte("Claim not yet generated")) 92 | } 93 | 94 | func closeGzipStream(writer *gzip.Writer) error { 95 | err := writer.Flush() 96 | if err != nil { 97 | return err 98 | } 99 | err = writer.Close() 100 | if err != nil { 101 | return err 102 | } 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /clientclaims/claimstatusdir/abc@example_com.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shadowshot-x/micro-product-go/0ac7419b0399e3023901b3a2e04020503b4cbf5e/clientclaims/claimstatusdir/abc@example_com.jpeg -------------------------------------------------------------------------------- /clientclaims/clientclaims.go: -------------------------------------------------------------------------------- 1 | package clientclaims 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | 9 | "go.uber.org/zap" 10 | ) 11 | 12 | // UploadController is the Upload route handler 13 | type UploadController struct { 14 | logger *zap.Logger 15 | } 16 | 17 | // NewUploadController returns a frsh Upload controller 18 | func NewUploadController(logger *zap.Logger) *UploadController { 19 | return &UploadController{ 20 | logger: logger, 21 | } 22 | } 23 | 24 | // Upload File Handler 25 | func (ctrl *UploadController) UploadFile(rw http.ResponseWriter, r *http.Request) { 26 | 27 | // Create a MultiPart form with size of 128 KB main memory 28 | err := r.ParseMultipartForm(128 * 1024) 29 | // if this allocation is not possible or there is error in parsing 30 | if err != nil { 31 | ctrl.logger.Warn("Unable to parse request body", zap.Error(err)) 32 | rw.WriteHeader(http.StatusBadRequest) 33 | rw.Write([]byte("Request Data Faulty")) 34 | return 35 | } 36 | //we use formfile. The request should have the file key with the file as value 37 | // handler we get here has details about the file 38 | file, handler, err := r.FormFile("file") 39 | if err != nil { 40 | ctrl.logger.Warn("Unable to return file for the provided form key", zap.String("key", "file"), zap.Error(err)) 41 | rw.WriteHeader(http.StatusBadRequest) 42 | rw.Write([]byte("Request file of nonexistant/incorrect format")) 43 | return 44 | } 45 | 46 | //create the file in the directory and copy the file to the folder 47 | f, err := os.OpenFile("./clientclaims/saveimgdir/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) 48 | if err != nil { 49 | ctrl.logger.Warn("Unable to create file", zap.String("file", fmt.Sprintf("./clientclaims/saveimgdir/%s", handler.Filename)), zap.Error(err)) 50 | rw.WriteHeader(http.StatusInternalServerError) 51 | rw.Write([]byte("Unable to save the file to the servers disk")) 52 | return 53 | } 54 | io.Copy(f, file) 55 | 56 | ctrl.logger.Info("File Uploaded Successfully", zap.String("file", fmt.Sprintf("./clientclaims/saveimgdir/%s", handler.Filename))) 57 | // this means file upload successful 58 | rw.WriteHeader(http.StatusOK) 59 | rw.Write([]byte("File Uploaded Successfully")) 60 | } 61 | -------------------------------------------------------------------------------- /clientclaims/saveimgdir/download.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shadowshot-x/micro-product-go/0ac7419b0399e3023901b3a2e04020503b4cbf5e/clientclaims/saveimgdir/download.jpeg -------------------------------------------------------------------------------- /couponservice/README.md: -------------------------------------------------------------------------------- 1 | # Redis Database Usage with Minikube 2 | 3 | Temporary Link - https://docs.google.com/document/d/10-Jr488fS3_iUGBnu8iyh10Ou1BL8Hija0T9f3bDAvk/edit?usp=sharing 4 | 5 | We are building a coupon service which is essentially like a streaming service for some requests. We will use Redis Streams for Queuing. We will then make the image of the service and deploy the redis instance and image to a minikube kubernetes cluster. The essential commands will be in this Readme. 6 | 7 | ## Working of the Coupon Service 8 | 9 | We have 4 major regions (APAC, NA, SA, EU) with running service who want coupons to distribute to their local users. They constantly demand coupon codes from our server. As merchants add the coupons, they become available for redemption. 10 | ## Workflow of the Architecture 11 | 12 | ![coupon-redis-architecture drawio](https://user-images.githubusercontent.com/43992469/146631429-cd2b8236-c710-41dc-b29b-70ac6b089f76.png) 13 | 14 | ## A look indise the Minikube Cluster 15 | 16 | ![k8-micro-redis drawio](https://user-images.githubusercontent.com/43992469/146636850-ca7bfc11-6ea7-4b66-b0de-2679b82442fa.png) 17 | 18 | ## What we are Building? 19 | We are making a microservice that has a coupon code distribution instance. This can be demanded from different consumers. The coupons are stored in our database categorized based on merchants. The merchants add their codes to our database using REST API. For each merchant we have a Redis list. After every coupon addition, messages are added to redis stream based on the coupons in the list. These streams are subscibed by consumers who then get code based on requests. We also purge all the older requests every 24 hours if they have not been fulfilled. 20 | 21 | 22 | 23 | ## Redis Database 24 | 25 | Redis is an open source database that can be treated as a key-value store that we can use for Database purposes(NoSQL), Caching and for Message brokering. 26 | 27 | We will run redis in a Docker container. This way we will have the image ready and we can deploy this to minikube. 28 | 29 | `$ docker run --name coupon-redis-instance -p 6379:6379 -d redis` 30 | 31 | ## Redis Structure 32 | 33 | For every new merchant, lets add a new list to the Redis database with vendorname as the key. Each time vendor adds a coupon list, we should add it to the redis db list corresponding to that vendor name. 34 | 35 | `curl http://localhost:9090/coupon/addcoupon --request POST --header 'Couponname:off_50_flat' --header 'Couponvendor:vendor1' --header 'Coupondescription:Avail flat 50 off on all products' --header 'Couponcode:EU778' --header 'Couponregion:EU'` 36 | 37 | `curl http://localhost:9090/coupon/getvendorcoupons --request GET --header 'Vendorname:vendor1'` 38 | 39 | `curl http://localhost:9090/coupon/delregionstream --request DELETE --header 'Region:EU'` 40 | 41 | ## Running the Redis Consumer Code [for testing with Minikube] 42 | 43 | We are using consumer groups of redis to read from a stream. It is a good practice as we want each message to used just once and in FIFO order. 44 | 45 | `go run couponservice/couponregionclient/main.go` 46 | 47 | ## Minikube Instructions 48 | 49 | We will push our image to dockerhub so that this can be downloaded by minikube. Do remember to login first. 50 | 51 | `docker push shadowshotx/product-go-micro` 52 | 53 | Now, we will deploy the .yaml files to create deployment and services. 54 | 55 | Lets start the minikube instance. 56 | `minikube start` 57 | 58 | Remember to have `minikube` and `kubectl` installed on your local. 59 | 60 | `kubectl apply -f couponservice/deployments/redis-deployment.yaml` 61 | 62 | `kubectl apply -f couponservice/deployments/redis-service.yaml` 63 | 64 | Now wait for the Redis Instances to get up and running. Then run the following commands. 65 | 66 | `kubectl apply -f couponservice/deployments/go-micro-deployment.yaml` 67 | 68 | `kubectl apply -f couponservice/deployments/go-micro-service.yaml` 69 | 70 | Now, check the status of the services and pods by running 71 | 72 | `kubectl get pods` 73 | 74 | `kubectl get services` 75 | 76 | Remember to stop your minikube instance. 77 | 78 | `minikube stop` 79 | 80 | -------------------------------------------------------------------------------- /couponservice/couponregionclient/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/go-redis/redis" 8 | ) 9 | 10 | // A very simple consumer application 11 | func main() { 12 | // instances that connect to the stream to read data are consumers. 13 | // we use consumer groups to get read scalability. 14 | 15 | // declare the essentials and make redis connections 16 | var host = "localhost" 17 | var port = "6379" 18 | if os.Getenv("REDIS_HOST") != "" { 19 | host = os.Getenv("REDIS_HOST") 20 | } 21 | if string(os.Getenv("REDIS_PORT")) != "" { 22 | port = string(os.Getenv("REDIS_PORT")) 23 | } 24 | client := redis.NewClient(&redis.Options{ 25 | Addr: host + ":" + port, 26 | Password: "", 27 | DB: 0, 28 | }) 29 | 30 | area := "coupon-EU" 31 | 32 | // lets create a consumer group 33 | status, err := client.XGroupCreate(area, "client-EU", "0").Result() 34 | fmt.Println(status) 35 | 36 | if err != nil { 37 | fmt.Println("Could not create group", err) 38 | return 39 | } 40 | 41 | // XReadGroup is used by groups to read from stream. 42 | // Here we are using a special consumer id ">". This ensures that the message we are getting has 43 | // never been sent to another consumer. 44 | // However, if you dont specify ">", you will get a pending messages which have not been acknowledged. 45 | // XAck command removes the message from the history. 46 | 47 | // if we set NoAck true, this means our message is added to the message history of pending messages. 48 | // We can call XAck when the coupon code has been used by the client. 49 | streamData, err := client.XReadGroup(&redis.XReadGroupArgs{ 50 | Group: "client-EU", 51 | Consumer: "consumer-1", 52 | Streams: []string{area, ">"}, 53 | Count: 1, 54 | NoAck: true, 55 | }).Result() 56 | 57 | // Here XReadGroup will wait indefinitely for messages if the stream is empty. As soon as 58 | // there is a message, it will poll the COUNT number of messages and then exit. 59 | 60 | if err != nil { 61 | fmt.Println("Could not Read from stream", err) 62 | return 63 | } 64 | 65 | fmt.Println(streamData) 66 | } 67 | -------------------------------------------------------------------------------- /couponservice/couponstream.go: -------------------------------------------------------------------------------- 1 | package couponservice 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/go-redis/redis/v8" 11 | "github.com/jasonlvhit/gocron" 12 | "github.com/shadowshot-x/micro-product-go/couponservice/store" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | var ctx = context.Background() 17 | 18 | // StreamController is the Upload route handler 19 | type StreamController struct { 20 | logger *zap.Logger 21 | rdbi *redis.Client 22 | } 23 | 24 | // NewCouponStreamController returns a frsh Stream controller 25 | func NewCouponStreamController(logger *zap.Logger, instance *redis.Client) *StreamController { 26 | return &StreamController{ 27 | logger: logger, 28 | rdbi: instance, 29 | } 30 | } 31 | 32 | func handleNotInHeader(rw http.ResponseWriter, r *http.Request, param string) { 33 | rw.WriteHeader(http.StatusBadRequest) 34 | rw.Write([]byte(fmt.Sprintf("%s Missing", param))) 35 | } 36 | 37 | // function to flush to the database 38 | func flushdb(rdbi *redis.Client) { 39 | rdbi.FlushDB(ctx) 40 | } 41 | 42 | // This function is called by the main server to get the redis instance. 43 | // The instance is again returned to the controller for this package. Now all the functions can access this like zap logger. 44 | func RedisInstanceGenerator(logger *zap.Logger) *redis.Client { 45 | 46 | // declare the essentials and make redis connections 47 | var host = "localhost" 48 | var port = "6379" 49 | 50 | // get credentials from the Environment Variables 51 | if os.Getenv("REDIS_HOST") != "" { 52 | host = os.Getenv("REDIS_HOST") 53 | } 54 | if string(os.Getenv("REDIS_PORT")) != "" { 55 | port = string(os.Getenv("REDIS_PORT")) 56 | } 57 | 58 | // Declare the redis client 59 | // We can keep a password as an environment variable. However, I wont use this for simplicity. 60 | client := redis.NewClient(&redis.Options{ 61 | Addr: host + ":" + port, 62 | Password: "", 63 | DB: 0, 64 | }) 65 | 66 | // check status of connection. 67 | // It returns "PONG" 68 | _, err := client.Ping(ctx).Result() 69 | 70 | if err != nil { 71 | logger.Error("Redis connection failed", zap.Error(err)) 72 | return nil 73 | } 74 | 75 | // Lets call zap to get the instance. 76 | logger.Info("Redis Instance Started", zap.Any("server details", map[string]interface{}{ 77 | "Host": client.Options().Addr, 78 | "Network": client.Options().Network, 79 | })) 80 | 81 | // this will make sure that every day at 12:00 AM, the database is flushed. 82 | gocron.Every(1).Day().At("00:00").Do(flushdb, client) 83 | 84 | // return the client to be used to router controller. 85 | return client 86 | } 87 | 88 | func (ctrl *StreamController) AddCouponList(rw http.ResponseWriter, r *http.Request) { 89 | if _, ok := r.Header["Couponname"]; !ok { 90 | ctrl.logger.Warn("Coupon Name was not found in the header") 91 | handleNotInHeader(rw, r, "name") 92 | return 93 | } 94 | if _, ok := r.Header["Couponvendor"]; !ok { 95 | ctrl.logger.Warn("Coupon Vendor was not found in the header") 96 | handleNotInHeader(rw, r, "Vendor") 97 | return 98 | } 99 | if _, ok := r.Header["Couponcode"]; !ok { 100 | ctrl.logger.Warn("Coupon Inventory was not found in the header") 101 | handleNotInHeader(rw, r, "Inventory") 102 | return 103 | } 104 | if _, ok := r.Header["Coupondescription"]; !ok { 105 | ctrl.logger.Warn("Coupon Description was not found in the header") 106 | handleNotInHeader(rw, r, "Description") 107 | return 108 | } 109 | if _, ok := r.Header["Couponregion"]; !ok { 110 | ctrl.logger.Warn("Coupon Region was not found in the header") 111 | handleNotInHeader(rw, r, "Region") 112 | return 113 | } 114 | 115 | coupon := store.Coupon{ 116 | Name: r.Header["Couponname"][0], 117 | VendorName: r.Header["Couponvendor"][0], 118 | Code: r.Header["Couponcode"][0], 119 | Description: r.Header["Coupondescription"][0], 120 | Region: r.Header["Couponregion"][0], 121 | } 122 | 123 | couponJson, err := json.Marshal(coupon) 124 | if err != nil { 125 | ctrl.logger.Error("Cannot Marshal coupon", zap.Error(err)) 126 | rw.WriteHeader(http.StatusInternalServerError) 127 | rw.Write([]byte("An Internal server error ocurred")) 128 | return 129 | } 130 | 131 | // this automatically handles cases for new users. 132 | // if we want an already existing list and not create a new list, we use lpushx/rpushx 133 | _, err = ctrl.rdbi.RPush(ctx, coupon.VendorName, []interface{}{couponJson}).Result() 134 | if err != nil { 135 | ctrl.logger.Error("Fatal Redis Error", zap.Error(err)) 136 | rw.WriteHeader(http.StatusInternalServerError) 137 | rw.Write([]byte("An Internal server error ocurred")) 138 | return 139 | } 140 | 141 | // We have 4 regions. I want 4 streams to be there for each region. This will be given by Couponregion. 142 | // Any number of consumers can poll from this stream coming from that region. 143 | 144 | // it will be wise to put a string check over here. We dont need to add new streams if region is corrupt in Http request 145 | // Also, we need pretty flexible region names. We need to minimize Hardcoding in our application. 146 | region := r.Header["Couponregion"][0] 147 | if region != "APAC" && region != "NA" && region != "EU" && region == "SA" { 148 | // region does not exist. 149 | rw.WriteHeader(http.StatusBadRequest) 150 | rw.Write([]byte("Provided Region does not exist")) 151 | return 152 | } 153 | 154 | err = ctrl.rdbi.XAdd(ctx, &redis.XAddArgs{ 155 | Stream: "coupon-" + r.Header["Couponregion"][0], 156 | Values: map[string]interface{}{ 157 | "coupon": couponJson, 158 | }, 159 | }).Err() 160 | 161 | if err != nil { 162 | ctrl.logger.Error("Fatal Redis Error", zap.Error(err)) 163 | rw.WriteHeader(http.StatusInternalServerError) 164 | rw.Write([]byte("An Internal server error ocurred")) 165 | return 166 | } 167 | 168 | ctrl.logger.Info("Coupon added to the stream") 169 | 170 | rw.WriteHeader(http.StatusOK) 171 | rw.Write([]byte("Coupon added to Region Stream")) 172 | } 173 | 174 | // GetCouponForInternalValidation returns all the coupons of the certain vendor. 175 | // we might need this for internal validation if the need arises. It must be remembered 176 | // that the Redis database will be flushed every 24 hours. We dont want to fill the Database with excess data. 177 | func (ctrl *StreamController) GetCouponForInternalValidation(rw http.ResponseWriter, r *http.Request) { 178 | // we ger vendor name in the header. 179 | if _, ok := r.Header["Vendorname"]; !ok { 180 | ctrl.logger.Warn("Vendor Name was not found in the header") 181 | handleNotInHeader(rw, r, "vendor") 182 | return 183 | } 184 | 185 | res, err := ctrl.rdbi.LRange(ctx, r.Header["Vendorname"][0], 0, 0).Result() 186 | 187 | if err != nil { 188 | ctrl.logger.Error("Fatal Redis Error", zap.Error(err)) 189 | rw.WriteHeader(http.StatusInternalServerError) 190 | rw.Write([]byte("An Internal server error ocurred")) 191 | return 192 | } 193 | 194 | ctrl.logger.Info("Redis Info", zap.Any("listdata", res)) 195 | 196 | userResponse, err := json.Marshal(res) 197 | 198 | if err != nil { 199 | ctrl.logger.Error("Cannot Marshal []string", zap.Error(err)) 200 | rw.WriteHeader(http.StatusInternalServerError) 201 | rw.Write([]byte("An Internal server error ocurred")) 202 | return 203 | } 204 | 205 | rw.WriteHeader(http.StatusOK) 206 | rw.Write([]byte(userResponse)) 207 | } 208 | 209 | // Lets assume that at any moment, You want to purge the stream. We need a method for that too. 210 | // We should use role authentication here. 211 | func (ctrl *StreamController) PurgeStream(rw http.ResponseWriter, r *http.Request) { 212 | //Lets take in the region 213 | if _, ok := r.Header["Region"]; !ok { 214 | ctrl.logger.Warn("Region was not found in the header") 215 | handleNotInHeader(rw, r, "region") 216 | return 217 | } 218 | 219 | streamName := "coupon-" + r.Header["Region"][0] 220 | 221 | // this deletes the redis stream for the given region 222 | status, err := ctrl.rdbi.Del(ctx, streamName).Result() 223 | 224 | if err != nil { 225 | ctrl.logger.Error("Fatal Redis Error", zap.Error(err)) 226 | rw.WriteHeader(http.StatusInternalServerError) 227 | rw.Write([]byte("An Internal server error ocurred")) 228 | return 229 | } 230 | 231 | if status >= 1 { 232 | ctrl.logger.Info("Stream Deleted", zap.Any("streamName", streamName)) 233 | rw.WriteHeader(http.StatusOK) 234 | rw.Write([]byte("Successfully deleted the stream")) 235 | return 236 | } 237 | 238 | ctrl.logger.Info("Stream Does not Exist", zap.Any("streamName", streamName)) 239 | rw.WriteHeader(http.StatusBadRequest) 240 | rw.Write([]byte("Stream does not exist.")) 241 | } 242 | -------------------------------------------------------------------------------- /couponservice/deployments/go-micro-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment # Type of Kubernetes resource 3 | metadata: 4 | name: product-go-micro # Unique name of the Deployment 5 | spec: 6 | replicas: 3 # Number of pods to run at any given time in the Node 7 | selector: 8 | matchLabels: 9 | app: product-go-micro # This deployment applies to any Pods matching the specified label 10 | template: # This deployment will create a set of pods using the configurations in this template 11 | metadata: 12 | labels: # The labels that will be applied to all of the pods in this deployment 13 | app: product-go-micro 14 | spec: 15 | containers: 16 | - name: product-go-micro # Name of the containers which will be run as pods. 17 | image: shadowshotx/product-go-micro # We specify the image name in spec. This will be taken from docker hub 18 | imagePullPolicy: IfNotPresent # We only pull if the latest image is not present 19 | resources: 20 | requests: 21 | cpu: 50m # You can even specify the resources for each container! If this exceeds, you can spin up another instance 22 | memory: 50Mi 23 | ports: # This is the main port where the container will wait for requests inside the node. 24 | - containerPort: 9090 25 | env: # Specified Environment variables to connect to Redis. 26 | - name: REDIS_HOST 27 | value: redis-coupon-db 28 | - name: REDIS_PORT 29 | value: "6379" -------------------------------------------------------------------------------- /couponservice/deployments/go-micro-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: product-go-micro-service # Service name 5 | spec: 6 | type: NodePort # This can be accessed using Node IP from outside 7 | ports: 8 | - name: http 9 | port: 9090 # Our service will run on http Port 9090 10 | targetPort: 9090 # Our application to exposed to this port 9090 11 | selector: 12 | app: product-go-micro # this selector matches labels and assigns all pods this service -------------------------------------------------------------------------------- /couponservice/deployments/redis-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: redis-coupon-db # This will be the name of our deployment. 5 | labels: 6 | app: redis # This is the label of the deployment 7 | spec: 8 | selector: 9 | matchLabels: # We use these to select the labeled pods 10 | app: redis 11 | mservice: coupon 12 | type: db 13 | replicas: 2 # I would run 2 pods simultaneously 14 | template: 15 | metadata: 16 | labels: # Labels to be applied to the Pods in this deployment 17 | app: redis 18 | mservice: coupon 19 | type: db 20 | spec: # This is the docker container specifications 21 | containers: 22 | - name: coupon-db 23 | image: redis # Image name 24 | resources: 25 | requests: 26 | cpu: 100m 27 | memory: 100Mi 28 | ports: # Main container port. We need not interact with this. 29 | - containerPort: 6379 -------------------------------------------------------------------------------- /couponservice/deployments/redis-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: redis-coupon-db # Name of the Kubernetes resource 5 | labels: # This will match the pods to this service 6 | app: redis 7 | mservice: coupon 8 | type: db 9 | spec: 10 | type: ClusterIP # ClusterIP is for internal communication only. You cannot access this from outside 11 | ports: 12 | - port: 6379 # Here we bind the ports to port of the service. Make sure the port is not utilized 13 | targetPort: 6379 14 | selector: # Find pods with these labels and map them. 15 | app: redis 16 | mservice: coupon 17 | type: db -------------------------------------------------------------------------------- /couponservice/store/coupon.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | type Coupon struct { 4 | Code string `json:code` 5 | Name string `json:name` 6 | Description string `json:description` 7 | VendorName string `json:vendor` 8 | Region string `json:region` 9 | } 10 | -------------------------------------------------------------------------------- /functional_tests/transformer_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | // +build integration 3 | 4 | package tests 5 | 6 | import ( 7 | "github.com/jarcoal/httpmock" 8 | "github.com/shadowshot-x/micro-product-go/ordertransformerservice" 9 | "go.uber.org/zap" 10 | "net/http" 11 | "net/http/httptest" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | // we would again need http mocks to prevent external calls 17 | // but here we will test our code end to end. 18 | // We just want to test that the call runs from starting controller to the end http call 19 | func TestHealthCheckHandler(t *testing.T) { 20 | log, _ := zap.NewProduction() 21 | defer log.Sync() 22 | 23 | transc := ordertransformerservice.NewTransformerController(log) 24 | transc.Store_json_dir = "../ordertransformerservice/json_store/" 25 | transc.Region_rules_dir = "../ordertransformerservice/region_rules/" 26 | 27 | httpmock.Activate() 28 | defer httpmock.DeactivateAndReset() 29 | 30 | httpmock.RegisterResponder("POST", "https://httpbin.org/post", func(req *http.Request) (*http.Response, error) { 31 | resp, err := httpmock.NewJsonResponse(200, "200 OK") 32 | if err != nil { 33 | return httpmock.NewStringResponse(500, "Error Generating Response"), nil 34 | } 35 | return resp, nil 36 | }) 37 | 38 | req, err := http.NewRequest("GET", "/transformer/transform", nil) 39 | if err != nil { 40 | t.Fatalf("Got error in creating Http Test Request, %v", err) 41 | } 42 | 43 | outputCatcher := httptest.NewRecorder() 44 | h := http.HandlerFunc(transc.TransformerHandler) 45 | h.ServeHTTP(outputCatcher, req) 46 | 47 | outputBody := outputCatcher.Body 48 | 49 | want := "Files Parsed and Validated" 50 | if !strings.Contains(outputBody.String(), want) { 51 | t.Errorf("Got Unexpected Output in the HTTP Response. want: %v, got: %v\n", outputBody.String(), want) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shadowshot-x/micro-product-go 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/go-redis/redis v6.15.5+incompatible 7 | github.com/go-redis/redis/v8 v8.11.4 8 | github.com/gorilla/handlers v1.5.1 9 | github.com/gorilla/mux v1.8.0 10 | github.com/jarcoal/httpmock v1.1.0 11 | github.com/jasonlvhit/gocron v0.0.1 12 | github.com/jinzhu/gorm v1.9.16 13 | github.com/joho/godotenv v1.4.0 14 | github.com/prometheus/client_golang v1.11.0 15 | go.uber.org/zap v1.19.1 16 | gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 17 | gopkg.in/yaml.v2 v2.4.0 18 | ) 19 | 20 | require ( 21 | github.com/beorn7/perks v1.0.1 // indirect 22 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 23 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 24 | github.com/felixge/httpsnoop v1.0.2 // indirect 25 | github.com/go-sql-driver/mysql v1.5.0 // indirect 26 | github.com/golang/protobuf v1.5.2 // indirect 27 | github.com/gorilla/websocket v1.4.2 // indirect 28 | github.com/jinzhu/inflection v1.0.0 // indirect 29 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 30 | github.com/prometheus/client_model v0.2.0 // indirect 31 | github.com/prometheus/common v0.26.0 // indirect 32 | github.com/prometheus/procfs v0.6.0 // indirect 33 | go.uber.org/atomic v1.7.0 // indirect 34 | go.uber.org/multierr v1.6.0 // indirect 35 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 // indirect 36 | google.golang.org/protobuf v1.26.0 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 3 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 4 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 8 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 9 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 10 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 11 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 12 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 13 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 14 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 16 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 17 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= 22 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 23 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 24 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 25 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 26 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 27 | github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 28 | github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= 29 | github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 30 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 31 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 32 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 33 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 34 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 35 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 36 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 37 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 38 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 39 | github.com/go-redis/redis v6.15.5+incompatible h1:pLky8I0rgiblWfa8C1EV7fPEUv0aH6vKRaYHc/YRHVk= 40 | github.com/go-redis/redis v6.15.5+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 41 | github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= 42 | github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= 43 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 44 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 45 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 46 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 47 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 48 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 49 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 50 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 51 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 52 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 53 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 54 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 55 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 56 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 57 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 58 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 59 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 60 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 61 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 62 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 63 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 64 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 65 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 66 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 67 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 68 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 69 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 70 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 71 | github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= 72 | github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= 73 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 74 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 75 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 76 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 77 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 78 | github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE= 79 | github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= 80 | github.com/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU= 81 | github.com/jasonlvhit/gocron v0.0.1/go.mod h1:k9a3TV8VcU73XZxfVHCHWMWF9SOqgoku0/QlY2yvlA4= 82 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= 83 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 84 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 85 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 86 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 87 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 88 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 89 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 90 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 91 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 92 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 93 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 94 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 95 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 96 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 97 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 98 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 99 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 100 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 101 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 102 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 103 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 104 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 105 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 106 | github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= 107 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 108 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 109 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 110 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 111 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 112 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 113 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 114 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 115 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 116 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 117 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 118 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 119 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 120 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 121 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 122 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 123 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 124 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 125 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 126 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 127 | github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= 128 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 129 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 130 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 131 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 132 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 133 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 134 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 135 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 136 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 137 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 138 | github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= 139 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 140 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 141 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 142 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 143 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 144 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 145 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 146 | github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= 147 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 148 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 149 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 150 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 151 | github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= 152 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 153 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 154 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 155 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 156 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 157 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 158 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 159 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 160 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 161 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 162 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 163 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 164 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 165 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 166 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 167 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 168 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= 169 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 170 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 171 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 172 | go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= 173 | go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 174 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 175 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 176 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 177 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 178 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 179 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 180 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 181 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 182 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 183 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 184 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 185 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 186 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 187 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 188 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 189 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 190 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 191 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 192 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 193 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 194 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 195 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 196 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 197 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 198 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 199 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 200 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 201 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 202 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 203 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 204 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 205 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 206 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 207 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 208 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 209 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 210 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 211 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 212 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 213 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 214 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 215 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 216 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 217 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 218 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 219 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 220 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 221 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 222 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 223 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 224 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 225 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 226 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 227 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 228 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 229 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 230 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 231 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= 232 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 233 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 234 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 235 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 236 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 237 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 238 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 239 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 240 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 241 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 242 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 243 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 244 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 245 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 246 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 247 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 248 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 249 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 250 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 251 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 252 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 253 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 254 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 255 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 256 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 257 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 258 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 259 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 260 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 261 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 262 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 263 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 264 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 265 | gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 h1:sY2a+y0j4iDrajJcorb+a0hJIQ6uakU5gybjfLWHlXo= 266 | gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376/go.mod h1:BHKOc1m5wm8WwQkMqYBoo4vNxhmF7xg8+xhG8L+Cy3M= 267 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 268 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 269 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 270 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 271 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 272 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 273 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 274 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 275 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 276 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 277 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 278 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 279 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 280 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | 10 | gohandlers "github.com/gorilla/handlers" 11 | "github.com/gorilla/mux" 12 | "github.com/joho/godotenv" 13 | "github.com/prometheus/client_golang/prometheus/promhttp" 14 | "github.com/shadowshot-x/micro-product-go/authservice" 15 | "github.com/shadowshot-x/micro-product-go/authservice/middleware" 16 | "github.com/shadowshot-x/micro-product-go/clientclaims" 17 | "github.com/shadowshot-x/micro-product-go/couponservice" 18 | "github.com/shadowshot-x/micro-product-go/monitormodule" 19 | "github.com/shadowshot-x/micro-product-go/ordertransformerservice" 20 | "github.com/shadowshot-x/micro-product-go/productservice" 21 | "go.uber.org/zap" 22 | ) 23 | 24 | func PingHandler(rw http.ResponseWriter, r *http.Request) { 25 | rw.Write([]byte("Your App seems Healthy")) 26 | } 27 | 28 | func simplePostHandler(rw http.ResponseWriter, r *http.Request) { 29 | fileName, err := os.OpenFile("./metricDetails.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 30 | if err != nil { 31 | log.Fatal("error in file ops", zap.Error(err)) 32 | } 33 | defer fileName.Close() 34 | reqBody, err := ioutil.ReadAll(r.Body) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | fileName.Write([]byte(reqBody)) 39 | fileName.Write([]byte("\n")) 40 | 41 | rw.Write([]byte("Post Request Recieved for the Success")) 42 | } 43 | 44 | func main() { 45 | log, _ := zap.NewProduction() 46 | defer log.Sync() 47 | 48 | log.Info("Starting...") 49 | 50 | err := godotenv.Load(".env") 51 | 52 | if err != nil { 53 | log.Error("Error loading .env file", zap.Error(err)) 54 | } 55 | 56 | err = monitormodule.MonitorBinder(log) 57 | if err != nil { 58 | fmt.Println(err) 59 | return 60 | } 61 | 62 | mainRouter := mux.NewRouter() 63 | 64 | suc := authservice.NewSignupController(log) 65 | sic := authservice.NewSigninController(log) 66 | uc := clientclaims.NewUploadController(log) 67 | dc := clientclaims.NewDownloadController(log) 68 | tm := middleware.NewTokenMiddleware(log) 69 | pc := productservice.NewProductController(log) 70 | transc := ordertransformerservice.NewTransformerController(log) 71 | 72 | redisInstance := couponservice.RedisInstanceGenerator(log) 73 | cc := couponservice.NewCouponStreamController(log, redisInstance) 74 | 75 | // ping function 76 | mainRouter.HandleFunc("/ping", PingHandler) 77 | mainRouter.HandleFunc("/checkRoutine", simplePostHandler).Methods("POST") 78 | 79 | // We will create a Subrouter for Authentication service 80 | // route for sign up and signin. The Function will come from auth-service package 81 | // checks if given header params already exists. If not,it adds the user 82 | authRouter := mainRouter.PathPrefix("/auth").Subrouter() 83 | authRouter.HandleFunc("/signup", suc.SignupHandler).Methods("POST") 84 | 85 | // The Signin will send the JWT Token back as we are making microservices. 86 | // JWT token will make sure that other services are protected. 87 | // So, ultimately, we would need a middleware 88 | authRouter.HandleFunc("/signin", sic.SigninHandler).Methods("GET") 89 | 90 | // File Upload SubRouter 91 | claimsRouter := mainRouter.PathPrefix("/claims").Subrouter() 92 | claimsRouter.HandleFunc("/upload", uc.UploadFile) 93 | claimsRouter.HandleFunc("/download", dc.DownloadFile) 94 | claimsRouter.Use(tm.TokenValidationMiddleware) 95 | 96 | //Initialize the Gorm connection 97 | // pc.InitGormConnection() 98 | productRouter := mainRouter.PathPrefix("/product").Subrouter() 99 | productRouter.HandleFunc("/getprods", pc.GetAllProductsHandler).Methods("GET") 100 | productRouter.HandleFunc("/addprod", pc.AddProductHandler).Methods("POST") 101 | productRouter.HandleFunc("/getprodbyid", pc.GetAllProductByIdHandler).Methods("GET") 102 | productRouter.HandleFunc("/deletebyid", pc.DeleteProductHandler).Methods("DELETE") 103 | productRouter.HandleFunc("/customquery", pc.CustomQueryHandler).Methods("GET", "POST") 104 | 105 | //Coupon Service SubRouter 106 | couponRouter := mainRouter.PathPrefix("/coupon").Subrouter() 107 | couponRouter.HandleFunc("/addcoupon", cc.AddCouponList).Methods("POST") 108 | couponRouter.HandleFunc("/getvendorcoupons", cc.GetCouponForInternalValidation).Methods("GET") 109 | couponRouter.HandleFunc("/delregionstream", cc.PurgeStream).Methods("DELETE") 110 | 111 | // Transformer Service SubRouter 112 | transformerOrderRouter := mainRouter.PathPrefix("/transformer").Subrouter() 113 | transformerOrderRouter.HandleFunc("/transform", transc.TransformerHandler).Methods("GET") 114 | 115 | // CORS Header 116 | cors := gohandlers.CORS(gohandlers.AllowedOrigins([]string{"http://localhost:3000"})) 117 | 118 | // Adding Prometheus http handler to expose the metrics 119 | // this will display our metrics as well as some standard metrics 120 | mainRouter.Path("/metrics").Handler(promhttp.Handler()) 121 | // Add the Middleware to different subrouter 122 | // HTTP Server 123 | // Add Time outs 124 | server := &http.Server{ 125 | Addr: ":9090", 126 | Handler: cors(mainRouter), 127 | } 128 | err = server.ListenAndServe() 129 | if err != nil { 130 | log.Error("Error Booting the Server", zap.Error(err)) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /monitormodule/README.md: -------------------------------------------------------------------------------- 1 | # Monitor Module 2 | 3 | Temporary Blog :- https://docs.google.com/document/d/1li7g99RCL5XOL9zyC_34A0jgwbmzDAGTwmshOxjLVsA/edit?usp=sharing 4 | A VERY SIMPLE module to monitor the existing microservices. Some of the main tasks for this issue :- 5 | 1. Modify each microservice to have a function to tell the health of the application. This we can see from the Prometheus points. We can get number of successful metrics and errors. 6 | 2. Implement a new module that will run in parallel inside your main application. It will monitor each microservice application based on a JSON file provided in the config by sending a ping request to the health function after a time interval of some seconds configured in the same JSON. 7 | 3. Module will run purely with GoRoutines, Channels and Sync package. The Module will actually be a data pipeline with a defined architecture taking care of fan-in and fan-out. 8 | 4. The data should be written to a client every 10 seconds. The client will be coded and can have very simple stdout listener using REST API. 9 | 10 | Data Pipeline Steps :- 11 | 12 | - Init function which initiates the GoRoutines based on a JSON file. It syncs the below 3 steps for Data Pipeline. 13 | - Data Aggregation. Have dedicated GoRoutines to monitor application health and send the results of application in an unbuffered channel. 14 | - Data Transformation. Output returned by Health functions is in string format. Have multiple GoRoutines read from this 1 unbuffered channel and transform the data to a specific Output struct and Stringify this into a JSON string. Each GoRoutine posts to its own channel. 15 | - Data Transportation. Have another GoRoutine to aggregate the data using Fan-in mechanism copying data of multiple channels into one channel and post a REST request to the Monitoring Data Ingester. 16 | 17 | ![Monitor Pipeline HLD](https://user-images.githubusercontent.com/43992469/161415406-8a03fd78-d0a6-4be0-a7a8-a32e17eb2322.png) 18 | 19 | ![Concurrent Data Pipeline](https://user-images.githubusercontent.com/43992469/161415458-53038080-e9b9-4fd9-a26c-3bd8d08840be.png) 20 | 21 | 22 | -------------------------------------------------------------------------------- /monitormodule/config.yaml: -------------------------------------------------------------------------------- 1 | metadataregion: APAC 2 | metadatapipelineid: 2022 3 | signin: signin_total,signin_success,signin_fail,signin_error 4 | signup: signup_total,signup_success,signup_fail -------------------------------------------------------------------------------- /monitormodule/monitor.go: -------------------------------------------------------------------------------- 1 | package monitormodule 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "reflect" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "go.uber.org/zap" 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | const endpoint string = "http://localhost:9090/metrics" 19 | const outputEndpoint string = "http://localhost:9090/checkRoutine" 20 | const metricConfFile string = "./monitormodule/config.yaml" 21 | 22 | // this will have endpoints to monitor as per config.yaml 23 | type confMap struct { 24 | Metadataregion string 25 | Metadatapipelineid int 26 | Signin string 27 | Signup string 28 | } 29 | 30 | // we need some metadata of the record 31 | // this can be populated using this struct 32 | type transformationOutput struct { 33 | Time time.Time 34 | Region string 35 | PipelineId int 36 | MetricDetails []string 37 | } 38 | 39 | // dataAggregator combines data from the microservices provided 40 | func dataAggregator(log *zap.Logger, trackData confMap, key string, aggregateChan chan<- []string) error { 41 | // data is fetched every 5 seconds from the microservice specified. 42 | fetchInterval := time.NewTicker(5 * time.Second) 43 | // we start an infinite loop as we want to monitor always. 44 | for { 45 | // the above fetchInterval gives us a channel which we query 46 | select { 47 | case <-fetchInterval.C: 48 | // we send the get request to prometheus http endpoint 49 | resp, err := http.Get(endpoint) 50 | // if there is no error, move ahead, else stop the goroutine immediately 51 | if err != nil { 52 | log.Error("Error", zap.Any("message", err)) 53 | return err 54 | } 55 | // read the response body from the server 56 | body, err := io.ReadAll(resp.Body) 57 | if err != nil { 58 | log.Error(err.Error()) 59 | } 60 | // now we pick out the metrics that we want 61 | // this is based on the conf yaml file 62 | contents := string(body) 63 | aggregate := []string{} 64 | // i have used this to get value dynamically from a struct 65 | reflection := reflect.ValueOf(trackData) 66 | reflectionField := reflect.Indirect(reflection).FieldByName(key) 67 | // get the keys we want to fetch 68 | allKeys := strings.Split(reflectionField.String(), ",") 69 | for _, metricValue := range strings.Split(contents, "\n") { 70 | for _, metricKey := range allKeys { 71 | // check if the given line has the metric we are looking for 72 | if strings.Contains(metricValue, metricKey) { 73 | // ignore the first character 74 | if metricValue[0] != '#' { 75 | aggregate = append(aggregate, metricValue) 76 | } 77 | } 78 | } 79 | } 80 | // save the result in the aggregateChannel 81 | aggregateChan <- aggregate 82 | } 83 | } 84 | } 85 | 86 | // dataTransformer reads from aggregate channel and transorms into a struct. This is output to a unique channel for this goroutine only. 87 | func dataTransformer(log *zap.Logger, aggeregateChan <-chan []string, transformChan chan<- string, routineId int, region string, pipelineid int) error { 88 | // read from the channel 89 | for val := range aggeregateChan { 90 | tr := transformationOutput{ 91 | Time: time.Now(), 92 | Region: region, 93 | PipelineId: pipelineid, 94 | MetricDetails: val, 95 | } 96 | // marshal into byte array 97 | op, err := json.Marshal(tr) 98 | if err != nil { 99 | log.Error("Error Encountered", zap.Int("routine id", routineId), zap.Any("error", err)) 100 | } 101 | // send the output to the respective channel 102 | transformChan <- string(op) 103 | } 104 | return nil 105 | } 106 | 107 | // dataTransportation send a POST request to our endpoint which is on our server only 108 | func dataTransportation(log *zap.Logger, transportChan <-chan string, successChan chan<- string) error { 109 | for op := range transportChan { 110 | // this is fanned in channel 111 | // we send a POST request with our metric data 112 | response, err := http.Post(outputEndpoint, "application/json", bytes.NewBuffer([]byte(op))) 113 | if err != nil { 114 | log.Error("Error in transportation", zap.Error(err)) 115 | } 116 | responseBody, err := ioutil.ReadAll(response.Body) 117 | if err != nil { 118 | log.Error("Error in Reading Output body", zap.Error(err)) 119 | return err 120 | } 121 | // this is a temp channel to show how waitgroups function 122 | successChan <- string(responseBody) 123 | } 124 | return nil 125 | } 126 | 127 | // simple function that prints the channels value available. It takes in the Waitgroup 128 | func showDemo(log *zap.Logger, wg *sync.WaitGroup, finalChan <-chan string) { 129 | log.Info("final Chan ", zap.String("message", <-finalChan)) 130 | wg.Done() 131 | } 132 | 133 | // MonitorBinder binds all the goroutines and combines the output 134 | func MonitorBinder(log *zap.Logger) error { 135 | // read the yaml file and unmarshal to the metricMap 136 | content, err := ioutil.ReadFile(metricConfFile) 137 | if err != nil { 138 | return err 139 | } 140 | // we read the info from config yaml file 141 | metricEndpoints := confMap{} 142 | yaml.Unmarshal(content, &metricEndpoints) 143 | log.Info("Contents", zap.Any("Any", metricEndpoints)) 144 | 145 | // setting up unbuffered channel 146 | aggregateChan := make(chan []string) 147 | // Go routintes to monitor metrcis for 2 sevices 148 | // there are signin and signup 149 | go dataAggregator(log, metricEndpoints, "Signin", aggregateChan) 150 | go dataAggregator(log, metricEndpoints, "Signup", aggregateChan) 151 | 152 | // these are the output channel for transormer function 153 | transform1Chan := make(chan string) 154 | transform2Chan := make(chan string) 155 | 156 | // these 2 goroutines transform the data and send output to transform1Chan and transform2Chan 157 | go dataTransformer(log, aggregateChan, transform1Chan, 1, metricEndpoints.Metadataregion, metricEndpoints.Metadatapipelineid) 158 | go dataTransformer(log, aggregateChan, transform2Chan, 2, metricEndpoints.Metadataregion, metricEndpoints.Metadatapipelineid) 159 | 160 | transportChan := make(chan string) 161 | // now we need fanin of 2 channels to a single channel. This final channel will be passed for transportation 162 | // we can write another goroutine for this 163 | go func() { 164 | for { 165 | select { 166 | case op1 := <-transform1Chan: 167 | transportChan <- op1 168 | case op2 := <-transform2Chan: 169 | transportChan <- op2 170 | case <-time.After(3 * time.Second): 171 | // the last statement imposes a timeout that maybe needed if the above routines are taking time to execute 172 | } 173 | } 174 | }() 175 | // this has te output from our server 176 | successChan := make(chan string) 177 | 178 | // spin 2 gorotines in parallel to get the successChan fast 179 | go dataTransportation(log, transportChan, successChan) 180 | go dataTransportation(log, transportChan, successChan) 181 | 182 | // now we implement a WaitGroup to get the first 5 values only of the response body 183 | // this will 184 | var wg sync.WaitGroup 185 | for i := 0; i < 5; i++ { 186 | // add to the waitgroup. Acts like a semaphore 187 | wg.Add(1) 188 | go showDemo(log, &wg, successChan) 189 | } 190 | // this goroutine only runs after the first 5 responses have been printed 191 | go func() { 192 | wg.Wait() 193 | log.Info("=========== WE ARE DONE FOR THE DEMO ===========") 194 | }() 195 | return nil 196 | } 197 | -------------------------------------------------------------------------------- /ordertransformerservice/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shadowshot-x/micro-product-go/0ac7419b0399e3023901b3a2e04020503b4cbf5e/ordertransformerservice/.DS_Store -------------------------------------------------------------------------------- /ordertransformerservice/README.md: -------------------------------------------------------------------------------- 1 | # Order Transformer Service 2 | 3 | 1. Order Validator and Enrichment Application. Data is provided to us in form of a JSON file. We will have 4 functions :- 4 | i.) One parses and stores multiple JSON files into a single struct to be processed further. It also takes into account a yaml file which defines the rules to validate the JSON. 5 | ii.) Next, based on some yaml rules (taken from a file), we validate this struct. 6 | iii.) Based on the same yaml, we perform data enrichment using text templates. 7 | iv.) This is sent forward to another backend endpoint. 8 | 2. Understanding Mocks and Functional Testing 9 | i.) Write Unit tests for the individual functions. 10 | ii.) Show 2 ways we can mock the external http call. Normal `Patching` and `HTTPMOCKS` 11 | iii.) Write Functional Tests for the whole application (Only this wrapper code. We can extend this to master after this blog). 12 | iv.) Understand more about the domain of unit and functional tests. 13 | 3. Jenkins setup with Docker and connect to Github 14 | 4. Code the CI/CD steps with Jenkins 15 | 5. Continuous Deployment with Minikube, Jenkins and Github. 16 | 6. Cloud Insights : Understanding Hosted Devops Pipelines and their power. 17 | 18 | Keep all the Jenkins files and everything local to a single directory. Make sure Functional tests don't break the current application. 19 | 20 | # Rules 21 | 22 | You can provide rules like 23 | 1. Amount Filter 24 | i.) amountfilter: > 18000 25 | ii.) amountfilter: < 18000 26 | iii.) amountfilter: = 18000 27 | 2. CreateAt Filter 28 | i.) createatfilter: > 1000000 29 | ii.) createatfilter: < 1000000 30 | iii.) createatfilter: = 1000000 31 | 3. Remove the order with product ids from the last list. 32 | i.) blacklistproduct: - 1 33 | - 2 34 | 4. Get orders from a single email id 35 | i.) emailfilter: abc@example.com 36 | -------------------------------------------------------------------------------- /ordertransformerservice/json_store/orders_APAC.json: -------------------------------------------------------------------------------- 1 | { 2 | "region": "APAC", 3 | "orderlist": [{ 4 | "orderid" : "1", 5 | "poductlist": [ "2" ,"3"], 6 | "amount": 33.44, 7 | "useremail": "abc@example.com", 8 | "create_at": "" 9 | }, { 10 | "orderid" : "2", 11 | "poductlist": ["1", "2" ,"3"], 12 | "amount": 33.44, 13 | "useremail": "abc@example.com", 14 | "create_at": "" 15 | }] 16 | } -------------------------------------------------------------------------------- /ordertransformerservice/json_store/orders_EU.json: -------------------------------------------------------------------------------- 1 | { 2 | "region": "APAC", 3 | "orderlist": [{ 4 | "orderid" : "1", 5 | "poductlist": [ "2" ,"3"], 6 | "amount": 33.44, 7 | "useremail": "abc@example.com", 8 | "create_at": "" 9 | }, { 10 | "orderid" : "2", 11 | "poductlist": ["1", "2" ,"3"], 12 | "amount": 33.44, 13 | "useremail": "abc@example.com", 14 | "create_at": "" 15 | }] 16 | } -------------------------------------------------------------------------------- /ordertransformerservice/json_store/orders_NA.json: -------------------------------------------------------------------------------- 1 | { 2 | "region": "APAC", 3 | "orderlist": [{ 4 | "orderid" : "1", 5 | "poductlist": [ "2" ,"3"], 6 | "amount": 33.44, 7 | "useremail": "abc@example.com", 8 | "create_at": "" 9 | }, { 10 | "orderid" : "2", 11 | "poductlist": ["1", "2" ,"3"], 12 | "amount": 33.44, 13 | "useremail": "abc@example.com", 14 | "create_at": "" 15 | }] 16 | } -------------------------------------------------------------------------------- /ordertransformerservice/json_store/orders_SA.json: -------------------------------------------------------------------------------- 1 | { 2 | "region": "APAC", 3 | "orderlist": [{ 4 | "orderid" : "1", 5 | "poductlist": [ "2" ,"3"], 6 | "amount": 33.44, 7 | "useremail": "abc@example.com", 8 | "create_at": "" 9 | }, { 10 | "orderid" : "2", 11 | "poductlist": ["1", "2" ,"3"], 12 | "amount": 33.44, 13 | "useremail": "abc@example.com", 14 | "create_at": "" 15 | }] 16 | } -------------------------------------------------------------------------------- /ordertransformerservice/region_rules/directive_APAC.yaml: -------------------------------------------------------------------------------- 1 | region: APAC 2 | rulelist: 3 | - amountfilter: "<18000" 4 | - blacklistproduct: 5 | - "1" 6 | - emailfilter: abc@example.com -------------------------------------------------------------------------------- /ordertransformerservice/region_rules/directive_EU.yaml: -------------------------------------------------------------------------------- 1 | region: APAC 2 | rulelist: 3 | - amountfilter: ">18000" 4 | - blacklistproduct: 5 | - "1" 6 | - emailfilter: abc@example.com -------------------------------------------------------------------------------- /ordertransformerservice/region_rules/directive_NA.yaml: -------------------------------------------------------------------------------- 1 | region: APAC 2 | rulelist: 3 | - amountfilter: ">18000" 4 | - blacklistproduct: 5 | - "1" 6 | - emailfilter: abc@example.com -------------------------------------------------------------------------------- /ordertransformerservice/region_rules/directive_SA.yaml: -------------------------------------------------------------------------------- 1 | region: APAC 2 | rulelist: 3 | - amountfilter: ">18000" 4 | - blacklistproduct: 5 | - "1" 6 | - emailfilter: abc@example.com -------------------------------------------------------------------------------- /ordertransformerservice/store/orders.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "encoding/json" 4 | 5 | type Orders struct { 6 | Region string 7 | OrderList []Order 8 | } 9 | 10 | type Order struct { 11 | OrderId string 12 | ProductList []string 13 | Amount float64 14 | UserEmail string 15 | UserAddress string 16 | Create_At string 17 | } 18 | 19 | func CreateOrdersStruct(inp []byte) (Orders, error) { 20 | output := Orders{} 21 | err := json.Unmarshal(inp, &output) 22 | if err != nil { 23 | return Orders{}, err 24 | } 25 | return output, nil 26 | } 27 | -------------------------------------------------------------------------------- /ordertransformerservice/store/rules.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "gopkg.in/yaml.v2" 4 | 5 | type Rules struct { 6 | Region string 7 | RuleList []Rule 8 | } 9 | 10 | type Rule struct { 11 | AmountFilter string 12 | BlacklistProduct []string 13 | EmailFilter string 14 | } 15 | 16 | func CreateRulesStruct(inp []byte) (Rules, error) { 17 | output := Rules{} 18 | err := yaml.Unmarshal(inp, &output) 19 | if err != nil { 20 | return Rules{}, err 21 | } 22 | return output, nil 23 | } 24 | -------------------------------------------------------------------------------- /ordertransformerservice/transformer.go: -------------------------------------------------------------------------------- 1 | package ordertransformerservice 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/shadowshot-x/micro-product-go/ordertransformerservice/store" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | type OrderCompilation struct { 19 | APAC store.Orders 20 | EU store.Orders 21 | NA store.Orders 22 | SA store.Orders 23 | } 24 | 25 | type RulesCompilation struct { 26 | rules map[string]store.Rules 27 | } 28 | 29 | type OrderTransformation struct { 30 | filteredOrders []store.Order 31 | } 32 | 33 | const rules_dir = "./ordertransformerservice/region_rules/" 34 | const json_dir = "./ordertransformerservice/json_store/" 35 | 36 | // TransformerHandler is the Transformer route handler 37 | type TransformerController struct { 38 | logger *zap.Logger 39 | Store_json_dir string 40 | Region_rules_dir string 41 | } 42 | 43 | // NewTransformerController returns a frsh Transformer controller 44 | func NewTransformerController(logger *zap.Logger) *TransformerController { 45 | return &TransformerController{ 46 | logger: logger, 47 | Store_json_dir: json_dir, 48 | Region_rules_dir: rules_dir, 49 | } 50 | } 51 | 52 | func parser(store_directory, rules_directory string) (OrderCompilation, RulesCompilation, error) { 53 | files, err := ioutil.ReadDir(store_directory) 54 | if err != nil { 55 | return OrderCompilation{}, RulesCompilation{}, err 56 | } 57 | 58 | allOrders := OrderCompilation{} 59 | for _, file := range files { 60 | orderFile, err := ioutil.ReadFile(store_directory + file.Name()) 61 | if err != nil { 62 | return OrderCompilation{}, RulesCompilation{}, err 63 | } 64 | contents := string(orderFile) 65 | orders, err := store.CreateOrdersStruct(orderFile) 66 | if err != nil { 67 | return OrderCompilation{}, RulesCompilation{}, err 68 | } 69 | 70 | if strings.Contains(contents, "APAC") { 71 | allOrders.APAC = orders 72 | } else if strings.Contains(contents, "EU") { 73 | allOrders.EU = orders 74 | } else if strings.Contains(contents, "NA") { 75 | allOrders.NA = orders 76 | } else if strings.Contains(contents, "SA") { 77 | allOrders.SA = orders 78 | } else { 79 | return OrderCompilation{}, RulesCompilation{}, errors.New("incorrect region provided in rules") 80 | } 81 | } 82 | 83 | files, err = ioutil.ReadDir(rules_directory) 84 | if err != nil { 85 | return OrderCompilation{}, RulesCompilation{}, err 86 | } 87 | 88 | allRules := RulesCompilation{ 89 | rules: map[string]store.Rules{}, 90 | } 91 | for _, file := range files { 92 | rulesFile, err := ioutil.ReadFile(rules_directory + file.Name()) 93 | if err != nil { 94 | return OrderCompilation{}, RulesCompilation{}, err 95 | } 96 | contents := string(rulesFile) 97 | rules, err := store.CreateRulesStruct(rulesFile) 98 | if err != nil { 99 | return OrderCompilation{}, RulesCompilation{}, err 100 | } 101 | 102 | if strings.Contains(contents, "APAC") { 103 | allRules.rules["APAC"] = rules 104 | fmt.Println(allRules.rules["APAC"]) 105 | } else if strings.Contains(contents, "EU") { 106 | allRules.rules["EU"] = rules 107 | } else if strings.Contains(contents, "NA") { 108 | allRules.rules["NA"] = rules 109 | } else if strings.Contains(contents, "SA") { 110 | allRules.rules["SA"] = rules 111 | } else { 112 | return OrderCompilation{}, RulesCompilation{}, errors.New("incorrect region provided in rules") 113 | } 114 | } 115 | return allOrders, allRules, nil 116 | } 117 | 118 | // validates a single order to all the rules corresponding to region 119 | func validation(order store.Order, allRules RulesCompilation, region string) (bool, error) { 120 | regionRules := allRules.rules[region] 121 | 122 | for _, rule := range regionRules.RuleList { 123 | if rule.AmountFilter != "" { 124 | rg := regexp.MustCompile(`>|<|=`) 125 | filterAmt, err := strconv.ParseFloat(rg.Split(rule.AmountFilter, -1)[1], 64) 126 | if err != nil { 127 | return false, err 128 | } 129 | if rule.AmountFilter[0] == '>' { 130 | if order.Amount < filterAmt { 131 | return false, nil 132 | } 133 | } else if rule.AmountFilter[0] == '<' { 134 | if order.Amount > filterAmt { 135 | return false, nil 136 | } 137 | } else if rule.AmountFilter[0] == '=' { 138 | if order.Amount != filterAmt { 139 | return false, nil 140 | } 141 | } 142 | } 143 | if rule.EmailFilter != "" { 144 | if order.UserEmail != rule.EmailFilter { 145 | return false, nil 146 | } 147 | } 148 | if len(rule.BlacklistProduct) != 0 { 149 | for _, id := range rule.BlacklistProduct { 150 | for _, rg := range order.ProductList { 151 | if rg == id { 152 | return false, nil 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | return true, nil 160 | } 161 | 162 | func processOrderTransformation(finalFiles []store.Order) (string, error) { 163 | output := OrderTransformation{filteredOrders: finalFiles} 164 | byteOutput, err := json.Marshal(output) 165 | if err != nil { 166 | return "", err 167 | } 168 | c := &http.Client{} 169 | 170 | req, err := http.NewRequest("POST", "https://httpbin.org/post", bytes.NewBuffer(byteOutput)) 171 | if err != nil { 172 | return "", err 173 | } 174 | response, err := c.Do(req) 175 | if err != nil { 176 | return "", err 177 | } 178 | defer response.Body.Close() 179 | responseBody, err := ioutil.ReadAll(response.Body) 180 | if err != nil { 181 | return "", err 182 | } 183 | return string(responseBody), nil 184 | } 185 | 186 | func (ctrl *TransformerController) handleInternalError(rw http.ResponseWriter, region string, err error) { 187 | ctrl.logger.Error("Error in Validating", zap.Any("error", err), zap.String("region", region)) 188 | rw.WriteHeader(http.StatusInternalServerError) 189 | rw.Write([]byte("Error in Validating for APAC")) 190 | } 191 | 192 | // adds the user to the database of users 193 | func (ctrl *TransformerController) TransformerHandler(rw http.ResponseWriter, r *http.Request) { 194 | orderCompilation, ruleCompilation, err := parser(ctrl.Store_json_dir, ctrl.Region_rules_dir) 195 | if err != nil { 196 | ctrl.logger.Error("Error in Parsing orders and rules", zap.Any("error", err)) 197 | rw.WriteHeader(http.StatusInternalServerError) 198 | rw.Write([]byte("Error Parsing Files and rules")) 199 | return 200 | } 201 | 202 | ctrl.logger.Info("all orders", zap.Any("orders", orderCompilation)) 203 | ctrl.logger.Info("all rules", zap.Any("rules", ruleCompilation)) 204 | 205 | filteredFiles := []store.Order{} 206 | 207 | // Region 1 208 | apacOrders := orderCompilation.APAC 209 | for _, apacOrder := range apacOrders.OrderList { 210 | check, err := validation(apacOrder, ruleCompilation, "APAC") 211 | if err != nil { 212 | ctrl.handleInternalError(rw, "APAC", err) 213 | return 214 | } 215 | if check { 216 | filteredFiles = append(filteredFiles, apacOrder) 217 | } else { 218 | ctrl.logger.Info("Order Rejected", zap.Any("order", apacOrder)) 219 | } 220 | } 221 | euOrders := orderCompilation.EU 222 | for _, euOrders := range euOrders.OrderList { 223 | check, err := validation(euOrders, ruleCompilation, "EU") 224 | if err != nil { 225 | ctrl.handleInternalError(rw, "APAC", err) 226 | return 227 | } 228 | if check { 229 | filteredFiles = append(filteredFiles, euOrders) 230 | } else { 231 | ctrl.logger.Info("Order Rejected", zap.Any("order", euOrders)) 232 | } 233 | } 234 | naOrders := orderCompilation.NA 235 | for _, naOrders := range naOrders.OrderList { 236 | check, err := validation(naOrders, ruleCompilation, "NA") 237 | if err != nil { 238 | ctrl.handleInternalError(rw, "APAC", err) 239 | return 240 | } 241 | if check { 242 | filteredFiles = append(filteredFiles, naOrders) 243 | } else { 244 | ctrl.logger.Info("Order Rejected", zap.Any("order", naOrders)) 245 | } 246 | } 247 | saOrders := orderCompilation.NA 248 | for _, saOrders := range saOrders.OrderList { 249 | check, err := validation(saOrders, ruleCompilation, "SA") 250 | if err != nil { 251 | ctrl.handleInternalError(rw, "APAC", err) 252 | return 253 | } 254 | if check { 255 | filteredFiles = append(filteredFiles, saOrders) 256 | } else { 257 | ctrl.logger.Info("Order Rejected", zap.Any("order", saOrders)) 258 | } 259 | } 260 | 261 | linkinfo, err := processOrderTransformation(filteredFiles) 262 | if err != nil { 263 | ctrl.logger.Error("Error in sending file processed orders", zap.Any("error", err)) 264 | rw.WriteHeader(http.StatusInternalServerError) 265 | rw.Write([]byte("Error in sending file processed orders")) 266 | } 267 | 268 | ctrl.logger.Info("Link", zap.Any("Details", linkinfo)) 269 | rw.WriteHeader(http.StatusOK) 270 | rw.Write([]byte("Files Parsed and Validated" + linkinfo + "\n" + fmt.Sprintf("%v", filteredFiles))) 271 | } 272 | -------------------------------------------------------------------------------- /ordertransformerservice/transformer_test.go: -------------------------------------------------------------------------------- 1 | package ordertransformerservice 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/jarcoal/httpmock" 13 | "github.com/shadowshot-x/micro-product-go/ordertransformerservice/store" 14 | ) 15 | 16 | // this is a normal testing scenario where a simple code workflow is tested. 17 | // It is tested based on possible errors. 18 | func TestValidation(t *testing.T) { 19 | order := store.Order{ 20 | OrderId: "1", 21 | ProductList: []string{"2", "3"}, 22 | Amount: 100, 23 | UserEmail: "abc@example.com", 24 | } 25 | allRules := RulesCompilation{ 26 | rules: map[string]store.Rules{ 27 | "APAC": { 28 | Region: "APAC", 29 | RuleList: []store.Rule{ 30 | { 31 | AmountFilter: "<200", 32 | EmailFilter: "abc@example.com", 33 | }, 34 | }, 35 | }, 36 | }, 37 | } 38 | t.Run("All Good", func(t *testing.T) { 39 | check, err := validation(order, allRules, "APAC") 40 | if err != nil { 41 | t.Fatalf("Got unexpected error: %v", err) 42 | } 43 | if !check { 44 | t.Fatalf("Correct Rule Invalidated!") 45 | } 46 | }) 47 | t.Run("Amount Filter Check", func(t *testing.T) { 48 | allRules.rules["APAC"].RuleList[0].AmountFilter = ">200" 49 | check, err := validation(order, allRules, "APAC") 50 | if err != nil { 51 | t.Fatalf("Got unexpected error: %v", err) 52 | } 53 | if check { 54 | t.Fatalf("Incorrect Rule Validated!") 55 | } 56 | 57 | allRules.rules["APAC"].RuleList[0].AmountFilter = "=200" 58 | check, err = validation(order, allRules, "APAC") 59 | if err != nil { 60 | t.Fatalf("Got unexpected error: %v", err) 61 | } 62 | if check { 63 | t.Fatalf("Incorrect Rule Validated!") 64 | } 65 | 66 | allRules.rules["APAC"].RuleList[0].AmountFilter = "<10" 67 | check, err = validation(order, allRules, "APAC") 68 | if err != nil { 69 | t.Fatalf("Got unexpected error: %v", err) 70 | } 71 | if check { 72 | t.Fatalf("Incorrect Rule Validated!") 73 | } 74 | 75 | allRules.rules["APAC"].RuleList[0].AmountFilter = "<200" 76 | }) 77 | t.Run("Email Filter Check", func(t *testing.T) { 78 | allRules.rules["APAC"].RuleList[0].EmailFilter = "incorrect@example.com" 79 | check, err := validation(order, allRules, "APAC") 80 | if err != nil { 81 | t.Fatalf("Got unexpected error: %v", err) 82 | } 83 | if check { 84 | t.Fatalf("Incorrect Rule Validated!") 85 | } 86 | allRules.rules["APAC"].RuleList[0].EmailFilter = "abc@example.com" 87 | }) 88 | t.Run("Blacklist Check", func(t *testing.T) { 89 | allRules.rules["APAC"].RuleList[0].BlacklistProduct = []string{"2"} 90 | check, err := validation(order, allRules, "APAC") 91 | if err != nil { 92 | t.Fatalf("Got unexpected error: %v", err) 93 | } 94 | if check { 95 | t.Fatalf("Incorrect Rule Validated!") 96 | } 97 | }) 98 | 99 | t.Run("Error: Incorrect Filter Provided", func(t *testing.T) { 100 | allRules.rules["APAC"].RuleList[0].AmountFilter = ">>>>" 101 | _, err := validation(order, allRules, "APAC") 102 | if err == nil { 103 | t.Fatalf("Did not get error when expected: %v", err) 104 | } 105 | }) 106 | } 107 | 108 | // in this test, we need to build temporary files 109 | func TestParser(t *testing.T) { 110 | // we need to set up temporary directories for this case. 111 | // golang provides this in ioutil.TempDir 112 | dir1, err := ioutil.TempDir("./", "") 113 | if err != nil { 114 | t.Fatalf("Unable to create Temp Dir 1 : %s", dir1) 115 | } 116 | dir2, err := ioutil.TempDir("./", "") 117 | if err != nil { 118 | t.Fatalf("Unable to create Temp Dir 1 : %s", dir2) 119 | } 120 | // we need to make sure the temp directories are removed 121 | defer os.RemoveAll(dir1) 122 | defer os.RemoveAll(dir2) 123 | // now we can set up temporary files 124 | oTempName := "apacOrder.json" 125 | f1, err := os.Create(fmt.Sprintf("./%s/%s", dir1, oTempName)) 126 | if err != nil { 127 | t.Fatalf("Unable to create temporary file %s", oTempName) 128 | } 129 | contents := `{ 130 | "region": "APAC", 131 | "orderlist": [{ 132 | "orderid" : "1", 133 | "amount": 33.44, 134 | "useremail": "abc@example.com", 135 | "create_at": "" 136 | }] 137 | }` 138 | f1.WriteString(contents) 139 | rTempName := "apacDirective.yaml" 140 | f2, err := os.Create(fmt.Sprintf("./%s/%s", dir2, rTempName)) 141 | if err != nil { 142 | t.Fatalf("Unable to create temporary file %s", rTempName) 143 | } 144 | contents = ` 145 | region: APAC 146 | rulelist: 147 | - amountfilter: "<18000" 148 | ` 149 | f2.WriteString(contents) 150 | t.Run("Good: All Pass", func(t *testing.T) { 151 | oc, rc, err := parser(fmt.Sprintf("./%s/", dir1), fmt.Sprintf("./%s/", dir2)) 152 | if err != nil { 153 | t.Fatalf("Got unexpected error %v", err) 154 | } 155 | expectedOc := OrderCompilation{ 156 | APAC: store.Orders{ 157 | Region: "APAC", 158 | OrderList: []store.Order{ 159 | { 160 | OrderId: "1", 161 | Amount: 33.44, 162 | UserEmail: "abc@example.com", 163 | }, 164 | }, 165 | }, 166 | } 167 | expectedRc := RulesCompilation{ 168 | rules: map[string]store.Rules{ 169 | "APAC": { 170 | Region: "APAC", 171 | RuleList: []store.Rule{ 172 | { 173 | AmountFilter: "<18000", 174 | }, 175 | }, 176 | }, 177 | }, 178 | } 179 | if !reflect.DeepEqual(oc, expectedOc) { 180 | t.Fatalf("Incorrect OrderCompilation Generated. %v, %v", oc, expectedOc) 181 | } 182 | if !reflect.DeepEqual(rc, expectedRc) { 183 | t.Fatalf("Incorrect ruleCompilation Generated. %v, %v", rc, expectedRc) 184 | } 185 | }) 186 | } 187 | 188 | func TestProcessOrderTransformtion(t *testing.T) { 189 | httpmock.Activate() 190 | defer httpmock.DeactivateAndReset() 191 | 192 | httpmock.RegisterResponder("POST", "https://httpbin.org/post", func(req *http.Request) (*http.Response, error) { 193 | resp, err := httpmock.NewJsonResponse(200, "200 OK") 194 | if err != nil { 195 | return httpmock.NewStringResponse(500, "Error Generating Response"), nil 196 | } 197 | return resp, nil 198 | }) 199 | 200 | resp, err := processOrderTransformation([]store.Order{}) 201 | if err != nil { 202 | t.Fatalf("Unexpected Error Encountered :%v", err) 203 | } 204 | expected := "200 OK" 205 | 206 | if !strings.Contains(resp, expected) { 207 | t.Fatalf("Unexpected Message from server. %v", resp) 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /productservice/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Add Product 3 | `curl http://localhost:9090/product/addprod --request POST --header 'Productname:prod2' --header 'Productvendor:vendor2' --header 'Productinventory:6' --header 'Productdescription:description is here'` 4 | 5 | ## Get All Products 6 | `curl http://localhost:9090/product/getprods --request GET` 7 | 8 | ## Get One Product by Id 9 | `curl http://localhost:9090/product/getprodbyid --request GET --header 'Id:1'` 10 | 11 | ## Delete Product by Id 12 | `curl http://localhost:9090/product/deletebyid --request DELETE --header 'Id:2'` 13 | 14 | ## Custom Query Examples 15 | `curl http://localhost:9090/product/customquery --request GET --header 'Type:get' --header 'Query:SELECT name FROM products;'` 16 | 17 | `curl http://localhost:9090/product/customquery --request GET --header 'Type:get' --header "Query:UPDATE products SET name = \"prod1\" WHERE id = 3;"` -------------------------------------------------------------------------------- /productservice/productservice.go: -------------------------------------------------------------------------------- 1 | package productservice 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/jinzhu/gorm" 12 | _ "github.com/jinzhu/gorm/dialects/mysql" 13 | "github.com/shadowshot-x/micro-product-go/productservice/store" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | var db *gorm.DB 18 | 19 | func GetSecret() string { 20 | return os.Getenv("MYSQL_SECRET") 21 | } 22 | 23 | // ProductController is the getproduct route handler 24 | type ProductController struct { 25 | logger *zap.Logger 26 | } 27 | 28 | // NewProductController returns a frsh Upload controller 29 | func NewProductController(logger *zap.Logger) *ProductController { 30 | return &ProductController{ 31 | logger: logger, 32 | } 33 | } 34 | 35 | func handleNotInHeader(rw http.ResponseWriter, r *http.Request, param string) { 36 | rw.WriteHeader(http.StatusBadRequest) 37 | rw.Write([]byte(fmt.Sprintf("%s Missing", param))) 38 | } 39 | 40 | func (ctrl *ProductController) InitGormConnection() { 41 | // database configuration for mysql 42 | // first we fetch the mysql secret string stored in environment variables 43 | sqlsecret := GetSecret() 44 | // if secret is empty, we want to warn the user 45 | if sqlsecret == "" { 46 | ctrl.logger.Warn("Unable to get mysql secret") 47 | return 48 | } 49 | var err error 50 | // lets open the conncection 51 | db, err = gorm.Open("mysql", GetSecret()) 52 | if err != nil { 53 | ctrl.logger.Warn("Connection Failed to Open", zap.Error(err)) 54 | } else { 55 | ctrl.logger.Info("Connection Established") 56 | } 57 | 58 | //We have the database name in our Environment secret. 59 | // Auto Migrate creates a table named products in that Database 60 | db.AutoMigrate(&store.Product{}) 61 | } 62 | 63 | func (ctrl *ProductController) GetAllProductsHandler(rw http.ResponseWriter, r *http.Request) { 64 | // we know we will get a list of all products. 65 | AllProducts := []store.Product{} 66 | // here db.Find fetches all the existing Elements in Products and stores them in AllProducts 67 | db.Find(&AllProducts) 68 | // We can Send back all values to the ResponseWriter by jsonencoding the results 69 | json.NewEncoder(rw).Encode(AllProducts) 70 | } 71 | 72 | func (ctrl *ProductController) GetAllProductByIdHandler(rw http.ResponseWriter, r *http.Request) { 73 | // we know we will get a list of all products with a certain id. 74 | Products := store.Product{} 75 | if _, ok := r.Header["Id"]; !ok { 76 | ctrl.logger.Warn("Id was not found in the header") 77 | handleNotInHeader(rw, r, "Id") 78 | return 79 | } 80 | 81 | // here db.First fetches the first existing Elements in Products and stores them in Product 82 | // we need to record errors because if none exist, that is an error. 83 | err := db.First(&Products, r.Header["Id"][0]).Error 84 | 85 | if err != nil { 86 | ctrl.logger.Error("The stated record was not found") 87 | rw.WriteHeader(http.StatusBadRequest) 88 | rw.Write([]byte("Record not found")) 89 | return 90 | } 91 | // We can Send back all values to the ResponseWriter by jsonencoding the results 92 | rw.WriteHeader(http.StatusOK) 93 | json.NewEncoder(rw).Encode(Products) 94 | } 95 | 96 | func (ctrl *ProductController) AddProductHandler(rw http.ResponseWriter, r *http.Request) { 97 | //validate the request first 98 | if _, ok := r.Header["Productname"]; !ok { 99 | ctrl.logger.Warn("Name was not found in the header") 100 | handleNotInHeader(rw, r, "name") 101 | return 102 | } 103 | if _, ok := r.Header["Productvendor"]; !ok { 104 | ctrl.logger.Warn("Vendor was not found in the header") 105 | handleNotInHeader(rw, r, "Vendor") 106 | return 107 | } 108 | if _, ok := r.Header["Productinventory"]; !ok { 109 | ctrl.logger.Warn("Inventory was not found in the header") 110 | handleNotInHeader(rw, r, "Inventory") 111 | return 112 | } 113 | if _, ok := r.Header["Productdescription"]; !ok { 114 | ctrl.logger.Warn("Description was not found in the header") 115 | handleNotInHeader(rw, r, "Description") 116 | return 117 | } 118 | // We want to get the details of the Product first. So these have to be in the request 119 | inventory, err := strconv.Atoi(r.Header["Productinventory"][0]) 120 | if err != nil { 121 | // if we get an error, we dont want to execute any further 122 | ctrl.logger.Error("Error converting string to integer in inventory", zap.Error(err)) 123 | return 124 | } 125 | // define the object we want to add 126 | newProduct := store.Product{ 127 | Name: r.Header["Productname"][0], 128 | VendorName: r.Header["Productvendor"][0], 129 | Inventory: inventory, 130 | Description: r.Header["Productdescription"][0], 131 | CreateAt: time.Now(), 132 | } 133 | db.Omit("Id").Create(&newProduct) 134 | 135 | rw.WriteHeader(http.StatusOK) 136 | rw.Write([]byte("Record was added")) 137 | } 138 | 139 | func (ctrl *ProductController) DeleteProductHandler(rw http.ResponseWriter, r *http.Request) { 140 | // This is the ideal case where we want to make sure the user is authenticated and has a certain role 141 | // however, right now I am keeping it simple 142 | // lets create an issue for this 143 | 144 | // first we see the request has the id for the product to be deleted. 145 | if _, ok := r.Header["Id"]; !ok { 146 | ctrl.logger.Warn("Id was not found in the header") 147 | handleNotInHeader(rw, r, "Id") 148 | return 149 | } 150 | 151 | // Now we know that the request has the parameter. Lets see how the gorm handles deletion 152 | // We can use the Where clause in gorm to query this. 153 | // db.Where(fmt.Sprintf("Id = %s", r.Header["Id"][0])).Delete(&store.Product{}) 154 | // There is another easy way. As we know that Id is primary Key, we can do the following :- 155 | err := db.Delete(&store.Product{}, r.Header["Id"][0]).Error 156 | 157 | // this would mean there is an internal error 158 | if err != nil { 159 | ctrl.logger.Error("Could not delete the Given Product") 160 | rw.WriteHeader(http.StatusBadRequest) 161 | rw.Write([]byte("Record could not be deleted")) 162 | return 163 | } 164 | 165 | // succesful request 166 | rw.WriteHeader(http.StatusOK) 167 | rw.Write([]byte("Record deleted")) 168 | } 169 | 170 | // However, this is becoming a bit strict. 171 | // We dont want to add a function everytime we get a new SQL query required. 172 | // Doing this in gorm will be difficult. 173 | // We would need a wrapper to provide user with this support, but this will take significant effort 174 | // However something that executes queries as is can do the job. 175 | // So, we can have an Exec Query of Gorm to deal with this. Custom Queries! 176 | 177 | func (ctrl *ProductController) CustomQueryHandler(rw http.ResponseWriter, r *http.Request) { 178 | 179 | // to Query ie. to use SELECT we have .Raw in Gorm 180 | // to Execute like delete, add and update, we have .Exec in Gorm 181 | if _, ok := r.Header["Type"]; !ok { 182 | ctrl.logger.Warn("Type was not found in the header") 183 | handleNotInHeader(rw, r, "Type") 184 | return 185 | } 186 | 187 | if _, ok := r.Header["Query"]; !ok { 188 | ctrl.logger.Warn("Query was not found in the header") 189 | handleNotInHeader(rw, r, "Query") 190 | return 191 | } 192 | 193 | if r.Header["Type"][0] == "get" { 194 | // we know that we can only get an array of Products. 195 | var Products []store.Product 196 | err := db.Raw(r.Header["Query"][0]).Scan(&Products).Error 197 | 198 | if err != nil { 199 | ctrl.logger.Error("Could not Execute your Query") 200 | rw.WriteHeader(http.StatusBadRequest) 201 | rw.Write([]byte("Query not executed")) 202 | return 203 | } 204 | 205 | rw.WriteHeader(http.StatusOK) 206 | json.NewEncoder(rw).Encode(Products) 207 | return 208 | } else if r.Header["Type"][0] == "exec" { 209 | err := db.Exec(r.Header["Query"][0]).Error 210 | 211 | if err != nil { 212 | ctrl.logger.Error("Could not Execute your Query") 213 | rw.WriteHeader(http.StatusBadRequest) 214 | rw.Write([]byte("Query not executed")) 215 | return 216 | } 217 | rw.WriteHeader(http.StatusOK) 218 | rw.Write([]byte("Query Executed")) 219 | return 220 | } 221 | 222 | rw.WriteHeader(http.StatusBadRequest) 223 | rw.Write([]byte("Incorrect Query Type")) 224 | } 225 | -------------------------------------------------------------------------------- /productservice/store/product.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "time" 4 | 5 | type Product struct { 6 | Id int `json:"id"` 7 | Name string `json:"name"` 8 | VendorName string `json:"vendor"` 9 | Inventory int `json:"inventory"` 10 | Description string `json:"description"` 11 | CreateAt time.Time `json:"create_at"` 12 | } 13 | --------------------------------------------------------------------------------