168 lines
4.6 KiB
Go
168 lines
4.6 KiB
Go
package services
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/texm/dokku-go"
|
|
"gitlab.com/texm/shokku/internal/env"
|
|
"gitlab.com/texm/shokku/internal/models"
|
|
"gitlab.com/texm/shokku/internal/server/dto"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
ErrDestroyingLinkedService = echo.NewHTTPError(http.StatusBadRequest,
|
|
"cannot destroy a linked service")
|
|
ErrServiceNameTaken = echo.NewHTTPError(http.StatusBadRequest,
|
|
"service name exists")
|
|
)
|
|
|
|
func manageService(e *env.Env, c echo.Context, mgmt string, flags string) error {
|
|
var req dto.GenericServiceRequest
|
|
if err := dto.BindRequest(c, &req); err != nil {
|
|
return err.ToHTTP()
|
|
}
|
|
|
|
cmd := fmt.Sprintf("%s:%s %s %s", req.Type, mgmt, req.Name, flags)
|
|
if _, err := e.Dokku.Exec(cmd); err != nil {
|
|
return fmt.Errorf(mgmt+"ing service", err)
|
|
}
|
|
|
|
return c.NoContent(http.StatusOK)
|
|
}
|
|
|
|
func StartService(e *env.Env, c echo.Context) error {
|
|
return manageService(e, c, "start", "")
|
|
}
|
|
func StopService(e *env.Env, c echo.Context) error {
|
|
return manageService(e, c, "stop", "")
|
|
}
|
|
func RestartService(e *env.Env, c echo.Context) error {
|
|
return manageService(e, c, "restart", "")
|
|
}
|
|
|
|
func DestroyService(e *env.Env, c echo.Context) error {
|
|
var req dto.GenericServiceRequest
|
|
if err := dto.BindRequest(c, &req); err != nil {
|
|
return err.ToHTTP()
|
|
}
|
|
|
|
dbSvc, lookupErr := lookupDBServiceByName(e, req.Name)
|
|
if lookupErr != nil {
|
|
return echo.ErrNotFound
|
|
}
|
|
|
|
cmd := fmt.Sprintf("%s:destroy %s -f", req.Type, req.Name)
|
|
if _, err := e.Dokku.Exec(cmd); err != nil {
|
|
var dokkuErr *dokku.ExitCodeError
|
|
if errors.As(err, &dokkuErr) {
|
|
if strings.HasSuffix(dokkuErr.Output(), "Cannot delete linked service") {
|
|
return ErrDestroyingLinkedService
|
|
}
|
|
}
|
|
return fmt.Errorf("destroying service: %w", err)
|
|
}
|
|
|
|
if err := e.DB.Delete(&dbSvc).Error; err != nil {
|
|
log.Error().Err(err).Interface("svc", dbSvc).Msg("error deleting service")
|
|
return echo.ErrInternalServerError
|
|
}
|
|
|
|
return c.NoContent(http.StatusOK)
|
|
}
|
|
|
|
// use short opt strings otherwise arg parse is funky
|
|
func maybeAppendStringOptionFlag(flags *[]string, opt string, cfgOption *string) {
|
|
if cfgOption != nil {
|
|
*flags = append(*flags, fmt.Sprintf("-%s '%s'", opt, *cfgOption))
|
|
}
|
|
}
|
|
|
|
func getServiceCreateFlags(req dto.GenericServiceCreationConfig) string {
|
|
flags := &[]string{}
|
|
|
|
if req.CustomEnv != nil {
|
|
envVars := make([]string, len(*req.CustomEnv))
|
|
for i, e := range *req.CustomEnv {
|
|
envVars[i] = fmt.Sprintf("%s=%s", e[0], e[1])
|
|
}
|
|
envStr := strings.Join(envVars, ";")
|
|
*flags = append(*flags, fmt.Sprintf("-C \"%s\"", envStr))
|
|
}
|
|
|
|
maybeAppendStringOptionFlag(flags, "c", req.ConfigOptions)
|
|
maybeAppendStringOptionFlag(flags, "i", req.Image)
|
|
maybeAppendStringOptionFlag(flags, "m", req.MemoryLimit)
|
|
maybeAppendStringOptionFlag(flags, "p", req.Password)
|
|
maybeAppendStringOptionFlag(flags, "r", req.RootPassword)
|
|
maybeAppendStringOptionFlag(flags, "s", req.SharedMemorySize)
|
|
|
|
return strings.Join(*flags, " ")
|
|
}
|
|
|
|
func CreateNewGenericService(e *env.Env, c echo.Context) error {
|
|
var req dto.CreateGenericServiceRequest
|
|
if err := dto.BindRequest(c, &req); err != nil {
|
|
log.Debug().Err(err.ToHTTP()).Interface("req", req).Msgf("req failed")
|
|
return err.ToHTTP()
|
|
}
|
|
|
|
dbSvc := models.Service{
|
|
Name: req.Name,
|
|
Type: req.ServiceType,
|
|
}
|
|
if e.DB.Where("name = ?", req.Name).Find(&dbSvc).RowsAffected != 0 {
|
|
return ErrServiceNameTaken
|
|
}
|
|
|
|
e.DB.Save(&dbSvc)
|
|
|
|
flags := getServiceCreateFlags(req.Config)
|
|
cmd := fmt.Sprintf("%s:create %s %s", req.ServiceType, req.Name, flags)
|
|
_, err := e.Dokku.Exec(cmd)
|
|
if err != nil {
|
|
return fmt.Errorf("creating %s service: %w", req.ServiceType, err)
|
|
}
|
|
|
|
return c.NoContent(http.StatusOK)
|
|
}
|
|
|
|
func CloneService(e *env.Env, c echo.Context) error {
|
|
var req dto.CloneServiceRequest
|
|
if err := dto.BindRequest(c, &req); err != nil {
|
|
return err.ToHTTP()
|
|
}
|
|
|
|
dbSvc, notFoundErr := lookupDBServiceByName(e, req.Name)
|
|
if notFoundErr != nil {
|
|
return echo.NewHTTPError(http.StatusNotFound, "service not found")
|
|
}
|
|
|
|
_, notFoundErr = lookupDBServiceByName(e, req.NewName)
|
|
if notFoundErr == nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "new service name exists")
|
|
}
|
|
|
|
cmd := fmt.Sprintf("%s:clone %s %s", dbSvc.Type, req.Name, req.NewName)
|
|
_, err := e.Dokku.Exec(cmd)
|
|
if err != nil {
|
|
return fmt.Errorf("cloning %s service: %w", dbSvc.Type, err)
|
|
}
|
|
|
|
newSvc := models.Service{
|
|
Name: req.NewName,
|
|
Type: dbSvc.Type,
|
|
}
|
|
if err := e.DB.Save(&newSvc).Error; err != nil {
|
|
log.Error().Err(err).
|
|
Str("name", req.NewName).Str("type", dbSvc.Type).
|
|
Msg("failed to save cloned service to db")
|
|
return echo.ErrInternalServerError
|
|
}
|
|
|
|
return c.NoContent(http.StatusOK)
|
|
}
|