diff --git a/README.md b/README.md index c7649d7..afb60ff 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ Which allows you to start your containers on demand and shut them down automatic - [Creating your own loading theme](#creating-your-own-loading-theme) - [Blocking the loading until the session is ready](#blocking-the-loading-until-the-session-is-ready) - [Saving the state to a file](#saving-the-state-to-a-file) + - [Sablier Healthcheck](#sablier-healthcheck) + - [Using the `/health` route](#using-the-health-route) + - [Using the `sablier health` command](#using-the-sablier-health-command) - [Glossary](#glossary) - [Versioning](#versioning) - [Credits](#credits) @@ -294,7 +297,29 @@ If the file doesn't exist it will be created, and it will be syned upon exit. Loaded instances that expired during the restart won't be changed though, they will simply be ignored. +## Sablier Healthcheck +### Using the `/health` route + +You can use the route `/health` to check for healthiness. + +- Returns 200 `OK` when ready +- Returns 503 `Service Unavailable` when terminating + +### Using the `sablier health` command + +You can use the command `sablier health` to check for healthiness. + +`sablier health` takes on argument `--url` which defaults to `http://localhost:10000/health`. + +```yml +services: + sablier: + image: acouvreur/sablier:1.2.0 + healthcheck: + test: ["sablier", "health"] + interval: 1m30s +``` ## Glossary diff --git a/app/http/healthcheck/healthcheck.go b/app/http/healthcheck/healthcheck.go new file mode 100644 index 0000000..8f8747b --- /dev/null +++ b/app/http/healthcheck/healthcheck.go @@ -0,0 +1,31 @@ +package healthcheck + +import ( + "io" + "net/http" +) + +const ( + healthy = true + unhealthy = false +) + +func Health(url string) (string, bool) { + resp, err := http.Get(url) + + if err != nil { + return err.Error(), unhealthy + } + + body, err := io.ReadAll(resp.Body) + + if err != nil { + return err.Error(), unhealthy + } + + if resp.StatusCode >= 400 { + return string(body), unhealthy + } + + return string(body), healthy +} diff --git a/app/http/routes/health.go b/app/http/routes/health.go new file mode 100644 index 0000000..755e6e2 --- /dev/null +++ b/app/http/routes/health.go @@ -0,0 +1,33 @@ +package routes + +import ( + "context" + "net/http" + + "github.com/gin-gonic/gin" +) + +type Health struct { + TerminatingStatusCode int `description:"Terminating status code" json:"terminatingStatusCode,omitempty" yaml:"terminatingStatusCode,omitempty" export:"true"` + terminating bool +} + +func (h *Health) SetDefaults() { + h.TerminatingStatusCode = http.StatusServiceUnavailable +} + +func (h *Health) WithContext(ctx context.Context) { + go func() { + <-ctx.Done() + h.terminating = true + }() +} + +func (h *Health) ServeHTTP(c *gin.Context) { + statusCode := http.StatusOK + if h.terminating { + statusCode = h.TerminatingStatusCode + } + + c.String(statusCode, http.StatusText(statusCode)) +} diff --git a/app/http/server.go b/app/http/server.go index e89da5a..9a0a240 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -34,6 +34,10 @@ func Start(serverConf config.Server, strategyConf config.Strategy, sessionManage api.GET("/strategies/dynamic/themes", strategy.ServeDynamicThemes) api.GET("/strategies/blocking", strategy.ServeBlocking) } + health := routes.Health{} + health.SetDefaults() + health.WithContext(ctx) + base.GET("/health", health.ServeHTTP) } srv := &http.Server{ diff --git a/cmd/health.go b/cmd/health.go new file mode 100644 index 0000000..6c1ce72 --- /dev/null +++ b/cmd/health.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/acouvreur/sablier/app/http/healthcheck" + "github.com/spf13/cobra" +) + +var healthCmd = &cobra.Command{ + Use: "health", + Short: "Calls the health endpoint of a Sablier instance", + Run: func(cmd *cobra.Command, args []string) { + details, healthy := healthcheck.Health(cmd.Flag("url").Value.String()) + + if healthy { + fmt.Fprintf(os.Stderr, "healthy: %v\n", details) + os.Exit(0) + } else { + fmt.Fprintf(os.Stderr, "unhealthy: %v\n", details) + os.Exit(1) + } + }, +} diff --git a/cmd/root.go b/cmd/root.go index a78638b..8bf29a2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -79,6 +79,9 @@ It provides an integrations with multiple reverse proxies and different loading rootCmd.AddCommand(startCmd) rootCmd.AddCommand(versionCmd) + healthCmd.Flags().String("url", "http://localhost:10000/health", "Sablier health endpoint") + rootCmd.AddCommand(healthCmd) + return rootCmd }