Support tcp/http liveness/readiness probe (#1449)

This commit is contained in:
ichx
2021-11-03 23:30:38 +08:00
committed by GitHub
parent ce46a5ba01
commit d55071e9d6
14 changed files with 1529 additions and 221 deletions
+2 -57
View File
@@ -45,7 +45,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
)
/**
@@ -534,62 +533,8 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic
template.Spec.Volumes = append(template.Spec.Volumes, volumes...)
template.Spec.Affinity = ConfigAffinity(service)
// Configure the HealthCheck
// We check to see if it's blank
if !reflect.DeepEqual(service.HealthChecks.Liveness, kobject.HealthCheck{}) {
probe := api.Probe{}
if len(service.HealthChecks.Liveness.Test) > 0 {
probe.Handler = api.Handler{
Exec: &api.ExecAction{
Command: service.HealthChecks.Liveness.Test,
},
}
} else if !reflect.ValueOf(service.HealthChecks.Liveness.HTTPPath).IsZero() &&
!reflect.ValueOf(service.HealthChecks.Liveness.HTTPPort).IsZero() {
probe.Handler = api.Handler{
HTTPGet: &api.HTTPGetAction{
Path: service.HealthChecks.Liveness.HTTPPath,
Port: intstr.FromInt(int(service.HealthChecks.Liveness.HTTPPort)),
},
}
} else {
return errors.New("Health check must contain a command")
}
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.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
}
template.Spec.Containers[0].LivenessProbe = configProbe(service.HealthChecks.Liveness)
template.Spec.Containers[0].ReadinessProbe = configProbe(service.HealthChecks.Readiness)
if service.StopGracePeriod != "" {
template.Spec.TerminationGracePeriodSeconds, err = DurationStrToSecondsInt(service.StopGracePeriod)
+84 -29
View File
@@ -352,41 +352,96 @@ func TestIsDir(t *testing.T) {
}
}
// TestServiceWithoutPort this tests if Headless Service is created for services without Port.
// TestServiceWithHealthCheck this tests if Headless Service is created for services with HealthCheck.
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,
testCases := map[string]struct {
service kobject.ServiceConfig
}{
"Exec": {
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,
},
},
},
Liveness: kobject.HealthCheck{
Test: []string{"arg1", "arg2"},
Timeout: 11,
Interval: 6,
Retries: 4,
StartPeriod: 61,
},
"HTTPGet": {
service: kobject.ServiceConfig{
ContainerName: "name",
Image: "image",
ServiceType: "Headless",
HealthChecks: kobject.HealthChecks{
Readiness: kobject.HealthCheck{
HTTPPath: "/health",
HTTPPort: 8080,
Timeout: 10,
Interval: 5,
Retries: 3,
StartPeriod: 60,
},
Liveness: kobject.HealthCheck{
HTTPPath: "/ready",
HTTPPort: 8080,
Timeout: 11,
Interval: 6,
Retries: 4,
StartPeriod: 61,
},
},
},
},
"TCPSocket": {
service: kobject.ServiceConfig{
ContainerName: "name",
Image: "image",
ServiceType: "Headless",
HealthChecks: kobject.HealthChecks{
Readiness: kobject.HealthCheck{
TCPPort: 8080,
Timeout: 10,
Interval: 5,
Retries: 3,
StartPeriod: 60,
},
Liveness: kobject.HealthCheck{
TCPPort: 8080,
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)
for _, testCase := range testCases {
k := Kubernetes{}
komposeObject := kobject.KomposeObject{
ServiceConfigs: map[string]kobject.ServiceConfig{"app": testCase.service},
}
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)
}
}
}
-2
View File
@@ -1363,8 +1363,6 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject.
ImagePullPolicy(name, service),
RestartPolicy(name, service),
SecurityContext(name, service),
LivenessProbe(service),
ReadinessProbe(service),
HostName(service),
DomainName(service),
ResourcesLimits(service),
@@ -744,6 +744,84 @@ func TestServiceAccountNameOnMultipleContainers(t *testing.T) {
}
}
func TestHealthCheckOnMultipleContainers(t *testing.T) {
groupName := "pod_group"
createHealthCheck := func(TCPPort int32) kobject.HealthCheck {
return kobject.HealthCheck{
TCPPort: TCPPort,
}
}
createConfig := func(name string, livenessTCPPort, readinessTCPPort int32) kobject.ServiceConfig {
config := newSimpleServiceConfig()
config.Labels = map[string]string{compose.LabelServiceGroup: groupName}
config.Name = name
config.ContainerName = name
config.HealthChecks.Liveness = createHealthCheck(livenessTCPPort)
config.HealthChecks.Readiness = createHealthCheck(readinessTCPPort)
return config
}
testCases := map[string]struct {
komposeObject kobject.KomposeObject
opt kobject.ConvertOptions
expectedContainers map[string]api.Container
}{
"Converted multiple containers to Deployments": {
kobject.KomposeObject{
ServiceConfigs: map[string]kobject.ServiceConfig{
"app1": createConfig("app1", 8081, 9091),
"app2": createConfig("app2", 8082, 9092),
},
},
kobject.ConvertOptions{ServiceGroupMode: "label", CreateD: true},
map[string]api.Container{
"app1": {
LivenessProbe: configProbe(createHealthCheck(8081)),
ReadinessProbe: configProbe(createHealthCheck(9091)),
},
"app2": {
LivenessProbe: configProbe(createHealthCheck(8082)),
ReadinessProbe: configProbe(createHealthCheck(9092)),
},
},
},
}
for name, test := range testCases {
t.Log("Test case:", name)
k := Kubernetes{}
// Run Transform
objs, err := k.Transform(test.komposeObject, test.opt)
if err != nil {
t.Error(errors.Wrap(err, "k.Transform failed"))
}
// Check results
for _, obj := range objs {
if deployment, ok := obj.(*appsv1.Deployment); ok {
if len(deployment.Spec.Template.Spec.Containers) != len(test.expectedContainers) {
t.Errorf("Containers len is not equal, expected %d, got %d",
len(deployment.Spec.Template.Spec.Containers), len(test.expectedContainers))
}
for _, result := range deployment.Spec.Template.Spec.Containers {
expected, ok := test.expectedContainers[result.Name]
if !ok {
t.Errorf("Container %s doesn't expected", result.Name)
}
if !reflect.DeepEqual(result.LivenessProbe, expected.LivenessProbe) {
t.Errorf("Container %s: LivenessProbe expected %v returned, got %v", result.Name, expected.LivenessProbe, result.LivenessProbe)
}
if !reflect.DeepEqual(result.ReadinessProbe, expected.ReadinessProbe) {
t.Errorf("Container %s: ReadinessProbe expected %v returned, got %v", result.Name, expected.ReadinessProbe, result.ReadinessProbe)
}
}
}
}
}
}
func TestCreatePVC(t *testing.T) {
storageClassName := "custom-storage-class-name"
k := Kubernetes{}
+44 -73
View File
@@ -34,14 +34,16 @@ func AddContainer(service kobject.ServiceConfig, opt kobject.ConvertOptions) Pod
}
podSpec.Containers = append(podSpec.Containers, api.Container{
Name: name,
Image: image,
Env: envs,
Command: service.Command,
Args: service.Args,
WorkingDir: service.WorkingDir,
Stdin: service.Stdin,
TTY: service.Tty,
Name: name,
Image: image,
Env: envs,
Command: service.Command,
Args: service.Args,
WorkingDir: service.WorkingDir,
Stdin: service.Stdin,
TTY: service.Tty,
LivenessProbe: configProbe(service.HealthChecks.Liveness),
ReadinessProbe: configProbe(service.HealthChecks.Readiness),
})
podSpec.Affinity = ConfigAffinity(service)
@@ -258,75 +260,44 @@ func DomainName(service kobject.ServiceConfig) PodSpecOption {
}
}
func LivenessProbe(service kobject.ServiceConfig) PodSpecOption {
return func(podSpec *PodSpec) {
// Configure the HealthCheck
// We check to see if it's blank
if !reflect.DeepEqual(service.HealthChecks.Liveness, kobject.HealthCheck{}) {
probe := api.Probe{}
if len(service.HealthChecks.Liveness.Test) > 0 {
probe.Handler = api.Handler{
Exec: &api.ExecAction{
Command: service.HealthChecks.Liveness.Test,
},
}
} else if !reflect.ValueOf(service.HealthChecks.Liveness.HTTPPath).IsZero() &&
!reflect.ValueOf(service.HealthChecks.Liveness.HTTPPort).IsZero() {
probe.Handler = api.Handler{
HTTPGet: &api.HTTPGetAction{
Path: service.HealthChecks.Liveness.HTTPPath,
Port: intstr.FromInt(int(service.HealthChecks.Liveness.HTTPPort)),
},
}
} else {
panic(errors.New("Health check must contain a command"))
}
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.Liveness.StartPeriod
for i := range podSpec.Containers {
podSpec.Containers[i].LivenessProbe = &probe
}
}
func configProbe(healthCheck kobject.HealthCheck) *api.Probe {
probe := api.Probe{}
// We check to see if it's blank or disable
if reflect.DeepEqual(healthCheck, kobject.HealthCheck{}) || healthCheck.Disable {
return nil
}
}
func ReadinessProbe(service kobject.ServiceConfig) PodSpecOption {
return func(podSpec *PodSpec) {
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 {
panic(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
for i := range podSpec.Containers {
podSpec.Containers[i].ReadinessProbe = &probeHealthCheckReadiness
}
if len(healthCheck.Test) > 0 {
probe.Handler = api.Handler{
Exec: &api.ExecAction{
Command: healthCheck.Test,
},
}
} else if !reflect.ValueOf(healthCheck.HTTPPath).IsZero() && !reflect.ValueOf(healthCheck.HTTPPort).IsZero() {
probe.Handler = api.Handler{
HTTPGet: &api.HTTPGetAction{
Path: healthCheck.HTTPPath,
Port: intstr.FromInt(int(healthCheck.HTTPPort)),
},
}
} else if !reflect.ValueOf(healthCheck.TCPPort).IsZero() {
probe.Handler = api.Handler{
TCPSocket: &api.TCPSocketAction{
Port: intstr.FromInt(int(healthCheck.TCPPort)),
},
}
} else {
panic(errors.New("Health check must contain a command"))
}
probe.TimeoutSeconds = healthCheck.Timeout
probe.PeriodSeconds = healthCheck.Interval
probe.FailureThreshold = healthCheck.Retries
// See issue: https://github.com/docker/cli/issues/116
// StartPeriod has been added to v3.4 of the compose
probe.InitialDelaySeconds = healthCheck.StartPeriod
return &probe
}
func ServiceAccountName(serviceAccountName string) PodSpecOption {