Skip to content
29 changes: 29 additions & 0 deletions internal/api/handlers/cloning_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,34 @@ func (ch *CloningHandler) CloneTemplateHandler(c *gin.Context) {

log.Printf("User %s requested cloning of template %s", username, req.Template)

publishedTemplates, err := ch.Service.DatabaseService.GetPublishedTemplates()
if err != nil {
log.Printf("Error fetching published templates: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to fetch published templates",
"details": err.Error(),
})
return
}

// Check if the requested template is in the list of published templates
templateFound := false
for _, tmpl := range publishedTemplates {
if tmpl.Name == req.Template {
templateFound = true
break
}
}

if !templateFound {
log.Printf("Template %s not found or not published", req.Template)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Template not found or not published",
"details": fmt.Sprintf("Template %s is not available for cloning", req.Template),
})
return
}

// Create the cloning request using the new format
cloneReq := cloning.CloneRequest{
Template: req.Template,
Expand Down Expand Up @@ -121,6 +149,7 @@ func (ch *CloningHandler) AdminCloneTemplateHandler(c *gin.Context) {
Template: req.Template,
Targets: targets,
CheckExistingDeployments: false,
StartingVMID: req.StartingVMID,
}

// Perform clone operation
Expand Down
60 changes: 59 additions & 1 deletion internal/api/handlers/dashboard_handler.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package handlers

import (
"fmt"
"log"
"net/http"
"strings"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)

Expand All @@ -16,7 +20,7 @@ func NewDashboardHandler(authHandler *AuthHandler, proxmoxHandler *ProxmoxHandle
}

// ADMIN: GetDashboardStatsHandler retrieves all dashboard statistics in a single request
func (dh *DashboardHandler) GetDashboardStatsHandler(c *gin.Context) {
func (dh *DashboardHandler) GetAdminDashboardStatsHandler(c *gin.Context) {
stats := DashboardStats{}

// Get user count
Expand Down Expand Up @@ -71,3 +75,57 @@ func (dh *DashboardHandler) GetDashboardStatsHandler(c *gin.Context) {
"stats": stats,
})
}

// PRIVATE: GetUserDashboardStatsHandler retrieves all user dashboard statistics in a single request
func (dh *DashboardHandler) GetUserDashboardStatsHandler(c *gin.Context) {
session := sessions.Default(c)
username := session.Get("id").(string)

// Get user's deployed pods
pods, err := dh.cloningHandler.Service.GetPods(username)
if err != nil {
log.Printf("Error retrieving pods for user %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve pods", "details": err.Error()})
return
}

// Loop through the user's deployed pods and add template information
for i := range pods {
templateName := strings.Replace(pods[i].Name[5:], fmt.Sprintf("_%s", username), "", 1)
templateInfo, err := dh.cloningHandler.Service.DatabaseService.GetTemplateInfo(templateName)
if err != nil {
log.Printf("Error retrieving template info for pod %s: %v", pods[i].Name, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve template info for pod", "details": err.Error()})
return
}
pods[i].Template = templateInfo
}

// Get user's information
userInfo, err := dh.authHandler.ldapService.GetUser(username)
if err != nil {
log.Printf("Error retrieving user info for %s: %v", username, err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to retrieve user info",
"details": err.Error(),
})
return
}

// Get public pod templates
templates, err := dh.cloningHandler.Service.DatabaseService.GetTemplates()
if err != nil {
log.Printf("Error retrieving templates: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to retrieve templates",
"details": err.Error(),
})
return
}

c.JSON(http.StatusOK, gin.H{
"pods": pods,
"user_info": userInfo,
"templates": templates,
})
}
7 changes: 4 additions & 3 deletions internal/api/handlers/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ type GroupsRequest struct {
}

type AdminCloneRequest struct {
Template string `json:"template" binding:"required,min=1,max=100" validate:"alphanum,ascii"`
Usernames []string `json:"usernames" binding:"omitempty,dive,min=1,max=100" validate:"dive,alphanum,ascii"`
Groups []string `json:"groups" binding:"omitempty,dive,min=1,max=100" validate:"dive,alphanum,ascii"`
Template string `json:"template" binding:"required,min=1,max=100" validate:"alphanum,ascii"`
Usernames []string `json:"usernames" binding:"omitempty,dive,min=1,max=100" validate:"dive,alphanum,ascii"`
Groups []string `json:"groups" binding:"omitempty,dive,min=1,max=100" validate:"dive,alphanum,ascii"`
StartingVMID int `json:"starting_vmid" binding:"omitempty,min=100,max=999900"`
}

type DeletePodRequest struct {
Expand Down
7 changes: 2 additions & 5 deletions internal/api/routes/admin_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@ import (
)

// registerAdminRoutes defines all routes accessible to admin users
func registerAdminRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler, proxmoxHandler *handlers.ProxmoxHandler, cloningHandler *handlers.CloningHandler) {
// Create dashboard handler
dashboardHandler := handlers.NewDashboardHandler(authHandler, proxmoxHandler, cloningHandler)

func registerAdminRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler, proxmoxHandler *handlers.ProxmoxHandler, cloningHandler *handlers.CloningHandler, dashboardHandler *handlers.DashboardHandler) {
// GET Requests
g.GET("/dashboard", dashboardHandler.GetDashboardStatsHandler)
g.GET("/dashboard", dashboardHandler.GetAdminDashboardStatsHandler)
g.GET("/cluster", proxmoxHandler.GetClusterResourceUsageHandler)
g.GET("/users", authHandler.GetUsersHandler)
g.GET("/groups", authHandler.GetGroupsHandler)
Expand Down
3 changes: 2 additions & 1 deletion internal/api/routes/private_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import (
)

// registerPrivateRoutes defines all routes accessible to authenticated users
func registerPrivateRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler, proxmoxHandler *handlers.ProxmoxHandler, cloningHandler *handlers.CloningHandler) {
func registerPrivateRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler, cloningHandler *handlers.CloningHandler, dashboardHandler *handlers.DashboardHandler) {
// GET Requests
g.GET("/dashboard", dashboardHandler.GetUserDashboardStatsHandler)
g.GET("/session", authHandler.SessionHandler)
g.GET("/pods", cloningHandler.GetPodsHandler)
g.GET("/templates", cloningHandler.GetTemplatesHandler)
Expand Down
1 change: 1 addition & 0 deletions internal/api/routes/public_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ func registerPublicRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler,
// GET Requests
g.GET("/health", handlers.HealthCheckHandler(authHandler, cloningHandler))
g.POST("/login", authHandler.LoginHandler)
// g.POST("/register", authHandler.RegisterHandler)
}
7 changes: 5 additions & 2 deletions internal/api/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ import (

// RegisterRoutes sets up all API routes with their respective middleware and handlers
func RegisterRoutes(r *gin.Engine, authHandler *handlers.AuthHandler, proxmoxHandler *handlers.ProxmoxHandler, cloningHandler *handlers.CloningHandler) {
// Create centralized dashboard handler
dashboardHandler := handlers.NewDashboardHandler(authHandler, proxmoxHandler, cloningHandler)

// Public routes (no authentication required)
public := r.Group("/api/v1")
registerPublicRoutes(public, authHandler, cloningHandler)

// Private routes (authentication required)
private := r.Group("/api/v1")
private.Use(middleware.AuthRequired)
registerPrivateRoutes(private, authHandler, proxmoxHandler, cloningHandler)
registerPrivateRoutes(private, authHandler, cloningHandler, dashboardHandler)

// Admin routes (authentication + admin privileges required)
admin := r.Group("/api/v1/admin")
admin.Use(middleware.AdminRequired)
registerAdminRoutes(admin, authHandler, proxmoxHandler, cloningHandler)
registerAdminRoutes(admin, authHandler, proxmoxHandler, cloningHandler, dashboardHandler)
}
50 changes: 28 additions & 22 deletions internal/cloning/cloning_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
if req.CheckExistingDeployments {
for _, target := range req.Targets {
targetPoolName := fmt.Sprintf("%s_%s", req.Template, target.Name)
isDeployed, err := cs.IsDeployed(targetPoolName)
isValid, err := cs.ValidateCloneRequest(targetPoolName, target.Name)
if err != nil {
return fmt.Errorf("failed to check if template is deployed for %s: %w", target.Name, err)
return fmt.Errorf("failed to validate the deployment of template for %s: %w", target.Name, err)
}
if isDeployed {
return fmt.Errorf("template %s is already or in the process of being deployed for %s", req.Template, target.Name)
if !isValid {
return fmt.Errorf("template %s is already deployed for %s or they have exceeded the maximum of 5 deployed pods", req.Template, target.Name)
}
}
}
Expand Down Expand Up @@ -127,9 +127,22 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
return fmt.Errorf("failed to get next pod IDs: %w", err)
}

vmIDs, err := cs.ProxmoxService.GetNextVMIDs(len(req.Targets) * numVMsPerTarget)
if err != nil {
return fmt.Errorf("failed to get next VM IDs: %w", err)
// Lock the vmid allocation mutex to prevent race conditions during vmid allocation
cs.vmidMutex.Lock()

// Use StartingVMID from request if provided, otherwise get next available VMIDs
var vmIDs []int
numVMs := len(req.Targets) * numVMsPerTarget
if req.StartingVMID != 0 {
log.Printf("Starting VMID allocation from specified starting VMID: %d", req.StartingVMID)
for i := range numVMs {
vmIDs = append(vmIDs, req.StartingVMID+i)
}
} else {
vmIDs, err = cs.ProxmoxService.GetNextVMIDs(numVMs)
if err != nil {
return fmt.Errorf("failed to get next VM IDs: %w", err)
}
}

for i := range req.Targets {
Expand Down Expand Up @@ -169,7 +182,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
NewVMID: target.VMIDs[0],
TargetNode: bestNode,
}
err = cs.ProxmoxService.CloneVMWithConfig(routerCloneReq)
err = cs.ProxmoxService.CloneVM(routerCloneReq)
if err != nil {
errors = append(errors, fmt.Sprintf("failed to clone router VM for %s: %v", target.Name, err))
} else {
Expand All @@ -191,7 +204,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
NewVMID: target.VMIDs[i+1],
TargetNode: bestNode,
}
err := cs.ProxmoxService.CloneVMWithConfig(vmCloneReq)
err := cs.ProxmoxService.CloneVM(vmCloneReq)
if err != nil {
errors = append(errors, fmt.Sprintf("failed to clone VM %s for %s: %v", vm.Name, target.Name, err))
}
Expand Down Expand Up @@ -223,6 +236,9 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
}
}

// Release the vmid allocation mutex now that all of the VMs are cloned on proxmox
cs.vmidMutex.Unlock()

// 9. Configure VNet of all VMs
log.Printf("Configuring VNets for %d targets", len(req.Targets))
for _, target := range req.Targets {
Expand Down Expand Up @@ -254,12 +270,8 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
}

// Wait for router to be running
routerVM := proxmox.VM{
Node: routerInfo.Node,
VMID: routerInfo.VMID,
}
log.Printf("Waiting for router VM to be running for %s (VMID: %d)", routerInfo.TargetName, routerInfo.VMID)
err = cs.ProxmoxService.WaitForRunning(routerVM)
err = cs.ProxmoxService.WaitForRunning(routerInfo.Node, routerInfo.VMID)
if err != nil {
errors = append(errors, fmt.Sprintf("failed to start router VM for %s: %v", routerInfo.TargetName, err))
}
Expand All @@ -268,14 +280,8 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
// 11. Configure all pod routers (separate step after all routers are running)
log.Printf("Configuring %d pod routers", len(clonedRouters))
for _, routerInfo := range clonedRouters {
// Only configure routers that successfully started
routerVM := proxmox.VM{
Node: routerInfo.Node,
VMID: routerInfo.VMID,
}

// Double-check that router is still running before configuration
err = cs.ProxmoxService.WaitForRunning(routerVM)
err = cs.ProxmoxService.WaitForRunning(routerInfo.Node, routerInfo.VMID)
if err != nil {
errors = append(errors, fmt.Sprintf("router not running before configuration for %s: %v", routerInfo.TargetName, err))
continue
Expand Down Expand Up @@ -360,7 +366,7 @@ func (cs *CloningService) DeletePod(pod string) error {
// Wait for all previously running VMs to be stopped
if len(runningVMs) > 0 {
for _, vm := range runningVMs {
if err := cs.ProxmoxService.WaitForStopped(vm); err != nil {
if err := cs.ProxmoxService.WaitForStopped(vm.Node, vm.VMID); err != nil {
// Continue with deletion even if we can't confirm the VM is stopped
}
}
Expand Down
23 changes: 17 additions & 6 deletions internal/cloning/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ func (cs *CloningService) GetPods(username string) ([]Pod, error) {
}

// Build regex pattern to match username or any of their group names
regexPattern := fmt.Sprintf(`1[0-9]{3}_.*_(%s|%s)`, username, strings.Join(groups, "|"))
groupsWithUser := append(groups, username)
regexPattern := fmt.Sprintf(`1[0-9]{3}_.*_(%s)`, strings.Join(groupsWithUser, "|"))

// Get pods based on regex pattern
pods, err := cs.MapVirtualResourcesToPods(regexPattern)
Expand Down Expand Up @@ -75,18 +76,28 @@ func (cs *CloningService) MapVirtualResourcesToPods(regex string) ([]Pod, error)
return pods, nil
}

func (cs *CloningService) IsDeployed(templateName string) (bool, error) {
func (cs *CloningService) ValidateCloneRequest(templateName string, username string) (bool, error) {
podPools, err := cs.AdminGetPods()
if err != nil {
return false, fmt.Errorf("failed to get pod pools: %w", err)
return false, fmt.Errorf("failed to get deployed pods: %w", err)
}

var alreadyDeployed = false
var numDeployments = 0

for _, pod := range podPools {
// Remove the Pod ID number and _ to compare
if pod.Name[5:] == templateName {
return true, nil
if !alreadyDeployed && pod.Name[5:] == templateName {
alreadyDeployed = true
}

if strings.Contains(pod.Name, username) {
numDeployments++
}
}

return false, nil
// Valid if not already deployed and user has less than 5 deployments
var isValidCloneRequest = !alreadyDeployed && numDeployments < 5

return isValidCloneRequest, nil
}
Loading