From c774bc7f8d911f70ccb2f2f723468e9ad2fe3c41 Mon Sep 17 00:00:00 2001 From: Steffen Neubauer Date: Tue, 13 Jun 2023 15:13:05 +0200 Subject: [PATCH] feat: add limiter service written in go The limiter service is a reverse proxy written in go that adds rate limiting functionality to make the vote process more fair. This is to highlight that code synchronization works as well with compiled languages like go as with dynamic languages like python. --- .gitignore | 3 +++ api/garden.yml | 10 +++---- limiter/Dockerfile | 7 +++++ limiter/Dockerfile.syncmode | 8 ++++++ limiter/garden.yml | 54 +++++++++++++++++++++++++++++++++++++ limiter/src/.air.toml | 44 ++++++++++++++++++++++++++++++ limiter/src/.gitignore | 1 + limiter/src/go.mod | 12 +++++++++ limiter/src/go.sum | 16 +++++++++++ limiter/src/main.go | 46 +++++++++++++++++++++++++++++++ result/garden.yml | 2 +- vote/garden.yml | 8 +++--- vote/vue.config.js | 2 +- 13 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 limiter/Dockerfile create mode 100644 limiter/Dockerfile.syncmode create mode 100644 limiter/garden.yml create mode 100644 limiter/src/.air.toml create mode 100644 limiter/src/.gitignore create mode 100644 limiter/src/go.mod create mode 100644 limiter/src/go.sum create mode 100644 limiter/src/main.go diff --git a/.gitignore b/.gitignore index e8db23d..31bb65f 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,6 @@ typings/ # IDEs .vscode .history + +# macOS +.DS_Store diff --git a/api/garden.yml b/api/garden.yml index 588c7fa..f5a6683 100644 --- a/api/garden.yml +++ b/api/garden.yml @@ -1,14 +1,14 @@ --- kind: Build type: container -name: api-build +name: api description: The backend build for the voting UI --- kind: Deploy type: container name: api -build: api-build +build: api description: The backend deploy for the voting UI spec: args: [python, app.py] @@ -25,7 +25,7 @@ spec: ingresses: - path: / port: http - hostname: api.${variables.base-hostname} + hostname: api-${variables.base-hostname} healthCheck: httpGet: path: /health @@ -38,6 +38,6 @@ kind: Test type: container name: unit description: Unit test for backend API -build: api-build +build: api spec: - args: ["echo", "ok"] \ No newline at end of file + args: ["echo", "ok"] diff --git a/limiter/Dockerfile b/limiter/Dockerfile new file mode 100644 index 0000000..b670ffa --- /dev/null +++ b/limiter/Dockerfile @@ -0,0 +1,7 @@ +FROM golang:alpine as prod +RUN mkdir /app +ADD ./src /app/ +WORKDIR /app +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . +CMD ["/app/main"] +EXPOSE 8080 diff --git a/limiter/Dockerfile.syncmode b/limiter/Dockerfile.syncmode new file mode 100644 index 0000000..a9eec24 --- /dev/null +++ b/limiter/Dockerfile.syncmode @@ -0,0 +1,8 @@ +FROM cosmtrek/air:latest as sync + +# We do not copy anything on purpose in sync mode. +# Code will be copied using mutagen as part of Garden's code synchronization feature. +RUN mkdir /app +WORKDIR /app + +EXPOSE 8080 diff --git a/limiter/garden.yml b/limiter/garden.yml new file mode 100644 index 0000000..496c92a --- /dev/null +++ b/limiter/garden.yml @@ -0,0 +1,54 @@ +# Production container to be used without sync mode. +kind: Build +type: container +name: limiter-prod +include: + - src + - Dockerfile +exclude: + - src/tmp/* +spec: + dockerfile: "Dockerfile" + +--- + +# The sync container only provides the tools necessary for building the library +# when using it during inner loop development together with Garden's sync mode. +kind: Build +type: container +name: limiter-sync +include: + - Dockerfile.syncmode +spec: + dockerfile: "Dockerfile.syncmode" + +--- + +kind: Deploy +type: container +name: limiter +build: + $if: ${command.params contains 'sync' && (command.params.sync contains 'limiter' || isEmpty(command.params.sync))} + $then: limiter-sync + $else: limiter-prod +include: [] +dependencies: + - deploy.api +spec: + sync: + command: [air] + paths: + - target: /app + source: ./src + mode: one-way-replica + exclude: ["tmp/*/**"] + ports: + - name: http + containerPort: 8080 + servicePort: 80 + ingresses: + - path: / + port: http + hostname: limiter-${var.base-hostname} + env: + PROXY_URL: http://api diff --git a/limiter/src/.air.toml b/limiter/src/.air.toml new file mode 100644 index 0000000..ad439e1 --- /dev/null +++ b/limiter/src/.air.toml @@ -0,0 +1,44 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 0 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/limiter/src/.gitignore b/limiter/src/.gitignore new file mode 100644 index 0000000..a9a5aec --- /dev/null +++ b/limiter/src/.gitignore @@ -0,0 +1 @@ +tmp diff --git a/limiter/src/go.mod b/limiter/src/go.mod new file mode 100644 index 0000000..70476b9 --- /dev/null +++ b/limiter/src/go.mod @@ -0,0 +1,12 @@ +module example.garden.io/m + +go 1.20 + +require github.com/didip/tollbooth/v7 v7.0.1 + +require ( + github.com/didip/tollbooth v4.0.2+incompatible // indirect + github.com/go-pkgz/expirable-cache v0.1.0 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + golang.org/x/time v0.3.0 // indirect +) diff --git a/limiter/src/go.sum b/limiter/src/go.sum new file mode 100644 index 0000000..f24a355 --- /dev/null +++ b/limiter/src/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M= +github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY= +github.com/didip/tollbooth/v7 v7.0.1 h1:TkT4sBKoQoHQFPf7blQ54iHrZiTDnr8TceU+MulVAog= +github.com/didip/tollbooth/v7 v7.0.1/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4= +github.com/go-pkgz/expirable-cache v0.1.0 h1:3bw0m8vlTK8qlwz5KXuygNBTkiKRTPrAGXU0Ej2AC1g= +github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/limiter/src/main.go b/limiter/src/main.go new file mode 100644 index 0000000..a0598e0 --- /dev/null +++ b/limiter/src/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + + "github.com/didip/tollbooth" +) + +func main() { + remote, err := url.Parse(os.Getenv("PROXY_URL")) + if err != nil { + panic(err) + } + + // Rate limiter: Allow one vote per second. + // Suggestion: Try reducing this to 0.1 to allow one vote every 10 seconds. + // When using garden deploy --sync, the code change will be automatically synchronized + // See also https://docs.garden.io/guides/code-synchronization + limiter := tollbooth.NewLimiter(1, nil) + limiter.SetMessage("Please slow down.") + limiter.SetOnLimitReached(func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s: Limiting: Too many requests", r.URL) + }) + + // Reverse proxy + handler := func(p *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { + log.Println("Hello world!") + + return func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s: OK: Forwarding", r.URL) + r.Host = remote.Host + p.ServeHTTP(w, r) + } + } + + proxy := httputil.NewSingleHostReverseProxy(remote) + http.Handle("/", tollbooth.LimitFuncHandler(limiter, handler(proxy))) + err = http.ListenAndServe(":8080", nil) + if err != nil { + panic(err) + } +} diff --git a/result/garden.yml b/result/garden.yml index 9517d9d..48dd1be 100644 --- a/result/garden.yml +++ b/result/garden.yml @@ -20,7 +20,7 @@ spec: ingresses: - path: / port: ui - hostname: result.${var.base-hostname} + hostname: result-${var.base-hostname} ports: - name: ui protocol: TCP diff --git a/vote/garden.yml b/vote/garden.yml index eaf422b..1028314 100644 --- a/vote/garden.yml +++ b/vote/garden.yml @@ -27,12 +27,12 @@ spec: ingresses: - path: / port: http - hostname: vote.${var.base-hostname} + hostname: vote-${var.base-hostname} env: - HOSTNAME: vote.${var.base-hostname} + HOSTNAME: vote-${var.base-hostname} VUE_APP_USERNAME: ${local.username} dependencies: - - deploy.api + - deploy.limiter - deploy.result --- @@ -55,7 +55,7 @@ type: container name: e2e-runner dependencies: [deploy.vote] spec: - image: ${actions.deploy.vote.outputs.deployedImageId} + image: ${actions.build.vote.outputs.deploymentImageId} --- kind: Test diff --git a/vote/vue.config.js b/vote/vue.config.js index 0efae93..361919b 100644 --- a/vote/vue.config.js +++ b/vote/vue.config.js @@ -5,7 +5,7 @@ module.exports = { progress: false, proxy: { '^/api': { - target: 'http://api', + target: `http://limiter`, // limiter is a rate limiting reverse proxy for the api changeOrigin: true, secure: false, logLevel: 'debug',