Skip to content

Commit 8b6e957

Browse files
authored
Merge pull request #6 from cpp-cyber/development
Development
2 parents 5f9c907 + a0ff9e9 commit 8b6e957

File tree

15 files changed

+372
-164
lines changed

15 files changed

+372
-164
lines changed

internal/api/handlers/cloning_handler.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,34 @@ func (ch *CloningHandler) CloneTemplateHandler(c *gin.Context) {
6060

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

63+
publishedTemplates, err := ch.Service.DatabaseService.GetPublishedTemplates()
64+
if err != nil {
65+
log.Printf("Error fetching published templates: %v", err)
66+
c.JSON(http.StatusInternalServerError, gin.H{
67+
"error": "Failed to fetch published templates",
68+
"details": err.Error(),
69+
})
70+
return
71+
}
72+
73+
// Check if the requested template is in the list of published templates
74+
templateFound := false
75+
for _, tmpl := range publishedTemplates {
76+
if tmpl.Name == req.Template {
77+
templateFound = true
78+
break
79+
}
80+
}
81+
82+
if !templateFound {
83+
log.Printf("Template %s not found or not published", req.Template)
84+
c.JSON(http.StatusBadRequest, gin.H{
85+
"error": "Template not found or not published",
86+
"details": fmt.Sprintf("Template %s is not available for cloning", req.Template),
87+
})
88+
return
89+
}
90+
6391
// Create the cloning request using the new format
6492
cloneReq := cloning.CloneRequest{
6593
Template: req.Template,
@@ -121,6 +149,7 @@ func (ch *CloningHandler) AdminCloneTemplateHandler(c *gin.Context) {
121149
Template: req.Template,
122150
Targets: targets,
123151
CheckExistingDeployments: false,
152+
StartingVMID: req.StartingVMID,
124153
}
125154

126155
// Perform clone operation

internal/api/handlers/dashboard_handler.go

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package handlers
22

33
import (
4+
"fmt"
5+
"log"
46
"net/http"
7+
"strings"
58

9+
"github.com/gin-contrib/sessions"
610
"github.com/gin-gonic/gin"
711
)
812

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

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

2226
// Get user count
@@ -71,3 +75,57 @@ func (dh *DashboardHandler) GetDashboardStatsHandler(c *gin.Context) {
7175
"stats": stats,
7276
})
7377
}
78+
79+
// PRIVATE: GetUserDashboardStatsHandler retrieves all user dashboard statistics in a single request
80+
func (dh *DashboardHandler) GetUserDashboardStatsHandler(c *gin.Context) {
81+
session := sessions.Default(c)
82+
username := session.Get("id").(string)
83+
84+
// Get user's deployed pods
85+
pods, err := dh.cloningHandler.Service.GetPods(username)
86+
if err != nil {
87+
log.Printf("Error retrieving pods for user %s: %v", username, err)
88+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve pods", "details": err.Error()})
89+
return
90+
}
91+
92+
// Loop through the user's deployed pods and add template information
93+
for i := range pods {
94+
templateName := strings.Replace(pods[i].Name[5:], fmt.Sprintf("_%s", username), "", 1)
95+
templateInfo, err := dh.cloningHandler.Service.DatabaseService.GetTemplateInfo(templateName)
96+
if err != nil {
97+
log.Printf("Error retrieving template info for pod %s: %v", pods[i].Name, err)
98+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve template info for pod", "details": err.Error()})
99+
return
100+
}
101+
pods[i].Template = templateInfo
102+
}
103+
104+
// Get user's information
105+
userInfo, err := dh.authHandler.ldapService.GetUser(username)
106+
if err != nil {
107+
log.Printf("Error retrieving user info for %s: %v", username, err)
108+
c.JSON(http.StatusInternalServerError, gin.H{
109+
"error": "Failed to retrieve user info",
110+
"details": err.Error(),
111+
})
112+
return
113+
}
114+
115+
// Get public pod templates
116+
templates, err := dh.cloningHandler.Service.DatabaseService.GetTemplates()
117+
if err != nil {
118+
log.Printf("Error retrieving templates: %v", err)
119+
c.JSON(http.StatusInternalServerError, gin.H{
120+
"error": "Failed to retrieve templates",
121+
"details": err.Error(),
122+
})
123+
return
124+
}
125+
126+
c.JSON(http.StatusOK, gin.H{
127+
"pods": pods,
128+
"user_info": userInfo,
129+
"templates": templates,
130+
})
131+
}

internal/api/handlers/types.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ type GroupsRequest struct {
6666
}
6767

6868
type AdminCloneRequest struct {
69-
Template string `json:"template" binding:"required,min=1,max=100" validate:"alphanum,ascii"`
70-
Usernames []string `json:"usernames" binding:"omitempty,dive,min=1,max=100" validate:"dive,alphanum,ascii"`
71-
Groups []string `json:"groups" binding:"omitempty,dive,min=1,max=100" validate:"dive,alphanum,ascii"`
69+
Template string `json:"template" binding:"required,min=1,max=100" validate:"alphanum,ascii"`
70+
Usernames []string `json:"usernames" binding:"omitempty,dive,min=1,max=100" validate:"dive,alphanum,ascii"`
71+
Groups []string `json:"groups" binding:"omitempty,dive,min=1,max=100" validate:"dive,alphanum,ascii"`
72+
StartingVMID int `json:"starting_vmid" binding:"omitempty,min=100,max=999900"`
7273
}
7374

7475
type DeletePodRequest struct {

internal/api/routes/admin_routes.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,9 @@ import (
66
)
77

88
// registerAdminRoutes defines all routes accessible to admin users
9-
func registerAdminRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler, proxmoxHandler *handlers.ProxmoxHandler, cloningHandler *handlers.CloningHandler) {
10-
// Create dashboard handler
11-
dashboardHandler := handlers.NewDashboardHandler(authHandler, proxmoxHandler, cloningHandler)
12-
9+
func registerAdminRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler, proxmoxHandler *handlers.ProxmoxHandler, cloningHandler *handlers.CloningHandler, dashboardHandler *handlers.DashboardHandler) {
1310
// GET Requests
14-
g.GET("/dashboard", dashboardHandler.GetDashboardStatsHandler)
11+
g.GET("/dashboard", dashboardHandler.GetAdminDashboardStatsHandler)
1512
g.GET("/cluster", proxmoxHandler.GetClusterResourceUsageHandler)
1613
g.GET("/users", authHandler.GetUsersHandler)
1714
g.GET("/groups", authHandler.GetGroupsHandler)

internal/api/routes/private_routes.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import (
66
)
77

88
// registerPrivateRoutes defines all routes accessible to authenticated users
9-
func registerPrivateRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler, proxmoxHandler *handlers.ProxmoxHandler, cloningHandler *handlers.CloningHandler) {
9+
func registerPrivateRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler, cloningHandler *handlers.CloningHandler, dashboardHandler *handlers.DashboardHandler) {
1010
// GET Requests
11+
g.GET("/dashboard", dashboardHandler.GetUserDashboardStatsHandler)
1112
g.GET("/session", authHandler.SessionHandler)
1213
g.GET("/pods", cloningHandler.GetPodsHandler)
1314
g.GET("/templates", cloningHandler.GetTemplatesHandler)

internal/api/routes/public_routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ func registerPublicRoutes(g *gin.RouterGroup, authHandler *handlers.AuthHandler,
1010
// GET Requests
1111
g.GET("/health", handlers.HealthCheckHandler(authHandler, cloningHandler))
1212
g.POST("/login", authHandler.LoginHandler)
13+
// g.POST("/register", authHandler.RegisterHandler)
1314
}

internal/api/routes/routes.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@ import (
88

99
// RegisterRoutes sets up all API routes with their respective middleware and handlers
1010
func RegisterRoutes(r *gin.Engine, authHandler *handlers.AuthHandler, proxmoxHandler *handlers.ProxmoxHandler, cloningHandler *handlers.CloningHandler) {
11+
// Create centralized dashboard handler
12+
dashboardHandler := handlers.NewDashboardHandler(authHandler, proxmoxHandler, cloningHandler)
13+
1114
// Public routes (no authentication required)
1215
public := r.Group("/api/v1")
1316
registerPublicRoutes(public, authHandler, cloningHandler)
1417

1518
// Private routes (authentication required)
1619
private := r.Group("/api/v1")
1720
private.Use(middleware.AuthRequired)
18-
registerPrivateRoutes(private, authHandler, proxmoxHandler, cloningHandler)
21+
registerPrivateRoutes(private, authHandler, cloningHandler, dashboardHandler)
1922

2023
// Admin routes (authentication + admin privileges required)
2124
admin := r.Group("/api/v1/admin")
2225
admin.Use(middleware.AdminRequired)
23-
registerAdminRoutes(admin, authHandler, proxmoxHandler, cloningHandler)
26+
registerAdminRoutes(admin, authHandler, proxmoxHandler, cloningHandler, dashboardHandler)
2427
}

internal/cloning/cloning_service.go

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
7272
if req.CheckExistingDeployments {
7373
for _, target := range req.Targets {
7474
targetPoolName := fmt.Sprintf("%s_%s", req.Template, target.Name)
75-
isDeployed, err := cs.IsDeployed(targetPoolName)
75+
isValid, err := cs.ValidateCloneRequest(targetPoolName, target.Name)
7676
if err != nil {
77-
return fmt.Errorf("failed to check if template is deployed for %s: %w", target.Name, err)
77+
return fmt.Errorf("failed to validate the deployment of template for %s: %w", target.Name, err)
7878
}
79-
if isDeployed {
80-
return fmt.Errorf("template %s is already or in the process of being deployed for %s", req.Template, target.Name)
79+
if !isValid {
80+
return fmt.Errorf("template %s is already deployed for %s or they have exceeded the maximum of 5 deployed pods", req.Template, target.Name)
8181
}
8282
}
8383
}
@@ -127,9 +127,22 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
127127
return fmt.Errorf("failed to get next pod IDs: %w", err)
128128
}
129129

130-
vmIDs, err := cs.ProxmoxService.GetNextVMIDs(len(req.Targets) * numVMsPerTarget)
131-
if err != nil {
132-
return fmt.Errorf("failed to get next VM IDs: %w", err)
130+
// Lock the vmid allocation mutex to prevent race conditions during vmid allocation
131+
cs.vmidMutex.Lock()
132+
133+
// Use StartingVMID from request if provided, otherwise get next available VMIDs
134+
var vmIDs []int
135+
numVMs := len(req.Targets) * numVMsPerTarget
136+
if req.StartingVMID != 0 {
137+
log.Printf("Starting VMID allocation from specified starting VMID: %d", req.StartingVMID)
138+
for i := range numVMs {
139+
vmIDs = append(vmIDs, req.StartingVMID+i)
140+
}
141+
} else {
142+
vmIDs, err = cs.ProxmoxService.GetNextVMIDs(numVMs)
143+
if err != nil {
144+
return fmt.Errorf("failed to get next VM IDs: %w", err)
145+
}
133146
}
134147

135148
for i := range req.Targets {
@@ -169,7 +182,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
169182
NewVMID: target.VMIDs[0],
170183
TargetNode: bestNode,
171184
}
172-
err = cs.ProxmoxService.CloneVMWithConfig(routerCloneReq)
185+
err = cs.ProxmoxService.CloneVM(routerCloneReq)
173186
if err != nil {
174187
errors = append(errors, fmt.Sprintf("failed to clone router VM for %s: %v", target.Name, err))
175188
} else {
@@ -191,7 +204,7 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
191204
NewVMID: target.VMIDs[i+1],
192205
TargetNode: bestNode,
193206
}
194-
err := cs.ProxmoxService.CloneVMWithConfig(vmCloneReq)
207+
err := cs.ProxmoxService.CloneVM(vmCloneReq)
195208
if err != nil {
196209
errors = append(errors, fmt.Sprintf("failed to clone VM %s for %s: %v", vm.Name, target.Name, err))
197210
}
@@ -223,6 +236,9 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
223236
}
224237
}
225238

239+
// Release the vmid allocation mutex now that all of the VMs are cloned on proxmox
240+
cs.vmidMutex.Unlock()
241+
226242
// 9. Configure VNet of all VMs
227243
log.Printf("Configuring VNets for %d targets", len(req.Targets))
228244
for _, target := range req.Targets {
@@ -254,12 +270,8 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
254270
}
255271

256272
// Wait for router to be running
257-
routerVM := proxmox.VM{
258-
Node: routerInfo.Node,
259-
VMID: routerInfo.VMID,
260-
}
261273
log.Printf("Waiting for router VM to be running for %s (VMID: %d)", routerInfo.TargetName, routerInfo.VMID)
262-
err = cs.ProxmoxService.WaitForRunning(routerVM)
274+
err = cs.ProxmoxService.WaitForRunning(routerInfo.Node, routerInfo.VMID)
263275
if err != nil {
264276
errors = append(errors, fmt.Sprintf("failed to start router VM for %s: %v", routerInfo.TargetName, err))
265277
}
@@ -268,14 +280,8 @@ func (cs *CloningService) CloneTemplate(req CloneRequest) error {
268280
// 11. Configure all pod routers (separate step after all routers are running)
269281
log.Printf("Configuring %d pod routers", len(clonedRouters))
270282
for _, routerInfo := range clonedRouters {
271-
// Only configure routers that successfully started
272-
routerVM := proxmox.VM{
273-
Node: routerInfo.Node,
274-
VMID: routerInfo.VMID,
275-
}
276-
277283
// Double-check that router is still running before configuration
278-
err = cs.ProxmoxService.WaitForRunning(routerVM)
284+
err = cs.ProxmoxService.WaitForRunning(routerInfo.Node, routerInfo.VMID)
279285
if err != nil {
280286
errors = append(errors, fmt.Sprintf("router not running before configuration for %s: %v", routerInfo.TargetName, err))
281287
continue
@@ -360,7 +366,7 @@ func (cs *CloningService) DeletePod(pod string) error {
360366
// Wait for all previously running VMs to be stopped
361367
if len(runningVMs) > 0 {
362368
for _, vm := range runningVMs {
363-
if err := cs.ProxmoxService.WaitForStopped(vm); err != nil {
369+
if err := cs.ProxmoxService.WaitForStopped(vm.Node, vm.VMID); err != nil {
364370
// Continue with deletion even if we can't confirm the VM is stopped
365371
}
366372
}

internal/cloning/pods.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ func (cs *CloningService) GetPods(username string) ([]Pod, error) {
2222
}
2323

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

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

78-
func (cs *CloningService) IsDeployed(templateName string) (bool, error) {
79+
func (cs *CloningService) ValidateCloneRequest(templateName string, username string) (bool, error) {
7980
podPools, err := cs.AdminGetPods()
8081
if err != nil {
81-
return false, fmt.Errorf("failed to get pod pools: %w", err)
82+
return false, fmt.Errorf("failed to get deployed pods: %w", err)
8283
}
8384

85+
var alreadyDeployed = false
86+
var numDeployments = 0
87+
8488
for _, pod := range podPools {
8589
// Remove the Pod ID number and _ to compare
86-
if pod.Name[5:] == templateName {
87-
return true, nil
90+
if !alreadyDeployed && pod.Name[5:] == templateName {
91+
alreadyDeployed = true
92+
}
93+
94+
if strings.Contains(pod.Name, username) {
95+
numDeployments++
8896
}
8997
}
9098

91-
return false, nil
99+
// Valid if not already deployed and user has less than 5 deployments
100+
var isValidCloneRequest = !alreadyDeployed && numDeployments < 5
101+
102+
return isValidCloneRequest, nil
92103
}

0 commit comments

Comments
 (0)