API and Database Deployment Update
This commit is contained in:
@@ -1,14 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *Handler) RegisterAdminRoutes(r *gin.Engine) {
|
||||
admin := r.Group("/api/admin", AuthMiddleware(), AdminMiddleware())
|
||||
{
|
||||
@@ -17,7 +13,6 @@ func (h *Handler) RegisterAdminRoutes(r *gin.Engine) {
|
||||
admin.GET("/stats", h.adminGetStats)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) adminListUsers(c *gin.Context) {
|
||||
var users []models.User
|
||||
if err := db.DB.Preload("Projects").Find(&users).Error; err != nil {
|
||||
@@ -26,7 +21,6 @@ func (h *Handler) adminListUsers(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, users)
|
||||
}
|
||||
|
||||
func (h *Handler) adminDeleteUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if err := db.DB.Where("id = ?", id).Delete(&models.User{}).Error; err != nil {
|
||||
@@ -35,16 +29,13 @@ func (h *Handler) adminDeleteUser(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
|
||||
}
|
||||
|
||||
func (h *Handler) adminGetStats(c *gin.Context) {
|
||||
var userCount int64
|
||||
var projectCount int64
|
||||
var deploymentCount int64
|
||||
|
||||
db.DB.Model(&models.User{}).Count(&userCount)
|
||||
db.DB.Model(&models.Project{}).Count(&projectCount)
|
||||
db.DB.Model(&models.Deployment{}).Count(&deploymentCount)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": userCount,
|
||||
"projects": projectCount,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"clickploy/internal/builder"
|
||||
"clickploy/internal/deployer"
|
||||
"clickploy/internal/ports"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"clickploy/internal/builder"
|
||||
"clickploy/internal/deployer"
|
||||
"clickploy/internal/ports"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
@@ -32,7 +33,6 @@ type DeployRequest struct {
|
||||
Port int `json:"port"`
|
||||
GitToken string `json:"git_token"`
|
||||
}
|
||||
|
||||
type DeployResponse struct {
|
||||
Status string `json:"status"`
|
||||
AppName string `json:"app_name"`
|
||||
@@ -45,32 +45,27 @@ 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)
|
||||
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,
|
||||
@@ -79,14 +74,42 @@ func (h *Handler) handleDeploy(c *gin.Context) {
|
||||
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) {
|
||||
localIP := GetLocalIP()
|
||||
publicIP := GetPublicIP()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"version": "v0.1.0",
|
||||
"status": "All systems normal",
|
||||
"version": "v0.1.0",
|
||||
"status": "All systems normal",
|
||||
"local_ip": localIP,
|
||||
"public_ip": publicIP,
|
||||
})
|
||||
}
|
||||
func GetLocalIP() string {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "Unknown"
|
||||
}
|
||||
for _, address := range addrs {
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
func GetPublicIP() string {
|
||||
resp, err := http.Get("https://api.ipify.org?format=text")
|
||||
if err != nil {
|
||||
return "Unknown"
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
ip, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "Unknown"
|
||||
}
|
||||
return string(ip)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"clickploy/internal/auth"
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
@@ -16,7 +17,6 @@ type AuthRequest struct {
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
User models.User `json:"user"`
|
||||
@@ -29,126 +29,131 @@ func (h *Handler) RegisterAuthRoutes(r *gin.Engine) {
|
||||
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
|
||||
}
|
||||
|
||||
var count int64
|
||||
db.DB.Model(&models.User{}).Count(&count)
|
||||
|
||||
apiKeyBytes := make([]byte, 32)
|
||||
rand.Read(apiKeyBytes)
|
||||
apiKey := "cp_" + hex.EncodeToString(apiKeyBytes)
|
||||
userID, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 10)
|
||||
user := models.User{
|
||||
ID: userID,
|
||||
Email: req.Email,
|
||||
Password: hashed,
|
||||
Name: req.Name,
|
||||
Avatar: "https://github.com/shadcn.png",
|
||||
Avatar: "https://ui-avatars.com/api/?name=" + req.Name,
|
||||
IsAdmin: count == 0,
|
||||
APIKey: apiKey,
|
||||
}
|
||||
|
||||
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.GET("/", h.getMe)
|
||||
userGroup.PUT("/profile", h.updateProfile)
|
||||
userGroup.PUT("/password", h.updatePassword)
|
||||
userGroup.POST("/key", h.regenerateAPIKey)
|
||||
}
|
||||
}
|
||||
|
||||
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.Where("id = ?", userID).First(&user); 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.Where("id = ?", userID).First(&user); 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"})
|
||||
}
|
||||
func (h *Handler) getMe(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
var user models.User
|
||||
if result := db.DB.Where("id = ?", userID).First(&user); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
func (h *Handler) regenerateAPIKey(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
apiKeyBytes := make([]byte, 32)
|
||||
rand.Read(apiKeyBytes)
|
||||
apiKey := "cp_" + hex.EncodeToString(apiKeyBytes)
|
||||
if err := db.DB.Model(&models.User{}).Where("id = ?", userID).Update("api_key", apiKey).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update API key"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"api_key": apiKey})
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"clickploy/internal/auth"
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
@@ -19,21 +15,25 @@ func AuthMiddleware() gin.HandlerFunc {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
claims, err := auth.ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
var user models.User
|
||||
if result := db.DB.Where("api_key = ?", tokenString).First(&user); result.Error == nil {
|
||||
c.Set("userID", user.ID)
|
||||
c.Set("email", user.Email)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("userID", claims.UserID)
|
||||
c.Set("email", claims.Email)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func AdminMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID, exists := c.Get("userID")
|
||||
@@ -42,20 +42,17 @@ func AdminMiddleware() gin.HandlerFunc {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := db.DB.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsAdmin {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Admin privileges required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
@@ -22,16 +23,51 @@ func (h *Handler) RegisterProjectRoutes(r *gin.Engine) {
|
||||
protected.POST("/projects", h.createProject)
|
||||
protected.GET("/projects", h.listProjects)
|
||||
protected.GET("/projects/:id", h.getProject)
|
||||
protected.PUT("/projects/:id", h.updateProject)
|
||||
protected.PUT("/projects/:id/env", h.updateProjectEnv)
|
||||
protected.POST("/projects/:id/redeploy", h.redeployProject)
|
||||
protected.GET("/activity", h.getActivity)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) updateProject(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
projectID := c.Param("id")
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
BuildCommand string `json:"build_command"`
|
||||
StartCommand string `json:"start_command"`
|
||||
InstallCommand string `json:"install_command"`
|
||||
Runtime string `json:"runtime"`
|
||||
GitToken string `json:"git_token"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var project models.Project
|
||||
if result := db.DB.Where("id = ? AND owner_id = ?", projectID, userID).First(&project); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
project.Name = req.Name
|
||||
project.BuildCommand = req.BuildCommand
|
||||
project.StartCommand = req.StartCommand
|
||||
project.InstallCommand = req.InstallCommand
|
||||
project.Runtime = req.Runtime
|
||||
project.RepoURL = req.RepoURL
|
||||
if req.GitToken != "" {
|
||||
project.GitToken = req.GitToken
|
||||
}
|
||||
if err := db.DB.Save(&project).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update project"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, project)
|
||||
}
|
||||
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"`
|
||||
}
|
||||
@@ -39,20 +75,17 @@ func (h *Handler) updateProjectEnv(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var project models.Project
|
||||
if result := db.DB.Where("id = ? AND owner_id = ?", projectID, 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,
|
||||
@@ -65,99 +98,93 @@ func (h *Handler) updateProjectEnv(c *gin.Context) {
|
||||
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")
|
||||
|
||||
var req struct {
|
||||
Commit string `json:"commit"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
var project models.Project
|
||||
if result := db.DB.Preload("EnvVars").Where("id = ? AND owner_id = ?", projectID, userID).First(&project); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
depID, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 10)
|
||||
initialCommit := "MANUAL"
|
||||
if req.Commit != "" {
|
||||
initialCommit = req.Commit
|
||||
}
|
||||
deployment := models.Deployment{
|
||||
ID: depID,
|
||||
ProjectID: project.ID,
|
||||
Status: "building",
|
||||
Commit: "MANUAL",
|
||||
Logs: "Starting manual redeploy...",
|
||||
Commit: initialCommit,
|
||||
Logs: fmt.Sprintf("Starting redeploy for commit %s...", initialCommit),
|
||||
}
|
||||
db.DB.Create(&deployment)
|
||||
|
||||
envMap := make(map[string]string)
|
||||
for _, env := range project.EnvVars {
|
||||
envMap[env.Key] = env.Value
|
||||
}
|
||||
resolvedEnv, err := h.resolveEnvVars(c.Request.Context(), userID.(string), envMap)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
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, commitHash, err := h.builder.Build(project.RepoURL, project.Name, project.GitToken, project.BuildCommand, project.StartCommand, project.InstallCommand, project.Runtime, envMap, multi)
|
||||
imageName, commitHash, err := h.builder.Build(project.RepoURL, req.Commit, project.Name, project.GitToken, project.BuildCommand, project.StartCommand, project.InstallCommand, project.Runtime, resolvedEnv, 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
|
||||
}
|
||||
|
||||
// Update commit hash if we got one
|
||||
if commitHash != "" {
|
||||
deployment.Commit = commitHash
|
||||
}
|
||||
|
||||
var envStrings []string
|
||||
for _, env := range project.EnvVars {
|
||||
envStrings = append(envStrings, fmt.Sprintf("%s=%s", env.Key, env.Value))
|
||||
for k, v := range resolvedEnv {
|
||||
envStrings = append(envStrings, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
containerID, err := h.deployer.RunContainer(c.Request.Context(), imageName, project.Name, project.Port, envStrings)
|
||||
containerID, err := h.deployer.RunContainer(context.Background(), 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"`
|
||||
@@ -166,31 +193,28 @@ func (h *Handler) createProject(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
resolvedEnv, err := h.resolveEnvVars(c.Request.Context(), userID.(string), req.EnvVars)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
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})
|
||||
}
|
||||
|
||||
secretBytes := make([]byte, 16)
|
||||
rand.Read(secretBytes)
|
||||
webhookSecret := hex.EncodeToString(secretBytes)
|
||||
|
||||
id, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 10)
|
||||
|
||||
project := models.Project{
|
||||
ID: id,
|
||||
Name: req.Name,
|
||||
@@ -205,12 +229,10 @@ func (h *Handler) createProject(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
depID, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 10)
|
||||
deployment := models.Deployment{
|
||||
ID: depID,
|
||||
@@ -220,45 +242,71 @@ func (h *Handler) createProject(c *gin.Context) {
|
||||
Logs: "Starting build...",
|
||||
}
|
||||
db.DB.Create(&deployment)
|
||||
|
||||
go func() {
|
||||
var logBuffer bytes.Buffer
|
||||
streamer := &StreamWriter{DeploymentID: deployment.ID}
|
||||
multi := io.MultiWriter(&logBuffer, streamer)
|
||||
|
||||
imageName, commitHash, err := h.builder.Build(req.Repo, req.Name, req.GitToken, req.BuildCommand, req.StartCommand, req.InstallCommand, req.Runtime, req.EnvVars, multi)
|
||||
imageName, commitHash, err := h.builder.Build(req.Repo, "", req.Name, req.GitToken, req.BuildCommand, req.StartCommand, req.InstallCommand, req.Runtime, resolvedEnv, 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
|
||||
}
|
||||
|
||||
// Update commit hash
|
||||
if commitHash != "" {
|
||||
deployment.Commit = commitHash
|
||||
}
|
||||
|
||||
containerID, err := h.deployer.RunContainer(c.Request.Context(), imageName, req.Name, port, envStrings)
|
||||
var envStrings []string
|
||||
for k, v := range resolvedEnv {
|
||||
envStrings = append(envStrings, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
containerID, err := h.deployer.RunContainer(context.Background(), 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) resolveEnvVars(ctx context.Context, userID string, envVars map[string]string) (map[string]string, error) {
|
||||
resolved := make(map[string]string)
|
||||
for k, v := range envVars {
|
||||
if strings.HasPrefix(v, "$DB:") {
|
||||
dbName := strings.TrimPrefix(v, "$DB:")
|
||||
var database models.Database
|
||||
if err := db.DB.Where("name = ? AND owner_id = ?", dbName, userID).First(&database).Error; err != nil {
|
||||
return nil, fmt.Errorf("database '%s' not found", dbName)
|
||||
}
|
||||
if database.Type != "mongodb" {
|
||||
return nil, fmt.Errorf("database '%s' is not a mongodb instance (auto-resolution only supported for mongo)", dbName)
|
||||
}
|
||||
env, err := h.deployer.GetContainerEnv(ctx, database.ContainerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get env for database '%s': %v", dbName, err)
|
||||
}
|
||||
var user, pass string
|
||||
for _, e := range env {
|
||||
if len(e) > 25 && e[:25] == "MONGO_INITDB_ROOT_USERNAME=" {
|
||||
user = e[25:]
|
||||
} else if len(e) > 25 && e[:25] == "MONGO_INITDB_ROOT_PASSWORD=" {
|
||||
pass = e[25:]
|
||||
}
|
||||
}
|
||||
resolved[k] = fmt.Sprintf("mongodb://%s:%s@172.17.0.1:%d/?authSource=admin", user, pass, database.Port)
|
||||
} else {
|
||||
resolved[k] = v
|
||||
}
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
func (h *Handler) listProjects(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
var projects []models.Project
|
||||
@@ -268,11 +316,9 @@ func (h *Handler) listProjects(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, projects)
|
||||
}
|
||||
|
||||
func (h *Handler) getProject(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
projectID := c.Param("id")
|
||||
|
||||
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")
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
)
|
||||
|
||||
type CreateDatabaseRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required,oneof=sqlite"`
|
||||
Type string `json:"type" binding:"required,oneof=sqlite mongodb"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
type StorageStatsResponse struct {
|
||||
TotalMB float64 `json:"total_mb"`
|
||||
UsedMB float64 `json:"used_mb"`
|
||||
@@ -30,28 +30,67 @@ func (h *Handler) RegisterStorageRoutes(r *gin.Engine) {
|
||||
api.GET("/stats", h.handleGetStorageStats)
|
||||
api.GET("/databases", h.handleListDatabases)
|
||||
api.POST("/databases", h.handleCreateDatabase)
|
||||
api.PUT("/databases/:id", h.handleUpdateDatabase)
|
||||
api.DELETE("/databases/:id", h.handleDeleteDatabase)
|
||||
api.GET("/databases/:id/credentials", h.handleGetDatabaseCredentials)
|
||||
api.PUT("/databases/:id/credentials", h.handleUpdateDatabaseCredentials)
|
||||
api.POST("/databases/:id/stop", h.handleStopDatabase)
|
||||
api.POST("/databases/:id/restart", h.handleRestartDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleUpdateDatabase(c *gin.Context) {
|
||||
userId := c.GetString("userID")
|
||||
dbId := c.Param("id")
|
||||
var req struct {
|
||||
Port int `json:"port"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var database models.Database
|
||||
if err := db.DB.Where("id = ? AND owner_id = ?", dbId, userId).First(&database).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found"})
|
||||
return
|
||||
}
|
||||
if database.Type == "mongodb" && req.Port != 0 && req.Port != database.Port {
|
||||
envVars, err := h.deployer.GetContainerEnv(c.Request.Context(), database.ContainerID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve current container configuration"})
|
||||
return
|
||||
}
|
||||
wd, _ := os.Getwd()
|
||||
volumePath := filepath.Join(wd, "data", "volumes", fmt.Sprintf("%s_%s", userId, database.Name))
|
||||
containerName := fmt.Sprintf("mongo_%s_%s", userId, database.Name)
|
||||
newId, err := h.deployer.StartDatabaseContainer(c.Request.Context(), "mongo:latest", containerName, req.Port, volumePath, envVars)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to restart container: %v", err)})
|
||||
return
|
||||
}
|
||||
database.Port = req.Port
|
||||
database.ContainerID = newId
|
||||
if err := db.DB.Save(&database).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update database record"})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, database)
|
||||
}
|
||||
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.GetString("userID")
|
||||
var dbs []models.Database
|
||||
@@ -61,7 +100,6 @@ func (h *Handler) handleListDatabases(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, dbs)
|
||||
}
|
||||
|
||||
func (h *Handler) handleCreateDatabase(c *gin.Context) {
|
||||
userId := c.GetString("userID")
|
||||
var req CreateDatabaseRequest
|
||||
@@ -69,7 +107,6 @@ func (h *Handler) handleCreateDatabase(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
newDB := models.Database{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
@@ -77,22 +114,257 @@ func (h *Handler) handleCreateDatabase(c *gin.Context) {
|
||||
OwnerID: userId,
|
||||
SizeMB: 0,
|
||||
}
|
||||
|
||||
dataDir := "./data/user_dbs"
|
||||
os.MkdirAll(dataDir, 0755)
|
||||
|
||||
dbPath := filepath.Join(dataDir, fmt.Sprintf("%s_%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"})
|
||||
if req.Type == "sqlite" {
|
||||
dataDir := "./data/user_dbs"
|
||||
os.MkdirAll(dataDir, 0755)
|
||||
dbPath := filepath.Join(dataDir, fmt.Sprintf("%s_%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()
|
||||
} else if req.Type == "mongodb" {
|
||||
port := 27017
|
||||
if req.Port != 0 {
|
||||
p, err := h.ports.GetPort(req.Name, req.Port)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Port %d is not available", req.Port)})
|
||||
return
|
||||
}
|
||||
port = p
|
||||
} else {
|
||||
p, err := h.ports.GetPort(req.Name, 27017)
|
||||
if err == nil {
|
||||
port = p
|
||||
} else {
|
||||
p, err := h.ports.GetPort(req.Name, 0)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to allocate port"})
|
||||
return
|
||||
}
|
||||
port = p
|
||||
}
|
||||
}
|
||||
newDB.Port = port
|
||||
username := "root"
|
||||
password, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 16)
|
||||
wd, _ := os.Getwd()
|
||||
volumePath := filepath.Join(wd, "data", "volumes", fmt.Sprintf("%s_%s", userId, req.Name))
|
||||
os.MkdirAll(volumePath, 0755)
|
||||
containerName := fmt.Sprintf("mongo_%s_%s", userId, req.Name)
|
||||
envVars := []string{
|
||||
fmt.Sprintf("MONGO_INITDB_ROOT_USERNAME=%s", username),
|
||||
fmt.Sprintf("MONGO_INITDB_ROOT_PASSWORD=%s", password),
|
||||
}
|
||||
id, err := h.deployer.StartDatabaseContainer(c.Request.Context(), "mongo:latest", containerName, port, volumePath, envVars)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to start database container: %v", err)})
|
||||
return
|
||||
}
|
||||
newDB.ContainerID = id
|
||||
newDB.Status = "running"
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"database": newDB,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"uri": fmt.Sprintf("mongodb://%s:%s@localhost:%d/?authSource=admin", username, password, port),
|
||||
})
|
||||
if err := db.DB.Create(&newDB).Error; err != nil {
|
||||
fmt.Printf("Failed to save DB record: %v\n", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
func (h *Handler) handleDeleteDatabase(c *gin.Context) {
|
||||
userId := c.GetString("userID")
|
||||
dbId := c.Param("id")
|
||||
var database models.Database
|
||||
if err := db.DB.Where("id = ? AND owner_id = ?", dbId, userId).First(&database).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found"})
|
||||
return
|
||||
}
|
||||
if database.Type == "sqlite" {
|
||||
dataDir := "./data/user_dbs"
|
||||
dbPath := filepath.Join(dataDir, fmt.Sprintf("%s_%s.db", userId, database.Name))
|
||||
if err := os.Remove(dbPath); err != nil && !os.IsNotExist(err) {
|
||||
fmt.Printf("Failed to delete database file: %v\n", err)
|
||||
}
|
||||
} else if database.Type == "mongodb" {
|
||||
if database.ContainerID != "" {
|
||||
if err := h.deployer.RemoveContainer(c.Request.Context(), database.ContainerID); err != nil {
|
||||
fmt.Printf("Failed to remove container: %v\n", err)
|
||||
}
|
||||
}
|
||||
wd, _ := os.Getwd()
|
||||
volumePath := filepath.Join(wd, "data", "volumes", fmt.Sprintf("%s_%s", userId, database.Name))
|
||||
os.RemoveAll(volumePath)
|
||||
}
|
||||
if err := db.DB.Delete(&database).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete database record"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "deleted"})
|
||||
}
|
||||
func (h *Handler) handleGetDatabaseCredentials(c *gin.Context) {
|
||||
userId := c.GetString("userID")
|
||||
dbId := c.Param("id")
|
||||
var database models.Database
|
||||
if err := db.DB.Where("id = ? AND owner_id = ?", dbId, userId).First(&database).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found"})
|
||||
return
|
||||
}
|
||||
if database.Type != "mongodb" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Credential management only supported for MongoDB"})
|
||||
return
|
||||
}
|
||||
envVars, err := h.deployer.GetContainerEnv(c.Request.Context(), database.ContainerID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve container configuration"})
|
||||
return
|
||||
}
|
||||
var username, password string
|
||||
for _, env := range envVars {
|
||||
if len(env) > 25 && env[:25] == "MONGO_INITDB_ROOT_USERNAME=" {
|
||||
username = env[25:]
|
||||
} else if len(env) > 25 && env[:25] == "MONGO_INITDB_ROOT_PASSWORD=" {
|
||||
password = env[25:]
|
||||
}
|
||||
}
|
||||
uri := fmt.Sprintf("mongodb://%s:%s@localhost:%d/?authSource=admin", username, password, database.Port)
|
||||
publicUri := fmt.Sprintf("mongodb://%s:%s@<HOST>:%d/?authSource=admin", username, password, database.Port)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"username": username,
|
||||
"password": password,
|
||||
"uri": uri,
|
||||
"public_uri": publicUri,
|
||||
})
|
||||
}
|
||||
func (h *Handler) handleUpdateDatabaseCredentials(c *gin.Context) {
|
||||
userId := c.GetString("userID")
|
||||
dbId := c.Param("id")
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var database models.Database
|
||||
if err := db.DB.Where("id = ? AND owner_id = ?", dbId, userId).First(&database).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found"})
|
||||
return
|
||||
}
|
||||
if database.Type != "mongodb" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Credential management only supported for MongoDB"})
|
||||
return
|
||||
}
|
||||
envVars, err := h.deployer.GetContainerEnv(c.Request.Context(), database.ContainerID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve container configuration"})
|
||||
return
|
||||
}
|
||||
var currentUser, currentPass string
|
||||
var otherEnv []string
|
||||
for _, env := range envVars {
|
||||
if len(env) > 25 && env[:25] == "MONGO_INITDB_ROOT_USERNAME=" {
|
||||
currentUser = env[25:]
|
||||
} else if len(env) > 25 && env[:25] == "MONGO_INITDB_ROOT_PASSWORD=" {
|
||||
currentPass = env[25:]
|
||||
} else {
|
||||
otherEnv = append(otherEnv, env)
|
||||
}
|
||||
}
|
||||
if currentUser == "" || currentPass == "" {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not determine current credentials from container"})
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
needsRestart := false
|
||||
if req.Username != currentUser {
|
||||
createCmd := fmt.Sprintf("db.getSiblingDB('admin').createUser({user: '%s', pwd: '%s', roles: ['root']})", req.Username, req.Password)
|
||||
if err := h.deployer.ExecContainer(ctx, database.ContainerID, []string{"mongosh", "admin", "-u", currentUser, "-p", currentPass, "--eval", createCmd}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create new user: %v", err)})
|
||||
return
|
||||
}
|
||||
dropCmd := fmt.Sprintf("db.getSiblingDB('admin').dropUser('%s')", currentUser)
|
||||
if err := h.deployer.ExecContainer(ctx, database.ContainerID, []string{"mongosh", "admin", "-u", req.Username, "-p", req.Password, "--eval", dropCmd}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to drop old user: %v", err)})
|
||||
return
|
||||
}
|
||||
needsRestart = true
|
||||
} else if req.Password != currentPass {
|
||||
changeCmd := fmt.Sprintf("db.getSiblingDB('admin').changeUserPassword('%s', '%s')", currentUser, req.Password)
|
||||
if err := h.deployer.ExecContainer(ctx, database.ContainerID, []string{"mongosh", "admin", "-u", currentUser, "-p", currentPass, "--eval", changeCmd}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to change password: %v", err)})
|
||||
return
|
||||
}
|
||||
needsRestart = true
|
||||
}
|
||||
if needsRestart {
|
||||
newEnv := append(otherEnv,
|
||||
fmt.Sprintf("MONGO_INITDB_ROOT_USERNAME=%s", req.Username),
|
||||
fmt.Sprintf("MONGO_INITDB_ROOT_PASSWORD=%s", req.Password),
|
||||
)
|
||||
wd, _ := os.Getwd()
|
||||
volumePath := filepath.Join(wd, "data", "volumes", fmt.Sprintf("%s_%s", userId, database.Name))
|
||||
containerName := fmt.Sprintf("mongo_%s_%s", userId, database.Name)
|
||||
newId, err := h.deployer.StartDatabaseContainer(ctx, "mongo:latest", containerName, database.Port, volumePath, newEnv)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to restart container with new creds: %v", err)})
|
||||
return
|
||||
}
|
||||
database.ContainerID = newId
|
||||
if err := db.DB.Save(&database).Error; err != nil {
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "updated"})
|
||||
}
|
||||
func (h *Handler) handleStopDatabase(c *gin.Context) {
|
||||
userId := c.GetString("userID")
|
||||
dbId := c.Param("id")
|
||||
var database models.Database
|
||||
if err := db.DB.Where("id = ? AND owner_id = ?", dbId, userId).First(&database).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found"})
|
||||
return
|
||||
}
|
||||
if database.Type == "mongodb" && database.ContainerID != "" {
|
||||
if err := h.deployer.StopContainer(c.Request.Context(), database.ContainerID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to stop container: %v", err)})
|
||||
return
|
||||
}
|
||||
database.Status = "stopped"
|
||||
if err := db.DB.Save(&database).Error; err != nil {
|
||||
fmt.Printf("Failed to update db status: %v\n", err)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "stopped"})
|
||||
}
|
||||
func (h *Handler) handleRestartDatabase(c *gin.Context) {
|
||||
userId := c.GetString("userID")
|
||||
dbId := c.Param("id")
|
||||
var database models.Database
|
||||
if err := db.DB.Where("id = ? AND owner_id = ?", dbId, userId).First(&database).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found"})
|
||||
return
|
||||
}
|
||||
if database.Type == "mongodb" && database.ContainerID != "" {
|
||||
if err := h.deployer.RestartContainer(c.Request.Context(), database.ContainerID); err != nil {
|
||||
if err := h.deployer.StartContainer(c.Request.Context(), database.ContainerID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to restart container: %v", err)})
|
||||
return
|
||||
}
|
||||
}
|
||||
database.Status = "running"
|
||||
if err := db.DB.Save(&database).Error; err != nil {
|
||||
fmt.Printf("Failed to update db status: %v\n", err)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "restarted"})
|
||||
}
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"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[string][]chan []byte
|
||||
}
|
||||
|
||||
var Hub = &LogHub{
|
||||
streams: make(map[string][]chan []byte),
|
||||
}
|
||||
|
||||
func (h *LogHub) Broadcast(deploymentID string, p []byte) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
@@ -34,7 +28,6 @@ func (h *LogHub) Broadcast(deploymentID string, p []byte) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LogHub) Subscribe(deploymentID string) chan []byte {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
@@ -42,7 +35,6 @@ func (h *LogHub) Subscribe(deploymentID string) chan []byte {
|
||||
h.streams[deploymentID] = append(h.streams[deploymentID], ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (h *LogHub) Unsubscribe(deploymentID string, ch chan []byte) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
@@ -59,34 +51,28 @@ func (h *LogHub) Unsubscribe(deploymentID string, ch chan []byte) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type StreamWriter struct {
|
||||
DeploymentID string
|
||||
}
|
||||
|
||||
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) {
|
||||
deploymentID := c.Param("id")
|
||||
if deploymentID == "" {
|
||||
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(deploymentID)
|
||||
defer Hub.Unsubscribe(deploymentID, logChan)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if _, _, err := conn.NextReader(); err != nil {
|
||||
@@ -95,7 +81,6 @@ func (h *Handler) streamDeploymentLogs(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for logChunk := range logChan {
|
||||
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
if err := conn.WriteMessage(websocket.TextMessage, logChunk); err != nil {
|
||||
@@ -103,7 +88,6 @@ func (h *Handler) streamDeploymentLogs(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) RegisterStreamRoutes(r *gin.Engine) {
|
||||
r.GET("/api/deployments/:id/logs/stream", h.streamDeploymentLogs)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"clickploy/internal/db"
|
||||
"clickploy/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
)
|
||||
@@ -16,27 +15,22 @@ import (
|
||||
func (h *Handler) RegisterWebhookRoutes(r *gin.Engine) {
|
||||
r.POST("/projects/:projectID/webhook/:webhookID", h.handleWebhook)
|
||||
}
|
||||
|
||||
func (h *Handler) handleWebhook(c *gin.Context) {
|
||||
projectID := c.Param("projectID")
|
||||
webhookSecret := c.Param("webhookID")
|
||||
|
||||
if projectID == "" || webhookSecret == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid webhook url"})
|
||||
return
|
||||
}
|
||||
|
||||
var project models.Project
|
||||
if result := db.DB.Preload("EnvVars").Where("id = ?", projectID).First(&project); result.Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if project.WebhookSecret != webhookSecret {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Webhook Secret"})
|
||||
return
|
||||
}
|
||||
|
||||
depID, _ := gonanoid.Generate("abcdefghijklmnopqrstuvwxyz0123456789", 10)
|
||||
deployment := models.Deployment{
|
||||
ID: depID,
|
||||
@@ -49,18 +43,15 @@ func (h *Handler) handleWebhook(c *gin.Context) {
|
||||
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, commitHash, err := h.builder.Build(project.RepoURL, project.Name, project.GitToken, project.BuildCommand, project.StartCommand, project.InstallCommand, project.Runtime, envMap, multi)
|
||||
imageName, commitHash, 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"
|
||||
@@ -68,16 +59,13 @@ func (h *Handler) handleWebhook(c *gin.Context) {
|
||||
db.DB.Save(&deployment)
|
||||
return
|
||||
}
|
||||
|
||||
if commitHash != "" {
|
||||
deployment.Commit = commitHash
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -85,12 +73,10 @@ func (h *Handler) handleWebhook(c *gin.Context) {
|
||||
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})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user