Skip to content

Commit

Permalink
Add support for multiple ping targets. (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickdappollonio authored Dec 19, 2024
1 parent 0c9c88a commit 61bd79d
Show file tree
Hide file tree
Showing 32 changed files with 1,951 additions and 214 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:
go-version-file: go.mod
- name: Test application
run: go test ./...
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/testing-pr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Testing Pull Request
on:
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test-app:
name: Test Goreleaser Application
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Test application
run: go test ./...
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Dry-run goreleaser application
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: ~> v2
args: release --snapshot --skip=publish --clean
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: Testing
name: Testing Commit
on:
push:
pull_request:

jobs:
test-app:
Expand All @@ -10,11 +9,11 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Test application
run: go test ./...
- name: Compile application
run: go build
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dist/
wait-for
2 changes: 1 addition & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ archives:
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
version_template: "{{ incpatch .Version }}-next"
dockers:
- image_templates:
- "ghcr.io/patrickdappollonio/wait-for:{{ .Tag }}-amd64"
Expand Down
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Patrick D'appollonio

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.
53 changes: 47 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `wait-for`

A tiny Go application with zero dependencies. Given a number of TCP or UDP `host:port` pairs, the app will wait until either all are available or a timeout is reached. `wait-for` supports pinging TCP or UDP hosts, by prefixing the host with `tcp://` or `udp://`, respectively. If no prefix is provided, the app will default to TCP.
A Go application with zero dependencies. Given a number of hosts, the app will wait until either all are available or a timeout is reached. `wait-for` supports pinging several host types (see [supported probes](#supported-probes)), by prefixing the host with a specific protocol. If no prefix is provided, the app will default to TCP.

Kudos to @vishnubob for the [original implementation in Bash](https://github.com/vishnubob/wait-for-it).

Expand All @@ -19,20 +19,37 @@ This will ping both `google.com` on port `443` and `mysql.example.com` on port `

All the parameters accepted by the application are shown in the help section, as shown below.

### Command-line help

```text
wait-for allows you to wait for a TCP resource to respond to requests.
wait-for allows you to wait for a resource to respond to requests.
It does this by performing a connection to the specified host and port. If
there's no resource behind it and the connection cannot be established, the
request is retried until either the timeout is reached or the resource becomes
available.
It does this by performing a TCP connection to the specified host and port. If there's
no resource behind it and the connection cannot be established, the request is retried
until either the timeout is reached or the resource becomes available.
Each protocol defines its own way of checking for the resource. For example, a
TCP connection will attempt to connect to the host and port specified, while a
MySQL connection will attempt to connect to the host and port, and then ping the
database.
By default, the standard timeout is 10 seconds.
By default, the standard timeout is 10 seconds but it can be customized for all
requests. The time between each request is 1 second, but this can also be
customized.
For documentation, visit: https://github.com/patrickdappollonio/wait-for.
Usage:
wait-for [flags]
Examples:
wait-for -s localhost:80 wait for a web server to accept connections
wait-for -s mysql.example.local:3306 wait for a MySQL database to accept connections
wait-for -s udp://localhost:53 wait for a DNS server to accept connections
wait-for --host localhost:80 --host localhost:81 wait for multiple resources to accept connections
wait-for --host mysql://localhost:3306 wait until a MySQL database is ready to accept connections and responds to pings
Flags:
-e, --every duration time to wait between each request attempt against the host (default 1s)
-h, --help help for wait-for
Expand All @@ -42,6 +59,18 @@ Flags:
--version version for wait-for
```

### Supported probes

The following probes are supported:

* [TCP probe](docs/tcp-probe.md)
* [UDP probe](docs/udp-probe.md)
* [HTTP & HTTPS probe](docs/http-https-probe.md)
* [MySQL probe](docs/mysql-probe.md) *(experimental)*
* [PostgreSQL probe](docs/postgres-probe.md) *(experimental)*

If you're interested in adding a new probe, please refer to the [Adding new probes documentation](docs/readme.md#adding-new-probes).

### Usage with Kubernetes

Simply use this tool as an `initContainer` before your application runs, and validate whether your databases or any TCP-accessible resource (such as websites, too) are up and running, or fail early with proper knowledge of the situation.
Expand Down Expand Up @@ -71,3 +100,15 @@ spec:
- name: nginx-container
image: nginx
```
### Validating connectivity to a MySQL or Postgres database
If you want to validate that a MySQL database is up and running, you can use the `mysql://` or `postgres://` prefix. This will attempt to connect to the host and port specified, and then ping the database as well. This is different than the default TCP probe, which only checks if the server is accepting connections on the specified port.

For more details, check the [MySQL probe documentation](docs/mysql-probe.md) and the [PostgreSQL probe documentation](docs/postgres-probe.md).

### Validating connectivity to an HTTP or HTTPS endpoint

If you want to validate that an HTTP or HTTPS endpoint is up and running, you can use the `http://` or `https://` prefix. This will attempt to connect to the host and port specified, and then perform an HTTP GET request to the root path (`/`) of the server where the server must respond within 1 second. This is different than the default TCP probe, which only checks if the server is accepting connections on the specified port.

For HTTPS requests, the certificate is also validated. For more details, check the [HTTP & HTTPS probe documentation](docs/http-https-probe.md).
82 changes: 57 additions & 25 deletions app.go
Original file line number Diff line number Diff line change
@@ -1,52 +1,77 @@
package main

import (
"errors"
"fmt"
"io/fs"
"time"

"github.com/patrickdappollonio/wait-for/wait"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var version = "development"

const (
helpShort = "wait-for allows you to wait for a TCP resource to respond to requests."
helpShort = "wait-for allows you to wait for a resource to respond to requests."

helpLong = `wait-for allows you to wait for a TCP resource to respond to requests.
helpLong = `wait-for allows you to wait for a resource to respond to requests.
It does this by performing a TCP connection to the specified host and port. If there's
no resource behind it and the connection cannot be established, the request is retried
until either the timeout is reached or the resource becomes available.
It does this by performing a connection to the specified host and port. If there's no resource behind it and the connection cannot be established, the request is retried until either the timeout is reached or the resource becomes available.
By default, the standard timeout is 10 seconds.
Each protocol defines its own way of checking for the resource. For example, a TCP connection will attempt to connect to the host and port specified, while a MySQL connection will attempt to connect to the host and port, and then ping the database.
By default, the standard timeout is 10 seconds but it can be customized for all requests. The time between each request is 1 second, but this can also be customized.
For documentation, visit: https://github.com/patrickdappollonio/wait-for.`
)

func root() *cobra.Command {
var (
hosts []string
timeout time.Duration
step time.Duration
verbose bool
)
var cfgFile string
var hosts []string

rootCommand := &cobra.Command{
Use: "wait-for",
Short: helpShort,
Long: helpLong,
Version: version,
Use: "wait-for",
Short: helpShort,
Long: wrap(helpLong, 80),
Version: version,
Example: exampleCommands("wait-for", []example{
{command: "-s localhost:80", helper: "wait for a web server to accept connections"},
{command: "-s mysql.example.local:3306", helper: "wait for a MySQL database to accept connections"},
{command: "-s udp://localhost:53", helper: "wait for a DNS server to accept connections"},
{command: "--host localhost:80 --host localhost:81", helper: "wait for multiple resources to accept connections"},
{command: "--host mysql://localhost:3306", helper: "wait until a MySQL database is ready to accept connections and responds to pings"},
{command: "--host postgres://localhost:5432", helper: "wait until a PostgreSQL database is ready to accept connections and responds to pings"},
{command: "--host http://localhost:8080", helper: "wait until an HTTP server is ready to accept connections and responds to requests with a 200-299 status code"},
{command: "--host https://localhost:443", helper: "wait until an HTTPS server is ready to accept connections and responds to requests with a 200-299 status code and a valid certificate"},
{command: "--config targets.yaml", helper: "load hosts and settings from a YAML file"},
}),
SilenceUsage: true,
SilenceErrors: true,
RunE: func(_ *cobra.Command, args []string) error {
w, err := wait.New(hosts, step, timeout, verbose)
if err != nil {
return err
// Read config file if available
viper.SetConfigFile(cfgFile)
if err := viper.ReadInConfig(); err != nil {
// If config not found, it's not fatal unless we rely on it
if errors.Is(err, &fs.PathError{}) {
return fmt.Errorf("error reading config file: %w", err)
}
}

// Merge hosts flag with viper flags
hosts = append(hosts, viper.GetStringSlice("host")...)

// Retrieve final values from viper after merging CLI flags and config
app := &wait.App{
Hosts: hosts,
Timeout: viper.GetDuration("timeout"),
Every: viper.GetDuration("every"),
Verbose: viper.GetBool("verbose"),
}

fmt.Println(w.String())
if err := w.PingAll(); err != nil {
// Run the application
if err := app.Run(); err != nil {
return err
}

Expand All @@ -55,10 +80,17 @@ func root() *cobra.Command {
},
}

rootCommand.Flags().StringSliceVarP(&hosts, "host", "s", []string{}, "hosts to connect to in the format \"host:port\" with optional protocol prefix (tcp:// or udp://)")
rootCommand.Flags().DurationVarP(&timeout, "timeout", "t", time.Second*10, "maximum time to wait for the endpoints to respond before giving up")
rootCommand.Flags().DurationVarP(&step, "every", "e", time.Second*1, "time to wait between each request attempt against the host")
rootCommand.Flags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose output -- will print every time a request is made")
// Flags for the program
rootCommand.Flags().StringSliceVarP(&hosts, "host", "s", []string{}, `hosts to connect to in the format "host:port" or with protocol prefix for one of the supported protocols (e.g. "udp://host:port")`)
rootCommand.Flags().DurationP("timeout", "t", 10*time.Second, "maximum time to wait for the endpoints to respond before giving up")
rootCommand.Flags().DurationP("every", "e", 1*time.Second, "time to wait between each request attempt against the host")
rootCommand.Flags().BoolP("verbose", "v", false, "enable verbose output -- will print every time a request is made")
rootCommand.Flags().StringVar(&cfgFile, "config", "targets.yaml", "config file to load hosts and settings from")

// Bind flags to viper except hosts and config file
viper.BindPFlag("timeout", rootCommand.Flags().Lookup("timeout"))
viper.BindPFlag("every", rootCommand.Flags().Lookup("every"))
viper.BindPFlag("verbose", rootCommand.Flags().Lookup("verbose"))

return rootCommand
}
55 changes: 55 additions & 0 deletions docs/configuration-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Configuring `wait-for` with a configuration file

`wait-for` supports reading configuration from a file from a file called `targets.yaml` (configurable with `--config`). This is useful when you have a lot of hosts to check and you don't want to pass them all as command-line arguments.

The following is an example YAML configuration file:

```yaml
# file: targets.yaml
hosts:
- "tcp://localhost:8080"
- "udp://localhost:53"
timeout: 30s
every: 2s
verbose: true
```
This is equal to calling the CLI with the following arguments:
```bash
wait-for \
--host "tcp://localhost:8080" \
--host "udp://localhost:53" \
--timeout 30s \
--every 2s \
--verbose
```

You can mix-and-match hosts: any hosts provided via the configuration file will be merged with the hosts provided via the command line argument `--host` or `-s`, for example, the following config file and the following command will ping all endpoints (both from the config file and the command line):

```bash
$ cat targets.yaml
# file: targets.yaml
hosts:
- "tcp://localhost:8080"
- "udp://localhost:53"
timeout: 30s
every: 2s
verbose: true

$ wait-for \
--host "localhost:80" \
--host "localhost:81" \
--timeout 10s
```

The above command will ping the following hosts by merging the two sources (configuration file and command-line flags):

```text
tcp://localhost:8080
udp://localhost:53
tcp://localhost:80
tcp://localhost:81
```

A host present both in the command-line arguments and in the configuration file will be pinged twice.
23 changes: 23 additions & 0 deletions docs/http-https-probe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# HTTP & HTTPS

The HTTP and HTTPS probes are used to send an HTTP or HTTPS `GET` request to a server and check the response. A request is successful not only if the HTTP server was able to provide a connection but also if the response status code is within the range of 200 to 299. If the request responds within this range, the probe will exit successfully.

If the connection cannot be established or the response status code is outside the range of 200 to 299, the probe will retry until either the timeout is reached or the resource becomes available.

An example request to `http://localhost:80` would look like this:

```bash
wait-for --host "http://localhost:80"
```

An example request to `https://localhost:443` would look like this:

```bash
wait-for --host "https://localhost:443"
```

## Certificate Validation

The HTTPS probe (that is, where a target host is configured to use `https://` protocol) will attempt to validate the certificate chain and the hostname. If the certificate chain is invalid or the hostname doesn't match, the probe will exit with an error and the resource will be considered unavailable.

A valid HTTPS request on resources with custom certificates will require you to provide the CA certificate to the probe. By default, any certs stored in `/etc/ssl/certs/ca-certificates.crt` will be used to validate the connection.
Loading

0 comments on commit 61bd79d

Please sign in to comment.