Initial commit
This commit is contained in:
92
backend/internal/api/api.go
Normal file
92
backend/internal/api/api.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"clickploy/internal/builder"
|
||||
"clickploy/internal/deployer"
|
||||
"clickploy/internal/ports"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
builder *builder.Builder
|
||||
deployer *deployer.Deployer
|
||||
ports *ports.Manager
|
||||
}
|
||||
|
||||
func NewHandler(b *builder.Builder, d *deployer.Deployer, p *ports.Manager) *Handler {
|
||||
return &Handler{
|
||||
builder: b,
|
||||
deployer: d,
|
||||
ports: p,
|
||||
}
|
||||
}
|
||||
|
||||
type DeployRequest struct {
|
||||
Repo string `json:"repo" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Port int `json:"port"`
|
||||
GitToken string `json:"git_token"`
|
||||
}
|
||||
|
||||
type DeployResponse struct {
|
||||
Status string `json:"status"`
|
||||
AppName string `json:"app_name"`
|
||||
Port int `json:"port"`
|
||||
URL string `json:"url"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterRoutes(r *gin.Engine) {
|
||||
r.POST("/deploy", h.handleDeploy)
|
||||
h.RegisterStreamRoutes(r)
|
||||
}
|
||||
|
||||
func (h *Handler) handleDeploy(c *gin.Context) {
|
||||
var req DeployRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
imageName, err := h.builder.Build(req.Repo, req.Name, req.GitToken, "", "", "", "", nil, os.Stdout)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Build failed: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
port, err := h.ports.GetPort(req.Name, req.Port)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Port allocation failed: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.deployer.RunContainer(c.Request.Context(), imageName, req.Name, port, nil)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Deployment failed: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, DeployResponse{
|
||||
Status: "success",
|
||||
AppName: req.Name,
|
||||
Port: port,
|
||||
URL: fmt.Sprintf("http://localhost:%d", port),
|
||||
Message: "Container started successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterSystemRoutes(r *gin.Engine) {
|
||||
r.GET("/api/system/status", h.handleSystemStatus)
|
||||
}
|
||||
|
||||
func (h *Handler) handleSystemStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"version": "v0.1.0",
|
||||
"status": "All systems normal",
|
||||
})
|
||||
}
|
||||
147
backend/internal/api/auth.go
Normal file
147
backend/internal/api/auth.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"clickploy/internal/auth"
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
User models.User `json:"user"`
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterAuthRoutes(r *gin.Engine) {
|
||||
authGroup := r.Group("/auth")
|
||||
{
|
||||
authGroup.POST("/register", h.register)
|
||||
authGroup.POST("/login", h.login)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) register(c *gin.Context) {
|
||||
var req AuthRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
hashed, err := auth.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
user := models.User{
|
||||
Email: req.Email,
|
||||
Password: hashed,
|
||||
Name: req.Name,
|
||||
Avatar: "https://github.com/shadcn.png",
|
||||
}
|
||||
|
||||
if result := db.DB.Create(&user); result.Error != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Email already exists"})
|
||||
return
|
||||
}
|
||||
|
||||
token, _ := auth.GenerateToken(user.ID, user.Email)
|
||||
c.JSON(http.StatusCreated, AuthResponse{Token: token, User: user})
|
||||
}
|
||||
|
||||
func (h *Handler) login(c *gin.Context) {
|
||||
var req AuthRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if result := db.DB.Where("email = ?", req.Email).First(&user); result.Error != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
if !auth.CheckPassword(req.Password, user.Password) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
token, _ := auth.GenerateToken(user.ID, user.Email)
|
||||
c.JSON(http.StatusOK, AuthResponse{Token: token, User: user})
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterUserRoutes(r *gin.Engine) {
|
||||
userGroup := r.Group("/api/user", AuthMiddleware())
|
||||
{
|
||||
userGroup.PUT("/profile", h.updateProfile)
|
||||
userGroup.PUT("/password", h.updatePassword)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) updateProfile(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if result := db.DB.First(&user, userID); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
user.Name = req.Name
|
||||
}
|
||||
if req.Email != "" {
|
||||
user.Email = req.Email
|
||||
}
|
||||
|
||||
db.DB.Save(&user)
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
func (h *Handler) updatePassword(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
var req struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if result := db.DB.First(&user, userID); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if !auth.CheckPassword(req.OldPassword, user.Password) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Incorrect old password"})
|
||||
return
|
||||
}
|
||||
|
||||
hashed, _ := auth.HashPassword(req.NewPassword)
|
||||
user.Password = hashed
|
||||
db.DB.Save(&user)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Password updated"})
|
||||
}
|
||||
33
backend/internal/api/middleware.go
Normal file
33
backend/internal/api/middleware.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"clickploy/internal/auth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
claims, err := auth.ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("userID", claims.UserID)
|
||||
c.Set("email", claims.Email)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
283
backend/internal/api/project.go
Normal file
283
backend/internal/api/project.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"clickploy/internal/auth"
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (h *Handler) RegisterProjectRoutes(r *gin.Engine) {
|
||||
protected := r.Group("/api", AuthMiddleware())
|
||||
{
|
||||
protected.POST("/projects", h.createProject)
|
||||
protected.GET("/projects", h.listProjects)
|
||||
protected.GET("/projects/:id", h.getProject)
|
||||
protected.PUT("/projects/:id/env", h.updateProjectEnv)
|
||||
protected.POST("/projects/:id/redeploy", h.redeployProject)
|
||||
protected.GET("/activity", h.getActivity)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) updateProjectEnv(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
projectID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
EnvVars map[string]string `json:"env_vars"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
pid, err := strconv.ParseUint(projectID, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var project models.Project
|
||||
if result := db.DB.Where("id = ? AND owner_id = ?", pid, userID).First(&project); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
tx := db.DB.Begin()
|
||||
if err := tx.Where("project_id = ?", project.ID).Delete(&models.EnvVar{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update env vars"})
|
||||
return
|
||||
}
|
||||
|
||||
for k, v := range req.EnvVars {
|
||||
envVar := models.EnvVar{
|
||||
ProjectID: project.ID,
|
||||
Key: k,
|
||||
Value: v,
|
||||
}
|
||||
if err := tx.Create(&envVar).Error; err != nil {
|
||||
tx.Rollback()
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save env var"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
c.JSON(http.StatusOK, gin.H{"status": "updated"})
|
||||
}
|
||||
|
||||
func (h *Handler) redeployProject(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
projectID := c.Param("id")
|
||||
|
||||
pid, err := strconv.ParseUint(projectID, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var project models.Project
|
||||
if result := db.DB.Preload("EnvVars").Where("id = ? AND owner_id = ?", pid, userID).First(&project); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
deployment := models.Deployment{
|
||||
ProjectID: project.ID,
|
||||
Status: "building",
|
||||
Commit: "MANUAL",
|
||||
Logs: "Starting manual redeploy...",
|
||||
}
|
||||
db.DB.Create(&deployment)
|
||||
|
||||
go func() {
|
||||
var logBuffer bytes.Buffer
|
||||
streamer := &StreamWriter{DeploymentID: deployment.ID}
|
||||
multi := io.MultiWriter(&logBuffer, streamer)
|
||||
|
||||
envMap := make(map[string]string)
|
||||
for _, env := range project.EnvVars {
|
||||
envMap[env.Key] = env.Value
|
||||
}
|
||||
|
||||
imageName, err := h.builder.Build(project.RepoURL, project.Name, project.GitToken, project.BuildCommand, project.StartCommand, project.InstallCommand, project.Runtime, envMap, multi)
|
||||
deployment.Logs = logBuffer.String()
|
||||
|
||||
if err != nil {
|
||||
deployment.Status = "failed"
|
||||
deployment.Logs += fmt.Sprintf("\n\nBuild Error: %v", err)
|
||||
db.DB.Save(&deployment)
|
||||
return
|
||||
}
|
||||
|
||||
var envStrings []string
|
||||
for _, env := range project.EnvVars {
|
||||
envStrings = append(envStrings, fmt.Sprintf("%s=%s", env.Key, env.Value))
|
||||
}
|
||||
|
||||
containerID, err := h.deployer.RunContainer(c.Request.Context(), imageName, project.Name, project.Port, envStrings)
|
||||
if err != nil {
|
||||
deployment.Status = "failed"
|
||||
deployment.Logs += fmt.Sprintf("\n\nContainer Error: %v", err)
|
||||
db.DB.Save(&deployment)
|
||||
return
|
||||
}
|
||||
|
||||
deployment.Status = "live"
|
||||
deployment.URL = fmt.Sprintf("http://localhost:%d", project.Port)
|
||||
deployment.Logs += fmt.Sprintf("\n\nContainer ID: %s", containerID)
|
||||
db.DB.Save(&deployment)
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "redeployment_started", "deployment_id": deployment.ID})
|
||||
}
|
||||
|
||||
func (h *Handler) getActivity(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
var deployments []models.Deployment
|
||||
|
||||
err := db.DB.Joins("JOIN projects ON projects.id = deployments.project_id").
|
||||
Where("projects.owner_id = ?", userID).
|
||||
Order("deployments.created_at desc").
|
||||
Limit(20).
|
||||
Preload("Project").
|
||||
Find(&deployments).Error
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch activity"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, deployments)
|
||||
}
|
||||
|
||||
func (h *Handler) createProject(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
|
||||
var req struct {
|
||||
DeployRequest
|
||||
EnvVars map[string]string `json:"env_vars"`
|
||||
BuildCommand string `json:"build_command"`
|
||||
StartCommand string `json:"start_command"`
|
||||
InstallCommand string `json:"install_command"`
|
||||
Runtime string `json:"runtime"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
port, err := h.ports.GetPort(req.Name, req.Port)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Port allocation failed: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
var envVarsModel []models.EnvVar
|
||||
var envStrings []string
|
||||
for k, v := range req.EnvVars {
|
||||
envStrings = append(envStrings, fmt.Sprintf("%s=%s", k, v))
|
||||
envVarsModel = append(envVarsModel, models.EnvVar{Key: k, Value: v})
|
||||
}
|
||||
|
||||
webhookSecret, _ := auth.HashPassword(fmt.Sprintf("%s-%d-%d", req.Name, userID, time.Now().UnixNano()))
|
||||
|
||||
project := models.Project{
|
||||
Name: req.Name,
|
||||
RepoURL: req.Repo,
|
||||
OwnerID: userID.(uint),
|
||||
Port: port,
|
||||
WebhookSecret: webhookSecret,
|
||||
GitToken: req.GitToken,
|
||||
EnvVars: envVarsModel,
|
||||
BuildCommand: req.BuildCommand,
|
||||
StartCommand: req.StartCommand,
|
||||
InstallCommand: req.InstallCommand,
|
||||
Runtime: req.Runtime,
|
||||
}
|
||||
|
||||
if result := db.DB.Create(&project); result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save project to DB"})
|
||||
return
|
||||
}
|
||||
|
||||
deployment := models.Deployment{
|
||||
ProjectID: project.ID,
|
||||
Status: "building",
|
||||
Commit: "HEAD",
|
||||
Logs: "Starting build...",
|
||||
}
|
||||
db.DB.Create(&deployment)
|
||||
|
||||
go func() {
|
||||
var logBuffer bytes.Buffer
|
||||
streamer := &StreamWriter{DeploymentID: deployment.ID}
|
||||
multi := io.MultiWriter(&logBuffer, streamer)
|
||||
|
||||
imageName, err := h.builder.Build(req.Repo, req.Name, req.GitToken, req.BuildCommand, req.StartCommand, req.InstallCommand, req.Runtime, req.EnvVars, multi)
|
||||
deployment.Logs = logBuffer.String()
|
||||
|
||||
if err != nil {
|
||||
deployment.Status = "failed"
|
||||
deployment.Logs += fmt.Sprintf("\n\nBuild Error: %v", err)
|
||||
db.DB.Save(&deployment)
|
||||
return
|
||||
}
|
||||
|
||||
containerID, err := h.deployer.RunContainer(c.Request.Context(), imageName, req.Name, port, envStrings)
|
||||
if err != nil {
|
||||
deployment.Status = "failed"
|
||||
deployment.Logs += fmt.Sprintf("\n\nContainer Error: %v", err)
|
||||
db.DB.Save(&deployment)
|
||||
return
|
||||
}
|
||||
|
||||
deployment.Status = "live"
|
||||
deployment.URL = fmt.Sprintf("http://localhost:%d", port)
|
||||
deployment.Logs += fmt.Sprintf("\n\nContainer ID: %s", containerID)
|
||||
db.DB.Save(&deployment)
|
||||
}()
|
||||
|
||||
project.Deployments = []models.Deployment{deployment}
|
||||
c.JSON(http.StatusOK, project)
|
||||
}
|
||||
|
||||
func (h *Handler) listProjects(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
var projects []models.Project
|
||||
if result := db.DB.Preload("Deployments").Where("owner_id = ?", userID).Find(&projects); result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch projects"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, projects)
|
||||
}
|
||||
|
||||
func (h *Handler) getProject(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
projectID := c.Param("id")
|
||||
|
||||
pid, err := strconv.ParseUint(projectID, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var project models.Project
|
||||
if result := db.DB.Order("created_at desc").Preload("Deployments", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("deployments.created_at desc")
|
||||
}).Preload("EnvVars").Where("id = ? AND owner_id = ?", pid, userID).First(&project); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, project)
|
||||
}
|
||||
98
backend/internal/api/storage.go
Normal file
98
backend/internal/api/storage.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type CreateDatabaseRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required,oneof=sqlite"`
|
||||
}
|
||||
|
||||
type StorageStatsResponse struct {
|
||||
TotalMB float64 `json:"total_mb"`
|
||||
UsedMB float64 `json:"used_mb"`
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterStorageRoutes(r *gin.Engine) {
|
||||
api := r.Group("/api/storage")
|
||||
api.Use(AuthMiddleware())
|
||||
{
|
||||
api.GET("/stats", h.handleGetStorageStats)
|
||||
api.GET("/databases", h.handleListDatabases)
|
||||
api.POST("/databases", h.handleCreateDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleGetStorageStats(c *gin.Context) {
|
||||
var stat syscall.Statfs_t
|
||||
wd, _ := os.Getwd()
|
||||
|
||||
if err := syscall.Statfs(wd, &stat); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get disk stats"})
|
||||
return
|
||||
}
|
||||
|
||||
totalBytes := stat.Blocks * uint64(stat.Bsize)
|
||||
availBytes := stat.Bavail * uint64(stat.Bsize)
|
||||
usedBytes := totalBytes - availBytes
|
||||
|
||||
c.JSON(http.StatusOK, StorageStatsResponse{
|
||||
TotalMB: float64(totalBytes) / 1024 / 1024,
|
||||
UsedMB: float64(usedBytes) / 1024 / 1024,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) handleListDatabases(c *gin.Context) {
|
||||
userId := c.GetUint("userID")
|
||||
var dbs []models.Database
|
||||
if err := db.DB.Where("owner_id = ?", userId).Find(&dbs).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list databases"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, dbs)
|
||||
}
|
||||
|
||||
func (h *Handler) handleCreateDatabase(c *gin.Context) {
|
||||
userId := c.GetUint("userID")
|
||||
var req CreateDatabaseRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
newDB := models.Database{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Status: "available",
|
||||
OwnerID: userId,
|
||||
SizeMB: 0,
|
||||
}
|
||||
|
||||
dataDir := "./data/user_dbs"
|
||||
os.MkdirAll(dataDir, 0755)
|
||||
|
||||
dbPath := filepath.Join(dataDir, fmt.Sprintf("%d_%s.db", userId, req.Name))
|
||||
file, err := os.Create(dbPath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create database file"})
|
||||
return
|
||||
}
|
||||
file.Close()
|
||||
|
||||
if err := db.DB.Create(&newDB).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save database record"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, newDB)
|
||||
}
|
||||
111
backend/internal/api/stream.go
Normal file
111
backend/internal/api/stream.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
type LogHub struct {
|
||||
mu sync.Mutex
|
||||
streams map[uint][]chan []byte
|
||||
}
|
||||
|
||||
var Hub = &LogHub{
|
||||
streams: make(map[uint][]chan []byte),
|
||||
}
|
||||
|
||||
func (h *LogHub) Broadcast(deploymentID uint, p []byte) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if sinks, ok := h.streams[deploymentID]; ok {
|
||||
for _, sink := range sinks {
|
||||
select {
|
||||
case sink <- p:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LogHub) Subscribe(deploymentID uint) chan []byte {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
ch := make(chan []byte, 256)
|
||||
h.streams[deploymentID] = append(h.streams[deploymentID], ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (h *LogHub) Unsubscribe(deploymentID uint, ch chan []byte) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if sinks, ok := h.streams[deploymentID]; ok {
|
||||
for i, sink := range sinks {
|
||||
if sink == ch {
|
||||
h.streams[deploymentID] = append(sinks[:i], sinks[i+1:]...)
|
||||
close(ch)
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(h.streams[deploymentID]) == 0 {
|
||||
delete(h.streams, deploymentID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type StreamWriter struct {
|
||||
DeploymentID uint
|
||||
}
|
||||
|
||||
func (w *StreamWriter) Write(p []byte) (n int, err error) {
|
||||
c := make([]byte, len(p))
|
||||
copy(c, p)
|
||||
Hub.Broadcast(w.DeploymentID, c)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (h *Handler) streamDeploymentLogs(c *gin.Context) {
|
||||
deploymentIDStr := c.Param("id")
|
||||
deploymentID, err := strconv.ParseUint(deploymentIDStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
logChan := Hub.Subscribe(uint(deploymentID))
|
||||
defer Hub.Unsubscribe(uint(deploymentID), logChan)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if _, _, err := conn.NextReader(); err != nil {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for logChunk := range logChan {
|
||||
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
if err := conn.WriteMessage(websocket.TextMessage, logChunk); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterStreamRoutes(r *gin.Engine) {
|
||||
r.GET("/api/deployments/:id/logs/stream", h.streamDeploymentLogs)
|
||||
}
|
||||
89
backend/internal/api/webhook.go
Normal file
89
backend/internal/api/webhook.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *Handler) RegisterWebhookRoutes(r *gin.Engine) {
|
||||
r.POST("/webhooks/trigger", h.handleWebhook)
|
||||
}
|
||||
|
||||
func (h *Handler) handleWebhook(c *gin.Context) {
|
||||
projectIDHex := c.Query("project_id")
|
||||
if projectIDHex == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "project_id required"})
|
||||
return
|
||||
}
|
||||
|
||||
pid, err := strconv.ParseUint(projectIDHex, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var project models.Project
|
||||
if result := db.DB.Preload("EnvVars").First(&project, pid); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
deployment := models.Deployment{
|
||||
ProjectID: project.ID,
|
||||
Status: "building",
|
||||
Commit: "WEBHOOK",
|
||||
Logs: "Webhook triggered. Starting build...",
|
||||
}
|
||||
if err := db.DB.Create(&deployment).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create deployment record"})
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
var logBuffer bytes.Buffer
|
||||
streamer := &StreamWriter{DeploymentID: deployment.ID}
|
||||
multi := io.MultiWriter(&logBuffer, streamer)
|
||||
|
||||
envMap := make(map[string]string)
|
||||
for _, env := range project.EnvVars {
|
||||
envMap[env.Key] = env.Value
|
||||
}
|
||||
|
||||
imageName, err := h.builder.Build(project.RepoURL, project.Name, project.GitToken, project.BuildCommand, project.StartCommand, project.InstallCommand, project.Runtime, envMap, multi)
|
||||
deployment.Logs = logBuffer.String()
|
||||
if err != nil {
|
||||
deployment.Status = "failed"
|
||||
deployment.Logs += fmt.Sprintf("\n\nBuild Failed: %v", err)
|
||||
db.DB.Save(&deployment)
|
||||
return
|
||||
}
|
||||
|
||||
var envStrings []string
|
||||
for _, env := range project.EnvVars {
|
||||
envStrings = append(envStrings, fmt.Sprintf("%s=%s", env.Key, env.Value))
|
||||
}
|
||||
|
||||
containerID, err := h.deployer.RunContainer(c.Request.Context(), imageName, project.Name, project.Port, envStrings)
|
||||
if err != nil {
|
||||
deployment.Status = "failed"
|
||||
deployment.Logs += fmt.Sprintf("\n\nDeployment Failed: %v", err)
|
||||
db.DB.Save(&deployment)
|
||||
return
|
||||
}
|
||||
|
||||
deployment.Status = "live"
|
||||
deployment.URL = fmt.Sprintf("http://localhost:%d", project.Port)
|
||||
deployment.Logs += fmt.Sprintf("\n\nContainer ID: %s", containerID)
|
||||
db.DB.Save(&deployment)
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "redeployment_started", "deployment_id": deployment.ID})
|
||||
}
|
||||
54
backend/internal/auth/utils.go
Normal file
54
backend/internal/auth/utils.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var SecretKey = []byte("super-secret-key-change-me")
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func CheckPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func GenerateToken(userID uint, email string) (string, error) {
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(SecretKey)
|
||||
}
|
||||
|
||||
func ValidateToken(tokenString string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return SecretKey, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
138
backend/internal/builder/builder.go
Normal file
138
backend/internal/builder/builder.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Builder struct{}
|
||||
|
||||
func NewBuilder() *Builder {
|
||||
return &Builder{}
|
||||
}
|
||||
|
||||
func (b *Builder) Build(repoURL, appName, gitToken, buildCmd, startCmd, installCmd, runtime string, envVars map[string]string, logWriter io.Writer) (string, error) {
|
||||
workDir := filepath.Join("/tmp", "paas-builds", appName)
|
||||
if err := os.RemoveAll(workDir); err != nil {
|
||||
return "", fmt.Errorf("failed to clean work dir: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(workDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create work dir: %w", err)
|
||||
}
|
||||
|
||||
cloneURL := repoURL
|
||||
if gitToken != "" {
|
||||
u, err := url.Parse(repoURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid repo url: %w", err)
|
||||
}
|
||||
u.User = url.UserPassword("oauth2", gitToken)
|
||||
cloneURL = u.String()
|
||||
}
|
||||
|
||||
fmt.Fprintf(logWriter, ">>> Cloning repository %s...\n", repoURL)
|
||||
cloneCmd := exec.Command("git", "clone", "--depth", "1", cloneURL, ".")
|
||||
cloneCmd.Dir = workDir
|
||||
cloneCmd.Stdout = logWriter
|
||||
cloneCmd.Stderr = logWriter
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("git clone failed: %w", err)
|
||||
}
|
||||
|
||||
if runtime == "" {
|
||||
runtime = "nodejs"
|
||||
}
|
||||
|
||||
var nixPkgs string
|
||||
var defaultInstall, defaultBuild, defaultStart string
|
||||
|
||||
switch runtime {
|
||||
case "bun":
|
||||
nixPkgs = `["bun"]`
|
||||
defaultInstall = "bun install"
|
||||
defaultBuild = "bun run build"
|
||||
defaultStart = "bun run start"
|
||||
case "deno":
|
||||
nixPkgs = `["deno"]`
|
||||
defaultInstall = "deno cache"
|
||||
defaultBuild = "deno task build"
|
||||
defaultStart = "deno task start"
|
||||
case "pnpm":
|
||||
nixPkgs = `["nodejs_20", "pnpm"]`
|
||||
defaultInstall = "pnpm install"
|
||||
defaultBuild = "pnpm run build"
|
||||
defaultStart = "pnpm run start"
|
||||
default:
|
||||
nixPkgs = `["nodejs_20"]`
|
||||
defaultInstall = "npm ci --legacy-peer-deps || npm install --legacy-peer-deps"
|
||||
defaultBuild = "npm run build"
|
||||
defaultStart = "npm run start"
|
||||
}
|
||||
|
||||
installStr := defaultInstall
|
||||
if installCmd != "" {
|
||||
installStr = installCmd
|
||||
}
|
||||
|
||||
buildStr := defaultBuild
|
||||
if buildCmd != "" {
|
||||
buildStr = buildCmd
|
||||
}
|
||||
|
||||
startStr := defaultStart
|
||||
if startCmd != "" {
|
||||
startStr = startCmd
|
||||
}
|
||||
|
||||
nixpacksConfig := fmt.Sprintf(`
|
||||
[phases.setup]
|
||||
nixPkgs = %s
|
||||
|
||||
[phases.install]
|
||||
cmds = ["%s"]
|
||||
|
||||
[phases.build]
|
||||
cmds = ["%s"]
|
||||
|
||||
[start]
|
||||
cmd = "%s"
|
||||
`, nixPkgs, installStr, buildStr, startStr)
|
||||
|
||||
if _, err := os.Stat(filepath.Join(workDir, "package.json")); err == nil {
|
||||
configPath := filepath.Join(workDir, "nixpacks.toml")
|
||||
if err := os.WriteFile(configPath, []byte(nixpacksConfig), 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to write nixpacks.toml: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
imageName := strings.ToLower(appName)
|
||||
|
||||
fmt.Fprintf(logWriter, "\n>>> Starting Nixpacks build for %s...\n", imageName)
|
||||
|
||||
args := []string{"build", ".", "--name", imageName, "--no-cache"}
|
||||
for k, v := range envVars {
|
||||
args = append(args, "--env", fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
nixCmd := exec.Command("nixpacks", args...)
|
||||
nixCmd.Dir = workDir
|
||||
nixCmd.Stdout = logWriter
|
||||
nixCmd.Stderr = logWriter
|
||||
|
||||
nixCmd.Env = append(os.Environ(),
|
||||
"NIXPACKS_NO_CACHE=1",
|
||||
)
|
||||
|
||||
if err := nixCmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("nixpacks build failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(logWriter, "\n>>> Build successful!\n")
|
||||
|
||||
return imageName, nil
|
||||
}
|
||||
33
backend/internal/db/db.go
Normal file
33
backend/internal/db/db.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"clickploy/internal/models"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func Init(storagePath string) {
|
||||
var err error
|
||||
dbPath := filepath.Join(storagePath, "clickploy.db")
|
||||
|
||||
DB, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database:", err)
|
||||
}
|
||||
|
||||
log.Println("Migrating database...")
|
||||
err = DB.AutoMigrate(&models.User{}, &models.Project{}, &models.Deployment{}, &models.EnvVar{}, &models.Database{})
|
||||
if err != nil {
|
||||
log.Fatal("Failed to migrate database:", err)
|
||||
}
|
||||
log.Println("Database initialized successfully")
|
||||
}
|
||||
65
backend/internal/deployer/deployer.go
Normal file
65
backend/internal/deployer/deployer.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
type Deployer struct {
|
||||
cli *client.Client
|
||||
}
|
||||
|
||||
func NewDeployer() (*Deployer, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create docker client: %w", err)
|
||||
}
|
||||
return &Deployer{cli: cli}, nil
|
||||
}
|
||||
|
||||
func (d *Deployer) RunContainer(ctx context.Context, imageName, appName string, hostPort int, envVars []string) (string, error) {
|
||||
config := &container.Config{
|
||||
Image: imageName,
|
||||
ExposedPorts: nat.PortSet{
|
||||
"3000/tcp": struct{}{},
|
||||
},
|
||||
Env: envVars,
|
||||
}
|
||||
|
||||
hostConfig := &container.HostConfig{
|
||||
PortBindings: nat.PortMap{
|
||||
"3000/tcp": []nat.PortBinding{
|
||||
{
|
||||
HostIP: "0.0.0.0",
|
||||
HostPort: fmt.Sprintf("%d", hostPort),
|
||||
},
|
||||
},
|
||||
},
|
||||
RestartPolicy: container.RestartPolicy{
|
||||
Name: "unless-stopped",
|
||||
},
|
||||
}
|
||||
|
||||
_ = d.cli.ContainerRemove(ctx, appName, types.ContainerRemoveOptions{Force: true})
|
||||
|
||||
resp, err := d.cli.ContainerCreate(ctx, config, hostConfig, nil, nil, appName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create container: %w", err)
|
||||
}
|
||||
|
||||
if err := d.cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
|
||||
return "", fmt.Errorf("failed to start container: %w", err)
|
||||
}
|
||||
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
func (d *Deployer) StreamLogs(ctx context.Context, containerID string) (io.ReadCloser, error) {
|
||||
return d.cli.ContainerLogs(ctx, containerID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true, Follow: true})
|
||||
}
|
||||
56
backend/internal/models/models.go
Normal file
56
backend/internal/models/models.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Email string `gorm:"uniqueIndex;not null" json:"email"`
|
||||
Password string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Projects []Project `gorm:"foreignKey:OwnerID" json:"projects"`
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"uniqueIndex" json:"name"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
OwnerID uint `json:"owner_id"`
|
||||
Port int `json:"port"`
|
||||
WebhookSecret string `json:"webhook_secret"`
|
||||
GitToken string `json:"-"`
|
||||
BuildCommand string `json:"build_command"`
|
||||
StartCommand string `json:"start_command"`
|
||||
InstallCommand string `json:"install_command"`
|
||||
Runtime string `json:"runtime"`
|
||||
Deployments []Deployment `gorm:"foreignKey:ProjectID" json:"deployments"`
|
||||
EnvVars []EnvVar `gorm:"foreignKey:ProjectID" json:"env_vars"`
|
||||
}
|
||||
|
||||
type EnvVar struct {
|
||||
gorm.Model
|
||||
ProjectID uint `json:"project_id"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type Deployment struct {
|
||||
gorm.Model
|
||||
ProjectID uint `json:"project_id"`
|
||||
Project Project `json:"project" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||
Status string `json:"status"`
|
||||
Commit string `json:"commit"`
|
||||
Logs string `json:"logs"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
gorm.Model
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
OwnerID uint `json:"owner_id"`
|
||||
SizeMB float64 `json:"size_mb"`
|
||||
}
|
||||
80
backend/internal/ports/ports.go
Normal file
80
backend/internal/ports/ports.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package ports
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
startPort int
|
||||
endPort int
|
||||
allocations map[string]int
|
||||
}
|
||||
|
||||
func NewManager(start, end int) *Manager {
|
||||
return &Manager{
|
||||
startPort: start,
|
||||
endPort: end,
|
||||
allocations: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetPort(appName string, specificPort int) (int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if port, exists := m.allocations[appName]; exists {
|
||||
if specificPort > 0 && specificPort != port {
|
||||
return 0, fmt.Errorf("app %s is already running on port %d", appName, port)
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
|
||||
if specificPort > 0 {
|
||||
if err := m.checkPortAvailable(specificPort); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
m.allocations[appName] = specificPort
|
||||
return specificPort, nil
|
||||
}
|
||||
|
||||
for port := m.startPort; port <= m.endPort; port++ {
|
||||
if err := m.checkPortAvailable(port); err == nil {
|
||||
if !m.isPortAllocatedInternal(port) {
|
||||
m.allocations[appName] = port
|
||||
return port, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("no available ports in range %d-%d", m.startPort, m.endPort)
|
||||
}
|
||||
|
||||
func (m *Manager) isPortAllocatedInternal(port int) bool {
|
||||
for _, p := range m.allocations {
|
||||
if p == port {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) checkPortAvailable(port int) error {
|
||||
if port < m.startPort || port > m.endPort {
|
||||
return fmt.Errorf("port %d is out of allowed range %d-%d", port, m.startPort, m.endPort)
|
||||
}
|
||||
|
||||
if m.isPortAllocatedInternal(port) {
|
||||
return fmt.Errorf("port %d is internally allocated", port)
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("localhost:%d", port)
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_ = conn.Close()
|
||||
return fmt.Errorf("port %d is already in use", port)
|
||||
}
|
||||
Reference in New Issue
Block a user