diff --git a/CHANGES.md b/CHANGES.md index a07229fe..ad3f8e55 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ ## v4.2.1 (unreleased) * Change the `volumes` subcommand to handle sizes in GB. +* Add `--checks-grace-period` to set the grace period for health checks, for example with `koyeb service update app/service --checks 8000:http:/healtcheck --checks-grace-period 8000=10`. ## v4.2.0 diff --git a/docs/reference.md b/docs/reference.md index ff49d550..80fb4406 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -280,6 +280,9 @@ See examples of koyeb service create --help For TCP healthchecks, use the format :tcp, for example --checks 8080:tcp To delete a healthcheck, use !PORT, for example --checks '!8080' + --checks-grace-period strings Set healthcheck grace period in seconds. + Use the format =, for example --checks-grace-period 8080=10 + --docker string Docker image --docker-args strings Set arguments to the docker command. To provide multiple arguments, use the --docker-args flag multiple times. --docker-command string Set the docker CMD explicitly. To provide arguments to the command, use the --docker-args flag. @@ -568,6 +571,9 @@ koyeb deploy / [flags] For TCP healthchecks, use the format :tcp, for example --checks 8080:tcp To delete a healthcheck, use !PORT, for example --checks '!8080' + --checks-grace-period strings Set healthcheck grace period in seconds. + Use the format =, for example --checks-grace-period 8080=10 + --env strings Update service environment variables using the format KEY=VALUE, for example --env FOO=bar To use the value of a secret as an environment variable, specify the secret name preceded by @, for example --env FOO=@bar To delete an environment variable, prefix its name with '!', for example --env '!FOO' @@ -1364,6 +1370,9 @@ $> koyeb service create myservice --app myapp --docker nginx --port 80:tcp For TCP healthchecks, use the format :tcp, for example --checks 8080:tcp To delete a healthcheck, use !PORT, for example --checks '!8080' + --checks-grace-period strings Set healthcheck grace period in seconds. + Use the format =, for example --checks-grace-period 8080=10 + --docker string Docker image --docker-args strings Set arguments to the docker command. To provide multiple arguments, use the --docker-args flag multiple times. --docker-command string Set the docker CMD explicitly. To provide arguments to the command, use the --docker-args flag. @@ -1779,6 +1788,9 @@ $> koyeb service update myapp/myservice --port 80:tcp --route '!/' For TCP healthchecks, use the format :tcp, for example --checks 8080:tcp To delete a healthcheck, use !PORT, for example --checks '!8080' + --checks-grace-period strings Set healthcheck grace period in seconds. + Use the format =, for example --checks-grace-period 8080=10 + --docker string Docker image --docker-args strings Set arguments to the docker command. To provide multiple arguments, use the --docker-args flag multiple times. --docker-command string Set the docker CMD explicitly. To provide arguments to the command, use the --docker-args flag. diff --git a/go.sum b/go.sum index 44fe6851..d22c40ac 100644 --- a/go.sum +++ b/go.sum @@ -165,8 +165,6 @@ github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/koyeb/koyeb-api-client-go v0.0.0-20240614093523-e18bb900e4f4 h1:5B1SXB8vrUXKOQ1cZdzdNcMsSzhILwfPchk9UKel3s8= -github.com/koyeb/koyeb-api-client-go v0.0.0-20240614093523-e18bb900e4f4/go.mod h1:+oQfFj2WL3gi9Pb+UHbob4D7xaT52mPfKyH1UvWa4PQ= github.com/koyeb/koyeb-api-client-go v0.0.0-20240626143115-aa41e51698e2 h1:5g98xW8nXOiZ6NPbJdbBXSA7ZCrrlfXhB0Ymw2sVZA0= github.com/koyeb/koyeb-api-client-go v0.0.0-20240626143115-aa41e51698e2/go.mod h1:+oQfFj2WL3gi9Pb+UHbob4D7xaT52mPfKyH1UvWa4PQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= diff --git a/pkg/koyeb/services.go b/pkg/koyeb/services.go index 1fd94496..567e07b3 100644 --- a/pkg/koyeb/services.go +++ b/pkg/koyeb/services.go @@ -3,6 +3,7 @@ package koyeb import ( "fmt" "regexp" + "strconv" "strings" "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" @@ -346,6 +347,12 @@ func (h *ServiceHandler) addServiceDefinitionFlagsForAllSources(flags *pflag.Fla "For TCP healthchecks, use the format :tcp, for example --checks 8080:tcp\n"+ "To delete a healthcheck, use !PORT, for example --checks '!8080'\n", ) + flags.StringSlice( + "checks-grace-period", + nil, + "Set healthcheck grace period in seconds.\n"+ + "Use the format =, for example --checks-grace-period 8080=10\n", + ) flags.StringSlice( "volumes", nil, @@ -356,12 +363,25 @@ func (h *ServiceHandler) addServiceDefinitionFlagsForAllSources(flags *pflag.Fla // Configure aliases: for example, allow user to use --port instead of --ports flags.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { aliases := map[string]string{ - "port": "ports", - "check": "checks", - "healthcheck": "checks", - "health-check": "checks", - "healthchecks": "checks", - "health-checks": "checks", + "port": "ports", + "check": "checks", + + "healthcheck": "checks", + "healthcheck-grace": "checks-grace-period", + "healthcheck-grace-period": "checks-grace-period", + + "health-check": "checks", + "health-check-grace": "checks-grace-period", + "health-check-grace-period": "checks-grace-period", + + "healthchecks": "checks", + "healthchecks-grace": "checks-grace-period", + "healthchecks-grace-period": "checks-grace-period", + + "health-checks": "checks", + "health-checks-graee": "checks-grace-period", + "health-checks-grace-period": "checks-grace-period", + "route": "routes", "volume": "volumes", "region": "regions", @@ -729,9 +749,153 @@ func (h *ServiceHandler) parseChecks(type_ koyeb.DeploymentDefinitionType, flags Solution: "Fix the service type or remove the healthchecks from your service, and try again", } } + + checksGracePeriod, _ := flags.GetStringSlice("checks-grace-period") + for _, val := range checksGracePeriod { + if err := h.parseChecksGracePeriod(newChecks, val); err != nil { + return nil, err + } + } return newChecks, nil } +// parseChecksGracePeriod parses the --checks-grace-period flag and updates the healthchecks with the specified grace period. +func (h *ServiceHandler) parseChecksGracePeriod(checks []koyeb.DeploymentHealthCheck, grace string) error { + parts := strings.Split(grace, "=") + if len(parts) != 2 { + return &errors.CLIError{ + What: "Invalid grace period", + Why: "--checks-grace-period should be formatted as =", + Additional: []string{ + "If unambiguous, can be the port number of the healthcheck", + "If several checks are defined on the same port, provide the full definition for the check, for example 8080:tcp or 8080:http or 8080:http:/healthcheck", + }, + Orig: nil, + Solution: "Provide a valid grace period and try again", + } + } + + graceValue, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return &errors.CLIError{ + What: "Invalid grace period", + Why: fmt.Sprintf("the grace period should be a number of seconds, not %s", parts[1]), + Additional: nil, + Orig: nil, + Solution: "Provide a valid grace period and try again", + } + } + + var match *koyeb.DeploymentHealthCheck + // Find a healthcheck matching the identifier provided in the --checks-grace-period flag + for idx := range checks { + equal, err := h.healthcheckEqual(parts[0], checks[idx]) + if err != nil { + return err + } + if equal && match != nil { + return &errors.CLIError{ + What: "Ambiguous grace period", + Why: `--checks-grace-period matches multiple healthchecks`, + Additional: []string{ + fmt.Sprintf("The value %s matches several healthchecks", grace), + "Provide a more specific identifier, for example 8080:http:/healthcheck or 8080:tcp", + }, + Orig: nil, + Solution: "Provide an unambiguous identifier and try again", + } + } + if equal { + match = &checks[idx] + } + } + if match == nil { + return &errors.CLIError{ + What: "Invalid grace period", + Why: "--checks-grace-period does not match any healthcheck", + Additional: []string{ + "The flag --checks-grace-period has been specified, but no healthcheck matches the identifier", + }, + Orig: nil, + Solution: "Fix the flag --checks-grace-period to match an existing healthcheck or remove the grace period, and try again", + } + } + (*match).GracePeriod = &graceValue + return nil +} + +// Returns true if the string "candidate" is equal to "healthcheck". The string +// "a" is a dotted string representation of a healthcheck, for example 8000, +// 8000:tcp, 8000:http or 8000:http:/path. +func (h *ServiceHandler) healthcheckEqual(candidate string, healthcheck koyeb.DeploymentHealthCheck) (bool, error) { + parts := strings.Split(candidate, ":") + + if len(parts) == 0 { + return false, &errors.CLIError{ + What: "Invalid grace period", + Why: "the healthcheck identifier should start with a port number", + Additional: nil, + Orig: nil, + Solution: "Fix the flag --checks-grace-period to match an existing healthcheck or remove the grace period, and try again", + } + } + + port, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return false, &errors.CLIError{ + What: "Invalid grace period", + Why: "the healthcheck identifier should start with a port number", + Additional: []string{ + "The flag --checks-grace-period has been specified, but no healthcheck matches the identifier", + }, + Orig: nil, + Solution: "Fix the flag --checks-grace-period to match an existing healthcheck or remove the grace period, and try again", + } + } + + switch { + case healthcheck.HasHttp(): + switch len(parts) { + case 1: + return port == *healthcheck.GetHttp().Port, nil + case 2: + return port == *healthcheck.GetHttp().Port && strings.ToLower(parts[1]) == "http", nil + case 3: + return port == *healthcheck.GetHttp().Port && strings.ToLower(parts[1]) == "http" && parts[2] == *healthcheck.GetHttp().Path, nil + default: + return false, &errors.CLIError{ + What: "Invalid grace period", + Why: "the healthcheck identifier is invalid", + Additional: []string{ + "For TCP healtchecks, use the format :tcp, for example --checks 8080:tcp", + "For HTTP healthchecks, use the format :http, for example 8080, :http or :http:, for example --checks 8080:http:/health", + }, + Orig: nil, + Solution: "Fix the flag --checks-grace-period to match an existing healthcheck or remove the grace period, and try again", + } + } + case healthcheck.HasTcp(): + switch len(parts) { + case 1: + return port == *healthcheck.GetTcp().Port, nil + case 2: + return port == *healthcheck.GetTcp().Port && strings.ToLower(parts[1]) == "tcp", nil + default: + return false, &errors.CLIError{ + What: "Invalid grace period", + Why: "the healthcheck identifier is invalid", + Additional: []string{ + "For TCP healtchecks, use the format :tcp, for example --checks 8080:tcp", + "For HTTP healthchecks, use the format :http, for example 8080, :http or :http:, for example --checks 8080:http:/health", + }, + Orig: nil, + Solution: "Fix the flag --checks-grace-period to match an existing healthcheck or remove the grace period, and try again", + } + } + } + return false, nil +} + // Parse --regions func (h *ServiceHandler) parseRegions(flags *pflag.FlagSet, currentRegions []string) ([]string, error) { newRegions, err := parseListFlags("regions", flags_list.NewRegionsListFromFlags, flags, currentRegions)