diff --git a/plugins/app-json/appjson.go b/plugins/app-json/appjson.go index cba952105..5b11c2492 100644 --- a/plugins/app-json/appjson.go +++ b/plugins/app-json/appjson.go @@ -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 { diff --git a/plugins/scheduler-k3s/template.go b/plugins/scheduler-k3s/template.go index f5e63dc55..fca23d116 100644 --- a/plugins/scheduler-k3s/template.go +++ b/plugins/scheduler-k3s/template.go @@ -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" diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index c8bd628f2..2d4d751cb 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -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,