416 lines
10 KiB
Go
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)
|
|
}
|
|
*/
|