feat: implement port forwarding
This commit is contained in:
@@ -3,7 +3,7 @@ package migration
|
||||
import (
|
||||
"embed"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/sqlite"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
_ "modernc.org/sqlite"
|
||||
|
@@ -27,7 +27,10 @@ CREATE TABLE IF NOT EXISTS template_files
|
||||
file_path TEXT NOT NULL,
|
||||
content BLOB NOT NULL,
|
||||
|
||||
CONSTRAINT pk_template_files PRIMARY KEY (template_id, file_path)
|
||||
CONSTRAINT pk_template_files PRIMARY KEY (template_id, file_path),
|
||||
CONSTRAINT fk_template_template_files FOREIGN KEY (template_id) REFERENCES templates (id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS template_images
|
||||
@@ -36,7 +39,10 @@ CREATE TABLE IF NOT EXISTS template_images
|
||||
image_tag TEXT NOT NULL,
|
||||
image_id TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT pk_template_images PRIMARY KEY (template_id, image_tag, image_id)
|
||||
CONSTRAINT pk_template_images PRIMARY KEY (template_id, image_tag, image_id),
|
||||
CONSTRAINT fk_template_template_images FOREIGN KEY (template_id) REFERENCES templates (id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS port_mappings
|
||||
@@ -45,5 +51,8 @@ CREATE TABLE IF NOT EXISTS port_mappings
|
||||
container_port INTEGER NOT NULL,
|
||||
subdomain TEXT,
|
||||
|
||||
CONSTRAINT pk_port_mappings PRIMARY KEY (workspace_id, container_port, subdomain)
|
||||
CONSTRAINT pk_port_mappings PRIMARY KEY (workspace_id, container_port, subdomain),
|
||||
CONSTRAINT fk_workspace_port_mappings FOREIGN KEY (workspace_id) REFERENCES workspaces (id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
)
|
@@ -48,6 +48,10 @@ func (p *ReverseProxy) AddEntry(subdomain string, url *url.URL) {
|
||||
p.httpProxies[subdomain] = proxy
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) RemoveEntry(subdomain string) {
|
||||
delete(p.httpProxies, subdomain)
|
||||
}
|
||||
|
||||
func (p *ReverseProxy) shouldHandleRequest(c echo.Context) bool {
|
||||
h := strings.Replace(p.hostName, ".", "\\.", -1)
|
||||
reg, err := regexp.Compile(".*\\." + h)
|
||||
@@ -83,7 +87,8 @@ func (p *ReverseProxy) handleRequest(c echo.Context) error {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
||||
proxy, ok := p.httpProxies[subdomain]
|
||||
first := strings.Split(subdomain, ".")[0]
|
||||
proxy, ok := p.httpProxies[first]
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ func createDockerTemplate(ctx context.Context, tx bun.Tx, opts createTemplateOpt
|
||||
FilePath: "README.md",
|
||||
Content: make([]byte, 0),
|
||||
}
|
||||
files := []templateFile{dockerfile, readme}
|
||||
files := []*templateFile{&dockerfile, &readme}
|
||||
|
||||
if err = tx.NewInsert().Model(&t).Returning("*").Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
@@ -59,6 +59,8 @@ func createDockerTemplate(ctx context.Context, tx bun.Tx, opts createTemplateOpt
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.Files = files
|
||||
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,8 @@ type updateWorkspaceRequestBody struct {
|
||||
PortMappings []portMapping `json:"ports"`
|
||||
}
|
||||
|
||||
const keyCurrentWorkspace = "currentWorkspace"
|
||||
|
||||
func fetchAllWorkspaces(c echo.Context) error {
|
||||
mgr := workspaceManagerFrom(c)
|
||||
workspaces, err := mgr.findAllWorkspaces(c.Request().Context())
|
||||
@@ -26,28 +28,44 @@ func fetchAllWorkspaces(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, workspaces)
|
||||
}
|
||||
|
||||
func currentWorkspace(c echo.Context) *workspace {
|
||||
return c.Get(keyCurrentWorkspace).(*workspace)
|
||||
}
|
||||
|
||||
func currentWorkspaceMiddleware(ignoreMissing bool) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
workspaceName := c.Param("workspaceName")
|
||||
if workspaceName == "" || !workspaceNameRegex.MatchString(workspaceName) {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
||||
mgr := workspaceManagerFrom(c)
|
||||
workspace, err := mgr.findWorkspace(c.Request().Context(), workspaceName)
|
||||
if err != nil {
|
||||
if errors.Is(err, errWorkspaceNotFound) {
|
||||
if ignoreMissing {
|
||||
c.Set(keyCurrentWorkspace, nil)
|
||||
} else {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.Set(keyCurrentWorkspace, workspace)
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateOrCreateWorkspace(c echo.Context) error {
|
||||
workspaceName := c.Param("workspaceName")
|
||||
if workspaceName == "" {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
workspace := currentWorkspace(c)
|
||||
if workspace == nil {
|
||||
return createWorkspace(c, c.Param("workspaceName"))
|
||||
}
|
||||
|
||||
if !workspaceNameRegex.MatchString(workspaceName) {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
mgr := workspaceManagerFrom(c)
|
||||
|
||||
exists, err := mgr.hasWorkspace(ctx, workspaceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return createWorkspace(c, workspaceName)
|
||||
}
|
||||
|
||||
return updateWorkspace(c, workspaceName)
|
||||
return updateWorkspace(c, workspace)
|
||||
}
|
||||
|
||||
func createWorkspace(c echo.Context, workspaceName string) error {
|
||||
@@ -72,7 +90,7 @@ func createWorkspace(c echo.Context, workspaceName string) error {
|
||||
return c.JSON(http.StatusOK, w)
|
||||
}
|
||||
|
||||
func updateWorkspace(c echo.Context, workspaceName string) error {
|
||||
func updateWorkspace(c echo.Context, workspace *workspace) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var body updateWorkspaceRequestBody
|
||||
@@ -83,14 +101,6 @@ func updateWorkspace(c echo.Context, workspaceName string) error {
|
||||
|
||||
mgr := workspaceManagerFrom(c)
|
||||
|
||||
workspace, err := mgr.findWorkspace(ctx, workspaceName)
|
||||
if err != nil {
|
||||
if errors.Is(err, errWorkspaceNotFound) {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
switch status(body.Status) {
|
||||
case statusStopped:
|
||||
if err = mgr.stopWorkspace(ctx, workspace); err != nil {
|
||||
@@ -115,13 +125,9 @@ func updateWorkspace(c echo.Context, workspaceName string) error {
|
||||
}
|
||||
|
||||
func deleteWorkspace(c echo.Context) error {
|
||||
workspaceName := c.Param("workspaceName")
|
||||
if workspaceName == "" {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
||||
workspace := currentWorkspace(c)
|
||||
mgr := workspaceManagerFrom(c)
|
||||
if err := mgr.deleteWorkspace(c.Request().Context(), workspaceName); err != nil {
|
||||
if err := mgr.deleteWorkspace(c.Request().Context(), workspace); err != nil {
|
||||
if errors.Is(err, errWorkspaceNotFound) {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
@@ -130,3 +136,31 @@ func deleteWorkspace(c echo.Context) error {
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func deleteWorkspacePortMapping(c echo.Context) error {
|
||||
workspace := currentWorkspace(c)
|
||||
mgr := workspaceManagerFrom(c)
|
||||
|
||||
portName := c.Param("portName")
|
||||
if portName == "" {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
||||
var portMapping *portMapping
|
||||
for _, m := range workspace.PortMappings {
|
||||
if m.Subdomain == portName {
|
||||
portMapping = &m
|
||||
break
|
||||
}
|
||||
}
|
||||
if portMapping == nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
||||
err := mgr.deletePortMapping(c.Request().Context(), workspace, portMapping)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
func DefineRoutes(g *echo.Group, services service.Services) {
|
||||
g.Use(newWorkspaceManagerMiddleware(services))
|
||||
g.GET("/workspaces", fetchAllWorkspaces)
|
||||
g.POST("/workspaces/:workspaceName", updateOrCreateWorkspace)
|
||||
g.DELETE("/workspaces/:workspaceName", deleteWorkspace)
|
||||
g.POST("/workspaces/:workspaceName", updateOrCreateWorkspace, currentWorkspaceMiddleware(true))
|
||||
g.DELETE("/workspaces/:workspaceName", deleteWorkspace, currentWorkspaceMiddleware(false))
|
||||
g.DELETE("/workspaces/:workspaceName/forwarded-ports/:portName", deleteWorkspacePortMapping, currentWorkspaceMiddleware(false))
|
||||
}
|
||||
|
@@ -45,7 +45,7 @@ type portMapping struct {
|
||||
ContainerPort int `json:"port"`
|
||||
Subdomain string `json:"subdomain"`
|
||||
|
||||
Workspace workspace `bun:"rel:belongs-to,join:workspace_id=id"`
|
||||
Workspace workspace `bun:"rel:belongs-to,join:workspace_id=id" json:"-"`
|
||||
}
|
||||
|
||||
// status represents the status of a workspace.
|
||||
@@ -121,15 +121,19 @@ func SyncAll(ctx context.Context, services service.Services) error {
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if err = errors.Join(errs...); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.NewDelete().Model(&deletedWorkspaces).WherePK().Exec(ctx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
if len(deletedWorkspaces) > 0 {
|
||||
_, err = tx.NewDelete().Model(&deletedWorkspaces).WherePK().Exec(ctx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
|
@@ -38,7 +38,7 @@ var errWorkspaceNotFound = errors.New("workspace not found")
|
||||
|
||||
func (mgr workspaceManager) findAllWorkspaces(ctx context.Context) ([]workspace, error) {
|
||||
var workspaces []workspace
|
||||
err := mgr.db.NewSelect().Model(&workspaces).Scan(ctx)
|
||||
err := mgr.db.NewSelect().Model(&workspaces).Relation("PortMappings").Scan(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return make([]workspace, 0), nil
|
||||
@@ -100,6 +100,7 @@ func (mgr workspaceManager) findAllWorkspaces(ctx context.Context) ([]workspace,
|
||||
func (mgr workspaceManager) findWorkspace(ctx context.Context, name string) (*workspace, error) {
|
||||
var w workspace
|
||||
err := mgr.db.NewSelect().Model(&w).
|
||||
Relation("PortMappings").
|
||||
Where("name = ?", name).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
@@ -214,24 +215,12 @@ func (mgr workspaceManager) createWorkspace(ctx context.Context, opts createWork
|
||||
return &w, nil
|
||||
}
|
||||
|
||||
func (mgr workspaceManager) deleteWorkspace(ctx context.Context, name string) error {
|
||||
func (mgr workspaceManager) deleteWorkspace(ctx context.Context, workspace *workspace) error {
|
||||
tx, err := mgr.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var workspace workspace
|
||||
if err = tx.NewSelect().
|
||||
Model(&workspace).
|
||||
Where("name = ?", name).
|
||||
Scan(ctx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return errWorkspaceNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
inspect, err := mgr.dockerClient.ContainerInspect(ctx, workspace.ContainerID)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
@@ -252,7 +241,7 @@ func (mgr workspaceManager) deleteWorkspace(ctx context.Context, name string) er
|
||||
}
|
||||
|
||||
res, err := tx.NewDelete().
|
||||
Model(&workspace).
|
||||
Model(workspace).
|
||||
WherePK().
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
@@ -357,3 +346,29 @@ func (mgr workspaceManager) addPortMappings(ctx context.Context, workspace *work
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mgr workspaceManager) deletePortMapping(ctx context.Context, workspace *workspace, portMapping *portMapping) error {
|
||||
tx, err := mgr.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.NewDelete().Model(portMapping).
|
||||
Where("workspace_id = ?", workspace.ID).
|
||||
Where("subdomain = ?", portMapping.Subdomain).
|
||||
Where("container_port = ?", portMapping.ContainerPort).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
mgr.reverseProxy.RemoveEntry(portMapping.Subdomain)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user