init from gitlab
This commit is contained in:
103
internal/server/api/apps/builds.go
Normal file
103
internal/server/api/apps/builds.go
Normal 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)
|
||||
}
|
||||
38
internal/server/api/apps/config.go
Normal file
38
internal/server/api/apps/config.go
Normal 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)
|
||||
}
|
||||
97
internal/server/api/apps/domains.go
Normal file
97
internal/server/api/apps/domains.go
Normal 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)
|
||||
}
|
||||
415
internal/server/api/apps/management.go
Normal file
415
internal/server/api/apps/management.go
Normal 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)
|
||||
}
|
||||
*/
|
||||
84
internal/server/api/apps/networks.go
Normal file
84
internal/server/api/apps/networks.go
Normal 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)
|
||||
}
|
||||
190
internal/server/api/apps/processes.go
Normal file
190
internal/server/api/apps/processes.go
Normal 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),
|
||||
})
|
||||
}
|
||||
70
internal/server/api/apps/routes.go
Normal file
70
internal/server/api/apps/routes.go
Normal 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))
|
||||
}
|
||||
60
internal/server/api/apps/services.go
Normal file
60
internal/server/api/apps/services.go
Normal 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,
|
||||
})
|
||||
}
|
||||
229
internal/server/api/apps/setup.go
Normal file
229
internal/server/api/apps/setup.go
Normal 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)
|
||||
}
|
||||
107
internal/server/api/apps/storage.go
Normal file
107
internal/server/api/apps/storage.go
Normal 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)
|
||||
}
|
||||
47
internal/server/api/auth/auth.go
Normal file
47
internal/server/api/auth/auth.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
"gitlab.com/texm/shokku/internal/server/auth"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func HandleLogout(e *env.Env, c echo.Context) error {
|
||||
e.Auth.ClearTokenCookies(c)
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func HandleRefreshAuth(e *env.Env, c echo.Context) error {
|
||||
user, err := e.Auth.GetUserFromContext(c)
|
||||
if err != nil {
|
||||
log.Error().Msg("failed to parse user from context")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
claims := auth.UserClaims{
|
||||
Name: user.Name,
|
||||
}
|
||||
token, err := e.Auth.NewToken(claims)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to create jwt")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
e.Auth.SetTokenCookies(c, token)
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func HandleGetDetails(e *env.Env, c echo.Context) error {
|
||||
user, err := e.Auth.GetUserFromContext(c)
|
||||
if err != nil {
|
||||
log.Error().Msg("failed to parse user from context")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, echo.Map{
|
||||
"username": user.Name,
|
||||
})
|
||||
}
|
||||
76
internal/server/api/auth/github.go
Normal file
76
internal/server/api/auth/github.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"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/dto"
|
||||
"gitlab.com/texm/shokku/internal/server/github"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetGithubAuthInfo(e *env.Env, c echo.Context) error {
|
||||
var ghApp models.GithubApp
|
||||
if err := e.DB.First(&ghApp).Error; err != nil {
|
||||
log.Error().Err(err).Msg("no github app in db")
|
||||
return echo.ErrBadRequest
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, dto.GetGithubAuthInfoResponse{
|
||||
ClientID: ghApp.ClientId,
|
||||
})
|
||||
}
|
||||
|
||||
func CompleteGithubAuth(e *env.Env, c echo.Context) error {
|
||||
var req dto.GithubAuthRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.ErrBadRequest
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
params := github.CodeExchangeParams{
|
||||
Code: req.Code,
|
||||
Scopes: []string{},
|
||||
RedirectURL: req.RedirectURL,
|
||||
}
|
||||
client, err := github.ExchangeCode(ctx, e, params)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to exchange code for client")
|
||||
return echo.ErrBadRequest
|
||||
}
|
||||
|
||||
user, err := client.GetUser(ctx)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get user")
|
||||
return echo.ErrBadRequest
|
||||
}
|
||||
|
||||
var count int64
|
||||
dbUser := models.User{Name: user.GetLogin()}
|
||||
r := e.DB.Model(&dbUser).Where(&dbUser).Count(&count)
|
||||
if r.Error != nil {
|
||||
log.Error().
|
||||
Err(r.Error).
|
||||
Str("name", user.GetLogin()).
|
||||
Msg("user lookup error")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
if count == 0 {
|
||||
return echo.ErrForbidden
|
||||
}
|
||||
|
||||
claims := auth.UserClaims{
|
||||
Name: user.GetLogin(),
|
||||
}
|
||||
token, err := e.Auth.NewToken(claims)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to create jwt")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
e.Auth.SetTokenCookies(c, token)
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
58
internal/server/api/auth/password.go
Normal file
58
internal/server/api/auth/password.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
auth "gitlab.com/texm/shokku/internal/server/auth"
|
||||
"gitlab.com/texm/shokku/internal/server/dto"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitlab.com/texm/shokku/internal/models"
|
||||
)
|
||||
|
||||
func HandlePasswordLogin(e *env.Env, c echo.Context) error {
|
||||
var req dto.PasswordLoginRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.ErrBadRequest
|
||||
}
|
||||
|
||||
var dbUser models.User
|
||||
res := e.DB.Where("name = ?", req.Username).Take(&dbUser)
|
||||
if res.Error != nil {
|
||||
return echo.ErrForbidden
|
||||
}
|
||||
|
||||
pwAuth, ok := e.Auth.(*auth.PasswordAuthenticator)
|
||||
if !ok {
|
||||
log.Error().Msg("failed to cast authenticator to pw auth")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
if !pwAuth.VerifyHash([]byte(req.Password), dbUser.PasswordHash) {
|
||||
return echo.ErrForbidden
|
||||
}
|
||||
|
||||
if dbUser.TotpEnabled {
|
||||
if !totp.Validate(req.TotpCode, dbUser.TotpSecret) {
|
||||
return c.JSON(http.StatusOK, dto.PasswordLoginResponse{
|
||||
Success: true,
|
||||
NeedsTotp: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
claims := auth.UserClaims{
|
||||
Name: req.Username,
|
||||
}
|
||||
token, err := e.Auth.NewToken(claims)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to create jwt")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
e.Auth.SetTokenCookies(c, token)
|
||||
|
||||
return c.JSON(http.StatusOK, dto.PasswordLoginResponse{
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
16
internal/server/api/auth/routes.go
Normal file
16
internal/server/api/auth/routes.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
)
|
||||
|
||||
func RegisterRoutes(e *env.Env, g *echo.Group, authG *echo.Group) {
|
||||
g.POST("/login", e.H(HandlePasswordLogin))
|
||||
g.GET("/github", e.H(GetGithubAuthInfo))
|
||||
g.POST("/github/auth", e.H(CompleteGithubAuth))
|
||||
g.POST("/logout", e.H(HandleLogout))
|
||||
|
||||
authG.GET("/details", e.H(HandleGetDetails))
|
||||
authG.POST("/refresh", e.H(HandleRefreshAuth))
|
||||
}
|
||||
34
internal/server/api/command.go
Normal file
34
internal/server/api/command.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
"gitlab.com/texm/shokku/internal/server/commands"
|
||||
"gitlab.com/texm/shokku/internal/server/dto"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetCommandExecutionStatus(e *env.Env, c echo.Context) error {
|
||||
var req dto.GetCommandExecutionStatusRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
status, err := commands.GetExecutionStatus(req.ExecutionID)
|
||||
if err != nil {
|
||||
if errors.Is(err, commands.ErrNotPolled) {
|
||||
return c.JSON(http.StatusOK, dto.CommandExecutionStatusResponse{
|
||||
Started: false,
|
||||
})
|
||||
}
|
||||
return echo.ErrNotFound
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, dto.CommandExecutionStatusResponse{
|
||||
CombinedOutput: status.CombinedOutput,
|
||||
Started: true,
|
||||
Finished: status.Finished,
|
||||
Success: status.StreamError == nil,
|
||||
})
|
||||
}
|
||||
58
internal/server/api/github.go
Normal file
58
internal/server/api/github.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/google/go-github/v48/github"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
"gitlab.com/texm/shokku/internal/models"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func ReceiveGithubWebhookEvent(e *env.Env, c echo.Context) error {
|
||||
var ghApp models.GithubApp
|
||||
if ghErr := e.DB.First(&ghApp).Error; ghErr != nil {
|
||||
log.Error().Err(ghErr).Msg("failed to retrieve github app info")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
req := c.Request()
|
||||
secret := []byte(ghApp.WebhookSecret)
|
||||
payload, validationErr := github.ValidatePayload(req, secret)
|
||||
if validationErr != nil {
|
||||
return echo.ErrBadRequest
|
||||
}
|
||||
event, parseErr := github.ParseWebHook(github.WebHookType(req), payload)
|
||||
if parseErr != nil {
|
||||
log.Error().Err(parseErr).Msg("failed to parse webhook")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
var err error
|
||||
switch event := event.(type) {
|
||||
case *github.MetaEvent:
|
||||
err = processMetaEvent(e, event)
|
||||
case *github.PushEvent:
|
||||
err = processPushEvent(e, event)
|
||||
default:
|
||||
log.Error().
|
||||
Interface("type", event).
|
||||
Msg("received unsupported webhook event")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func processMetaEvent(e *env.Env, event *github.MetaEvent) error {
|
||||
log.Debug().Interface("event", event).Msg("got meta event")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processPushEvent(e *env.Env, event *github.PushEvent) error {
|
||||
return nil
|
||||
}
|
||||
26
internal/server/api/routes.go
Normal file
26
internal/server/api/routes.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
"gitlab.com/texm/shokku/internal/server/api/apps"
|
||||
"gitlab.com/texm/shokku/internal/server/api/auth"
|
||||
"gitlab.com/texm/shokku/internal/server/api/services"
|
||||
"gitlab.com/texm/shokku/internal/server/api/settings"
|
||||
"gitlab.com/texm/shokku/internal/server/api/setup"
|
||||
)
|
||||
|
||||
func RegisterRoutes(e *env.Env, authMiddleware []echo.MiddlewareFunc) {
|
||||
g := e.Router.Group("/api")
|
||||
|
||||
setup.RegisterRoutes(e, g.Group("/setup"))
|
||||
auth.RegisterRoutes(e, g.Group("/auth"), g.Group("/auth", authMiddleware...))
|
||||
|
||||
protectedApi := g.Group("", authMiddleware...)
|
||||
apps.RegisterRoutes(e, protectedApi.Group("/apps"))
|
||||
services.RegisterRoutes(e, protectedApi.Group("/services"))
|
||||
settings.RegisterRoutes(e, protectedApi.Group("/settings"))
|
||||
|
||||
protectedApi.GET("/exec/status", e.H(GetCommandExecutionStatus))
|
||||
g.POST("/github/events", e.H(ReceiveGithubWebhookEvent))
|
||||
}
|
||||
57
internal/server/api/routes_test.go
Normal file
57
internal/server/api/routes_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
"gitlab.com/texm/shokku/internal/server/dto"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func createQueryContext(route string, params url.Values) (*env.Env, echo.Context) {
|
||||
e := env.NewTestingEnvironment()
|
||||
|
||||
uri := fmt.Sprintf("%s/?%s", route, params.Encode())
|
||||
req := httptest.NewRequest(http.MethodGet, uri, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
c := e.Router.NewContext(req, rec)
|
||||
|
||||
e.Router.GET("/ping", e.H(pingRoute))
|
||||
|
||||
return e, c
|
||||
}
|
||||
|
||||
type pingRequest struct {
|
||||
Foo string `query:"foo" validate:"alphanum"`
|
||||
}
|
||||
|
||||
func pingRoute(e *env.Env, c echo.Context) error {
|
||||
var req pingRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func TestPingRouteValidatesSuccess(t *testing.T) {
|
||||
q := make(url.Values)
|
||||
q.Set("foo", "bar")
|
||||
e, c := createQueryContext("/ping", q)
|
||||
|
||||
err := pingRoute(e, c)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPingRouteValidatesFail(t *testing.T) {
|
||||
q := make(url.Values)
|
||||
q.Set("foo", "bar!!")
|
||||
e, c := createQueryContext("/ping", q)
|
||||
|
||||
err := pingRoute(e, c)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
209
internal/server/api/services/backups.go
Normal file
209
internal/server/api/services/backups.go
Normal 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)
|
||||
}
|
||||
167
internal/server/api/services/management.go
Normal file
167
internal/server/api/services/management.go
Normal 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)
|
||||
}
|
||||
34
internal/server/api/services/routes.go
Normal file
34
internal/server/api/services/routes.go
Normal 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))
|
||||
}
|
||||
220
internal/server/api/services/services.go
Normal file
220
internal/server/api/services/services.go
Normal 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,
|
||||
})
|
||||
}
|
||||
46
internal/server/api/settings/networks.go
Normal file
46
internal/server/api/settings/networks.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
"gitlab.com/texm/shokku/internal/server/dto"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func CreateNetwork(e *env.Env, c echo.Context) error {
|
||||
var req dto.AlterNetworkRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
if err := e.Dokku.CreateNetwork(req.Network); err != nil {
|
||||
return fmt.Errorf("error creating network: %w", err)
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func DestroyNetwork(e *env.Env, c echo.Context) error {
|
||||
var req dto.AlterNetworkRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
if err := e.Dokku.DestroyNetwork(req.Network); err != nil {
|
||||
return fmt.Errorf("error destroying network: %w", err)
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func ListNetworks(e *env.Env, c echo.Context) error {
|
||||
networks, err := e.Dokku.ListNetworks()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listing networks: %w", err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, dto.ListNetworksResponse{
|
||||
Networks: networks,
|
||||
})
|
||||
}
|
||||
75
internal/server/api/settings/registry.go
Normal file
75
internal/server/api/settings/registry.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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 SetDockerRegistry(e *env.Env, c echo.Context) error {
|
||||
var req dto.SetDockerRegistryRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
if err := e.Dokku.LoginDockerRegistry(req.Server, req.Username, req.Password); err != nil {
|
||||
return echo.NewHTTPError(http.StatusForbidden, err.Error())
|
||||
}
|
||||
|
||||
propErr := e.Dokku.SetAppDockerRegistryProperty("--global", dokku.DockerRegistryPropertyServer, req.Server)
|
||||
if propErr != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, propErr.Error())
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func GetDockerRegistryReport(e *env.Env, c echo.Context) error {
|
||||
report, err := e.Dokku.GetDockerRegistryReport()
|
||||
if err != nil && !errors.Is(err, dokku.NoDeployedAppsError) {
|
||||
return fmt.Errorf("failed to get registry report: %w", err)
|
||||
}
|
||||
|
||||
var response dto.GetDockerRegistryReportResponse
|
||||
for _, appReport := range report {
|
||||
response.Server = appReport.GlobalServer
|
||||
response.PushOnRelease = appReport.GlobalPushOnRelease
|
||||
break
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func AddGitAuth(e *env.Env, c echo.Context) error {
|
||||
var req dto.AddGitAuthRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
if err := e.Dokku.GitSetAuth(req.Host, req.Username, req.Password); err != nil {
|
||||
return echo.NewHTTPError(http.StatusForbidden, err.Error())
|
||||
}
|
||||
|
||||
if err := e.Dokku.GitAllowHost(req.Host); err != nil {
|
||||
return echo.NewHTTPError(http.StatusForbidden, err.Error())
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func RemoveGitAuth(e *env.Env, c echo.Context) error {
|
||||
var req dto.RemoveGitAuthRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
if err := e.Dokku.GitRemoveAuth(req.Host); err != nil {
|
||||
return echo.NewHTTPError(http.StatusForbidden, err.Error())
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
33
internal/server/api/settings/routes.go
Normal file
33
internal/server/api/settings/routes.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
)
|
||||
|
||||
func RegisterRoutes(e *env.Env, g *echo.Group) {
|
||||
g.GET("/versions", e.H(GetVersions))
|
||||
|
||||
g.GET("/events", e.H(GetEventLogs))
|
||||
g.GET("/events/list", e.H(GetEventLogsList))
|
||||
g.POST("/events", e.H(SetEventLoggingEnabled))
|
||||
|
||||
g.GET("/users", e.H(GetUsers))
|
||||
g.GET("/ssh-keys", e.H(GetSSHKeys))
|
||||
|
||||
g.GET("/domains", e.H(GetGlobalDomains))
|
||||
g.POST("/domains", e.H(AddGlobalDomain))
|
||||
g.DELETE("/domains", e.H(RemoveGlobalDomain))
|
||||
|
||||
g.GET("/networks", e.H(ListNetworks))
|
||||
g.POST("/networks", e.H(CreateNetwork))
|
||||
g.DELETE("/networks", e.H(DestroyNetwork))
|
||||
|
||||
g.GET("/plugins", e.H(ListPlugins))
|
||||
|
||||
g.GET("/registry", e.H(GetDockerRegistryReport))
|
||||
g.POST("/registry", e.H(SetDockerRegistry))
|
||||
|
||||
g.POST("/git-auth", e.H(AddGitAuth))
|
||||
g.DELETE("/git-auth", e.H(RemoveGitAuth))
|
||||
}
|
||||
156
internal/server/api/settings/settings.go
Normal file
156
internal/server/api/settings/settings.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
"gitlab.com/texm/shokku/internal/models"
|
||||
"gitlab.com/texm/shokku/internal/server/dto"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func GetVersions(e *env.Env, c echo.Context) error {
|
||||
dokkuVersion, err := e.Dokku.GetDokkuVersion()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting dokku version: %w", err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, &dto.GetVersionsResponse{
|
||||
Dokku: dokkuVersion,
|
||||
Shokku: e.Version,
|
||||
})
|
||||
}
|
||||
|
||||
func GetUsers(e *env.Env, c echo.Context) error {
|
||||
var dbUsers []models.User
|
||||
res := e.DB.Model(models.User{}).Preload("SSHKeys").Find(&dbUsers)
|
||||
if res.Error != nil {
|
||||
return fmt.Errorf("querying db users: %w", res.Error)
|
||||
}
|
||||
|
||||
users := make([]dto.User, len(dbUsers))
|
||||
for i, dbUser := range dbUsers {
|
||||
keys := make([]string, len(dbUser.SSHKeys))
|
||||
for j, key := range dbUser.SSHKeys {
|
||||
keys[j] = key.Key
|
||||
}
|
||||
users[i] = dto.User{
|
||||
Name: dbUser.Name,
|
||||
Source: dbUser.Source,
|
||||
SSHKeys: keys,
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, dto.GetUsersResponse{
|
||||
Users: users,
|
||||
})
|
||||
}
|
||||
|
||||
func GetSSHKeys(e *env.Env, c echo.Context) error {
|
||||
keys, err := e.Dokku.ListSSHKeys()
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing ssh keys: %w", err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, &dto.GetSSHKeysResponse{
|
||||
Keys: keys,
|
||||
})
|
||||
}
|
||||
|
||||
func GetGlobalDomains(e *env.Env, c echo.Context) error {
|
||||
report, err := e.Dokku.GetGlobalDomainsReport()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting global domains report: %w", err)
|
||||
}
|
||||
|
||||
if len(report.Domains) == 0 {
|
||||
report.Domains = make([]string, 0)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, &dto.GetGlobalDomainsResponse{
|
||||
Domains: report.Domains,
|
||||
Enabled: report.Enabled,
|
||||
})
|
||||
}
|
||||
|
||||
func AddGlobalDomain(e *env.Env, c echo.Context) error {
|
||||
var req dto.AlterGlobalDomainRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
// TODO: domain verification etc
|
||||
if err := e.Dokku.AddGlobalDomain(req.Domain); err != nil {
|
||||
return fmt.Errorf("adding global domain: %w", err)
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func RemoveGlobalDomain(e *env.Env, c echo.Context) error {
|
||||
var req dto.DeleteGlobalDomainRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
// TODO: domain verification etc
|
||||
if err := e.Dokku.RemoveGlobalDomain(req.Domain); err != nil {
|
||||
return fmt.Errorf("removing global domain: %w", err)
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func SetEventLoggingEnabled(e *env.Env, c echo.Context) error {
|
||||
var req dto.SetEventLoggingEnabledRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
if err := e.Dokku.SetEventLoggingEnabled(req.Enabled); err != nil {
|
||||
return fmt.Errorf("setting event logging: %w", err)
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func GetEventLogsList(e *env.Env, c echo.Context) error {
|
||||
events, err := e.Dokku.ListLoggedEvents()
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing global domain: %w", err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, dto.GetEventLogsListResponse{
|
||||
Events: events,
|
||||
})
|
||||
}
|
||||
|
||||
func GetEventLogs(e *env.Env, c echo.Context) error {
|
||||
logs, err := e.Dokku.GetEventLogs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting event logs: %w", err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, dto.GetEventLogsResponse{
|
||||
Logs: logs,
|
||||
})
|
||||
}
|
||||
|
||||
func ListPlugins(e *env.Env, c echo.Context) error {
|
||||
plugins, err := e.Dokku.ListPlugins()
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing plugins: %w", err)
|
||||
}
|
||||
info := make([]dto.PluginInfo, len(plugins))
|
||||
for i := 0; i < len(plugins); i++ {
|
||||
p := plugins[i]
|
||||
info[i] = dto.PluginInfo{
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
Enabled: p.Enabled,
|
||||
Description: p.Description,
|
||||
}
|
||||
}
|
||||
return c.JSON(http.StatusOK, &dto.ListPluginsResponse{
|
||||
Plugins: info,
|
||||
})
|
||||
}
|
||||
111
internal/server/api/setup/github.go
Normal file
111
internal/server/api/setup/github.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/dto"
|
||||
"gitlab.com/texm/shokku/internal/server/github"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
githubAppInstallURL = "https://github.com/apps/%s/installations/new/permissions?target_id=%d"
|
||||
)
|
||||
|
||||
func GetGithubSetupStatus(e *env.Env, c echo.Context) error {
|
||||
var ghApp models.GithubApp
|
||||
r := e.DB.Find(&ghApp)
|
||||
if r.Error != nil && !errors.Is(r.Error, sql.ErrNoRows) {
|
||||
log.Error().Err(r.Error).Msg("Failed to lookup github app")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, dto.GetGithubSetupStatus{
|
||||
AppCreated: r.RowsAffected > 0,
|
||||
})
|
||||
}
|
||||
|
||||
func CreateGithubApp(e *env.Env, c echo.Context) error {
|
||||
var req dto.CreateGithubAppRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
cfg, err := github.CompleteAppManifest(e, req.Code)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, dto.CreateGithubAppResponse{
|
||||
Slug: cfg.GetSlug(),
|
||||
})
|
||||
}
|
||||
|
||||
func GetGithubAppInstallInfo(e *env.Env, c echo.Context) error {
|
||||
ctx := context.Background()
|
||||
|
||||
client, clientErr := github.GetAppClient(e)
|
||||
if clientErr != nil {
|
||||
log.Error().Err(clientErr).Msg("failed to get app client")
|
||||
return echo.NewHTTPError(http.StatusBadRequest, clientErr)
|
||||
}
|
||||
|
||||
app, appErr := client.GetApp(ctx)
|
||||
if appErr != nil {
|
||||
log.Error().Err(appErr).Msg("failed to get github client app")
|
||||
return echo.NewHTTPError(http.StatusBadRequest, appErr)
|
||||
}
|
||||
url := fmt.Sprintf(githubAppInstallURL, app.GetSlug(), app.Owner.GetID())
|
||||
|
||||
return c.JSON(http.StatusOK, dto.InstallGithubAppResponse{
|
||||
InstallURL: url,
|
||||
})
|
||||
}
|
||||
|
||||
func CompleteGithubSetup(e *env.Env, c echo.Context) error {
|
||||
var req dto.CompleteGithubSetupRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
client, clientErr := github.GetAppClient(e)
|
||||
if clientErr != nil {
|
||||
log.Error().Err(clientErr).Msg("failed to get app client")
|
||||
return echo.ErrBadRequest
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
inst, _, instErr := client.Apps.GetInstallation(ctx, req.InstallationId)
|
||||
if instErr != nil {
|
||||
log.Error().
|
||||
Err(instErr).
|
||||
Int64("id", req.InstallationId).
|
||||
Msg("failed to get installation")
|
||||
return echo.ErrBadRequest
|
||||
}
|
||||
|
||||
if err := setupServerWithAuthMethod(e, auth.MethodGithub); err != nil {
|
||||
log.Error().Err(err).Msg("failed to setup github auth")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := github.SyncUsersToDB(e); err != nil {
|
||||
log.Error().Err(err).Msg("failed to sync github users")
|
||||
}
|
||||
}()
|
||||
|
||||
log.Debug().
|
||||
Int64("id", inst.GetID()).
|
||||
Msg("installed github app")
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
101
internal/server/api/setup/password.go
Normal file
101
internal/server/api/setup/password.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gitlab.com/texm/shokku/internal/models"
|
||||
"gitlab.com/texm/shokku/internal/server/auth"
|
||||
"image/png"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
"gitlab.com/texm/shokku/internal/server/dto"
|
||||
)
|
||||
|
||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func generateRandomString(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func GenerateTotp(e *env.Env, c echo.Context) error {
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "shokku",
|
||||
AccountName: "admin account",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate totp key: %w", err)
|
||||
}
|
||||
|
||||
var imgBuf bytes.Buffer
|
||||
img, imgErr := key.Image(160, 160)
|
||||
if imgErr != nil {
|
||||
return fmt.Errorf("failed to generate totp image: %w", imgErr)
|
||||
}
|
||||
if encErr := png.Encode(&imgBuf, img); encErr != nil {
|
||||
return fmt.Errorf("failed to encode totp image to png: %w", encErr)
|
||||
}
|
||||
return c.JSON(http.StatusOK, dto.GenerateTotpResponse{
|
||||
Secret: key.Secret(),
|
||||
Image: base64.StdEncoding.EncodeToString(imgBuf.Bytes()),
|
||||
RecoveryCode: generateRandomString(12),
|
||||
})
|
||||
}
|
||||
|
||||
func ConfirmTotp(e *env.Env, c echo.Context) error {
|
||||
var req dto.ConfirmTotpRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, dto.ConfirmTotpResponse{
|
||||
Valid: totp.Validate(req.Code, req.Secret),
|
||||
})
|
||||
}
|
||||
|
||||
func CompletePasswordSetup(e *env.Env, c echo.Context) error {
|
||||
var req dto.CompletePasswordSetupRequest
|
||||
if err := dto.BindRequest(c, &req); err != nil {
|
||||
return err.ToHTTP()
|
||||
}
|
||||
|
||||
if err := setupServerWithAuthMethod(e, auth.MethodPassword); err != nil {
|
||||
log.Error().Err(err).Msg("failed to setup github auth")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
pw, ok := e.Auth.(*auth.PasswordAuthenticator)
|
||||
if !ok {
|
||||
log.Error().Msg("failed to cast e.Auth to pw auth")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
passwordHash, pwErr := pw.HashPassword([]byte(req.Password))
|
||||
if pwErr != nil {
|
||||
log.Error().Err(pwErr).Msg("failed to hash password")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
Name: req.Username,
|
||||
Source: "manual",
|
||||
PasswordHash: passwordHash,
|
||||
TotpEnabled: req.Enable2FA,
|
||||
RecoveryCode: req.RecoveryCode,
|
||||
TotpSecret: req.TotpSecret,
|
||||
}
|
||||
if err := e.DB.Save(&user).Error; err != nil {
|
||||
log.Error().Err(err).Msg("failed to save initial password auth user")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
20
internal/server/api/setup/routes.go
Normal file
20
internal/server/api/setup/routes.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"gitlab.com/texm/shokku/internal/env"
|
||||
)
|
||||
|
||||
func RegisterRoutes(e *env.Env, g *echo.Group) {
|
||||
g.GET("/status", e.H(GetStatus))
|
||||
g.GET("/verify-key", e.H(GetSetupKeyValid))
|
||||
|
||||
g.POST("/github/create-app", e.H(CreateGithubApp))
|
||||
g.GET("/github/install-info", e.H(GetGithubAppInstallInfo))
|
||||
g.GET("/github/status", e.H(GetGithubSetupStatus))
|
||||
g.POST("/github/completed", e.H(CompleteGithubSetup))
|
||||
|
||||
g.POST("/password", e.H(CompletePasswordSetup))
|
||||
g.POST("/totp/new", e.H(GenerateTotp))
|
||||
g.POST("/totp/confirm", e.H(ConfirmTotp))
|
||||
}
|
||||
65
internal/server/api/setup/setup.go
Normal file
65
internal/server/api/setup/setup.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
"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/dto"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetStatus(e *env.Env, c echo.Context) error {
|
||||
method := string(e.Auth.GetMethod())
|
||||
log.Debug().
|
||||
Bool("is_setup", e.SetupCompleted).
|
||||
Str("method", method).
|
||||
Msg("get setup status")
|
||||
return c.JSON(http.StatusOK, dto.GetSetupStatusResponse{
|
||||
IsSetup: e.SetupCompleted,
|
||||
Method: method,
|
||||
})
|
||||
}
|
||||
|
||||
func GetSetupKeyValid(e *env.Env, c echo.Context) error {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func setupServerWithAuthMethod(e *env.Env, method auth.Method) error {
|
||||
var state models.Server
|
||||
e.DB.FirstOrCreate(&state)
|
||||
|
||||
state.IsSetup = true
|
||||
state.AuthMethod = method
|
||||
if err := e.DB.Save(&state).Error; err != nil {
|
||||
log.Error().Err(err).Msg("failed to save setup state")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
newAuth, authErr := createAuthenticator(e, method)
|
||||
if authErr != nil {
|
||||
log.Error().Err(authErr).Msg("failed to init new authenticator")
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
e.Auth = newAuth
|
||||
e.SetupCompleted = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createAuthenticator(e *env.Env, method auth.Method) (auth.Authenticator, error) {
|
||||
config := auth.Config{
|
||||
SigningKey: e.Auth.GetSigningKey(),
|
||||
CookieDomain: e.Auth.GetCookieDomain(),
|
||||
TokenLifetime: e.Auth.GetTokenLifetime(),
|
||||
}
|
||||
switch method {
|
||||
case auth.MethodGithub:
|
||||
return auth.NewGithubAuthenticator(config)
|
||||
case auth.MethodPassword:
|
||||
return auth.NewPasswordAuthenticator(config, auth.DefaultBCryptCost)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown method %s", method)
|
||||
}
|
||||
Reference in New Issue
Block a user