Add start of lib and test client
This commit is contained in:
parent
5ecb803260
commit
bc34b2d813
14 changed files with 404 additions and 9 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -9,6 +9,9 @@
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
|
||||||
|
# build directory
|
||||||
|
build/
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
|
||||||
|
|
9
LICENSE
9
LICENSE
|
@ -1,9 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024 faercol
|
|
||||||
|
|
||||||
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.
|
|
59
check.go
Normal file
59
check.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package gohealthchecks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Check interface {
|
||||||
|
path() string
|
||||||
|
params() map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type uuidCheck uuid.UUID
|
||||||
|
|
||||||
|
func (c uuidCheck) path() string {
|
||||||
|
return uuid.UUID(c).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c uuidCheck) params() map[string]string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUUIDCheck(rawUUID string) (Check, error) {
|
||||||
|
val, err := uuid.Parse(rawUUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return uuidCheck(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type slugCheck struct {
|
||||||
|
pingKey string
|
||||||
|
slug string
|
||||||
|
autoCreate bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c slugCheck) path() string {
|
||||||
|
return path.Join(c.pingKey, c.slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c slugCheck) params() map[string]string {
|
||||||
|
res := map[string]string{}
|
||||||
|
createVal := "0"
|
||||||
|
if c.autoCreate {
|
||||||
|
createVal = "1"
|
||||||
|
}
|
||||||
|
res["create"] = createVal
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSlugCheck(pingKey, slug string, autoCreate bool) Check {
|
||||||
|
return slugCheck{
|
||||||
|
pingKey: pingKey,
|
||||||
|
slug: slug,
|
||||||
|
autoCreate: autoCreate,
|
||||||
|
}
|
||||||
|
}
|
5
cmd/main.go
Normal file
5
cmd/main.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
Execute()
|
||||||
|
}
|
42
cmd/root.go
Normal file
42
cmd/root.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
gohealthchecks "git.faercol.me/faercol/go-healthchecks"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "healthchecks-ping",
|
||||||
|
Short: "Client for the healthchecks ping API",
|
||||||
|
Long: ``,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
pingHost string
|
||||||
|
checkUUID string
|
||||||
|
pingKey string
|
||||||
|
checkSlug string
|
||||||
|
autoCreate bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||||
|
func Execute() {
|
||||||
|
err := rootCmd.Execute()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().StringVar(&pingHost, "host", gohealthchecks.HealthchecksIOPingHost, "healthchecks host to use")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&autoCreate, "auto-create", false, "auto create check if it does not exist (slug only)")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&checkUUID, "uuid", "", "check UUID")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&pingKey, "ping-key", "", "ping key")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&checkSlug, "slug", "", "check-slug")
|
||||||
|
rootCmd.MarkFlagsRequiredTogether("ping-key", "slug")
|
||||||
|
rootCmd.MarkFlagsMutuallyExclusive("uuid", "slug")
|
||||||
|
rootCmd.MarkFlagsOneRequired("uuid", "slug")
|
||||||
|
}
|
30
cmd/start.go
Normal file
30
cmd/start.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
gohealthchecks "git.faercol.me/faercol/go-healthchecks"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var startCmd = &cobra.Command{
|
||||||
|
Use: "start",
|
||||||
|
Short: "Signal the check has started",
|
||||||
|
Long: ``,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
check := parseCheckFlags()
|
||||||
|
clt := gohealthchecks.NewPingClient(pingHost)
|
||||||
|
err := clt.ReportStart(cmd.Context(), check)
|
||||||
|
if err != nil {
|
||||||
|
failf("Failed to notify start: %s\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Notified check start")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(startCmd)
|
||||||
|
}
|
30
cmd/success.go
Normal file
30
cmd/success.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
gohealthchecks "git.faercol.me/faercol/go-healthchecks"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var successCmd = &cobra.Command{
|
||||||
|
Use: "success",
|
||||||
|
Short: "Signal the check has successfully completed",
|
||||||
|
Long: ``,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
check := parseCheckFlags()
|
||||||
|
clt := gohealthchecks.NewPingClient(pingHost)
|
||||||
|
err := clt.ReportSuccess(cmd.Context(), check)
|
||||||
|
if err != nil {
|
||||||
|
failf("Failed to notify success: %s\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Notified check success")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(successCmd)
|
||||||
|
}
|
36
cmd/utils.go
Normal file
36
cmd/utils.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
gohealthchecks "git.faercol.me/faercol/go-healthchecks"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustParseUUIDCheck(rawVal string) gohealthchecks.Check {
|
||||||
|
check, err := gohealthchecks.NewUUIDCheck(rawVal)
|
||||||
|
if err != nil {
|
||||||
|
failf("Invalid UUID: %s\n", err)
|
||||||
|
}
|
||||||
|
return check
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCheckFlags() gohealthchecks.Check {
|
||||||
|
var check gohealthchecks.Check
|
||||||
|
if checkUUID != "" {
|
||||||
|
check = mustParseUUIDCheck(checkUUID)
|
||||||
|
} else {
|
||||||
|
check = gohealthchecks.NewSlugCheck(pingKey, checkSlug, autoCreate)
|
||||||
|
}
|
||||||
|
return check
|
||||||
|
}
|
||||||
|
|
||||||
|
func failf(format string, args ...any) {
|
||||||
|
fmt.Fprintf(os.Stderr, format, args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func failln(msg string) {
|
||||||
|
fmt.Fprintln(os.Stderr, msg)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
4
config.go
Normal file
4
config.go
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
package gohealthchecks
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
}
|
3
consts.go
Normal file
3
consts.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package gohealthchecks
|
||||||
|
|
||||||
|
const HealthchecksIOPingHost = "https://hc-ping.com"
|
11
error.go
Normal file
11
error.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package gohealthchecks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCheckNotFound = errors.New("check not found on remote host")
|
||||||
|
ErrSlugConflict = errors.New("provided slug has a conflict")
|
||||||
|
ErrRateLimit = errors.New("rate limit exceeded")
|
||||||
|
)
|
13
go.mod
Normal file
13
go.mod
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module git.faercol.me/faercol/go-healthchecks
|
||||||
|
|
||||||
|
go 1.23.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.1.2
|
||||||
|
github.com/spf13/cobra v1.8.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
)
|
12
go.sum
Normal file
12
go.sum
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||||
|
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
156
pingclient.go
Normal file
156
pingclient.go
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
package gohealthchecks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleResponse(resp *http.Response) error {
|
||||||
|
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated {
|
||||||
|
return nil // don't do anything more here, we would just have a `OK` or `Created` in the body
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case http.StatusNotFound:
|
||||||
|
return ErrCheckNotFound
|
||||||
|
case http.StatusConflict:
|
||||||
|
return ErrSlugConflict
|
||||||
|
case http.StatusTooManyRequests:
|
||||||
|
return ErrRateLimit
|
||||||
|
case http.StatusBadRequest:
|
||||||
|
return fmt.Errorf("invalid URL format: %s", string(respBody))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected statuscode %d (%s)", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addQueryParams(reqURL *url.URL, check Check) {
|
||||||
|
q := reqURL.Query()
|
||||||
|
for param, value := range check.params() {
|
||||||
|
q.Add(param, value)
|
||||||
|
}
|
||||||
|
reqURL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
type PingClient interface {
|
||||||
|
ReportSuccess(ctx context.Context, check Check) error
|
||||||
|
ReportStart(ctx context.Context, check Check) error
|
||||||
|
ReportFailure(ctx context.Context, check Check) error
|
||||||
|
LogMessage(ctx context.Context, check Check, log []byte) error
|
||||||
|
ReportExitCode(ctx context.Context, check Check, exitCode int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type pingClient struct {
|
||||||
|
httpClt http.Client
|
||||||
|
host string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pingClient) ReportSuccess(ctx context.Context, check Check) error {
|
||||||
|
url, err := url.JoinPath(c.host, check.path())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid check or hostname provided: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build ping request: %w", err)
|
||||||
|
}
|
||||||
|
addQueryParams(req.URL, check)
|
||||||
|
|
||||||
|
resp, err := c.httpClt.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send ping: %w", err)
|
||||||
|
}
|
||||||
|
return handleResponse(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pingClient) ReportStart(ctx context.Context, check Check) error {
|
||||||
|
url, err := url.JoinPath(c.host, check.path(), "start")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid check or hostname provided: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build ping request: %w", err)
|
||||||
|
}
|
||||||
|
addQueryParams(req.URL, check)
|
||||||
|
|
||||||
|
resp, err := c.httpClt.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to notify start: %w", err)
|
||||||
|
}
|
||||||
|
return handleResponse(resp)
|
||||||
|
}
|
||||||
|
func (c *pingClient) ReportFailure(ctx context.Context, check Check) error {
|
||||||
|
url, err := url.JoinPath(c.host, check.path(), "fail")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid check or hostname provided: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build ping request: %w", err)
|
||||||
|
}
|
||||||
|
addQueryParams(req.URL, check)
|
||||||
|
|
||||||
|
resp, err := c.httpClt.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send ping: %w", err)
|
||||||
|
}
|
||||||
|
return handleResponse(resp)
|
||||||
|
}
|
||||||
|
func (c *pingClient) LogMessage(ctx context.Context, check Check, log []byte) error {
|
||||||
|
url, err := url.JoinPath(c.host, check.path(), "log")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid check or hostname provided: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody := bytes.NewBuffer(log)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build log request: %w", err)
|
||||||
|
}
|
||||||
|
addQueryParams(req.URL, check)
|
||||||
|
|
||||||
|
resp, err := c.httpClt.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send log: %w", err)
|
||||||
|
}
|
||||||
|
return handleResponse(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pingClient) ReportExitCode(ctx context.Context, check Check, exitCode int) error {
|
||||||
|
url, err := url.JoinPath(c.host, check.path(), strconv.FormatInt(int64(exitCode), 10))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid check or hostname provided: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build exitcode request: %w", err)
|
||||||
|
}
|
||||||
|
addQueryParams(req.URL, check)
|
||||||
|
|
||||||
|
resp, err := c.httpClt.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send exitcode: %w", err)
|
||||||
|
}
|
||||||
|
return handleResponse(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPingClient(host string) PingClient {
|
||||||
|
return &pingClient{
|
||||||
|
httpClt: *http.DefaultClient,
|
||||||
|
host: host,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue