{"id":16861803,"url":"https://github.com/maelvls/users-grpc","last_synced_at":"2026-03-03T01:38:13.253Z","repository":{"id":48864176,"uuid":"190594120","full_name":"maelvls/users-grpc","owner":"maelvls","description":"🐳 A gRPC microservice for dealing with users and its CLI client + Helm chart ✨","archived":false,"fork":false,"pushed_at":"2023-02-25T07:04:22.000Z","size":413,"stargazers_count":9,"open_issues_count":17,"forks_count":1,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-11-20T01:02:25.463Z","etag":null,"topics":["cli","golang","goreleaser","grpc","helm-chart","microservice"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maelvls.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-06-06T14:18:51.000Z","updated_at":"2025-02-12T03:30:10.000Z","dependencies_parsed_at":"2024-06-20T11:00:06.304Z","dependency_job_id":"0770acb4-f510-495d-be0c-81bd4f6737b1","html_url":"https://github.com/maelvls/users-grpc","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/maelvls/users-grpc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maelvls%2Fusers-grpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maelvls%2Fusers-grpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maelvls%2Fusers-grpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maelvls%2Fusers-grpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maelvls","download_url":"https://codeload.github.com/maelvls/users-grpc/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maelvls%2Fusers-grpc/sbom","scorecard":{"id":610705,"data":{"date":"2025-08-11","repo":{"name":"github.com/maelvls/users-grpc","commit":"535eb968093d654d3b01707d8aee9e338f25bb01"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":1.9,"checks":[{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Pinned-Dependencies","score":4,"reason":"dependency not pinned by hash detected -- score normalized to 4","details":["Warn: containerImage not pinned by hash: ci/Dockerfile:3","Warn: containerImage not pinned by hash: ci/Dockerfile:19: pin your Docker image by updating alpine to alpine@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1","Warn: goCommand not pinned by hash: ci/Dockerfile:11-13","Info:   0 out of   2 containerImage dependencies pinned","Info:   2 out of   3 goCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.txt:0","Info: FSF or OSI recognized license: MIT License: LICENSE.txt:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":0,"reason":"Project has not signed or included provenance with any releases.","details":["Warn: release artifact 1.2.2 not signed: https://api.github.com/repos/maelvls/users-grpc/releases/34262091","Warn: release artifact 1.2.1 not signed: https://api.github.com/repos/maelvls/users-grpc/releases/34219343","Warn: release artifact 1.2.0 not signed: https://api.github.com/repos/maelvls/users-grpc/releases/34186892","Warn: release artifact 1.1.1 not signed: https://api.github.com/repos/maelvls/users-grpc/releases/34084388","Warn: release artifact 1.1.0 not signed: https://api.github.com/repos/maelvls/users-grpc/releases/34053664","Warn: release artifact 1.2.2 does not have provenance: https://api.github.com/repos/maelvls/users-grpc/releases/34262091","Warn: release artifact 1.2.1 does not have provenance: https://api.github.com/repos/maelvls/users-grpc/releases/34219343","Warn: release artifact 1.2.0 does not have provenance: https://api.github.com/repos/maelvls/users-grpc/releases/34186892","Warn: release artifact 1.1.1 does not have provenance: https://api.github.com/repos/maelvls/users-grpc/releases/34084388","Warn: release artifact 1.1.0 does not have provenance: https://api.github.com/repos/maelvls/users-grpc/releases/34053664"],"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"19 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GO-2022-0322 / GHSA-cg3q-j54f-5p7p","Warn: Project is vulnerable to: GO-2022-0236 / GHSA-h86h-8ppg-mxmh","Warn: Project is vulnerable to: GO-2021-0238 / GHSA-83g2-8m93-v3w7","Warn: Project is vulnerable to: GO-2022-0288","Warn: Project is vulnerable to: GO-2022-0969 / GHSA-69cg-p879-7622","Warn: Project is vulnerable to: GO-2022-1144 / GHSA-xrjj-mj9h-534m","Warn: Project is vulnerable to: GO-2023-1571 / GHSA-vvpx-j8f3-3w6h","Warn: Project is vulnerable to: GO-2023-1988 / GHSA-2wrh-6pvc-2jm9","Warn: Project is vulnerable to: GO-2023-2102 / GHSA-4374-p667-p6c8","Warn: Project is vulnerable to: GO-2023-2153 / GHSA-m425-mq94-257g / GHSA-qppj-fm5r-hxr3","Warn: Project is vulnerable to: GO-2024-2687 / GHSA-4v7x-pqxf-cx7m","Warn: Project is vulnerable to: GO-2024-3333","Warn: Project is vulnerable to: GO-2025-3503 / GHSA-qxp5-gwg8-xv66","Warn: Project is vulnerable to: GO-2025-3595 / GHSA-vvgc-356p-c3xw","Warn: Project is vulnerable to: GO-2022-0493 / GHSA-p782-xgp4-8hr8","Warn: Project is vulnerable to: GO-2021-0113 / GHSA-ppp9-7jff-5vj2","Warn: Project is vulnerable to: GO-2022-1059 / GHSA-69ch-w2m2-3vjp","Warn: Project is vulnerable to: GO-2024-2611 / GHSA-8r3f-844c-mc37","Warn: Project is vulnerable to: GO-2022-0603 / GHSA-hp87-p4gw-j4gq"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-21T02:35:12.670Z","repository_id":48864176,"created_at":"2025-08-21T02:35:12.670Z","updated_at":"2025-08-21T02:35:12.670Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30029705,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-03T00:31:48.536Z","status":"ssl_error","status_checked_at":"2026-03-03T00:30:56.176Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","golang","goreleaser","grpc","helm-chart","microservice"],"created_at":"2024-10-13T14:33:26.971Z","updated_at":"2026-03-03T01:38:13.233Z","avatar_url":"https://github.com/maelvls.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Simple gRPC user service and its CLI client\n\n[![Build Status](https://cloud.drone.io/api/badges/maelvls/users-grpc/status.svg)](https://cloud.drone.io/maelvls/users-grpc)\n[![Coverage Status](https://coveralls.io/repos/github/maelvls/users-grpc/badge.svg?branch=master)](https://coveralls.io/github/maelvls/users-grpc?branch=master)\n[![codecov](https://codecov.io/gh/maelvls/users-grpc/branch/master/graph/badge.svg)](https://codecov.io/gh/maelvls/users-grpc)\n[![Go Report Card](https://goreportcard.com/badge/github.com/maelvls/users-grpc)](https://goreportcard.com/report/github.com/maelvls/users-grpc)\n[![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/maelvls)](https://artifacthub.io/packages/search?repo=maelvls)\n\n\u003e So many shiny badges, I guess it doesn't mean anything anymore! I have\n\u003e been testing many services in order to select the good ones. Badges is a\n\u003e way of keeping track of them all 😅\n\n[![asciicast](https://asciinema.org/a/251067.svg)](https://asciinema.org/a/251067)\n\n- [Stack](#stack)\n- [Use](#use)\n- [Install](#install)\n  - [Docker images](#docker-images)\n  - [Binaries (Github Releases)](#binaries-github-releases)\n  - [Using go-get](#using-go-get)\n  - [Kubernetes \u0026 Helm](#kubernetes--helm)\n- [Develop and hack it](#develop-and-hack-it)\n  - [Testing](#testing)\n  - [Develop using Docker](#develop-using-docker)\n- [Technical notes](#technical-notes)\n  - [Vendor or not vendor and go 1.11 modules](#vendor-or-not-vendor-and-go-111-modules)\n  - [`users-cli version`](#users-cli-version)\n  - [Protobuf generation](#protobuf-generation)\n  - [Logs, debug and verbosity](#logs-debug-and-verbosity)\n  - [Moved from Traefik to Nginx](#moved-from-traefik-to-nginx)\n- [Examples that I read for inspiration](#examples-that-i-read-for-inspiration)\n- [Using the Helm chart](#using-the-helm-chart)\n- [Updating \u0026 uploading the Helm charts](#updating--uploading-the-helm-charts)\n- [Future work](#future-work)\n  - [Using an on-disk database](#using-an-on-disk-database)\n  - [Distributed tracing and logs](#distributed-tracing-and-logs)\n  - [Publishing Helm chart to Github Pages and publishing to Homebrew](#publishing-helm-chart-to-github-pages-and-publishing-to-homebrew)\n- [Design discussion](#design-discussion)\n\n## Stack\n\n- **CI/CD**: Drone.io (tests, coverage, build docker image, upload\n  `users-cli` CLI binaries to Github Releases using [`goreleaser`][goreleaser])\n- **Coverage**: Coveralls, Codecov\n- **Code Quality**: Go Report Card, GolangCI (CI \u0026 local git pre-push\n  hook).\n- **OCI orchestration**: Kubernetes,\n  [Kind](https://github.com/kubernetes-sigs/kind) for testing, Civo for\n  live testing (see related [k.maelvls.dev][])\n- **Config management**: Helm\n- **Dependency analysis** (the DevSecOps trend): [dependabot][] (updates go\n  modules dependencies daily)\n- **Local dev**: Vim \u0026 VSCode, `golangci-lint`, `protoc`, `prototool`,\n  `grpcurl`, [`gotests`][gotests], [gomock][]\n\n  ```sh\n  brew install golangci/tap/golangci-lint protobuf prototool grpcurl\n  ```\n\n[dependabot]: https://dependabot.com/\n[gotests]: https://github.com/cweill/gotests\n[gomock]: https://github.com/golang/mock\n\n## Use\n\nRefer to [Install](#install) below for getting `users-cli` and\n`users-server`.\n\nFirst, let `users-server` run somewhere:\n\n```sh\nusers-server\n```\n\nThen, we can query it using the CLI client. The possible actions are\n\n- create a user\n- fetch a user by his email ('get')\n- list all users (the server loads some sample users on startup)\n- search users by a string that matches their names\n- search users by a age range\n\nTo test the CLI, you can also try the `users-server` I have running on my\ncluster (see the users-grpc Helm config files in\n[maelvls/k.maelvls.dev](https://github.com/maelvls/k.maelvls.dev/tree/master/helm)).\nYou can reach the server at \u003cusers-server.k.maelvls.dev:443\u003e by running:\n\n```sh\necho \"address: users-server.k.maelvls.dev:443\" \u003e\u003e ~/.users-cli.yml\nusers-cli list\n```\n\nExamples with `users-cli`:\n\n```sh\n$ users-cli create --email=mael.valais@gmail.com --firstname=\"Maël\" --lastname=\"Valais\" --postaladdress=\"Toulouse\"\n\n$ users-cli get mael.valais@gmail.com\nMaël Valais \u003cmael.valais@gmail.com\u003e (0 years old, address: Toulouse)\n\n$ users-cli list\nAcevedo Quinn \u003cacevedo.quinn@email.us\u003e (22 years old, address: 403 Lawn Court, Walland, Federated States Of Micronesia, 8260)\nAlford Cole \u003calford.cole@email.net\u003e (33 years old, address: 763 Halleck Street, Elbert, Nevada, 3291)\nAngeline Stokes \u003cangeline.stokes@email.biz\u003e (48 years old, address: 526 Java Street, Hailesboro, Pennsylvania, 1648)\nBeasley Byrd \u003cbeasley.byrd@email.io\u003e (56 years old, address: 213 McKibbin Street, Veguita, New Jersey, 3943)\nBenjamin Frazier \u003cbenjamin.frazier@email.net\u003e (31 years old, address: 289 Cyrus Avenue, Templeton, Maine, 5964)\nBillie Norton \u003cbillie.norton@email.io\u003e (28 years old, address: 699 Rapelye Street, Dupuyer, Ohio, 4175)\n...\nStone Briggs \u003cstone.briggs@email.info\u003e (31 years old, address: 531 Atkins Avenue, Neahkahnie, Tennessee, 3981)\nValencia Dorsey \u003cvalencia.dorsey@email.info\u003e (51 years old, address: 941 Merit Court, Grill, Mississippi, 4961)\nWalter Prince \u003cwalter.prince@email.co.uk\u003e (26 years old, address: 204 Ralph Avenue, Gibbsville, Michigan, 6698)\nWilkerson Mosley \u003cwilkerson.mosley@email.biz\u003e (48 years old, address: 734 Kosciusko Street, Marbury, Connecticut, 3037)\n\n$ users-cli search --name=alenc\nJenifer Valencia \u003cjenifer.valencia@email.us\u003e (52 years old, address: 948 Jefferson Street, Guthrie, Louisiana, 2483)\nValencia Dorsey \u003cvalencia.dorsey@email.info\u003e (51 years old, address: 941 Merit Court, Grill, Mississippi, 4961)\n\n$ users-cli search --agefrom=30 --ageto=42\nBenjamin Frazier \u003cbenjamin.frazier@email.net\u003e (31 years old, address: 289 Cyrus Avenue, Templeton, Maine, 5964)\nStone Briggs \u003cstone.briggs@email.info\u003e (31 years old, address: 531 Atkins Avenue, Neahkahnie, Tennessee, 3981)\nAlford Cole \u003calford.cole@email.net\u003e (33 years old, address: 763 Halleck Street, Elbert, Nevada, 3291)\nBrock Stanley \u003cbrock.stanley@email.me\u003e (35 years old, address: 748 Aster Court, Elwood, Guam, 7446)\nIna Perkins \u003cina.perkins@email.me\u003e (35 years old, address: 899 Miami Court, Temperanceville, Virginia, 2821)\nHardin Patton \u003chardin.patton@email.com\u003e (42 years old, address: 241 Russell Street, Robinson, Oregon, 9576)\n```\n\nHere is what the help looks like:\n\n```sh\n$ users-cli help\n\nFor setting the address of the form HOST:PORT, you can\n- use the flag --address=:8000\n- or use the env var ADDRESS\n- or you can set 'address: localhost:8000' in $HOME/.users-cli.yml\n\nUsage:\n  users-cli [command]\n\nAvailable Commands:\n  create      creates a new user\n  get         prints an user by its email (must be exact, not partial)\n  help        Help about any command\n  list        lists all users\n  search      searches users from the remote users-server\n  version     Print the version and git commit to stdout\n\nFlags:\n      --address string   'host:port' to bind to (default \":8000\")\n      --config string    config file (default is $HOME/.users-cli.yaml)\n  -h, --help             help for users-cli\n  -v, --verbose          verbose output\n\nUse \"users-cli [command] --help\" for more information about a command.\n```\n\n## Install\n\n### Docker images\n\nDocker images are created on each tag. The 'latest' tag represents the\nlatest commit on master. I use multi-stages dockerfile so that the\nresulting image is less that 20MB (using Alpine/musl-libc). `latest` tag\nshould only be used for dev purposes as it points to the image of the\nlatest commit. I use [moving-tags][] `1`, `1.0` and fixed tag `1.0.0` (for\nexample). To run the server on port 8123 locally:\n\n```sh\n$ docker run -e LOG_FORMAT=text -e PORT=8123 -p 80:8123/tcp --rm -it maelvls/users-grpc:1\nINFO[0000] serving on port 8123 (version 1.1.0)\n```\n\n[moving-tags]: http://plugins.drone.io/drone-plugins/drone-docker/#autotag\n\nTo run `users-cli`:\n\n```sh\ndocker run --rm -it maelvls/users-grpc:1 users-cli --address=192.168.99.1:80 list\n```\n\n\u003e This 172.17.0.1 address is required because communicating between\n\u003e containers through the host requires to use the IP of the docker0\n\u003e interface instead of the loopback.\n\n### Binaries (Github Releases)\n\nBinaries `users-cli` and `users-server` are available on the [Github\nReleases page][github-releases].\n\nReleasing binaries was not necessary (except maybe for the CLI client) but\nI love the idea of Go (so easy to cross-compile + one single\nstatically-linked binary) so I wanted to try it. Goreleaser is a fantastic\ntool for that purpose! That's where Go shines: tooling. It is exceptional\n(except for [gopls][]n the Go Language Server) but it's getting better and\nbetter). Most importantly, tooling is fast at execution and also at\ncompilation (contrary to Rust where compilation takes much more time --\nLLVM + way richer and complex language -- see my comparison\n[rust-vs-go][]).\n\n[github-releases]: https://github.com/maelvls/users-grpc/releases\n[gopls]: https://github.com/golang/go/wiki/gopls\n[rust-vs-go]: https://github.com/maelvls/rust-chat\n\n### Using go-get\n\n```sh\ngo get github.com/maelvls/users-grpc/cmd/...\n```\n\n### Kubernetes \u0026 Helm\n\nI use Helm 3 in this example. See [below](#using-the-helm-chart) for an\nexample with a Trafik ingress and cert-manager.\n\n```sh\nhelm repo add maelvls https://maelvls.dev/helm-charts \u0026\u0026 helm repo update\nhelm upgrade --install maelvls/users-grpc --create-namespace --namespace users-grpc --set image.tag=1.1.1\n```\n\n## Develop and hack it\n\nHere is the minimal set of things you need to get started for hacking this\nproject:\n\n```sh\ngit clone https://github.com/maelvls/users-grpc\ncd users-grpc/\n\nbrew install protobuf # only if .proto files are changed\ngo generate ./...     # only if .proto files are changed\n\ngo run ./cmd/users-server \u0026\ngo run ./cmd/users-cli\n```\n\n### Testing\n\nI wrote two kinds of tests:\n\n- Unit tests to make sure that the database logic works as expected. Tests\n  are wrapped in transactions which are rolled back after the test. I use\n  [gotests](https://github.com/cweill/gotests) for easing the TDD workflow.\n  Whenever I add a new function, I just have to run `go run\n  github.com/cweill/gotests/gotests -all -w pkg/service/*`.\n\n  To run the unit tests:\n\n  ```sh\n  go test ./... -short\n  ```\n\n- End-to-end tests where both the CLI and server are built and run. These\n  tests check the user-facing behaviors, e.g., that the CLI arguments work\n  as expected and that the CLI returns the expected exit code. To run those:\n\n  ```sh\n  go test ./test/e2e\n  ```\n\nI used [gomock][] for mocking the behavior of the \"user service\" when testing\nthe GRPC endpoints. I also used Gomega's gexec package just for easing the\nprocess of creating binaries for the end-to-end tests.\n\nYou might notice two different testing libraries being used:\n[testify](https://github.com/stretchr/testify) and\n[go-testdeep](https://github.com/maxatome/go-testdeep). Testify is quite\nstandard (and that's why I used it in the e2e tests), but the go-testdeep is better is some ways:\n\n- go-testdeep has colors (including with the diffs), testify doesn't,\n- go-testdeep \"expected\" and \"got\" parameters are in the correct order:\n\n  ```go\n  // testify is confusing:\n  assert.Equal(t, expected, got)\n  assert.Contains(t, got, expected) // Inverted?\n  assert.NoError(t, got) // Inverted too?\n\n  // go-testdeep is more consistent:\n  td.Cmd(t, got, expected)\n  td.CmpNoError(t, got)\n  ```\n\n- ~~one caveat with go-testdeep though: it doesn't show which error was encountered when running `td.CmpNoError`~~. The issue seems to be gone; the type of the expected error is shown. The author of go-testdeep was [very helpful](https://github.com/maelvls/users-grpc/issues/71#issue-757762725) with this, thanks to him!\n\nOn top of all the current testing, it would be good to add a \"deploy\"\nend-to-end suite that would test the helm chart.\n\n### Develop using Docker\n\n```sh\ndocker build . -f ci/Dockerfile --tag maelvls/users-grpc\n```\n\nIn order to debug docker builds, you can stop the build process before the\nbare-alpine stage by doing:\n\n```sh\ndocker build . -f ci/Dockerfile --tag maelvls/users-grpc --target=builder\n```\n\nYou can test the service is running correctly by using\n[`grpc-health-probe`][grpc-health-probe] (note that I also ship\n`grpc-health-probe` in the docker image so that liveness and readiness\nchecks are easy to do from kubenertes):\n\n```sh\n$ PORT=8000 go run ./cmd/users-server \u0026\n$ go get github.com/grpc-ecosystem/grpc-health-probe\n$ grpc-health-probe -addr=:8000\n\nstatus: SERVING\n```\n\nFrom the docker container itself:\n\n```sh\n$ docker run --rm -d --name=users-grpc maelvls/users-grpc:1\n$ docker exec -i users-grpc grpc-health-probe -addr=:8000\n\nstatus: SERVING\n\n$ docker kill users-grpc\n```\n\nFor building the CLI, I used the cobra cli generator:\n\n```sh\ngo get github.com/spf13/cobra/cobra\n```\n\nUsing Uber's [prototool][], we can debug the gRPC server (a bit like when\nwe use `httpie` or `curl` for HTTP REST APIs). I couple it with [`jo`][jo]\nwhich eases the process of dealing with JSON on the command line:\n\n```sh\n$ prototool grpc --address :8000 --method user.UserService/GetByEmail --data \"$(jo email='valencia.dorsey@email.info')\" | jq\n\n{\n  \"status\": {\n    \"code\": \"SUCCESS\"\n  },\n  \"user\": {\n    \"id\": \"5cfdf218f7efd273906c5b9e\",\n    \"age\": 51,\n    \"name\": {\n      \"first\": \"Valencia\",\n      \"last\": \"Dorsey\"\n    },\n    \"email\": \"valencia.dorsey@email.info\",\n    \"phone\": \"+1 (906) 568-2594\",\n    \"address\": \"941 Merit Court, Grill, Mississippi, 4961\"\n  }\n}\n```\n\nOr you can use grpcurl:\n\n```sh\n# Oneliner when you are stuck execing in some container...\ncurl -L https://github.com/fullstorydev/grpcurl/releases/download/v1.7.0/grpcurl_1.7.0_$(uname -s | tr '[:upper:]' '[:lower:]')_x86_64.tar.gz | tar xz \u0026\u0026 install grpcurl /usr/local/bin\n\n# Or inside your cluster:\nkubectl run foo -it --rm --image=fullstorydev/grpcurl\n```\n\n[grpc-health-probe]: https://github.com/grpc-ecosystem/grpc-health-probe\n[prototool]: https://github.com/uber/prototool\n[jo]: https://github.com/jpmens/jo\n\n## Technical notes\n\n### Vendor or not vendor and go 1.11 modules\n\nI use `GO111MODULES=on`! (see my [blog\npost](https://dev.to/maelvls/why-is-go111module-everywhere-and-everything-about-go-modules-24k)\nabout Go modules) In the first iterations of this project, I was vendoring\n(using `go mod vendor`) and checked the vendor/ folder in with the code.\nThen, I realized things have evolved and it is not necessary anymore (as of\njune 2019; see [should-i-vendor][] as things may evolve).\n\nThat said, I often use `go mod vendor` which comes very handy (I can browse the\ndependencies sources easily, everything is at hand).\n\n[should-i-vendor]: https://www.reddit.com/r/golang/comments/9ai79z/correct_usage_of_go_modules_vendor_still_connects/\n\n### `users-cli version`\n\nAt build time, I use `-ldflags` for setting global variables\n(`main.version` (), `main.date` (RFC3339) and `main.commit`). At first, I\nwas using [govvv][] to ease the process. I then realized govvv didn't help\nas much as I thought; instead, if I want to have a build containing this\ninformation, I use `-ldflags` manually (in Dockerfile for example). For\nbinaries puloaded to Github Releases, [`goreleaser`][goreleaser] handles\nthat for me. For example, a manual build looks like:\n\n```hs\ngo build -ldflags \"-X main.version='$(git describe --tags --always | sed 's/^v//')' -X main.commit=$(git rev-parse --short HEAD) -X main.date=$(date --rfc-3339=date)\" ./...\n```\n\n\u003e Note: for some reason, `-X main.date='$DATE'` cannot accept spaces in\n\u003e `$DATE` even though I use quoting. I'll have to investigate further.\n\n[govvv]: https://github.com/ahmetb/govvv\n[goreleaser]: https://github.com/goreleaser/goreleaser\n\n### Protobuf generation\n\nIdeally, the `.proto` and the generated `.pb.go` should be separated from\nmy service, e.g. `github.com/maelvls/schema` with semver versionning and\nauto-generated `.pb.go` by the CI (see this [SO\ndiscussion](proto-monorepo)). Or maybe the `.pb.go` should be owned by\ntheir respective services... Depending on the use of GO111MODULES or `dep`.\n\nFor `*.pb.go` generation, I use the annotation `//go:generate protoc`. In order\nto re-generate the pb files from the proto files, don't forget to do:\n\n```sh\ngo generate ./...\n```\n\n[proto-monorepo]: https://stackoverflow.com/questions/55250716/organization-of-protobuf-files-in-a-microservice-architecture\n\n### Logs, debug and verbosity\n\nThe client outputs human-friendly messages; the server can either output\n[logfmt](https://brandur.org/logfmt) or json for its logs, and has a `-v`\nflag for cranking up the verbosity. A step further (that I did not\nimplement yet) is to log all gRPC handlers activity (through gRPC\ninterceptors). One way of doing that is proposed in [go-grpc-middleware][].\n\n### Moved from Traefik to Nginx\n\nInitially, I used Traefik pretty much\n[everywhere](https://maelvls.dev/avoid-gke-lb-with-hostport/). The reason I\nchose Traefik is its ease of use and the fact that it embeds an Ingress\ncontroller, which means the support for the Ingress objets is first-class.\n\nWhile trying to use TLS passthrough using the SNI as the routing\ninformation for both gRPC and Websockets, I realized that Traefik (both v1\nand v2) are just too limited in many ways.\n\n1. Traefik v1 did not support TCP connections; it was only\n   [added](https://github.com/traefik/traefik/pull/4587) in late 2019 in\n   Traefik v2. Unfortunately, Traefik v2 totally changed the ingress\n   annotations API.\n2. Traefik v2 brings support to TLS passthrough; the KubernetesCRD (the\n   name given to its Kubernetes provider) makes it available through the\n   IngressRouteTCP kind. For example:\n\n   ```yaml\n   apiVersion: traefik.containo.us/v1alpha1\n   kind: IngressRouteTCP\n   metadata:\n     name: users-grpc\n     namespace: users-grpc\n   spec:\n     entryPoints:\n       - websecure\n     routes:\n     - match: HostSNI(`users-server.k.maelvls.dev`)\n       services:\n       - name: grpc\n         port: 8000\n       passthrough: true\n   ```\n\n   One major problem is that these new CRDs are not supported by other\n   tools like cert-manager. Usually, cert-manager creates a secret named\n   `mytls` when I have an ingress of the form:\n\n   ```yaml\n   kind: Ingress\n   spec:\n     # skipped some fields\n     tls:\n     - hosts:\n       - some.k.maelvls.dev\n       secretName: mytls\n   ```\n\n   The work around is to create the cert-manager's certificate manually.\n\n   The major issue is that I\n   [use](https://github.com/maelvls/k.maelvls.dev)\n   [k8s_gateway](https://coredns.io/explugins/k8s_gateway/) to get names\n   for each of my ingresses. I have a secondary CoreDNS; I delegate the\n   zone `k.maelvls.dev` to it and it watches the ingresses `hosts` field to\n   create `A` records.\n\n   So I decided to skip Traefik altogether. CRDs isn't a good option when\n   most tools don't integrate with them.\n\n   \u003e Note that ExternalDNS also [does not\n   \u003e support](https://github.com/traefik/traefik/issues/4655) these new\n   \u003e CRDs as of November 2020. But since I only use ExternalDNS for my\n   \u003e ingress (Traefik), this does not impact me.\n\n   I thought about using Caddy v2 but its ingress controller is still a\n   [work in progress](https://github.com/caddyserver/ingress) as of\n   November 2020. So I just went with the widely used Nginx. Its ingress\n   controller have a ton of useful annotations such as `ssl-passthrough`.\n   [Not\n   perfect](https://kubernetes.github.io/ingress-nginx/user-guide/tls/#ssl-passthrough),\n   but at least it does what I need:\n\n   \u003e This feature is implemented by intercepting all traffic on the\n   \u003e configured HTTPS port (default: 443) and handing it over to a local\n   \u003e TCP proxy. This bypasses NGINX completely and introduces a\n   \u003e non-negligible performance penalty.\n\n   To be honest, I wish Traefik v2 was supporting a \"legacy\" mode where\n   each IngressRoute would be mirrored with an Ingress object (see\n   [5865](https://github.com/traefik/traefik/issues/5865)). The Ingress\n   object would be created with a special ingress class such as\n\n   ```yaml\n   kubernetes.io/ingress.class: dummy\n   ```\n\n   Anyway, I found a workaround. I create both the Ingress (so that's going\n   to be listening for https and redirecting http to https) and the\n   IngressRouteTCP. My understanding is that the TCP routing happens before\n   the HTTP routing.\n\n   As a recap, here is the config to use:\n\n   ```yaml\n   # users-grpc-extras.yaml\n   apiVersion: traefik.containo.us/v1alpha1\n   kind: IngressRouteTCP\n   metadata:\n     name: users-grpc\n     namespace: users-grpc\n     annotations:\n       fake-ingress: \"true\"\n   spec:\n     entryPoints:\n       - websecure\n     routes:\n       - match: HostSNI(`users-server.k.maelvls.dev`)\n         services:\n           - name: users-grpc\n             port: 8000\n     tls:\n       passthrough: true\n   ```\n\n   ```yaml\n   # users-grpc-helm.yaml\n   image:\n    tag: \"1.2.1\"\n   ingress:\n     enabled: true\n     annotations:\n       kubernetes.io/ingress.class: traefik\n       cert-manager.io/cluster-issuer: letsencrypt-prod\n     hosts: [users-server.k.maelvls.dev]\n     tls:\n       - hosts: [users-server.k.maelvls.dev]\n         secretName: tls\n   tls:\n     enabled: true\n     # The secret must contain the fields 'tls.key' and 'tls.crt'.\n     secretName: tls\n   ```\n\n   and then (using Helm 3):\n\n   ```sh\n   kubectl apply -f users-grpc-extras.yaml\n   helm upgrade --install users-grpc maelvls/users-grpc --create-namespace --namespace users-grpc --values helm/users-grpc-helm.yaml\n   ```\n\n## Examples that I read for inspiration\n\n- [go-micro-services][] (lacks tests but excellent geographic-related\n  business case)\n- [route_guide][] (example from the official grpc-go)\n- [go-scaffold][] (mainly for the BDD unit + using Ginkgo)\n- [todogo][] (just for the general layout)\n- [Medium: _Simple API backed by PostgresQL, Golang and\n  gRPC_][medium-grpc-pg] for grpc middleware (opentracing interceptor,\n  prometheus metrics, gRPC-specific logging with logrus, tags\n  retry/failover, circuit-breaking -- alghouth these last two might be\n  better handled by a service proxy such as [linkerd2][])\n- the Go standard library was also extremely useful for learning how to\n  write idiomatic code. The `net` one is a gold mine (on top of that I love\n  all the networking bits).\n\n[medium-grpc-pg]: https://medium.com/@vptech/complexity-is-the-bane-of-every-software-engineer-e2878d0ad45a\n[go-micro-services]: https://github.com/harlow/go-micro-services\n[route_guide]: https://github.com/grpc/grpc-go/tree/master/examples/route_guide\n[go-scaffold]: https://github.com/orbs-network/go-scaffold\n[todogo]: https://github.com/kgantsov/todogo\n[helm-gh-pages-example]: https://github.com/int128/helm-github-pages\n[linkerd2]: https://github.com/linkerd/linkerd2\n\n## Using the Helm chart\n\nIn order to test the deployment of my service, I create a Helm chart (as\nwell as a static `kubernetes.yml` -- which is way less flexible) and used\nminikube in order to test it. I implemented the [grpc-healthcheck][] so that Kubernetes's readyness and\nliveness checks can work with this service. What I did:\n\n1. clean logs in JSON ([logrus][]) for easy integration with Elastic/ELK\n2. health probe working (readiness)\n3. `helm test --cleanup users-grpc` passes\n4. the service can be exposed via an Ingress controller such as Traefik or\n   Nginx. For example, using the Helm + Civo K3s + Terraform configuration at\n   [k.maelvls.dev][]:\n\n   ```yaml\n   # users-grpc.yaml\n   image:\n     tag: 1.1.1\n\n   service:\n     annotations:\n       # Traffic between Traefik and the users-server pod will be left\n       # unencrypted (h2c mode, i.e., HTTP/2 cleartext). This annotation tells\n       # Traefik to try to connect to the upstream users-server using h2c.\n       # https://doc.traefik.io/traefik/master/routing/providers/kubernetes-ingress/\n       traefik.ingress.kubernetes.io/service.serversscheme: h2c\n\n   ingress:\n     enabled: true\n     hosts: [users-server.k.maelvls.dev]\n     annotations:\n       kubernetes.io/ingress.class: traefik\n       cert-manager.io/cluster-issuer: letsencrypt-prod\n\n     tls:\n       - hosts: [users-server.k.maelvls.dev]\n         secretName: tls\n   ```\n\n   We can then have the service from the internet through Traefik (Ingress\n   Controller) with dynamic per-endpoint TLS ([cert-manager][]) and DNS\n   ([external-dns][]).\n\n   The helm chart is available at \u003chttps://maelvls.dev/helm-charts\u003e and are\n   updated on every tag by the CI. Note that the `image` tag may be out of\n   date!\n\n   ```sh\n   helm repo add maelvls https://maelvls.dev/helm-charts \u0026\u0026 helm repo update\n   helm upgrade --install maelvls/grpc-users --name users-grpc --create-namespace --namespace users-grpc --values users-grpc.yaml\n   ```\n\n[k.maelvls.dev]: https://github.com/maelvls/k.maelvls.dev\n[grpc-healthcheck]: https://github.com/grpc/grpc/blob/master/doc/health-checking.md\n[logrus]: https://github.com/sirupsen/logrus\n[external-dns]: https://github.com/kubernetes-incubator/external-dns\n[cert-manager]: https://github.com/jetstack/cert-manager\n\nTo bootstrap the kubernetes YAML configuration for this service using my\nHelm chart, I use:\n\n```sh\nhelm template users-grpc ./ci/helm/users-grpc --create-namespace --namespace users-grpc --set image.tag=latest \u003e ci/deployment.yml\n```\n\nWe can now apply the configuration without using Helm. Note that I changed\nthe ClusterIP to NodePort so that no LoadBalancer, Ingress Controller nor\n`kubectl proxy` is needed to access the service.\n\n```sh\n$ kubectl apply -f ci/deployment.yml\n$ kubectl get svc users-grpc\nNAME        TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE\nusers-grpc   NodePort   10.110.71.154   \u003cnone\u003e        8000:32344/TCP   15m\n```\n\nNow, in order to access it, we must retrieve the minikube cluster IP (i.e.,\nits service IP, the IP used by kubectl for sending commands).\n\n```sh\n$ minikube status\nkubectl: Correctly Configured: pointing to minikube-vm at 192.168.99.105\n```\n\nWe then use the address `192.168.99.105:32344`. Let's try with\n[grpc-health-probe][]:\n\n```sh\n% grpc-health-probe -addr=192.168.99.105:32344\nstatus: SERVING\n```\n\nYey!! 🎉🎉🎉\n\n## Updating \u0026 uploading the Helm charts\n\nTo update the helm chart served at \u003chttps://maelvls.dev/helm-charts\u003e, I use\nthe drone.io's \"build promoting\" feature with\n[chart-releaser](https://github.com/helm/chart-releaser). Make sure to\nupdate the chart version at `ci/helm/users-grpc/Chart.yaml`, push the\nchanges, wait until the CI build is done and then either (1) go to the\nDrone UI and click \"Deploy\" and use the target \"helm\", or use the\nCLI:\n\n```sh\nbrew install drone\ndrone build ls maelvls/users-grpc --event push --limit 1\n# Copy the build ID, e.g., \"305\".\nexport DRONE_TOKEN=...\ndrone build promote maelvls/users-grpc 305 helm\n```\n\n## Future work\n\nHere is a small list of things that could be implemented now that a MVP\nmicroservice is working.\n\n### Using an on-disk database\n\nNow that the \"service\" part can be unit-tested thanks to the transaction\nrollback mechanism, it would be quite easy to move the project from\n[go-memdb](https://github.com/hashicorp/go-memdb) (in-memory database) to\npostgres. I started doing just that in [this\nPR](https://github.com/maelvls/users-grpc/pull/65).\n\n### Distributed tracing and logs\n\n- Jaeger: very nice for debugging a cascade of gRPC calls. It requires a\n  gRPC interceptor compatible with Opentracing.\n- Logs: [logrus][] can log every request or only failing requests, and this can\n  be easily implemented using a gRPC interceptor (again!)\n\nThese middlewares are listed and available at [go-grpc-middleware][].\n\n[go-grpc-middleware]: https://github.com/grpc-ecosystem/go-grpc-middleware\n\n### Publishing Helm chart to Github Pages and publishing to Homebrew\n\nI could publish the `users-cli` and `users-server` as a Homebrew tag, e.g.\nat \u003chttps://github.com/maelvls/homebrew-tap\u003e.\n\n## Design discussion\n\n\u003e Why use gRPC versus a simple REST API?\n\nInitially, this project was the result of a tech test that required\ncandidates to rely on gRPC. And after playing with a real deployment of the\n`users-server`, I think that there was no good reason to go with gRPC: I\ndon't have any performance requirement, which means I only get the \"cons\"\nof gRPC (hard to debug on the wire), and not much benefit (except for the\nfact that the API spec is formally describe thanks to the protobuf spec and\nthe client and server implementations are auto-generated). Next time, I'll\nprobably go with a simple REST API (or JSON-RPC if this service isn't\nreally a web service; e.g. like gopls which is exposed using a UNIX or TCP\nsocket).\n\n\u003e Why use an in-memory database (go-memdb) over an on-disk database like\n\u003e Postgres?\n\nSince I initially had less than a week to learn Go and finish this tech\ntest, I needed a quick way of storing things. Integrating with Postgres\nfelt like a burden, I had to make sure everything was well tested and that\nthe app would be deployable. I would also have made the unit-testing part\nharder since I unit test using a real DB instance (see below). But I made\nsure I could still use Postgres in my unit tests using transactions passed\nto the \"service\" functions (e.g., `AddUser`) in order to make them testable\nwith a rollback mechanism. Each unit test would:\n\n1. Start a transaction,\n2. Insert some sample data,\n3. Run the unit test, e.g. `Test_AddUser`,\n4. Rollback.\n\nThis way, I need a single database and the unit tests do not \"taint\" each\nother. I started working on moving from go-memdb to postgres [in this\nPR](https://github.com/maelvls/users-grpc/pull/65).\n\n\u003e Why are unit tests using a real database implementation?\n\nAs Kent Beck and Ian Cooper often say, the only important requirement on\nunit tests are their speed and reproduciability:\n\n\u003e [Ian Cooper,\n\u003e 2017](https://www.youtube.com/watch?v=EZ05e7EMOLM\u0026feature=youtu.be\u0026t=2100)\n\u003e — We avoid file system, database, simply because these shared fixtures\n\u003e elements prevent us running in isolation from other tests, or cause our\n\u003e tests to be slow. [...] If there is no shared fixture problem, it is\n\u003e perfectly fine in a unit test to talk to a database or a file system.\n\nI don't really mind if my unit tests depend on running a `docker run\npostgres`; I just want them to be fast and each test case isolated from\neach other using a transaction-rollback. And in the case of the \"service\"\nlayer (e.g., `AddUser`), I think that the SQL queries should be tested\ninstead of being mocked.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaelvls%2Fusers-grpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaelvls%2Fusers-grpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaelvls%2Fusers-grpc/lists"}