Files
dokku-ui/internal/server/api/apps/management.go
2023-04-25 14:33:14 +08:00

416 lines
10 KiB
Go

package apps
import (
"errors"
"fmt"
"github.com/rs/zerolog/log"
"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"
"time"
"github.com/labstack/echo/v4"
"github.com/texm/dokku-go"
)
const FilteredApp = "shokku"
func lookupDBAppByName(e *env.Env, name string) (*models.App, error) {
dbApp := models.App{Name: name}
res := e.DB.Where("name = ?", name).Find(&dbApp)
if res.Error != nil {
return nil, res.Error
}
if res.RowsAffected == 0 {
return nil, fmt.Errorf("no app found for %s", name)
}
return &dbApp, nil
}
func GetAppsList(e *env.Env, c echo.Context) error {
appsList, err := e.Dokku.ListApps()
apps := make([]dto.GetAppsListItem, 0)
for _, name := range appsList {
if name != FilteredApp {
apps = append(apps, dto.GetAppsListItem{
Name: name,
// TODO: get type
Type: "",
})
}
}
if err != nil && !errors.Is(err, dokku.NoDeployedAppsError) {
return fmt.Errorf("getting apps overview: %w", err)
}
return c.JSON(http.StatusOK, dto.GetAppsListResponse{
Apps: apps,
})
}
func GetAppsProcessReport(e *env.Env, c echo.Context) error {
allReports, err := e.Dokku.GetAllProcessReport()
if err != nil {
return fmt.Errorf("failed to get apps report: %w", err)
}
apps := make([]dto.GetAppOverviewResponse, 0)
for name, psReport := range allReports {
if name == FilteredApp {
continue
}
app, lookupErr := lookupDBAppByName(e, name)
if lookupErr != nil {
return fmt.Errorf("failed to lookup app %s: %w", name, lookupErr)
}
apps = append(apps, dto.GetAppOverviewResponse{
Name: name,
IsSetup: app.IsSetup,
SetupMethod: app.SetupMethod,
IsDeployed: psReport.Deployed,
IsRunning: psReport.Running,
NumProcesses: psReport.Processes,
CanScale: psReport.CanScale,
Restore: psReport.Restore,
})
}
return c.JSON(http.StatusOK, dto.GetAllAppsOverviewResponse{
Apps: apps,
})
}
func GetAppOverview(e *env.Env, c echo.Context) error {
var req dto.GetAppOverviewRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
app, lookupErr := lookupDBAppByName(e, req.Name)
if lookupErr != nil {
return fmt.Errorf("failed to lookup app %s: %w", req.Name, lookupErr)
}
psReport, psErr := e.Dokku.GetAppProcessReport(req.Name)
if psErr != nil {
return fmt.Errorf("getting apps process report: %w", psErr)
}
gitReport, gitErr := e.Dokku.GitGetAppReport(req.Name)
if gitErr != nil {
return fmt.Errorf("getting app git report: %w", gitErr)
}
return c.JSON(http.StatusOK, dto.GetAppOverviewResponse{
IsSetup: app.IsSetup,
SetupMethod: app.SetupMethod,
IsDeployed: psReport.Deployed,
GitDeployBranch: gitReport.DeployBranch,
GitLastUpdated: gitReport.LastUpdatedAt,
IsRunning: psReport.Running,
})
}
func GetAppInfo(e *env.Env, c echo.Context) error {
var req dto.GetAppInfoRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
info, err := e.Dokku.GetAppReport(req.Name)
if err != nil {
return fmt.Errorf("getting app info: %w", err)
}
res := &dto.GetAppInfoResponse{
Info: dto.AppInfo{
Name: req.Name,
Directory: info.Directory,
DeploySource: info.DeploySource,
DeploySourceMetadata: info.DeploySourceMetadata,
CreatedAt: time.UnixMilli(info.CreatedAtTimestamp),
IsLocked: info.IsLocked,
},
}
return c.JSON(http.StatusOK, res)
}
func CreateApp(e *env.Env, c echo.Context) error {
var req dto.ManageAppRequest
if reqErr := dto.BindRequest(c, &req); reqErr != nil {
log.Debug().
Err(reqErr.ToHTTP()).
Str("appName", req.Name).
Msg("bind err")
return reqErr.ToHTTP()
}
_, lookupErr := lookupDBAppByName(e, req.Name)
if lookupErr == nil {
return echo.ErrBadRequest
}
if createErr := e.Dokku.CreateApp(req.Name); createErr != nil {
return fmt.Errorf("creating app: %w", createErr)
}
if dbErr := e.DB.Create(&models.App{Name: req.Name}).Error; dbErr != nil {
log.Error().Err(dbErr).Str("name", req.Name).Msg("failed to create db app")
return echo.ErrInternalServerError
}
return c.NoContent(http.StatusOK)
}
func DestroyApp(e *env.Env, c echo.Context) error {
var req dto.DestroyAppRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbApp, dbErr := lookupDBAppByName(e, req.Name)
if dbErr != nil {
log.Error().Err(dbErr).Str("name", req.Name).Msg("failed to lookup app")
return echo.ErrNotFound
}
if err := e.Dokku.DestroyApp(req.Name); err != nil {
return fmt.Errorf("destroying app: %w", err)
}
// TODO: hard delete app
if err := e.DB.Delete(&dbApp).Error; err != nil {
log.Error().Err(err).Str("name", req.Name).Msg("failed to delete app")
return echo.ErrInternalServerError
}
return c.NoContent(http.StatusOK)
}
func RenameApp(e *env.Env, c echo.Context) error {
var req dto.RenameAppRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbApp, dbErr := lookupDBAppByName(e, req.CurrentName)
if dbErr != nil {
log.Error().Err(dbErr).Str("name", req.CurrentName).
Msg("failed to lookup app")
return echo.ErrNotFound
}
if _, newDbErr := lookupDBAppByName(e, req.NewName); newDbErr == nil {
return echo.ErrBadRequest
}
dbApp.Name = req.NewName
if saveErr := e.DB.Save(&dbApp).Error; saveErr != nil {
log.Error().Err(saveErr).
Str("name", req.NewName).
Msg("failed to save db app")
}
options := &dokku.AppManagementOptions{SkipDeploy: true}
if renameErr := e.Dokku.RenameApp(req.CurrentName, req.NewName, options); renameErr != nil {
return fmt.Errorf("renaming app: %w", renameErr)
}
return c.NoContent(http.StatusOK)
}
func StartApp(e *env.Env, c echo.Context) error {
var req dto.ManageAppRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
cmd := func() (*dokku.CommandOutputStream, error) {
return e.Dokku.StartApp(req.Name, nil)
}
return c.JSON(http.StatusOK, dto.CommandExecutionResponse{
ExecutionID: commands.RequestExecution(cmd, nil),
})
}
func StopApp(e *env.Env, c echo.Context) error {
var req dto.ManageAppRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
cmd := func() (*dokku.CommandOutputStream, error) {
return e.Dokku.StopApp(req.Name, nil)
}
return c.JSON(http.StatusOK, dto.CommandExecutionResponse{
ExecutionID: commands.RequestExecution(cmd, nil),
})
}
func RestartApp(e *env.Env, c echo.Context) error {
var req dto.ManageAppRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
cmd := func() (*dokku.CommandOutputStream, error) {
return e.Dokku.RestartApp(req.Name, nil)
}
return c.JSON(http.StatusOK, dto.CommandExecutionResponse{
ExecutionID: commands.RequestExecution(cmd, nil),
})
}
func RebuildApp(e *env.Env, c echo.Context) error {
var req dto.ManageAppRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbApp, appErr := lookupDBAppByName(e, req.Name)
if appErr != nil {
return echo.NewHTTPError(http.StatusBadRequest, "couldnt get app")
}
if !dbApp.IsSetup {
return echo.NewHTTPError(http.StatusBadRequest, "not setup")
}
var cfg models.AppSetupConfig
cfgErr := e.DB.Where("app_id = ?", dbApp.ID).First(&cfg).Error
if cfgErr != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "no setup config")
}
method := setupMethod(dbApp.SetupMethod)
var cmd commands.AsyncDokkuCommand
if method == methodGit {
cmd = func() (*dokku.CommandOutputStream, error) {
return e.Dokku.RebuildApp(req.Name, nil)
}
} else if method == methodSyncRepo {
opts := &dokku.GitSyncOptions{
Build: true,
GitRef: cfg.RepoGitRef,
}
cmd = func() (*dokku.CommandOutputStream, error) {
return e.Dokku.GitSyncAppRepo(req.Name, cfg.RepoURL, opts)
}
} else if method == methodDocker {
image := cfg.Image
cmd = func() (*dokku.CommandOutputStream, error) {
return e.Dokku.GitCreateFromImage(req.Name, image, nil)
}
} else {
log.Error().
Str("method", dbApp.SetupMethod).
Str("name", dbApp.Name).
Msg("invalid app setup method")
return echo.NewHTTPError(http.StatusBadRequest, "invalid setup method")
}
return c.JSON(http.StatusOK, dto.CommandExecutionResponse{
ExecutionID: commands.RequestExecution(cmd, nil),
})
}
func GetAppDeployChecks(e *env.Env, c echo.Context) error {
var req dto.GetAppDeployChecksRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
report, err := e.Dokku.GetAppDeployChecksReport(req.Name)
if err != nil {
return fmt.Errorf("getting app deploy checks: %w", err)
}
return c.JSON(http.StatusOK, dto.GetAppDeployChecksResponse{
AllDisabled: report.AllDisabled,
AllSkipped: report.AllSkipped,
DisabledProcesses: report.DisabledProcesses,
SkippedProcesses: report.SkippedProcesses,
})
}
func SetAppDeployChecks(e *env.Env, c echo.Context) error {
var req dto.SetAppDeployChecksRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
var err error
switch req.State {
case "enabled":
err = e.Dokku.EnableAppDeployChecks(req.Name)
case "disabled":
err = e.Dokku.DisableAppDeployChecks(req.Name)
case "skipped":
err = e.Dokku.SetAppDeployChecksSkipped(req.Name)
default:
return echo.NewHTTPError(http.StatusBadRequest, "unknown state")
}
if err != nil {
return fmt.Errorf("setting app deploy checks to %s: %w", req.State, err)
}
return c.NoContent(http.StatusOK)
}
func GetAppLogs(e *env.Env, c echo.Context) error {
var req dto.GetAppLogsRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
logs, err := e.Dokku.GetAppLogs(req.Name)
if err != nil {
if errors.Is(err, dokku.AppNotDeployedError) {
return c.JSON(http.StatusOK, dto.GetAppLogsResponse{})
}
return fmt.Errorf("getting app logs: %w", err)
}
return c.JSON(http.StatusOK, dto.GetAppLogsResponse{
Logs: strings.Split(logs, "\n"),
})
}
/*
func AppExecInProcess(e *env.Env, c echo.Context) error {
var req dto.AppExecInProcessRequest
if err := dto.BindRequest(c, &req); err != nil {
log.Debug().Str("err", err.String()).Interface("req", req).Msg("bind")
return err.ToHTTP()
}
cmd := fmt.Sprintf(`enter %s %s %s`, req.AppName, req.ProcessName, req.Command)
output, execErr := e.Dokku.Exec(cmd)
res := dto.AppExecInProcessResponse{
Output: output,
}
if execErr != nil {
res.Error = execErr.Error()
var sshExitErr *dokku.ExitCodeError
if errors.As(execErr, &sshExitErr) {
res.Error = sshExitErr.Output()
}
}
return c.JSON(http.StatusOK, res)
}
*/