init from gitlab

This commit is contained in:
texm
2023-04-25 14:33:14 +08:00
parent 6a85a41ff0
commit c8202a5c82
281 changed files with 19861 additions and 1 deletions

View File

@@ -0,0 +1,209 @@
package services
import (
"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/server/commands"
"gitlab.com/texm/shokku/internal/server/dto"
"net/http"
)
func GetServiceBackupReport(e *env.Env, c echo.Context) error {
var req dto.GetServiceBackupReportRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbSvc, err := lookupDBServiceByName(e, req.Name)
if err != nil {
return echo.ErrNotFound
}
cmd := fmt.Sprintf("%s:backup-schedule-cat %s", dbSvc.Type, req.Name)
backupSchedule, err := e.Dokku.Exec(cmd)
if err != nil {
backupSchedule = ""
}
report := dto.ServiceBackupReport{
AuthSet: dbSvc.BackupAuthSet,
EncryptionSet: dbSvc.BackupEncryptionSet,
Bucket: dbSvc.BackupBucket,
Schedule: backupSchedule,
}
return c.JSON(http.StatusOK, dto.GetServiceBackupReportResponse{
Report: report,
})
}
func SetServiceBackupAuth(e *env.Env, c echo.Context) error {
var req dto.SetServiceBackupsAuthRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbSvc, err := lookupDBServiceByName(e, req.Name)
if err != nil {
return echo.ErrNotFound
}
cfg := req.Config
args := fmt.Sprintf("%s %s %s %s %s", cfg.AccessKeyId, cfg.SecretKey,
cfg.Region, cfg.SignatureVersion, cfg.EndpointUrl)
cmd := fmt.Sprintf("%s:backup-auth %s %s", dbSvc.Type, req.Name, args)
if _, execErr := e.Dokku.Exec(cmd); execErr != nil {
return fmt.Errorf("setting backup auth: %w", execErr)
}
dbSvc.BackupAuthSet = true
if err := e.DB.Save(&dbSvc).Error; err != nil {
log.Error().Err(err).Msg("error updating service backup auth")
return echo.ErrInternalServerError
}
return c.NoContent(http.StatusOK)
}
func SetServiceBackupBucket(e *env.Env, c echo.Context) error {
var req dto.SetServiceBackupsBucketRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbSvc, err := lookupDBServiceByName(e, req.Name)
if err != nil {
return echo.ErrNotFound
}
dbSvc.BackupBucket = req.Bucket
if dbErr := e.DB.Save(&dbSvc).Error; dbErr != nil {
log.Error().Err(dbErr).Msg("error updating service backup bucket")
return echo.ErrInternalServerError
}
return c.NoContent(http.StatusOK)
}
func RunServiceBackup(e *env.Env, c echo.Context) error {
var req dto.RunServiceBackupRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbSvc, err := lookupDBServiceByName(e, req.Name)
if err != nil {
return echo.ErrNotFound
}
if dbSvc.BackupBucket == "" {
return echo.NewHTTPError(http.StatusBadRequest, "backup bucket not set")
} else if !dbSvc.BackupAuthSet {
return echo.NewHTTPError(http.StatusBadRequest, "backup auth not set")
}
dokkuCmd := fmt.Sprintf("%s:backup %s %s", dbSvc.Type, dbSvc.Name, dbSvc.BackupBucket)
cmd := func() (*dokku.CommandOutputStream, error) {
return e.Dokku.ExecStreaming(dokkuCmd)
}
return c.JSON(http.StatusOK, dto.CommandExecutionResponse{
ExecutionID: commands.RequestExecution(cmd, nil),
})
}
func SetServiceBackupSchedule(e *env.Env, c echo.Context) error {
var req dto.SetServiceBackupsScheduleRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbSvc, err := lookupDBServiceByName(e, req.Name)
if err != nil {
return echo.ErrNotFound
}
if dbSvc.BackupBucket == "" {
return echo.NewHTTPError(http.StatusBadRequest,
"service backup bucket not set")
}
cmd := fmt.Sprintf(`%s:backup-schedule %s "%s" %s`, dbSvc.Type,
req.Name, req.Schedule, dbSvc.BackupBucket)
if out, err := e.Dokku.Exec(cmd); err != nil {
log.Debug().Str("output", out).Msg("backup schedule output")
return fmt.Errorf("setting backup schedule: %w", err)
}
return c.NoContent(http.StatusOK)
}
func RemoveServiceBackupSchedule(e *env.Env, c echo.Context) error {
var req dto.ManageServiceRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
cmd := fmt.Sprintf(`%s:backup-unschedule %s`, req.Type, req.Name)
if out, err := e.Dokku.Exec(cmd); err != nil {
log.Debug().Str("output", out).Msg("backup schedule output")
return fmt.Errorf("removing backup schedule: %w", err)
}
return c.NoContent(http.StatusOK)
}
func SetServiceBackupEncryption(e *env.Env, c echo.Context) error {
var req dto.SetServiceBackupsEncryptionRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbSvc, err := lookupDBServiceByName(e, req.Name)
if err != nil {
return echo.ErrNotFound
}
cmd := fmt.Sprintf(`%s:backup-set-encryption %s %s`, dbSvc.Type,
req.Name, req.Passphrase)
if out, err := e.Dokku.Exec(cmd); err != nil {
log.Debug().Str("output", out).Msg("set backup encryption output")
return fmt.Errorf("setting backup encryption: %w", err)
}
dbSvc.BackupEncryptionSet = true
if saveErr := e.DB.Save(&dbSvc).Error; saveErr != nil {
log.Error().Err(saveErr).Msg("error updating service backup encryption")
return echo.ErrInternalServerError
}
return c.NoContent(http.StatusOK)
}
func RemoveServiceBackupEncryption(e *env.Env, c echo.Context) error {
var req dto.ManageServiceRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbSvc, err := lookupDBServiceByName(e, req.Name)
if err != nil {
return echo.ErrNotFound
}
cmd := fmt.Sprintf(`%s:backup-unset-encryption %s`, req.Type, req.Name)
if out, err := e.Dokku.Exec(cmd); err != nil {
log.Debug().Str("output", out).Msg("unset backup encryption output")
return fmt.Errorf("removing backup encryption: %w", err)
}
dbSvc.BackupEncryptionSet = false
if saveErr := e.DB.Save(&dbSvc).Error; saveErr != nil {
log.Error().Err(saveErr).Msg("error updating service backup encryption")
return echo.ErrInternalServerError
}
return c.NoContent(http.StatusOK)
}

View File

@@ -0,0 +1,167 @@
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)
}

View File

@@ -0,0 +1,34 @@
package services
import (
"github.com/labstack/echo/v4"
"gitlab.com/texm/shokku/internal/env"
)
func RegisterRoutes(e *env.Env, g *echo.Group) {
g.GET("/list", e.H(ListServices))
g.GET("/info", e.H(GetServiceInfo))
g.GET("/type", e.H(GetServiceType))
g.GET("/logs", e.H(GetServiceLogs))
g.POST("/create", e.H(CreateNewGenericService))
g.POST("/clone", e.H(CloneService))
g.POST("/start", e.H(StartService))
g.POST("/stop", e.H(StopService))
g.POST("/restart", e.H(RestartService))
g.POST("/destroy", e.H(DestroyService))
g.POST("/link", e.H(LinkGenericServiceToApp))
g.POST("/unlink", e.H(UnlinkGenericServiceFromApp))
g.GET("/linked-apps", e.H(GetServiceLinkedApps))
backups := g.Group("/backups")
backups.GET("/report", e.H(GetServiceBackupReport))
backups.POST("/auth", e.H(SetServiceBackupAuth))
backups.POST("/bucket", e.H(SetServiceBackupBucket))
backups.POST("/run", e.H(RunServiceBackup))
backups.POST("/schedule", e.H(SetServiceBackupSchedule))
backups.DELETE("/schedule", e.H(RemoveServiceBackupSchedule))
backups.POST("/encryption", e.H(SetServiceBackupEncryption))
backups.DELETE("/encryption", e.H(RemoveServiceBackupEncryption))
}

View File

@@ -0,0 +1,220 @@
package services
import (
"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/commands"
"gitlab.com/texm/shokku/internal/server/dto"
"net/http"
"strings"
)
var (
dokkuErrPrefix = "! "
serviceTypes = []string{"redis", "postgres", "mysql", "mongo"}
)
func lookupDBServiceByName(e *env.Env, name string) (*models.Service, error) {
dbSvc := models.Service{
Name: name,
}
res := e.DB.Where("name = ?", name).Find(&dbSvc)
if res.Error != nil {
return nil, res.Error
}
if res.RowsAffected == 0 {
return nil, fmt.Errorf("no service found for %s", name)
}
return &dbSvc, nil
}
func splitDokkuListOutput(output string) ([]string, error) {
if strings.HasPrefix(output, dokkuErrPrefix) {
return nil, nil
}
if output == "" {
return []string{}, nil
}
return strings.Split(output, "\n"), nil
}
func getServiceAppLinks(e *env.Env, serviceName string, serviceType string) ([]string, error) {
linksCmd := fmt.Sprintf("%s:links %s --quiet", serviceType, serviceName)
out, err := e.Dokku.Exec(linksCmd)
if err != nil {
return nil, err
}
return splitDokkuListOutput(out)
}
func getServiceList(e *env.Env, serviceType string) ([]string, error) {
listCmd := fmt.Sprintf("%s:list --quiet", serviceType)
out, err := e.Dokku.Exec(listCmd)
if err != nil {
return nil, err
}
if strings.Contains(out, "There are no") {
return []string{}, nil
}
return splitDokkuListOutput(out)
}
func ListServices(e *env.Env, c echo.Context) error {
serviceList := []dto.ServiceInfo{}
for _, serviceType := range serviceTypes {
services, err := getServiceList(e, serviceType)
if err != nil {
return fmt.Errorf("getting list for %s services: %w", serviceType, err)
}
for _, name := range services {
serviceList = append(serviceList, dto.ServiceInfo{
Name: name,
Type: serviceType,
})
}
}
return c.JSON(http.StatusOK, dto.ListServicesResponse{
Services: serviceList,
})
}
func GetServiceType(e *env.Env, c echo.Context) error {
var req dto.GetServiceTypeRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbSvc, err := lookupDBServiceByName(e, req.Name)
if err != nil {
return echo.ErrNotFound
}
return c.JSON(http.StatusOK, dto.GetServiceTypeResponse{
Type: dbSvc.Type,
})
}
func GetServiceInfo(e *env.Env, c echo.Context) error {
var req dto.GenericServiceRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
info := map[string]string{
"version": "", "internal-ip": "", "status": "", // "dsn": "",
}
for key := range info {
cmd := fmt.Sprintf("%s:info %s --%s", req.Type, req.Name, key)
out, err := e.Dokku.Exec(cmd)
if err != nil {
return fmt.Errorf("getting service info: %w", err)
}
info[key] = out
}
return c.JSON(http.StatusOK, dto.GetServiceInfoResponse{
Info: info,
})
}
func getGenericLinkFlags(req dto.LinkGenericServiceToAppRequest) string {
var flags []string
if req.Alias != "" {
flag := fmt.Sprintf("--alias %s", req.Alias)
flags = append(flags, flag)
}
if req.QueryString != "" {
flag := fmt.Sprintf("--querystring %s", req.QueryString)
flags = append(flags, flag)
}
return strings.Join(flags, " ")
}
func LinkGenericServiceToApp(e *env.Env, c echo.Context) error {
var req dto.LinkGenericServiceToAppRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbSvc, err := lookupDBServiceByName(e, req.ServiceName)
if err != nil {
return echo.ErrNotFound
}
flags := getGenericLinkFlags(req)
linkCmd := fmt.Sprintf("%s:link %s %s %s",
dbSvc.Type, dbSvc.Name, req.AppName, flags)
cmd := func() (*dokku.CommandOutputStream, error) {
return e.Dokku.ExecStreaming(linkCmd)
}
return c.JSON(http.StatusOK, dto.CommandExecutionResponse{
ExecutionID: commands.RequestExecution(cmd, nil),
})
}
func UnlinkGenericServiceFromApp(e *env.Env, c echo.Context) error {
var req dto.LinkGenericServiceToAppRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbSvc, err := lookupDBServiceByName(e, req.ServiceName)
if err != nil {
return echo.ErrNotFound
}
unlinkCmd := fmt.Sprintf("%s:unlink %s %s", dbSvc.Type,
dbSvc.Name, req.AppName)
cmd := func() (*dokku.CommandOutputStream, error) {
return e.Dokku.ExecStreaming(unlinkCmd)
}
return c.JSON(http.StatusOK, dto.CommandExecutionResponse{
ExecutionID: commands.RequestExecution(cmd, nil),
})
}
func GetServiceLinkedApps(e *env.Env, c echo.Context) error {
var req dto.GenericServiceRequest
if err := dto.BindRequest(c, &req); err != nil {
log.Error().Err(err.ToHTTP()).Msg("error")
return err.ToHTTP()
}
apps, err := getServiceAppLinks(e, req.Name, req.Type)
if err != nil {
return fmt.Errorf("getting linked apps: %w", err)
}
return c.JSON(http.StatusOK, dto.GetServiceLinkedAppsResponse{
Apps: apps,
})
}
func GetServiceLogs(e *env.Env, c echo.Context) error {
var req dto.GenericServiceRequest
if err := dto.BindRequest(c, &req); err != nil {
log.Error().Err(err.ToHTTP()).Msg("error")
return err.ToHTTP()
}
cmd := fmt.Sprintf("%s:logs %s", req.Type, req.Name)
out, err := e.Dokku.Exec(cmd)
if err != nil {
return fmt.Errorf("getting linked apps: %w", err)
}
logs := strings.Split(out, "\n")
return c.JSON(http.StatusOK, dto.GetServiceLogsResponse{
Logs: logs,
})
}