From 670c5a2fb787862c57a5ee9ae1263619567aba83 Mon Sep 17 00:00:00 2001 From: Madhu Date: Sat, 3 Jun 2023 08:56:32 -0400 Subject: [PATCH] feat: initial commit --- .dockerignore | 2 + .github/CODEOWNERS | 1 + .github/dependabot.yml | 20 +++++++ .github/workflows/build-and-push.yml | 53 ++++++++++++++++++ .github/workflows/lint.yml | 15 +++++ .github/workflows/release.yml | 25 +++++++++ .github/workflows/test.yml | 15 +++++ .gitignore | 6 ++ Containerfile | 15 +++++ LICENSE.md | 19 +++++++ Makefile | 32 +++++++++++ bin/.gitkeep | 0 go.mod | 17 ++++++ go.sum | 18 ++++++ internal/app.go | 42 ++++++++++++++ internal/constants.go | 30 ++++++++++ internal/models.go | 15 +++++ internal/ports.go | 77 +++++++++++++++++++++++++ internal/ports_test.go | 45 +++++++++++++++ internal/run.go | 84 ++++++++++++++++++++++++++++ internal/run_test.go | 44 +++++++++++++++ internal/test_utils.go | 22 ++++++++ main.go | 13 +++++ readme.md | 77 +++++++++++++++++++++++++ 24 files changed, 687 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/CODEOWNERS create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build-and-push.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 Containerfile create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 bin/.gitkeep create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/app.go create mode 100644 internal/constants.go create mode 100644 internal/models.go create mode 100644 internal/ports.go create mode 100644 internal/ports_test.go create mode 100644 internal/run.go create mode 100644 internal/run_test.go create mode 100644 internal/test_utils.go create mode 100644 main.go create mode 100644 readme.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8e90db4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +bin/* +!bin/.gitkeep \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..5dc5a29 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @madhuravius diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..638a1f1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + labels: + - dependencies + - actions + - Skip Changelog + schedule: + interval: weekly + day: sunday + - package-ecosystem: gomod + directory: / + labels: + - dependencies + - go + - Skip Changelog + schedule: + interval: weekly + day: sunday \ No newline at end of file diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml new file mode 100644 index 0000000..dc5d8d1 --- /dev/null +++ b/.github/workflows/build-and-push.yml @@ -0,0 +1,53 @@ +# Credit to Mark van Holsteijn - shorturl.at/rzLPR +name: Build and publish the container image + +on: + push: + tags: + - '*' + branches: + - 'main' + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - + name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - + name: Get tag + id: repository + run: echo "tag=$(git describe --tags HEAD)" > $GITHUB_ENV + + - + name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - + name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - + name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: ./Containerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ghcr.io/${{ github.repository }}:${{ env.tag }} \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..6b5a5be --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,15 @@ +name: Lint + +on: + push: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '>=1.20.0' + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..98dbfc1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +name: Go Releaser + +on: + release: + types: + - published + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4 + if: startsWith(github.ref, 'refs/tags/') + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6644dd3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,15 @@ +name: Test + +on: + push: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: '>=1.20.0' + - run: go install gotest.tools/gotestsum@latest + - run: gotestsum --format testname -- -coverprofile=cover.out ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..894cd44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.idea + +bin/* +!bin/.gitkeep +coverage.out \ No newline at end of file diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..b5ae593 --- /dev/null +++ b/Containerfile @@ -0,0 +1,15 @@ +FROM docker.io/golang:latest as build + +WORKDIR /app +COPY Makefile . +COPY go.* . +RUN make deps + +COPY . . +RUN make build + +# cannot use scratch image as some bits are needed for the webserver +FROM docker.io/debian:stable-slim +COPY --from=build /app/bin/ready /app/bin/ready + +ENTRYPOINT ["/app/bin/ready"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..b6bb5e0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2023 madhuravius + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..55e161a --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +deps: + go mod download +.PHONY: deps + +init: deps + go generate ./... +.PHONY: init + +build-image: + docker build -f ./Containerfile -t ghcr.io/madhuravius/ready:latest . +.PHONY: build-image + +build: + go build -ldflags="-s -w" -o ./bin/ready main.go +.PHONY: build + +lint: + docker run \ + --rm \ + -v $(shell pwd):/app \ + -w /app \ + docker.io/golangci/golangci-lint:v1.52 \ + golangci-lint run +.PHONY: lint + +format: + go fmt ./... +.PHONY: format + +test: + go test ./... -v -coverprofile=coverage.out +.PHONY: test diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a017c1a --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module ready + +go 1.20 + +require ( + github.com/stretchr/testify v1.8.4 + github.com/urfave/cli/v2 v2.25.5 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4c3f983 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli/v2 v2.25.5 h1:d0NIAyhh5shGscroL7ek/Ya9QYQE0KNabJgiUinIQkc= +github.com/urfave/cli/v2 v2.25.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app.go b/internal/app.go new file mode 100644 index 0000000..8e7ef1d --- /dev/null +++ b/internal/app.go @@ -0,0 +1,42 @@ +package internal + +import "github.com/urfave/cli/v2" + +func NewApp() *cli.App { + return &cli.App{ + Name: "ready", + Usage: "wait for a group of hosts and ports to be ready", + Commands: []*cli.Command{ + { + Name: RunKey, + Aliases: []string{""}, + Usage: "start ready", + Action: func(cCtx *cli.Context) error { + if err := RunLoop(cCtx); err != nil { + return err + } + return nil + }, + }, + }, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: DebugKey, + Required: false, + Value: true, + Usage: "if enabled, will print out logs", + }, + &cli.StringSliceFlag{ + Name: HostPortsKey, + Required: true, + Usage: "as a csv, specify a range of hosts and ports to check (ex: \"localhost:3000,test:1234\" )", + }, + &cli.IntFlag{ + Value: 30, + Name: TimeoutKey, + Required: false, + Usage: "as an integer, maximum number of seconds to wait and error if ready checks do not all pass by", + }, + }, + } +} diff --git a/internal/constants.go b/internal/constants.go new file mode 100644 index 0000000..ca2b2a9 --- /dev/null +++ b/internal/constants.go @@ -0,0 +1,30 @@ +package internal + +import ( + "errors" + "time" +) + +const ( + // RunKey - main key for subcommand to run the app + RunKey = "run" + // DebugKey - enable debug mode that gives some more logs + DebugKey = "debug" + // HostPortsKey - flag to search on when trying to search against ports + HostPortsKey = "host-ports" + // TimeoutKey - flag to wait for application to error if not found + TimeoutKey = "timeout" + // DebugLogInterval - amount of time to wait in between each call + DebugLogInterval = time.Second * 5 + // PortCheckTimeout - amount of time to check if a port is open + PortCheckTimeout = time.Second + // Heartbeat - regular heartbeat for all critical functions in core loop + Heartbeat = time.Millisecond * 100 +) + +var ( + // ErrorTimeoutNotReady - error that gets raised when ready checks are not met in time + ErrorTimeoutNotReady = errors.New("err: operation timed out, ready checks not all met") + // ErrorHostPortBadFormat - error if bad input err + ErrorHostPortBadFormat = errors.New("err: host port provided in bad format (not - host:port)") +) diff --git a/internal/models.go b/internal/models.go new file mode 100644 index 0000000..f4b5b26 --- /dev/null +++ b/internal/models.go @@ -0,0 +1,15 @@ +package internal + +import "time" + +type HostPort struct { + host string + port int +} + +type RunConfig struct { + debug bool + hostPortsReady map[HostPort]bool + started time.Time + timeout time.Duration +} diff --git a/internal/ports.go b/internal/ports.go new file mode 100644 index 0000000..13a951a --- /dev/null +++ b/internal/ports.go @@ -0,0 +1,77 @@ +package internal + +import ( + "fmt" + "net" + "strconv" + "strings" + "sync" +) + +func extractHostAndPortFromString(hostPortValue string) (string, int, error) { + hostPortSplit := strings.Split(hostPortValue, ":") + + if len(hostPortSplit) < 2 { + return "", 0, ErrorHostPortBadFormat + } + + port, err := strconv.Atoi(hostPortSplit[1]) + if err != nil { + return "", 0, err + } + + return hostPortSplit[0], port, nil +} + +func isPortActive(host string, port int) bool { + conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, strconv.Itoa(port)), PortCheckTimeout) + if err != nil { + return false + } + if conn != nil { + defer func(conn net.Conn) { + _ = conn.Close() + }(conn) + } + return conn != nil +} + +func (r *RunConfig) portsFoundCount() int { + count := 0 + for _, status := range r.hostPortsReady { + if status == true { + count += 1 + } + } + return count +} + +func (r *RunConfig) allPortsFound() bool { + for _, status := range r.hostPortsReady { + if status != true { + return false + } + } + return true +} + +func (r *RunConfig) checkPorts() { + wg := sync.WaitGroup{} + for hostPort, status := range r.hostPortsReady { + if status == true { + // already checked to be there, continue + continue + } + + // loop through remaining ports to see if any are remaining + wg.Add(1) + go func(wg *sync.WaitGroup, hostPort HostPort, r *RunConfig) { + defer wg.Done() + if isPortActive(hostPort.host, hostPort.port) { + r.hostPortsReady[hostPort] = true + r.printDebugStatement(fmt.Sprintf("Found %s:%d to be available.\n", hostPort.host, hostPort.port)) + } + }(&wg, hostPort, r) + } + wg.Wait() +} diff --git a/internal/ports_test.go b/internal/ports_test.go new file mode 100644 index 0000000..5ed1946 --- /dev/null +++ b/internal/ports_test.go @@ -0,0 +1,45 @@ +package internal + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + // TestPort - using this port for all tests + TestPort = 22000 + // TestPortFailure - using this port for fail tests + TestPortFailure = 22001 +) + +func TestExtractHostAndPortFromStringSuccess(t *testing.T) { + host, port, err := extractHostAndPortFromString("localhost:1234") + assert.Nil(t, err) + assert.Equal(t, host, "localhost") + assert.Equal(t, port, 1234) +} + +func TestExtractHostAndPortFromStringFailureBadInput(t *testing.T) { + host, port, err := extractHostAndPortFromString("localhost") + assert.Equal(t, err, ErrorHostPortBadFormat) + assert.Equal(t, host, "") + assert.Equal(t, port, 0) +} + +func TestExtractHostAndPortFromStringFailureBadInt(t *testing.T) { + host, port, err := extractHostAndPortFromString("localhost:lol") + assert.NotNil(t, err) + assert.Equal(t, host, "") + assert.Equal(t, port, 0) +} + +func TestIsPortActiveFalse(t *testing.T) { + // potentially flaky test, expects nothing to be running on this port + assert.False(t, isPortActive("localhost", TestPort)) +} + +func TestIsPortActiveTrue(t *testing.T) { + defer startWebServer(TestPort).Close() + assert.True(t, isPortActive("localhost", TestPort)) +} diff --git a/internal/run.go b/internal/run.go new file mode 100644 index 0000000..b291374 --- /dev/null +++ b/internal/run.go @@ -0,0 +1,84 @@ +package internal + +import ( + "fmt" + "log" + "time" + + "github.com/urfave/cli/v2" +) + +func NewRunConfig(debug bool, portsToCheck cli.StringSlice, started time.Time, timeout int) (*RunConfig, error) { + portsOpen := make(map[HostPort]bool) + for _, hostPortValue := range portsToCheck.Value() { + host, port, err := extractHostAndPortFromString(hostPortValue) + if err != nil { + return nil, err + } + portsOpen[HostPort{ + host: host, + port: port, + }] = false + + if debug { + log.Printf("Will check for %s:%d.\n", host, port) + } + } + return &RunConfig{ + hostPortsReady: portsOpen, + started: started, + timeout: time.Duration(timeout) * time.Second, + }, nil +} + +func (r *RunConfig) printDebugStatement(statement string) { + if r.debug { + log.Println(statement) + } +} + +func RunLoop(ctx *cli.Context) error { + portsToCheck := ctx.Value(HostPortsKey).(cli.StringSlice) + timeout := ctx.Value(TimeoutKey).(int) + debug := ctx.Value(DebugKey).(bool) + + started := time.Now() + r, err := NewRunConfig(debug, portsToCheck, started, timeout) + if err != nil { + return err + } + + r.printDebugStatement(fmt.Sprintf("Starting ready checks (for up to %d seconds).\n", timeout)) + go func(r *RunConfig) { + for { + if r.allPortsFound() { + r.printDebugStatement("Checked all ports. They're ready!") + break + } + r.checkPorts() + time.Sleep(PortCheckTimeout) + } + }(r) + + go func(r *RunConfig) { + for { + time.Sleep(DebugLogInterval) + r.printDebugStatement(fmt.Sprintf("Still running ready checks (%d/%d)...\n", r.portsFoundCount(), len(r.hostPortsReady))) + } + }(r) + + stop := make(chan bool) + for { + if time.Now().After(started.Add(r.timeout)) { + return ErrorTimeoutNotReady + } + select { + case <-time.After(Heartbeat): + if r.allPortsFound() { + return nil + } + case <-stop: + return nil + } + } +} diff --git a/internal/run_test.go b/internal/run_test.go new file mode 100644 index 0000000..7aec971 --- /dev/null +++ b/internal/run_test.go @@ -0,0 +1,44 @@ +package internal + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRunLoopSuccess(t *testing.T) { + defer startWebServer(TestPort).Close() + app := NewApp() + err := app.Run([]string{ + "--debugfalse", + "--timeout=1", + fmt.Sprintf("--host-ports=localhost:%d", TestPort), + "run", + }) + assert.Nil(t, err) +} + +func TestRunLoopPartialSuccess(t *testing.T) { + defer startWebServer(TestPort).Close() + app := NewApp() + err := app.Run([]string{ + "--debugfalse", + "--timeout=1", + fmt.Sprintf("--host-ports=localhost:%d,localhost:%d", TestPort, TestPortFailure), + "run", + }) + assert.NotNil(t, err) + assert.Equal(t, err, ErrorTimeoutNotReady) +} + +func TestRunLoopFailure(t *testing.T) { + app := NewApp() + err := app.Run([]string{ + "--debugfalse", + "--timeout=1", + fmt.Sprintf("--host-ports=localhost:%d", TestPortFailure), + "run", + }) + assert.NotNil(t, err) + assert.Equal(t, err, ErrorTimeoutNotReady) +} diff --git a/internal/test_utils.go b/internal/test_utils.go new file mode 100644 index 0000000..0454782 --- /dev/null +++ b/internal/test_utils.go @@ -0,0 +1,22 @@ +package internal + +import ( + "fmt" + "github.com/urfave/cli/v2" + "net" + "os" +) + +func startWebServer(port int) net.Listener { + l, err := net.Listen("tcp", fmt.Sprintf("localhost: %d", port)) + if err != nil { + fmt.Println("Error listening:", err.Error()) + os.Exit(1) + } + return l +} + +func setCommonContextTestValues(ctx *cli.Context) { + ctx.Set(TimeoutKey, "1") + ctx.Set(DebugKey, "false") +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..fd2e6bf --- /dev/null +++ b/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + "os" + "ready/internal" +) + +func main() { + if err := internal.NewApp().Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..50cc846 --- /dev/null +++ b/readme.md @@ -0,0 +1,77 @@ +# Ready + +Simple tool to loop through a set of checks to let you know if in a ready state. + +Will also wait (default up to 30s) to see if all conditions are met. + +## Basic Usage + +Call it by binary and do something like below + +```shell +# will wait up to 15 seconds for postgres and redis +ready --timeout 15 --host-ports localhost:5432,localhost:6379 run +``` + +You can also call it via docker: + +```shell +docker run \ + --net=host \ + -it ghcr.io/madhuravius/ready:latest \ + --timeout 15 \ + --host-ports localhost:5432,localhost:6379 \ + run +``` + +Standard out text: + +```sh +> ready +NAME: + ready - wait for a group of hosts and ports to be ready + +USAGE: + ready [global options] command [command options] [arguments...] + +COMMANDS: + run, start ready + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --debug if enabled, will print out logs (default: true) + --host-ports value [ --host-ports value ] as a csv, specify a range of hosts and ports to check (ex: "localhost:3000,test:1234" ) + --timeout value as an integer, maximum number of seconds to wait and error if ready checks do not all pass by (default: 30) + --help, -h show help +``` + +## Why? + +I always used scripts like this one: + +```sh +# !/usr/bin/env bash +set -e + +if [ -z "$1" -o -z "$2" ] +then + echo "Usage: ./service_started.sh HOST PORT" + exit 1 +fi +echo "Waiting for port $1:$2 to become available..." +while ! nc -z $1 $2 2>/dev/null +do + let elapsed=elapsed+1 + if [ "$elapsed" -gt 30 ] + then + echo "TIMED OUT !" + exit 1 + fi + sleep 1; +done + +echo "READY !" +``` + +Which I rehash repeatedly and often get elaborate when more than a few +processes are involved. \ No newline at end of file