diff --git a/plugins/scheduler-k3s/functions.go b/plugins/scheduler-k3s/functions.go index d4fb376ac..7b95ddaa4 100644 --- a/plugins/scheduler-k3s/functions.go +++ b/plugins/scheduler-k3s/functions.go @@ -4,21 +4,43 @@ import ( "context" "encoding/json" "fmt" + "os" + "time" "github.com/dokku/dokku/plugins/common" - "github.com/dokku/dokku/plugins/config" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/tools/remotecommand" + "k8s.io/kubectl/pkg/util/term" + "k8s.io/kubernetes/pkg/client/conditions" "mvdan.cc/sh/v3/shell" ) +type EnterPodInput struct { + AppName string + Clientset KubernetesClient + Command []string + Entrypoint string + ProcessType string + SelectedPod v1.Pod +} + +type GetPodInput struct { + Clientset KubernetesClient + Namespace string + Selector string +} + // StartCommandInput contains all the information needed to get the start command type StartCommandInput struct { // AppName is the name of the app @@ -45,6 +67,21 @@ type KubernetesClient struct { RestConfig rest.Config } +type WaitForPodBySelectorRunningInput struct { + Clientset KubernetesClient + Namespace string + Selector string + Timeout int + Waiter func(ctx context.Context, clientset KubernetesClient, podName, namespace string) wait.ConditionWithContextFunc +} + +type WaitForPodToExistInput struct { + Clientset KubernetesClient + Namespace string + RetryCount int + Selector string +} + func NewKubernetesClient() (KubernetesClient, error) { clientConfig := KubernetesClientConfig() restConf, err := clientConfig.ClientConfig() @@ -132,15 +169,69 @@ func createKubernetesNamespace(ctx context.Context, namespaceName string) error return nil } +func enterPod(ctx context.Context, input EnterPodInput) error { + coreclient, err := corev1client.NewForConfig(&input.Clientset.RestConfig) + if err != nil { + return fmt.Errorf("Error creating corev1 client: %w", err) + } + + req := coreclient.RESTClient().Post(). + Resource("pods"). + Namespace(input.SelectedPod.Namespace). + Name(input.SelectedPod.Name). + SubResource("exec") + + req.Param("container", fmt.Sprintf("%s-%s", input.AppName, input.ProcessType)) + req.Param("stdin", "true") + req.Param("stdout", "true") + req.Param("stderr", "true") + req.Param("tty", "true") + + if input.Entrypoint != "" { + req.Param("command", input.Entrypoint) + } + for _, cmd := range input.Command { + req.Param("command", cmd) + } + + t := term.TTY{ + In: os.Stdin, + Out: os.Stdout, + Raw: true, + } + size := t.GetSize() + sizeQueue := t.MonitorSize(size) + + return t.Safe(func() error { + exec, err := remotecommand.NewSPDYExecutor(&input.Clientset.RestConfig, "POST", req.URL()) + if err != nil { + return fmt.Errorf("Error creating executor: %w", err) + } + + return exec.StreamWithContext(ctx, remotecommand.StreamOptions{ + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + Tty: true, + TerminalSizeQueue: sizeQueue, + }) + }) +} + 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 + startCommandResp, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ + Trigger: "config-get", + Args: []string{input.AppName, "DOKKU_START_CMD"}, + CaptureOutput: true, + StreamStdio: false, + }) + if err == nil && startCommandResp.ExitCode == 0 { + command = startCommandResp.Stdout } if input.ImageSourceType == "herokuish" { @@ -148,9 +239,14 @@ func extractStartCommand(input StartCommandInput) string { } if input.ImageSourceType == "dockerfile" { - startCommandOverride, ok := config.Get(input.AppName, "DOKKU_DOCKERFILE_START_CMD") - if ok { - command = startCommandOverride + startCommandDockerfileResp, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ + Trigger: "config-get", + Args: []string{input.AppName, "DOKKU_DOCKERFILE_START_CMD"}, + CaptureOutput: true, + StreamStdio: false, + }) + if err == nil && startCommandDockerfileResp.ExitCode == 0 { + command = startCommandDockerfileResp.Stdout } } @@ -221,3 +317,74 @@ func getStartCommand(input StartCommandInput) (StartCommandOutput, error) { Command: fields, }, nil } + +func getPod(ctx context.Context, input GetPodInput) (v1.PodList, error) { + listOptions := metav1.ListOptions{LabelSelector: input.Selector} + podList, err := input.Clientset.Client.CoreV1().Pods(input.Namespace).List(ctx, listOptions) + return *podList, err +} + +func isPodReady(ctx context.Context, clientset KubernetesClient, podName, namespace string) wait.ConditionWithContextFunc { + return func(ctx context.Context) (bool, error) { + fmt.Printf(".") // progress bar! + + pod, err := clientset.Client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) + if err != nil { + return false, err + } + + switch pod.Status.Phase { + case v1.PodRunning: + return true, nil + case v1.PodFailed, v1.PodSucceeded: + return false, conditions.ErrPodCompleted + } + return false, nil + } +} + +func waitForPodBySelectorRunning(ctx context.Context, input WaitForPodBySelectorRunningInput) error { + podList, err := waitForPodToExist(ctx, WaitForPodToExistInput{ + Clientset: input.Clientset, + Namespace: input.Namespace, + RetryCount: 3, + Selector: input.Selector, + }) + if err != nil { + return fmt.Errorf("Error waiting for pod to exist: %w", err) + } + + if len(podList.Items) == 0 { + return fmt.Errorf("no pods in %s with selector %s", input.Namespace, input.Selector) + } + + timeout := time.Duration(input.Timeout) * time.Second + for _, pod := range podList.Items { + if err := wait.PollUntilContextTimeout(ctx, time.Second, timeout, false, input.Waiter(ctx, input.Clientset, pod.Name, pod.Namespace)); err != nil { + print("\n") + return err + } + } + print("\n") + return nil +} + +func waitForPodToExist(ctx context.Context, input WaitForPodToExistInput) (v1.PodList, error) { + var podList v1.PodList + var err error + for i := 0; i < input.RetryCount; i++ { + podList, err = getPod(ctx, GetPodInput{ + Clientset: input.Clientset, + Namespace: input.Namespace, + Selector: input.Selector, + }) + if err == nil { + break + } + time.Sleep(1 * time.Second) + } + if err != nil { + return podList, fmt.Errorf("Error listing pods: %w", err) + } + return podList, nil +} diff --git a/plugins/scheduler-k3s/go.mod b/plugins/scheduler-k3s/go.mod index e5912efa4..677660577 100644 --- a/plugins/scheduler-k3s/go.mod +++ b/plugins/scheduler-k3s/go.mod @@ -4,7 +4,8 @@ go 1.21 require ( github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000 - github.com/dokku/dokku/plugins/config v0.0.0-20240112043630-928c388f10fa + github.com/dokku/dokku/plugins/config v0.0.0-00010101000000-000000000000 + github.com/dokku/dokku/plugins/cron v0.0.0-00010101000000-000000000000 github.com/fatih/color v1.16.0 github.com/go-resty/resty/v2 v2.11.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 @@ -54,6 +55,7 @@ require ( github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dokku/dokku/plugins/app-json v0.0.0-00010101000000-000000000000 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect @@ -111,6 +113,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -123,6 +126,7 @@ require ( github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rubenv/sql-migrate v1.5.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/columnize v2.1.2+incompatible // indirect @@ -169,4 +173,10 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) +replace github.com/dokku/dokku/plugins/app-json => ../app-json + replace github.com/dokku/dokku/plugins/common => ../common + +replace github.com/dokku/dokku/plugins/config => ../config + +replace github.com/dokku/dokku/plugins/cron => ../cron diff --git a/plugins/scheduler-k3s/go.sum b/plugins/scheduler-k3s/go.sum index 79ff56cb4..e79fd1b0a 100644 --- a/plugins/scheduler-k3s/go.sum +++ b/plugins/scheduler-k3s/go.sum @@ -102,8 +102,6 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/dokku/dokku/plugins/config v0.0.0-20240112043630-928c388f10fa h1:lIscxUTyCvJSHV9oY3JBQazshpvSc7H2kEBwIZddMfc= -github.com/dokku/dokku/plugins/config v0.0.0-20240112043630-928c388f10fa/go.mod h1:7lDnO8LtGfKX1eFKAXunqGGTqwzJNaf6V7IkKhxlN2g= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -319,6 +317,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -373,6 +373,8 @@ github.com/rancher/dynamiclistener v0.3.5 h1:5TaIHvkDGmZKvc96Huur16zfTKOiLhDtK4S github.com/rancher/dynamiclistener v0.3.5/go.mod h1:dW/YF6/m2+uEyJ5VtEcd9THxda599HP6N9dSXk81+k0= github.com/rancher/wharfie v0.6.5 h1:Xxj/ki8d4BFeQBvyViW9jX8BqxaPlZwXvRD8BG2Mm/8= github.com/rancher/wharfie v0.6.5/go.mod h1:iGXn5Y9GEhsx1h/lPUXAklK4DoY4+6NOJF+Li1j248Y= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= diff --git a/plugins/scheduler-k3s/triggers.go b/plugins/scheduler-k3s/triggers.go index b709088b4..6b6447b11 100644 --- a/plugins/scheduler-k3s/triggers.go +++ b/plugins/scheduler-k3s/triggers.go @@ -29,10 +29,6 @@ import ( corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" - corev1client "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/client-go/tools/remotecommand" - "k8s.io/kubectl/pkg/util/term" "k8s.io/kubernetes/pkg/client/conditions" "k8s.io/utils/ptr" ) @@ -69,25 +65,6 @@ func TriggerPostAppRenameSetup(oldAppName string, newAppName string) error { return nil } -// TriggerSchedulerPostDelete destroys the scheduler-k3s data for a given app container -func TriggerSchedulerPostDelete(scheduler string, appName string) error { - if scheduler != "k3s" { - return nil - } - namespace := common.PropertyGetDefault("scheduler-k3s", appName, "namespace", "default") - helmAgent, err := NewHelmAgent(namespace, DeployLogPrinter) - if err != nil { - return fmt.Errorf("Error creating helm agent: %w", err) - } - - err = helmAgent.UninstallChart(fmt.Sprintf("dokku-%s", appName)) - if err != nil { - return fmt.Errorf("Error uninstalling chart: %w", err) - } - - return nil -} - // TriggerPostDelete destroys the scheduler-k3s data for a given app container func TriggerPostDelete(appName string) error { dataErr := common.RemoveAppDataDirectory("scheduler-k3s", appName) @@ -535,64 +512,6 @@ func TriggerSchedulerEnter(scheduler string, appName string, processType string, }) } -type EnterPodInput struct { - AppName string - Clientset KubernetesClient - Command []string - Entrypoint string - ProcessType string - SelectedPod v1.Pod -} - -func enterPod(ctx context.Context, input EnterPodInput) error { - coreclient, err := corev1client.NewForConfig(&input.Clientset.RestConfig) - if err != nil { - return fmt.Errorf("Error creating corev1 client: %w", err) - } - - req := coreclient.RESTClient().Post(). - Resource("pods"). - Namespace(input.SelectedPod.Namespace). - Name(input.SelectedPod.Name). - SubResource("exec") - - req.Param("container", fmt.Sprintf("%s-%s", input.AppName, input.ProcessType)) - req.Param("stdin", "true") - req.Param("stdout", "true") - req.Param("stderr", "true") - req.Param("tty", "true") - - if input.Entrypoint != "" { - req.Param("command", input.Entrypoint) - } - for _, cmd := range input.Command { - req.Param("command", cmd) - } - - t := term.TTY{ - In: os.Stdin, - Out: os.Stdout, - Raw: true, - } - size := t.GetSize() - sizeQueue := t.MonitorSize(size) - - return t.Safe(func() error { - exec, err := remotecommand.NewSPDYExecutor(&input.Clientset.RestConfig, "POST", req.URL()) - if err != nil { - return fmt.Errorf("Error creating executor: %w", err) - } - - return exec.StreamWithContext(ctx, remotecommand.StreamOptions{ - Stdin: os.Stdin, - Stdout: os.Stdout, - Stderr: os.Stderr, - Tty: true, - TerminalSizeQueue: sizeQueue, - }) - }) -} - // TriggerSchedulerLogs displays logs for a given application func TriggerSchedulerLogs(scheduler string, appName string, processType string, tail bool, quiet bool, numLines int64) error { if scheduler != "k3s" { @@ -712,59 +631,6 @@ func TriggerSchedulerLogs(scheduler string, appName string, processType string, return nil } -// TriggerSchedulerStop stops an application -func TriggerSchedulerStop(scheduler string, appName string) error { - if scheduler != "k3s" { - return nil - } - - clientset, err := NewKubernetesClient() - if err != nil { - return fmt.Errorf("Error creating kubernetes client: %w", err) - } - - ctx, cancel := context.WithCancel(context.Background()) - signals := make(chan os.Signal, 1) - signal.Notify(signals, os.Interrupt, syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGQUIT, - syscall.SIGTERM) - go func() { - <-signals - cancel() - }() - - namespace := common.PropertyGetDefault("scheduler-k3s", appName, "namespace", "default") - deploymentList, err := clientset.Client.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{ - LabelSelector: fmt.Sprintf("dokku.com/app-name=%s", appName), - }) - if err != nil { - return fmt.Errorf("Error listing deployments: %w", err) - } - - for _, deployment := range deploymentList.Items { - processType, ok := deployment.Annotations["dokku.com/process-type"] - if !ok { - return fmt.Errorf("Deployment %s does not have a process type annotation", deployment.Name) - } - common.LogVerboseQuiet(fmt.Sprintf("Stopping %s process", processType)) - _, err := clientset.Client.AppsV1().Deployments(namespace).UpdateScale(ctx, deployment.Name, &autoscalingv1.Scale{ - ObjectMeta: metav1.ObjectMeta{ - Name: deployment.Name, - Namespace: namespace, - }, - Spec: autoscalingv1.ScaleSpec{ - Replicas: 0, - }, - }, metav1.UpdateOptions{}) - if err != nil { - return fmt.Errorf("Error updating deployment scale: %w", err) - } - } - - return nil -} - // TriggerSchedulerRun runs a command in an ephemeral container func TriggerSchedulerRun(scheduler string, appName string, envCount int, args []string) error { if scheduler != "k3s" { @@ -954,7 +820,7 @@ func TriggerSchedulerRun(scheduler string, appName string, envCount int, args [] } batchJobSelector := fmt.Sprintf("batch.kubernetes.io/job-name=%s", createdJob.Name) - podList, err := WaitForPodToExist(ctx, WaitForPodToExistInput{ + podList, err := waitForPodToExist(ctx, WaitForPodToExistInput{ Clientset: clientset, Namespace: namespace, RetryCount: 3, @@ -972,7 +838,7 @@ func TriggerSchedulerRun(scheduler string, appName string, envCount int, args [] return nil } - err = WaitForPodBySelectorRunning(ctx, WaitForPodBySelectorRunningInput{ + err = waitForPodBySelectorRunning(ctx, WaitForPodBySelectorRunningInput{ Clientset: clientset, Namespace: namespace, Selector: batchJobSelector, @@ -981,7 +847,7 @@ func TriggerSchedulerRun(scheduler string, appName string, envCount int, args [] }) if err != nil { if errors.Is(err, conditions.ErrPodCompleted) { - podList, podErr := GetPod(ctx, GetPodInput{ + podList, podErr := getPod(ctx, GetPodInput{ Clientset: clientset, Namespace: namespace, Selector: batchJobSelector, @@ -1009,7 +875,7 @@ func TriggerSchedulerRun(scheduler string, appName string, envCount int, args [] return fmt.Errorf("Error waiting for pod to be running: %w", err) } - podList, err = GetPod(ctx, GetPodInput{ + podList, err = getPod(ctx, GetPodInput{ Clientset: clientset, Namespace: namespace, Selector: batchJobSelector, @@ -1065,94 +931,74 @@ func TriggerSchedulerRun(scheduler string, appName string, envCount int, args [] return nil } -func isPodReady(ctx context.Context, clientset KubernetesClient, podName, namespace string) wait.ConditionWithContextFunc { - return func(ctx context.Context) (bool, error) { - fmt.Printf(".") // progress bar! - - pod, err := clientset.Client.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{}) - if err != nil { - return false, err - } - - switch pod.Status.Phase { - case v1.PodRunning: - return true, nil - case v1.PodFailed, v1.PodSucceeded: - return false, conditions.ErrPodCompleted - } - return false, nil - } -} - -type WaitForPodBySelectorRunningInput struct { - Clientset KubernetesClient - Namespace string - Selector string - Timeout int - Waiter func(ctx context.Context, clientset KubernetesClient, podName, namespace string) wait.ConditionWithContextFunc -} - -type WaitForPodToExistInput struct { - Clientset KubernetesClient - Namespace string - RetryCount int - Selector string -} - -func WaitForPodToExist(ctx context.Context, input WaitForPodToExistInput) (v1.PodList, error) { - var podList v1.PodList - var err error - for i := 0; i < input.RetryCount; i++ { - podList, err = GetPod(ctx, GetPodInput{ - Clientset: input.Clientset, - Namespace: input.Namespace, - Selector: input.Selector, - }) - if err == nil { - break - } - time.Sleep(1 * time.Second) +// TriggerSchedulerPostDelete destroys the scheduler-k3s data for a given app container +func TriggerSchedulerPostDelete(scheduler string, appName string) error { + if scheduler != "k3s" { + return nil } + namespace := common.PropertyGetDefault("scheduler-k3s", appName, "namespace", "default") + helmAgent, err := NewHelmAgent(namespace, DeployLogPrinter) if err != nil { - return podList, fmt.Errorf("Error listing pods: %w", err) + return fmt.Errorf("Error creating helm agent: %w", err) } - return podList, nil -} -type GetPodInput struct { - Clientset KubernetesClient - Namespace string - Selector string -} - -func GetPod(ctx context.Context, input GetPodInput) (v1.PodList, error) { - listOptions := metav1.ListOptions{LabelSelector: input.Selector} - podList, err := input.Clientset.Client.CoreV1().Pods(input.Namespace).List(ctx, listOptions) - return *podList, err -} - -func WaitForPodBySelectorRunning(ctx context.Context, input WaitForPodBySelectorRunningInput) error { - podList, err := WaitForPodToExist(ctx, WaitForPodToExistInput{ - Clientset: input.Clientset, - Namespace: input.Namespace, - RetryCount: 3, - Selector: input.Selector, - }) + err = helmAgent.UninstallChart(fmt.Sprintf("dokku-%s", appName)) if err != nil { - return fmt.Errorf("Error waiting for pod to exist: %w", err) + return fmt.Errorf("Error uninstalling chart: %w", err) + } + + return nil +} + +// TriggerSchedulerStop stops an application +func TriggerSchedulerStop(scheduler string, appName string) error { + if scheduler != "k3s" { + return nil + } + + clientset, err := NewKubernetesClient() + if err != nil { + return fmt.Errorf("Error creating kubernetes client: %w", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + signals := make(chan os.Signal, 1) + signal.Notify(signals, os.Interrupt, syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGQUIT, + syscall.SIGTERM) + go func() { + <-signals + cancel() + }() + + namespace := common.PropertyGetDefault("scheduler-k3s", appName, "namespace", "default") + deploymentList, err := clientset.Client.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("dokku.com/app-name=%s", appName), + }) + if err != nil { + return fmt.Errorf("Error listing deployments: %w", err) + } + + for _, deployment := range deploymentList.Items { + processType, ok := deployment.Annotations["dokku.com/process-type"] + if !ok { + return fmt.Errorf("Deployment %s does not have a process type annotation", deployment.Name) + } + common.LogVerboseQuiet(fmt.Sprintf("Stopping %s process", processType)) + _, err := clientset.Client.AppsV1().Deployments(namespace).UpdateScale(ctx, deployment.Name, &autoscalingv1.Scale{ + ObjectMeta: metav1.ObjectMeta{ + Name: deployment.Name, + Namespace: namespace, + }, + Spec: autoscalingv1.ScaleSpec{ + Replicas: 0, + }, + }, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("Error updating deployment scale: %w", err) + } } - if len(podList.Items) == 0 { - return fmt.Errorf("no pods in %s with selector %s", input.Namespace, input.Selector) - } - - timeout := time.Duration(input.Timeout) * time.Second - for _, pod := range podList.Items { - if err := wait.PollUntilContextTimeout(ctx, time.Second, timeout, false, input.Waiter(ctx, input.Clientset, pod.Name, pod.Namespace)); err != nil { - print("\n") - return err - } - } - print("\n") return nil }