feat: implement healthcheck support

This commit is contained in:
Jose Diaz-Gonzalez
2024-01-18 18:42:55 -05:00
parent da4771e6e4
commit 8762bede07
3 changed files with 133 additions and 1 deletions

View File

@@ -65,6 +65,42 @@ type Formation struct {
// MaxParallel is the maximum number of processes to start in parallel
MaxParallel *int `json:"max_parallel"`
}
type Healthcheck struct {
Attempts int32 `json:"attempts,omitempty"`
Command []string `json:"command,omitempty"`
Content string `json:"content,omitempty"`
HTTPHeaders []HTTPHeader `json:"httpHeaders,omitempty"`
InitialDelay int32 `json:"initialDelay,omitempty"`
Listening bool `json:"listening,omitempty"`
Name string `json:"name,omitempty"`
Path string `json:"path,omitempty"`
Port int `json:"port,omitempty"`
Scheme string `json:"scheme,omitempty"`
Timeout int32 `json:"timeout,omitempty"`
Type HealthcheckType `json:"type,omitempty"`
Uptime int32 `json:"uptime,omitempty"`
Wait int32 `json:"wait,omitempty"`
Warn bool `json:"warn,omitempty"`
OnFailure *OnFailure `json:"onFailure,omitempty"`
}
type HealthcheckType string
const (
HealthcheckType_Liveness HealthcheckType = "liveness"
HealthcheckType_Readiness HealthcheckType = "readiness"
HealthcheckType_Startup HealthcheckType = "startup"
)
type HTTPHeader struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
}
type OnFailure struct {
Command []string `json:"command,omitempty"`
Url string `json:"url,omitempty"`
}
// Healthcheck is a struct that represents a single healthcheck from an app.json file
type Healthcheck struct {

View File

@@ -7,6 +7,7 @@ import (
"path/filepath"
"strings"
appjson "github.com/dokku/dokku/plugins/app-json"
"github.com/dokku/dokku/plugins/common"
traefikv1alpha1 "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
orderedmap "github.com/wk8/go-ordered-map/v2"
@@ -45,6 +46,7 @@ type Deployment struct {
Image string
ImagePullSecrets string
ImageSourceType string
Healthchecks []appjson.Healthcheck
Namespace string
PrimaryPort int32
PortMaps []PortMap
@@ -402,6 +404,89 @@ func templateKubernetesDeployment(input Deployment) (appsv1.Deployment, error) {
deployment.Spec.Template.Spec.Containers[0].Args = input.Command
}
if len(input.Healthchecks) > 0 {
livenessChecks := []corev1.Probe{}
readinessChecks := []corev1.Probe{}
startupChecks := []corev1.Probe{}
uptimeSeconds := []int32{}
for _, healthcheck := range input.Healthchecks {
probe := corev1.Probe{
ProbeHandler: corev1.ProbeHandler{},
InitialDelaySeconds: healthcheck.InitialDelay,
PeriodSeconds: healthcheck.Wait,
TimeoutSeconds: healthcheck.Timeout,
FailureThreshold: healthcheck.Attempts,
SuccessThreshold: int32(1),
}
if len(healthcheck.Command) > 0 {
probe.ProbeHandler.Exec = &corev1.ExecAction{
Command: healthcheck.Command,
}
} else if healthcheck.Listening {
probe.ProbeHandler.TCPSocket = &corev1.TCPSocketAction{
Port: intstr.FromInt32(input.PrimaryPort),
}
for _, header := range healthcheck.HTTPHeaders {
if header.Name == "Host" {
probe.ProbeHandler.TCPSocket.Host = header.Value
}
}
} else if healthcheck.Path != "" {
probe.ProbeHandler.HTTPGet = &corev1.HTTPGetAction{
Path: healthcheck.Path,
Port: intstr.FromInt32(input.PrimaryPort),
HTTPHeaders: []corev1.HTTPHeader{},
}
if healthcheck.Scheme != "" {
probe.ProbeHandler.HTTPGet.Scheme = corev1.URIScheme(strings.ToUpper(healthcheck.Scheme))
}
for _, header := range healthcheck.HTTPHeaders {
probe.ProbeHandler.HTTPGet.HTTPHeaders = append(probe.ProbeHandler.HTTPGet.HTTPHeaders, corev1.HTTPHeader{
Name: header.Name,
Value: header.Value,
})
}
} else if healthcheck.Uptime > 0 {
uptimeSeconds = append(uptimeSeconds, healthcheck.Uptime)
}
if healthcheck.Type == appjson.HealthcheckType_Liveness {
livenessChecks = append(livenessChecks, probe)
} else if healthcheck.Type == appjson.HealthcheckType_Readiness {
readinessChecks = append(readinessChecks, probe)
} else if healthcheck.Type == appjson.HealthcheckType_Startup {
startupChecks = append(startupChecks, probe)
}
}
if len(livenessChecks) > 1 {
common.LogWarn("Multiple liveness checks are not supported, only the first one will be used")
}
if len(readinessChecks) > 1 {
common.LogWarn("Multiple readiness checks are not supported, only the first one will be used")
}
if len(startupChecks) > 1 {
common.LogWarn("Multiple startup checks are not supported, only the first one will be used")
}
if len(uptimeSeconds) > 1 {
common.LogWarn("Multiple uptime checks are not supported, only the first one will be used")
}
if len(livenessChecks) > 0 {
deployment.Spec.Template.Spec.Containers[0].LivenessProbe = &livenessChecks[0]
}
if len(readinessChecks) > 0 {
deployment.Spec.Template.Spec.Containers[0].ReadinessProbe = &readinessChecks[0]
}
if len(startupChecks) > 0 {
deployment.Spec.Template.Spec.Containers[0].StartupProbe = &startupChecks[0]
}
if len(uptimeSeconds) > 0 {
deployment.Spec.MinReadySeconds = uptimeSeconds[0]
}
}
if input.ProcessType == "web" {
for _, portMap := range input.PortMaps {
protocol := "TCP"

View File

@@ -16,6 +16,7 @@ import (
"syscall"
"time"
appjson "github.com/dokku/dokku/plugins/app-json"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/config"
"github.com/dokku/dokku/plugins/cron"
@@ -221,6 +222,11 @@ data:
}
}
appJSON, err := appjson.GetAppJSON(appName)
if err != nil {
return fmt.Errorf("Error getting app.json for deployment: %w", err)
}
workingDir := common.GetWorkingDir(appName, image)
deployments := map[string]appsv1.Deployment{}
i := 0
@@ -239,7 +245,11 @@ data:
i++
replicaCountPlaceholder := int32(i * 1000)
// todo: implement healthchecks
healthchecks, ok := appJSON.Healthchecks[processType]
if !ok {
healthchecks = []appjson.Healthcheck{}
}
// todo: implement deployment annotations
// todo: implement pod annotations
// todo: implement volumes
@@ -249,6 +259,7 @@ data:
Image: image,
ImagePullSecrets: imagePullSecrets,
ImageSourceType: imageSourceType,
Healthchecks: healthchecks,
Namespace: namespace,
PrimaryPort: primaryPort,
PortMaps: portMaps,