├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── acctest ├── key.pem ├── main_test.go ├── stubserver.go └── x509 │ └── certificate.pem ├── main.go └── main_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE and build system artifacts 2 | *.dll 3 | *.dylib 4 | *.exe 5 | *.iml 6 | *.so 7 | *.test 8 | .idea/ 9 | .protoc 10 | gprobe 11 | release/ 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.0 - 2018-01-30 4 | 5 | ### Added 6 | 7 | - TLS support 8 | 9 | `--tls` verify server with CA certificates installed on this system 10 | `--tls-insecure` do NOT verify server (accept any certificate) 11 | `--tls-cafile value` verify server with CA certificate stored in specified file 12 | `--tls-capath value` verify server with CA certificates located under specified path 13 | - Windows build 14 | 15 | ### Changed 16 | 17 | - `--timeout` option now affects both dialing to server and RPC call (before that dialing had hard-coded 1s timeout) 18 | - more descriptive messages are printed out in case of failure 19 | 20 | ## 1.0.0 - 2017-12-13 21 | 22 | ### Added 23 | 24 | - Probing servers and services via gRPC health-checking protocol 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | PUBLIC DOMAIN NOTICE 2 | National Center for Biotechnology Information 3 | 4 | This software/database is a "United States Government Work" under the 5 | terms of the United States Copyright Act. It was written as part of 6 | the author's official duties as a United States Government employee and 7 | thus cannot be copyrighted. This software/database is freely available 8 | to the public for use. The National Library of Medicine and the U.S. 9 | Government have not placed any restriction on its use or reproduction. 10 | 11 | Although all reasonable efforts have been taken to ensure the accuracy 12 | and reliability of the software and data, the NLM and the U.S. 13 | Government do not and cannot warrant the performance or results that 14 | may be obtained by using this software or data. The NLM and the U.S. 15 | Government disclaim all warranties, express or implied, including 16 | warranties of performance, merchantability or fitness for any particular 17 | purpose. 18 | 19 | Please cite the author in any work or product based on this material. 20 | 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY=gprobe 2 | VERSION=$(shell git describe 2>/dev/null || echo "0.0.0") 3 | RELEASE=release 4 | RELEASE_FORMAT=${BINARY}-${GOOS}-${GOARCH}-${VERSION}.tar.gz 5 | 6 | default: bin 7 | .PHONY: bin 8 | bin: deps ${BINARY} 9 | 10 | .PHONY: release 11 | release: release/gprobe-linux-amd64-${VERSION}.tar.gz 12 | release: release/gprobe-linux-386-${VERSION}.tar.gz 13 | release: release/gprobe-darwin-amd64-${VERSION}.tar.gz 14 | release: release/gprobe-windows-amd64-${VERSION}.tar.gz 15 | 16 | .PHONY: release-dir 17 | release-dir: 18 | mkdir -p ${RELEASE} 19 | 20 | ${RELEASE}/%-${VERSION}.tar.gz: | release-dir 21 | GOOS=$(shell echo $* | cut -d '-' -f 2) GOARCH=$(shell echo $* | cut -d '-' -f 3) \ 22 | go build -ldflags="-s -w -X main.version=${VERSION} -v" -o ${RELEASE}/${BINARY} 23 | tar -C ${RELEASE} -czf $@ ${BINARY} 24 | rm ${RELEASE}/${BINARY} 25 | 26 | ${BINARY}: 27 | go build -ldflags="-s -w -X main.version=${VERSION} -v" -o $@ 28 | 29 | .PHONY: lint 30 | lint: 31 | go get -u github.com/golang/lint/golint 32 | golint -set_exit_status ./... 33 | 34 | .PHONY: test 35 | test: 36 | go test -v $(go list ./... | grep -v /acctest/) 37 | 38 | .PHONY: acctest 39 | acctest: ${BINARY} 40 | go test -v ./acctest/... -args -gprobe `pwd`/${BINARY} 41 | 42 | .PHONY: deps 43 | deps: 44 | go get -u ./... 45 | 46 | .PHONY: test-deps 47 | test-deps: 48 | go get -t -u ./... 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -f ${BINARY} 53 | rm -rf ${RELEASE} 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____ ____ 3 | / \/ \ | 4 | | | ,--. ,--. .-- .--. |,--. ,---. 5 | \::::::::::; | | | | | | | | | |---' 6 | `:::::::;' '--| |--' | '--' '---' `--- 7 | `:::;' / | 8 | `' 9 | ``` 10 | 11 | _gprobe_ is a CLI client for the 12 | [gRPC health-checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md). 13 | 14 | ## Usage 15 | 16 | Assuming server is listening on `localhost:1234` 17 | 18 | Check server health (it is considered healthy if it has `grpc.health.v1.Health` service and is able to serve requests) 19 | 20 | ```bash 21 | gprobe localhost:1234 22 | ``` 23 | 24 | Check specific service health 25 | 26 | ```bash 27 | gprobe localhost:1234 my.package.MyService 28 | ``` 29 | 30 | Get help 31 | 32 | ```bash 33 | gprobe -h 34 | ``` 35 | 36 | ## Building from source 37 | 38 | Valid _go_ environment is required to build `gprobe` (`go` is in `PATH`, `GOPATH` is set, etc.). 39 | 40 | Clone code into valid `GOPATH` location 41 | 42 | ```bash 43 | git clone git@github.com:ncbi/gprobe.git $GOPATH/src/github.com/ncbi/gprobe 44 | ``` 45 | 46 | Build distributable tarballs for all OSes 47 | 48 | ```bash 49 | make release 50 | ``` 51 | 52 | Build binary for current OS 53 | 54 | ```bash 55 | make bin 56 | ``` 57 | 58 | ## Development 59 | 60 | This project follows git-flow branching model. All development is done off of the `develop` branch. `HEAD` in 61 | `production` branch should always point to a tagged release. There's no `master` branch to avoid possible confusion. 62 | 63 | To contribute: 64 | 65 | 1. Fork or create a feature branch from the latest `develop`, commit your work there 66 | ```bash 67 | git checkout develop 68 | git pull 69 | git checkout -b feature/ 70 | ``` 71 | 2. Run `go fmt` and all the checks before committing any code 72 | ```bash 73 | go fmt ./... 74 | make lint test acctest 75 | ``` 76 | 3. When the change is ready in a separate commit update `CHANGELOG.md` describing the change. Follow 77 | [keepachangelog](http://keepachangelog.com/en/1.0.0/) guidelines 78 | 4. Create PR to develop 79 | 80 | To release: 81 | 82 | 1. Create a release branch from the latest `develop` and update `CHANGELOG.md` there, setting version and date 83 | ```bash 84 | git checkout -b release/1.2.3 85 | ``` 86 | 2. Create PR to `production` 87 | 3. Once PR is merged, tag `HEAD` commit using annotated tag 88 | ```bash 89 | git tag -a 1.2.3 -m "1.2.3" 90 | ``` 91 | 4. Merge `production` back to `develop`. Do not use `fast-forward` merges 92 | ```bash 93 | git checkout develop 94 | git merge --no-ff production 95 | ``` 96 | -------------------------------------------------------------------------------- /acctest/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDP1dCRnLuw2Jo2 3 | 1GoCOdunHC3uBjUUTI0CGog+9RP2gsNgGdNHM77Htg6Qjrs51PngkYpC5GFpfkbx 4 | 6CaFYpsPOWyJcdUnDnieDhMTHMu7PwHKWf1hgSJxoiNbQKjNH+sJTW+au3pshuoX 5 | JMl0uibis+pyRtfD3MK5wanASlmEC2ldyiEXW8JZgARY94/R7/c7AIMsH1YJq4PR 6 | CJ8pQmaV7xSrlT+/r+bB9JtBNTFkehaC/D1hf7IihcCwlanHgGiVJdPpprX+CASM 7 | 8EVrXNmm55Xq+edSc4yHeb2l8AH8OXrsDfNNwuVv1f70PTmae7WgKdXfLR1avDUf 8 | oDxCPoAoVgncl5e6Wm0zGFVzuJtHl+tgyzFqwduXjnm7MGY/0IODFaS37DKxxaFC 9 | Ff5y1TuaIeOZZBzt1+2RDFw5vOajB/SL76VgM06qi+TW9hCES4jAS6H56KUP0Rpl 10 | MxRCe7mRU+2w8+RuOhJKtjT8YPPWCP3NfvfS/iCsk9OIOuJQycdonXs7pfruvftA 11 | /Y1j8gRhrExauIeIYGIaq4KRolKQDaAUQd2EFg95UVVpTpEZL9bYM/YBDccH+W8B 12 | hgo6N2n0eUEFqiclWFtvAN1vAAMTe1/CjYh41HkqIlxA51rJ+7tekRd8xr2BrKKc 13 | 9Vu1POmTGdrjr30OET8+6jzdL5MwewIDAQABAoICAQCUMKeDe9bUVM04tSJVLf3x 14 | XIVfR2vHaoHMczCce1DdnwVB24grJ7krWyNtbWgP50y4E+4ang7bEl/xko8M4m8f 15 | XtmF8vWB4K6eO/jb0tdtTpKvPpUNVe9CSNKe+S6i+9QxkNY35N94zIXTNLa0FRsu 16 | 4AwVqW+lRx5NJsorwperMBvT9RC9P/8Go+H1sacJkOmeV1IwPrOxN2tIu7YIzECr 17 | PYpmgYev3PNTbl7ZEt2CAA9XHBWEFHHmbaoj/sLM7kEjv5Im8minlf3wpE1LLSw/ 18 | 9raNkdyfjKYx3tsbm1M/DZkZASVvV70Sjeo5KgKNpRGu/sVxWRCqJrJWN4Ff1oK3 19 | dXi1zdoC56mNsqDLaHftFBy6T85UlNJhvXrNxr9JI8SaflCBgLrV1yT+5SkYjc4t 20 | xqAAl18TFk3eZL7Wf4rK6Z9c1NlUW/H44OHgd3sUQMdmbaKhT9eyxlohdMTl7ly/ 21 | l2fczZGoaX4dwNvrw6MQGFC/J4e7x2FARTm6UAbVeJ9hqwx3qY02MgCVO8ZhCdPk 22 | aJ7rxFUIdhYHY84sGN27zxjx4ja7b/NdEB8glVXewPpWuZbngLMgWvlLGsLdQfoS 23 | DZmXT4B6AwA/6DW2tMKMYAv+yud5VmeiXQ9vRkuwT4lHLY9CcoFbl63PIkeeKDHe 24 | +9r7mkbUtmOgiw8qRcDJeQKCAQEA/sU7oKemdKl9HXnheNh+ql3C9/t1OaHkHlKv 25 | g964r2rjIpYhiNS59kmXizA1KEJAmk2BZfV7gSuarbGU8v4WAcQQUEJ/c0aqf02K 26 | 7XfH/dIEf2C3IngTYybN08Eq8uQNyrNsXcIYCEjY9vDedouP0J5kZN+e66Aqxj8/ 27 | dp3nvIt8XL/0eGwq5xa6tJwhX3EzGim34Hv4Ifb0TGeko52fah1oCAfqmYRMJlH6 28 | zJhBhCH7IszlqItx3t6+KEg4dUIx1Yi4nh7XI9zQg+fzssh9kDeBsbpCRnUPat9m 29 | FgCLsZRaQYTW50xt3DTEYS8sGzjfmrhXXHEDek/oZwvZAr1k3wKCAQEA0NaX+hmP 30 | vQvqBhBVYfSH/KAU9JCyF5yWoMJlJOOyb4ryUU1Fs3cAS8vMCUkRcZK/Q7icpm4r 31 | k+3/2BPZeOzWhSOGJTvJ1oSsS9cmuq4MSJDmDXP4G0FS7uAKOj1HUjXwd7dWrHvE 32 | IlW9PrvVNXOLedzA8k45ZCQRBawsLoaBJg64EZsLc87PEECxGiSBiN4aabr191xc 33 | J5IqF51P6D2mUdhnf6GZvBmBTtzs9ocmGEV5Efv/VO/CcgL5kQOOR7jpB0lIIHwF 34 | 2WjLLalEQ1r6iBQSYswXdcTQT5MYX+DxXFv//qePFuaM1O4c3G4ODhJrGHsez1EN 35 | g4uFnKmBoBmr5QKCAQAY3S7gkvwPzqrDQa3bmWVjQxtQEF50bXRR8Ufn2sizdf8M 36 | 1RIYxIoRm0UK9H17nFups365cKfJB3RlFzuuK1YCfhwJeTPvECp7mhnA6zu9bc26 37 | kLnOx2E9AAB+dg+2/MLL0Y7154do55MlJoTPlPdIKO0rWxerb0o9ZtbOwMJpCEPu 38 | 2V0Gk6fsPa+jCMnJAsc1+nRTmEWzKuLUwhizTyLLvGr2va8LpHm6E64iYYmjV52m 39 | 29BeDp3iXmK4k7PO3dL3QAykgeYFPfuro+uIu0Bl3sTtj3wAXFRQ3dScuRjpD81v 40 | L4O5tx/RqeSwh2YKkhZghzUfdHgea8YGqIVZWxqJAoIBAQDGyn8AYzSgD6dE/mdI 41 | RyzrHLbV1qawMy5u+Jyu8M/5vZnMKnIe0zhE7knazOL96WKHZEQ5aMWymurfFIX+ 42 | xfOt6JLY/oCy4rffuX30VZj7unJCfBHAX/5BxKH3rj0l1JKCYtLufSHGTTdHcCUU 43 | LFioN6qy/CNFX8+URsAHyaFGSNyOZbgRFNul7O6oo/dqAYHDA2T/gbt3L3tB300h 44 | FQ4s+oIKzBk7JEwidcpbIWrxz6/fnrD+ePvu60YE9A2L2Eh51xgBVA19VnORk36X 45 | XxL8VZ7qzLvILwDbvnmFSup1sF2OWpGqiuukBMUUTu6yFnY7Z3d8gPsMLNOSvQfX 46 | DpjBAoIBAAGrx9X6JFCz0rE3fSyZVr9+tYVcz0kt+B9a3bN08/szO7c6xBbGeaRS 47 | 5wrTVomMeiQWd5q7xttAB+XhqqnJqoPt+tDJj5CA239lNHFCOv+ayk0f5Luequ2B 48 | hI8ylsa17oPKrPj+wR1WHfMQg6F+hFay29Kz9usQJY0ciXb1pE31Qm28ldnAiWX0 49 | hO98do3+E5mS/XcTuerXTRQoctFaXeYVx5tV6XHYth0kULAdEX5/z6ZvGXFS7K/3 50 | V608CusCn3MsRYjFoRTHOL2LswhM4yGbJBcMg+tVCpS6K4nwBCbf/+ro/1oKrm8f 51 | LiCYg6qO9tI/KXTUZjr6VMtlZ13CAEY= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /acctest/main_test.go: -------------------------------------------------------------------------------- 1 | // PUBLIC DOMAIN NOTICE 2 | // National Center for Biotechnology Information 3 | // 4 | // This software/database is a "United States Government Work" under the 5 | // terms of the United States Copyright Act. It was written as part of 6 | // the author's official duties as a United States Government employee and 7 | // thus cannot be copyrighted. This software/database is freely available 8 | // to the public for use. The National Library of Medicine and the U.S. 9 | // Government have not placed any restriction on its use or reproduction. 10 | // 11 | // Although all reasonable efforts have been taken to ensure the accuracy 12 | // and reliability of the software and data, the NLM and the U.S. 13 | // Government do not and cannot warrant the performance or results that 14 | // may be obtained by using this software or data. The NLM and the U.S. 15 | // Government disclaim all warranties, express or implied, including 16 | // warranties of performance, merchantability or fitness for any particular 17 | // purpose. 18 | // 19 | // Please cite the author in any work or product based on this material. 20 | 21 | package acctest 22 | 23 | import ( 24 | "bytes" 25 | "flag" 26 | "fmt" 27 | "github.com/stretchr/testify/assert" 28 | "io" 29 | "log" 30 | "os" 31 | "os/exec" 32 | "syscall" 33 | "testing" 34 | 35 | hv1 "google.golang.org/grpc/health/grpc_health_v1" 36 | ) 37 | 38 | var ( 39 | port int 40 | caFile string 41 | caPath string 42 | key string 43 | bin string 44 | stubSrvAddr string 45 | ) 46 | 47 | func init() { 48 | flag.IntVar(&port, "stub-port", 54321, "port for the stub server") 49 | flag.StringVar(&caFile, "stub-cafile", "x509/certificate.pem", "path to the x509 certificate file") 50 | flag.StringVar(&caPath, "stub-capath", "x509/", "path to the x509 certificates dir") 51 | flag.StringVar(&key, "stub-key", "key.pem", "path to the stub server private key") 52 | flag.StringVar(&bin, "gprobe", "../gprobe", "path to the gprobe binary") 53 | } 54 | 55 | func TestMain(m *testing.M) { 56 | flag.Parse() 57 | stubSrvAddr = fmt.Sprintf("%s:%d", "localhost", port) 58 | os.Exit(m.Run()) 59 | } 60 | 61 | func TestShouldReturnServingForRunningServer(t *testing.T) { 62 | // given 63 | srv, _, err := StartInsecureServer(port) 64 | if err != nil { 65 | log.Fatalf("can't start stub server: %v", err) 66 | } 67 | defer srv.GracefulStop() 68 | 69 | // when 70 | stdout, stderr, exitcode := runBin(t, stubSrvAddr) 71 | 72 | assert.Equal(t, 0, exitcode) 73 | assert.Equal(t, "SERVING\n", stdout) 74 | assert.Empty(t, stderr) 75 | } 76 | 77 | func TestShouldFailIfServerIsNotListening(t *testing.T) { 78 | // given no server 79 | 80 | // when 81 | stdout, stderr, exitcode := runBin(t, stubSrvAddr) 82 | 83 | // then 84 | assert.Equal(t, 127, exitcode) 85 | assert.Empty(t, stdout) 86 | assert.Contains(t, stderr, "application isn't listening") 87 | } 88 | 89 | func TestShouldFailIfServerDoesNotImplementHealthCheckProtocol(t *testing.T) { 90 | // given 91 | srv, err := StartEmptyServer(port) 92 | if err != nil { 93 | log.Fatalf("can't start stub server: %v", err) 94 | } 95 | defer srv.GracefulStop() 96 | 97 | // when 98 | stdout, stderr, exitcode := runBin(t, stubSrvAddr) 99 | 100 | // then 101 | assert.Equal(t, 127, exitcode) 102 | assert.Empty(t, stdout) 103 | assert.Equal(t, stderr, "rpc error: server doesn't implement gRPC health-checking protocol\n") 104 | } 105 | 106 | func TestShouldReturnServingForHealthyService(t *testing.T) { 107 | // given 108 | srv, svc, err := StartInsecureServer(port) 109 | if err != nil { 110 | log.Fatalf("can't start stub server: %v", err) 111 | } 112 | defer srv.GracefulStop() 113 | svc.SetServingStatus("foo", hv1.HealthCheckResponse_SERVING) 114 | 115 | // when 116 | stdout, stderr, exitcode := runBin(t, stubSrvAddr, "foo") 117 | 118 | // then 119 | assert.Equal(t, 0, exitcode) 120 | assert.Equal(t, "SERVING\n", stdout) 121 | assert.Empty(t, stderr) 122 | } 123 | 124 | func TestShouldReturnNotServingForUnhealthyService(t *testing.T) { 125 | // given 126 | srv, svc, err := StartInsecureServer(port) 127 | if err != nil { 128 | log.Fatalf("can't start stub server: %v", err) 129 | } 130 | defer srv.GracefulStop() 131 | svc.SetServingStatus("foo", hv1.HealthCheckResponse_NOT_SERVING) 132 | 133 | // when 134 | stdout, stderr, exitcode := runBin(t, stubSrvAddr, "foo") 135 | 136 | // then 137 | assert.Equal(t, 2, exitcode) 138 | assert.Equal(t, "NOT_SERVING\n", stdout) 139 | assert.Contains(t, stderr, "health-check failed") 140 | } 141 | 142 | func TestShouldNotFailForUnhealthyServiceIfNoFailIsSet(t *testing.T) { 143 | // given 144 | srv, svc, err := StartInsecureServer(port) 145 | if err != nil { 146 | log.Fatalf("can't start stub server: %v", err) 147 | } 148 | defer srv.GracefulStop() 149 | svc.SetServingStatus("foo", hv1.HealthCheckResponse_NOT_SERVING) 150 | 151 | // when 152 | stdout, stderr, exitcode := runBin(t, "--no-fail", stubSrvAddr, "foo") 153 | 154 | // then 155 | assert.Equal(t, 0, exitcode) 156 | assert.Equal(t, "NOT_SERVING\n", stdout) 157 | assert.Empty(t, stderr) 158 | } 159 | 160 | func TestShouldFailIfServiceHealthCheckIsNotRegistered(t *testing.T) { 161 | // given 162 | srv, _, err := StartInsecureServer(port) 163 | if err != nil { 164 | log.Fatalf("can't start stub server: %v", err) 165 | } 166 | defer srv.GracefulStop() 167 | 168 | // when 169 | stdout, stderr, exitcode := runBin(t, stubSrvAddr, "my.service.Foo") 170 | 171 | // then 172 | assert.Equal(t, 127, exitcode) 173 | assert.Empty(t, stdout) 174 | assert.Equal(t, stderr, "rpc error: unknown service my.service.Foo\n") 175 | } 176 | 177 | // TLS tests 178 | 179 | func TestShouldFailOnTlsVerificationWithSelfSignedCert(t *testing.T) { 180 | // given 181 | srv, _, err := StartServer(port, caFile, key) 182 | if err != nil { 183 | log.Fatalf("can't start stub server: %v", err) 184 | } 185 | defer srv.GracefulStop() 186 | 187 | // when 188 | stdout, stderr, exitcode := runBin(t, "--tls", stubSrvAddr) 189 | 190 | // then 191 | assert.Equal(t, 127, exitcode) 192 | assert.Empty(t, stdout) 193 | assert.Contains(t, stderr, "TLS handshake failed") 194 | } 195 | 196 | func TestShouldBeAbleToSkipTlsVerification(t *testing.T) { 197 | // given 198 | srv, _, err := StartServer(port, caFile, key) 199 | if err != nil { 200 | log.Fatalf("can't start stub server: %v", err) 201 | } 202 | defer srv.GracefulStop() 203 | 204 | // when 205 | stdout, stderr, exitcode := runBin(t, "--tls-insecure", stubSrvAddr) 206 | 207 | // then 208 | assert.Equal(t, 0, exitcode) 209 | assert.Equal(t, "SERVING\n", stdout) 210 | assert.Empty(t, stderr) 211 | } 212 | 213 | func TestShouldBeAbleToSetCustomCAFile(t *testing.T) { 214 | // given 215 | srv, _, err := StartServer(port, caFile, key) 216 | if err != nil { 217 | log.Fatalf("can't start stub server: %v", err) 218 | } 219 | defer srv.GracefulStop() 220 | 221 | // when 222 | stdout, stderr, exitcode := runBin(t, "--tls-cafile", caFile, stubSrvAddr) 223 | 224 | // then 225 | assert.Equal(t, 0, exitcode) 226 | assert.Equal(t, "SERVING\n", stdout) 227 | assert.Empty(t, stderr) 228 | } 229 | 230 | func TestShouldBeAbleToSetCustomCAPath(t *testing.T) { 231 | // given 232 | srv, _, err := StartServer(port, caFile, key) 233 | if err != nil { 234 | log.Fatalf("can't start stub server: %v", err) 235 | } 236 | defer srv.GracefulStop() 237 | 238 | // when 239 | stdout, stderr, exitcode := runBin(t, "--tls-capath", caPath, stubSrvAddr) 240 | 241 | // then 242 | assert.Equal(t, 0, exitcode) 243 | assert.Equal(t, "SERVING\n", stdout) 244 | assert.Empty(t, stderr) 245 | } 246 | 247 | func runBin(t *testing.T, args ...string) (stdout string, stderr string, exitcode int) { 248 | gprobe := exec.Command(bin, args...) 249 | stdoutPipe, _ := gprobe.StdoutPipe() 250 | stderrPipe, _ := gprobe.StderrPipe() 251 | 252 | err := gprobe.Start() 253 | if err != nil { 254 | t.Error(err) 255 | } 256 | 257 | stdout = readPipe(t, stdoutPipe) 258 | stderr = readPipe(t, stderrPipe) 259 | exitcode = waitForExitCode(t, gprobe) 260 | 261 | return 262 | } 263 | 264 | func readPipe(t *testing.T, reader io.Reader) string { 265 | buf := new(bytes.Buffer) 266 | _, err := io.Copy(buf, reader) 267 | if err != nil { 268 | t.Error(err) 269 | } 270 | return buf.String() 271 | } 272 | 273 | func waitForExitCode(t *testing.T, cmd *exec.Cmd) (exitcode int) { 274 | err := cmd.Wait() 275 | if err != nil { 276 | if exiterr, ok := err.(*exec.ExitError); ok { 277 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 278 | exitcode = status.ExitStatus() 279 | } 280 | } else { 281 | exitcode = -1 282 | t.Error(err) 283 | } 284 | } 285 | return 286 | } 287 | -------------------------------------------------------------------------------- /acctest/stubserver.go: -------------------------------------------------------------------------------- 1 | // PUBLIC DOMAIN NOTICE 2 | // National Center for Biotechnology Information 3 | // 4 | // This software/database is a "United States Government Work" under the 5 | // terms of the United States Copyright Act. It was written as part of 6 | // the author's official duties as a United States Government employee and 7 | // thus cannot be copyrighted. This software/database is freely available 8 | // to the public for use. The National Library of Medicine and the U.S. 9 | // Government have not placed any restriction on its use or reproduction. 10 | // 11 | // Although all reasonable efforts have been taken to ensure the accuracy 12 | // and reliability of the software and data, the NLM and the U.S. 13 | // Government do not and cannot warrant the performance or results that 14 | // may be obtained by using this software or data. The NLM and the U.S. 15 | // Government disclaim all warranties, express or implied, including 16 | // warranties of performance, merchantability or fitness for any particular 17 | // purpose. 18 | // 19 | // Please cite the author in any work or product based on this material. 20 | 21 | package acctest 22 | 23 | import ( 24 | "fmt" 25 | "google.golang.org/grpc" 26 | "google.golang.org/grpc/credentials" 27 | "google.golang.org/grpc/health" 28 | hv1 "google.golang.org/grpc/health/grpc_health_v1" 29 | "net" 30 | ) 31 | 32 | // StartServer starts new gRPC application with simple health service. 33 | // It is callers responsibility to Stop the server 34 | func StartServer(port int, certFile string, keyFile string) (*grpc.Server, *health.Server, error) { 35 | transportCredentials, err := credentials.NewServerTLSFromFile(certFile, keyFile) 36 | if err != nil { 37 | return nil, nil, err 38 | } 39 | return doStart(port, grpc.Creds(transportCredentials)) 40 | } 41 | 42 | // StartInsecureServer starts new gRPC application with simple health service. 43 | // It is callers responsibility to Stop the server 44 | func StartInsecureServer(port int) (*grpc.Server, *health.Server, error) { 45 | return doStart(port) 46 | } 47 | 48 | func doStart(port int, options ...grpc.ServerOption) (server *grpc.Server, service *health.Server, err error) { 49 | listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) 50 | if err != nil { 51 | return 52 | } 53 | server = grpc.NewServer(options...) 54 | service = health.NewServer() 55 | hv1.RegisterHealthServer(server, service) 56 | 57 | go server.Serve(listener) 58 | return server, service, nil 59 | } 60 | 61 | // StartEmptyServer starts gRPC server application with no services 62 | func StartEmptyServer(port int) (server *grpc.Server, err error) { 63 | listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) 64 | if err != nil { 65 | return 66 | } 67 | server = grpc.NewServer() 68 | 69 | go server.Serve(listener) 70 | return server, nil 71 | } 72 | -------------------------------------------------------------------------------- /acctest/x509/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE/jCCAuagAwIBAgIJAOUm28+Yjo8kMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xNzEyMjgwNDQ2MTZaFw00NTA1MTQwNDQ2MTZaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC 5 | ggIBAM/V0JGcu7DYmjbUagI526ccLe4GNRRMjQIaiD71E/aCw2AZ00czvse2DpCO 6 | uznU+eCRikLkYWl+RvHoJoVimw85bIlx1ScOeJ4OExMcy7s/AcpZ/WGBInGiI1tA 7 | qM0f6wlNb5q7emyG6hckyXS6JuKz6nJG18PcwrnBqcBKWYQLaV3KIRdbwlmABFj3 8 | j9Hv9zsAgywfVgmrg9EInylCZpXvFKuVP7+v5sH0m0E1MWR6FoL8PWF/siKFwLCV 9 | qceAaJUl0+mmtf4IBIzwRWtc2abnler551JzjId5vaXwAfw5euwN803C5W/V/vQ9 10 | OZp7taAp1d8tHVq8NR+gPEI+gChWCdyXl7pabTMYVXO4m0eX62DLMWrB25eOebsw 11 | Zj/Qg4MVpLfsMrHFoUIV/nLVO5oh45lkHO3X7ZEMXDm85qMH9IvvpWAzTqqL5Nb2 12 | EIRLiMBLofnopQ/RGmUzFEJ7uZFT7bDz5G46Ekq2NPxg89YI/c1+99L+IKyT04g6 13 | 4lDJx2idezul+u69+0D9jWPyBGGsTFq4h4hgYhqrgpGiUpANoBRB3YQWD3lRVWlO 14 | kRkv1tgz9gENxwf5bwGGCjo3afR5QQWqJyVYW28A3W8AAxN7X8KNiHjUeSoiXEDn 15 | Wsn7u16RF3zGvYGsopz1W7U86ZMZ2uOvfQ4RPz7qPN0vkzB7AgMBAAGjUzBRMB0G 16 | A1UdDgQWBBT+pbYheKFdRl8CJLLgUgi5uQlZmTAfBgNVHSMEGDAWgBT+pbYheKFd 17 | Rl8CJLLgUgi5uQlZmTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IC 18 | AQBDa1v9VawozMfWUWB1iBrpnJx5jb4Xbjvlwag7pgO8e2PFq2rBPxxCEHc2usQS 19 | tcF6EuHfpDRlhu431Xuw+6HhgakoqPlgaxgXBn6zbteDVSxOQfij377F09cYAww5 20 | jnKCaItTSFxWW0kSlPI6m1QyWDFHSshgpvRRr58yY8OJbn9oAEKp39x9gprgVWuw 21 | vcjgUCKCCouXburYmW82Yo0A5c5jnyDbFeM3iC2N85KoencQAYt/tKT4N65+Ublf 22 | 1aAeRGUrByfUfNEly00Clg3It7yamGRGLFHdd0yNBohcm7HfEsiACkyd0yaHaAxY 23 | TbmujnNRNkiHYeAjcMqOKUnK2ZymKmzC0ALpoAQaHpws7qKqXJ7K6Nh5jixNRcYT 24 | I0nkQCO8GbMl6/TkuevGRqxEQnmCHTek7y0T493HeZDGMsgWhbH8jvZvCAAWXvBf 25 | 7iPyU9d6P0Q0u4Hcn7wP/IBKCq06L448yP18SPwoNYAlKbcCZ067O+4Zdf5yAgU5 26 | PiWk9rbbMbWB7nQ8yZ86Z8wuZ76dftnJ8ASYJJk38SUkrQC3AoKhj6+GKcXLwZEV 27 | OVq5m9NCM695wo+5d0rnusz2EPAQpvLE4bFveH/2wEk8yduYCvAQC3PwD47ypsy0 28 | Vz/Aa7mPgEj/yAp1Z3eh0DrAiF3fux3OVlha5BiTntYd/w== 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // PUBLIC DOMAIN NOTICE 2 | // National Center for Biotechnology Information 3 | // 4 | // This software/database is a "United States Government Work" under the 5 | // terms of the United States Copyright Act. It was written as part of 6 | // the author's official duties as a United States Government employee and 7 | // thus cannot be copyrighted. This software/database is freely available 8 | // to the public for use. The National Library of Medicine and the U.S. 9 | // Government have not placed any restriction on its use or reproduction. 10 | // 11 | // Although all reasonable efforts have been taken to ensure the accuracy 12 | // and reliability of the software and data, the NLM and the U.S. 13 | // Government do not and cannot warrant the performance or results that 14 | // may be obtained by using this software or data. The NLM and the U.S. 15 | // Government disclaim all warranties, express or implied, including 16 | // warranties of performance, merchantability or fitness for any particular 17 | // purpose. 18 | // 19 | // Please cite the author in any work or product based on this material. 20 | 21 | package main 22 | 23 | import ( 24 | "context" 25 | "crypto/tls" 26 | "fmt" 27 | "github.com/hashicorp/go-rootcerts" 28 | "github.com/urfave/cli" 29 | "google.golang.org/grpc" 30 | "google.golang.org/grpc/codes" 31 | "google.golang.org/grpc/credentials" 32 | hv1 "google.golang.org/grpc/health/grpc_health_v1" 33 | "google.golang.org/grpc/status" 34 | "os" 35 | "time" 36 | ) 37 | 38 | // version variable is set during compilation using ldflags 39 | var version string 40 | 41 | const ( 42 | // ExitCodeUsage is returned if application used incorrectly 43 | ExitCodeUsage = 1 44 | // ExitCodeHealthCheckNegative is returned if health status is not SERVING 45 | ExitCodeHealthCheckNegative = 2 46 | // ExitCodeUnexpected is returned if any other error happens 47 | ExitCodeUnexpected = 127 48 | ) 49 | 50 | // appFlags holds flags passed to application 51 | type appFlags struct { 52 | timeout time.Duration 53 | noFail bool 54 | tls bool 55 | tlsInsecure bool 56 | tlsCAFile string 57 | tlsCAPath string 58 | } 59 | 60 | // appConfig holds processed application config 61 | type appConfig struct { 62 | timeout time.Duration 63 | noFail bool 64 | serverAddress string 65 | serviceName string 66 | creds credentials.TransportCredentials 67 | } 68 | 69 | // mainFn is main application business logic 70 | type mainFn func(config *appConfig) *cli.ExitError 71 | 72 | func createApp(mainFn mainFn) *cli.App { 73 | app := cli.NewApp() 74 | flags := &appFlags{} 75 | 76 | app.Name = "gprobe" 77 | app.Usage = "universal gRPC health-checker. See https://github.com/grpc/grpc/blob/master/doc/health-checking.md" 78 | app.UsageText = "gprobe [options] server_address [service_name]" 79 | app.Version = version 80 | app.HideHelp = true 81 | app.OnUsageError = func(context *cli.Context, err error, isSubcommand bool) error { 82 | cli.ShowAppHelp(context) 83 | return cli.NewExitError(err.Error(), ExitCodeUsage) 84 | } 85 | cli.VersionPrinter = func(c *cli.Context) { 86 | fmt.Fprintf(c.App.Writer, "%s\n", c.App.Version) 87 | } 88 | app.Flags = []cli.Flag{ 89 | cli.DurationFlag{ 90 | Name: "timeout, t", 91 | Usage: "Operation timeout", 92 | Destination: &flags.timeout, 93 | Value: 1 * time.Second, 94 | }, 95 | cli.BoolFlag{ 96 | Name: "no-fail, n", 97 | Usage: "Do not fail if service status is other than SERVING. Note: this has no effect on server check", 98 | Destination: &flags.noFail, 99 | }, 100 | cli.BoolFlag{ 101 | Name: "tls", 102 | Usage: "Use TLS, verify server with CA certificates installed on this system", 103 | Destination: &flags.tls, 104 | }, 105 | cli.BoolFlag{ 106 | Name: "tls-insecure", 107 | Usage: "Use TLS, do NOT verify server (accept any certificate)", 108 | Destination: &flags.tlsInsecure, 109 | }, 110 | cli.StringFlag{ 111 | Name: "tls-cafile", 112 | EnvVar: "GPROBE_CAFILE", 113 | Usage: "Use TLS, verify server with CA certificate stored in specified file", 114 | Destination: &flags.tlsCAFile, 115 | }, 116 | cli.StringFlag{ 117 | Name: "tls-capath", 118 | EnvVar: "GPROBE_CAPATH", 119 | Usage: "Use TLS, verify server with CA certificates located under specified path", 120 | Destination: &flags.tlsCAPath, 121 | }, 122 | } 123 | app.Action = func(c *cli.Context) error { 124 | appConfig, err := createConfig(flags, c.Args()) 125 | if err != nil { 126 | return c.App.OnUsageError(c, err, false) 127 | } 128 | // Pass all input to mainFn 129 | return mainFn(appConfig) 130 | } 131 | return app 132 | } 133 | 134 | func createConfig(flags *appFlags, args cli.Args) (config *appConfig, err error) { 135 | config = &appConfig{} 136 | switch len(args) { 137 | case 2: 138 | config.serviceName = args.Get(1) 139 | config.serverAddress = args.Get(0) 140 | break 141 | case 1: 142 | config.serverAddress = args.Get(0) 143 | break 144 | default: 145 | return nil, fmt.Errorf("exactly 1 to 2 arguments are required") 146 | } 147 | 148 | creds, err := parseCredentials(flags) 149 | if err != nil { 150 | return nil, fmt.Errorf("can't parse TLS configuration: %s", err.Error()) 151 | } 152 | 153 | config.creds = creds 154 | config.timeout = flags.timeout 155 | config.noFail = flags.noFail 156 | return 157 | } 158 | 159 | func parseCredentials(flags *appFlags) (credentials.TransportCredentials, error) { 160 | // rootcerts library accepts both CAFile and CAPath, however handles only one of two, the other is ignored 161 | // to avoid ambiguity in behavior we do additional flags validation and explicitly allow only one flag set 162 | switch countTLSFlags(flags) { 163 | case 0: 164 | // no tls 165 | return nil, nil 166 | case 1: 167 | tlsConfig, err := createTLSConfig(flags.tlsCAFile, flags.tlsCAPath, flags.tlsInsecure) 168 | if err != nil { 169 | return nil, err 170 | } 171 | creds := credentials.NewTLS(tlsConfig) 172 | return creds, nil 173 | default: 174 | err := fmt.Errorf("at most one of --tls, --tls-insecure, --tls-cafile and --tls-capath is allowed") 175 | return nil, err 176 | } 177 | } 178 | 179 | func countTLSFlags(flags *appFlags) int { 180 | tlsFlagsSet := 0 181 | if flags.tls { 182 | tlsFlagsSet++ 183 | } 184 | if flags.tlsInsecure { 185 | tlsFlagsSet++ 186 | } 187 | if len(flags.tlsCAFile) > 0 { 188 | tlsFlagsSet++ 189 | } 190 | if len(flags.tlsCAPath) > 0 { 191 | tlsFlagsSet++ 192 | } 193 | return tlsFlagsSet 194 | } 195 | 196 | func createTLSConfig(caFile string, caPath string, insecure bool) (tlsConfig *tls.Config, err error) { 197 | tlsConfig = &tls.Config{} 198 | 199 | if insecure { 200 | tlsConfig.InsecureSkipVerify = true 201 | return 202 | } 203 | 204 | certs := &rootcerts.Config{ 205 | CAFile: caFile, 206 | CAPath: caPath, 207 | } 208 | err = rootcerts.ConfigureTLS(tlsConfig, certs) 209 | if err != nil { 210 | tlsConfig = nil 211 | return 212 | } 213 | 214 | return 215 | } 216 | 217 | func main() { 218 | createApp(appMain).Run(os.Args) 219 | } 220 | 221 | func appMain(config *appConfig) *cli.ExitError { 222 | ctx, cancel := context.WithTimeout(context.Background(), config.timeout) 223 | defer cancel() 224 | 225 | connection, err := connect(ctx, config.serverAddress, config.creds) 226 | if err != nil { 227 | // actually should never happen because we use non-blocking dialer and failFast RPC (defaults) 228 | return cli.NewExitError(fmt.Sprintf("can't connect to application: %s", err.Error()), ExitCodeUnexpected) 229 | } 230 | defer connection.Close() 231 | 232 | status, err := check(ctx, connection, config.serviceName) 233 | if err != nil { 234 | return cli.NewExitError(err.Error(), ExitCodeUnexpected) 235 | } 236 | 237 | fmt.Fprintln(os.Stdout, status.String()) 238 | if !(config.noFail || status == hv1.HealthCheckResponse_SERVING) { 239 | return cli.NewExitError("health-check failed", ExitCodeHealthCheckNegative) 240 | } 241 | 242 | // for some reason returning nil here causes err == nil to be false in urfave/cli/errors.go:79 243 | return cli.NewExitError("", 0) 244 | } 245 | 246 | func connect(ctx context.Context, serverAddress string, creds credentials.TransportCredentials) (connection *grpc.ClientConn, err error) { 247 | var dialOption grpc.DialOption 248 | if creds == nil { 249 | dialOption = grpc.WithInsecure() 250 | } else { 251 | dialOption = grpc.WithTransportCredentials(creds) 252 | } 253 | connection, err = grpc.DialContext(ctx, serverAddress, dialOption) 254 | return 255 | } 256 | 257 | func check(ctx context.Context, connection *grpc.ClientConn, service string) (status hv1.HealthCheckResponse_ServingStatus, err error) { 258 | client := hv1.NewHealthClient(connection) 259 | response, err := client.Check(ctx, &hv1.HealthCheckRequest{ 260 | Service: service, 261 | }) 262 | 263 | if response != nil { 264 | status = response.Status 265 | } 266 | 267 | err = toHumanReadable(err, service) 268 | 269 | return 270 | } 271 | 272 | func toHumanReadable(err error, service string) error { 273 | code := status.Code(err) 274 | switch code { 275 | case codes.OK: 276 | return err // err is nil 277 | case codes.Unavailable: 278 | return fmt.Errorf("connection refused: application isn't listening or TLS handshake failed") 279 | case codes.Unimplemented: 280 | return fmt.Errorf("rpc error: server doesn't implement gRPC health-checking protocol") 281 | case codes.NotFound: 282 | return fmt.Errorf("rpc error: unknown service %s", service) 283 | default: 284 | if s, isRPCError := status.FromError(err); isRPCError { 285 | // display only message from generic rpc errors, hide code 286 | return fmt.Errorf("rpc error: %s", s.Message()) 287 | } 288 | return err 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // PUBLIC DOMAIN NOTICE 2 | // National Center for Biotechnology Information 3 | // 4 | // This software/database is a "United States Government Work" under the 5 | // terms of the United States Copyright Act. It was written as part of 6 | // the author's official duties as a United States Government employee and 7 | // thus cannot be copyrighted. This software/database is freely available 8 | // to the public for use. The National Library of Medicine and the U.S. 9 | // Government have not placed any restriction on its use or reproduction. 10 | // 11 | // Although all reasonable efforts have been taken to ensure the accuracy 12 | // and reliability of the software and data, the NLM and the U.S. 13 | // Government do not and cannot warrant the performance or results that 14 | // may be obtained by using this software or data. The NLM and the U.S. 15 | // Government disclaim all warranties, express or implied, including 16 | // warranties of performance, merchantability or fitness for any particular 17 | // purpose. 18 | // 19 | // Please cite the author in any work or product based on this material. 20 | 21 | package main 22 | 23 | import ( 24 | "testing" 25 | "time" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/urfave/cli" 29 | ) 30 | 31 | func Test_createConfig_args_narg1(t *testing.T) { 32 | // given 33 | args := cli.Args{"server"} 34 | flags := &appFlags{} 35 | 36 | // when 37 | config, err := createConfig(flags, args) 38 | 39 | // then 40 | assert.NoError(t, err) 41 | assert.Equal(t, "server", config.serverAddress) 42 | assert.Empty(t, config.serviceName) 43 | } 44 | 45 | func Test_createConfig_args_narg2(t *testing.T) { 46 | // given 47 | args := cli.Args{"server", "svc"} 48 | flags := &appFlags{} 49 | 50 | // when 51 | config, err := createConfig(flags, args) 52 | 53 | // then 54 | assert.NoError(t, err) 55 | assert.Equal(t, "server", config.serverAddress) 56 | assert.Equal(t, "svc", config.serviceName) 57 | } 58 | 59 | func Test_createConfig_args_narg3(t *testing.T) { 60 | // given 61 | args := cli.Args{"foo", "bar", "baz"} 62 | flags := &appFlags{} 63 | 64 | // when 65 | _, err := createConfig(flags, args) 66 | 67 | // then 68 | assert.Error(t, err) 69 | } 70 | 71 | func Test_createConfig_args_narg0(t *testing.T) { 72 | // given 73 | args := cli.Args{} 74 | flags := &appFlags{} 75 | 76 | // when 77 | _, err := createConfig(flags, args) 78 | 79 | // then 80 | assert.Error(t, err) 81 | } 82 | 83 | func Test_createConfig_flags_empty(t *testing.T) { 84 | // given 85 | args := cli.Args{"foo"} 86 | flags := &appFlags{} 87 | 88 | // when 89 | config, err := createConfig(flags, args) 90 | 91 | // then 92 | assert.NoError(t, err) 93 | assert.Nil(t, config.creds) 94 | assert.False(t, config.noFail) 95 | } 96 | 97 | func Test_createConfig_flags(t *testing.T) { 98 | // given 99 | args := cli.Args{"foo"} 100 | flags := &appFlags{ 101 | tls: true, 102 | noFail: true, 103 | timeout: time.Minute, 104 | } 105 | 106 | // when 107 | config, err := createConfig(flags, args) 108 | 109 | // then 110 | assert.NoError(t, err) 111 | assert.NotNil(t, config.creds) 112 | assert.Equal(t, time.Minute, config.timeout) 113 | assert.True(t, config.noFail) 114 | } 115 | 116 | func Test_parseCredentials_tls(t *testing.T) { 117 | // given 118 | dataset := []struct { 119 | flags *appFlags 120 | credsReturned bool 121 | errorReturned bool 122 | message string 123 | }{ 124 | // success 125 | {&appFlags{tls: true}, true, false, ""}, 126 | {&appFlags{tlsInsecure: true}, true, false, ""}, 127 | {&appFlags{tlsCAFile: "acctest/x509/certificate.pem"}, true, false, ""}, 128 | {&appFlags{tlsCAPath: "acctest/x509"}, true, false, ""}, 129 | // fail 130 | {&appFlags{tlsCAFile: "acctest/key.pem"}, false, true, "should fail, acctest/key.pem is not a valid certificate"}, 131 | {&appFlags{tlsCAFile: "123098.pem"}, false, true, "should fail, 123098.pem does not exist"}, 132 | {&appFlags{tls: true, tlsInsecure: true}, false, true, "only one tls option should be specified"}, 133 | {&appFlags{tls: true, tlsCAFile: "acctest/x509/certificate.pem"}, false, true, "only one tls option should be specified"}, 134 | {&appFlags{tlsInsecure: true, tlsCAFile: "acctest/x509/certificate.pem"}, false, true, "only one tls option should be specified"}, 135 | {&appFlags{tlsCAFile: "acctest/x509/certificate.pem", tlsCAPath: "acctest/x509"}, false, true, "only one tls option should be specified"}, 136 | } 137 | 138 | for _, tt := range dataset { 139 | // when 140 | creds, err := parseCredentials(tt.flags) 141 | 142 | // then 143 | if tt.credsReturned { 144 | assert.NotNil(t, creds, tt.message) 145 | } else { 146 | assert.Nil(t, creds, tt.message) 147 | } 148 | 149 | if tt.errorReturned { 150 | assert.Error(t, err, tt.message) 151 | } else { 152 | assert.NoError(t, err, tt.message) 153 | } 154 | } 155 | } 156 | 157 | func Test_countTLSFlags(t *testing.T) { 158 | // given 159 | dataset := []struct { 160 | flags *appFlags 161 | count int 162 | }{ 163 | {&appFlags{}, 0}, 164 | {&appFlags{tls: true}, 1}, 165 | {&appFlags{tls: true, tlsInsecure: true}, 2}, 166 | {&appFlags{tls: true, tlsInsecure: true, tlsCAFile: "file"}, 3}, 167 | {&appFlags{tls: true, tlsInsecure: true, tlsCAFile: "file", tlsCAPath: "path"}, 4}, 168 | } 169 | 170 | for _, tt := range dataset { 171 | // when 172 | cnt := countTLSFlags(tt.flags) 173 | 174 | // then 175 | assert.Equal(t, tt.count, cnt) 176 | } 177 | } 178 | --------------------------------------------------------------------------------