├── .gitignore
├── Caddyfile
├── LICENSE.md
├── Makefile
├── README.md
├── authentication-service.dockerfile
├── authentication-service
├── cmd
│ └── api
│ │ ├── discover.go
│ │ ├── handlers.go
│ │ ├── helpers.go
│ │ ├── main.go
│ │ └── routes.go
├── data
│ └── models.go
├── go.mod
├── go.sum
└── users.sql
├── broker-service.dockerfile
├── broker-service
├── Makefile
├── cmd
│ └── api
│ │ ├── discover.go
│ │ ├── handlers.go
│ │ ├── helpers.go
│ │ ├── main.go
│ │ └── routes.go
├── event
│ ├── emitter.go
│ └── event.go
├── go.mod
├── go.sum
└── logs
│ ├── logs.pb.go
│ ├── logs.proto
│ └── logs_grpc.pb.go
├── caddy.dockerfile
├── db-data
├── .gitignore
└── .gitkeep
├── docker-compose.yml
├── front-end.dockerfile
├── front-end
├── cmd
│ └── web
│ │ ├── main.go
│ │ └── templates
│ │ ├── base.layout.gohtml
│ │ ├── footer.partial.gohtml
│ │ ├── header.partial.gohtml
│ │ └── test.page.gohtml
└── go.mod
├── ingress.yml
├── k8s.md
├── k8s
├── authentication.yml
├── broker.yml
├── listener.yml
├── logger.yml
├── mail.yml
├── mailhog.yml
├── mongo.yml
└── rabbitmq.yml
├── listener-service.dockerfile
├── listener-service
├── go.mod
├── go.sum
├── lib
│ └── event
│ │ ├── consumer.go
│ │ └── event.go
└── main.go
├── logger-service.dockerfile
├── logger-service
├── Makefile
├── cmd
│ └── web
│ │ ├── discover.go
│ │ ├── grpc.go
│ │ ├── handlers.go
│ │ ├── helpers.go
│ │ ├── main.go
│ │ ├── middleware.go
│ │ ├── render.go
│ │ ├── routes.go
│ │ └── rpc.go
├── data
│ └── models.go
├── go.mod
├── go.sum
├── logs
│ ├── logs.pb.go
│ ├── logs.proto
│ ├── logs_grpc.pb.go
│ └── readme.md
└── templates
│ ├── base.layout.gohtml
│ ├── dashboard.page.gohtml
│ ├── entry.page.gohtml
│ └── login.page.gohtml
├── mail-service.dockerfile
├── mail-service
├── cmd
│ └── api
│ │ ├── discover.go
│ │ ├── handlers.go
│ │ ├── helpers.go
│ │ ├── mailer.go
│ │ ├── main.go
│ │ └── routes.go
├── go.mod
├── go.sum
└── templates
│ ├── mail.html.tmpl
│ └── mail.plain.tmpl
├── multistage-dockerfiles
├── authentication-service.dockerfile
├── broker-service.dockerfile
├── listener-service.dockerfile
├── logger-service.dockerfile
└── mail-service.dockerfile
├── postgres.yml
├── swarm.md
└── swarm.yml
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vscode
3 | coverage.out
4 | frontApp
5 | logServiceApp
6 | mailerServiceApp
7 | authApp
8 | brokerApp
9 | listener
10 | frontEndLinux
--------------------------------------------------------------------------------
/Caddyfile:
--------------------------------------------------------------------------------
1 | {
2 | email you@gmail.com
3 | }
4 |
5 | (static) {
6 | @static {
7 | file
8 | path *.ico *.css *.js *.gif *.jpg *.jpeg *.png *.svg *.woff *.json
9 | }
10 | header @static Cache-Control max-age=5184000
11 | }
12 |
13 | (security) {
14 | header {
15 | # enable HSTS
16 | Strict-Transport-Security max-age=31536000;
17 | # disable clients from sniffing the media type
18 | X-Content-Type-Options nosniff
19 | # keep referrer data off of HTTP connections
20 | Referrer-Policy no-referrer-when-downgrade
21 | }
22 | }
23 |
24 | localhost:80 {
25 | encode zstd gzip
26 | import static
27 |
28 | reverse_proxy http://front-end:8081
29 | }
30 |
31 | backend:80 {
32 | reverse_proxy http://broker-service:8080
33 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | ### Copyright (c) 2022 Trevor Sawler
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 | FRONT_END_BINARY=frontApp
2 | LOGGER_BINARY=logServiceApp
3 | BROKER_BINARY=brokerApp
4 | AUTH_BINARY=authApp
5 | LISTENER_BINARY=listener
6 | MAIL_BINARY=mailerServiceApp
7 | AUTH_VERSION=1.0.0
8 | BROKER_VERSION=1.0.0
9 | LISTENER_VERSION=1.0.2
10 | MAIL_VERSION=1.0.0
11 | LOGGER_VERSION=1.0.0
12 |
13 | ## up: starts all containers in the background without forcing build
14 | up:
15 | @echo "Starting docker images..."
16 | docker-compose up -d
17 | @echo "Docker images started!"
18 |
19 | ## down: stop docker compose
20 | down:
21 | @echo "Stopping docker images..."
22 | docker-compose down
23 | @echo "Docker stopped!"
24 |
25 | ## build_dockerfiles: builds all dockerfile images
26 | build_dockerfiles: build_auth build_broker build_listener build_logger build_mail front_end_linux
27 | @echo "Building dockerfiles..."
28 | docker build -f front-end.dockerfile -t tsawler/front-end .
29 | docker build -f authentication-service.dockerfile -t tsawler/authentication:${AUTH_VERSION} .
30 | docker build -f broker-service.dockerfile -t tsawler/broker:1.0.0 .
31 | docker build -f listener-service.dockerfile -t tsawler/listener:1.0.2 .
32 | docker build -f mail-service.dockerfile -t tsawler/mail:1.0.0 .
33 | docker build -f logger-service.dockerfile -t tsawler/logger:1.0.0 .
34 |
35 | ## push_dockerfiles: pushes tagged versions to docker hub
36 | push_dockerfiles: build_dockerfiles
37 | docker push tsawler/authentication:${AUTH_VERSION}
38 | docker push tsawler/broker:${BROKER_VERSION}
39 | docker push tsawler/listener:${LISTENER_VERSION}
40 | docker push tsawler/mail:${MAIL_VERSION}
41 | docker push tsawler/logger:${LOGGER_VERSION}
42 | @echo "Done!"
43 |
44 | ## front_end_linux: builds linux executable for front end
45 | front_end_linux:
46 | @echo "Building linux version of front end..."
47 | cd front-end && env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o frontEndLinux ./cmd/web
48 | @echo "Done!"
49 |
50 | ## swarm_up: starts the swarm
51 | swarm_up:
52 | @echo "Starting swarm..."
53 | docker stack deploy -c swarm.yml myapp
54 |
55 | ## swarm_down: stops the swarm
56 | swarm_down:
57 | @echo "Stopping swarm..."
58 | docker stack rm myapp
59 |
60 | ## build_auth: builds the authentication binary as a linux executable
61 | build_auth:
62 | @echo "Building authentication binary.."
63 | cd authentication-service && env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ${AUTH_BINARY} ./cmd/api
64 | @echo "Authentication binary built!"
65 |
66 | ## build_logger: builds the logger binary as a linux executable
67 | build_logger:
68 | @echo "Building logger binary..."
69 | cd logger-service && env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ${LOGGER_BINARY} ./cmd/web
70 | @echo "Logger binary built!"
71 |
72 | ## build_broker: builds the broker binary as a linux executable
73 | build_broker:
74 | @echo "Building broker binary..."
75 | cd broker-service && env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ${BROKER_BINARY} ./cmd/api
76 | @echo "Broker binary built!"
77 |
78 | ## build_listener: builds the listener binary as a linux executable
79 | build_listener:
80 | @echo "Building listener binary..."
81 | cd listener-service && env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ${LISTENER_BINARY} .
82 | @echo "Listener binary built!"
83 |
84 | ## build_mail: builds the mail binary as a linux executable
85 | build_mail:
86 | @echo "Building mailer binary..."
87 | cd mail-service && env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o ${MAIL_BINARY} ./cmd/api
88 | @echo "Mailer binary built!"
89 |
90 |
91 | ## up_build: stops docker-compose (if running), builds all projects and starts docker compose
92 | up_build: build_auth build_broker build_listener build_logger build_mail
93 | @echo "Stopping docker images (if running...)"
94 | docker-compose down
95 | @echo "Building (when required) and starting docker images..."
96 | docker-compose up --build -d
97 | @echo "Docker images built and started!"
98 |
99 | ## auth: stops authentication-service, removes docker image, builds service, and starts it
100 | auth: build_auth
101 | @echo "Building authentication-service docker image..."
102 | - docker-compose stop authentication-service
103 | - docker-compose rm -f authentication-service
104 | docker-compose up --build -d authentication-service
105 | docker-compose start authentication-service
106 | @echo "authentication-service built and started!"
107 |
108 | ## broker: stops broker-service, removes docker image, builds service, and starts it
109 | broker: build_broker
110 | @echo "Building broker-service docker image..."
111 | - docker-compose stop broker-service
112 | - docker-compose rm -f broker-service
113 | docker-compose up --build -d broker-service
114 | docker-compose start broker-service
115 | @echo "broker-service rebuilt and started!"
116 |
117 | ## logger: stops logger-service, removes docker image, builds service, and starts it
118 | logger: build_logger
119 | @echo "Building logger-service docker image..."
120 | - docker-compose stop logger-service
121 | - docker-compose rm -f logger-service
122 | docker-compose up --build -d logger-service
123 | docker-compose start logger-service
124 | @echo "broker-service rebuilt and started!"
125 |
126 | ## mail: stops mail-service, removes docker image, builds service, and starts it
127 | mail: build_mail
128 | @echo "Building mail-service docker image..."
129 | - docker-compose stop mail-service
130 | - docker-compose rm -f mail-service
131 | docker-compose up --build -d mail-service
132 | docker-compose start mail-service
133 | @echo "mail-service rebuilt and started!"
134 |
135 | ## listener: stops listener-service, removes docker image, builds service, and starts it
136 | listener: build_listener
137 | @echo "Building listener-service docker image..."
138 | - docker-compose stop listener-service
139 | - docker-compose rm -f listener-service
140 | docker-compose up --build -d listener-service
141 | docker-compose start listener-service
142 | @echo "listener-service rebuilt and started!"
143 |
144 | ## start: starts the front end
145 | start:
146 | @echo "Starting front end"
147 | cd front-end && go build -o ${FRONT_END_BINARY} ./cmd/web
148 | cd front-end && ./${FRONT_END_BINARY} &
149 |
150 | ## stop: stop the front end
151 | stop:
152 | @echo "Stopping front end..."
153 | @-pkill -SIGTERM -f "./${FRONT_END_BINARY}"
154 | @echo "Stopped front end!"
155 |
156 | ## test: runs all tests
157 | test:
158 | @echo "Testing..."
159 | go test -v ./...
160 |
161 | ## clean: runs go clean and deletes binaries
162 | clean:
163 | @echo "Cleaning..."
164 | @cd broker-service && rm -f ${BROKER_BINARY}
165 | @cd broker-service && go clean
166 | @cd listener-service && rm -f ${LISTENER_BINARY}
167 | @cd listener-service && go clean
168 | @cd authentication-service && rm -f ${AUTH_BINARY}
169 | @cd authentication-service && go clean
170 | @cd mail-service && rm -f ${MAIL_BINARY}
171 | @cd mail-service && go clean
172 | @cd logger-service && rm -f ${LOGGER_BINARY}
173 | @cd logger-service && go clean
174 | @cd front-end && go clean
175 | @cd front-end && rm -f ${FRONT_END_BINARY}
176 | @echo "Cleaned!"
177 |
178 | ## help: displays help
179 | help: Makefile
180 | @echo " Choose a command:"
181 | @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://golang.org)
2 | [](https://raw.githubusercontent.com/tsawler/goblender/master/LICENSE)
3 |
4 | # Working with Microservices in Go
5 |
6 | This is the source code for the Udemy course **Working with Microservices and Go**. This project
7 | consists of a number of loosely coupled microservices, all written in Go:
8 |
9 | - broker-service: an optional single entry point to connect to all services from one place (accepts JSON;
10 | sends JSON, makes calls via gRPC, and pushes to RabbitMQ)
11 | - authentication-service: authenticates users against a Postgres database (accepts JSON)
12 | - logger-service: logs important events to a MongoDB database (accepts RPC, gRPC, and JSON)
13 | - queue-listener-service: consumes messages from amqp (RabbitMQ) and initiates actions based on payload (sends via RPC)
14 | - mail-service: sends email (accepts JSON)
15 |
16 | All services (except the broker) register their access urls with etcd, and renew their leases automatically.
17 | This allows us to implement a simple service discovery system, where all service URLs are accessible with
18 | "service maps" in the Config type used to share application configuration in the broker service.
19 |
20 | In addition to the microservices, the included `docker-compose.yml` at the root level of the project
21 | starts the following services:
22 |
23 | - Postgresql - used by the authentication service to store user accounts
24 | - MongoDB - used by the logger service to save logs from all services
25 | - etcd - used for service discovery
26 | - mailhog - used as a fake mail server to work with the mail service
27 |
28 | ## Running the project
29 | From the root level of the project, execute this command (this assumes that you have
30 | [GNU make](https://www.gnu.org/software/make/) and a recent version
31 | of [Docker](https://www.docker.com/products/docker-desktop) installed on your machine):
32 |
33 | ~~~
34 | make up_build
35 | ~~~
36 |
37 | If the code has not changed, subsequent runs can just be `make up`.
38 |
39 | Then start the front end:
40 |
41 | ~~~
42 | make start
43 | ~~~
44 |
45 | Hit the front end with your web browser at `http://localhost:80`. You can also access a web
46 | front end to the logger service by going to `http://localhost:8082` (or whatever port you
47 | specify in the `docker-compose.yml file`).
48 |
49 | To stop everything:
50 |
51 | ~~~
52 | make stop
53 | make down
54 | ~~~
55 |
56 | While working on code, you can rebuild just the service you are working on by
57 | executing
58 |
59 | `make auth`
60 |
61 | Where `auth` is one of the services:
62 |
63 | - auth
64 | - broker
65 | - logger
66 | - listener
67 | - mail
68 |
69 | All make commands:
70 |
71 | ~~~
72 | tcs@Grendel go-microservices % make help
73 | Choose a command:
74 | up starts all containers in the background without forcing build
75 | down stop docker compose
76 | build_auth builds the authentication binary as a linux executable
77 | build_logger builds the logger binary as a linux executable
78 | build_broker builds the broker binary as a linux executable
79 | build_listener builds the listener binary as a linux executable
80 | build_mail builds the mail binary as a linux executable
81 | up_build stops docker-compose (if running), builds all projects and starts docker compose
82 | auth stops authentication-service, removes docker image, builds service, and starts it
83 | broker stops broker-service, removes docker image, builds service, and starts it
84 | logger stops logger-service, removes docker image, builds service, and starts it
85 | mail stops mail-service, removes docker image, builds service, and starts it
86 | listener stops listener-service, removes docker image, builds service, and starts it
87 | start starts the front end
88 | stop stop the front end
89 | test runs all tests
90 | clean runs go clean and deletes binaries
91 | help displays help
92 | ~~~
--------------------------------------------------------------------------------
/authentication-service.dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 | RUN mkdir /app
3 |
4 | COPY authentication-service/authApp /app
5 |
6 | # Run the server executable
7 | CMD [ "/app/authApp" ]
--------------------------------------------------------------------------------
/authentication-service/cmd/api/discover.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | //
4 | //// registerService registers the correct entry for this service in etcd
5 | //func (app *Config) registerService() {
6 | // cli, _ := connectToEtcd()
7 | // kv := clientv3.NewKV(cli)
8 | //
9 | // app.Etcd = cli
10 | //
11 | // lease := clientv3.NewLease(cli)
12 | // grantResp, err := lease.Grant(context.TODO(), 10)
13 | // if err != nil {
14 | // log.Println("Error creating lease", err)
15 | // }
16 | //
17 | // // insert something with the lease
18 | // _, err = kv.Put(context.TODO(), fmt.Sprintf("/auth/%s", app.randomString(32)), "authentication-service", clientv3.WithLease(grantResp.ID))
19 | // if err != nil {
20 | // log.Println("Error inserting using lease", err)
21 | // }
22 | //
23 | // // keep lease alive
24 | // kalRes, err := lease.KeepAlive(context.TODO(), grantResp.ID)
25 | // if err != nil {
26 | // log.Println("Error with keepalive", err)
27 | // }
28 | // go app.listenToKeepAlive(kalRes)
29 | //}
30 | //
31 | //// listenToKeepAlive just consumes channel responses from etcd's KeepAlive method
32 | //func (app *Config) listenToKeepAlive(kalRes <-chan *clientv3.LeaseKeepAliveResponse) {
33 | // defer func() {
34 | // if r := recover(); r != nil {
35 | // log.Println("Error", fmt.Sprintf("%v", r))
36 | // }
37 | // }()
38 | //
39 | // // the only reason this exists is to consume the response from etcd's KeepAlive, because
40 | // // if we don't, unexpected behaviour is the result.
41 | // for {
42 | // _ = <-kalRes
43 | // }
44 | //}
45 | //
46 | //// connectToEtcd tries to connect to etcd, for up to 30 seconds
47 | //func connectToEtcd() (*clientv3.Client, error) {
48 | // var cli *clientv3.Client
49 | // var counts = 0
50 | //
51 | // for {
52 | // c, err := clientv3.New(clientv3.Config{Endpoints: []string{"etcd:2379"},
53 | // DialTimeout: 5 * time.Second,
54 | // })
55 | // if err != nil {
56 | // fmt.Println("etcd not ready...")
57 | // counts++
58 | // } else {
59 | // fmt.Println()
60 | // cli = c
61 | // break
62 | // }
63 | //
64 | // if counts > 15 {
65 | // return nil, err
66 | // }
67 | // fmt.Println("Backing off for 2 seconds...")
68 | // time.Sleep(2 * time.Second)
69 | // continue
70 | // }
71 | // log.Println("Connected to etcd!")
72 | // return cli, nil
73 | //}
74 |
--------------------------------------------------------------------------------
/authentication-service/cmd/api/handlers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "net/http"
9 | )
10 |
11 | // Authenticate accepts a json payload and attempts to authenticate a user
12 | func (app *Config) Authenticate(w http.ResponseWriter, r *http.Request) {
13 | var requestPayload struct {
14 | Email string `json:"email"`
15 | Password string `json:"password"`
16 | }
17 |
18 | err := app.readJSON(w, r, &requestPayload)
19 | if err != nil {
20 | _ = app.errorJSON(w, err, http.StatusBadRequest)
21 | return
22 | }
23 |
24 | // validate against database
25 | user, err := app.Models.User.GetByEmail(requestPayload.Email)
26 | if err != nil {
27 | _ = app.errorJSON(w, errors.New("invalid credentials"), http.StatusUnauthorized)
28 | return
29 | }
30 |
31 | valid, err := user.PasswordMatches(requestPayload.Password)
32 | if err != nil || !valid {
33 | _ = app.errorJSON(w, errors.New("invalid credentials"), http.StatusUnauthorized)
34 | return
35 | }
36 |
37 | // log request
38 | err = app.logRequest("authentication", fmt.Sprintf("%s logged in", user.Email))
39 | if err != nil {
40 | _ = app.errorJSON(w, err, http.StatusBadRequest)
41 | }
42 |
43 | payload := jsonResponse{
44 | Error: false,
45 | Message: fmt.Sprintf("Logged in user %s", requestPayload.Email),
46 | //Data: User{
47 | // ID: 1,
48 | // FirstName: "Jack",
49 | // LastName: "Smith",
50 | // Email: "jack@smith.com",
51 | // Active: 1,
52 | //},
53 | Data: user,
54 | }
55 |
56 | _ = app.writeJSON(w, http.StatusAccepted, payload)
57 | }
58 |
59 | func (app *Config) logRequest(name, data string) error {
60 | var entry struct {
61 | Name string `json:"name"`
62 | Data string `json:"data"`
63 | }
64 | entry.Name = name
65 | entry.Data = data
66 |
67 | jsonData, _ := json.MarshalIndent(entry, "", "\t")
68 | logServiceURL := "http://logger-service/log"
69 |
70 | request, err := http.NewRequest("POST", logServiceURL, bytes.NewBuffer(jsonData))
71 | request.Header.Set("Content-Type", "application/json")
72 |
73 | client := &http.Client{}
74 | _, err = client.Do(request)
75 | if err != nil {
76 | return err
77 | }
78 |
79 | return nil
80 | }
81 |
--------------------------------------------------------------------------------
/authentication-service/cmd/api/helpers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/json"
6 | "errors"
7 | "io"
8 | "net/http"
9 | )
10 |
11 | const randomStringSource = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0987654321_+"
12 |
13 | // jsonResponse is the type used for sending JSON around
14 | type jsonResponse struct {
15 | Error bool `json:"error"`
16 | Message string `json:"message"`
17 | Data any `json:"data,omitempty"`
18 | }
19 |
20 | // readJSON tries to read the body of a request and converts it into JSON
21 | func (app *Config) readJSON(w http.ResponseWriter, r *http.Request, data any) error {
22 | maxBytes := 1048576 // one megabyte
23 | r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
24 |
25 | dec := json.NewDecoder(r.Body)
26 | err := dec.Decode(data)
27 | if err != nil {
28 | return err
29 | }
30 |
31 | err = dec.Decode(&struct{}{})
32 | if err != io.EOF {
33 | return errors.New("body must have only a single json value")
34 | }
35 |
36 | return nil
37 | }
38 |
39 | // writeJSON takes a response status code and arbitrary data and writes a json response to the client
40 | func (app *Config) writeJSON(w http.ResponseWriter, status int, data any, headers ...http.Header) error {
41 | out, err := json.Marshal(data)
42 | if err != nil {
43 | return err
44 | }
45 |
46 | if len(headers) > 0 {
47 | for key, value := range headers[0] {
48 | w.Header()[key] = value
49 | }
50 | }
51 |
52 | w.Header().Set("Content-Type", "application/json")
53 | w.WriteHeader(status)
54 | _, err = w.Write(out)
55 | if err != nil {
56 | return err
57 | }
58 |
59 | return nil
60 | }
61 |
62 | // errorJSON takes an error, and optionally a response status code, and generates and sends
63 | // a json error response
64 | func (app *Config) errorJSON(w http.ResponseWriter, err error, status ...int) error {
65 | statusCode := http.StatusBadRequest
66 |
67 | if len(status) > 0 {
68 | statusCode = status[0]
69 | }
70 |
71 | var payload jsonResponse
72 | payload.Error = true
73 | payload.Message = err.Error()
74 |
75 | return app.writeJSON(w, statusCode, payload)
76 | }
77 |
78 | // randomString returns a random string of letters of length n
79 | func (app *Config) randomString(n int) string {
80 | s, r := make([]rune, n), []rune(randomStringSource)
81 | for i := range s {
82 | p, _ := rand.Prime(rand.Reader, len(r))
83 | x, y := p.Uint64(), uint64(len(r))
84 | s[i] = r[x%y]
85 | }
86 | return string(s)
87 | }
88 |
--------------------------------------------------------------------------------
/authentication-service/cmd/api/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "authentication/data"
5 | "database/sql"
6 | "fmt"
7 | _ "github.com/jackc/pgconn"
8 | _ "github.com/jackc/pgx/v4"
9 | _ "github.com/jackc/pgx/v4/stdlib"
10 | "log"
11 | "net/http"
12 | "os"
13 | "time"
14 | )
15 |
16 | const webPort = "80"
17 |
18 | var counts int64
19 |
20 | type Config struct {
21 | DB *sql.DB
22 | Models data.Models
23 | //Etcd *clientv3.Client
24 | }
25 |
26 | func main() {
27 | log.Println("---------------------------------------------")
28 | log.Println("Attempting to connect to Postgres...")
29 | // connect to the database
30 | conn := connectToDB()
31 | if conn == nil {
32 | log.Panic("can't connect to postgres!")
33 | }
34 |
35 | app := Config{
36 | DB: conn,
37 | Models: data.New(conn),
38 | }
39 |
40 | //app.registerService()
41 | //defer app.Etcd.Close()
42 |
43 | srv := &http.Server{
44 | Addr: fmt.Sprintf(":%s", webPort),
45 | Handler: app.routes(),
46 | }
47 |
48 | log.Printf("Starting authentication end service on port %s\n", webPort)
49 | err := srv.ListenAndServe()
50 |
51 | if err != nil {
52 | log.Panic(err)
53 | }
54 | }
55 |
56 | func connectToDB() *sql.DB {
57 | // connect to postgres
58 | dsn := os.Getenv("DSN")
59 |
60 | for {
61 | connection, err := openDB(dsn)
62 | if err != nil {
63 | log.Println("Postgres not ready...")
64 | counts++
65 | } else {
66 | log.Println("Connected to database!")
67 | return connection
68 | }
69 |
70 | if counts > 10 {
71 | log.Println(err)
72 | return nil
73 | }
74 |
75 | log.Println("Backing off for two seconds...")
76 | time.Sleep(2 * time.Second)
77 | continue
78 | }
79 | }
80 |
81 | func openDB(dsn string) (*sql.DB, error) {
82 | db, err := sql.Open("pgx", dsn)
83 | if err != nil {
84 | return nil, err
85 | }
86 |
87 | err = db.Ping()
88 | if err != nil {
89 | return nil, err
90 | }
91 |
92 | return db, nil
93 | }
94 |
--------------------------------------------------------------------------------
/authentication-service/cmd/api/routes.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/go-chi/chi/v5"
5 | "github.com/go-chi/chi/v5/middleware"
6 | "github.com/go-chi/cors"
7 | "net/http"
8 | )
9 |
10 | func (app *Config) routes() http.Handler {
11 | mux := chi.NewRouter()
12 |
13 | // specify who is allowed to connect to our API service
14 | mux.Use(cors.Handler(cors.Options{
15 | AllowedOrigins: []string{"*"},
16 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
17 | AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
18 | ExposedHeaders: []string{"Link"},
19 | AllowCredentials: true,
20 | MaxAge: 300,
21 | }))
22 |
23 | mux.Use(middleware.Heartbeat("/ping"))
24 |
25 | mux.Post("/authenticate", app.Authenticate)
26 |
27 | return mux
28 | }
29 |
--------------------------------------------------------------------------------
/authentication-service/data/models.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "errors"
7 | "log"
8 | "time"
9 |
10 | "golang.org/x/crypto/bcrypt"
11 | )
12 |
13 | const dbTimeout = time.Second * 3
14 |
15 | var db *sql.DB
16 |
17 | // New is the function used to create an instance of the data package. It returns the type
18 | // Model, which embeds all the types we want to be available to our application.
19 | func New(dbPool *sql.DB) Models {
20 | db = dbPool
21 |
22 | return Models{
23 | User: User{},
24 | }
25 | }
26 |
27 | // Models is the type for this package. Note that any model that is included as a member
28 | // in this type is available to us throughout the application, anywhere that the
29 | // app variable is used, provided that the model is also added in the New function.
30 | type Models struct {
31 | User User
32 | }
33 |
34 | // User is the structure which holds one user from the database.
35 | type User struct {
36 | ID int `json:"id"`
37 | Email string `json:"email"`
38 | FirstName string `json:"first_name,omitempty"`
39 | LastName string `json:"last_name,omitempty"`
40 | Password string `json:"-"`
41 | Active int `json:"active"`
42 | CreatedAt time.Time `json:"created_at"`
43 | UpdatedAt time.Time `json:"updated_at"`
44 | }
45 |
46 | // GetAll returns a slice of all users, sorted by last name
47 | func (u *User) GetAll() ([]*User, error) {
48 | ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
49 | defer cancel()
50 |
51 | query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at
52 | from users order by last_name`
53 |
54 | rows, err := db.QueryContext(ctx, query)
55 | if err != nil {
56 | return nil, err
57 | }
58 | defer rows.Close()
59 |
60 | var users []*User
61 |
62 | for rows.Next() {
63 | var user User
64 | err := rows.Scan(
65 | &user.ID,
66 | &user.Email,
67 | &user.FirstName,
68 | &user.LastName,
69 | &user.Password,
70 | &user.Active,
71 | &user.CreatedAt,
72 | &user.UpdatedAt,
73 | )
74 | if err != nil {
75 | log.Println("Error scanning", err)
76 | return nil, err
77 | }
78 |
79 | users = append(users, &user)
80 | }
81 |
82 | return users, nil
83 | }
84 |
85 | // GetByEmail returns one user by email
86 | func (u *User) GetByEmail(email string) (*User, error) {
87 | ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
88 | defer cancel()
89 |
90 | query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at from users where email = $1`
91 |
92 | var user User
93 | row := db.QueryRowContext(ctx, query, email)
94 |
95 | err := row.Scan(
96 | &user.ID,
97 | &user.Email,
98 | &user.FirstName,
99 | &user.LastName,
100 | &user.Password,
101 | &user.Active,
102 | &user.CreatedAt,
103 | &user.UpdatedAt,
104 | )
105 |
106 | if err != nil {
107 | return nil, err
108 | }
109 |
110 | return &user, nil
111 | }
112 |
113 | // GetOne returns one user by id
114 | func (u *User) GetOne(id int) (*User, error) {
115 | ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
116 | defer cancel()
117 |
118 | query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at from users where id = $1`
119 |
120 | var user User
121 | row := db.QueryRowContext(ctx, query, id)
122 |
123 | err := row.Scan(
124 | &user.ID,
125 | &user.Email,
126 | &user.FirstName,
127 | &user.LastName,
128 | &user.Password,
129 | &user.Active,
130 | &user.CreatedAt,
131 | &user.UpdatedAt,
132 | )
133 |
134 | if err != nil {
135 | return nil, err
136 | }
137 |
138 | return &user, nil
139 | }
140 |
141 | // Update updates one user in the database, using the information
142 | // stored in the receiver u
143 | func (u *User) Update() error {
144 | ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
145 | defer cancel()
146 |
147 | stmt := `update users set
148 | email = $1,
149 | first_name = $2,
150 | last_name = $3,
151 | user_active = $4,
152 | updated_at = $5
153 | where id = $6
154 | `
155 |
156 | _, err := db.ExecContext(ctx, stmt,
157 | u.Email,
158 | u.FirstName,
159 | u.LastName,
160 | u.Active,
161 | time.Now(),
162 | u.ID,
163 | )
164 |
165 | if err != nil {
166 | return err
167 | }
168 |
169 | return nil
170 | }
171 |
172 | // Delete deletes one user from the database, by User.ID
173 | func (u *User) Delete() error {
174 | ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
175 | defer cancel()
176 |
177 | stmt := `delete from users where id = $1`
178 |
179 | _, err := db.ExecContext(ctx, stmt, u.ID)
180 | if err != nil {
181 | return err
182 | }
183 |
184 | return nil
185 | }
186 |
187 | // DeleteByID deletes one user from the database, by ID
188 | func (u *User) DeleteByID(id int) error {
189 | ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
190 | defer cancel()
191 |
192 | stmt := `delete from users where id = $1`
193 |
194 | _, err := db.ExecContext(ctx, stmt, id)
195 | if err != nil {
196 | return err
197 | }
198 |
199 | return nil
200 | }
201 |
202 | // Insert inserts a new user into the database, and returns the ID of the newly inserted row
203 | func (u *User) Insert(user User) (int, error) {
204 | ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
205 | defer cancel()
206 |
207 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
208 | if err != nil {
209 | return 0, err
210 | }
211 |
212 | var newID int
213 | stmt := `insert into users (email, first_name, last_name, password, user_active, created_at, updated_at)
214 | values ($1, $2, $3, $4, $5, $6, $7) returning id`
215 |
216 | err = db.QueryRowContext(ctx, stmt,
217 | user.Email,
218 | user.FirstName,
219 | user.LastName,
220 | hashedPassword,
221 | user.Active,
222 | time.Now(),
223 | time.Now(),
224 | ).Scan(&newID)
225 |
226 | if err != nil {
227 | return 0, err
228 | }
229 |
230 | return newID, nil
231 | }
232 |
233 | // ResetPassword is the method we will use to change a user's password.
234 | func (u *User) ResetPassword(password string) error {
235 | ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
236 | defer cancel()
237 |
238 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
239 | if err != nil {
240 | return err
241 | }
242 |
243 | stmt := `update users set password = $1 where id = $2`
244 | _, err = db.ExecContext(ctx, stmt, hashedPassword, u.ID)
245 | if err != nil {
246 | return err
247 | }
248 |
249 | return nil
250 | }
251 |
252 | // PasswordMatches uses Go's bcrypt package to compare a user supplied password
253 | // with the hash we have stored for a given user in the database. If the password
254 | // and hash match, we return true; otherwise, we return false.
255 | func (u *User) PasswordMatches(plainText string) (bool, error) {
256 | err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(plainText))
257 | if err != nil {
258 | switch {
259 | case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword):
260 | // invalid password
261 | return false, nil
262 | default:
263 | return false, err
264 | }
265 | }
266 |
267 | return true, nil
268 | }
269 |
--------------------------------------------------------------------------------
/authentication-service/go.mod:
--------------------------------------------------------------------------------
1 | module authentication
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/go-chi/chi/v5 v5.0.8
7 | github.com/go-chi/cors v1.2.1
8 | github.com/jackc/pgconn v1.14.0
9 | github.com/jackc/pgx/v4 v4.18.1
10 | go.etcd.io/etcd/client/v3 v3.5.2
11 | golang.org/x/crypto v0.6.0
12 | )
13 |
14 | require (
15 | github.com/coreos/go-semver v0.3.0 // indirect
16 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect
17 | github.com/gogo/protobuf v1.3.2 // indirect
18 | github.com/golang/protobuf v1.5.2 // indirect
19 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect
20 | github.com/jackc/pgio v1.0.0 // indirect
21 | github.com/jackc/pgpassfile v1.0.0 // indirect
22 | github.com/jackc/pgproto3/v2 v2.3.2 // indirect
23 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
24 | github.com/jackc/pgtype v1.14.0 // indirect
25 | go.etcd.io/etcd/api/v3 v3.5.2 // indirect
26 | go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect
27 | go.uber.org/atomic v1.9.0 // indirect
28 | go.uber.org/multierr v1.8.0 // indirect
29 | go.uber.org/zap v1.21.0 // indirect
30 | golang.org/x/net v0.6.0 // indirect
31 | golang.org/x/sys v0.5.0 // indirect
32 | golang.org/x/text v0.7.0 // indirect
33 | google.golang.org/genproto v0.0.0-20220328150716-24ca77f39d1f // indirect
34 | google.golang.org/grpc v1.45.0 // indirect
35 | google.golang.org/protobuf v1.28.0 // indirect
36 | )
37 |
--------------------------------------------------------------------------------
/authentication-service/users.sql:
--------------------------------------------------------------------------------
1 |
2 |
3 | --
4 | -- Name: user_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
5 | --
6 |
7 | CREATE SEQUENCE public.user_id_seq
8 | START WITH 1
9 | INCREMENT BY 1
10 | NO MINVALUE
11 | NO MAXVALUE
12 | CACHE 1;
13 |
14 |
15 | ALTER TABLE public.user_id_seq OWNER TO postgres;
16 |
17 | SET default_tablespace = '';
18 |
19 | SET default_table_access_method = heap;
20 |
21 | --
22 | -- Name: users; Type: TABLE; Schema: public; Owner: postgres
23 | --
24 |
25 | CREATE TABLE public.users (
26 | id integer DEFAULT nextval('public.user_id_seq'::regclass) NOT NULL,
27 | email character varying(255),
28 | first_name character varying(255),
29 | last_name character varying(255),
30 | password character varying(60),
31 | user_active integer DEFAULT 0,
32 | created_at timestamp without time zone,
33 | updated_at timestamp without time zone
34 | );
35 |
36 |
37 | ALTER TABLE public.users OWNER TO postgres;
38 |
39 | --
40 | -- Name: user_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
41 | --
42 |
43 | SELECT pg_catalog.setval('public.user_id_seq', 1, true);
44 |
45 |
46 | --
47 | -- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
48 | --
49 |
50 | ALTER TABLE ONLY public.users
51 | ADD CONSTRAINT users_pkey PRIMARY KEY (id);
52 |
53 |
54 | INSERT INTO "public"."users"("email","first_name","last_name","password","user_active","created_at","updated_at")
55 | VALUES
56 | (E'admin@example.com',E'Admin',E'User',E'$2a$12$1zGLuYDDNvATh4RA4avbKuheAMpb1svexSzrQm7up.bnpwQHs0jNe',1,E'2022-03-14 00:00:00',E'2022-03-14 00:00:00');
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/broker-service.dockerfile:
--------------------------------------------------------------------------------
1 | # The base go-image
2 | FROM alpine:latest
3 | RUN mkdir /app
4 |
5 | COPY broker-service/brokerApp /app
6 |
7 | # Run the server executable
8 | CMD [ "/app/brokerApp" ]
--------------------------------------------------------------------------------
/broker-service/Makefile:
--------------------------------------------------------------------------------
1 | gen:
2 | protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative logs/logs.proto
3 |
--------------------------------------------------------------------------------
/broker-service/cmd/api/discover.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | clientv3 "go.etcd.io/etcd/client/v3"
6 | "time"
7 | )
8 |
9 | //func (app *Config) getServiceURLs() {
10 | // kv := clientv3.NewKV(app.Etcd)
11 | // app.MailServiceURLs = make(map[string]string)
12 | // app.LogServiceURLs = make(map[string]string)
13 | // app.AuthServiceURLs = make(map[string]string)
14 | //
15 | // prefixes := []string{"/mail/", "/logger/", "/auth/"}
16 | //
17 | // // range through all the services we want to discover
18 | // for _, curPrefix := range prefixes {
19 | // getResp, err := kv.Get(context.TODO(), curPrefix, clientv3.WithPrefix())
20 | // if err != nil {
21 | // log.Println(err)
22 | // }
23 | //
24 | // for _, k := range getResp.Kvs {
25 | // //log.Println("Key", string(k.Key))
26 | // //log.Println("Adding", string(k.Value), "to", curPrefix, "service map; key was", string(k.Key))
27 | // switch curPrefix {
28 | // case "/mail/":
29 | // app.MailServiceURLs[string(k.Value)] = ""
30 | // case "/logger/":
31 | // app.LogServiceURLs[string(k.Value)] = ""
32 | // case "/auth/":
33 | // app.AuthServiceURLs[string(k.Value)] = ""
34 | // }
35 | // }
36 | // }
37 | //}
38 |
39 | // watchEtcd runs in the background, looking for changes in etcd. When it finds changes
40 | // hosts, it updates the appropriate map in the *Config receiver.
41 | //func (app *Config) watchEtcd() {
42 | // for {
43 | // // watch for service changes
44 | // watchKey := app.Etcd.Watch(context.Background(), "/mail/", clientv3.WithPrefix())
45 | // for resp := range watchKey {
46 | // for _, item := range resp.Events {
47 | // // get our values as strings so that we can work with them
48 | // eventType := item.Type.String()
49 | // key := string(item.Kv.Key)
50 | // value := string(item.Kv.Value)
51 | // var deleteURL = false
52 | // if strings.Contains(eventType, "DELETE") {
53 | // deleteURL = true
54 | // }
55 | //
56 | // // add to or remove from service maps (using url as key, and empty string as value)
57 | // switch {
58 | // case strings.HasPrefix(key, "mail"):
59 | // // mail
60 | // if deleteURL {
61 | // log.Println("Removing", value, "from mail service map")
62 | // delete(app.MailServiceURLs, key)
63 | // } else {
64 | // log.Println("Adding", value, "to mail service map")
65 | // app.MailServiceURLs[value] = ""
66 | // }
67 | //
68 | // case strings.HasPrefix(key, "logger"):
69 | // // logger
70 | // if deleteURL {
71 | // delete(app.LogServiceURLs, key)
72 | // } else {
73 | // app.LogServiceURLs[value] = ""
74 | // }
75 | //
76 | // case strings.HasPrefix(key, "auth"):
77 | // // authentication
78 | // if deleteURL {
79 | // delete(app.AuthServiceURLs, key)
80 | // } else {
81 | // app.AuthServiceURLs[value] = ""
82 | // }
83 | // }
84 | // }
85 | // }
86 | // }
87 | //}
88 |
89 | // GetServiceURL will get a service's url from those listed as available in etcd
90 | //func (app *Config) GetServiceURL(serviceType string) string {
91 | // var serviceURL string
92 | //
93 | // // get service URL from etcd
94 | // switch serviceType {
95 | // case "mail":
96 | // serviceURL = getUrlFromMap(app.MailServiceURLs)
97 | // case "logger":
98 | // serviceURL = getUrlFromMap(app.LogServiceURLs)
99 | // case "auth":
100 | // serviceURL = getUrlFromMap(app.AuthServiceURLs)
101 | // }
102 | //
103 | // return serviceURL
104 | //}
105 |
106 | // getUrlFromMap returns a random value from available urls in
107 | // service maps. Since maps are never guaranteed to be in the same order,
108 | // grabbing the first value is sufficient for our purposes.
109 | func getUrlFromMap(m map[string]string) string {
110 | var u string
111 | for k := range m {
112 | u = k
113 | break
114 | }
115 | return u
116 | }
117 |
118 | // connectToEtcd tries to connect to etcd, for up to 30 seconds
119 | func connectToEtcd() (*clientv3.Client, error) {
120 | var cli *clientv3.Client
121 | var counts = 0
122 |
123 | for {
124 | c, err := clientv3.New(clientv3.Config{Endpoints: []string{"etcd:2379"},
125 | DialTimeout: 5 * time.Second,
126 | })
127 | if err != nil {
128 | fmt.Println("etcd not ready...")
129 | counts++
130 | } else {
131 | fmt.Println()
132 | cli = c
133 | break
134 | }
135 |
136 | if counts > 15 {
137 | return nil, err
138 | }
139 | fmt.Println("Backing off for 2 seconds...")
140 | time.Sleep(2 * time.Second)
141 | continue
142 | }
143 | fmt.Println("Connected to etcd!")
144 | return cli, nil
145 | }
146 |
--------------------------------------------------------------------------------
/broker-service/cmd/api/handlers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "broker/event"
5 | "broker/logs"
6 | "bytes"
7 | "context"
8 | "encoding/json"
9 | "errors"
10 | "fmt"
11 | "google.golang.org/grpc"
12 | "google.golang.org/grpc/credentials/insecure"
13 | "log"
14 | "net/http"
15 | "net/rpc"
16 | "time"
17 | )
18 |
19 | const loggerGRPCAddress = "logger-service:50001"
20 |
21 | // Payload is the type for data we push into RabbitMQ
22 | type Payload struct {
23 | Name string `json:"name"`
24 | Data any `json:"data"`
25 | }
26 |
27 | // RequestPayload is the type describing the data that we received
28 | // from the user's browser. We embed a custom type for each of the
29 | // possible payloads (mail, auth, and log).
30 | type RequestPayload struct {
31 | Action string `json:"action"`
32 | Mail MailPayload `json:"mail,omitempty"`
33 | Auth AuthPayload `json:"auth,omitempty"`
34 | Log LogPayload `json:"log,omitempty"`
35 | }
36 |
37 | // AuthPayload is the type embedded in RequestPayload for auth
38 | type AuthPayload struct {
39 | Email string `json:"email"`
40 | Password string `json:"password"`
41 | }
42 |
43 | // LogPayload is the type embedded in RequestPayload for logging
44 | type LogPayload struct {
45 | Name string `json:"name"`
46 | Data string `json:"data"`
47 | }
48 |
49 | // MailPayload is the type embedded in RequestPayload for sending email
50 | type MailPayload struct {
51 | From string `json:"from"`
52 | To string `json:"to"`
53 | Subject string `json:"subject"`
54 | Message string `json:"message"`
55 | }
56 |
57 | // Broker is a simple test handler for the broker
58 | func (app *Config) Broker(w http.ResponseWriter, r *http.Request) {
59 | err := app.pushToQueue("broker_hit", r.RemoteAddr)
60 | if err != nil {
61 | log.Println(err)
62 | }
63 |
64 | var payload jsonResponse
65 | payload.Message = "Received request"
66 |
67 | out, _ := json.MarshalIndent(payload, "", "\t")
68 | w.Header().Set("Content-Type", "application/json")
69 | w.WriteHeader(http.StatusAccepted)
70 | _, _ = w.Write(out)
71 | }
72 |
73 | // HandleSubmission handles a JSON payload that describes an action to take,
74 | // processes it, and sends it where it needs to go
75 | func (app *Config) HandleSubmission(w http.ResponseWriter, r *http.Request) {
76 | var requestPayload RequestPayload
77 |
78 | err := app.readJSON(w, r, &requestPayload)
79 | if err != nil {
80 | _ = app.errorJSON(w, err)
81 | return
82 | }
83 |
84 | switch requestPayload.Action {
85 | case "mail":
86 | app.sendMail(w, requestPayload.Mail)
87 | case "auth":
88 | app.authenticate(w, requestPayload.Auth)
89 | case "log":
90 | app.logItemViaRPC(w, requestPayload.Log)
91 | default:
92 | _ = app.errorJSON(w, errors.New("unknown action"))
93 | }
94 | }
95 |
96 | func (app *Config) logViaJSON(w http.ResponseWriter, entry LogPayload) {
97 | jsonData, _ := json.MarshalIndent(entry, "", "\t")
98 | logServiceURL := "http://logger-service/log"
99 |
100 | request, err := http.NewRequest("POST", logServiceURL, bytes.NewBuffer(jsonData))
101 | request.Header.Set("Content-Type", "application/json")
102 |
103 | client := &http.Client{}
104 | response, err := client.Do(request)
105 | if err != nil {
106 | _ = app.errorJSON(w, err, http.StatusBadRequest)
107 | return
108 | }
109 | defer response.Body.Close()
110 |
111 | // make sure we get back the right status code
112 | if response.StatusCode != http.StatusAccepted {
113 | _ = app.errorJSON(w, errors.New("error calling logger service"), http.StatusBadRequest)
114 | return
115 | }
116 |
117 | // send json back to our end user
118 | var payload jsonResponse
119 | payload.Error = false
120 | payload.Message = "Logged!"
121 |
122 | _ = app.writeJSON(w, http.StatusAccepted, payload)
123 | }
124 |
125 | // sendMail sends an email through the mail-service. It receives a json payload
126 | // of type requestPayload, with MailPayload embedded.
127 | func (app *Config) sendMail(w http.ResponseWriter, msg MailPayload) {
128 | jsonData, _ := json.MarshalIndent(msg, "", "\t")
129 |
130 | // call the mail service; we need a request, so let's build one, and populate
131 | // its body with the jsonData we just created. First we get the correct server
132 | // to call from our service map.
133 | //mailServiceURL := fmt.Sprintf("http://%s/send", app.GetServiceURL("mail"))
134 | mailServiceURL := fmt.Sprintf("http://%s/send", "mail-service")
135 |
136 | // now post to the mail service
137 | request, err := http.NewRequest("POST", mailServiceURL, bytes.NewBuffer(jsonData))
138 | request.Header.Set("Content-Type", "application/json")
139 |
140 | client := &http.Client{}
141 | response, err := client.Do(request)
142 | if err != nil {
143 | _ = app.errorJSON(w, err, http.StatusBadRequest)
144 | return
145 | }
146 | defer response.Body.Close()
147 |
148 | // make sure we get back the right status code
149 | if response.StatusCode != http.StatusAccepted {
150 | _ = app.errorJSON(w, errors.New("error calling mail service"), http.StatusBadRequest)
151 | return
152 | }
153 |
154 | // send json back to our end user
155 | var payload jsonResponse
156 | payload.Error = false
157 | payload.Message = "Message sent to " + msg.To
158 |
159 | out, _ := json.MarshalIndent(payload, "", "\t")
160 | w.Header().Set("Content-Type", "application/json")
161 | w.WriteHeader(http.StatusAccepted)
162 | _, _ = w.Write(out)
163 |
164 | }
165 |
166 | // authenticate tries to log a user in through the authentication-service. It receives a json payload
167 | // of type requestPayload, with AuthPayload embedded.
168 | func (app *Config) authenticate(w http.ResponseWriter, a AuthPayload) {
169 | // create json we'll send to the authentication-service
170 | jsonData, _ := json.MarshalIndent(a, "", "\t")
171 |
172 | // call the authentication-service; we need a request, so let's build one, and populate
173 | // its body with the jsonData we just created. First we get the correct url for our
174 | // auth service from our service map.
175 | //authServiceURL := fmt.Sprintf("http://%s/authenticate", app.GetServiceURL("auth"))
176 | authServiceURL := fmt.Sprintf("http://%s/authenticate", "authentication-service")
177 |
178 | // now build the request and set header
179 | request, err := http.NewRequest("POST", authServiceURL, bytes.NewBuffer(jsonData))
180 | request.Header.Set("Content-Type", "application/json")
181 |
182 | // call the service
183 | client := &http.Client{}
184 | response, err := client.Do(request)
185 | if err != nil {
186 | _ = app.errorJSON(w, err, http.StatusBadRequest)
187 | return
188 | }
189 | defer response.Body.Close()
190 |
191 | // make sure we get back the right status code
192 | if response.StatusCode == http.StatusUnauthorized {
193 | _ = app.errorJSON(w, errors.New("invalid credentials"), http.StatusUnauthorized)
194 | return
195 | } else if response.StatusCode != http.StatusAccepted {
196 | _ = app.errorJSON(w, errors.New("error calling auth service"), http.StatusBadRequest)
197 | return
198 | }
199 |
200 | // create variable we'll read the response.Body from the authentication-service into
201 | var jsonFromService jsonResponse
202 |
203 | // decode the json we get from the authentication-service into our variable
204 | err = json.NewDecoder(response.Body).Decode(&jsonFromService)
205 | if err != nil {
206 | _ = app.errorJSON(w, err, http.StatusBadRequest)
207 | return
208 | }
209 |
210 | // did not authenticate successfully
211 | if jsonFromService.Error {
212 | // log it
213 | _ = app.pushToQueue("authentication", fmt.Sprintf("invalid login for %s", a.Email))
214 | // send error JSON back
215 | _ = app.errorJSON(w, err, http.StatusUnauthorized)
216 | return
217 | }
218 |
219 | // valid login, so send it to the logger service via RabbitMQ
220 | _ = app.pushToQueue("authentication", fmt.Sprintf("valid login for %s", a.Email))
221 |
222 | // send json back to our end user, with user info embedded
223 | var payload jsonResponse
224 | payload.Error = false
225 | payload.Message = "Authenticated!"
226 | payload.Data = jsonFromService.Data
227 |
228 | _ = app.writeJSON(w, http.StatusAccepted, payload)
229 | }
230 |
231 | // logItem logs an event using the logger-service. It makes the call by pushing the data to RabbitMQ.
232 | func (app *Config) logItem(w http.ResponseWriter, l LogPayload) {
233 | err := app.pushToQueue(l.Name, l.Data)
234 | if err != nil {
235 | log.Println(err)
236 | _ = app.errorJSON(w, err)
237 | }
238 |
239 | // send json back to our end user
240 | var payload jsonResponse
241 | payload.Error = false
242 | payload.Message = "logged"
243 |
244 | _ = app.writeJSON(w, http.StatusAccepted, payload)
245 | }
246 |
247 | type RPCPayload struct {
248 | Name string
249 | Data string
250 | }
251 |
252 | func (app *Config) logItemViaRPC(w http.ResponseWriter, l LogPayload) {
253 | client, err := rpc.Dial("tcp", "logger-service:5001")
254 | if err != nil {
255 | app.errorJSON(w, err)
256 | return
257 | }
258 |
259 | rpcPayload := RPCPayload{
260 | Name: l.Name,
261 | Data: l.Data,
262 | }
263 |
264 | var result string
265 | err = client.Call("RPCServer.LogInfo", rpcPayload, &result)
266 | if err != nil {
267 | app.errorJSON(w, err)
268 | return
269 | }
270 |
271 | payload := jsonResponse{
272 | Error: false,
273 | Message: result,
274 | }
275 |
276 | app.writeJSON(w, http.StatusAccepted, payload)
277 | }
278 |
279 | // pushToQueue pushes a message into RabbitMQ
280 | func (app *Config) pushToQueue(name, msg string) error {
281 | emitter, err := event.NewEventEmitter(app.Rabbit)
282 | if err != nil {
283 | log.Println(err)
284 | return err
285 | }
286 |
287 | payload := Payload{
288 | Name: name,
289 | Data: msg,
290 | }
291 |
292 | j, _ := json.MarshalIndent(&payload, "", " ")
293 | err = emitter.Push(string(j), "log.INFO")
294 | if err != nil {
295 | return err
296 | }
297 | return nil
298 | }
299 |
300 | // LogViaGRPC takes a JSON payload and logs it using gRPC as the transport
301 | func (app *Config) LogViaGRPC(w http.ResponseWriter, r *http.Request) {
302 | var requestPayload RequestPayload
303 |
304 | err := app.readJSON(w, r, &requestPayload)
305 | if err != nil {
306 | _ = app.errorJSON(w, err)
307 | return
308 | }
309 |
310 | conn, err := grpc.Dial(loggerGRPCAddress, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock())
311 | if err != nil {
312 | _ = app.errorJSON(w, err)
313 | return
314 | }
315 | defer conn.Close()
316 |
317 | c := logs.NewLogServiceClient(conn)
318 | ctx, cancel := context.WithTimeout(context.Background(), time.Second)
319 | defer cancel()
320 | _, err = c.WriteLog(ctx, &logs.LogRequest{
321 | LogEntry: &logs.Log{
322 | Name: requestPayload.Log.Name,
323 | Data: requestPayload.Log.Data,
324 | },
325 | })
326 | if err != nil {
327 | _ = app.errorJSON(w, err)
328 | return
329 | }
330 |
331 | var payload jsonResponse
332 | payload.Error = false
333 | payload.Message = "logged"
334 |
335 | _ = app.writeJSON(w, http.StatusAccepted, payload)
336 | }
337 |
--------------------------------------------------------------------------------
/broker-service/cmd/api/helpers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "io"
7 | "net/http"
8 | )
9 |
10 | type jsonResponse struct {
11 | Error bool `json:"error"`
12 | Message string `json:"message"`
13 | Data any `json:"data,omitempty"`
14 | }
15 |
16 | // readJSON tries to read the body of a request and converts it into JSON
17 | func (app *Config) readJSON(w http.ResponseWriter, r *http.Request, data any) error {
18 | maxBytes := 1048576 // one megabyte
19 | r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
20 |
21 | dec := json.NewDecoder(r.Body)
22 | err := dec.Decode(data)
23 | if err != nil {
24 | return err
25 | }
26 |
27 | err = dec.Decode(&struct{}{})
28 | if err != io.EOF {
29 | return errors.New("body must have only a single json value")
30 | }
31 |
32 | return nil
33 | }
34 |
35 | // writeJSON takes a response status code and arbitrary data and writes a json response to the client
36 | func (app *Config) writeJSON(w http.ResponseWriter, status int, data any, headers ...http.Header) error {
37 | out, err := json.Marshal(data)
38 | if err != nil {
39 | return err
40 | }
41 |
42 | if len(headers) > 0 {
43 | for key, value := range headers[0] {
44 | w.Header()[key] = value
45 | }
46 | }
47 |
48 | w.Header().Set("Content-Type", "application/json")
49 | w.WriteHeader(status)
50 | _, err = w.Write(out)
51 | if err != nil {
52 | return err
53 | }
54 |
55 | return nil
56 | }
57 |
58 | // errorJSON takes an error, and optionally a response status code, and generates and sends
59 | // a json error response
60 | func (app *Config) errorJSON(w http.ResponseWriter, err error, status ...int) error {
61 | statusCode := http.StatusBadRequest
62 |
63 | if len(status) > 0 {
64 | statusCode = status[0]
65 | }
66 |
67 | var payload jsonResponse
68 | payload.Error = true
69 | payload.Message = err.Error()
70 |
71 | return app.writeJSON(w, statusCode, payload)
72 | }
73 |
--------------------------------------------------------------------------------
/broker-service/cmd/api/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | amqp "github.com/rabbitmq/amqp091-go"
7 | clientv3 "go.etcd.io/etcd/client/v3"
8 | "log"
9 | "net/http"
10 | "os"
11 | "time"
12 | )
13 |
14 | // webPort the port that we listen on for api calls
15 | const webPort = "80"
16 |
17 | // Config is the type we'll use as a receiver to share application
18 | // configuration around our app.
19 | type Config struct {
20 | Rabbit *amqp.Connection
21 | Etcd *clientv3.Client
22 | LogServiceURLs map[string]string
23 | //MailServiceURLs map[string]string
24 | //AuthServiceURLs map[string]string
25 | }
26 |
27 | func main() {
28 | // don't continue until rabbitmq is ready
29 | rabbitConn, err := connectToRabbit()
30 | if err != nil {
31 | fmt.Println(err)
32 | os.Exit(1)
33 | }
34 | defer rabbitConn.Close()
35 |
36 | // don't continue until etcd is ready
37 | //etcConn, err := connectToEtcd()
38 | //if err != nil {
39 | // fmt.Println(err)
40 | // os.Exit(1)
41 | //}
42 | //defer etcConn.Close()
43 |
44 | app := Config{
45 | Rabbit: rabbitConn,
46 | //Etcd: etcConn,
47 | }
48 |
49 | // get service urls
50 | //app.getServiceURLs()
51 |
52 | // watch service urls
53 | //go app.watchEtcd()
54 |
55 | log.Println("Starting broker service on port", webPort)
56 |
57 | // define the http server
58 | srv := &http.Server{
59 | Addr: fmt.Sprintf(":%s", webPort),
60 | Handler: app.routes(),
61 | }
62 |
63 | // start the server
64 | err = srv.ListenAndServe()
65 | if err != nil {
66 | log.Panic(err)
67 | }
68 | }
69 |
70 | // connectToRabbit tries to connect to RabbitMQ, for up to 30 seconds
71 | func connectToRabbit() (*amqp.Connection, error) {
72 | var rabbitConn *amqp.Connection
73 | var counts int64
74 | var rabbitURL = os.Getenv("RABBIT_URL")
75 |
76 | for {
77 | connection, err := amqp.Dial(rabbitURL)
78 | if err != nil {
79 | fmt.Println("rabbitmq not ready...")
80 | counts++
81 | } else {
82 | fmt.Println()
83 | rabbitConn = connection
84 | break
85 | }
86 |
87 | if counts > 15 {
88 | fmt.Println(err)
89 | return nil, errors.New("cannot connect to rabbit")
90 | }
91 | fmt.Println("Backing off for 2 seconds...")
92 | time.Sleep(2 * time.Second)
93 | continue
94 | }
95 | fmt.Println("Connected to RabbitMQ!")
96 | return rabbitConn, nil
97 | }
98 |
--------------------------------------------------------------------------------
/broker-service/cmd/api/routes.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/go-chi/chi/v5"
5 | "github.com/go-chi/chi/v5/middleware"
6 | "github.com/go-chi/cors"
7 | "net/http"
8 | )
9 |
10 | func (app *Config) routes() http.Handler {
11 | mux := chi.NewRouter()
12 |
13 | // specify who is allowed to connect to our API service
14 | mux.Use(cors.Handler(cors.Options{
15 | AllowedOrigins: []string{"https://*", "http://*"},
16 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
17 | AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
18 | ExposedHeaders: []string{"Link"},
19 | AllowCredentials: true,
20 | MaxAge: 300,
21 | }))
22 |
23 | // a heartbeat route, to ensure things are up
24 | mux.Use(middleware.Heartbeat("/ping"))
25 |
26 | // this route is just to ensure things work, and is never
27 | // used after that
28 | mux.Get("/", app.Broker)
29 |
30 | mux.Post("/", app.Broker)
31 |
32 | // grpc route
33 | mux.Post("/log-grpc", app.LogViaGRPC)
34 |
35 | // a route for everything
36 | mux.Post("/handle", app.HandleSubmission)
37 |
38 | return mux
39 | }
40 |
--------------------------------------------------------------------------------
/broker-service/event/emitter.go:
--------------------------------------------------------------------------------
1 | package event
2 |
3 | import (
4 | "context"
5 | "log"
6 |
7 | amqp "github.com/rabbitmq/amqp091-go"
8 | )
9 |
10 | // Emitter for publishing AMQP events
11 | type Emitter struct {
12 | connection *amqp.Connection
13 | }
14 |
15 | func (e *Emitter) setup() error {
16 | channel, err := e.connection.Channel()
17 | if err != nil {
18 | panic(err)
19 | }
20 |
21 | defer channel.Close()
22 | return declareExchange(channel)
23 | }
24 |
25 | // Push (Publish) a specified message to the AMQP exchange
26 | func (e *Emitter) Push(event string, severity string) error {
27 | channel, err := e.connection.Channel()
28 | if err != nil {
29 | return err
30 | }
31 |
32 | defer channel.Close()
33 |
34 | log.Println("Pushing to", getExchangeName())
35 |
36 | err = channel.PublishWithContext(
37 | context.Background(),
38 | getExchangeName(),
39 | severity,
40 | false,
41 | false,
42 | amqp.Publishing{
43 | ContentType: "text/plain",
44 | Body: []byte(event),
45 | },
46 | )
47 |
48 | if err != nil {
49 | log.Println(err)
50 | return err
51 | }
52 | log.Printf("Sending message: %s -> %s", event, getExchangeName())
53 | return nil
54 | }
55 |
56 | // NewEventEmitter returns a new event.Emitter object
57 | // ensuring that the object is initialised, without error
58 | func NewEventEmitter(conn *amqp.Connection) (Emitter, error) {
59 | emitter := Emitter{
60 | connection: conn,
61 | }
62 |
63 | err := emitter.setup()
64 | if err != nil {
65 | return Emitter{}, err
66 | }
67 |
68 | return emitter, nil
69 | }
70 |
--------------------------------------------------------------------------------
/broker-service/event/event.go:
--------------------------------------------------------------------------------
1 | package event
2 |
3 | import (
4 | amqp "github.com/rabbitmq/amqp091-go"
5 | )
6 |
7 | func getExchangeName() string {
8 | return "logs_topic"
9 | }
10 |
11 | func declareExchange(ch *amqp.Channel) error {
12 | return ch.ExchangeDeclare(
13 | getExchangeName(), // name
14 | "topic", // type
15 | true, // durable
16 | false, // auto-deleted
17 | false, // internal
18 | false, // no-wait
19 | nil, // arguments
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/broker-service/go.mod:
--------------------------------------------------------------------------------
1 | module broker
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/go-chi/chi/v5 v5.0.8
7 | github.com/go-chi/cors v1.2.1
8 | github.com/rabbitmq/amqp091-go v1.7.0
9 | go.etcd.io/etcd/client/v3 v3.5.7
10 | google.golang.org/grpc v1.53.0
11 | google.golang.org/protobuf v1.28.1
12 | )
13 |
14 | require (
15 | github.com/coreos/go-semver v0.3.1 // indirect
16 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect
17 | github.com/gogo/protobuf v1.3.2 // indirect
18 | github.com/golang/protobuf v1.5.2 // indirect
19 | go.etcd.io/etcd/api/v3 v3.5.7 // indirect
20 | go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect
21 | go.uber.org/atomic v1.10.0 // indirect
22 | go.uber.org/multierr v1.9.0 // indirect
23 | go.uber.org/zap v1.24.0 // indirect
24 | golang.org/x/net v0.7.0 // indirect
25 | golang.org/x/sys v0.5.0 // indirect
26 | golang.org/x/text v0.7.0 // indirect
27 | google.golang.org/genproto v0.0.0-20230301171018-9ab4bdc49ad5 // indirect
28 | )
29 |
--------------------------------------------------------------------------------
/broker-service/logs/logs.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-go v1.27.1
4 | // protoc v3.19.4
5 | // source: logs/logs.proto
6 |
7 | package logs
8 |
9 | import (
10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect"
11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl"
12 | reflect "reflect"
13 | sync "sync"
14 | )
15 |
16 | const (
17 | // Verify that this generated code is sufficiently up-to-date.
18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
19 | // Verify that runtime/protoimpl is sufficiently up-to-date.
20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
21 | )
22 |
23 | type Log struct {
24 | state protoimpl.MessageState
25 | sizeCache protoimpl.SizeCache
26 | unknownFields protoimpl.UnknownFields
27 |
28 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
29 | Data string `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
30 | }
31 |
32 | func (x *Log) Reset() {
33 | *x = Log{}
34 | if protoimpl.UnsafeEnabled {
35 | mi := &file_logs_logs_proto_msgTypes[0]
36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
37 | ms.StoreMessageInfo(mi)
38 | }
39 | }
40 |
41 | func (x *Log) String() string {
42 | return protoimpl.X.MessageStringOf(x)
43 | }
44 |
45 | func (*Log) ProtoMessage() {}
46 |
47 | func (x *Log) ProtoReflect() protoreflect.Message {
48 | mi := &file_logs_logs_proto_msgTypes[0]
49 | if protoimpl.UnsafeEnabled && x != nil {
50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
51 | if ms.LoadMessageInfo() == nil {
52 | ms.StoreMessageInfo(mi)
53 | }
54 | return ms
55 | }
56 | return mi.MessageOf(x)
57 | }
58 |
59 | // Deprecated: Use Log.ProtoReflect.Descriptor instead.
60 | func (*Log) Descriptor() ([]byte, []int) {
61 | return file_logs_logs_proto_rawDescGZIP(), []int{0}
62 | }
63 |
64 | func (x *Log) GetName() string {
65 | if x != nil {
66 | return x.Name
67 | }
68 | return ""
69 | }
70 |
71 | func (x *Log) GetData() string {
72 | if x != nil {
73 | return x.Data
74 | }
75 | return ""
76 | }
77 |
78 | type LogRequest struct {
79 | state protoimpl.MessageState
80 | sizeCache protoimpl.SizeCache
81 | unknownFields protoimpl.UnknownFields
82 |
83 | LogEntry *Log `protobuf:"bytes,1,opt,name=logEntry,proto3" json:"logEntry,omitempty"`
84 | }
85 |
86 | func (x *LogRequest) Reset() {
87 | *x = LogRequest{}
88 | if protoimpl.UnsafeEnabled {
89 | mi := &file_logs_logs_proto_msgTypes[1]
90 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
91 | ms.StoreMessageInfo(mi)
92 | }
93 | }
94 |
95 | func (x *LogRequest) String() string {
96 | return protoimpl.X.MessageStringOf(x)
97 | }
98 |
99 | func (*LogRequest) ProtoMessage() {}
100 |
101 | func (x *LogRequest) ProtoReflect() protoreflect.Message {
102 | mi := &file_logs_logs_proto_msgTypes[1]
103 | if protoimpl.UnsafeEnabled && x != nil {
104 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
105 | if ms.LoadMessageInfo() == nil {
106 | ms.StoreMessageInfo(mi)
107 | }
108 | return ms
109 | }
110 | return mi.MessageOf(x)
111 | }
112 |
113 | // Deprecated: Use LogRequest.ProtoReflect.Descriptor instead.
114 | func (*LogRequest) Descriptor() ([]byte, []int) {
115 | return file_logs_logs_proto_rawDescGZIP(), []int{1}
116 | }
117 |
118 | func (x *LogRequest) GetLogEntry() *Log {
119 | if x != nil {
120 | return x.LogEntry
121 | }
122 | return nil
123 | }
124 |
125 | type LogResponse struct {
126 | state protoimpl.MessageState
127 | sizeCache protoimpl.SizeCache
128 | unknownFields protoimpl.UnknownFields
129 |
130 | Result string `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"`
131 | }
132 |
133 | func (x *LogResponse) Reset() {
134 | *x = LogResponse{}
135 | if protoimpl.UnsafeEnabled {
136 | mi := &file_logs_logs_proto_msgTypes[2]
137 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
138 | ms.StoreMessageInfo(mi)
139 | }
140 | }
141 |
142 | func (x *LogResponse) String() string {
143 | return protoimpl.X.MessageStringOf(x)
144 | }
145 |
146 | func (*LogResponse) ProtoMessage() {}
147 |
148 | func (x *LogResponse) ProtoReflect() protoreflect.Message {
149 | mi := &file_logs_logs_proto_msgTypes[2]
150 | if protoimpl.UnsafeEnabled && x != nil {
151 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
152 | if ms.LoadMessageInfo() == nil {
153 | ms.StoreMessageInfo(mi)
154 | }
155 | return ms
156 | }
157 | return mi.MessageOf(x)
158 | }
159 |
160 | // Deprecated: Use LogResponse.ProtoReflect.Descriptor instead.
161 | func (*LogResponse) Descriptor() ([]byte, []int) {
162 | return file_logs_logs_proto_rawDescGZIP(), []int{2}
163 | }
164 |
165 | func (x *LogResponse) GetResult() string {
166 | if x != nil {
167 | return x.Result
168 | }
169 | return ""
170 | }
171 |
172 | var File_logs_logs_proto protoreflect.FileDescriptor
173 |
174 | var file_logs_logs_proto_rawDesc = []byte{
175 | 0x0a, 0x0f, 0x6c, 0x6f, 0x67, 0x73, 0x2f, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74,
176 | 0x6f, 0x12, 0x04, 0x6c, 0x6f, 0x67, 0x73, 0x22, 0x2d, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x12,
177 | 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
178 | 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
179 | 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x33, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71,
180 | 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79,
181 | 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x4c, 0x6f,
182 | 0x67, 0x52, 0x08, 0x6c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x25, 0x0a, 0x0b, 0x4c,
183 | 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65,
184 | 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75,
185 | 0x6c, 0x74, 0x32, 0x3f, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
186 | 0x12, 0x31, 0x0a, 0x08, 0x57, 0x72, 0x69, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x12, 0x10, 0x2e, 0x6c,
187 | 0x6f, 0x67, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11,
188 | 0x2e, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
189 | 0x65, 0x22, 0x00, 0x42, 0x07, 0x5a, 0x05, 0x2f, 0x6c, 0x6f, 0x67, 0x73, 0x62, 0x06, 0x70, 0x72,
190 | 0x6f, 0x74, 0x6f, 0x33,
191 | }
192 |
193 | var (
194 | file_logs_logs_proto_rawDescOnce sync.Once
195 | file_logs_logs_proto_rawDescData = file_logs_logs_proto_rawDesc
196 | )
197 |
198 | func file_logs_logs_proto_rawDescGZIP() []byte {
199 | file_logs_logs_proto_rawDescOnce.Do(func() {
200 | file_logs_logs_proto_rawDescData = protoimpl.X.CompressGZIP(file_logs_logs_proto_rawDescData)
201 | })
202 | return file_logs_logs_proto_rawDescData
203 | }
204 |
205 | var file_logs_logs_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
206 | var file_logs_logs_proto_goTypes = []interface{}{
207 | (*Log)(nil), // 0: logs.Log
208 | (*LogRequest)(nil), // 1: logs.LogRequest
209 | (*LogResponse)(nil), // 2: logs.LogResponse
210 | }
211 | var file_logs_logs_proto_depIdxs = []int32{
212 | 0, // 0: logs.LogRequest.logEntry:type_name -> logs.Log
213 | 1, // 1: logs.LogService.WriteLog:input_type -> logs.LogRequest
214 | 2, // 2: logs.LogService.WriteLog:output_type -> logs.LogResponse
215 | 2, // [2:3] is the sub-list for method output_type
216 | 1, // [1:2] is the sub-list for method input_type
217 | 1, // [1:1] is the sub-list for extension type_name
218 | 1, // [1:1] is the sub-list for extension extendee
219 | 0, // [0:1] is the sub-list for field type_name
220 | }
221 |
222 | func init() { file_logs_logs_proto_init() }
223 | func file_logs_logs_proto_init() {
224 | if File_logs_logs_proto != nil {
225 | return
226 | }
227 | if !protoimpl.UnsafeEnabled {
228 | file_logs_logs_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
229 | switch v := v.(*Log); i {
230 | case 0:
231 | return &v.state
232 | case 1:
233 | return &v.sizeCache
234 | case 2:
235 | return &v.unknownFields
236 | default:
237 | return nil
238 | }
239 | }
240 | file_logs_logs_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
241 | switch v := v.(*LogRequest); i {
242 | case 0:
243 | return &v.state
244 | case 1:
245 | return &v.sizeCache
246 | case 2:
247 | return &v.unknownFields
248 | default:
249 | return nil
250 | }
251 | }
252 | file_logs_logs_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
253 | switch v := v.(*LogResponse); i {
254 | case 0:
255 | return &v.state
256 | case 1:
257 | return &v.sizeCache
258 | case 2:
259 | return &v.unknownFields
260 | default:
261 | return nil
262 | }
263 | }
264 | }
265 | type x struct{}
266 | out := protoimpl.TypeBuilder{
267 | File: protoimpl.DescBuilder{
268 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
269 | RawDescriptor: file_logs_logs_proto_rawDesc,
270 | NumEnums: 0,
271 | NumMessages: 3,
272 | NumExtensions: 0,
273 | NumServices: 1,
274 | },
275 | GoTypes: file_logs_logs_proto_goTypes,
276 | DependencyIndexes: file_logs_logs_proto_depIdxs,
277 | MessageInfos: file_logs_logs_proto_msgTypes,
278 | }.Build()
279 | File_logs_logs_proto = out.File
280 | file_logs_logs_proto_rawDesc = nil
281 | file_logs_logs_proto_goTypes = nil
282 | file_logs_logs_proto_depIdxs = nil
283 | }
284 |
--------------------------------------------------------------------------------
/broker-service/logs/logs.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package logs;
4 |
5 | option go_package = "/logs";
6 |
7 | message Log{
8 | string name = 1;
9 | string data =2;
10 | }
11 |
12 | message LogRequest {
13 | Log logEntry = 1;
14 | }
15 |
16 | message LogResponse{
17 | string result = 1;
18 | }
19 |
20 | service LogService{
21 | rpc WriteLog(LogRequest) returns (LogResponse) {}
22 | }
23 |
--------------------------------------------------------------------------------
/broker-service/logs/logs_grpc.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
2 | // versions:
3 | // - protoc-gen-go-grpc v1.2.0
4 | // - protoc v3.19.4
5 | // source: logs/logs.proto
6 |
7 | package logs
8 |
9 | import (
10 | context "context"
11 | grpc "google.golang.org/grpc"
12 | codes "google.golang.org/grpc/codes"
13 | status "google.golang.org/grpc/status"
14 | )
15 |
16 | // This is a compile-time assertion to ensure that this generated file
17 | // is compatible with the grpc package it is being compiled against.
18 | // Requires gRPC-Go v1.32.0 or later.
19 | const _ = grpc.SupportPackageIsVersion7
20 |
21 | // LogServiceClient is the client API for LogService service.
22 | //
23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
24 | type LogServiceClient interface {
25 | WriteLog(ctx context.Context, in *LogRequest, opts ...grpc.CallOption) (*LogResponse, error)
26 | }
27 |
28 | type logServiceClient struct {
29 | cc grpc.ClientConnInterface
30 | }
31 |
32 | func NewLogServiceClient(cc grpc.ClientConnInterface) LogServiceClient {
33 | return &logServiceClient{cc}
34 | }
35 |
36 | func (c *logServiceClient) WriteLog(ctx context.Context, in *LogRequest, opts ...grpc.CallOption) (*LogResponse, error) {
37 | out := new(LogResponse)
38 | err := c.cc.Invoke(ctx, "/logs.LogService/WriteLog", in, out, opts...)
39 | if err != nil {
40 | return nil, err
41 | }
42 | return out, nil
43 | }
44 |
45 | // LogServiceServer is the server API for LogService service.
46 | // All implementations must embed UnimplementedLogServiceServer
47 | // for forward compatibility
48 | type LogServiceServer interface {
49 | WriteLog(context.Context, *LogRequest) (*LogResponse, error)
50 | mustEmbedUnimplementedLogServiceServer()
51 | }
52 |
53 | // UnimplementedLogServiceServer must be embedded to have forward compatible implementations.
54 | type UnimplementedLogServiceServer struct {
55 | }
56 |
57 | func (UnimplementedLogServiceServer) WriteLog(context.Context, *LogRequest) (*LogResponse, error) {
58 | return nil, status.Errorf(codes.Unimplemented, "method WriteLog not implemented")
59 | }
60 | func (UnimplementedLogServiceServer) mustEmbedUnimplementedLogServiceServer() {}
61 |
62 | // UnsafeLogServiceServer may be embedded to opt out of forward compatibility for this service.
63 | // Use of this interface is not recommended, as added methods to LogServiceServer will
64 | // result in compilation errors.
65 | type UnsafeLogServiceServer interface {
66 | mustEmbedUnimplementedLogServiceServer()
67 | }
68 |
69 | func RegisterLogServiceServer(s grpc.ServiceRegistrar, srv LogServiceServer) {
70 | s.RegisterService(&LogService_ServiceDesc, srv)
71 | }
72 |
73 | func _LogService_WriteLog_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
74 | in := new(LogRequest)
75 | if err := dec(in); err != nil {
76 | return nil, err
77 | }
78 | if interceptor == nil {
79 | return srv.(LogServiceServer).WriteLog(ctx, in)
80 | }
81 | info := &grpc.UnaryServerInfo{
82 | Server: srv,
83 | FullMethod: "/logs.LogService/WriteLog",
84 | }
85 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
86 | return srv.(LogServiceServer).WriteLog(ctx, req.(*LogRequest))
87 | }
88 | return interceptor(ctx, in, info, handler)
89 | }
90 |
91 | // LogService_ServiceDesc is the grpc.ServiceDesc for LogService service.
92 | // It's only intended for direct use with grpc.RegisterService,
93 | // and not to be introspected or modified (even as a copy)
94 | var LogService_ServiceDesc = grpc.ServiceDesc{
95 | ServiceName: "logs.LogService",
96 | HandlerType: (*LogServiceServer)(nil),
97 | Methods: []grpc.MethodDesc{
98 | {
99 | MethodName: "WriteLog",
100 | Handler: _LogService_WriteLog_Handler,
101 | },
102 | },
103 | Streams: []grpc.StreamDesc{},
104 | Metadata: "logs/logs.proto",
105 | }
106 |
--------------------------------------------------------------------------------
/caddy.dockerfile:
--------------------------------------------------------------------------------
1 | FROM caddy:2.4.6-alpine
2 |
3 | COPY Caddyfile /etc/caddy/Caddyfile
4 |
--------------------------------------------------------------------------------
/db-data/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !*/
3 | !.gitignore
4 | !.gitkeep
--------------------------------------------------------------------------------
/db-data/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsawler/go-microservices/301cd369d52951cfdd3c991a44ae9b44ba42aa36/db-data/.gitkeep
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 |
5 | # # broker-service - main entry point; we call this from the front end
6 | # broker-service:
7 | # build:
8 | # context: .
9 | # dockerfile: ./broker-service.dockerfile
10 | # restart: always
11 | # ports:
12 | # - "8080:80"
13 | # deploy:
14 | # mode: replicated
15 | # replicas: 1
16 | # environment:
17 | # RABBIT_URL: "amqp://guest:guest@rabbitmq"
18 | #
19 | # # listener-service - watches rabbitmq for messages
20 | # listener-service:
21 | # build:
22 | # context: .
23 | # dockerfile: ./listener-service.dockerfile
24 | # deploy:
25 | # mode: replicated
26 | # replicas: 1
27 | # environment:
28 | # RABBIT_URL: "amqp://guest:guest@rabbitmq"
29 | #
30 | # # authentication-service - handles user auth
31 | # authentication-service:
32 | # build:
33 | # context: .
34 | # dockerfile: ./authentication-service.dockerfile
35 | # restart: always
36 | # ports:
37 | # - "8081:80"
38 | # deploy:
39 | # mode: replicated
40 | # replicas: 1
41 | # environment:
42 | # DSN: "host=postgres port=5432 user=postgres password=password dbname=users sslmode=disable timezone=UTC connect_timeout=5"
43 | #
44 | # # logger-service: a service to store logs
45 | # logger-service:
46 | # build:
47 | # context: .
48 | # dockerfile: ./logger-service.dockerfile
49 | # restart: always
50 | # ports:
51 | # - "8082:80"
52 | # deploy:
53 | # mode: replicated
54 | # replicas: 1
55 | # volumes:
56 | # - ./logger-service/templates/:/app/templates
57 | #
58 | # # mail-service - handles sending mail
59 | # mail-service:
60 | # build:
61 | # context: .
62 | # dockerfile: ./mail-service.dockerfile
63 | # restart: always
64 | # deploy:
65 | # mode: replicated
66 | # replicas: 1
67 | # environment:
68 | # MAIL_DOMAIN: localhost
69 | # MAIL_HOST: mailhog
70 | # MAIL_PORT: 1025
71 | # MAIL_ENCRYPTION: none
72 | # MAIL_USERNAME: ""
73 | # MAIL_PASSWORD: ""
74 | # FROM_NAME: "John Smith"
75 | # FROM_ADDRESS: john.smith@example.com
76 | #
77 | # # rabbitmq: the rabbitmq server
78 | # rabbitmq:
79 | # image: 'rabbitmq:3.9-alpine'
80 | # ports:
81 | # - "5672:5672"
82 | # deploy:
83 | # mode: replicated
84 | # replicas: 1
85 | # volumes:
86 | # - ./db-data/rabbitmq/:/var/lib/rabbitmq/
87 | #
88 | # # mailhog: a fake smtp server with a web interface
89 | # mailhog:
90 | # image: 'mailhog/mailhog:latest'
91 | # ports:
92 | # - "1025:1025"
93 | # - "8025:8025"
94 | # deploy:
95 | # mode: replicated
96 | # replicas: 1
97 | #
98 | # mongo: start MongoDB and ensure that data is stored to a mounted volume
99 | mongo:
100 | image: 'mongo:4.2.17-bionic'
101 | ports:
102 | - "27017:27017"
103 | # restart: always
104 | deploy:
105 | mode: replicated
106 | replicas: 1
107 | environment:
108 | MONGO_INITDB_DATABASE: logs
109 | MONGO_INITDB_ROOT_USERNAME: admin
110 | MONGO_INITDB_ROOT_PASSWORD: password
111 | volumes:
112 | - ./db-data/mongo/:/data/db
113 |
114 | # postgres: start Postgres, and ensure that data is stored to a mounted volume
115 | postgres:
116 | image: 'postgres:14.2'
117 | ports:
118 | - "5432:5432"
119 | restart: always
120 | deploy:
121 | mode: replicated
122 | replicas: 1
123 | environment:
124 | POSTGRES_USER: postgres
125 | POSTGRES_PASSWORD: password
126 | POSTGRES_DB: users
127 | volumes:
128 | - ./db-data/postgres/:/var/lib/postgresql/data/
129 |
130 | # etcd: start etcd server
131 | # etcd:
132 | # image: docker.io/bitnami/etcd:3
133 | # environment:
134 | # - ALLOW_NONE_AUTHENTICATION=yes
135 | # deploy:
136 | # mode: replicated
137 | # replicas: 1
138 | # volumes:
139 | # - ./db-data/etcd/:/bitnami/etcd
140 |
141 |
--------------------------------------------------------------------------------
/front-end.dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 | RUN mkdir /app
3 |
4 | COPY front-end/frontEndLinux /app
5 |
6 | # Run the server executable
7 | CMD [ "/app/frontEndLinux" ]
--------------------------------------------------------------------------------
/front-end/cmd/web/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "fmt"
6 | "html/template"
7 | "log"
8 | "net/http"
9 | )
10 |
11 | func main() {
12 | // the handler to display our page
13 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
14 | render(w, "test.page.gohtml")
15 | })
16 |
17 | // start the web server
18 | fmt.Println("Starting front end service on port 80")
19 | err := http.ListenAndServe(":80", nil)
20 | if err != nil {
21 | log.Panic(err)
22 | }
23 | }
24 |
25 | //go:embed templates
26 | var templateFS embed.FS
27 |
28 | // render generates a page of html from our template files
29 | func render(w http.ResponseWriter, t string) {
30 | // all the required templates for any page
31 | partials := []string{
32 | "templates/base.layout.gohtml",
33 | "templates/header.partial.gohtml",
34 | "templates/footer.partial.gohtml",
35 | }
36 |
37 | // append the template we received as a parameter
38 | var templateSlice []string
39 | templateSlice = append(templateSlice, fmt.Sprintf("templates/%s", t))
40 |
41 | for _, x := range partials {
42 | templateSlice = append(templateSlice, x)
43 | }
44 |
45 | // parse the templates
46 | //tmpl, err := template.ParseFiles(templateSlice...)
47 | tmpl, err := template.ParseFS(templateFS, templateSlice...)
48 | if err != nil {
49 | http.Error(w, err.Error(), http.StatusInternalServerError)
50 | return
51 | }
52 |
53 | // execute the template
54 | if err := tmpl.Execute(w, nil); err != nil {
55 | http.Error(w, err.Error(), http.StatusInternalServerError)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/front-end/cmd/web/templates/base.layout.gohtml:
--------------------------------------------------------------------------------
1 | {{define "base" }}
2 |
3 |
4 |
5 | {{template "header" .}}
6 |
7 |
8 |
9 | {{block "content" .}}
10 |
11 | {{end}}
12 |
13 | {{block "js" .}}
14 |
15 | {{end}}
16 |
17 | {{template "footer" .}}
18 |
19 |
20 |
21 |
22 | {{end}}
--------------------------------------------------------------------------------
/front-end/cmd/web/templates/footer.partial.gohtml:
--------------------------------------------------------------------------------
1 | {{define "footer"}}
2 |
3 |
4 |
5 |
6 | Copyright © GoCode.ca
7 |
8 |
9 |
10 | {{end}}
--------------------------------------------------------------------------------
/front-end/cmd/web/templates/header.partial.gohtml:
--------------------------------------------------------------------------------
1 | {{define "header"}}
2 |
3 |
4 |
5 |
7 |
8 | Microservices in Go
9 |
10 |
11 |
12 |
13 | {{end}}
--------------------------------------------------------------------------------
/front-end/cmd/web/templates/test.page.gohtml:
--------------------------------------------------------------------------------
1 | {{template "base" .}}
2 |
3 | {{define "content" }}
4 |
5 |
19 |
20 |
21 |
Sent
22 |
23 |
Nothing sent yet...
24 |
25 |
26 |
27 |
Received
28 |
29 |
Nothing received yet...
30 |
31 |
32 |
33 |
34 | {{end}}
35 |
36 | {{define "js"}}
37 |
236 | {{end}}
237 |
--------------------------------------------------------------------------------
/front-end/go.mod:
--------------------------------------------------------------------------------
1 | module frontend
2 |
3 | go 1.18
4 |
--------------------------------------------------------------------------------
/ingress.yml:
--------------------------------------------------------------------------------
1 | apiVersion: networking.k8s.io/v1
2 | kind: Ingress
3 | metadata:
4 | name: example-ingress
5 | annotations:
6 | nginx.ingress.kubernetes.io/rewrite-target: /$1
7 | spec:
8 | rules:
9 | - host: broker-service.local
10 | http:
11 | paths:
12 | - path: /
13 | pathType: Prefix
14 | backend:
15 | service:
16 | name: broker-service
17 | port:
18 | number: 8080
19 |
20 |
--------------------------------------------------------------------------------
/k8s.md:
--------------------------------------------------------------------------------
1 | # K8S
2 |
3 | ```
4 | minikube start --nodes=2
5 |
6 | minikube status
7 | docker ps
8 | kubectl get nodes
9 | kubectl get pods -A
10 | kubectl get pods
11 | kubectl apply -f deployment.yml # or kubectl apply -f
12 | kubectl get pods
13 | kubectl get svc
14 | kubectl get deployments
15 | minikube dashboard
16 | ```
17 |
18 | # hit the app
19 | ```
20 | kubectl expose deployment broker --type=LoadBalancer --port=8080 --target-port=80
21 | minikube tunnel
22 | ```
23 |
24 | # stop service/app
25 | ```
26 | kubectl delete deployments
27 | kubectl delete services
28 | ```
29 |
30 | # stop minikube
31 | ```
32 | minikube stop
33 | ```
34 |
--------------------------------------------------------------------------------
/k8s/authentication.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: authentication-service
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: authentication-service
10 | template:
11 | metadata:
12 | labels:
13 | app: authentication-service
14 | spec:
15 | containers:
16 | - name: authentication-service
17 | image: "tsawler/authentication-service:1.0.0"
18 | env:
19 | - name: DSN
20 | value: "host=host.minikube.internal port=5432 user=postgres password=password dbname=users sslmode=disable timezone=UTC connect_timeout=5"
21 | resources:
22 | limits:
23 | memory: "128Mi"
24 | cpu: "500m"
25 | ports:
26 | - containerPort: 80
27 |
28 | ---
29 |
30 | apiVersion: v1
31 | kind: Service
32 | metadata:
33 | name: authentication-service
34 | spec:
35 | selector:
36 | app: authentication-service
37 | ports:
38 | - protocol: TCP
39 | port: 80
40 | targetPort: 80
--------------------------------------------------------------------------------
/k8s/broker.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: broker-service
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: broker-service
10 | template:
11 | metadata:
12 | labels:
13 | app: broker-service
14 | spec:
15 | containers:
16 | - name: broker-service
17 | image: "tsawler/broker-service:1.0.1"
18 | env:
19 | - name: RABBIT_URL
20 | value: "amqp://guest:guest@rabbitmq"
21 | resources:
22 | limits:
23 | memory: "128Mi"
24 | cpu: "500m"
25 | ports:
26 | - containerPort: 80
27 |
28 | ---
29 |
30 | apiVersion: v1
31 | kind: Service
32 | metadata:
33 | name: broker-service
34 | spec:
35 | selector:
36 | app: broker-service
37 | ports:
38 | - protocol: TCP
39 | port: 80
40 | name: web-port
41 | targetPort: 80
42 |
43 |
--------------------------------------------------------------------------------
/k8s/listener.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: listener
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: listener
10 | template:
11 | metadata:
12 | labels:
13 | app: listener
14 | spec:
15 | containers:
16 | - name: listener
17 | image: "tsawler/listener-service:1.0.0"
18 | env:
19 | - name: RABBIT_URL
20 | value: "amqp://guest:guest@rabbitmq"
21 | ports:
22 | - containerPort: 80
23 |
24 | ---
25 |
26 | apiVersion: v1
27 | kind: Service
28 | metadata:
29 | name: listener
30 | spec:
31 | selector:
32 | app: listener
33 | ports:
34 | - protocol: TCP
35 | port: 80
36 | targetPort: 80
--------------------------------------------------------------------------------
/k8s/logger.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: logger-service
5 | spec:
6 | replicas: 2
7 | selector:
8 | matchLabels:
9 | app: logger-service
10 | template:
11 | metadata:
12 | labels:
13 | app: logger-service
14 | spec:
15 | containers:
16 | - name: logger
17 | image: "tsawler/logger-service:1.0.1"
18 | env:
19 | - name: RABBIT_URL
20 | value: "amqp://guest:guest@rabbitmq"
21 | ports:
22 | - containerPort: 80
23 | - containerPort: 5001
24 | - containerPort: 50001
25 |
26 | ---
27 |
28 | apiVersion: v1
29 | kind: Service
30 | metadata:
31 | name: logger-service
32 | spec:
33 | selector:
34 | app: logger-service
35 | ports:
36 | - protocol: TCP
37 | port: 80
38 | name: web-port
39 | targetPort: 80
40 | - protocol: TCP
41 | port: 5001
42 | name: rpc-port
43 | targetPort: 5001
44 | - protocol:
45 | port: 50001
46 | name: grpc-port
47 | targetPort: 50001
--------------------------------------------------------------------------------
/k8s/mail.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: mail-service
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: mail-service
10 | template:
11 | metadata:
12 | labels:
13 | app: mail-service
14 | spec:
15 | containers:
16 | - name: mail-service
17 | image: "tsawler/mail-service:1.0.0"
18 | env:
19 | - name: MAIL_DOMAIN
20 | value: "localhost"
21 | - name: MAIL_HOST
22 | value: "mailhog"
23 | - name: MAIL_PORT
24 | value: "1025"
25 | - name: MAIL_ENCRYPTION
26 | value: "localhost"
27 | - name: MAIL_USERNAME
28 | value: ""
29 | - name: MAIL_PASSWORD
30 | value: ""
31 | - name: FROM_NAME
32 | value: "John Smith"
33 | - name: FROM_ADDRESS
34 | value: "john.smith@example.com"
35 | ports:
36 | - containerPort: 80
37 |
38 | ---
39 |
40 | apiVersion: v1
41 | kind: Service
42 | metadata:
43 | name: mail-service
44 | spec:
45 | selector:
46 | app: mail-service
47 | ports:
48 | - protocol: TCP
49 | port: 80
50 | targetPort: 80
--------------------------------------------------------------------------------
/k8s/mailhog.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: mailhog
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: mailhog
10 | template:
11 | metadata:
12 | labels:
13 | app: mailhog
14 | spec:
15 | containers:
16 | - name: mailhog
17 | image: "mailhog/mailhog:latest"
18 | ports:
19 | - containerPort: 1025
20 | - containerPort: 8025
21 |
22 | ---
23 |
24 | apiVersion: v1
25 | kind: Service
26 | metadata:
27 | name: mailhog
28 | spec:
29 | selector:
30 | app: mailhog
31 | ports:
32 | - protocol: TCP
33 | name: smtp-port
34 | port: 1025
35 | targetPort: 1025
36 | - protocol: TCP
37 | name: web-port
38 | port: 8025
39 | targetPort: 8025
--------------------------------------------------------------------------------
/k8s/mongo.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: mongo
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: mongo
10 | template:
11 | metadata:
12 | labels:
13 | app: mongo
14 | spec:
15 | containers:
16 | - name: mongo
17 | image: "mongo:4.2.17-bionic"
18 | env:
19 | - name: MONGO_INITDB_DATABASE
20 | value: "logs"
21 | - name: MONGO_INITDB_ROOT_USERNAME
22 | value: "admin"
23 | - name: MONGO_INITDB_ROOT_PASSWORD
24 | value: "password"
25 | ports:
26 | - containerPort: 27017
27 |
28 | ---
29 |
30 | apiVersion: v1
31 | kind: Service
32 | metadata:
33 | name: mongo
34 | spec:
35 | selector:
36 | app: mongo
37 | ports:
38 | - protocol: TCP
39 | name: main-port
40 | port: 27017
41 | targetPort: 27017
42 |
--------------------------------------------------------------------------------
/k8s/rabbitmq.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: rabbitmq
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: rabbitmq
10 | template:
11 | metadata:
12 | labels:
13 | app: rabbitmq
14 | spec:
15 | containers:
16 | - name: rabbitmq
17 | image: "rabbitmq:3.9-alpine"
18 | ports:
19 | - containerPort: 5672
20 |
21 | ---
22 |
23 | apiVersion: v1
24 | kind: Service
25 | metadata:
26 | name: rabbitmq
27 | spec:
28 | selector:
29 | app: rabbitmq
30 | ports:
31 | - protocol: TCP
32 | port: 5672
33 | targetPort: 5672
34 |
--------------------------------------------------------------------------------
/listener-service.dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 | RUN mkdir /app
3 |
4 | COPY listener-service/listener /app
5 |
6 | # Run the server executable
7 | CMD [ "/app/listener" ]
--------------------------------------------------------------------------------
/listener-service/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/tsawler/go-rabbit
2 |
3 | go 1.18
4 |
5 | require github.com/rabbitmq/amqp091-go v1.7.0
6 |
--------------------------------------------------------------------------------
/listener-service/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
4 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
5 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7 | github.com/rabbitmq/amqp091-go v1.3.0 h1:A/QuHiNw7LMCJsxx9iZn5lrIz6OrhIn7Dfk5/1YatWM=
8 | github.com/rabbitmq/amqp091-go v1.3.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM=
9 | github.com/rabbitmq/amqp091-go v1.3.2 h1:zezKg1S58+q/9Ej7DIqFL6TP6NGMyGPb4ykEm4n94cY=
10 | github.com/rabbitmq/amqp091-go v1.3.2/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM=
11 | github.com/rabbitmq/amqp091-go v1.7.0 h1:V5CF5qPem5OGSnEo8BoSbsDGwejg6VUJsKEdneaoTUo=
12 | github.com/rabbitmq/amqp091-go v1.7.0/go.mod h1:wfClAtY0C7bOHxd3GjmF26jEHn+rR/0B3+YV+Vn9/NI=
13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
14 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
15 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
16 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
17 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
18 | go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
19 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
20 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
21 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
22 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
23 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
24 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
25 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
26 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
27 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
28 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
29 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
30 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
31 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
32 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
33 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
35 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
36 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
37 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
38 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
39 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
40 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
41 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
42 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
43 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
45 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
46 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
47 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
48 |
--------------------------------------------------------------------------------
/listener-service/lib/event/consumer.go:
--------------------------------------------------------------------------------
1 | package event
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "net/rpc"
10 |
11 | amqp "github.com/rabbitmq/amqp091-go"
12 | )
13 |
14 | // Consumer is the type used for receiving AMPQ events
15 | type Consumer struct {
16 | conn *amqp.Connection
17 | queueName string
18 | }
19 |
20 | // NewConsumer returns a new Consumer
21 | func NewConsumer(conn *amqp.Connection) (Consumer, error) {
22 | consumer := Consumer{
23 | conn: conn,
24 | }
25 | err := consumer.setup()
26 | if err != nil {
27 | return Consumer{}, err
28 | }
29 |
30 | return consumer, nil
31 | }
32 |
33 | // setup opens a channel and declares the exchange
34 | func (consumer *Consumer) setup() error {
35 | channel, err := consumer.conn.Channel()
36 | if err != nil {
37 | return err
38 | }
39 | return declareExchange(channel)
40 | }
41 |
42 | // Payload is the type used for pushing events to RabbitMQ
43 | type Payload struct {
44 | Name string `json:"name"`
45 | Data string `json:"data"`
46 | }
47 |
48 | // Listen will listen for all new queue publications
49 | func (consumer *Consumer) Listen(topics []string) error {
50 | ch, err := consumer.conn.Channel()
51 | if err != nil {
52 | return err
53 | }
54 | defer ch.Close()
55 |
56 | q, err := declareRandomQueue(ch)
57 | if err != nil {
58 | return err
59 | }
60 |
61 | for _, s := range topics {
62 | err = ch.QueueBind(
63 | q.Name,
64 | s,
65 | getExchangeName(),
66 | false,
67 | nil,
68 | )
69 |
70 | if err != nil {
71 | log.Println(err)
72 | return err
73 | }
74 | }
75 |
76 | messages, err := ch.Consume(q.Name, "", true, false, false, false, nil)
77 | if err != nil {
78 | return err
79 | }
80 |
81 | forever := make(chan bool)
82 | go func() {
83 | for d := range messages {
84 | // get the JSON payload and unmarshal it into a variable
85 | var payload Payload
86 | _ = json.Unmarshal(d.Body, &payload)
87 |
88 | // do something with the payload
89 | go handlePayload(payload)
90 | }
91 | }()
92 |
93 | log.Printf("[*] Waiting for message [Exchange, Queue][%s, %s].", getExchangeName(), q.Name)
94 | <-forever
95 | return nil
96 | }
97 |
98 | // handlePayload takes an action based on the name of an event in the queue
99 | func handlePayload(payload Payload) {
100 | // logic to process payload goes in here
101 | switch payload.Name {
102 | case "broker_hit":
103 | // just a test to make sure everything works
104 | res, err := rpcPushToLogger("LogInfo", payload)
105 | if err != nil {
106 | log.Println(err)
107 | }
108 | fmt.Println("Response from RPC:", res)
109 |
110 | case "auth", "authentication":
111 | // we are trying to authenticate someone
112 | err := authenticate(payload)
113 | if err != nil {
114 | log.Println(err)
115 | }
116 |
117 | // you can have as many cases as you want here, but naturally you'll have to write the logic
118 | // to connect to a given microservice
119 |
120 | default:
121 | // log whatever we get
122 | res, err := rpcPushToLogger("LogInfo", payload)
123 | if err != nil {
124 | log.Println(err)
125 | }
126 | fmt.Println("Response from RPC:", res)
127 | }
128 | }
129 |
130 | // rpcPushToLogger pushes data to the logger-service via RPC, where
131 | // it gets stored into a mongo database
132 | func rpcPushToLogger(function string, data any) (string, error) {
133 | c, err := rpc.Dial("tcp", "logger-service:5001")
134 | if err != nil {
135 | log.Println(err)
136 | return "", err
137 | }
138 |
139 | fmt.Println("Connected via rpc...")
140 | var result string
141 | err = c.Call("RPCServer."+function, data, &result)
142 | if err != nil {
143 | return "", err
144 | }
145 |
146 | return result, nil
147 | }
148 |
149 | // authenticate is a stub that we'll never actually use, but it is here
150 | // as we get used to how to interact with services
151 | func authenticate(payload Payload) error {
152 | // TODO actually authenticate via JSON
153 | log.Printf("Got payload of %v", payload)
154 | return nil
155 | }
156 |
157 | func logEvent(entry Payload) error {
158 | jsonData, _ := json.MarshalIndent(entry, "", "\t")
159 |
160 | logServiceURL := "http://logger-service/log"
161 |
162 | request, err := http.NewRequest("POST", logServiceURL, bytes.NewBuffer(jsonData))
163 | if err != nil {
164 | return err
165 | }
166 |
167 | request.Header.Set("Content-Type", "application/json")
168 | client := &http.Client{}
169 |
170 | response, err := client.Do(request)
171 | if err != nil {
172 | return err
173 | }
174 | defer response.Body.Close()
175 |
176 | if response.StatusCode != http.StatusAccepted {
177 | return err
178 | }
179 |
180 | return nil
181 | }
182 |
--------------------------------------------------------------------------------
/listener-service/lib/event/event.go:
--------------------------------------------------------------------------------
1 | package event
2 |
3 | import (
4 | amqp "github.com/rabbitmq/amqp091-go"
5 | )
6 |
7 | func getExchangeName() string {
8 | return "logs_topic"
9 | }
10 |
11 | // declareRandomQueue just creates a random queue
12 | func declareRandomQueue(ch *amqp.Channel) (amqp.Queue, error) {
13 | return ch.QueueDeclare(
14 | "", // name
15 | false, // durable
16 | false, // delete when unused
17 | true, // exclusive
18 | false, // no-wait
19 | nil, // arguments
20 | )
21 | }
22 |
23 | // declareExchange creates an exchange, with a name
24 | func declareExchange(ch *amqp.Channel) error {
25 | return ch.ExchangeDeclare(
26 | getExchangeName(), // name
27 | "topic", // type
28 | true, // durable
29 | false, // auto-deleted
30 | false, // internal
31 | false, // no-wait
32 | nil, // arguments
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/listener-service/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | amqp "github.com/rabbitmq/amqp091-go"
6 | "github.com/tsawler/go-rabbit/lib/event"
7 | "log"
8 | "math"
9 | "os"
10 | "time"
11 | )
12 |
13 | func main() {
14 | // try to connect to RabbitMQ
15 | rabbitConn, err := connect()
16 | if err != nil {
17 | log.Println(err)
18 | os.Exit(1)
19 | }
20 | defer rabbitConn.Close()
21 |
22 | // start listening for messages
23 | log.Println("Listening for and consuming RabbitMQ messages...")
24 |
25 | // create a new consumer
26 | consumer, err := event.NewConsumer(rabbitConn)
27 | if err != nil {
28 | panic(err)
29 | }
30 |
31 | // consumer.Listen watches the queue and consumes events for all the provided topics.
32 | err = consumer.Listen([]string{"log.INFO", "log.WARNING", "log.ERROR"})
33 | if err != nil {
34 | log.Println(err)
35 | }
36 | }
37 |
38 | // connect tries to connect to RabbitMQ, and delays between attempts.
39 | // If we can't connect after 5 tries (with increasing delays), return an error
40 | func connect() (*amqp.Connection, error) {
41 | var counts int64
42 | var backOff = 1 * time.Second
43 | var connection *amqp.Connection
44 | var rabbitURL = os.Getenv("RABBIT_URL")
45 |
46 | // don't continue until rabbitmq is ready
47 | for {
48 | c, err := amqp.Dial(rabbitURL)
49 | if err != nil {
50 | fmt.Println("RabbitMQ not yet ready...")
51 | counts++
52 | } else {
53 | // we have a connection to rabbitmq, so set connection = c and break out of
54 | // this loop
55 | connection = c
56 | fmt.Println()
57 | break
58 | }
59 |
60 | if counts > 5 {
61 | // if we can't connect after five tries, something is wrong...
62 | fmt.Println(err)
63 | return nil, err
64 | }
65 | fmt.Printf("Backing off for %d seconds...\n", int(math.Pow(float64(counts), 2)))
66 | backOff = time.Duration(math.Pow(float64(counts), 2)) * time.Second
67 | time.Sleep(backOff)
68 | continue
69 | }
70 | return connection, nil
71 | }
72 |
--------------------------------------------------------------------------------
/logger-service.dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 | RUN mkdir /app
3 | RUN mkdir /templates
4 |
5 | COPY logger-service/logServiceApp /app
6 | COPY logger-service/templates/. /templates
7 |
8 | # Run the server executable
9 | CMD [ "/app/logServiceApp" ]
--------------------------------------------------------------------------------
/logger-service/Makefile:
--------------------------------------------------------------------------------
1 | gen:
2 | protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative logs/logs.proto
3 |
--------------------------------------------------------------------------------
/logger-service/cmd/web/discover.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | //// registerService registers the correct entry for this service in etcd
4 | //func (app *Config) registerService() {
5 | // // get a connection to etcd
6 | // cli, _ := connectToEtcd()
7 | // kv := clientv3.NewKV(cli)
8 | //
9 | // app.Etcd = cli
10 | //
11 | // // create a lease that lasts 10 seconds
12 | // lease := clientv3.NewLease(cli)
13 | // grantResp, err := lease.Grant(context.TODO(), 10)
14 | // if err != nil {
15 | // log.Println("Error creating lease", err)
16 | // }
17 | //
18 | // // insert something with the lease
19 | // _, err = kv.Put(context.TODO(), fmt.Sprintf("/logger/%s", app.randomString(32)), "logger-service", clientv3.WithLease(grantResp.ID))
20 | // if err != nil {
21 | // log.Println("Error inserting using lease", err)
22 | // }
23 | //
24 | // // keep lease alive
25 | // kalRes, err := lease.KeepAlive(context.TODO(), grantResp.ID)
26 | // if err != nil {
27 | // log.Println("Error with keepalive", err)
28 | // }
29 | // go app.listenToKeepAlive(kalRes)
30 | //}
31 | //
32 | //// listenToKeepAlive consumes the responses from etcd, or the lease will not work as expected.
33 | //// We don't have to do anything with them, but we have to consume them.
34 | //func (app *Config) listenToKeepAlive(kalRes <-chan *clientv3.LeaseKeepAliveResponse) {
35 | // defer func() {
36 | // if r := recover(); r != nil {
37 | // log.Println("Error", fmt.Sprintf("%v", r))
38 | // }
39 | // }()
40 | //
41 | // for {
42 | // _ = <-kalRes
43 | // }
44 | //}
45 | //
46 | //// connectToEtcd tries to connect to etcd, for up to 30 seconds
47 | //func connectToEtcd() (*clientv3.Client, error) {
48 | // var cli *clientv3.Client
49 | // var counts = 0
50 | //
51 | // for {
52 | // c, err := clientv3.New(clientv3.Config{Endpoints: []string{"etcd:2379"},
53 | // DialTimeout: 5 * time.Second,
54 | // })
55 | // if err != nil {
56 | // fmt.Println("etcd not ready...")
57 | // counts++
58 | // } else {
59 | // fmt.Println()
60 | // cli = c
61 | // break
62 | // }
63 | //
64 | // if counts > 15 {
65 | // return nil, err
66 | // }
67 | // fmt.Println("Backing off for 2 seconds...")
68 | // time.Sleep(2 * time.Second)
69 | // continue
70 | // }
71 | // log.Println("Connected to etcd!")
72 | // return cli, nil
73 | //}
74 |
--------------------------------------------------------------------------------
/logger-service/cmd/web/grpc.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "google.golang.org/grpc"
7 | "log"
8 | "log-service/data"
9 | "log-service/logs"
10 | "net"
11 | )
12 |
13 | // LogServer is type used for writing to the log via gRPC. Note that we embed the
14 | // data.Models type, so we have access to Mongo.
15 | type LogServer struct {
16 | logs.UnimplementedLogServiceServer
17 | Models data.Models
18 | }
19 |
20 | // WriteLog writes the log after receiving a call from a gRPC client. This function
21 | // must exist, and is defined in logs/logs.proto, in the "service LogService" bit
22 | // at the end of the file.
23 | func (l *LogServer) WriteLog(ctx context.Context, req *logs.LogRequest) (*logs.LogResponse, error) {
24 | input := req.GetLogEntry()
25 | log.Println("Log:", input.Name, input.Data)
26 |
27 | // write the log
28 | logEntry := data.LogEntry{
29 | Name: input.Name,
30 | Data: input.Data,
31 | }
32 |
33 | err := l.Models.LogEntry.Insert(logEntry)
34 | if err != nil {
35 | res := &logs.LogResponse{Result: "failed:"}
36 | return res, err
37 | }
38 |
39 | // return response
40 | res := &logs.LogResponse{Result: "logged!"}
41 |
42 | return res, nil
43 | }
44 |
45 | // gRPCListen starts the gRPC server
46 | func (app *Config) gRPCListen() {
47 | lis, err := net.Listen("tcp", fmt.Sprintf(":%s", gRpcPort))
48 | if err != nil {
49 | log.Fatalf("Failed to listen: %v", err)
50 | }
51 |
52 | s := grpc.NewServer()
53 |
54 | // register the service, handing it models (so we can write to the database)
55 | logs.RegisterLogServiceServer(s, &LogServer{Models: app.Models})
56 |
57 | log.Printf("gRPC server started at port %s", gRpcPort)
58 |
59 | if err := s.Serve(lis); err != nil {
60 | log.Fatalf("Failed to serve: %v", err)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/logger-service/cmd/web/handlers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "github.com/go-chi/chi/v5"
8 | "io"
9 | "log"
10 | "net/http"
11 | "time"
12 | )
13 |
14 | // authServiceURL is the url to the authentication service. Since we're using
15 | // Docker, we specify the appropriate entry from docker-compose.yml
16 | const authServiceURL = "http://authentication-service/authenticate"
17 |
18 | // JSONPayload is the type for JSON posted to this API
19 | type JSONPayload struct {
20 | Name string `json:"name"`
21 | Data string `json:"data"`
22 | }
23 |
24 | // WriteLog is the handler to accept a post request consisting of json payload,
25 | // and then write it to Mongo
26 | func (app *Config) WriteLog(w http.ResponseWriter, r *http.Request) {
27 | // read json into var
28 | var requestPayload JSONPayload
29 | _ = app.readJSON(w, r, &requestPayload)
30 |
31 | // insert the data
32 | err := app.logEvent(requestPayload.Name, requestPayload.Data)
33 | if err != nil {
34 | log.Println(err)
35 | _ = app.errorJSON(w, err, http.StatusBadRequest)
36 | return
37 | }
38 |
39 | // create the response we'll send back as JSON
40 | resp := jsonResponse{
41 | Error: false,
42 | Message: "logged",
43 | }
44 |
45 | // write the response back as JSON
46 | _ = app.writeJSON(w, http.StatusAccepted, resp)
47 | }
48 |
49 | // Logout logs the user out and redirects them to the login page
50 | func (app *Config) Logout(w http.ResponseWriter, r *http.Request) {
51 | // log the event
52 | _ = app.logEvent("authentication", fmt.Sprintf("%s logged out of the logger service", app.Session.GetString(r.Context(), "email")))
53 |
54 | // clean up session
55 | _ = app.Session.Destroy(r.Context())
56 | _ = app.Session.RenewToken(r.Context())
57 |
58 | // redirect to login page
59 | http.Redirect(w, r, "/login", http.StatusSeeOther)
60 | }
61 |
62 | // LoginPage displays the login page
63 | func (app *Config) LoginPage(w http.ResponseWriter, r *http.Request) {
64 | app.render(w, r, "login.page.gohtml", nil)
65 | }
66 |
67 | // LoginPagePost handles user login. Note that it calls the authentication microservice
68 | func (app *Config) LoginPagePost(w http.ResponseWriter, r *http.Request) {
69 | // it's always good to regenerate the session on login/logout
70 | _ = app.Session.RenewToken(r.Context())
71 |
72 | // parse the posted form data
73 | err := r.ParseForm()
74 | if err != nil {
75 | log.Println(err)
76 | }
77 |
78 | // get email and password from form post
79 | email := r.Form.Get("email")
80 | password := r.Form.Get("password")
81 |
82 | // create a variable we'll post to the auth service as JSON
83 | var requestPayload struct {
84 | Email string `json:"email"`
85 | Password string `json:"password"`
86 | }
87 | requestPayload.Email = email
88 | requestPayload.Password = password
89 |
90 | // create json we'll send to the authentication-service
91 | jsonData, _ := json.MarshalIndent(requestPayload, "", "\t")
92 |
93 | // call the authentication-service; we need a request, so let's build one, and populate
94 | // its body with the jsonData we just created
95 | request, err := http.NewRequest("POST", authServiceURL, bytes.NewBuffer(jsonData))
96 | request.Header.Set("Content-Type", "application/json")
97 |
98 | c := &http.Client{}
99 | response, err := c.Do(request)
100 | if err != nil {
101 | log.Println(err)
102 | _ = app.errorJSON(w, err, http.StatusBadRequest)
103 | return
104 | }
105 | defer response.Body.Close()
106 |
107 | // make sure we get back the right status code
108 | if response.StatusCode == http.StatusUnauthorized {
109 | log.Println("wrong status code", response.StatusCode)
110 | http.Redirect(w, r, "/login", http.StatusSeeOther)
111 | return
112 | } else if response.StatusCode != http.StatusAccepted {
113 | log.Println("did not get status accepted")
114 | http.Redirect(w, r, "/login", http.StatusSeeOther)
115 | return
116 | }
117 |
118 | // Read the body of the response
119 | body, err := io.ReadAll(response.Body)
120 | if err != nil {
121 | app.clientError(w, http.StatusBadRequest)
122 | return
123 | }
124 |
125 | // define a type that matches the JSON we're getting from the response body
126 | type userPayload struct {
127 | Error bool `json:"error"`
128 | Message string `json:"message"`
129 | Data struct {
130 | ID int `json:"id"`
131 | Email string `json:"email"`
132 | FirstName string `json:"first_name"`
133 | LastName string `json:"last_name"`
134 | Active int `json:"active"`
135 | CreatedAt time.Time `json:"created_at"`
136 | UpdatedAt time.Time `json:"updated_at"`
137 | } `json:"data"`
138 | }
139 |
140 | // declare a variable we can unmarshal the JSON into
141 | var user userPayload
142 |
143 | // since we received the request from a remote host with http.Client,
144 | // we need to build a new request with the body we received and pass it to
145 | // app.readJSON
146 | req, _ := http.NewRequest("POST", "/", bytes.NewReader(body))
147 |
148 | // read the JSON
149 | err = app.readJSON(w, req, &user)
150 | if err != nil {
151 | log.Println(err)
152 | app.clientError(w, http.StatusBadRequest)
153 | return
154 | }
155 |
156 | // log the event
157 | _ = app.logEvent("authentication", fmt.Sprintf("%s logged into the logger service", user.Data.Email))
158 |
159 | // set up session & log user in
160 | app.Session.Put(r.Context(), "userID", user.Data.ID)
161 | app.Session.Put(r.Context(), "email", user.Data.Email)
162 |
163 | http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
164 | }
165 |
166 | // Dashboard displays the dashboard page
167 | func (app *Config) Dashboard(w http.ResponseWriter, r *http.Request) {
168 | // get the list of all log entries from mongo
169 | logs, err := app.Models.LogEntry.All()
170 | if err != nil {
171 | log.Println("Error getting all log entries")
172 | app.clientError(w, http.StatusBadRequest)
173 | }
174 |
175 | templateData := make(map[string]any)
176 | templateData["logs"] = logs
177 |
178 | app.render(w, r, "dashboard.page.gohtml", &TemplateData{
179 | Data: templateData,
180 | })
181 | }
182 |
183 | // DisplayOne is the handler to display a single log entry
184 | func (app *Config) DisplayOne(w http.ResponseWriter, r *http.Request) {
185 | id := chi.URLParam(r, "id")
186 |
187 | entry, err := app.Models.LogEntry.GetOne(id)
188 | if err != nil {
189 | app.clientError(w, http.StatusBadRequest)
190 | return
191 | }
192 |
193 | templateData := make(map[string]any)
194 | templateData["entry"] = entry
195 |
196 | app.render(w, r, "entry.page.gohtml", &TemplateData{
197 | Data: templateData,
198 | })
199 | }
200 |
201 | // DeleteAll drops everything in the collection and redirects to the same page
202 | func (app *Config) DeleteAll(w http.ResponseWriter, r *http.Request) {
203 | err := app.Models.LogEntry.DropCollection()
204 | if err != nil {
205 | log.Println(err)
206 | }
207 |
208 | http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
209 | }
210 |
211 | // UpdateTimeStamp just demos how to update a document
212 | func (app *Config) UpdateTimeStamp(w http.ResponseWriter, r *http.Request) {
213 | id := chi.URLParam(r, "id")
214 |
215 | entry, err := app.Models.LogEntry.GetOne(id)
216 | if err != nil {
217 | log.Println("Error getting record:", err)
218 | app.clientError(w, http.StatusBadRequest)
219 | return
220 | }
221 |
222 | res, err := entry.Update()
223 | if err != nil {
224 | log.Println("Error updating", err)
225 | app.clientError(w, http.StatusBadRequest)
226 | return
227 | }
228 |
229 | log.Println("Result in handler:", res.ModifiedCount)
230 |
231 | http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
232 | }
233 |
--------------------------------------------------------------------------------
/logger-service/cmd/web/helpers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "log"
10 | "log-service/data"
11 | "net/http"
12 | "runtime/debug"
13 | "time"
14 | )
15 |
16 | const randomStringSource = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0987654321_+"
17 |
18 | type jsonResponse struct {
19 | Error bool `json:"error"`
20 | Message string `json:"message"`
21 | Data any `json:"data,omitempty"`
22 | }
23 |
24 | // readJSON tries to read the body of a request and converts it into JSON
25 | func (app *Config) readJSON(w http.ResponseWriter, r *http.Request, data any) error {
26 | maxBytes := 1048576 // one megabyte
27 | r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
28 |
29 | dec := json.NewDecoder(r.Body)
30 | err := dec.Decode(data)
31 | if err != nil {
32 | return err
33 | }
34 |
35 | err = dec.Decode(&struct{}{})
36 | if err != io.EOF {
37 | return errors.New("body must have only a single json value")
38 | }
39 |
40 | return nil
41 | }
42 |
43 | // writeJSON takes a response status code and arbitrary data and writes a json response to the client
44 | func (app *Config) writeJSON(w http.ResponseWriter, status int, data any, headers ...http.Header) error {
45 | out, err := json.Marshal(data)
46 | if err != nil {
47 | return err
48 | }
49 |
50 | if len(headers) > 0 {
51 | for key, value := range headers[0] {
52 | w.Header()[key] = value
53 | }
54 | }
55 |
56 | w.Header().Set("Content-Type", "application/json")
57 | w.WriteHeader(status)
58 | _, err = w.Write(out)
59 | if err != nil {
60 | return err
61 | }
62 |
63 | return nil
64 | }
65 |
66 | // errorJSON takes an error, and optionally a response status code, and generates and sends
67 | // a json error response
68 | func (app *Config) errorJSON(w http.ResponseWriter, err error, status ...int) error {
69 | statusCode := http.StatusBadRequest
70 |
71 | if len(status) > 0 {
72 | statusCode = status[0]
73 | }
74 |
75 | var payload jsonResponse
76 | payload.Error = true
77 | payload.Message = err.Error()
78 |
79 | return app.writeJSON(w, statusCode, payload)
80 | }
81 |
82 | // isAuthenticated checks to see if a user is authenticated by looking for userID in session
83 | func (app *Config) isAuthenticated(r *http.Request) bool {
84 | exists := app.Session.Exists(r.Context(), "userID")
85 | return exists
86 | }
87 |
88 | // clientError just fires back a client error when something goes wrong (bad request)
89 | func (app *Config) clientError(w http.ResponseWriter, status int) {
90 | log.Println("Client error with status of", status)
91 | http.Error(w, http.StatusText(status), status)
92 | }
93 |
94 | // serverError just fires back error 500 when something goes wrong
95 | func (app *Config) serverError(w http.ResponseWriter, err error) {
96 | trace := fmt.Sprintf("%s\n%s", err.Error(), debug.Stack())
97 | log.Println(trace)
98 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
99 | }
100 |
101 | // logEvent saves an event to the logs collection in Mongo
102 | func (app *Config) logEvent(name, content string) error {
103 | event := data.LogEntry{
104 | Name: name,
105 | Data: content,
106 | CreatedAt: time.Now(),
107 | }
108 | return app.Models.LogEntry.Insert(event)
109 | }
110 |
111 | // randomString returns a random string of letters of length n
112 | func (app *Config) randomString(n int) string {
113 | s, r := make([]rune, n), []rune(randomStringSource)
114 | for i := range s {
115 | p, _ := rand.Prime(rand.Reader, len(r))
116 | x, y := p.Uint64(), uint64(len(r))
117 | s[i] = r[x%y]
118 | }
119 | return string(s)
120 | }
121 |
--------------------------------------------------------------------------------
/logger-service/cmd/web/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/alexedwards/scs/v2"
7 | clientv3 "go.etcd.io/etcd/client/v3"
8 | "go.mongodb.org/mongo-driver/mongo"
9 | "go.mongodb.org/mongo-driver/mongo/options"
10 | "log"
11 | "log-service/data"
12 | "net"
13 | "net/http"
14 | "net/rpc"
15 | "time"
16 | )
17 |
18 | var client *mongo.Client
19 |
20 | const (
21 | webPort = "80"
22 | rpcPort = "5001"
23 | mongoURL = "mongodb://mongo:27017"
24 | gRpcPort = "50001"
25 | )
26 |
27 | type Config struct {
28 | Session *scs.SessionManager
29 | Models data.Models
30 | Etcd *clientv3.Client
31 | }
32 |
33 | func main() {
34 | // Connect to Mongo and get a client.
35 | mongoClient, err := connectToMongo()
36 | client = mongoClient
37 |
38 | // We'll use this context to disconnect from mongo, since it needs one.
39 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
40 | defer cancel()
41 |
42 | // close connection to Mongo when application exits
43 | defer func() {
44 | if err = client.Disconnect(ctx); err != nil {
45 | panic(err)
46 | }
47 | }()
48 |
49 | // Set up application configuration with session and our Models type,
50 | // which allows us to interact with Mongo.
51 | session := scs.New()
52 | session.Lifetime = 24 * time.Hour
53 | session.Cookie.Persist = true
54 | session.Cookie.SameSite = http.SameSiteLaxMode
55 | session.Cookie.Secure = false
56 |
57 | app := Config{
58 | Session: session,
59 | Models: data.New(client),
60 | }
61 |
62 | // connect to etcd and register service
63 | //app.registerService()
64 | //defer app.Etcd.Close()
65 |
66 | // Start webserver in its own GoRoutine
67 | go app.serve()
68 |
69 | // Start the gRPC server in its own GoRoutine
70 | go app.gRPCListen()
71 |
72 | // Register the RPC server.
73 | err = rpc.Register(new(RPCServer))
74 | if err != nil {
75 | return
76 | }
77 |
78 | // Listen for RPC connections on port rpcPort
79 | log.Println("Starting RPC Server on port", rpcPort)
80 | listen, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%s", rpcPort))
81 | if err != nil {
82 | fmt.Println(err)
83 | return
84 | }
85 | defer listen.Close()
86 |
87 | // this loop executes forever, waiting for connections
88 | for {
89 | rpcConn, err := listen.Accept()
90 | if err != nil {
91 | continue
92 | }
93 | log.Println("Working...")
94 | go rpc.ServeConn(rpcConn)
95 | }
96 | }
97 |
98 | // serve starts the web server.
99 | func (app *Config) serve() {
100 | srv := &http.Server{
101 | Addr: fmt.Sprintf(":%s", webPort),
102 | Handler: app.routes(),
103 | }
104 |
105 | fmt.Println("--------------------------------------")
106 | fmt.Println("Starting logging web service on port", webPort)
107 | err := srv.ListenAndServe()
108 | if err != nil {
109 | log.Panic(err)
110 | }
111 | }
112 |
113 | // Connect opens a connection to the Mongo database and returns a client.
114 | func connectToMongo() (*mongo.Client, error) {
115 | // create connect options
116 | clientOptions := options.Client().ApplyURI(mongoURL)
117 | clientOptions.SetAuth(options.Credential{
118 | Username: "admin",
119 | Password: "password",
120 | })
121 |
122 | // Connect to the MongoDB and return Client instance
123 | c, err := mongo.Connect(context.TODO(), clientOptions)
124 | if err != nil {
125 | fmt.Println("mongo.Connect() ERROR:", err)
126 | return nil, err
127 | }
128 |
129 | return c, nil
130 | }
131 |
--------------------------------------------------------------------------------
/logger-service/cmd/web/middleware.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "net/http"
4 |
5 | // SessionLoad loads and saves the session on every request
6 | func (app *Config) SessionLoad(next http.Handler) http.Handler {
7 | return app.Session.LoadAndSave(next)
8 | }
9 |
10 | // Auth requires that users be logged in for any route that uses this middleware
11 | func (app *Config) Auth(next http.Handler) http.Handler {
12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13 | if !app.isAuthenticated(r) {
14 | app.Session.Put(r.Context(), "error", "Log in first!")
15 | http.Redirect(w, r, "/login", http.StatusSeeOther)
16 | return
17 | }
18 | next.ServeHTTP(w, r)
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/logger-service/cmd/web/render.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "html/template"
6 | "net/http"
7 | )
8 |
9 | type TemplateData struct {
10 | Data map[string]any
11 | IsAuthenticated int
12 | }
13 |
14 | // addDefaultData adds whatever is specified in this function to all templates as
15 | // data that can be accessed directly.
16 | func (app *Config) addDefaultData(td *TemplateData, r *http.Request) *TemplateData {
17 | if app.Session.Exists(r.Context(), "userID") {
18 | td.IsAuthenticated = 1
19 | }
20 | return td
21 | }
22 |
23 | func (app *Config) render(w http.ResponseWriter, r *http.Request, t string, td *TemplateData) {
24 | // we only have one partial, which is actually a layout, but since all of our pages
25 | // require the layout, we need to include it when we call ParseFiles, below.
26 | // If you have other partials you use in your templates, add them to this slice.
27 | partials := []string{
28 | "./templates/base.layout.gohtml",
29 | }
30 |
31 | var templateSlice []string
32 | templateSlice = append(templateSlice, fmt.Sprintf("./templates/%s", t))
33 |
34 | for _, x := range partials {
35 | templateSlice = append(templateSlice, x)
36 | }
37 |
38 | tmpl, err := template.ParseFiles(templateSlice...)
39 | if err != nil {
40 | http.Error(w, err.Error(), http.StatusInternalServerError)
41 | return
42 | }
43 | if td == nil {
44 | td = &TemplateData{}
45 | }
46 | td = app.addDefaultData(td, r)
47 |
48 | if err := tmpl.Execute(w, td); err != nil {
49 | http.Error(w, err.Error(), http.StatusInternalServerError)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/logger-service/cmd/web/routes.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/go-chi/chi/v5"
5 | "github.com/go-chi/chi/v5/middleware"
6 | "github.com/go-chi/cors"
7 | "net/http"
8 | )
9 |
10 | func (app *Config) routes() http.Handler {
11 | mux := chi.NewRouter()
12 | mux.Use(middleware.Recoverer)
13 | mux.Use(middleware.Heartbeat("/ping"))
14 |
15 | mux.Mount("/", app.webRouter())
16 | mux.Mount("/api", app.apiRouter())
17 |
18 | return mux
19 | }
20 |
21 | // apiRouter is for api routes (no session load)
22 | func (app *Config) apiRouter() http.Handler {
23 | mux := chi.NewRouter()
24 |
25 | // specify who is allowed to connect to our API service
26 | mux.Use(cors.Handler(cors.Options{
27 | AllowedOrigins: []string{"https://*", "http://*"},
28 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
29 | AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
30 | ExposedHeaders: []string{"Link"},
31 | AllowCredentials: true,
32 | MaxAge: 300,
33 | }))
34 |
35 | mux.Post("/log", app.WriteLog)
36 | return mux
37 | }
38 |
39 | // webRouter is for web routes
40 | func (app *Config) webRouter() http.Handler {
41 | mux := chi.NewRouter()
42 | mux.Use(app.SessionLoad)
43 |
44 | mux.Get("/", app.LoginPage)
45 | mux.Get("/login", app.LoginPage)
46 | mux.Post("/login", app.LoginPagePost)
47 | mux.Get("/logout", app.Logout)
48 |
49 | mux.Route("/admin", func(mux chi.Router) {
50 | mux.Use(app.Auth)
51 | mux.Get("/dashboard", app.Dashboard)
52 | mux.Get("/log-entry/{id}", app.DisplayOne)
53 | mux.Get("/delete-all", app.DeleteAll)
54 | mux.Get("/update/{id}", app.UpdateTimeStamp)
55 | })
56 |
57 | return mux
58 | }
59 |
--------------------------------------------------------------------------------
/logger-service/cmd/web/rpc.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 | "log-service/data"
7 | "time"
8 | )
9 |
10 | // RPCServer is the type for our RPC Server. Methods that take this as a receiver are available
11 | // over RPC, as long as they are exported.
12 | type RPCServer struct{}
13 |
14 | // RPCPayload is the type for data we receive from RPC
15 | type RPCPayload struct {
16 | Name string
17 | Data string
18 | }
19 |
20 | // LogInfo writes our payload to mongo
21 | func (r *RPCServer) LogInfo(payload RPCPayload, resp *string) error {
22 | collection := client.Database("logs").Collection("logs")
23 | _, err := collection.InsertOne(context.TODO(), data.LogEntry{
24 | Name: payload.Name,
25 | Data: payload.Data,
26 | CreatedAt: time.Now(),
27 | })
28 | if err != nil {
29 | log.Println("Error writing log in rpc.go, LogInfo", err)
30 | return err
31 | }
32 |
33 | // resp is the message sent back to the RPC caller
34 | *resp = "Processed payload via RPC: " + payload.Name
35 | return nil
36 | }
37 |
--------------------------------------------------------------------------------
/logger-service/data/models.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import (
4 | "context"
5 | "go.mongodb.org/mongo-driver/bson"
6 | "go.mongodb.org/mongo-driver/bson/primitive"
7 | "go.mongodb.org/mongo-driver/mongo"
8 | "go.mongodb.org/mongo-driver/mongo/options"
9 | "log"
10 | "time"
11 | )
12 |
13 | // client is our mongo client that allows us to perform operations on the Mongo database.
14 | var client *mongo.Client
15 |
16 | // New is the function used to create an instance of the data package. It returns the type
17 | // Model, which embeds all the types we want to be available to our application.
18 | func New(mongo *mongo.Client) Models {
19 | client = mongo
20 |
21 | return Models{
22 | LogEntry: LogEntry{},
23 | }
24 | }
25 |
26 | // Models is the type for this package. Note that any model that is included as a member
27 | // in this type is available to us throughout the application, anywhere that the
28 | // app variable is used, provided that the model is also added in the New function.
29 | type Models struct {
30 | LogEntry LogEntry
31 | }
32 |
33 | // LogEntry is the type for all data stored in the logs collection. Note that we specify
34 | // specific bson values, and we *must* include omitempty on ID, or newly inserted records will
35 | // have an empty id! We also specify JSON struct tags, even though we don't use them yet. We
36 | // might in the future.
37 | type LogEntry struct {
38 | ID string `bson:"_id,omitempty" json:"id,omitempty"`
39 | Name string `bson:"name" json:"name"`
40 | Data string `bson:"data" json:"data"`
41 | CreatedAt time.Time `bson:"created_at" json:"created_at"`
42 | UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
43 | }
44 |
45 | // Insert puts a document in the logs collection.
46 | func (l *LogEntry) Insert(entry LogEntry) error {
47 | collection := client.Database("logs").Collection("logs")
48 |
49 | _, err := collection.InsertOne(context.TODO(), LogEntry{
50 | Name: entry.Name,
51 | Data: entry.Data,
52 | CreatedAt: time.Now(),
53 | UpdatedAt: time.Now(),
54 | })
55 | if err != nil {
56 | log.Println("Error inserting log entry:", err)
57 | return err
58 | }
59 |
60 | return nil
61 | }
62 |
63 | // All returns all documents in the logs collection, by descending date/time.
64 | func (l *LogEntry) All() ([]*LogEntry, error) {
65 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
66 | defer cancel()
67 |
68 | collection := client.Database("logs").Collection("logs")
69 |
70 | opts := options.Find()
71 | opts.SetSort(bson.D{{"created_at", -1}})
72 |
73 | cursor, err := collection.Find(context.TODO(), bson.D{}, opts)
74 | if err != nil {
75 | log.Println("Finding all documents ERROR:", err)
76 | return nil, err
77 | }
78 | defer cursor.Close(ctx)
79 |
80 | var logs []*LogEntry
81 |
82 | for cursor.Next(ctx) {
83 | var item LogEntry
84 | err := cursor.Decode(&item)
85 | if err != nil {
86 | log.Println("Error scanning log into slice:", err)
87 | return nil, err
88 | } else {
89 | logs = append(logs, &item)
90 | }
91 | }
92 |
93 | return logs, nil
94 | }
95 |
96 | // GetOne returns a single document, by ID. Note that we have to convert the parameter id
97 | // which this function receives to a mongo.ObjectID, which is what Mongo actually requires in
98 | // order to call the FindOne() function.
99 | func (l *LogEntry) GetOne(id string) (*LogEntry, error) {
100 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
101 | defer cancel()
102 |
103 | collection := client.Database("logs").Collection("logs")
104 |
105 | docID, err := primitive.ObjectIDFromHex(id)
106 | if err != nil {
107 | return nil, err
108 | }
109 |
110 | var entry LogEntry
111 |
112 | err = collection.FindOne(ctx, bson.M{"_id": docID}).Decode(&entry)
113 | if err != nil {
114 | return nil, err
115 | }
116 |
117 | return &entry, nil
118 | }
119 |
120 | // DropCollection deletes the logs collection and everything in it
121 | func (l *LogEntry) DropCollection() error {
122 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
123 | defer cancel()
124 |
125 | collection := client.Database("logs").Collection("logs")
126 |
127 | if err := collection.Drop(ctx); err != nil {
128 | return err
129 | }
130 |
131 | return nil
132 | }
133 |
134 | // Update updates on record, by id
135 | func (l *LogEntry) Update() (*mongo.UpdateResult, error) {
136 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
137 | defer cancel()
138 |
139 | docID, err := primitive.ObjectIDFromHex(l.ID)
140 | if err != nil {
141 | return nil, err
142 | }
143 |
144 | log.Println("Matching", l.ID)
145 | collection := client.Database("logs").Collection("logs")
146 |
147 | result, err := collection.UpdateOne(
148 | ctx,
149 | bson.M{"_id": docID},
150 | bson.D{
151 | {"$set", bson.D{
152 | {"name", l.Name},
153 | {"data", l.Data},
154 | {"updated_at", time.Now()},
155 | }},
156 | },
157 | )
158 | log.Println("Matched:", result.MatchedCount)
159 | log.Println("Modified:", result.ModifiedCount)
160 | if err != nil {
161 | return nil, err
162 | }
163 |
164 | return result, nil
165 | }
166 |
--------------------------------------------------------------------------------
/logger-service/go.mod:
--------------------------------------------------------------------------------
1 | module log-service
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/alexedwards/scs/v2 v2.5.0
7 | github.com/go-chi/chi/v5 v5.0.8
8 | github.com/go-chi/cors v1.2.1
9 | go.etcd.io/etcd/client/v3 v3.5.7
10 | go.mongodb.org/mongo-driver v1.11.2
11 | google.golang.org/grpc v1.53.0
12 | google.golang.org/protobuf v1.28.1
13 | )
14 |
15 | require (
16 | github.com/coreos/go-semver v0.3.1 // indirect
17 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect
18 | github.com/go-stack/stack v1.8.1 // indirect
19 | github.com/gogo/protobuf v1.3.2 // indirect
20 | github.com/golang/protobuf v1.5.2 // indirect
21 | github.com/golang/snappy v0.0.4 // indirect
22 | github.com/klauspost/compress v1.16.0 // indirect
23 | github.com/montanaflynn/stats v0.7.0 // indirect
24 | github.com/pkg/errors v0.9.1 // indirect
25 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect
26 | github.com/xdg-go/scram v1.1.2 // indirect
27 | github.com/xdg-go/stringprep v1.0.4 // indirect
28 | github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
29 | go.etcd.io/etcd/api/v3 v3.5.7 // indirect
30 | go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect
31 | go.uber.org/atomic v1.10.0 // indirect
32 | go.uber.org/multierr v1.9.0 // indirect
33 | go.uber.org/zap v1.24.0 // indirect
34 | golang.org/x/crypto v0.6.0 // indirect
35 | golang.org/x/net v0.7.0 // indirect
36 | golang.org/x/sync v0.1.0 // indirect
37 | golang.org/x/sys v0.5.0 // indirect
38 | golang.org/x/text v0.7.0 // indirect
39 | google.golang.org/genproto v0.0.0-20230301171018-9ab4bdc49ad5 // indirect
40 | )
41 |
--------------------------------------------------------------------------------
/logger-service/logs/logs.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-go v1.27.1
4 | // protoc v3.19.4
5 | // source: logs.proto
6 |
7 | package logs
8 |
9 | import (
10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect"
11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl"
12 | reflect "reflect"
13 | sync "sync"
14 | )
15 |
16 | const (
17 | // Verify that this generated code is sufficiently up-to-date.
18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
19 | // Verify that runtime/protoimpl is sufficiently up-to-date.
20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
21 | )
22 |
23 | type Log struct {
24 | state protoimpl.MessageState
25 | sizeCache protoimpl.SizeCache
26 | unknownFields protoimpl.UnknownFields
27 |
28 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
29 | Data string `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
30 | }
31 |
32 | func (x *Log) Reset() {
33 | *x = Log{}
34 | if protoimpl.UnsafeEnabled {
35 | mi := &file_logs_proto_msgTypes[0]
36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
37 | ms.StoreMessageInfo(mi)
38 | }
39 | }
40 |
41 | func (x *Log) String() string {
42 | return protoimpl.X.MessageStringOf(x)
43 | }
44 |
45 | func (*Log) ProtoMessage() {}
46 |
47 | func (x *Log) ProtoReflect() protoreflect.Message {
48 | mi := &file_logs_proto_msgTypes[0]
49 | if protoimpl.UnsafeEnabled && x != nil {
50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
51 | if ms.LoadMessageInfo() == nil {
52 | ms.StoreMessageInfo(mi)
53 | }
54 | return ms
55 | }
56 | return mi.MessageOf(x)
57 | }
58 |
59 | // Deprecated: Use Log.ProtoReflect.Descriptor instead.
60 | func (*Log) Descriptor() ([]byte, []int) {
61 | return file_logs_proto_rawDescGZIP(), []int{0}
62 | }
63 |
64 | func (x *Log) GetName() string {
65 | if x != nil {
66 | return x.Name
67 | }
68 | return ""
69 | }
70 |
71 | func (x *Log) GetData() string {
72 | if x != nil {
73 | return x.Data
74 | }
75 | return ""
76 | }
77 |
78 | type LogRequest struct {
79 | state protoimpl.MessageState
80 | sizeCache protoimpl.SizeCache
81 | unknownFields protoimpl.UnknownFields
82 |
83 | LogEntry *Log `protobuf:"bytes,1,opt,name=logEntry,proto3" json:"logEntry,omitempty"`
84 | }
85 |
86 | func (x *LogRequest) Reset() {
87 | *x = LogRequest{}
88 | if protoimpl.UnsafeEnabled {
89 | mi := &file_logs_proto_msgTypes[1]
90 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
91 | ms.StoreMessageInfo(mi)
92 | }
93 | }
94 |
95 | func (x *LogRequest) String() string {
96 | return protoimpl.X.MessageStringOf(x)
97 | }
98 |
99 | func (*LogRequest) ProtoMessage() {}
100 |
101 | func (x *LogRequest) ProtoReflect() protoreflect.Message {
102 | mi := &file_logs_proto_msgTypes[1]
103 | if protoimpl.UnsafeEnabled && x != nil {
104 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
105 | if ms.LoadMessageInfo() == nil {
106 | ms.StoreMessageInfo(mi)
107 | }
108 | return ms
109 | }
110 | return mi.MessageOf(x)
111 | }
112 |
113 | // Deprecated: Use LogRequest.ProtoReflect.Descriptor instead.
114 | func (*LogRequest) Descriptor() ([]byte, []int) {
115 | return file_logs_proto_rawDescGZIP(), []int{1}
116 | }
117 |
118 | func (x *LogRequest) GetLogEntry() *Log {
119 | if x != nil {
120 | return x.LogEntry
121 | }
122 | return nil
123 | }
124 |
125 | type LogResponse struct {
126 | state protoimpl.MessageState
127 | sizeCache protoimpl.SizeCache
128 | unknownFields protoimpl.UnknownFields
129 |
130 | Result string `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"`
131 | }
132 |
133 | func (x *LogResponse) Reset() {
134 | *x = LogResponse{}
135 | if protoimpl.UnsafeEnabled {
136 | mi := &file_logs_proto_msgTypes[2]
137 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
138 | ms.StoreMessageInfo(mi)
139 | }
140 | }
141 |
142 | func (x *LogResponse) String() string {
143 | return protoimpl.X.MessageStringOf(x)
144 | }
145 |
146 | func (*LogResponse) ProtoMessage() {}
147 |
148 | func (x *LogResponse) ProtoReflect() protoreflect.Message {
149 | mi := &file_logs_proto_msgTypes[2]
150 | if protoimpl.UnsafeEnabled && x != nil {
151 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
152 | if ms.LoadMessageInfo() == nil {
153 | ms.StoreMessageInfo(mi)
154 | }
155 | return ms
156 | }
157 | return mi.MessageOf(x)
158 | }
159 |
160 | // Deprecated: Use LogResponse.ProtoReflect.Descriptor instead.
161 | func (*LogResponse) Descriptor() ([]byte, []int) {
162 | return file_logs_proto_rawDescGZIP(), []int{2}
163 | }
164 |
165 | func (x *LogResponse) GetResult() string {
166 | if x != nil {
167 | return x.Result
168 | }
169 | return ""
170 | }
171 |
172 | var File_logs_proto protoreflect.FileDescriptor
173 |
174 | var file_logs_proto_rawDesc = []byte{
175 | 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x6c, 0x6f,
176 | 0x67, 0x73, 0x22, 0x2d, 0x0a, 0x03, 0x4c, 0x6f, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
177 | 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a,
178 | 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74,
179 | 0x61, 0x22, 0x33, 0x0a, 0x0a, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
180 | 0x25, 0x0a, 0x08, 0x6c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
181 | 0x0b, 0x32, 0x09, 0x2e, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x08, 0x6c, 0x6f,
182 | 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x25, 0x0a, 0x0b, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73,
183 | 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18,
184 | 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x32, 0x3f, 0x0a,
185 | 0x0a, 0x4c, 0x6f, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x57,
186 | 0x72, 0x69, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x12, 0x10, 0x2e, 0x6c, 0x6f, 0x67, 0x73, 0x2e, 0x4c,
187 | 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x6c, 0x6f, 0x67, 0x73,
188 | 0x2e, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x07,
189 | 0x5a, 0x05, 0x2f, 0x6c, 0x6f, 0x67, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
190 | }
191 |
192 | var (
193 | file_logs_proto_rawDescOnce sync.Once
194 | file_logs_proto_rawDescData = file_logs_proto_rawDesc
195 | )
196 |
197 | func file_logs_proto_rawDescGZIP() []byte {
198 | file_logs_proto_rawDescOnce.Do(func() {
199 | file_logs_proto_rawDescData = protoimpl.X.CompressGZIP(file_logs_proto_rawDescData)
200 | })
201 | return file_logs_proto_rawDescData
202 | }
203 |
204 | var file_logs_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
205 | var file_logs_proto_goTypes = []interface{}{
206 | (*Log)(nil), // 0: logs.Log
207 | (*LogRequest)(nil), // 1: logs.LogRequest
208 | (*LogResponse)(nil), // 2: logs.LogResponse
209 | }
210 | var file_logs_proto_depIdxs = []int32{
211 | 0, // 0: logs.LogRequest.logEntry:type_name -> logs.Log
212 | 1, // 1: logs.LogService.WriteLog:input_type -> logs.LogRequest
213 | 2, // 2: logs.LogService.WriteLog:output_type -> logs.LogResponse
214 | 2, // [2:3] is the sub-list for method output_type
215 | 1, // [1:2] is the sub-list for method input_type
216 | 1, // [1:1] is the sub-list for extension type_name
217 | 1, // [1:1] is the sub-list for extension extendee
218 | 0, // [0:1] is the sub-list for field type_name
219 | }
220 |
221 | func init() { file_logs_proto_init() }
222 | func file_logs_proto_init() {
223 | if File_logs_proto != nil {
224 | return
225 | }
226 | if !protoimpl.UnsafeEnabled {
227 | file_logs_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
228 | switch v := v.(*Log); i {
229 | case 0:
230 | return &v.state
231 | case 1:
232 | return &v.sizeCache
233 | case 2:
234 | return &v.unknownFields
235 | default:
236 | return nil
237 | }
238 | }
239 | file_logs_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
240 | switch v := v.(*LogRequest); i {
241 | case 0:
242 | return &v.state
243 | case 1:
244 | return &v.sizeCache
245 | case 2:
246 | return &v.unknownFields
247 | default:
248 | return nil
249 | }
250 | }
251 | file_logs_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
252 | switch v := v.(*LogResponse); i {
253 | case 0:
254 | return &v.state
255 | case 1:
256 | return &v.sizeCache
257 | case 2:
258 | return &v.unknownFields
259 | default:
260 | return nil
261 | }
262 | }
263 | }
264 | type x struct{}
265 | out := protoimpl.TypeBuilder{
266 | File: protoimpl.DescBuilder{
267 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
268 | RawDescriptor: file_logs_proto_rawDesc,
269 | NumEnums: 0,
270 | NumMessages: 3,
271 | NumExtensions: 0,
272 | NumServices: 1,
273 | },
274 | GoTypes: file_logs_proto_goTypes,
275 | DependencyIndexes: file_logs_proto_depIdxs,
276 | MessageInfos: file_logs_proto_msgTypes,
277 | }.Build()
278 | File_logs_proto = out.File
279 | file_logs_proto_rawDesc = nil
280 | file_logs_proto_goTypes = nil
281 | file_logs_proto_depIdxs = nil
282 | }
283 |
--------------------------------------------------------------------------------
/logger-service/logs/logs.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package logs;
4 |
5 | option go_package = "/logs";
6 |
7 | message Log{
8 | string name = 1;
9 | string data =2;
10 | }
11 |
12 | message LogRequest {
13 | Log logEntry = 1;
14 | }
15 |
16 | message LogResponse{
17 | string result = 1;
18 | }
19 |
20 | service LogService{
21 | rpc WriteLog(LogRequest) returns (LogResponse) {}
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/logger-service/logs/logs_grpc.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
2 | // versions:
3 | // - protoc-gen-go-grpc v1.2.0
4 | // - protoc v3.19.4
5 | // source: logs.proto
6 |
7 | package logs
8 |
9 | import (
10 | context "context"
11 | grpc "google.golang.org/grpc"
12 | codes "google.golang.org/grpc/codes"
13 | status "google.golang.org/grpc/status"
14 | )
15 |
16 | // This is a compile-time assertion to ensure that this generated file
17 | // is compatible with the grpc package it is being compiled against.
18 | // Requires gRPC-Go v1.32.0 or later.
19 | const _ = grpc.SupportPackageIsVersion7
20 |
21 | // LogServiceClient is the client API for LogService service.
22 | //
23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
24 | type LogServiceClient interface {
25 | WriteLog(ctx context.Context, in *LogRequest, opts ...grpc.CallOption) (*LogResponse, error)
26 | }
27 |
28 | type logServiceClient struct {
29 | cc grpc.ClientConnInterface
30 | }
31 |
32 | func NewLogServiceClient(cc grpc.ClientConnInterface) LogServiceClient {
33 | return &logServiceClient{cc}
34 | }
35 |
36 | func (c *logServiceClient) WriteLog(ctx context.Context, in *LogRequest, opts ...grpc.CallOption) (*LogResponse, error) {
37 | out := new(LogResponse)
38 | err := c.cc.Invoke(ctx, "/logs.LogService/WriteLog", in, out, opts...)
39 | if err != nil {
40 | return nil, err
41 | }
42 | return out, nil
43 | }
44 |
45 | // LogServiceServer is the server API for LogService service.
46 | // All implementations must embed UnimplementedLogServiceServer
47 | // for forward compatibility
48 | type LogServiceServer interface {
49 | WriteLog(context.Context, *LogRequest) (*LogResponse, error)
50 | mustEmbedUnimplementedLogServiceServer()
51 | }
52 |
53 | // UnimplementedLogServiceServer must be embedded to have forward compatible implementations.
54 | type UnimplementedLogServiceServer struct {
55 | }
56 |
57 | func (UnimplementedLogServiceServer) WriteLog(context.Context, *LogRequest) (*LogResponse, error) {
58 | return nil, status.Errorf(codes.Unimplemented, "method WriteLog not implemented")
59 | }
60 | func (UnimplementedLogServiceServer) mustEmbedUnimplementedLogServiceServer() {}
61 |
62 | // UnsafeLogServiceServer may be embedded to opt out of forward compatibility for this service.
63 | // Use of this interface is not recommended, as added methods to LogServiceServer will
64 | // result in compilation errors.
65 | type UnsafeLogServiceServer interface {
66 | mustEmbedUnimplementedLogServiceServer()
67 | }
68 |
69 | func RegisterLogServiceServer(s grpc.ServiceRegistrar, srv LogServiceServer) {
70 | s.RegisterService(&LogService_ServiceDesc, srv)
71 | }
72 |
73 | func _LogService_WriteLog_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
74 | in := new(LogRequest)
75 | if err := dec(in); err != nil {
76 | return nil, err
77 | }
78 | if interceptor == nil {
79 | return srv.(LogServiceServer).WriteLog(ctx, in)
80 | }
81 | info := &grpc.UnaryServerInfo{
82 | Server: srv,
83 | FullMethod: "/logs.LogService/WriteLog",
84 | }
85 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
86 | return srv.(LogServiceServer).WriteLog(ctx, req.(*LogRequest))
87 | }
88 | return interceptor(ctx, in, info, handler)
89 | }
90 |
91 | // LogService_ServiceDesc is the grpc.ServiceDesc for LogService service.
92 | // It's only intended for direct use with grpc.RegisterService,
93 | // and not to be introspected or modified (even as a copy)
94 | var LogService_ServiceDesc = grpc.ServiceDesc{
95 | ServiceName: "logs.LogService",
96 | HandlerType: (*LogServiceServer)(nil),
97 | Methods: []grpc.MethodDesc{
98 | {
99 | MethodName: "WriteLog",
100 | Handler: _LogService_WriteLog_Handler,
101 | },
102 | },
103 | Streams: []grpc.StreamDesc{},
104 | Metadata: "logs.proto",
105 | }
106 |
--------------------------------------------------------------------------------
/logger-service/logs/readme.md:
--------------------------------------------------------------------------------
1 | ## gRPC stuff
2 |
3 | Install binaries:
4 |
5 | ```
6 | go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27
7 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
8 | ```
9 |
10 | Generate proto.
11 |
12 | Then, run command (inside logs directory):
13 |
14 | ```
15 | protoc --go_out=. \
16 | --go_opt=paths=source_relative \
17 | --go-grpc_out=. \
18 | --go-grpc_opt=paths=source_relative \
19 | logs.proto
20 | ```
--------------------------------------------------------------------------------
/logger-service/templates/base.layout.gohtml:
--------------------------------------------------------------------------------
1 | {{define "base" }}
2 |
3 |
4 |
5 |
6 |
8 |
9 | Logger Service
10 |
13 |
14 |
15 |
16 |
42 |
43 | {{block "content" .}}
44 |
45 | {{end}}
46 |
47 |
48 | {{block "js" .}}
49 |
50 | {{end}}
51 |
52 |
53 |
54 | {{end}}
--------------------------------------------------------------------------------
/logger-service/templates/dashboard.page.gohtml:
--------------------------------------------------------------------------------
1 | {{template "base" .}}
2 |
3 | {{define "content"}}
4 | {{$logs := index .Data "logs"}}
5 |
6 |
7 |
8 |
Dashboard
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 | ID |
21 | Name |
22 | Created |
23 | Updated |
24 | Update |
25 |
26 |
27 |
28 | {{if gt (len $logs) 0}}
29 | {{range $logs}}
30 |
31 | {{.ID}} |
32 | {{.Name}} |
33 | {{.CreatedAt.Format "2006-01-02 03:04:05"}} UTC |
34 | {{.UpdatedAt.Format "2006-01-02 03:04:05"}} UTC |
35 | Update timestamp |
36 |
37 | {{end}}
38 | {{else}}
39 |
40 | No entries |
41 |
42 | {{end}}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
57 |
58 |
Are you sure you want to delete all entries?
59 |
60 |
64 |
65 |
66 |
67 |
68 | {{end}}
69 |
70 | {{define "js"}}
71 |
76 | {{end}}
--------------------------------------------------------------------------------
/logger-service/templates/entry.page.gohtml:
--------------------------------------------------------------------------------
1 | {{template "base" .}}
2 |
3 | {{define "content"}}
4 | {{$entry := index .Data "entry"}}
5 |
6 |
7 |
8 |
Log Entry
9 |
10 |
11 |
12 | ID: {{$entry.ID}}
13 | Event: {{$entry.Name}}
14 | Date/Time: {{$entry.CreatedAt.Format "2006-01-02 03:04:05" }} UTC
15 | Details:
16 |
17 | {{$entry.Data}}
18 |
19 |
20 |
21 |
22 | {{end}}
--------------------------------------------------------------------------------
/logger-service/templates/login.page.gohtml:
--------------------------------------------------------------------------------
1 | {{template "base" .}}
2 |
3 | {{define "content"}}
4 |
24 | {{end}}
25 |
26 | {{define "js"}}
27 |
46 | {{end}}
--------------------------------------------------------------------------------
/mail-service.dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 | RUN mkdir /app
3 | RUN mkdir /templates
4 |
5 | COPY mail-service/mailerServiceApp /app
6 | COPY mail-service/templates /templates
7 |
8 | # Run the server executable
9 | CMD [ "/app/mailerServiceApp" ]
--------------------------------------------------------------------------------
/mail-service/cmd/api/discover.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | clientv3 "go.etcd.io/etcd/client/v3"
7 | "log"
8 | "time"
9 | )
10 |
11 | // registerService registers the correct entry for this service in etcd
12 | func (app *Config) registerService() {
13 | cli, _ := connectToEtcd()
14 | kv := clientv3.NewKV(cli)
15 |
16 | app.Etcd = cli
17 |
18 | lease := clientv3.NewLease(cli)
19 | grantResp, err := lease.Grant(context.TODO(), 10)
20 | if err != nil {
21 | log.Println("Error creating lease", err)
22 | }
23 |
24 | // insert something with the lease
25 | _, err = kv.Put(context.TODO(), fmt.Sprintf("/mail/%s", app.randomString(32)), "mail-service", clientv3.WithLease(grantResp.ID))
26 | if err != nil {
27 | log.Println("Error inserting using lease", err)
28 | }
29 |
30 | // keep lease alive
31 | kalRes, err := lease.KeepAlive(context.TODO(), grantResp.ID)
32 | if err != nil {
33 | log.Println("Error with keepalive", err)
34 | }
35 | go app.listenToKeepAlive(kalRes)
36 | }
37 |
38 | func (app *Config) listenToKeepAlive(kalRes <-chan *clientv3.LeaseKeepAliveResponse) {
39 | defer func() {
40 | if r := recover(); r != nil {
41 | log.Println("Error", fmt.Sprintf("%v", r))
42 | }
43 | }()
44 |
45 | for {
46 | _ = <-kalRes
47 | }
48 | }
49 |
50 | // connectToEtcd tries to connect to etcd, for up to 30 seconds
51 | func connectToEtcd() (*clientv3.Client, error) {
52 | var cli *clientv3.Client
53 | var counts = 0
54 |
55 | for {
56 | c, err := clientv3.New(clientv3.Config{Endpoints: []string{"etcd:2379"},
57 | DialTimeout: 5 * time.Second,
58 | })
59 | if err != nil {
60 | fmt.Println("etcd not ready...")
61 | counts++
62 | } else {
63 | fmt.Println()
64 | cli = c
65 | break
66 | }
67 |
68 | if counts > 15 {
69 | return nil, err
70 | }
71 | fmt.Println("Backing off for 2 seconds...")
72 | time.Sleep(2 * time.Second)
73 | continue
74 | }
75 | log.Println("Connected to etcd!")
76 | return cli, nil
77 | }
78 |
--------------------------------------------------------------------------------
/mail-service/cmd/api/handlers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | )
7 |
8 | // SendMail receives a json payload for a message, and sends it
9 | func (app *Config) SendMail(w http.ResponseWriter, r *http.Request) {
10 | type mailMessage struct {
11 | From string `json:"from"`
12 | To string `json:"to"`
13 | Subject string `json:"subject"`
14 | Message string `json:"message"`
15 | }
16 |
17 | var requestPayload mailMessage
18 |
19 | err := app.readJSON(w, r, &requestPayload)
20 | if err != nil {
21 | log.Println(err)
22 | _ = app.errorJSON(w, err)
23 | return
24 | }
25 |
26 | msg := Message{
27 | From: requestPayload.From,
28 | To: requestPayload.To,
29 | Subject: requestPayload.Subject,
30 | Data: requestPayload.Message,
31 | }
32 |
33 | err = app.Mailer.SendSMTPMessage(msg)
34 | if err != nil {
35 | log.Println(err)
36 | _ = app.errorJSON(w, err)
37 | return
38 | }
39 |
40 | payload := jsonResponse{
41 | Error: false,
42 | Message: "sent to " + requestPayload.To,
43 | }
44 |
45 | _ = app.writeJSON(w, http.StatusAccepted, payload)
46 | }
47 |
--------------------------------------------------------------------------------
/mail-service/cmd/api/helpers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/json"
6 | "errors"
7 | "io"
8 | "net/http"
9 | )
10 |
11 | const randomStringSource = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0987654321_+"
12 |
13 | type jsonResponse struct {
14 | Error bool `json:"error"`
15 | Message string `json:"message"`
16 | Data any `json:"data,omitempty"`
17 | }
18 |
19 | // readJSON tries to read the body of a request and converts it into JSON
20 | func (app *Config) readJSON(w http.ResponseWriter, r *http.Request, data any) error {
21 | maxBytes := 1048576 // one megabyte
22 | r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
23 |
24 | dec := json.NewDecoder(r.Body)
25 | err := dec.Decode(data)
26 | if err != nil {
27 | return err
28 | }
29 |
30 | err = dec.Decode(&struct{}{})
31 | if err != io.EOF {
32 | return errors.New("body must have only a single json value")
33 | }
34 |
35 | return nil
36 | }
37 |
38 | // writeJSON takes a response status code and arbitrary data and writes a json response to the client
39 | func (app *Config) writeJSON(w http.ResponseWriter, status int, data any, headers ...http.Header) error {
40 | out, err := json.Marshal(data)
41 | if err != nil {
42 | return err
43 | }
44 |
45 | if len(headers) > 0 {
46 | for key, value := range headers[0] {
47 | w.Header()[key] = value
48 | }
49 | }
50 |
51 | w.Header().Set("Content-Type", "application/json")
52 | w.WriteHeader(status)
53 | _, err = w.Write(out)
54 | if err != nil {
55 | return err
56 | }
57 |
58 | return nil
59 | }
60 |
61 | // errorJSON takes an error, and optionally a response status code, and generates and sends
62 | // a json error response
63 | func (app *Config) errorJSON(w http.ResponseWriter, err error, status ...int) error {
64 | statusCode := http.StatusBadRequest
65 |
66 | if len(status) > 0 {
67 | statusCode = status[0]
68 | }
69 |
70 | var payload jsonResponse
71 | payload.Error = true
72 | payload.Message = err.Error()
73 |
74 | return app.writeJSON(w, statusCode, payload)
75 | }
76 |
77 | // randomString returns a random string of letters of length n
78 | func (app *Config) randomString(n int) string {
79 | s, r := make([]rune, n), []rune(randomStringSource)
80 | for i := range s {
81 | p, _ := rand.Prime(rand.Reader, len(r))
82 | x, y := p.Uint64(), uint64(len(r))
83 | s[i] = r[x%y]
84 | }
85 | return string(s)
86 | }
87 |
--------------------------------------------------------------------------------
/mail-service/cmd/api/mailer.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "github.com/vanng822/go-premailer/premailer"
6 | mail "github.com/xhit/go-simple-mail/v2"
7 | "html/template"
8 | "time"
9 | )
10 |
11 | // Mail holds the information necessary to connect to an SMTP server
12 | type Mail struct {
13 | Domain string
14 | Host string
15 | Port int
16 | Username string
17 | Password string
18 | Encryption string
19 | FromAddress string
20 | FromName string
21 | }
22 |
23 | // Message is the type for an email message
24 | type Message struct {
25 | From string
26 | FromName string
27 | To string
28 | Subject string
29 | Attachments []string
30 | Data any
31 | DataMap map[string]any
32 | }
33 |
34 | // SendSMTPMessage builds and sends an email message using SMTP. This is called by ListenForMail,
35 | // and can also be called directly when necessary
36 | func (m *Mail) SendSMTPMessage(msg Message) error {
37 | if msg.From == "" {
38 | msg.From = m.FromAddress
39 | }
40 |
41 | if msg.FromName == "" {
42 | msg.FromName = m.FromName
43 | }
44 |
45 | data := map[string]any{
46 | "message": msg.Data,
47 | }
48 | msg.DataMap = data
49 |
50 | formattedMessage, err := m.buildHTMLMessage(msg)
51 | if err != nil {
52 | return err
53 | }
54 |
55 | plainMessage, err := m.buildPlainTextMessage(msg)
56 | if err != nil {
57 | return err
58 | }
59 |
60 | server := mail.NewSMTPClient()
61 | server.Host = m.Host
62 | server.Port = m.Port
63 | server.Username = m.Username
64 | server.Password = m.Password
65 | server.Encryption = m.getEncryption(m.Encryption)
66 | server.KeepAlive = false
67 | server.ConnectTimeout = 10 * time.Second
68 | server.SendTimeout = 10 * time.Second
69 |
70 | smtpClient, err := server.Connect()
71 | if err != nil {
72 | return err
73 | }
74 |
75 | email := mail.NewMSG()
76 | email.SetFrom(msg.From).
77 | AddTo(msg.To).
78 | SetSubject(msg.Subject)
79 |
80 | email.SetBody(mail.TextPlain, plainMessage)
81 | email.AddAlternative(mail.TextHTML, formattedMessage)
82 |
83 | // add attachments, if any
84 | if len(msg.Attachments) > 0 {
85 | for _, x := range msg.Attachments {
86 | email.AddAttachment(x)
87 | }
88 | }
89 |
90 | err = email.Send(smtpClient)
91 | if err != nil {
92 | return err
93 | }
94 |
95 | return nil
96 | }
97 |
98 | // buildHTMLMessage creates the html version of the message
99 | func (m *Mail) buildHTMLMessage(msg Message) (string, error) {
100 | templateToRender := "./templates/mail.html.tmpl"
101 |
102 | t, err := template.New("email-html").ParseFiles(templateToRender)
103 | if err != nil {
104 | return "", err
105 | }
106 |
107 | var tpl bytes.Buffer
108 | if err = t.ExecuteTemplate(&tpl, "body", msg.DataMap); err != nil {
109 | return "", err
110 | }
111 |
112 | formattedMessage := tpl.String()
113 | formattedMessage, err = m.inlineCSS(formattedMessage)
114 | if err != nil {
115 | return "", err
116 | }
117 | return formattedMessage, nil
118 | }
119 |
120 | // buildPlainTextMessage creates the plaintext version of the message
121 | func (m *Mail) buildPlainTextMessage(msg Message) (string, error) {
122 | templateToRender := "./templates/mail.plain.tmpl"
123 | t, err := template.New("email-plain").ParseFiles(templateToRender)
124 | if err != nil {
125 | return "", err
126 | }
127 |
128 | var tplPlain bytes.Buffer
129 | if err = t.ExecuteTemplate(&tplPlain, "body", msg.DataMap); err != nil {
130 | return "", err
131 | }
132 |
133 | plainMessage := tplPlain.String()
134 |
135 | return plainMessage, nil
136 | }
137 |
138 | // getEncryption returns the appropriate encryption type based on a string value
139 | func (m *Mail) getEncryption(e string) mail.Encryption {
140 | switch e {
141 | case "tls":
142 | return mail.EncryptionSTARTTLS
143 | case "ssl":
144 | return mail.EncryptionSSLTLS
145 | case "none", "":
146 | return mail.EncryptionNone
147 | default:
148 | return mail.EncryptionSTARTTLS
149 | }
150 | }
151 |
152 | // inlineCSS takes html input as a string, and inlines css where possible
153 | func (m *Mail) inlineCSS(s string) (string, error) {
154 | options := premailer.Options{
155 | RemoveClasses: false,
156 | CssToAttributes: false,
157 | KeepBangImportant: true,
158 | }
159 | prem, err := premailer.NewPremailerFromString(s, &options)
160 | if err != nil {
161 | return "", err
162 | }
163 |
164 | html, err := prem.Transform()
165 | if err != nil {
166 | return "", err
167 | }
168 |
169 | return html, nil
170 | }
171 |
--------------------------------------------------------------------------------
/mail-service/cmd/api/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | clientv3 "go.etcd.io/etcd/client/v3"
6 | "log"
7 | "net/http"
8 | "os"
9 | "strconv"
10 | )
11 |
12 | const webPort = "80"
13 |
14 | // Config is the application Config, shared with functions by using it as a receiver
15 | type Config struct {
16 | Mailer Mail
17 | Etcd *clientv3.Client
18 | }
19 |
20 | func main() {
21 | // create our configuration
22 | app := Config{
23 | Mailer: createMail(),
24 | }
25 |
26 | log.Println("Starting mail-service on port", webPort)
27 |
28 | // define a server that listens on port 80 and uses our routes()
29 | srv := &http.Server{
30 | Addr: fmt.Sprintf(":%s", webPort),
31 | Handler: app.routes(),
32 | }
33 |
34 | // connect to etcd and register service
35 | app.registerService()
36 | defer app.Etcd.Close()
37 |
38 | err := srv.ListenAndServe()
39 | if err != nil {
40 | log.Panic(err)
41 | }
42 | }
43 |
44 | // createMail creates a variable of type Mail and populates its values.
45 | // Typically, this kind of information comes from the environment, or from
46 | // command line parameters.
47 | func createMail() Mail {
48 | port, _ := strconv.Atoi(os.Getenv("MAIL_PORT"))
49 | s := Mail{
50 | Domain: os.Getenv("MAIL_DOMAIN"),
51 | Host: os.Getenv("MAIL_HOST"),
52 | Port: port,
53 | Username: os.Getenv("MAIL_USERNAME"),
54 | Password: os.Getenv("MAIL_PASSWORD"),
55 | Encryption: os.Getenv("MAIL_ENCRYPTION"),
56 | FromName: os.Getenv("FROM_NAME"),
57 | FromAddress: os.Getenv("FROM_ADDRESS"),
58 | }
59 |
60 | return s
61 | }
62 |
--------------------------------------------------------------------------------
/mail-service/cmd/api/routes.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/go-chi/chi/v5"
5 | "github.com/go-chi/chi/v5/middleware"
6 | "github.com/go-chi/cors"
7 | "net/http"
8 | )
9 |
10 | func (app *Config) routes() http.Handler {
11 | mux := chi.NewRouter()
12 | mux.Use(middleware.Heartbeat("/ping"))
13 |
14 | // specify who is allowed to connect to our API service
15 | mux.Use(cors.Handler(cors.Options{
16 | AllowedOrigins: []string{"http://*", "https://*"},
17 | AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
18 | AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
19 | ExposedHeaders: []string{"Link"},
20 | AllowCredentials: true,
21 | MaxAge: 300,
22 | }))
23 |
24 | mux.Post("/send", app.SendMail)
25 |
26 | return mux
27 | }
28 |
--------------------------------------------------------------------------------
/mail-service/go.mod:
--------------------------------------------------------------------------------
1 | module mail-service
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/go-chi/chi/v5 v5.0.7
7 | github.com/go-chi/cors v1.2.0
8 | github.com/vanng822/go-premailer v1.20.1
9 | github.com/xhit/go-simple-mail/v2 v2.11.0
10 | go.etcd.io/etcd/client/v3 v3.5.2
11 | )
12 |
13 | require (
14 | github.com/PuerkitoBio/goquery v1.8.0 // indirect
15 | github.com/andybalholm/cascadia v1.3.1 // indirect
16 | github.com/coreos/go-semver v0.3.0 // indirect
17 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect
18 | github.com/davecgh/go-spew v1.1.1 // indirect
19 | github.com/go-test/deep v1.0.8 // indirect
20 | github.com/gogo/protobuf v1.3.2 // indirect
21 | github.com/golang/protobuf v1.5.2 // indirect
22 | github.com/gorilla/css v1.0.0 // indirect
23 | github.com/stretchr/testify v1.7.0 // indirect
24 | github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
25 | github.com/vanng822/css v1.0.1 // indirect
26 | go.etcd.io/etcd/api/v3 v3.5.2 // indirect
27 | go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect
28 | go.uber.org/atomic v1.9.0 // indirect
29 | go.uber.org/multierr v1.8.0 // indirect
30 | go.uber.org/zap v1.21.0 // indirect
31 | golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect
32 | golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 // indirect
33 | golang.org/x/text v0.3.7 // indirect
34 | google.golang.org/genproto v0.0.0-20220328150716-24ca77f39d1f // indirect
35 | google.golang.org/grpc v1.45.0 // indirect
36 | google.golang.org/protobuf v1.28.0 // indirect
37 | )
38 |
--------------------------------------------------------------------------------
/mail-service/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
4 | github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
5 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
6 | github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
7 | github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
8 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
9 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
10 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
11 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
12 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
13 | github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
14 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
15 | github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
16 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
17 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
18 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
19 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
20 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
21 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
22 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
23 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
24 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
25 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
26 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
27 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
28 | github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
29 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
30 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
31 | github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
32 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
33 | github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
34 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
35 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
36 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
37 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
38 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
39 | github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
40 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
41 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
42 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
43 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
44 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
45 | github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
46 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
47 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
48 | github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
49 | github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
50 | github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE=
51 | github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
52 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
53 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
54 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
55 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
56 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
57 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
58 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
59 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
60 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
61 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
62 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
63 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
64 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
65 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
66 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
67 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
68 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
69 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
70 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
71 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
72 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
73 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
74 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
75 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
76 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
77 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
78 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
79 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
80 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
81 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
82 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
83 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
84 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
85 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
86 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
87 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
88 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
89 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
90 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
91 | github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
92 | github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
93 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
94 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
95 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
96 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
97 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
98 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
99 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
100 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
101 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
102 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
103 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
104 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
105 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
106 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
107 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
108 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
117 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
118 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
119 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
120 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
121 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
122 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
123 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
124 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
125 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
126 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
127 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
128 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
129 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
130 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
131 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
132 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
133 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
134 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
135 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
136 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
137 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
138 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
139 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
140 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
141 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
142 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
143 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
144 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
145 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
146 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
147 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
148 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
149 | github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
150 | github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
151 | github.com/unrolled/render v1.0.3/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM=
152 | github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8=
153 | github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
154 | github.com/vanng822/go-premailer v1.20.1 h1:2LTSIULXxNV5IOB5BSD3dlfOG95cq8qqExtRZMImTGA=
155 | github.com/vanng822/go-premailer v1.20.1/go.mod h1:RAxbRFp6M/B171gsKu8dsyq+Y5NGsUUvYfg+WQWusbE=
156 | github.com/vanng822/r2router v0.0.0-20150523112421-1023140a4f30/go.mod h1:1BVq8p2jVr55Ost2PkZWDrG86PiJ/0lxqcXoAcGxvWU=
157 | github.com/xhit/go-simple-mail/v2 v2.11.0 h1:o/056V50zfkO3Mm5tVdo9rG3ryg4ZmJ2XW5GMinHfVs=
158 | github.com/xhit/go-simple-mail/v2 v2.11.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
159 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
160 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
161 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
162 | go.etcd.io/etcd/api/v3 v3.5.2 h1:tXok5yLlKyuQ/SXSjtqHc4uzNaMqZi2XsoSPr/LlJXI=
163 | go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
164 | go.etcd.io/etcd/client/pkg/v3 v3.5.2 h1:4hzqQ6hIb3blLyQ8usCU4h3NghkqcsohEQ3o3VetYxE=
165 | go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
166 | go.etcd.io/etcd/client/v3 v3.5.2 h1:WdnejrUtQC4nCxK0/dLTMqKOB+U5TP/2Ya0BJL+1otA=
167 | go.etcd.io/etcd/client/v3 v3.5.2/go.mod h1:kOOaWFFgHygyT0WlSmL8TJiXmMysO/nNUlEsSsN6W4o=
168 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
169 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
170 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
171 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
172 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
173 | go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
174 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
175 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
176 | go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
177 | go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
178 | go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
179 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
180 | go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
181 | go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
182 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
183 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
184 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
185 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
186 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
187 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
188 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
189 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
190 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
191 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
192 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
193 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
194 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
195 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
196 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
197 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
198 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
199 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
200 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
201 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
202 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
203 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
204 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
205 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
206 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
207 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
208 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
209 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
210 | golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
211 | golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
212 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
213 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
214 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
215 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
216 | golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc=
217 | golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
218 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
219 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
220 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
221 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
222 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
223 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
224 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
225 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
226 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
227 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
228 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
229 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
230 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
231 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
232 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
233 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
234 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
235 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
236 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
237 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
238 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
239 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
240 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
241 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
242 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
243 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
244 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
245 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
246 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
247 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
248 | golang.org/x/sys v0.0.0-20220325203850-36772127a21f h1:TrmogKRsSOxRMJbLYGrB4SBbW+LJcEllYBLME5Zk5pU=
249 | golang.org/x/sys v0.0.0-20220325203850-36772127a21f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
250 | golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886 h1:eJv7u3ksNXoLbGSKuv2s/SIO4tJVxc/A+MTpzxDgz/Q=
251 | golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
252 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
253 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
254 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
255 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
256 | golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
257 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
258 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
259 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
260 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
261 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
262 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
263 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
264 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
265 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
266 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
267 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
268 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
269 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
270 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
271 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
272 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
273 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
274 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
275 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
276 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
277 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
278 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
279 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
280 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
281 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
282 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0=
283 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
284 | google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb h1:0m9wktIpOxGw+SSKmydXWB3Z3GTfcPP6+q75HCQa6HI=
285 | google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
286 | google.golang.org/genproto v0.0.0-20220328150716-24ca77f39d1f h1:9ug+SpnUXKl5LogY3yp9GHWUjUDCnFv4NjiP7yxS6Q4=
287 | google.golang.org/genproto v0.0.0-20220328150716-24ca77f39d1f/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
288 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
289 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
290 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
291 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
292 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
293 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
294 | google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
295 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
296 | google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M=
297 | google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
298 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
299 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
300 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
301 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
302 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
303 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
304 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
305 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
306 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
307 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
308 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
309 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
310 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
311 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
312 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
313 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
314 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
315 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
316 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
317 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
318 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
319 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
320 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
321 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
322 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
323 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
324 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
325 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
326 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
327 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
328 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
329 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
330 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
331 |
--------------------------------------------------------------------------------
/mail-service/templates/mail.html.tmpl:
--------------------------------------------------------------------------------
1 | {{define "body"}}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{.message}}
14 |
15 |
16 |
17 |
18 | {{end}}
--------------------------------------------------------------------------------
/mail-service/templates/mail.plain.tmpl:
--------------------------------------------------------------------------------
1 | {{define "body"}}
2 | {{.message}}
3 | {{end}}
--------------------------------------------------------------------------------
/multistage-dockerfiles/authentication-service.dockerfile:
--------------------------------------------------------------------------------
1 | # The base go-image
2 | FROM golang:1.18-alpine as builder
3 |
4 | # create a directory for the app
5 | RUN mkdir /app
6 |
7 | # copy all files from the current directory to the app directory
8 | COPY authentication-service/. /app
9 |
10 | # set working directory
11 | WORKDIR /app
12 |
13 | # build executable
14 | RUN CGO_ENABLED=0 go build -o authApp .
15 |
16 | RUN chmod +x /app/authApp
17 |
18 | # create a tiny image for use
19 | FROM alpine:latest
20 | RUN mkdir /app
21 |
22 | COPY --from=builder /app/authApp /app
23 |
24 | # Run the server executable
25 | CMD [ "/app/authApp" ]
--------------------------------------------------------------------------------
/multistage-dockerfiles/broker-service.dockerfile:
--------------------------------------------------------------------------------
1 | # The base go-image
2 | FROM golang:1.18-alpine as builder
3 |
4 | # create a directory for the app
5 | RUN mkdir /app
6 |
7 | # copy all files from the current directory to the app directory
8 | COPY broker-service/. /app
9 |
10 | # set working directory
11 | WORKDIR /app
12 |
13 | # build executable
14 | RUN CGO_ENABLED=0 go build -o brokerApp ./cmd/api
15 |
16 | RUN chmod +x /app/brokerApp
17 |
18 | # create a tiny image for use
19 | FROM alpine:latest
20 | RUN mkdir /app
21 |
22 | COPY --from=builder /app/brokerApp /app
23 |
24 | # Run the server executable
25 | CMD [ "/app/brokerApp" ]
--------------------------------------------------------------------------------
/multistage-dockerfiles/listener-service.dockerfile:
--------------------------------------------------------------------------------
1 | # The base go-image
2 | FROM golang:1.18-alpine as builder
3 |
4 | # create a directory for the app
5 | RUN mkdir /app
6 |
7 | # copy all files from the current directory to the app directory
8 | COPY listener-service /app
9 |
10 | # set working directory
11 | WORKDIR /app
12 |
13 | # build executable
14 | RUN CGO_ENABLED=0 go build -o listener .
15 |
16 | RUN chmod +x /app/listener
17 |
18 | # create a tiny image for use
19 | FROM alpine:latest
20 | RUN mkdir /app
21 |
22 | COPY --from=builder /app/listener /app
23 |
24 | # Run the server executable
25 | CMD [ "/app/listener" ]
--------------------------------------------------------------------------------
/multistage-dockerfiles/logger-service.dockerfile:
--------------------------------------------------------------------------------
1 | # The base go-image
2 | FROM golang:1.18-alpine as builder
3 |
4 | # create a directory for the app
5 | RUN mkdir /app
6 |
7 | # copy all files from the current directory to the app directory
8 | COPY logger-service/. /app
9 |
10 | # set working directory
11 | WORKDIR /app
12 |
13 | # build executable
14 | RUN CGO_ENABLED=0 go build -o logServiceApp ./cmd/web
15 |
16 | RUN chmod +x /app/logServiceApp
17 |
18 | # create a tiny image for use
19 | FROM alpine:latest
20 | RUN mkdir /app
21 | RUN mkdir /templates
22 |
23 | COPY --from=builder /app/logServiceApp /app
24 | COPY --from=builder /app/templates/. /templates
25 |
26 | # Run the server executable
27 | CMD [ "/app/logServiceApp" ]
--------------------------------------------------------------------------------
/multistage-dockerfiles/mail-service.dockerfile:
--------------------------------------------------------------------------------
1 | # The base go-image
2 | FROM golang:1.18-alpine as builder
3 |
4 | # create a directory for the app
5 | RUN mkdir /app
6 |
7 | # copy all files from the current directory to the app directory
8 | COPY mail-service/. /app
9 |
10 | # set working directory
11 | WORKDIR /app
12 |
13 | # build executable
14 | RUN CGO_ENABLED=0 go build -o mailerServiceApp .
15 |
16 | RUN chmod +x /app/mailerServiceApp
17 |
18 | # create a tiny image for use
19 | FROM alpine:latest
20 | RUN mkdir /app
21 | RUN mkdir /templates
22 |
23 | COPY --from=builder /app/mailerServiceApp /app
24 | COPY --from=builder /app/templates /templates
25 |
26 | # Run the server executable
27 | CMD [ "/app/mailerServiceApp" ]
--------------------------------------------------------------------------------
/postgres.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: postgres
5 | spec:
6 | replicas: 1
7 | selector:
8 | matchLabels:
9 | app: postgres
10 | template:
11 | metadata:
12 | labels:
13 | app: postgres
14 | spec:
15 | containers:
16 | - name: postgres
17 | image: "postgres:14.0"
18 | env:
19 | - name: POSTGRES_USER
20 | value: "postgres"
21 | - name: POSTGRES_PASSWORD
22 | value: "password"
23 | - name: "POSTGRES_DB"
24 | value: "users"
25 | ports:
26 | - containerPort: 5432
27 |
28 | ---
29 |
30 | apiVersion: v1
31 | kind: Service
32 | metadata:
33 | name: postgres
34 | spec:
35 | selector:
36 | app: postgres
37 | ports:
38 | - protocol: TCP
39 | port: 5432
40 | targetPort: 5432
--------------------------------------------------------------------------------
/swarm.md:
--------------------------------------------------------------------------------
1 | # Docker swarm
2 |
3 |
4 | ## Build images:
5 | ```bash
6 | docker build -f front-end.dockerfile -t tsawler/front-end:tag1 .
7 | docker push tsawler/front-end:tag1
8 | ```
9 |
10 | ## Manage
11 |
12 | ```bash
13 | docker swarm init
14 | docker swarm join-token worker
15 | docker swarm join-token manager
16 | docker stack deploy -c .yml
17 | docker service ls
18 | watch docker service ls
19 | docker service scale =
20 | ```
21 |
22 | ## Updating (pull image and scale first)
23 | ```bash
24 | docker service update --image tsawler/listener:1.0.1 myapp_listener-service
25 | ```
26 |
27 | ## Bringing swarm down
28 | Easy method:
29 | ```bash
30 | docker stack rm myapp
31 | ```
32 | To stop them, scale all services to 0, or just type
33 | ```bash
34 | docker swarm leave
35 | ```
--------------------------------------------------------------------------------
/swarm.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 |
5 | caddy:
6 | image: tsawler/micro-caddy:1.0.0
7 | deploy:
8 | mode: replicated
9 | replicas: 1
10 | placement:
11 | constraints:
12 | - node.hostname == docker-desktop
13 | volumes:
14 | - caddy_data:/data
15 | - caddy_config:/config
16 | ports:
17 | - "80:80"
18 | - "444:443"
19 |
20 | front-end:
21 | image: tsawler/front-end:1.0.0
22 | deploy:
23 | mode: replicated
24 | replicas: 1
25 |
26 | # broker-service - main entry point; we call this from the front end
27 | broker-service:
28 | image: tsawler/broker-service:1.0.1
29 | ports:
30 | - "8080:80"
31 | deploy:
32 | mode: replicated
33 | replicas: 1
34 |
35 | # listener-service - watches rabbitmq for messages
36 | listener-service:
37 | image: tsawler/listener-service:1.0.0
38 | deploy:
39 | mode: replicated
40 | replicas: 2
41 |
42 | # authentication-service - handles user auth
43 | authentication-service:
44 | image: tsawler/authentication-service:1.0.0
45 | deploy:
46 | mode: replicated
47 | replicas: 1
48 | environment:
49 | DSN: "host=postgres port=5432 user=postgres password=password dbname=users sslmode=disable timezone=UTC connect_timeout=5"
50 |
51 | # logger-service: a service to store logs
52 | logger-service:
53 | image: tsawler/logger-service:1.0.0
54 | ports:
55 | - "8082:80"
56 | deploy:
57 | mode: replicated
58 | replicas: 1
59 | volumes:
60 | - ./logger-service/templates/:/app/templates
61 |
62 | # mail-service - handles sending mail
63 | mail-service:
64 | image: tsawler/mail-service:1.0.0
65 | deploy:
66 | mode: replicated
67 | replicas: 1
68 | environment:
69 | MAIL_DOMAIN: localhost
70 | MAIL_HOST: mailhog
71 | MAIL_PORT: 1025
72 | MAIL_ENCRYPTION: none
73 | MAIL_USERNAME: ""
74 | MAIL_PASSWORD: ""
75 | FROM_NAME: "John Smith"
76 | FROM_ADDRESS: john.smith@example.com
77 |
78 | # rabbitmq: the rabbitmq server
79 | rabbitmq:
80 | image: 'rabbitmq:3-management'
81 | deploy:
82 | mode: global
83 | volumes:
84 | - ./db-data/rabbitmq/:/var/lib/rabbitmq/
85 |
86 | # mailhog: a fake smtp server with a web interface
87 | mailhog:
88 | image: 'mailhog/mailhog:latest'
89 | ports:
90 | - "8025:8025"
91 | deploy:
92 | mode: global
93 |
94 | # mongo: start MongoDB and ensure that data is stored to a mounted volume
95 | mongo:
96 | image: 'mongo:4.2.17-bionic'
97 | ports:
98 | - "27017:27017"
99 | deploy:
100 | mode: global
101 | environment:
102 | MONGO_INITDB_DATABASE: logs
103 | MONGO_INITDB_ROOT_USERNAME: admin
104 | MONGO_INITDB_ROOT_PASSWORD: password
105 | volumes:
106 | - ./db-data/mongo/:/data/db
107 |
108 | # postgres: start Postgres, and ensure that data is stored to a mounted volume
109 | postgres:
110 | image: 'postgres:14.2'
111 | ports:
112 | - "5432:5432"
113 | deploy:
114 | mode: global
115 | environment:
116 | POSTGRES_USER: postgres
117 | POSTGRES_PASSWORD: password
118 | POSTGRES_DB: users
119 | volumes:
120 | - ./db-data/postgres/:/var/lib/postgresql/data/
121 |
122 | # etcd: start etcd server
123 | etcd:
124 | image: docker.io/bitnami/etcd:3
125 | environment:
126 | - ALLOW_NONE_AUTHENTICATION=yes
127 | deploy:
128 | mode: global
129 | volumes:
130 | - ./db-data/etcd/:/bitnami/etcd
131 |
132 | volumes:
133 | caddy_data:
134 | external: true
135 | caddy_config:
--------------------------------------------------------------------------------