Sign inSign up

donkeyx/tcp-wait

By donkeyx

•Updated about 2 months ago

cli for pinging tcp host:ports for set interval and returning success or failure.

Image
0

10K+

donkeyx/tcp-wait repository overview

⁠tcp-wait

╭──────────────────────────────────────╮
│   🐴 DonkeyX's tcp-wait              │
╰──────────────────────────────────────╯

        //\\
       (/oo\)   .--------.
       (____)  | WAIT... |
        /||\   '--------'
       //||\\   📡 ping / ready / blip
      ^^ ^^ ^^
   "Is postgres back yet?"

Small tool to wait on stuff being up before you start the real process. Started life as a container pre-start helper; now it’s also an importable Go package, and has a watch mode for those upgrade / firewall blips.

Works as:

  • CLI — tcp-wait in Docker / k8s / scripts
  • Library — import "github.com/donkeyx/tcp-wait/v2"

| dockerhub | https://hub.docker.com/r/donkeyx/tcp-wait⁠ | | ghcr | ghcr.io/donkeyx/tcp-wait | | github | https://github.com/donkeyx/tcp-wait⁠ | | docs | https://pkg.go.dev/github.com/donkeyx/tcp-wait/v2⁠ |

Same donkey stable as cluster-utils-api⁠ / cluster-utils⁠ — this one’s the “wait until the other thing is actually up” bit.

⁠What it can wait on

KindReady when
tcpport accepts a connection
httpGET returns 200 (or whatever status you ask for)
redisPING gets PONG (not just “port is open”)
postgresanswers a startup message (skips “starting up” / recovery noise)
filepath exists (marker file, socket path, etc.)

TCP alone lies sometimes — redis can still be LOADING, postgres can accept sockets while it’s not ready. The redis/postgres checks speak just enough protocol that you don’t need go-redis / pgx hanging off this thing.

License is MIT — use it however you like; keep the copyright notice.

⁠Install

This is v2 (library + probes + watch). Module path has the /v2 suffix — that’s normal Go for majors.

# docker
docker run --rm donkeyx/tcp-wait:latest -version
docker run --rm ghcr.io/donkeyx/tcp-wait:latest -version

# go (note: cmd path — root is the library now)
go install github.com/donkeyx/tcp-wait/v2/cmd/tcp-wait@latest

# or grab a release binary
# https://github.com/donkeyx/tcp-wait/releases

Library:

go get github.com/donkeyx/tcp-wait/v2@latest

v1 was CLI-only at the module root (go install github.com/donkeyx/[email protected] still works if you need the old binary).

⁠CLI

# basic tcp (-hp is the old flag, still works; -tcp is the same thing)
tcp-wait -hp github.com:443
tcp-wait -tcp db:5432,cache:6379 -t 60

# http readiness
tcp-wait -http http://api:8080/readyz -t 30

# expected status and repeatable request headers
tcp-wait -http https://api:8443/readyz -http-status 204 \
  -http-header 'Authorization: Bearer token' -t 30

# TLS TCP/Redis probes; certificate verification remains enabled by default
tcp-wait -tls -tcp db:5432 -redis cache:6379 -t 60

# actually talk redis / postgres
tcp-wait -redis cache:6379 -postgres db:5432 -t 60

# wait for a file someone else writes
tcp-wait -file /var/run/app.ready -t 30

# wait, then exec your app (handy as a container entrypoint)
tcp-wait -tcp db:5432 -http http://api:8080/readyz \
  -t 60 -dial 2s -interval 500ms \
  -- /app/server --config /config.yaml
⁠Watch mode (the upgrade / firewall thing)

Normal mode exits as soon as everything is green. Watch mode keeps poking and only logs changes — up, down, or a blip (was up, now down). Super useful when you’re bouncing postgres/redis or waiting for a security group to actually open.

# sit on it until Ctrl-C (-t 0 = no time limit)
tcp-wait -watch -postgres db:5432 -redis cache:6379 -t 0 -interval 500ms -o text

# or cap the window (e.g. firewall change)
tcp-wait -w -tcp $HOST:5432 -t 600 -interval 1s -o text

You’ll see something like:

level=INFO msg="check up" check=postgres://db:5432
level=WARN msg="check blip" check=postgres://db:5432 err="..."
level=INFO msg="check up" check=postgres://db:5432
level=INFO msg="watch stopped" status="all up" reason=signal

check blip = it was good, then it wasn’t. That’s the hole during the upgrade.

⁠Flags
FlagDefaultNotes
-hp / -tcphost:port,... TCP
-httpURLs, GET, expect 200
-http-status200expected status for every -http URL
-http-headerrepeatable Name: value request header
-redishost:port,... PING
-postgreshost:port,... startup probe
-filepaths that must exist
-t20overall timeout seconds; 0 = no limit
-dial1sper-attempt timeout
-interval1ssleep between attempts
-ojsonjson or text
-qofferrors only (skip this for watch — you’ll hide the interesting lines)
-w / -watchoffcontinuous up/down/blip logging
-tlsoffTLS for TCP/Redis; configure HTTPS TLS settings
-tls-server-nameTLS certificate server-name override
-tls-insecure-skip-verifyoffdisable TLS certificate verification; development only
-versionversion + git hash
-- cmd…after wait succeeds, exec the command (don’t mix with -watch)

Logs: -o json (default, fine for k8s/loki) or -o text (nicer in a terminal). Final wait results include status (ready, timeout, or error) and checks fields for machine consumers.

Exits:

  • wait: 0 ready, 1 timeout / bad flags, 130 if you Ctrl-C
  • watch: 0 on Ctrl-C, or on timeout if everything’s up; 1 if the timer runs out still degraded
⁠k8s initContainer
initContainers:
  - name: wait-deps
    image: donkeyx/tcp-wait:latest
    args: ["-postgres", "db:5432", "-redis", "cache:6379", "-t", "120", "-o", "text"]
⁠compose / entrypoint wrap
command: ["tcp-wait", "-tcp", "db:5432", "-t", "60", "--", "/app/server"]

⁠Library

package main

import (
	"context"
	"errors"
	"log"
	"time"

	tcpwait "github.com/donkeyx/tcp-wait/v2"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	err := tcpwait.Wait(ctx, []tcpwait.Check{
		tcpwait.TCP("db:5432"),
		tcpwait.Redis("cache:6379"),
		tcpwait.Postgres("db:5432"),
		tcpwait.HTTP("http://api:8080/readyz"),
		tcpwait.File("/var/run/app.ready"),
	})
	if err != nil {
		// errors.Is(err, tcpwait.ErrTimeout) — message lists who never came up
		log.Fatal(err)
	}

	// knobs if you need them
	_ = tcpwait.WaitWithOptions(ctx, []tcpwait.Check{tcpwait.TCP("db:5432")}, tcpwait.Options{
		DialTimeout: time.Second,
		Interval:    500 * time.Millisecond,
	})

	// watch until ctx is done (timeout / cancel)
	res, _ := tcpwait.WatchWithOptions(ctx, []tcpwait.Check{
		tcpwait.Postgres("db:5432"),
		tcpwait.Redis("cache:6379"),
	}, tcpwait.Options{Interval: 500 * time.Millisecond})
	if !res.AllUp() {
		log.Printf("still down: %v", res.Down())
	}
}

TCP-only shortcut: tcpwait.WaitTCP(ctx, []string{"db:5432", "redis:6379"}).

Docs on pkg.go.dev⁠ (go doc . works locally either way).

⁠Develop

Needs Go 1.26+.

go test -race ./...
go build -o bin/tcp-wait ./cmd/tcp-wait

make test
make build
make docker-build   # local image tcp-wait:local
⁠CI / release (same shape as cluster-utils-api)
WorkflowWhenWhat
CIPR / mastertest, build, smoke, goreleaser check
DockerPR / masterPR = amd64 build only; push = multi-arch + GHA cache → Hub/GHCR
Releasev* tagGoReleaser binaries + multi-arch images + Hub README sync
CodeQLPR / master / weeklyscan

Docker Hub auth matches cluster-utils-api (no USER/PASS leftovers):

NameTypeEnvironments
DOCKERHUB_USERNAMEvariableci (docker workflow) + deployment (release)
DOCKERHUB_TOKENsecretsame — Hub access token (Read/Write)

Same values as the API repo is fine. Without DOCKERHUB_USERNAME set, Hub login is skipped (GHCR still works).

Hub image push can work while README sync returns 403 — token needs description rights, or ignore it; images still publish.

goreleaser check
make snapshot   # needs docker + goreleaser

git tag v2.0.1
git push origin v2.0.1

⁠License

MIT — see LICENSE⁠.

Tag summary

Content type

Image

Digest

sha256:a0bd2301e…

Size

6.5 MB

Last updated

about 2 months ago

docker pull donkeyx/tcp-wait