Add readiness healthcheck generation by label (#1366)

This commit is contained in:
João Vitor Paes de Barros do Carmo 2021-03-17 07:02:50 -03:00 committed by GitHub
parent 76565d80b2
commit 0036f0c32b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 184 additions and 10 deletions

View File

@ -137,7 +137,7 @@ type ServiceConfig struct {
GroupAdd []int64 `compose:"group_add"`
Volumes []Volumes `compose:""`
Secrets []dockerCliTypes.ServiceSecretConfig
HealthChecks HealthCheck `compose:""`
HealthChecks HealthChecks `compose:""`
Placement map[string]string `compose:""`
//This is for long LONG SYNTAX link(https://docs.docker.com/compose/compose-file/#long-syntax)
Configs []dockerCliTypes.ServiceConfigObjConfig `compose:""`
@ -147,6 +147,12 @@ type ServiceConfig struct {
WithKomposeAnnotation bool `compose:""`
}
// HealthChecks used to distinguish between liveness and readiness
type HealthChecks struct {
Liveness HealthCheck
Readiness HealthCheck
}
// HealthCheck the healthcheck configuration for a service
// "StartPeriod" is not yet added to compose, see:
// https://github.com/docker/cli/issues/116

View File

@ -44,6 +44,18 @@ const (
LabelImagePullSecret = "kompose.image-pull-secret"
// LabelImagePullPolicy defines Kubernetes PodSpec imagePullPolicy.
LabelImagePullPolicy = "kompose.image-pull-policy"
// HealthCheckReadinessDisable defines readiness health check disable
HealthCheckReadinessDisable = "kompose.service.healthcheck.readiness.disable"
// HealthCheckReadinessTest defines readiness health check test
HealthCheckReadinessTest = "kompose.service.healthcheck.readiness.test"
// HealthCheckReadinessInterval defines readiness health check interval
HealthCheckReadinessInterval = "kompose.service.healthcheck.readiness.interval"
// HealthCheckReadinessTimeout defines readiness health check timeout
HealthCheckReadinessTimeout = "kompose.service.healthcheck.readiness.timeout"
// HealthCheckReadinessRetries defines readiness health check retries
HealthCheckReadinessRetries = "kompose.service.healthcheck.readiness.retries"
// HealthCheckReadinessStartPeriod defines readiness health check start period
HealthCheckReadinessStartPeriod = "kompose.service.healthcheck.readiness.start_period"
// ServiceTypeHeadless ...
ServiceTypeHeadless = "Headless"

View File

@ -34,6 +34,7 @@ import (
"fmt"
shlex "github.com/google/shlex"
"github.com/kubernetes/kompose/pkg/kobject"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
@ -228,6 +229,66 @@ func loadV3Ports(ports []types.ServicePortConfig, expose []string) []kobject.Por
return komposePorts
}
/* Convert the HealthCheckConfig as designed by Docker to
a Kubernetes-compatible format.
*/
func parseHealthCheckReadiness(labels types.Labels) (kobject.HealthCheck, error) {
// initialize with CMD as default to not break at return (will be ignored if no test is informed)
test := []string{"CMD"}
var timeout, interval, retries, startPeriod int32
var disable bool
for key, value := range labels {
switch key {
case HealthCheckReadinessDisable:
disable = cast.ToBool(value)
case HealthCheckReadinessTest:
if len(value) > 0 {
test, _ = shlex.Split(value)
}
case HealthCheckReadinessInterval:
parse, err := time.ParseDuration(value)
if err != nil {
return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check interval variable")
}
interval = int32(parse.Seconds())
case HealthCheckReadinessTimeout:
parse, err := time.ParseDuration(value)
if err != nil {
return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check timeout variable")
}
timeout = int32(parse.Seconds())
case HealthCheckReadinessRetries:
retries = cast.ToInt32(value)
case HealthCheckReadinessStartPeriod:
parse, err := time.ParseDuration(value)
if err != nil {
return kobject.HealthCheck{}, errors.Wrap(err, "unable to parse health check startPeriod variable")
}
startPeriod = int32(parse.Seconds())
}
}
if test[0] == "NONE" {
disable = true
test = test[1:]
}
if test[0] == "CMD" || test[0] == "CMD-SHELL" {
test = test[1:]
}
// Due to docker/cli adding "CMD-SHELL" to the struct, we remove the first element of composeHealthCheck.Test
return kobject.HealthCheck{
Test: test,
Timeout: timeout,
Interval: interval,
Retries: retries,
StartPeriod: startPeriod,
Disable: disable,
}, nil
}
/* Convert the HealthCheckConfig as designed by Docker to
a Kubernetes-compatible format.
*/
@ -327,15 +388,24 @@ func dockerComposeToKomposeMapping(composeObject *types.Config) (kobject.Kompose
// labels
serviceConfig.DeployLabels = composeServiceConfig.Deploy.Labels
// HealthCheck
// HealthCheck Liveness
if composeServiceConfig.HealthCheck != nil && !composeServiceConfig.HealthCheck.Disable {
var err error
serviceConfig.HealthChecks, err = parseHealthCheck(*composeServiceConfig.HealthCheck)
serviceConfig.HealthChecks.Liveness, err = parseHealthCheck(*composeServiceConfig.HealthCheck)
if err != nil {
return kobject.KomposeObject{}, errors.Wrap(err, "Unable to parse health check")
}
}
// HealthCheck Readiness
var readiness, errReadiness = parseHealthCheckReadiness(*&composeServiceConfig.Labels)
if readiness.Test != nil && len(readiness.Test) > 0 && len(readiness.Test[0]) > 0 && !readiness.Disable {
serviceConfig.HealthChecks.Readiness = readiness
if errReadiness != nil {
return kobject.KomposeObject{}, errors.Wrap(errReadiness, "Unable to parse health check")
}
}
// restart-policy: deploy.restart_policy.condition will rewrite restart option
// see: https://docs.docker.com/compose/compose-file/#restart_policy
serviceConfig.Restart = composeServiceConfig.Restart

View File

@ -2,6 +2,8 @@ package testutils
import (
"errors"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@ -24,3 +26,25 @@ func CheckForHeadless(objects []runtime.Object) error {
}
return nil
}
// CheckForHealthCheckLivenessAndReadiness check if has liveness and readiness in healthcheck configured.
func CheckForHealthCheckLivenessAndReadiness(objects []runtime.Object) error {
serviceCreated := false
for _, obj := range objects {
if deployment, ok := obj.(*appsv1.Deployment); ok {
serviceCreated = true
// Check if it is a headless services
if deployment.Spec.Template.Spec.Containers[0].ReadinessProbe == nil {
return errors.New("there is not a ReadinessProbe")
}
if deployment.Spec.Template.Spec.Containers[0].LivenessProbe == nil {
return errors.New("there is not a LivenessGate")
}
}
}
if !serviceCreated {
return errors.New("no Service created")
}
return nil
}

View File

@ -510,30 +510,53 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic
template.Spec.NodeSelector = service.Placement
// Configure the HealthCheck
// We check to see if it's blank
if !reflect.DeepEqual(service.HealthChecks, kobject.HealthCheck{}) {
if !reflect.DeepEqual(service.HealthChecks.Liveness, kobject.HealthCheck{}) {
probe := api.Probe{}
if len(service.HealthChecks.Test) > 0 {
if len(service.HealthChecks.Liveness.Test) > 0 {
probe.Handler = api.Handler{
Exec: &api.ExecAction{
Command: service.HealthChecks.Test,
Command: service.HealthChecks.Liveness.Test,
},
}
} else {
return errors.New("Health check must contain a command")
}
probe.TimeoutSeconds = service.HealthChecks.Timeout
probe.PeriodSeconds = service.HealthChecks.Interval
probe.FailureThreshold = service.HealthChecks.Retries
probe.TimeoutSeconds = service.HealthChecks.Liveness.Timeout
probe.PeriodSeconds = service.HealthChecks.Liveness.Interval
probe.FailureThreshold = service.HealthChecks.Liveness.Retries
// See issue: https://github.com/docker/cli/issues/116
// StartPeriod has been added to docker/cli however, it is not yet added
// to compose. Once the feature has been implemented, this will automatically work
probe.InitialDelaySeconds = service.HealthChecks.StartPeriod
probe.InitialDelaySeconds = service.HealthChecks.Liveness.StartPeriod
template.Spec.Containers[0].LivenessProbe = &probe
}
if !reflect.DeepEqual(service.HealthChecks.Readiness, kobject.HealthCheck{}) {
probeHealthCheckReadiness := api.Probe{}
if len(service.HealthChecks.Readiness.Test) > 0 {
probeHealthCheckReadiness.Handler = api.Handler{
Exec: &api.ExecAction{
Command: service.HealthChecks.Readiness.Test,
},
}
} else {
return errors.New("Health check must contain a command")
}
probeHealthCheckReadiness.TimeoutSeconds = service.HealthChecks.Readiness.Timeout
probeHealthCheckReadiness.PeriodSeconds = service.HealthChecks.Readiness.Interval
probeHealthCheckReadiness.FailureThreshold = service.HealthChecks.Readiness.Retries
// See issue: https://github.com/docker/cli/issues/116
// StartPeriod has been added to docker/cli however, it is not yet added
// to compose. Once the feature has been implemented, this will automatically work
probeHealthCheckReadiness.InitialDelaySeconds = service.HealthChecks.Readiness.StartPeriod
template.Spec.Containers[0].ReadinessProbe = &probeHealthCheckReadiness
}
if service.StopGracePeriod != "" {
template.Spec.TerminationGracePeriodSeconds, err = DurationStrToSecondsInt(service.StopGracePeriod)

View File

@ -359,6 +359,45 @@ func TestIsDir(t *testing.T) {
}
}
// TestServiceWithoutPort this tests if Headless Service is created for services without Port.
func TestServiceWithHealthCheck(t *testing.T) {
service := kobject.ServiceConfig{
ContainerName: "name",
Image: "image",
ServiceType: "Headless",
HealthChecks: kobject.HealthChecks{
Readiness: kobject.HealthCheck{
Test: []string{"arg1", "arg2"},
Timeout: 10,
Interval: 5,
Retries: 3,
StartPeriod: 60,
},
Liveness: kobject.HealthCheck{
Test: []string{"arg1", "arg2"},
Timeout: 11,
Interval: 6,
Retries: 4,
StartPeriod: 61,
},
},
}
komposeObject := kobject.KomposeObject{
ServiceConfigs: map[string]kobject.ServiceConfig{"app": service},
}
k := Kubernetes{}
objects, err := k.Transform(komposeObject, kobject.ConvertOptions{CreateD: true, Replicas: 1})
if err != nil {
t.Error(errors.Wrap(err, "k.Transform failed"))
}
if err := testutils.CheckForHealthCheckLivenessAndReadiness(objects); err != nil {
t.Error(err)
}
}
// TestServiceWithoutPort this tests if Headless Service is created for services without Port.
func TestServiceWithoutPort(t *testing.T) {
service := kobject.ServiceConfig{