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,103 @@
package apps
import (
"fmt"
"github.com/labstack/echo/v4"
"github.com/texm/dokku-go"
"gitlab.com/texm/shokku/internal/env"
"gitlab.com/texm/shokku/internal/server/dto"
"net/http"
)
func GetAppBuilder(e *env.Env, c echo.Context) error {
var req dto.GetAppBuilderRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
report, err := e.Dokku.GetAppBuilderReport(req.Name)
if err != nil {
return fmt.Errorf("getting app builder: %w", err)
}
selectedBuilder := report.ComputedSelectedBuilder
if selectedBuilder == "" {
selectedBuilder = "auto"
}
return c.JSON(http.StatusOK, dto.GetAppBuilderResponse{
Selected: selectedBuilder,
})
}
func SetAppBuilder(e *env.Env, c echo.Context) error {
var req dto.SetAppBuilderRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
builders := map[string]dokku.AppBuilder{
"auto": "",
"dockerfile": dokku.AppBuilderDockerfile,
"herokuish": dokku.AppBuilderHerokuish,
"null": dokku.AppBuilderNull,
"buildpack": dokku.AppBuilderPack,
"lambda": dokku.AppBuilderLambda,
}
chosenBuilder, supported := builders[req.Builder]
if !supported {
return echo.NewHTTPError(http.StatusBadRequest,
fmt.Sprintf("unsupported builder '%s'", req.Builder))
}
err := e.Dokku.SetAppSelectedBuilder(req.Name, chosenBuilder)
if err != nil {
return fmt.Errorf("setting app builder: %w", err)
}
return c.NoContent(http.StatusOK)
}
func GetAppBuildDirectory(e *env.Env, c echo.Context) error {
var req dto.GetAppBuildDirectoryRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
report, err := e.Dokku.GetAppBuilderReport(req.Name)
if err != nil {
return fmt.Errorf("getting app build dir: %w", err)
}
return c.JSON(http.StatusOK, dto.GetAppBuildDirectoryResponse{
Directory: report.ComputedBuildDir,
})
}
func SetAppBuildDirectory(e *env.Env, c echo.Context) error {
var req dto.SetAppBuildDirectoryRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
err := e.Dokku.SetAppBuilderProperty(req.Name, dokku.BuilderPropertyBuildDir, req.Directory)
if err != nil {
return fmt.Errorf("setting app build dir: %w", err)
}
return c.NoContent(http.StatusOK)
}
func ClearAppBuildDirectory(e *env.Env, c echo.Context) error {
var req dto.ClearAppBuildDirectoryRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
err := e.Dokku.SetAppBuilderProperty(req.Name, dokku.BuilderPropertyBuildDir, "")
if err != nil {
return fmt.Errorf("clearing app build dir: %w", err)
}
return c.NoContent(http.StatusOK)
}

View File

@@ -0,0 +1,38 @@
package apps
import (
"fmt"
"github.com/labstack/echo/v4"
"gitlab.com/texm/shokku/internal/env"
"gitlab.com/texm/shokku/internal/server/dto"
"net/http"
)
func GetAppConfig(e *env.Env, c echo.Context) error {
var req dto.GetAppConfigRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
config, err := e.Dokku.GetAppConfig(req.Name)
if err != nil {
return fmt.Errorf("getting app config: %w", err)
}
return c.JSON(http.StatusOK, dto.GetAppConfigResponse{
Config: config,
})
}
func SetAppConfig(e *env.Env, c echo.Context) error {
var req dto.SetAppConfigRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
if err := e.Dokku.SetAppConfigValues(req.Name, req.Config, false); err != nil {
return fmt.Errorf("setting app config: %w", err)
}
return c.NoContent(http.StatusOK)
}

View File

@@ -0,0 +1,97 @@
package apps
import (
"fmt"
"github.com/labstack/echo/v4"
"gitlab.com/texm/shokku/internal/env"
"gitlab.com/texm/shokku/internal/server/dto"
"net/http"
)
func GetAppDomainsReport(e *env.Env, c echo.Context) error {
var req dto.GetAppDomainsReportRequest
if err := dto.BindRequest(c, &req); err != nil {
return echo.ErrBadRequest
}
report, err := e.Dokku.GetAppDomainsReport(req.Name)
if err != nil {
return fmt.Errorf("getting app domains report: %w", err)
}
if len(report.AppDomains) == 0 {
report.AppDomains = make([]string, 0)
}
if len(report.GlobalDomains) == 0 {
report.GlobalDomains = make([]string, 0)
}
return c.JSON(http.StatusOK, dto.GetAppDomainsReportResponse{
Domains: report.AppDomains,
Enabled: report.AppEnabled,
})
}
func SetAppDomainsEnabled(e *env.Env, c echo.Context) error {
var req dto.SetAppDomainsEnabledRequest
if err := dto.BindRequest(c, &req); err != nil {
return echo.ErrBadRequest
}
var err error
if req.Enabled {
err = e.Dokku.EnableAppDomains(req.Name)
} else {
err = e.Dokku.DisableAppDomains(req.Name)
}
if err != nil {
return fmt.Errorf("setting app domains enabled: %w", err)
}
return c.NoContent(http.StatusOK)
}
func GetAppLetsEncryptEnabled(e *env.Env, c echo.Context) error {
var req dto.GetAppLetsEncryptEnabledRequest
if err := dto.BindRequest(c, &req); err != nil {
return echo.ErrBadRequest
}
return c.NoContent(http.StatusNotImplemented)
}
func SetAppLetsEncryptEnabled(e *env.Env, c echo.Context) error {
var req dto.SetAppLetsEncryptEnabledRequest
if err := dto.BindRequest(c, &req); err != nil {
return echo.ErrBadRequest
}
return c.NoContent(http.StatusNotImplemented)
}
func AddAppDomain(e *env.Env, c echo.Context) error {
var req dto.AlterAppDomainRequest
if err := dto.BindRequest(c, &req); err != nil {
return echo.ErrBadRequest
}
if err := e.Dokku.AddAppDomain(req.Name, req.Domain); err != nil {
return fmt.Errorf("adding app domain: %w", err)
}
return c.NoContent(http.StatusOK)
}
func RemoveAppDomain(e *env.Env, c echo.Context) error {
var req dto.AlterAppDomainRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
if err := e.Dokku.RemoveAppDomain(req.Name, req.Domain); err != nil {
return fmt.Errorf("removing app domain: %w", err)
}
return c.NoContent(http.StatusOK)
}

View File

@@ -0,0 +1,415 @@
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)
}
*/

View File

@@ -0,0 +1,84 @@
package apps
import (
"fmt"
"github.com/labstack/echo/v4"
"github.com/texm/dokku-go"
"gitlab.com/texm/shokku/internal/env"
"gitlab.com/texm/shokku/internal/server/dto"
"net/http"
)
func GetAppNetworksReport(e *env.Env, c echo.Context) error {
var req dto.GetAppNetworksReportRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
report, err := e.Dokku.GetAppNetworkReport(req.Name)
if err != nil {
return fmt.Errorf("getting app network report: %w", err)
}
return c.JSON(http.StatusOK, dto.GetAppNetworksReportResponse{
AttachInitial: report.ComputedInitialNetwork,
AttachPostCreate: report.ComputedAttachPostCreate,
AttachPostDeploy: report.ComputedAttachPostDeploy,
BindAllInterfaces: report.ComputedBindAllInterfaces,
TLD: report.ComputedTLD,
WebListeners: report.WebListeners,
})
}
func SetAppNetworks(e *env.Env, c echo.Context) error {
var req dto.SetAppNetworksRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
if req.Initial != nil {
err := e.Dokku.SetAppNetworkProperty(req.Name,
dokku.NetworkPropertyInitialNetwork, *req.Initial)
if err != nil {
return fmt.Errorf("setting initial network: %w", err)
}
}
if req.PostCreate != nil {
err := e.Dokku.SetAppNetworkProperty(req.Name,
dokku.NetworkPropertyAttachPostCreate, *req.PostCreate)
if err != nil {
return fmt.Errorf("setting postcreate network: %w", err)
}
}
if req.PostDeploy != nil {
err := e.Dokku.SetAppNetworkProperty(req.Name,
dokku.NetworkPropertyAttachPostDeploy, *req.PostDeploy)
if err != nil {
return fmt.Errorf("setting postdeploy network: %w", err)
}
}
if req.BindAllInterfaces != nil {
bindAll := "false"
if *req.BindAllInterfaces {
bindAll = "true"
}
err := e.Dokku.SetAppNetworkProperty(req.Name,
dokku.NetworkPropertyBindAllInterfaces, bindAll)
if err != nil {
return fmt.Errorf("setting bind-all-interfaces: %w", err)
}
}
if req.TLD != nil {
err := e.Dokku.SetAppNetworkProperty(req.Name,
dokku.NetworkPropertyTLD, *req.TLD)
if err != nil {
return fmt.Errorf("setting TLD: %w", err)
}
}
return c.NoContent(http.StatusOK)
}

View File

@@ -0,0 +1,190 @@
package apps
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 GetAppProcesses(e *env.Env, c echo.Context) error {
var req dto.GetAppProcessesRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
scales, err := e.Dokku.GetAppProcessScale(req.Name)
if err != nil {
return fmt.Errorf("getting app process scale: %w", err)
}
processes := make([]string, len(scales))
i := 0
for processName := range scales {
processes[i] = processName
i++
}
return c.JSON(http.StatusOK, dto.GetAppProcessesResponse{
Processes: processes,
})
}
func GetAppProcessReport(e *env.Env, c echo.Context) error {
var req dto.GetAppProcessReportRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
resourceReport, err := e.Dokku.GetAppResourceReport(req.Name)
if err != nil {
return fmt.Errorf("getting app resource report: %w", err)
}
processScale, err := e.Dokku.GetAppProcessScale(req.Name)
if err != nil {
return fmt.Errorf("getting app process scale: %w", err)
}
processMap := map[string]dto.AppProcessInfo{}
for processName, scale := range processScale {
appResources := dto.AppProcessInfo{
Scale: scale,
}
if psSettings, ok := resourceReport.Processes[processName]; ok {
appResources.Resources = psSettings
}
processMap[processName] = appResources
}
return c.JSON(http.StatusOK, dto.GetAppProcessReportResponse{
ResourceDefaults: resourceReport.Defaults,
Processes: processMap,
})
}
func SetAppProcessResources(e *env.Env, c echo.Context) error {
var req dto.SetAppProcessResourcesRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
limits := req.ResourceLimits
reservations := req.ResourceReservations
if limits.CPU != nil {
err := e.Dokku.SetAppProcessResourceLimit(req.Name, req.Process,
dokku.ResourceCPU, *limits.CPU)
if err != nil {
return fmt.Errorf("setting app cpu limit: %w", err)
}
} else {
err := e.Dokku.ClearAppProcessResourceLimit(req.Name, req.Process, dokku.ResourceCPU)
if err != nil {
return fmt.Errorf("clearing app cpu limit: %w", err)
}
}
if limits.Memory != nil && limits.MemoryUnit != nil {
resSpec := dokku.ResourceSpec{
Name: "memory",
Suffix: *limits.MemoryUnit,
}
err := e.Dokku.SetAppProcessResourceLimit(req.Name, req.Process, resSpec, *limits.Memory)
if err != nil {
return fmt.Errorf("setting app mem limit: %w", err)
}
} else {
err := e.Dokku.ClearAppProcessResourceLimit(req.Name, req.Process, dokku.ResourceMemoryBytes)
if err != nil {
return fmt.Errorf("clearing app mem limit: %w", err)
}
}
if reservations.Memory != nil && reservations.MemoryUnit != nil {
resSpec := dokku.ResourceSpec{
Name: "memory",
Suffix: *reservations.MemoryUnit,
}
err := e.Dokku.SetAppProcessResourceReservation(req.Name, req.Process, resSpec,
*reservations.Memory)
if err != nil {
return fmt.Errorf("setting app mem reservation: %w", err)
}
} else {
err := e.Dokku.ClearAppProcessResourceReservation(req.Name, req.Process,
dokku.ResourceMemoryBytes)
if err != nil {
return fmt.Errorf("clearing app mem reservation: %w", err)
}
}
return c.NoContent(http.StatusOK)
}
func GetAppProcessScale(e *env.Env, c echo.Context) error {
var req dto.GetAppProcessScaleRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
scale, err := e.Dokku.GetAppProcessScale(req.Name)
if err != nil {
return fmt.Errorf("getting app process scale: %w", err)
}
return c.JSON(http.StatusOK, dto.GetAppProcessScaleResponse{
ProcessScale: scale,
})
}
func SetAppProcessDeployChecks(e *env.Env, c echo.Context) error {
var req dto.SetAppProcessDeployChecksRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
processes := []string{req.Process}
var err error
switch req.State {
case "enabled":
err = e.Dokku.EnableAppProcessesDeployChecks(req.Name, processes)
case "disabled":
err = e.Dokku.DisableAppProcessesDeployChecks(req.Name, processes)
case "skipped":
err = e.Dokku.SetAppProcessesDeployChecksSkipped(req.Name, processes)
default:
return echo.NewHTTPError(http.StatusBadRequest, "unknown state")
}
if err != nil {
return fmt.Errorf("setting app deploy checks to " + req.State)
}
return c.NoContent(http.StatusOK)
}
func SetAppProcessScale(e *env.Env, c echo.Context) error {
var req dto.SetAppProcessScaleRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
_, err := e.Auth.GetUserFromContext(c)
if err != nil {
log.Error().Err(err).Msg("failed to retrieve user from context")
return echo.ErrInternalServerError
}
cmd := func() (*dokku.CommandOutputStream, error) {
return e.Dokku.SetAppProcessScale(req.Name, req.Process, req.Scale, req.SkipDeploy)
}
return c.JSON(http.StatusOK, dto.CommandExecutionResponse{
ExecutionID: commands.RequestExecution(cmd, nil),
})
}

View File

@@ -0,0 +1,70 @@
package apps
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(GetAppsList))
g.GET("/report", e.H(GetAppsProcessReport))
g.GET("/overview", e.H(GetAppOverview))
g.POST("/create", e.H(CreateApp))
g.POST("/start", e.H(StartApp))
g.POST("/stop", e.H(StopApp))
g.POST("/restart", e.H(RestartApp))
g.POST("/rebuild", e.H(RebuildApp))
setupGroup := g.Group("/setup")
setupGroup.GET("/status", e.H(GetAppSetupStatus))
setupGroup.GET("/config", e.H(GetAppSetupConfig))
setupGroup.POST("/new-repo", e.H(SetupAppNewRepo))
setupGroup.POST("/sync-repo", e.H(SetupAppSyncRepo))
setupGroup.POST("/pull-image", e.H(SetupAppPullImage))
setupGroup.POST("/upload-archive", e.H(SetupAppUploadArchive))
g.POST("/destroy", e.H(DestroyApp))
g.GET("/info", e.H(GetAppInfo))
g.POST("/rename", e.H(RenameApp))
g.GET("/services", e.H(GetAppServices))
g.GET("/deploy-checks", e.H(GetAppDeployChecks))
g.POST("/deploy-checks", e.H(SetAppDeployChecks))
process := g.Group("/process")
process.GET("/list", e.H(GetAppProcesses))
process.GET("/report", e.H(GetAppProcessReport))
process.POST("/deploy-checks", e.H(SetAppProcessDeployChecks))
process.POST("/resources", e.H(SetAppProcessResources))
process.GET("/scale", e.H(GetAppProcessScale))
process.POST("/scale", e.H(SetAppProcessScale))
g.GET("/letsencrypt", e.H(GetAppLetsEncryptEnabled))
g.POST("/letsencrypt", e.H(SetAppLetsEncryptEnabled))
g.GET("/domains", e.H(GetAppDomainsReport))
g.POST("/domains/state", e.H(SetAppDomainsEnabled))
g.POST("/domain", e.H(AddAppDomain))
g.DELETE("/domain", e.H(RemoveAppDomain))
g.GET("/networks", e.H(GetAppNetworksReport))
g.POST("/networks", e.H(SetAppNetworks))
g.GET("/logs", e.H(GetAppLogs))
g.GET("/config", e.H(GetAppConfig))
g.POST("/config", e.H(SetAppConfig))
g.GET("/storage", e.H(GetAppStorage))
g.POST("/storage/mount", e.H(MountAppStorage))
g.POST("/storage/unmount", e.H(UnmountAppStorage))
g.GET("/builder", e.H(GetAppBuilder))
g.POST("/builder", e.H(SetAppBuilder))
g.GET("/build-directory", e.H(GetAppBuildDirectory))
g.POST("/build-directory", e.H(SetAppBuildDirectory))
g.DELETE("/build-directory", e.H(ClearAppBuildDirectory))
}

View File

@@ -0,0 +1,60 @@
package apps
import (
"fmt"
"github.com/labstack/echo/v4"
"gitlab.com/texm/shokku/internal/env"
"gitlab.com/texm/shokku/internal/server/dto"
"net/http"
"strings"
)
var (
dokkuErrPrefix = "! "
serviceTypes = []string{"redis", "postgres", "mysql", "mongo"}
)
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 getAppServiceLinks(e *env.Env, appName string, serviceType string) ([]string, error) {
linksCmd := fmt.Sprintf("%s:app-links %s --quiet", serviceType, appName)
out, err := e.Dokku.Exec(linksCmd)
if err != nil {
return nil, err
}
return splitDokkuListOutput(out)
}
func GetAppServices(e *env.Env, c echo.Context) error {
var req dto.GetAppServicesRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
serviceList := []dto.ServiceInfo{}
for _, serviceType := range serviceTypes {
links, err := getAppServiceLinks(e, req.Name, serviceType)
if err != nil {
return fmt.Errorf("getting links for " + serviceType)
}
for _, name := range links {
serviceList = append(serviceList, dto.ServiceInfo{
Name: name,
Type: serviceType,
})
}
}
return c.JSON(http.StatusOK, dto.ListServicesResponse{
Services: serviceList,
})
}

View File

@@ -0,0 +1,229 @@
package apps
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/auth"
"gitlab.com/texm/shokku/internal/server/commands"
"gitlab.com/texm/shokku/internal/server/dto"
"gorm.io/gorm/clause"
"net/http"
"net/url"
)
type setupMethod string
const (
methodGit = setupMethod("git")
methodSyncRepo = setupMethod("sync-repo")
methodDocker = setupMethod("docker")
methodArchive = setupMethod("archive")
)
var (
updateOnConflict = clause.OnConflict{
DoUpdates: clause.AssignmentColumns([]string{"updated_at"}),
}
)
func saveAppSetupMethod(e *env.Env, app *models.App, method setupMethod, cfg *models.AppSetupConfig) error {
app.IsSetup = true
app.SetupMethod = string(method)
if appErr := e.DB.Save(app).Error; appErr != nil {
log.Error().Err(appErr).
Str("app", app.Name).
Str("method", app.SetupMethod).
Msg("failed to save app table")
return fmt.Errorf("failed to save app table: %w", appErr)
}
cfg.AppID = app.ID
updateErr := e.DB.Clauses(updateOnConflict).Create(&cfg).Error
if updateErr != nil {
log.Error().Err(updateErr).
Str("app", app.Name).
Interface("cfg", cfg).
Msg("failed to update app setup config")
return fmt.Errorf("failed to save app setup config: %w", updateErr)
}
return nil
}
func GetAppSetupStatus(e *env.Env, c echo.Context) error {
var req dto.GetAppSetupStatusRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbApp, appErr := lookupDBAppByName(e, req.Name)
if appErr != nil {
return echo.ErrNotFound
}
return c.JSON(http.StatusOK, dto.GetAppSetupStatusResponse{
IsSetup: dbApp.IsSetup,
Method: dbApp.SetupMethod,
})
}
func GetAppSetupConfig(e *env.Env, c echo.Context) error {
var req dto.GetAppSetupConfigRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbApp, appErr := lookupDBAppByName(e, req.Name)
if appErr != nil {
return echo.ErrNotFound
}
var cfg models.AppSetupConfig
e.DB.Where("app_id = ?", dbApp.ID).FirstOrInit(&cfg)
return c.JSON(http.StatusOK, dto.GetAppSetupConfigResponse{
IsSetup: dbApp.IsSetup,
Method: dbApp.SetupMethod,
DeploymentBranch: cfg.DeployBranch,
RepoURL: cfg.RepoURL,
RepoGitRef: cfg.RepoGitRef,
Image: cfg.Image,
})
}
func SetupAppNewRepo(e *env.Env, c echo.Context) error {
var req dto.SetupAppNewRepoRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbApp, appErr := lookupDBAppByName(e, req.Name)
if appErr != nil {
return echo.ErrNotFound
}
if initErr := e.Dokku.GitInitializeApp(req.Name); initErr != nil {
return fmt.Errorf("initialising git repo: %w", initErr)
}
branch := req.DeploymentBranch
if branch == "" {
branch = "master"
}
if branch != "master" {
branchErr := e.Dokku.GitSetAppProperty(req.Name, dokku.GitPropertyDeployBranch, req.DeploymentBranch)
if branchErr != nil {
return fmt.Errorf("setting git deploy branch: %w", branchErr)
}
}
cfg := &models.AppSetupConfig{
DeployBranch: branch,
}
if saveErr := saveAppSetupMethod(e, dbApp, methodGit, cfg); saveErr != nil {
return saveErr
}
return c.NoContent(http.StatusOK)
}
func SetupAppSyncRepo(e *env.Env, c echo.Context) error {
var req dto.SetupAppSyncRepoRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbApp, appErr := lookupDBAppByName(e, req.Name)
if appErr != nil {
return echo.ErrNotFound
}
parsedURL, urlErr := url.Parse(req.RepositoryURL)
if urlErr != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid repo url")
}
if e.Auth.GetMethod() == auth.MethodGithub {
if parsedURL.Hostname() != "github.com" {
// return echo.ErrBadRequest
}
}
syncOpts := &dokku.GitSyncOptions{
Build: true,
GitRef: req.GitRef,
}
execFn := func() (*dokku.CommandOutputStream, error) {
return e.Dokku.GitSyncAppRepo(req.Name, req.RepositoryURL, syncOpts)
}
cfg := &models.AppSetupConfig{
AppID: dbApp.ID,
RepoURL: req.RepositoryURL,
RepoGitRef: req.GitRef,
}
cb := func() error {
return saveAppSetupMethod(e, dbApp, methodSyncRepo, cfg)
}
return c.JSON(http.StatusOK, dto.CommandExecutionResponse{
ExecutionID: commands.RequestExecution(execFn, cb),
})
}
func SetupAppPullImage(e *env.Env, c echo.Context) error {
var req dto.SetupAppPullImageRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
dbApp, appErr := lookupDBAppByName(e, req.Name)
if appErr != nil {
return echo.ErrNotFound
}
execFn := func() (*dokku.CommandOutputStream, error) {
return e.Dokku.GitCreateFromImage(req.Name, req.Image, nil)
}
cfg := &models.AppSetupConfig{
AppID: dbApp.ID,
Image: req.Image,
}
cb := func() error {
return saveAppSetupMethod(e, dbApp, methodDocker, cfg)
}
return c.JSON(http.StatusOK, dto.CommandExecutionResponse{
ExecutionID: commands.RequestExecution(execFn, cb),
})
}
func SetupAppUploadArchive(e *env.Env, c echo.Context) error {
var req dto.SetupAppUploadArchiveRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
// disable for now
if true {
return echo.ErrForbidden
}
file, err := c.FormFile("archive")
if err != nil {
log.Error().Err(err).Msg("invalid form file")
return echo.ErrInternalServerError
}
log.Info().
Str("app", req.Name).
Msgf("got file: %+v", file.Header)
return c.NoContent(http.StatusNotImplemented)
}

View File

@@ -0,0 +1,107 @@
package apps
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/dto"
"net/http"
)
func isMountInSlice(mount dokku.StorageBindMount, mounts []dokku.StorageBindMount) bool {
for _, m := range mounts {
if m == mount {
return true
}
}
return false
}
func GetAppStorage(e *env.Env, c echo.Context) error {
var req dto.GetAppStorageRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
report, err := e.Dokku.GetAppStorageReport(req.Name)
if err != nil {
return fmt.Errorf("getting app storage report: %w", err)
}
var allMounts []dokku.StorageBindMount
allMounts = append(allMounts, report.RunMounts...)
allMounts = append(allMounts, report.DeployMounts...)
allMounts = append(allMounts, report.BuildMounts...)
seenMap := map[string]bool{}
mounts := []dto.StorageMount{}
for _, dokkuMount := range allMounts {
if _, seen := seenMap[dokkuMount.String()]; seen {
continue
}
seenMap[dokkuMount.String()] = true
mounts = append(mounts, dto.StorageMount{
HostDir: dokkuMount.HostDir,
ContainerDir: dokkuMount.ContainerDir,
IsBuildMount: isMountInSlice(dokkuMount, report.BuildMounts),
IsRunMount: isMountInSlice(dokkuMount, report.RunMounts),
IsDeployMount: isMountInSlice(dokkuMount, report.DeployMounts),
})
}
return c.JSON(http.StatusOK, dto.GetAppStorageResponse{
Mounts: mounts,
})
}
func MountAppStorage(e *env.Env, c echo.Context) error {
var req dto.AlterAppStorageRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
if req.StorageType == "New Storage" {
// TODO: actually chown properly
err := e.Dokku.EnsureStorageDirectory(req.Name, dokku.StorageChownOptionHerokuish)
if err != nil {
return fmt.Errorf("ensuring app storage dir: %w", err)
}
}
mount := dokku.StorageBindMount{
HostDir: req.HostDir,
ContainerDir: req.ContainerDir,
}
if err := e.Dokku.MountAppStorage(req.Name, mount); err != nil {
return fmt.Errorf("mounting app storage dir: %w", err)
}
return c.NoContent(http.StatusOK)
}
func UnmountAppStorage(e *env.Env, c echo.Context) error {
var req dto.AlterAppStorageRequest
if err := dto.BindRequest(c, &req); err != nil {
return err.ToHTTP()
}
mount := dokku.StorageBindMount{
HostDir: req.HostDir,
ContainerDir: req.ContainerDir,
}
if err := e.Dokku.UnmountAppStorage(req.Name, mount); err != nil {
return fmt.Errorf("unmounting app storage dir: %w", err)
}
if req.RestartApp {
go func() {
if _, err := e.Dokku.RestartApp(req.Name, nil); err != nil {
log.Error().Err(err).Msg("error while restarting app")
}
}()
}
return c.NoContent(http.StatusOK)
}