feat: implement most of deployments

The big feature missing from deployments today is healthchecks, which require extra parsing of the app.json format since the healthchecks there aren't immediately serializable to kubernetes deployment healthchecks.

Other smaller missing functionality from the existing OSS kubernetes scheduler are:

- The ability to set custom pod annotations
- The ability to set custom deployment annotations
- The ability to mount volumes

Exposing these pieces of functionality will come at a later date, if at all.
This commit is contained in:
Jose Diaz-Gonzalez
2024-01-17 18:06:18 -05:00
parent 6c234c66a3
commit 6fb03eda4e
11 changed files with 1810 additions and 57 deletions

View File

@@ -0,0 +1,164 @@
package scheduler_k3s
import (
"context"
"encoding/json"
"fmt"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/config"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"mvdan.cc/sh/v3/shell"
)
// StartCommandInput contains all the information needed to get the start command
type StartCommandInput struct {
// AppName is the name of the app
AppName string
// ProcessType is the process type
ProcessType string
// ImageSourceType is the image source type
ImageSourceType string
// Port is the port
Port int32
// Env is the environment variables
Env map[string]string
}
// StartCommandOutput contains the start command
type StartCommandOutput struct {
// Command is the start command
Command []string
}
func NewClient() (*kubernetes.Clientset, error) {
config, err := clientcmd.BuildConfigFromFlags("", KubeConfigPath)
if err != nil {
return &kubernetes.Clientset{}, err
}
return kubernetes.NewForConfig(config)
}
func createKubernetesNamespace(ctx context.Context, namespaceName string) error {
client, err := NewClient()
if err != nil {
return err
}
namespaces, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
if err != nil {
return err
}
for _, namespace := range namespaces.Items {
if namespace.Name == namespaceName {
return nil
}
}
namespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespaceName,
},
}
_, err = client.CoreV1().Namespaces().Create(ctx, namespace, metav1.CreateOptions{})
if err != nil {
return err
}
return nil
}
func extractStartCommand(input StartCommandInput) string {
command := ""
if input.ImageSourceType == "herokuish" {
command = "/start " + input.ProcessType
}
startCommandOverride, ok := config.Get(input.AppName, "DOKKU_START_CMD")
if ok {
command = startCommandOverride
}
if input.ImageSourceType == "herokuish" {
return command
}
if input.ImageSourceType == "dockerfile" {
startCommandOverride, ok := config.Get(input.AppName, "DOKKU_DOCKERFILE_START_CMD")
if ok {
command = startCommandOverride
}
}
if command == "" {
procfileStartCommand, _ := common.PlugnTriggerOutputAsString("procfile-get-command", []string{input.AppName, input.ProcessType, fmt.Sprint(input.Port)}...)
if procfileStartCommand != "" {
command = procfileStartCommand
}
}
return command
}
type PortMap struct {
ContainerPort int32 `json:"container_port"`
HostPort int32 `json:"host_port"`
Scheme string `json:"scheme"`
}
func (p PortMap) IsAllowedHttp() bool {
return p.Scheme == "http" || p.ContainerPort == 80
}
func (p PortMap) IsAllowedHttps() bool {
return p.Scheme == "https" || p.ContainerPort == 443
}
func getPortMaps(appName string) ([]PortMap, error) {
portMaps := []PortMap{}
output, err := common.PlugnTriggerOutputAsString("ports-get", []string{appName, "json"}...)
if err != nil {
return portMaps, err
}
err = json.Unmarshal([]byte(output), &portMaps)
if err != nil {
return portMaps, err
}
allowedMappings := []PortMap{}
for _, portMap := range portMaps {
if !portMap.IsAllowedHttp() && !portMap.IsAllowedHttps() {
// todo: log warning
continue
}
allowedMappings = append(allowedMappings, portMap)
}
return allowedMappings, nil
}
func getStartCommand(input StartCommandInput) (StartCommandOutput, error) {
command := extractStartCommand(input)
fields, err := shell.Fields(command, func(name string) string {
if name == "PORT" {
return fmt.Sprint(input.Port)
}
return input.Env[name]
})
if err != nil {
return StartCommandOutput{}, err
}
return StartCommandOutput{
Command: fields,
}, nil
}