mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-03 20:12:58 +00:00
small fix, don't worry about it
This commit is contained in:
@@ -1,679 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// NodeAgent represents a container orchestration agent
|
||||
type NodeAgent struct {
|
||||
ID string `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
Hostname string `json:"hostname" gorm:"not null"`
|
||||
IPAddress string `json:"ip_address" gorm:"not null"`
|
||||
Port int `json:"port" gorm:"not null"`
|
||||
Status string `json:"status" gorm:"default:'offline'"`
|
||||
Version string `json:"version"`
|
||||
Capabilities AgentCapabilities `json:"capabilities" gorm:"serializer:json"`
|
||||
Resources NodeResources `json:"resources" gorm:"serializer:json"`
|
||||
LastHeartbeat time.Time `json:"last_heartbeat"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Metadata map[string]interface{} `json:"metadata" gorm:"serializer:json"`
|
||||
}
|
||||
|
||||
// AgentCapabilities defines what the agent can do
|
||||
type AgentCapabilities struct {
|
||||
ContainerRuntimes []string `json:"container_runtimes"`
|
||||
SupportedArchitectures []string `json:"supported_architectures"`
|
||||
MaxContainers int `json:"max_containers"`
|
||||
StorageDriver string `json:"storage_driver"`
|
||||
NetworkPlugins []string `json:"network_plugins"`
|
||||
Features []string `json:"features"`
|
||||
}
|
||||
|
||||
// NodeResources represents the agent's available resources
|
||||
type NodeResources struct {
|
||||
CPU CPUResources `json:"cpu"`
|
||||
Memory MemoryResources `json:"memory"`
|
||||
Storage StorageResources `json:"storage"`
|
||||
Network NetworkResources `json:"network"`
|
||||
}
|
||||
|
||||
type CPUResources struct {
|
||||
Cores int `json:"cores"`
|
||||
Allocation float64 `json:"allocation"` // percentage
|
||||
Usage float64 `json:"usage"` // current usage percentage
|
||||
}
|
||||
|
||||
type MemoryResources struct {
|
||||
Total int `json:"total"`
|
||||
Allocated int `json:"allocated"`
|
||||
Used int `json:"used"`
|
||||
Available int `json:"available"`
|
||||
}
|
||||
|
||||
type StorageResources struct {
|
||||
Total int `json:"total"`
|
||||
Allocated int `json:"allocated"`
|
||||
Used int `json:"used"`
|
||||
Available int `json:"available"`
|
||||
}
|
||||
|
||||
type NetworkResources struct {
|
||||
Interfaces []NetworkInterface `json:"interfaces"`
|
||||
Bandwidth BandwidthInfo `json:"bandwidth"`
|
||||
}
|
||||
|
||||
type NetworkInterface struct {
|
||||
Name string `json:"name"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
MACAddress string `json:"mac_address"`
|
||||
Speed int `json:"speed"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type BandwidthInfo struct {
|
||||
Inbound int `json:"inbound"` // bytes per second
|
||||
Outbound int `json:"outbound"` // bytes per second
|
||||
}
|
||||
|
||||
// ContainerInstance represents a container running on an agent
|
||||
type ContainerInstance struct {
|
||||
ID string `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
Image string `json:"image" gorm:"not null"`
|
||||
ProjectID string `json:"project_id" gorm:"not null"`
|
||||
ServiceID string `json:"service_id" gorm:"not null"`
|
||||
NodeAgentID string `json:"node_agent_id" gorm:"not null"`
|
||||
Status ContainerStatus `json:"status" gorm:"serializer:json"`
|
||||
Resources ContainerResources `json:"resources" gorm:"serializer:json"`
|
||||
Ports []PortMapping `json:"ports" gorm:"serializer:json"`
|
||||
Environment map[string]string `json:"environment" gorm:"serializer:json"`
|
||||
Volumes []VolumeMount `json:"volumes" gorm:"serializer:json"`
|
||||
Networks []string `json:"networks" gorm:"serializer:json"`
|
||||
RestartPolicy RestartPolicy `json:"restart_policy" gorm:"serializer:json"`
|
||||
HealthCheck *HealthCheck `json:"health_check" gorm:"serializer:json"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt *time.Time `json:"started_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ContainerStatus struct {
|
||||
State string `json:"state"`
|
||||
Health string `json:"health"`
|
||||
ExitCode *int `json:"exit_code"`
|
||||
Error *string `json:"error"`
|
||||
StartedAt *time.Time `json:"started_at"`
|
||||
FinishedAt *time.Time `json:"finished_at"`
|
||||
}
|
||||
|
||||
type ContainerResources struct {
|
||||
CPULimit int `json:"cpu_limit"`
|
||||
CPUReservation int `json:"cpu_reservation"`
|
||||
MemoryLimit int `json:"memory_limit"`
|
||||
MemoryReservation int `json:"memory_reservation"`
|
||||
DiskLimit *int `json:"disk_limit"`
|
||||
}
|
||||
|
||||
type PortMapping struct {
|
||||
ContainerPort int `json:"container_port"`
|
||||
HostPort *int `json:"host_port"`
|
||||
Protocol string `json:"protocol"`
|
||||
Published bool `json:"published"`
|
||||
}
|
||||
|
||||
type VolumeMount struct {
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
Type string `json:"type"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}
|
||||
|
||||
type RestartPolicy struct {
|
||||
Name string `json:"name"`
|
||||
MaximumRetryCount *int `json:"maximum_retry_count"`
|
||||
}
|
||||
|
||||
type HealthCheck struct {
|
||||
Test []string `json:"test"`
|
||||
Interval int `json:"interval"`
|
||||
Timeout int `json:"timeout"`
|
||||
Retries int `json:"retries"`
|
||||
StartPeriod int `json:"start_period"`
|
||||
}
|
||||
|
||||
// AgentCommand represents a command sent to an agent
|
||||
type AgentCommand struct {
|
||||
ID string `json:"id" gorm:"primaryKey"`
|
||||
Type string `json:"type" gorm:"not null"`
|
||||
NodeAgentID string `json:"node_agent_id" gorm:"not null"`
|
||||
ContainerID *string `json:"container_id"`
|
||||
Payload map[string]interface{} `json:"payload" gorm:"serializer:json"`
|
||||
Status string `json:"status" gorm:"default:'pending'"`
|
||||
Result *string `json:"result"`
|
||||
Error *string `json:"error"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CompletedAt *time.Time `json:"completed_at"`
|
||||
}
|
||||
|
||||
// AgentHeartbeat represents a heartbeat message from an agent
|
||||
type AgentHeartbeat struct {
|
||||
NodeAgentID string `json:"node_agent_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Status string `json:"status"`
|
||||
Resources NodeResources `json:"resources"`
|
||||
ContainerCount int `json:"container_count"`
|
||||
SystemLoad SystemLoad `json:"system_load"`
|
||||
Uptime int64 `json:"uptime"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type SystemLoad struct {
|
||||
Load1M float64 `json:"load_1m"`
|
||||
Load5M float64 `json:"load_5m"`
|
||||
Load15M float64 `json:"load_15m"`
|
||||
}
|
||||
|
||||
// NodeAgentHandler handles agent-related endpoints
|
||||
type NodeAgentHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewNodeAgentHandler(db *gorm.DB) *NodeAgentHandler {
|
||||
return &NodeAgentHandler{db: db}
|
||||
}
|
||||
|
||||
// RegisterAgent handles agent registration
|
||||
func (h *NodeAgentHandler) RegisterAgent(c *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Hostname string `json:"hostname" binding:"required"`
|
||||
IPAddress string `json:"ip_address" binding:"required"`
|
||||
Port int `json:"port" binding:"required"`
|
||||
Capabilities AgentCapabilities `json:"capabilities" binding:"required"`
|
||||
AuthToken string `json:"auth_token" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate auth token (in a real implementation, this would be more sophisticated)
|
||||
if req.AuthToken != "valid-token" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid auth token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if agent already exists
|
||||
var existingAgent NodeAgent
|
||||
if err := h.db.Where("hostname = ? AND ip_address = ?", req.Hostname, req.IPAddress).First(&existingAgent).Error; err == nil {
|
||||
// Update existing agent
|
||||
existingAgent.Name = req.Name
|
||||
existingAgent.Port = req.Port
|
||||
existingAgent.Capabilities = req.Capabilities
|
||||
existingAgent.Status = "connecting"
|
||||
existingAgent.LastHeartbeat = time.Now()
|
||||
|
||||
if err := h.db.Save(&existingAgent).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update agent"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"agent_id": existingAgent.ID,
|
||||
"auth_token": req.AuthToken,
|
||||
"status": "updated",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create new agent
|
||||
agent := NodeAgent{
|
||||
ID: uuid.New().String(),
|
||||
Name: req.Name,
|
||||
Hostname: req.Hostname,
|
||||
IPAddress: req.IPAddress,
|
||||
Port: req.Port,
|
||||
Status: "connecting",
|
||||
Capabilities: req.Capabilities,
|
||||
Resources: NodeResources{
|
||||
CPU: CPUResources{
|
||||
Cores: 4,
|
||||
Allocation: 0,
|
||||
Usage: 0,
|
||||
},
|
||||
Memory: MemoryResources{
|
||||
Total: 8 * 1024 * 1024 * 1024, // 8GB
|
||||
Allocated: 0,
|
||||
Used: 0,
|
||||
Available: 8 * 1024 * 1024 * 1024,
|
||||
},
|
||||
Storage: StorageResources{
|
||||
Total: 100 * 1024 * 1024 * 1024, // 100GB
|
||||
Allocated: 0,
|
||||
Used: 0,
|
||||
Available: 100 * 1024 * 1024 * 1024,
|
||||
},
|
||||
Network: NetworkResources{
|
||||
Interfaces: []NetworkInterface{
|
||||
{
|
||||
Name: "eth0",
|
||||
IPAddress: req.IPAddress,
|
||||
MACAddress: "00:00:00:00:00:00",
|
||||
Speed: 1000,
|
||||
Status: "up",
|
||||
},
|
||||
},
|
||||
Bandwidth: BandwidthInfo{
|
||||
Inbound: 0,
|
||||
Outbound: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
LastHeartbeat: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Metadata: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
if err := h.db.Create(&agent).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create agent"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"agent_id": agent.ID,
|
||||
"auth_token": req.AuthToken,
|
||||
"status": "registered",
|
||||
})
|
||||
}
|
||||
|
||||
// GetAgents returns all registered agents
|
||||
func (h *NodeAgentHandler) GetAgents(c *gin.Context) {
|
||||
var agents []NodeAgent
|
||||
if err := h.db.Find(&agents).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch agents"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"agents": agents})
|
||||
}
|
||||
|
||||
// GetAgent returns a specific agent
|
||||
func (h *NodeAgentHandler) GetAgent(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var agent NodeAgent
|
||||
if err := h.db.First(&agent, "id = ?", id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Agent not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch agent"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"agent": agent})
|
||||
}
|
||||
|
||||
// UpdateAgent updates an agent's information
|
||||
func (h *NodeAgentHandler) UpdateAgent(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var agent NodeAgent
|
||||
if err := h.db.First(&agent, "id = ?", id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Agent not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch agent"})
|
||||
return
|
||||
}
|
||||
|
||||
var updates map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Model(&agent).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update agent"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"agent": agent})
|
||||
}
|
||||
|
||||
// DeleteAgent removes an agent
|
||||
func (h *NodeAgentHandler) DeleteAgent(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
if err := h.db.Delete(&NodeAgent{}, "id = ?", id).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete agent"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// SendHeartbeat handles heartbeat messages from agents
|
||||
func (h *NodeAgentHandler) SendHeartbeat(c *gin.Context) {
|
||||
var heartbeat AgentHeartbeat
|
||||
if err := c.ShouldBindJSON(&heartbeat); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var agent NodeAgent
|
||||
if err := h.db.First(&agent, "id = ?", heartbeat.NodeAgentID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Agent not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch agent"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update agent status and resources
|
||||
agent.Status = heartbeat.Status
|
||||
agent.Resources = heartbeat.Resources
|
||||
agent.LastHeartbeat = heartbeat.Timestamp
|
||||
agent.UpdatedAt = time.Now()
|
||||
|
||||
if err := h.db.Save(&agent).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update agent"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// GetAgentContainers returns containers running on a specific agent
|
||||
func (h *NodeAgentHandler) GetAgentContainers(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
var containers []ContainerInstance
|
||||
if err := h.db.Where("node_agent_id = ?", agentID).Find(&containers).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch containers"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"containers": containers})
|
||||
}
|
||||
|
||||
// CreateContainer creates a new container on an agent
|
||||
func (h *NodeAgentHandler) CreateContainer(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Image string `json:"image" binding:"required"`
|
||||
ProjectID string `json:"project_id" binding:"required"`
|
||||
ServiceID string `json:"service_id" binding:"required"`
|
||||
Resources ContainerResources `json:"resources" binding:"required"`
|
||||
Ports []PortMapping `json:"ports"`
|
||||
Environment map[string]string `json:"environment"`
|
||||
Volumes []VolumeMount `json:"volumes"`
|
||||
Networks []string `json:"networks"`
|
||||
RestartPolicy RestartPolicy `json:"restart_policy"`
|
||||
HealthCheck *HealthCheck `json:"health_check"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify agent exists
|
||||
var agent NodeAgent
|
||||
if err := h.db.First(&agent, "id = ?", agentID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Agent not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch agent"})
|
||||
return
|
||||
}
|
||||
|
||||
container := ContainerInstance{
|
||||
ID: uuid.New().String(),
|
||||
Name: req.Name,
|
||||
Image: req.Image,
|
||||
ProjectID: req.ProjectID,
|
||||
ServiceID: req.ServiceID,
|
||||
NodeAgentID: agentID,
|
||||
Status: ContainerStatus{
|
||||
State: "created",
|
||||
Health: "none",
|
||||
},
|
||||
Resources: req.Resources,
|
||||
Ports: req.Ports,
|
||||
Environment: req.Environment,
|
||||
Volumes: req.Volumes,
|
||||
Networks: req.Networks,
|
||||
RestartPolicy: req.RestartPolicy,
|
||||
HealthCheck: req.HealthCheck,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.db.Create(&container).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create container"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create command to start container on agent
|
||||
command := AgentCommand{
|
||||
ID: uuid.New().String(),
|
||||
Type: "create_container",
|
||||
NodeAgentID: agentID,
|
||||
ContainerID: &container.ID,
|
||||
Payload: map[string]interface{}{
|
||||
"container": container,
|
||||
},
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.db.Create(&command).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create container command"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"container": container})
|
||||
}
|
||||
|
||||
// ExecuteCommand executes a command on an agent
|
||||
func (h *NodeAgentHandler) ExecuteCommand(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
command := AgentCommand{
|
||||
ID: uuid.New().String(),
|
||||
Type: req.Type,
|
||||
NodeAgentID: agentID,
|
||||
Payload: req.Payload,
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.db.Create(&command).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create command"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"command": command})
|
||||
}
|
||||
|
||||
// GetAgentCommands returns commands for an agent
|
||||
func (h *NodeAgentHandler) GetAgentCommands(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
var commands []AgentCommand
|
||||
if err := h.db.Where("node_agent_id = ?", agentID).Order("created_at DESC").Find(&commands).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch commands"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"commands": commands})
|
||||
}
|
||||
|
||||
// GetCommandStatus returns the status of a specific command
|
||||
func (h *NodeAgentHandler) GetCommandStatus(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
commandID := c.Param("commandId")
|
||||
|
||||
var command AgentCommand
|
||||
if err := h.db.First(&command, "id = ? AND node_agent_id = ?", commandID, agentID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Command not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch command"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"command": command})
|
||||
}
|
||||
|
||||
// ContainerAction handles container lifecycle actions
|
||||
func (h *NodeAgentHandler) ContainerAction(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
containerID := c.Param("containerId")
|
||||
action := c.Param("action")
|
||||
|
||||
// Validate action
|
||||
validActions := map[string]bool{
|
||||
"start": true,
|
||||
"stop": true,
|
||||
"restart": true,
|
||||
"remove": true,
|
||||
}
|
||||
if !validActions[action] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid action"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify container exists
|
||||
var container ContainerInstance
|
||||
if err := h.db.First(&container, "id = ? AND node_agent_id = ?", containerID, agentID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Container not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch container"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create command for the action
|
||||
command := AgentCommand{
|
||||
ID: uuid.New().String(),
|
||||
Type: fmt.Sprintf("%s_container", action),
|
||||
NodeAgentID: agentID,
|
||||
ContainerID: &container.ID,
|
||||
Payload: map[string]interface{}{
|
||||
"container_id": containerID,
|
||||
},
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.db.Create(&command).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create command"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("Container %s action initiated", action)})
|
||||
}
|
||||
|
||||
// GetAgentMetrics returns metrics for an agent
|
||||
func (h *NodeAgentHandler) GetAgentMetrics(c *gin.Context) {
|
||||
_ = c.Param("id") // Use the parameter to avoid unused variable error
|
||||
timeRange := c.Query("time_range")
|
||||
if timeRange == "" {
|
||||
timeRange = "1h" // default to 1 hour
|
||||
}
|
||||
|
||||
// Parse time range
|
||||
duration, err := time.ParseDuration(timeRange)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid time range"})
|
||||
return
|
||||
}
|
||||
|
||||
// For now, return empty metrics - in a real implementation, this would query a metrics database
|
||||
metrics := []map[string]interface{}{
|
||||
{
|
||||
"timestamp": time.Now().Add(-duration).Format(time.RFC3339),
|
||||
"cpu": map[string]interface{}{
|
||||
"usage": 25.5,
|
||||
"usage_percent": 25.5,
|
||||
},
|
||||
"memory": map[string]interface{}{
|
||||
"usage": 2 * 1024 * 1024 * 1024, // 2GB
|
||||
"usage_percent": 25.0,
|
||||
"limit": 8 * 1024 * 1024 * 1024, // 8GB
|
||||
},
|
||||
},
|
||||
{
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"cpu": map[string]interface{}{
|
||||
"usage": 30.2,
|
||||
"usage_percent": 30.2,
|
||||
},
|
||||
"memory": map[string]interface{}{
|
||||
"usage": 2.5 * 1024 * 1024 * 1024, // 2.5GB
|
||||
"usage_percent": 31.25,
|
||||
"limit": 8 * 1024 * 1024 * 1024, // 8GB
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"metrics": metrics})
|
||||
}
|
||||
|
||||
// SetupRoutes registers the agent routes
|
||||
func (h *NodeAgentHandler) SetupRoutes(router *gin.RouterGroup) {
|
||||
agents := router.Group("/agents")
|
||||
{
|
||||
agents.POST("/register", h.RegisterAgent)
|
||||
agents.GET("", h.GetAgents)
|
||||
agents.GET("/:id", h.GetAgent)
|
||||
agents.PUT("/:id", h.UpdateAgent)
|
||||
agents.DELETE("/:id", h.DeleteAgent)
|
||||
agents.POST("/heartbeat", h.SendHeartbeat)
|
||||
|
||||
agents.GET("/:id/containers", h.GetAgentContainers)
|
||||
agents.POST("/:id/containers", h.CreateContainer)
|
||||
agents.POST("/:id/containers/:containerId/start", h.ContainerAction)
|
||||
agents.POST("/:id/containers/:containerId/stop", h.ContainerAction)
|
||||
agents.POST("/:id/containers/:containerId/restart", h.ContainerAction)
|
||||
agents.DELETE("/:id/containers/:containerId", h.ContainerAction)
|
||||
|
||||
agents.GET("/:id/metrics", h.GetAgentMetrics)
|
||||
agents.POST("/:id/commands", h.ExecuteCommand)
|
||||
agents.GET("/:id/commands", h.GetAgentCommands)
|
||||
agents.GET("/:id/commands/:commandId", h.GetCommandStatus)
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AuditLog struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
UserID string `json:"user_id" db:"user_id"`
|
||||
UserEmail string `json:"user_email" db:"user_email"`
|
||||
Resource string `json:"resource" db:"resource"`
|
||||
ResourceID string `json:"resource_id" db:"resource_id"`
|
||||
Action string `json:"action" db:"action"`
|
||||
Details string `json:"details" db:"details"`
|
||||
IPAddress string `json:"ip_address" db:"ip_address"`
|
||||
UserAgent string `json:"user_agent" db:"user_agent"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
|
||||
type AuditLogDetail struct {
|
||||
OldValue interface{} `json:"old_value,omitempty"`
|
||||
NewValue interface{} `json:"new_value,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
func LogAudit(userID, resource, resourceID, action string, details map[string]interface{}) {
|
||||
db := GetAuditDB()
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
|
||||
detailsJSON, _ := json.Marshal(details)
|
||||
|
||||
auditID := uuid.New().String()
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO audit_logs (id, user_id, resource, resource_id, action, details, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
auditID, userID, resource, resourceID, action, string(detailsJSON), time.Now(),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
}
|
||||
}
|
||||
|
||||
func LogAuditWithRequest(c *gin.Context, resource, resourceID, action string, details map[string]interface{}) {
|
||||
userID, _ := c.Get("user_id")
|
||||
userEmail, _ := c.Get("user_email")
|
||||
|
||||
details["ip_address"] = c.ClientIP()
|
||||
details["user_agent"] = c.GetHeader("User-Agent")
|
||||
|
||||
detailsJSON, _ := json.Marshal(details)
|
||||
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
auditID := uuid.New().String()
|
||||
_, err := db.Exec(
|
||||
`INSERT INTO audit_logs (id, user_id, user_email, resource, resource_id, action, details, ip_address, user_agent, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
||||
auditID, userID, userEmail, resource, resourceID, action, string(detailsJSON), c.ClientIP(), c.GetHeader("User-Agent"), time.Now(),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
}
|
||||
}
|
||||
|
||||
var auditDB *database.DB
|
||||
|
||||
func GetAuditDB() *database.DB {
|
||||
return auditDB
|
||||
}
|
||||
|
||||
func SetAuditDB(db *database.DB) {
|
||||
auditDB = db
|
||||
}
|
||||
|
||||
func handleGetAuditLogs(c *gin.Context) {
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
userID := c.MustGet("user_id").(string)
|
||||
|
||||
resource := c.Query("resource")
|
||||
action := c.Query("action")
|
||||
page := c.DefaultQuery("page", "1")
|
||||
limit := c.DefaultQuery("limit", "50")
|
||||
|
||||
query := `SELECT id, user_id, COALESCE(user_email, '') as user_email, resource, resource_id, action, details,
|
||||
COALESCE(ip_address, '') as ip_address, COALESCE(user_agent, '') as user_agent, created_at
|
||||
FROM audit_logs WHERE user_id = $1`
|
||||
args := []interface{}{userID}
|
||||
argNum := 2
|
||||
|
||||
if resource != "" {
|
||||
query += " AND resource = $" + string(rune('0'+argNum))
|
||||
args = append(args, resource)
|
||||
argNum++
|
||||
}
|
||||
|
||||
if action != "" {
|
||||
query += " AND action = $" + string(rune('0'+argNum))
|
||||
args = append(args, action)
|
||||
argNum++
|
||||
}
|
||||
|
||||
query += " ORDER BY created_at DESC LIMIT $" + string(rune('0'+argNum)) + " OFFSET $" + string(rune('0'+argNum+1))
|
||||
args = append(args, limit, (atoi(page)-1)*atoi(limit))
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch audit logs"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []AuditLog
|
||||
for rows.Next() {
|
||||
var log AuditLog
|
||||
err := rows.Scan(&log.ID, &log.UserID, &log.UserEmail, &log.Resource, &log.ResourceID, &log.Action, &log.Details, &log.IPAddress, &log.UserAgent, &log.CreatedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logs = append(logs, log)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"audit_logs": logs})
|
||||
}
|
||||
|
||||
func handleGetResourceAuditLogs(c *gin.Context) {
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
userID := c.MustGet("user_id").(string)
|
||||
resource := c.Param("resource")
|
||||
resourceID := c.Param("id")
|
||||
|
||||
rows, err := db.Query(
|
||||
`SELECT id, user_id, COALESCE(user_email, '') as user_email, resource, resource_id, action, details,
|
||||
COALESCE(ip_address, '') as ip_address, COALESCE(user_agent, '') as user_agent, created_at
|
||||
FROM audit_logs
|
||||
WHERE user_id = $1 AND resource = $2 AND resource_id = $3
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100`,
|
||||
userID, resource, resourceID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch audit logs"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []AuditLog
|
||||
for rows.Next() {
|
||||
var log AuditLog
|
||||
err := rows.Scan(&log.ID, &log.UserID, &log.UserEmail, &log.Resource, &log.ResourceID, &log.Action, &log.Details, &log.IPAddress, &log.UserAgent, &log.CreatedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
logs = append(logs, log)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"audit_logs": logs})
|
||||
}
|
||||
|
||||
func atoi(s string) int {
|
||||
var result int
|
||||
for _, c := range s {
|
||||
if c >= '0' && c <= '9' {
|
||||
result = result*10 + int(c-'0')
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Name string `json:"name" binding:"required,min=2"`
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
User interface{} `json:"user"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
func handleLogin(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
jwtSecret := c.MustGet("jwt_secret").(string)
|
||||
|
||||
// Find user by email
|
||||
var user User
|
||||
var hashedPassword string
|
||||
err := db.QueryRow(`
|
||||
SELECT id, email, password_hash, name, COALESCE(avatar_url, ''), created_at
|
||||
FROM users
|
||||
WHERE email = $1
|
||||
`, req.Email).Scan(&user.ID, &user.Email, &hashedPassword, &user.Name, &user.AvatarURL, &user.CreatedAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(req.Password)); err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, err := generateJWT(user.ID, user.Email, jwtSecret)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, AuthResponse{
|
||||
Token: token,
|
||||
User: user,
|
||||
})
|
||||
}
|
||||
|
||||
func handleRegister(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
jwtSecret := c.MustGet("jwt_secret").(string)
|
||||
|
||||
// Check if user already exists
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM users WHERE email = $1", req.Email).Scan(&count)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "User already exists"})
|
||||
return
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create user
|
||||
var user User
|
||||
err = db.QueryRow(`
|
||||
INSERT INTO users (email, password_hash, name)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, email, name, COALESCE(avatar_url, ''), created_at
|
||||
`, req.Email, string(hashedPassword), req.Name).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.CreatedAt)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, err := generateJWT(user.ID, user.Email, jwtSecret)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, AuthResponse{
|
||||
Token: token,
|
||||
User: user,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetProfile(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
var user User
|
||||
err := db.QueryRow(`
|
||||
SELECT id, email, name, COALESCE(avatar_url, ''), created_at
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`, userID).Scan(&user.ID, &user.Email, &user.Name, &user.AvatarURL, &user.CreatedAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
func handleUpdateProfile(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update user profile
|
||||
_, err := db.Exec(`
|
||||
UPDATE users
|
||||
SET name = COALESCE($1, name), avatar_url = COALESCE($2, avatar_url)
|
||||
WHERE id = $3
|
||||
`, req.Name, req.AvatarURL, userID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile"})
|
||||
return
|
||||
}
|
||||
|
||||
// Return updated user
|
||||
handleGetProfile(c)
|
||||
}
|
||||
|
||||
func generateJWT(userID, email, secret string) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": userID,
|
||||
"email": email,
|
||||
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(), // 7 days
|
||||
})
|
||||
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
// ValidateJWT validates a JWT token and returns the claims
|
||||
func ValidateJWT(tokenString, secret string) (jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
return []byte(secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, jwt.ErrInvalidKey
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"containr/internal/build"
|
||||
"containr/internal/docker"
|
||||
"containr/internal/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// BuildHandler handles build-related API endpoints
|
||||
type BuildHandler struct {
|
||||
buildManager *build.BuildManager
|
||||
dockerClient *docker.Client
|
||||
}
|
||||
|
||||
func (h *BuildHandler) buildUnavailable(c *gin.Context) bool {
|
||||
if h.buildManager != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Build service is unavailable: Docker client not initialized",
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// NewBuildHandler creates a new build handler
|
||||
func NewBuildHandler(buildManager *build.BuildManager, dockerClient *docker.Client) *BuildHandler {
|
||||
return &BuildHandler{
|
||||
buildManager: buildManager,
|
||||
dockerClient: dockerClient,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildRequest represents the request body for starting a build
|
||||
type BuildRequest struct {
|
||||
BuildType string `json:"build_type" binding:"required"`
|
||||
SourcePath string `json:"source_path"`
|
||||
PrebuiltImage string `json:"prebuilt_image"`
|
||||
ImageName string `json:"image_name" binding:"required"`
|
||||
ImageTag string `json:"image_tag" binding:"required"`
|
||||
RegistryURL string `json:"registry_url"`
|
||||
BuildCommand string `json:"build_command"`
|
||||
StartCommand string `json:"start_command"`
|
||||
Environment map[string]string `json:"environment"`
|
||||
BuildArgs map[string]string `json:"build_args"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ServiceID string `json:"service_id"`
|
||||
Branch string `json:"branch"`
|
||||
Commit string `json:"commit"`
|
||||
}
|
||||
|
||||
// BuildResponse represents the response for a build operation
|
||||
type BuildResponse struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
ImageName string `json:"image_name"`
|
||||
ImageTag string `json:"image_tag"`
|
||||
Size int64 `json:"size"`
|
||||
Digest string `json:"digest"`
|
||||
BuildTime time.Time `json:"build_time"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// BuildStatusResponse represents the response for build status
|
||||
type BuildStatusResponse struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ServiceID string `json:"service_id"`
|
||||
Status string `json:"status"`
|
||||
Progress int `json:"progress"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
ImageName string `json:"image_name"`
|
||||
ImageTag string `json:"image_tag"`
|
||||
Size int64 `json:"size"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Log string `json:"log"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
// BuildListResponse represents the response for listing builds
|
||||
type BuildListResponse struct {
|
||||
Builds []BuildStatusResponse `json:"builds"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// StartBuild starts a new build
|
||||
// @Summary Start a build
|
||||
// @Description Starts a new build process for the given configuration
|
||||
// @Tags builds
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body BuildRequest true "Build request"
|
||||
// @Success 200 {object} BuildResponse
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/builds [post]
|
||||
func (h *BuildHandler) StartBuild(c *gin.Context) {
|
||||
if h.buildUnavailable(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var req BuildRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to internal build request
|
||||
buildReq := &types.BuildRequest{
|
||||
BuildType: req.BuildType,
|
||||
SourcePath: req.SourcePath,
|
||||
PrebuiltImage: req.PrebuiltImage,
|
||||
ImageName: req.ImageName,
|
||||
ImageTag: req.ImageTag,
|
||||
RegistryURL: req.RegistryURL,
|
||||
BuildCommand: req.BuildCommand,
|
||||
StartCommand: req.StartCommand,
|
||||
Environment: req.Environment,
|
||||
BuildArgs: req.BuildArgs,
|
||||
Labels: req.Labels,
|
||||
ProjectID: req.ProjectID,
|
||||
ServiceID: req.ServiceID,
|
||||
TriggeredBy: "api",
|
||||
Branch: req.Branch,
|
||||
Commit: req.Commit,
|
||||
}
|
||||
|
||||
// Validate build request
|
||||
if err := h.buildManager.ValidateBuildRequest(c.Request.Context(), buildReq); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Start build (this would be async in production)
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
_, err := h.buildManager.Build(ctx, buildReq)
|
||||
if err != nil {
|
||||
// Log error or update build status in database
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// For now, return a mock response
|
||||
// In production, this would return the actual build ID and status
|
||||
c.JSON(http.StatusOK, BuildResponse{
|
||||
ID: "build-" + strconv.FormatInt(time.Now().Unix(), 10),
|
||||
Status: "pending",
|
||||
ImageName: req.ImageName,
|
||||
ImageTag: req.ImageTag,
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
|
||||
// GetBuildStatus gets the status of a build
|
||||
// @Summary Get build status
|
||||
// @Description Gets the current status of a build
|
||||
// @Tags builds
|
||||
// @Produce json
|
||||
// @Param id path string true "Build ID"
|
||||
// @Success 200 {object} BuildStatusResponse
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/builds/{id} [get]
|
||||
func (h *BuildHandler) GetBuildStatus(c *gin.Context) {
|
||||
buildID := c.Param("id")
|
||||
|
||||
// For now, return a mock response
|
||||
// In production, this would query the database for the actual build status
|
||||
c.JSON(http.StatusOK, BuildStatusResponse{
|
||||
ID: buildID,
|
||||
Status: "completed",
|
||||
Progress: 100,
|
||||
StartedAt: time.Now().Add(-10 * time.Minute),
|
||||
ImageName: "example-app",
|
||||
ImageTag: "latest",
|
||||
Size: 1024 * 1024 * 100, // 100MB
|
||||
})
|
||||
}
|
||||
|
||||
// ListBuilds lists all builds
|
||||
// @Summary List builds
|
||||
// @Description Lists all builds with optional filtering
|
||||
// @Tags builds
|
||||
// @Produce json
|
||||
// @Param project_id query string false "Filter by project ID"
|
||||
// @Param service_id query string false "Filter by service ID"
|
||||
// @Param status query string false "Filter by status"
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param limit query int false "Items per page" default(20)
|
||||
// @Success 200 {object} BuildListResponse
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/builds [get]
|
||||
func (h *BuildHandler) ListBuilds(c *gin.Context) {
|
||||
projectID := c.Query("project_id")
|
||||
serviceID := c.Query("service_id")
|
||||
status := c.Query("status")
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
|
||||
// For now, return mock data
|
||||
// In production, this would query the database with filters
|
||||
builds := []BuildStatusResponse{
|
||||
{
|
||||
ID: "build-1",
|
||||
ProjectID: projectID,
|
||||
ServiceID: serviceID,
|
||||
Status: status,
|
||||
Progress: 100,
|
||||
StartedAt: time.Now().Add(-1 * time.Hour),
|
||||
ImageName: "example-app",
|
||||
ImageTag: "latest",
|
||||
Size: 1024 * 1024 * 100,
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, BuildListResponse{
|
||||
Builds: builds,
|
||||
Total: len(builds),
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
})
|
||||
}
|
||||
|
||||
// CancelBuild cancels a running build
|
||||
// @Summary Cancel build
|
||||
// @Description Cancels a running build
|
||||
// @Tags builds
|
||||
// @Produce json
|
||||
// @Param id path string true "Build ID"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/builds/{id}/cancel [post]
|
||||
func (h *BuildHandler) CancelBuild(c *gin.Context) {
|
||||
buildID := c.Param("id")
|
||||
|
||||
// For now, just return success
|
||||
// In production, this would actually cancel the build process
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Build " + buildID + " cancelled",
|
||||
})
|
||||
}
|
||||
|
||||
// GetBuildLogs gets the logs for a build
|
||||
// @Summary Get build logs
|
||||
// @Description Gets the build logs for a specific build
|
||||
// @Tags builds
|
||||
// @Produce text
|
||||
// @Param id path string true "Build ID"
|
||||
// @Param follow query bool false "Follow logs" default(false)
|
||||
// @Success 200 {string} string "Build logs"
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/builds/{id}/logs [get]
|
||||
func (h *BuildHandler) GetBuildLogs(c *gin.Context) {
|
||||
buildID := c.Param("id")
|
||||
follow := c.DefaultQuery("follow", "false") == "true"
|
||||
|
||||
// For now, return mock logs
|
||||
// In production, this would stream actual build logs
|
||||
logs := "Build " + buildID + " started\n"
|
||||
logs += "Detecting runtime...\n"
|
||||
logs += "Runtime detected: node\n"
|
||||
logs += "Building image...\n"
|
||||
logs += "Build completed successfully\n"
|
||||
|
||||
if follow {
|
||||
// In production, this would use Server-Sent Events to stream logs
|
||||
c.Header("Content-Type", "text/plain")
|
||||
c.String(http.StatusOK, logs)
|
||||
} else {
|
||||
c.Header("Content-Type", "text/plain")
|
||||
c.String(http.StatusOK, logs)
|
||||
}
|
||||
}
|
||||
|
||||
// GetBuildPlan gets the build plan for a service
|
||||
// @Summary Get build plan
|
||||
// @Description Gets the build plan for a service without actually building
|
||||
// @Tags builds
|
||||
// @Produce json
|
||||
// @Param request body BuildRequest true "Build request"
|
||||
// @Success 200 {object} build.BuildPlan
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/builds/plan [post]
|
||||
func (h *BuildHandler) GetBuildPlan(c *gin.Context) {
|
||||
if h.buildUnavailable(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var req BuildRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to internal build request
|
||||
buildReq := &types.BuildRequest{
|
||||
BuildType: req.BuildType,
|
||||
SourcePath: req.SourcePath,
|
||||
PrebuiltImage: req.PrebuiltImage,
|
||||
ImageName: req.ImageName,
|
||||
ImageTag: req.ImageTag,
|
||||
BuildCommand: req.BuildCommand,
|
||||
StartCommand: req.StartCommand,
|
||||
Environment: req.Environment,
|
||||
BuildArgs: req.BuildArgs,
|
||||
Labels: req.Labels,
|
||||
}
|
||||
|
||||
// Get build plan
|
||||
plan, err := h.buildManager.GetBuildPlan(c.Request.Context(), buildReq)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, plan)
|
||||
}
|
||||
|
||||
// DetectBuildType detects the build type for a given repository
|
||||
// @Summary Detect build type
|
||||
// @Description Detects the build type based on repository contents
|
||||
// @Tags builds
|
||||
// @Produce json
|
||||
// @Param source_path query string true "Source path"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/builds/detect [get]
|
||||
func (h *BuildHandler) DetectBuildType(c *gin.Context) {
|
||||
if h.buildUnavailable(c) {
|
||||
return
|
||||
}
|
||||
|
||||
sourcePath := c.Query("source_path")
|
||||
if sourcePath == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "source_path is required"})
|
||||
return
|
||||
}
|
||||
|
||||
buildType, err := h.buildManager.DetectBuildType(c.Request.Context(), sourcePath)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"build_type": string(buildType),
|
||||
})
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CronJob struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ProjectID string `json:"project_id" db:"project_id"`
|
||||
ServiceID string `json:"service_id" db:"service_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Schedule string `json:"schedule" db:"schedule"`
|
||||
Command string `json:"command" db:"command"`
|
||||
Timezone string `json:"timezone" db:"timezone"`
|
||||
Enabled bool `json:"enabled" db:"enabled"`
|
||||
LastRunAt *time.Time `json:"last_run_at" db:"last_run_at"`
|
||||
NextRunAt *time.Time `json:"next_run_at" db:"next_run_at"`
|
||||
LastStatus string `json:"last_status" db:"last_status"`
|
||||
LastOutput string `json:"last_output" db:"last_output"`
|
||||
Retention int `json:"retention" db:"retention"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
type CronExecution struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
CronJobID string `json:"cron_job_id" db:"cron_job_id"`
|
||||
StartedAt time.Time `json:"started_at" db:"started_at"`
|
||||
FinishedAt *time.Time `json:"finished_at" db:"finished_at"`
|
||||
Status string `json:"status" db:"status"`
|
||||
Output string `json:"output" db:"output"`
|
||||
Error string `json:"error" db:"error"`
|
||||
}
|
||||
|
||||
type CreateCronJobRequest struct {
|
||||
ProjectID string `json:"project_id" binding:"required"`
|
||||
ServiceID string `json:"service_id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Schedule string `json:"schedule" binding:"required"`
|
||||
Command string `json:"command" binding:"required"`
|
||||
Timezone string `json:"timezone"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Retention int `json:"retention"`
|
||||
}
|
||||
|
||||
type UpdateCronJobRequest struct {
|
||||
Name string `json:"name"`
|
||||
Schedule string `json:"schedule"`
|
||||
Command string `json:"command"`
|
||||
Timezone string `json:"timezone"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
Retention int `json:"retention"`
|
||||
}
|
||||
|
||||
func handleGetCronJobs(c *gin.Context) {
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
userID := c.MustGet("user_id").(string)
|
||||
projectID := c.Query("project_id")
|
||||
|
||||
query := `SELECT cj.id, cj.project_id, cj.service_id, cj.name, cj.schedule, cj.timezone,
|
||||
cj.enabled, cj.last_run_at, cj.next_run_at, cj.last_status, cj.last_output,
|
||||
cj.retention, cj.created_at, cj.updated_at
|
||||
FROM cron_jobs cj
|
||||
JOIN projects p ON cj.project_id = p.id
|
||||
WHERE p.owner_id = $1`
|
||||
args := []interface{}{userID}
|
||||
|
||||
if projectID != "" {
|
||||
query += " AND cj.project_id = $2"
|
||||
args = append(args, projectID)
|
||||
}
|
||||
|
||||
query += " ORDER BY cj.created_at DESC"
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch cron jobs"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var jobs []CronJob
|
||||
for rows.Next() {
|
||||
var job CronJob
|
||||
err := rows.Scan(&job.ID, &job.ProjectID, &job.ServiceID, &job.Name, &job.Schedule, &job.Timezone,
|
||||
&job.Enabled, &job.LastRunAt, &job.NextRunAt, &job.LastStatus, &job.LastOutput,
|
||||
&job.Retention, &job.CreatedAt, &job.UpdatedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
jobs = append(jobs, job)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"cron_jobs": jobs})
|
||||
}
|
||||
|
||||
func handleCreateCronJob(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
var req CreateCronJobRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var ownerCheck string
|
||||
err := db.QueryRow(
|
||||
`SELECT p.owner_id FROM projects p
|
||||
JOIN services s ON s.project_id = p.id
|
||||
WHERE s.id = $1`,
|
||||
req.ServiceID,
|
||||
).Scan(&ownerCheck)
|
||||
|
||||
if err != nil || ownerCheck != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Timezone == "" {
|
||||
req.Timezone = "UTC"
|
||||
}
|
||||
if req.Retention == 0 {
|
||||
req.Retention = 30
|
||||
}
|
||||
|
||||
nextRun := calculateNextRun(req.Schedule, req.Timezone)
|
||||
|
||||
job := CronJob{
|
||||
ID: uuid.New().String(),
|
||||
ProjectID: req.ProjectID,
|
||||
ServiceID: req.ServiceID,
|
||||
Name: req.Name,
|
||||
Schedule: req.Schedule,
|
||||
Command: req.Command,
|
||||
Timezone: req.Timezone,
|
||||
Enabled: req.Enabled,
|
||||
NextRunAt: nextRun,
|
||||
Retention: req.Retention,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
_, err = db.Exec(
|
||||
`INSERT INTO cron_jobs (id, project_id, service_id, name, schedule, command, timezone, enabled, next_run_at, retention, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
||||
job.ID, job.ProjectID, job.ServiceID, job.Name, job.Schedule, job.Command, job.Timezone, job.Enabled, job.NextRunAt, job.Retention, job.CreatedAt, job.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create cron job"})
|
||||
return
|
||||
}
|
||||
|
||||
LogAudit(userID, "cron_job", job.ID, "create", map[string]interface{}{
|
||||
"name": job.Name,
|
||||
"schedule": job.Schedule,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"cron_job": job})
|
||||
}
|
||||
|
||||
func handleGetCronJob(c *gin.Context) {
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
userID := c.MustGet("user_id").(string)
|
||||
jobID := c.Param("id")
|
||||
|
||||
var job CronJob
|
||||
var ownerCheck string
|
||||
err := db.QueryRow(
|
||||
`SELECT cj.id, cj.project_id, cj.service_id, cj.name, cj.schedule, cj.timezone,
|
||||
cj.enabled, cj.last_run_at, cj.next_run_at, cj.last_status, cj.last_output,
|
||||
cj.retention, cj.created_at, cj.updated_at, p.owner_id
|
||||
FROM cron_jobs cj
|
||||
JOIN projects p ON cj.project_id = p.id
|
||||
WHERE cj.id = $1`,
|
||||
jobID,
|
||||
).Scan(&job.ID, &job.ProjectID, &job.ServiceID, &job.Name, &job.Schedule, &job.Timezone,
|
||||
&job.Enabled, &job.LastRunAt, &job.NextRunAt, &job.LastStatus, &job.LastOutput,
|
||||
&job.Retention, &job.CreatedAt, &job.UpdatedAt, &ownerCheck)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Cron job not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if ownerCheck != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"cron_job": job})
|
||||
}
|
||||
|
||||
func handleUpdateCronJob(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
jobID := c.Param("id")
|
||||
|
||||
var req UpdateCronJobRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var ownerCheck string
|
||||
err := db.QueryRow(
|
||||
`SELECT p.owner_id FROM cron_jobs cj
|
||||
JOIN projects p ON cj.project_id = p.id
|
||||
WHERE cj.id = $1`,
|
||||
jobID,
|
||||
).Scan(&ownerCheck)
|
||||
|
||||
if err != nil || ownerCheck != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if req.Name != "" {
|
||||
updates["name"] = req.Name
|
||||
}
|
||||
if req.Schedule != "" {
|
||||
updates["schedule"] = req.Schedule
|
||||
updates["next_run_at"] = calculateNextRun(req.Schedule, "UTC")
|
||||
}
|
||||
if req.Command != "" {
|
||||
updates["command"] = req.Command
|
||||
}
|
||||
if req.Timezone != "" {
|
||||
updates["timezone"] = req.Timezone
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
updates["enabled"] = *req.Enabled
|
||||
}
|
||||
if req.Retention > 0 {
|
||||
updates["retention"] = req.Retention
|
||||
}
|
||||
updates["updated_at"] = time.Now()
|
||||
|
||||
updateQuery := "UPDATE cron_jobs SET "
|
||||
args := []interface{}{}
|
||||
argNum := 1
|
||||
for key, value := range updates {
|
||||
if argNum > 1 {
|
||||
updateQuery += ", "
|
||||
}
|
||||
updateQuery += key + " = $" + string(rune('0'+argNum))
|
||||
args = append(args, value)
|
||||
argNum++
|
||||
}
|
||||
updateQuery += " WHERE id = $" + string(rune('0'+argNum))
|
||||
args = append(args, jobID)
|
||||
|
||||
_, err = db.Exec(updateQuery, args...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update cron job"})
|
||||
return
|
||||
}
|
||||
|
||||
LogAudit(userID, "cron_job", jobID, "update", updates)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Cron job updated successfully"})
|
||||
}
|
||||
|
||||
func handleDeleteCronJob(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
jobID := c.Param("id")
|
||||
|
||||
var ownerCheck string
|
||||
err := db.QueryRow(
|
||||
`SELECT p.owner_id FROM cron_jobs cj
|
||||
JOIN projects p ON cj.project_id = p.id
|
||||
WHERE cj.id = $1`,
|
||||
jobID,
|
||||
).Scan(&ownerCheck)
|
||||
|
||||
if err != nil || ownerCheck != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.Exec("DELETE FROM cron_jobs WHERE id = $1", jobID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete cron job"})
|
||||
return
|
||||
}
|
||||
|
||||
LogAudit(userID, "cron_job", jobID, "delete", nil)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Cron job deleted successfully"})
|
||||
}
|
||||
|
||||
func handleGetCronExecutions(c *gin.Context) {
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
userID := c.MustGet("user_id").(string)
|
||||
jobID := c.Param("id")
|
||||
|
||||
var ownerCheck string
|
||||
err := db.QueryRow(
|
||||
`SELECT p.owner_id FROM cron_jobs cj
|
||||
JOIN projects p ON cj.project_id = p.id
|
||||
WHERE cj.id = $1`,
|
||||
jobID,
|
||||
).Scan(&ownerCheck)
|
||||
|
||||
if err != nil || ownerCheck != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := db.Query(
|
||||
`SELECT id, cron_job_id, started_at, finished_at, status, output, error
|
||||
FROM cron_executions
|
||||
WHERE cron_job_id = $1
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 100`,
|
||||
jobID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch executions"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var executions []CronExecution
|
||||
for rows.Next() {
|
||||
var exec CronExecution
|
||||
err := rows.Scan(&exec.ID, &exec.CronJobID, &exec.StartedAt, &exec.FinishedAt, &exec.Status, &exec.Output, &exec.Error)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
executions = append(executions, exec)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"executions": executions})
|
||||
}
|
||||
|
||||
func handleTriggerCronJob(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
jobID := c.Param("id")
|
||||
|
||||
var job CronJob
|
||||
var ownerCheck string
|
||||
err := db.QueryRow(
|
||||
`SELECT cj.command, p.owner_id FROM cron_jobs cj
|
||||
JOIN projects p ON cj.project_id = p.id
|
||||
WHERE cj.id = $1`,
|
||||
jobID,
|
||||
).Scan(&job.Command, &ownerCheck)
|
||||
|
||||
if err != nil || ownerCheck != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
execID := uuid.New().String()
|
||||
now := time.Now()
|
||||
|
||||
_, err = db.Exec(
|
||||
`INSERT INTO cron_executions (id, cron_job_id, started_at, status)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
execID, jobID, now, "running",
|
||||
)
|
||||
|
||||
go executeCronJob(jobID, execID, job.Command)
|
||||
|
||||
LogAudit(userID, "cron_job", jobID, "trigger", map[string]interface{}{
|
||||
"execution_id": execID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Cron job triggered",
|
||||
"execution_id": execID,
|
||||
})
|
||||
}
|
||||
|
||||
func calculateNextRun(schedule, timezone string) *time.Time {
|
||||
now := time.Now()
|
||||
next := now.Add(1 * time.Hour)
|
||||
return &next
|
||||
}
|
||||
|
||||
func executeCronJob(jobID, execID, command string) {
|
||||
db := auditDB
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
now := time.Now()
|
||||
db.Exec(
|
||||
`UPDATE cron_executions SET finished_at = $1, status = $2, output = $3 WHERE id = $4`,
|
||||
now, "success", "Job completed successfully", execID,
|
||||
)
|
||||
|
||||
db.Exec(
|
||||
`UPDATE cron_jobs SET last_run_at = $1, last_status = $2, next_run_at = $3 WHERE id = $4`,
|
||||
now, "success", time.Now().Add(1*time.Hour), jobID,
|
||||
)
|
||||
}
|
||||
|
||||
func init() {
|
||||
cronJobsData, _ := json.Marshal([]CronJob{})
|
||||
_ = cronJobsData
|
||||
}
|
||||
@@ -1,543 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// DatabaseService represents a managed database service
|
||||
type DatabaseService struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Type string `json:"type" db:"type"` // postgresql, redis, mysql
|
||||
Status string `json:"status" db:"status"` // running, stopped, building, error
|
||||
Version string `json:"version" db:"version"`
|
||||
Plan string `json:"plan" db:"plan"` // hobby, starter, standard, business
|
||||
Region string `json:"region" db:"region"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
ConnectionURL string `json:"connection_url"`
|
||||
Metrics DatabaseMetrics `json:"metrics"`
|
||||
Backups DatabaseBackupConfig `json:"backups"`
|
||||
Settings DatabaseSettings `json:"settings"`
|
||||
}
|
||||
|
||||
// DatabaseMetrics represents database performance metrics
|
||||
type DatabaseMetrics struct {
|
||||
CPU float64 `json:"cpu"`
|
||||
Memory float64 `json:"memory"`
|
||||
Storage float64 `json:"storage"`
|
||||
Connections int `json:"connections"`
|
||||
ReadIOPS int `json:"read_iops"`
|
||||
WriteIOPS int `json:"write_iops"`
|
||||
NetworkIn float64 `json:"network_in"`
|
||||
NetworkOut float64 `json:"network_out"`
|
||||
}
|
||||
|
||||
// DatabaseBackupConfig represents backup configuration
|
||||
type DatabaseBackupConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
LastBackup *time.Time `json:"last_backup,omitempty"`
|
||||
Retention int `json:"retention"` // days
|
||||
NextBackup *time.Time `json:"next_backup,omitempty"`
|
||||
Backups []DatabaseBackup `json:"backups"`
|
||||
}
|
||||
|
||||
// DatabaseBackup represents a single backup
|
||||
type DatabaseBackup struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Size string `json:"size"`
|
||||
Status string `json:"status"` // completed, failed, in_progress
|
||||
}
|
||||
|
||||
// DatabaseSettings represents database configuration
|
||||
type DatabaseSettings struct {
|
||||
MaxConnections int `json:"max_connections"`
|
||||
Timeout int `json:"timeout"` // seconds
|
||||
SSL bool `json:"ssl"`
|
||||
Logging bool `json:"logging"`
|
||||
}
|
||||
|
||||
// DatabaseCreateRequest represents a request to create a new database
|
||||
type DatabaseCreateRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required,oneof=postgresql redis mysql"`
|
||||
Plan string `json:"plan" binding:"required,oneof=hobby starter standard business"`
|
||||
Region string `json:"region" binding:"required"`
|
||||
}
|
||||
|
||||
// DatabaseUpdateRequest represents a request to update a database
|
||||
type DatabaseUpdateRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Plan string `json:"plan,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseActionRequest represents a request to perform database actions
|
||||
type DatabaseActionRequest struct {
|
||||
Action string `json:"action" binding:"required,oneof=start stop restart"`
|
||||
}
|
||||
|
||||
// DatabaseBackupRequest represents a request to create a backup
|
||||
type DatabaseBackupRequest struct {
|
||||
DatabaseID string `json:"database_id" binding:"required"`
|
||||
}
|
||||
|
||||
// DatabaseRestoreRequest represents a request to restore from backup
|
||||
type DatabaseRestoreRequest struct {
|
||||
DatabaseID string `json:"database_id" binding:"required"`
|
||||
BackupID string `json:"backup_id" binding:"required"`
|
||||
}
|
||||
|
||||
// DatabaseHandler handles database service operations
|
||||
type DatabaseHandler struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewDatabaseHandler creates a new database handler
|
||||
func NewDatabaseHandler(db *sql.DB) *DatabaseHandler {
|
||||
return &DatabaseHandler{db: db}
|
||||
}
|
||||
|
||||
// GetDatabases returns all database services for a user
|
||||
func (h *DatabaseHandler) GetDatabases(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
|
||||
query := `
|
||||
SELECT id, name, type, status, version, plan, region, created_at, updated_at
|
||||
FROM database_services
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := h.db.Query(query, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch databases"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var databases []DatabaseService
|
||||
for rows.Next() {
|
||||
var db DatabaseService
|
||||
err := rows.Scan(
|
||||
&db.ID, &db.Name, &db.Type, &db.Status, &db.Version,
|
||||
&db.Plan, &db.Region, &db.CreatedAt, &db.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan database"})
|
||||
return
|
||||
}
|
||||
|
||||
// Add mock metrics and configuration
|
||||
db.Metrics = h.generateMockMetrics()
|
||||
db.Backups = h.generateMockBackupConfig()
|
||||
db.Settings = h.generateMockSettings()
|
||||
db.ConnectionURL = h.generateConnectionURL(db)
|
||||
|
||||
databases = append(databases, db)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"databases": databases})
|
||||
}
|
||||
|
||||
// GetDatabase returns a specific database service
|
||||
func (h *DatabaseHandler) GetDatabase(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
databaseID := c.Param("id")
|
||||
|
||||
query := `
|
||||
SELECT id, name, type, status, version, plan, region, created_at, updated_at
|
||||
FROM database_services
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`
|
||||
|
||||
var db DatabaseService
|
||||
err := h.db.QueryRow(query, databaseID, userID).Scan(
|
||||
&db.ID, &db.Name, &db.Type, &db.Status, &db.Version,
|
||||
&db.Plan, &db.Region, &db.CreatedAt, &db.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch database"})
|
||||
return
|
||||
}
|
||||
|
||||
// Add detailed metrics and configuration
|
||||
db.Metrics = h.generateMockMetrics()
|
||||
db.Backups = h.generateMockBackupConfig()
|
||||
db.Settings = h.generateMockSettings()
|
||||
db.ConnectionURL = h.generateConnectionURL(db)
|
||||
|
||||
c.JSON(http.StatusOK, db)
|
||||
}
|
||||
|
||||
// CreateDatabase creates a new database service
|
||||
func (h *DatabaseHandler) CreateDatabase(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
|
||||
var req DatabaseCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate database ID
|
||||
databaseID := fmt.Sprintf("db_%d_%s", time.Now().Unix(), req.Name)
|
||||
|
||||
// Insert database into database
|
||||
query := `
|
||||
INSERT INTO database_services (id, user_id, name, type, status, version, plan, region, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
`
|
||||
|
||||
now := time.Now()
|
||||
version := h.getDefaultVersion(req.Type)
|
||||
|
||||
_, err := h.db.Exec(query, databaseID, userID, req.Name, req.Type, "building", version, req.Plan, req.Region, now, now)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create database"})
|
||||
return
|
||||
}
|
||||
|
||||
// In a real implementation, this would trigger the actual database provisioning
|
||||
// For now, we'll simulate it by updating the status to "running"
|
||||
go h.provisionDatabase(databaseID)
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": databaseID,
|
||||
"message": "Database provisioning started",
|
||||
"status": "building",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateDatabase updates a database service
|
||||
func (h *DatabaseHandler) UpdateDatabase(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
databaseID := c.Param("id")
|
||||
|
||||
var req DatabaseUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Build dynamic update query
|
||||
setParts := []string{}
|
||||
args := []interface{}{}
|
||||
argIndex := 1
|
||||
|
||||
if req.Name != "" {
|
||||
setParts = append(setParts, fmt.Sprintf("name = $%d", argIndex))
|
||||
args = append(args, req.Name)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if req.Plan != "" {
|
||||
setParts = append(setParts, fmt.Sprintf("plan = $%d", argIndex))
|
||||
args = append(args, req.Plan)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if len(setParts) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No fields to update"})
|
||||
return
|
||||
}
|
||||
|
||||
setParts = append(setParts, fmt.Sprintf("updated_at = $%d", argIndex))
|
||||
args = append(args, time.Now())
|
||||
argIndex++
|
||||
|
||||
args = append(args, databaseID, userID)
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
UPDATE database_services
|
||||
SET %s
|
||||
WHERE id = $%d AND user_id = $%d
|
||||
`, fmt.Sprintf("%s", setParts), argIndex, argIndex+1)
|
||||
|
||||
_, err := h.db.Exec(query, args...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update database"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Database updated successfully"})
|
||||
}
|
||||
|
||||
// DeleteDatabase deletes a database service
|
||||
func (h *DatabaseHandler) DeleteDatabase(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
databaseID := c.Param("id")
|
||||
|
||||
// Check if database exists and belongs to user
|
||||
var exists bool
|
||||
checkQuery := "SELECT EXISTS(SELECT 1 FROM database_services WHERE id = $1 AND user_id = $2)"
|
||||
err := h.db.QueryRow(checkQuery, databaseID, userID).Scan(&exists)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check database"})
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// In a real implementation, this would trigger the actual database deprovisioning
|
||||
// For now, we'll just delete the record
|
||||
deleteQuery := "DELETE FROM database_services WHERE id = $1 AND user_id = $2"
|
||||
_, err = h.db.Exec(deleteQuery, databaseID, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete database"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Database deleted successfully"})
|
||||
}
|
||||
|
||||
// PerformDatabaseAction performs actions on a database (start, stop, restart)
|
||||
func (h *DatabaseHandler) PerformDatabaseAction(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
databaseID := c.Param("id")
|
||||
|
||||
var req DatabaseActionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if database exists and belongs to user
|
||||
var exists bool
|
||||
checkQuery := "SELECT EXISTS(SELECT 1 FROM database_services WHERE id = $1 AND user_id = $2)"
|
||||
err := h.db.QueryRow(checkQuery, databaseID, userID).Scan(&exists)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check database"})
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update database status based on action
|
||||
var newStatus string
|
||||
switch req.Action {
|
||||
case "start":
|
||||
newStatus = "running"
|
||||
case "stop":
|
||||
newStatus = "stopped"
|
||||
case "restart":
|
||||
newStatus = "building" // Will be updated to running after restart
|
||||
go h.restartDatabase(databaseID)
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid action"})
|
||||
return
|
||||
}
|
||||
|
||||
updateQuery := "UPDATE database_services SET status = $1, updated_at = $2 WHERE id = $3 AND user_id = $4"
|
||||
_, err = h.db.Exec(updateQuery, newStatus, time.Now(), databaseID, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update database status"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": fmt.Sprintf("Database %s initiated", req.Action),
|
||||
"status": newStatus,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateBackup creates a backup of a database
|
||||
func (h *DatabaseHandler) CreateBackup(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
databaseID := c.Param("id")
|
||||
|
||||
// Check if database exists and belongs to user
|
||||
var exists bool
|
||||
checkQuery := "SELECT EXISTS(SELECT 1 FROM database_services WHERE id = $1 AND user_id = $2)"
|
||||
err := h.db.QueryRow(checkQuery, databaseID, userID).Scan(&exists)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check database"})
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate backup ID
|
||||
backupID := fmt.Sprintf("backup_%d_%s", time.Now().Unix(), databaseID)
|
||||
|
||||
// In a real implementation, this would trigger the actual backup process
|
||||
// For now, we'll simulate it
|
||||
go h.createBackupProcess(databaseID, backupID)
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"backup_id": backupID,
|
||||
"message": "Backup creation started",
|
||||
"status": "in_progress",
|
||||
})
|
||||
}
|
||||
|
||||
// RestoreBackup restores a database from a backup
|
||||
func (h *DatabaseHandler) RestoreBackup(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
databaseID := c.Param("id")
|
||||
|
||||
var req DatabaseRestoreRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if database exists and belongs to user
|
||||
var exists bool
|
||||
checkQuery := "SELECT EXISTS(SELECT 1 FROM database_services WHERE id = $1 AND user_id = $2)"
|
||||
err := h.db.QueryRow(checkQuery, databaseID, userID).Scan(&exists)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check database"})
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Database not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// In a real implementation, this would trigger the actual restore process
|
||||
// For now, we'll simulate it
|
||||
go h.restoreBackupProcess(databaseID, req.BackupID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Database restore started",
|
||||
"status": "in_progress",
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions for mock data generation
|
||||
|
||||
func (h *DatabaseHandler) generateMockMetrics() DatabaseMetrics {
|
||||
return DatabaseMetrics{
|
||||
CPU: 25.0 + (float64(time.Now().Unix() % 50)),
|
||||
Memory: 60.0 + (float64(time.Now().Unix() % 30)),
|
||||
Storage: 45.0 + (float64(time.Now().Unix() % 40)),
|
||||
Connections: 10 + (int(time.Now().Unix() % 20)),
|
||||
ReadIOPS: 150 + (int(time.Now().Unix() % 100)),
|
||||
WriteIOPS: 80 + (int(time.Now().Unix() % 50)),
|
||||
NetworkIn: 2.5 + (float64(time.Now().Unix()%10))/10,
|
||||
NetworkOut: 1.8 + (float64(time.Now().Unix()%8))/10,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) generateMockBackupConfig() DatabaseBackupConfig {
|
||||
return DatabaseBackupConfig{
|
||||
Enabled: true,
|
||||
LastBackup: &time.Time{},
|
||||
Retention: 30,
|
||||
NextBackup: &time.Time{},
|
||||
Backups: []DatabaseBackup{
|
||||
{
|
||||
ID: "backup_1",
|
||||
CreatedAt: time.Now().Add(-6 * time.Hour),
|
||||
Size: "245 MB",
|
||||
Status: "completed",
|
||||
},
|
||||
{
|
||||
ID: "backup_2",
|
||||
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||
Size: "238 MB",
|
||||
Status: "completed",
|
||||
},
|
||||
{
|
||||
ID: "backup_3",
|
||||
CreatedAt: time.Now().Add(-48 * time.Hour),
|
||||
Size: "241 MB",
|
||||
Status: "completed",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) generateMockSettings() DatabaseSettings {
|
||||
return DatabaseSettings{
|
||||
MaxConnections: 100,
|
||||
Timeout: 30,
|
||||
SSL: true,
|
||||
Logging: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) generateConnectionURL(db DatabaseService) string {
|
||||
switch db.Type {
|
||||
case "postgresql":
|
||||
return fmt.Sprintf("postgresql://user:password@%s.containr.local:5432/%s", db.Name, db.Name)
|
||||
case "redis":
|
||||
return fmt.Sprintf("redis://%s.containr.local:6379", db.Name)
|
||||
case "mysql":
|
||||
return fmt.Sprintf("mysql://user:password@%s.containr.local:3306/%s", db.Name, db.Name)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) getDefaultVersion(dbType string) string {
|
||||
switch dbType {
|
||||
case "postgresql":
|
||||
return "15.4"
|
||||
case "redis":
|
||||
return "7.2"
|
||||
case "mysql":
|
||||
return "8.0"
|
||||
default:
|
||||
return "latest"
|
||||
}
|
||||
}
|
||||
|
||||
// Mock provisioning functions (in real implementation, these would interact with container orchestration)
|
||||
|
||||
func (h *DatabaseHandler) provisionDatabase(databaseID string) {
|
||||
// Simulate provisioning time
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
// Update status to running
|
||||
query := "UPDATE database_services SET status = 'running', updated_at = $1 WHERE id = $2"
|
||||
h.db.Exec(query, time.Now(), databaseID)
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) restartDatabase(databaseID string) {
|
||||
// Simulate restart time
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
// Update status to running
|
||||
query := "UPDATE database_services SET status = 'running', updated_at = $1 WHERE id = $2"
|
||||
h.db.Exec(query, time.Now(), databaseID)
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) createBackupProcess(databaseID, backupID string) {
|
||||
// Simulate backup creation time
|
||||
time.Sleep(5 * time.Minute)
|
||||
|
||||
// In a real implementation, this would store backup metadata
|
||||
// For now, we'll just log it
|
||||
fmt.Printf("Backup %s created for database %s\n", backupID, databaseID)
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) restoreBackupProcess(databaseID, backupID string) {
|
||||
// Simulate restore time
|
||||
time.Sleep(10 * time.Minute)
|
||||
|
||||
// In a real implementation, this would restore the database
|
||||
// For now, we'll just log it
|
||||
fmt.Printf("Database %s restored from backup %s\n", databaseID, backupID)
|
||||
}
|
||||
@@ -1,569 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"containr/internal/deployment"
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type DeploymentModel struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
ServiceID uuid.UUID `json:"service_id" db:"service_id"`
|
||||
CommitHash *string `json:"commit_hash" db:"commit_hash"`
|
||||
Status string `json:"status" db:"status"`
|
||||
ImageName string `json:"image_name" db:"image_name"`
|
||||
ImageTag string `json:"image_tag" db:"image_tag"`
|
||||
BuildLog string `json:"build_log" db:"build_log"`
|
||||
RuntimeLog string `json:"runtime_log" db:"runtime_log"`
|
||||
Error *string `json:"error" db:"error"`
|
||||
StartedAt *time.Time `json:"started_at" db:"started_at"`
|
||||
CompletedAt *time.Time `json:"completed_at" db:"completed_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
type CreateDeploymentRequest struct {
|
||||
CommitHash string `json:"commit_hash"`
|
||||
Branch string `json:"branch"`
|
||||
Trigger string `json:"trigger"`
|
||||
EnvVars map[string]string `json:"env_vars"`
|
||||
}
|
||||
|
||||
type DeploymentResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ServiceID uuid.UUID `json:"service_id"`
|
||||
CommitHash *string `json:"commit_hash"`
|
||||
Status string `json:"status"`
|
||||
ImageName string `json:"image_name"`
|
||||
ImageTag string `json:"image_tag"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func handleGetDeployments(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
serviceIDStr := c.Param("id")
|
||||
serviceID, err := uuid.Parse(serviceIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"})
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var ownerCheck string
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT p.owner_id FROM services s
|
||||
JOIN projects p ON s.project_id = p.id
|
||||
WHERE s.id = $1`,
|
||||
serviceID,
|
||||
).Scan(&ownerCheck)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if ownerCheck != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := db.(*database.DB).Query(
|
||||
`SELECT id, service_id, commit_hash, status, image_name, image_tag,
|
||||
build_log, runtime_log, error, started_at, completed_at, created_at, updated_at
|
||||
FROM deployments
|
||||
WHERE service_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 50`,
|
||||
serviceID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var deployments []DeploymentModel
|
||||
for rows.Next() {
|
||||
var d DeploymentModel
|
||||
err := rows.Scan(
|
||||
&d.ID, &d.ServiceID, &d.CommitHash, &d.Status, &d.ImageName, &d.ImageTag,
|
||||
&d.BuildLog, &d.RuntimeLog, &d.Error, &d.StartedAt, &d.CompletedAt,
|
||||
&d.CreatedAt, &d.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan deployment"})
|
||||
return
|
||||
}
|
||||
deployments = append(deployments, d)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"deployments": deployments})
|
||||
}
|
||||
|
||||
func handleCreateDeployment(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
serviceIDStr := c.Param("id")
|
||||
serviceID, err := uuid.Parse(serviceIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateDeploymentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Trigger == "" {
|
||||
req.Trigger = "manual"
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var service Service
|
||||
var projectOwner string
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
|
||||
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
|
||||
s.created_at, s.updated_at, p.owner_id
|
||||
FROM services s
|
||||
JOIN projects p ON s.project_id = p.id
|
||||
WHERE s.id = $1`,
|
||||
serviceID,
|
||||
).Scan(
|
||||
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
|
||||
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
|
||||
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
|
||||
&service.CreatedAt, &service.UpdatedAt, &projectOwner,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if projectOwner != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Branch == "" {
|
||||
req.Branch = service.GitBranch
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var commitHash *string
|
||||
if trimmed := strings.TrimSpace(req.CommitHash); trimmed != "" {
|
||||
commitHash = &trimmed
|
||||
}
|
||||
|
||||
d := DeploymentModel{
|
||||
ID: uuid.New(),
|
||||
ServiceID: serviceID,
|
||||
CommitHash: commitHash,
|
||||
Status: "pending",
|
||||
ImageName: "",
|
||||
ImageTag: "",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
_, err = db.(*database.DB).Exec(
|
||||
`INSERT INTO deployments
|
||||
(id, service_id, commit_hash, status, image_name, image_tag, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||
d.ID, d.ServiceID, d.CommitHash, d.Status, d.ImageName, d.ImageTag, d.CreatedAt, d.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create deployment"})
|
||||
return
|
||||
}
|
||||
|
||||
engine, exists := c.Get("deployment_engine")
|
||||
if !exists || engine == nil {
|
||||
unavailableErr := "Deployment engine unavailable. Docker may not be configured on this server."
|
||||
completedAt := time.Now()
|
||||
_, _ = db.(*database.DB).Exec(
|
||||
`UPDATE deployments
|
||||
SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
|
||||
WHERE id = $3`,
|
||||
unavailableErr, completedAt, d.ID,
|
||||
)
|
||||
d.Status = "failed"
|
||||
d.Error = &unavailableErr
|
||||
d.CompletedAt = &completedAt
|
||||
} else {
|
||||
_, err = db.(*database.DB).Exec(
|
||||
`UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`,
|
||||
time.Now(), serviceID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update service status"})
|
||||
return
|
||||
}
|
||||
|
||||
engineInstance := engine.(*deployment.DeploymentEngine)
|
||||
go runDeploymentAndSync(context.Background(), db.(*database.DB), engineInstance, &d, service, req, userID.(string))
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, DeploymentResponse{
|
||||
ID: d.ID,
|
||||
ServiceID: d.ServiceID,
|
||||
CommitHash: d.CommitHash,
|
||||
Status: d.Status,
|
||||
Error: d.Error,
|
||||
CompletedAt: d.CompletedAt,
|
||||
CreatedAt: d.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func runDeploymentAndSync(
|
||||
parentCtx context.Context,
|
||||
db *database.DB,
|
||||
engine *deployment.DeploymentEngine,
|
||||
dbDeployment *DeploymentModel,
|
||||
service Service,
|
||||
req CreateDeploymentRequest,
|
||||
userID string,
|
||||
) {
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 30*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
sourcePath := strings.TrimSpace(service.BuildPath)
|
||||
if sourcePath == "" {
|
||||
sourcePath = "."
|
||||
}
|
||||
|
||||
deployReq := &deployment.DeploymentRequest{
|
||||
ProjectID: service.ProjectID.String(),
|
||||
ServiceID: service.ID.String(),
|
||||
Environment: service.Environment,
|
||||
Config: deployment.ServiceConfig{
|
||||
Name: service.Name,
|
||||
Image: service.Image,
|
||||
Environment: req.EnvVars,
|
||||
Replicas: 1,
|
||||
},
|
||||
BuildConfig: &deployment.BuildConfig{
|
||||
BuildType: "nixpacks",
|
||||
SourcePath: sourcePath,
|
||||
Branch: req.Branch,
|
||||
Commit: req.CommitHash,
|
||||
},
|
||||
Trigger: deployment.TriggerConfig{
|
||||
Type: req.Trigger,
|
||||
Source: "api",
|
||||
User: userID,
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
engineDeployment, err := engine.Deploy(ctx, deployReq)
|
||||
if err != nil {
|
||||
failedAt := time.Now()
|
||||
failure := "Failed to start deployment engine: " + err.Error()
|
||||
_, _ = db.Exec(
|
||||
`UPDATE deployments
|
||||
SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
|
||||
WHERE id = $3`,
|
||||
failure, failedAt, dbDeployment.ID,
|
||||
)
|
||||
_, _ = db.Exec(
|
||||
`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`,
|
||||
failedAt, service.ID,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
syncTicker := time.NewTicker(1 * time.Second)
|
||||
defer syncTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
failedAt := time.Now()
|
||||
timeoutErr := "Deployment timed out before completion"
|
||||
_, _ = db.Exec(
|
||||
`UPDATE deployments
|
||||
SET status = 'failed', error = $1, completed_at = $2, updated_at = $2
|
||||
WHERE id = $3`,
|
||||
timeoutErr, failedAt, dbDeployment.ID,
|
||||
)
|
||||
_, _ = db.Exec(
|
||||
`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`,
|
||||
failedAt, service.ID,
|
||||
)
|
||||
return
|
||||
case <-syncTicker.C:
|
||||
current, getErr := engine.GetDeployment(engineDeployment.ID)
|
||||
if getErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dbStatus := mapEngineStatusToDBStatus(current.Status)
|
||||
imageName, imageTag := splitImageReference(current.ImageName, dbDeployment.ImageTag)
|
||||
|
||||
var dbError interface{}
|
||||
if current.Error != "" {
|
||||
dbError = current.Error
|
||||
}
|
||||
|
||||
_, _ = db.Exec(
|
||||
`UPDATE deployments
|
||||
SET status = $1,
|
||||
image_name = $2,
|
||||
image_tag = $3,
|
||||
build_log = $4,
|
||||
runtime_log = $5,
|
||||
error = $6,
|
||||
started_at = $7,
|
||||
completed_at = $8,
|
||||
updated_at = $9
|
||||
WHERE id = $10`,
|
||||
dbStatus,
|
||||
imageName,
|
||||
imageTag,
|
||||
current.BuildLog,
|
||||
current.DeployLog,
|
||||
dbError,
|
||||
current.StartedAt,
|
||||
current.CompletedAt,
|
||||
time.Now(),
|
||||
dbDeployment.ID,
|
||||
)
|
||||
|
||||
switch dbStatus {
|
||||
case "deployed":
|
||||
_, _ = db.Exec(
|
||||
`UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`,
|
||||
time.Now(), service.ID,
|
||||
)
|
||||
return
|
||||
case "failed":
|
||||
_, _ = db.Exec(
|
||||
`UPDATE services SET status = 'failed', updated_at = $1 WHERE id = $2`,
|
||||
time.Now(), service.ID,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mapEngineStatusToDBStatus(status string) string {
|
||||
switch status {
|
||||
case "running":
|
||||
return "deployed"
|
||||
case "cancelled":
|
||||
return "failed"
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
func splitImageReference(image, fallbackTag string) (string, string) {
|
||||
if image == "" {
|
||||
return "", fallbackTag
|
||||
}
|
||||
|
||||
lastSlash := strings.LastIndex(image, "/")
|
||||
lastColon := strings.LastIndex(image, ":")
|
||||
if lastColon > lastSlash && !strings.Contains(image[lastColon:], "@") {
|
||||
return image[:lastColon], image[lastColon+1:]
|
||||
}
|
||||
|
||||
return image, fallbackTag
|
||||
}
|
||||
|
||||
func handleGetDeployment(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
deploymentIDStr := c.Param("id")
|
||||
deploymentID, err := uuid.Parse(deploymentIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid deployment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var d DeploymentModel
|
||||
var ownerCheck string
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
|
||||
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
|
||||
d.created_at, d.updated_at, p.owner_id
|
||||
FROM deployments d
|
||||
JOIN services s ON d.service_id = s.id
|
||||
JOIN projects p ON s.project_id = p.id
|
||||
WHERE d.id = $1`,
|
||||
deploymentID,
|
||||
).Scan(
|
||||
&d.ID, &d.ServiceID, &d.CommitHash, &d.Status, &d.ImageName, &d.ImageTag,
|
||||
&d.BuildLog, &d.RuntimeLog, &d.Error, &d.StartedAt, &d.CompletedAt,
|
||||
&d.CreatedAt, &d.UpdatedAt, &ownerCheck,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if ownerCheck != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"deployment": d})
|
||||
}
|
||||
|
||||
func handleRollbackDeployment(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
deploymentIDStr := c.Param("id")
|
||||
deploymentID, err := uuid.Parse(deploymentIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid deployment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var targetDeployment DeploymentModel
|
||||
var serviceID uuid.UUID
|
||||
var ownerCheck string
|
||||
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT d.id, d.service_id, d.commit_hash, d.status, d.image_name, d.image_tag,
|
||||
d.build_log, d.runtime_log, d.error, d.started_at, d.completed_at,
|
||||
d.created_at, d.updated_at, p.owner_id
|
||||
FROM deployments d
|
||||
JOIN services s ON d.service_id = s.id
|
||||
JOIN projects p ON s.project_id = p.id
|
||||
WHERE d.id = $1`,
|
||||
deploymentID,
|
||||
).Scan(
|
||||
&targetDeployment.ID, &serviceID, &targetDeployment.CommitHash, &targetDeployment.Status,
|
||||
&targetDeployment.ImageName, &targetDeployment.ImageTag, &targetDeployment.BuildLog,
|
||||
&targetDeployment.RuntimeLog, &targetDeployment.Error, &targetDeployment.StartedAt,
|
||||
&targetDeployment.CompletedAt, &targetDeployment.CreatedAt, &targetDeployment.UpdatedAt,
|
||||
&ownerCheck,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if ownerCheck != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
if targetDeployment.Status != "deployed" && targetDeployment.Status != "failed" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Can only rollback completed or failed deployments"})
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
rollbackID := uuid.New()
|
||||
rollback := DeploymentModel{
|
||||
ID: rollbackID,
|
||||
ServiceID: serviceID,
|
||||
CommitHash: targetDeployment.CommitHash,
|
||||
Status: "rolling_back",
|
||||
ImageName: targetDeployment.ImageName,
|
||||
ImageTag: targetDeployment.ImageTag,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
_, err = db.(*database.DB).Exec(
|
||||
`INSERT INTO deployments
|
||||
(id, service_id, commit_hash, status, image_name, image_tag, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||
rollback.ID, rollback.ServiceID, rollback.CommitHash, rollback.Status,
|
||||
rollback.ImageName, rollback.ImageTag, rollback.CreatedAt, rollback.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create rollback deployment"})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.(*database.DB).Exec(
|
||||
`UPDATE services SET status = 'building', updated_at = $1 WHERE id = $2`,
|
||||
time.Now(), serviceID,
|
||||
)
|
||||
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
db.(*database.DB).Exec(
|
||||
`UPDATE deployments SET status = 'deployed', completed_at = $1, updated_at = $1 WHERE id = $2`,
|
||||
time.Now(), rollbackID,
|
||||
)
|
||||
db.(*database.DB).Exec(
|
||||
`UPDATE services SET status = 'running', updated_at = $1 WHERE id = $2`,
|
||||
time.Now(), serviceID,
|
||||
)
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"deployment": DeploymentResponse{
|
||||
ID: rollback.ID,
|
||||
ServiceID: rollback.ServiceID,
|
||||
CommitHash: rollback.CommitHash,
|
||||
Status: rollback.Status,
|
||||
ImageName: rollback.ImageName,
|
||||
ImageTag: rollback.ImageTag,
|
||||
CreatedAt: rollback.CreatedAt,
|
||||
},
|
||||
"message": "Rollback initiated",
|
||||
})
|
||||
}
|
||||
@@ -1,458 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// GitProvider represents a Git provider (GitHub, GitLab, Bitbucket)
|
||||
type GitProvider struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"` // github, gitlab, bitbucket
|
||||
DisplayName string `json:"display_name" db:"display_name"`
|
||||
APIUrl string `json:"api_url" db:"api_url"`
|
||||
WebhookUrl string `json:"webhook_url" db:"webhook_url"`
|
||||
UserID string `json:"user_id" db:"user_id"`
|
||||
AccessToken string `json:"-" db:"access_token"` // Hidden in JSON responses
|
||||
CreatedAt string `json:"created_at" db:"created_at"`
|
||||
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// GitRepository represents a connected Git repository
|
||||
type GitRepository struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ProviderID string `json:"provider_id" db:"provider_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
FullName string `json:"full_name" db:"full_name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
CloneURL string `json:"clone_url" db:"clone_url"`
|
||||
WebhookURL string `json:"webhook_url" db:"webhook_url"`
|
||||
DefaultBranch string `json:"default_branch" db:"default_branch"`
|
||||
IsPrivate bool `json:"is_private" db:"is_private"`
|
||||
UserID string `json:"user_id" db:"user_id"`
|
||||
CreatedAt string `json:"created_at" db:"created_at"`
|
||||
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// GitWebhook represents a webhook configuration
|
||||
type GitWebhook struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
RepoID string `json:"repo_id" db:"repo_id"`
|
||||
ProviderID string `json:"provider_id" db:"provider_id"`
|
||||
Events string `json:"events" db:"events"` // JSON array of events
|
||||
Secret string `json:"-" db:"webhook_secret"` // Hidden in JSON responses
|
||||
Active bool `json:"active" db:"active"`
|
||||
CreatedAt string `json:"created_at" db:"created_at"`
|
||||
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateGitProviderRequest represents a request to create a Git provider
|
||||
type CreateGitProviderRequest struct {
|
||||
Name string `json:"name" binding:"required,oneof=github gitlab bitbucket"`
|
||||
DisplayName string `json:"display_name" binding:"required"`
|
||||
AccessToken string `json:"access_token" binding:"required"`
|
||||
}
|
||||
|
||||
// CreateGitRepoRequest represents a request to connect a Git repository
|
||||
type CreateGitRepoRequest struct {
|
||||
ProviderID string `json:"provider_id" binding:"required"`
|
||||
RepoFullName string `json:"repo_full_name" binding:"required"`
|
||||
}
|
||||
|
||||
// CreateWebhookRequest represents a request to create a webhook
|
||||
type CreateWebhookRequest struct {
|
||||
RepoID string `json:"repo_id" binding:"required"`
|
||||
Events []string `json:"events" binding:"required"`
|
||||
Branch string `json:"branch"`
|
||||
}
|
||||
|
||||
func handleGetGitProviders(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT id, name, display_name, api_url, webhook_url, user_id, created_at, updated_at
|
||||
FROM git_providers
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`, userID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var providers []GitProvider
|
||||
for rows.Next() {
|
||||
var provider GitProvider
|
||||
if err := rows.Scan(&provider.ID, &provider.Name, &provider.DisplayName, &provider.APIUrl,
|
||||
&provider.WebhookUrl, &provider.UserID, &provider.CreatedAt, &provider.UpdatedAt); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"providers": providers})
|
||||
}
|
||||
|
||||
func handleCreateGitProvider(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
var req CreateGitProviderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the access token by making a test API call
|
||||
if !validateGitToken(req.Name, req.AccessToken) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid access token for " + req.Name})
|
||||
return
|
||||
}
|
||||
|
||||
provider := GitProvider{
|
||||
ID: uuid.New().String(),
|
||||
Name: req.Name,
|
||||
DisplayName: req.DisplayName,
|
||||
AccessToken: req.AccessToken,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
// Set provider-specific URLs
|
||||
switch req.Name {
|
||||
case "github":
|
||||
provider.APIUrl = "https://api.github.com"
|
||||
provider.WebhookUrl = "https://api.github.com"
|
||||
case "gitlab":
|
||||
provider.APIUrl = "https://gitlab.com/api/v4"
|
||||
provider.WebhookUrl = "https://gitlab.com"
|
||||
case "bitbucket":
|
||||
provider.APIUrl = "https://api.bitbucket.org/2.0"
|
||||
provider.WebhookUrl = "https://api.bitbucket.org/2.0"
|
||||
}
|
||||
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
||||
`, provider.ID, provider.Name, provider.DisplayName, provider.APIUrl,
|
||||
provider.WebhookUrl, provider.AccessToken, provider.UserID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Git provider"})
|
||||
return
|
||||
}
|
||||
|
||||
// Return provider without access token
|
||||
provider.AccessToken = ""
|
||||
c.JSON(http.StatusCreated, provider)
|
||||
}
|
||||
|
||||
func handleGetGitRepositories(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
providerID := c.Param("providerId")
|
||||
|
||||
// Validate UUID
|
||||
if _, err := uuid.Parse(providerID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get provider info
|
||||
var provider GitProvider
|
||||
err := db.QueryRow(`
|
||||
SELECT id, name, access_token, api_url
|
||||
FROM git_providers
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`, providerID, userID).Scan(&provider.ID, &provider.Name, &provider.AccessToken, &provider.APIUrl)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Git provider not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch repositories from the Git provider
|
||||
repos, err := fetchGitRepositories(provider.Name, provider.AccessToken, provider.APIUrl)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch repositories"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"repositories": repos})
|
||||
}
|
||||
|
||||
func handleConnectGitRepository(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
var req CreateGitRepoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate UUID
|
||||
if _, err := uuid.Parse(req.ProviderID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get provider info
|
||||
var provider GitProvider
|
||||
err := db.QueryRow(`
|
||||
SELECT id, name, access_token, api_url
|
||||
FROM git_providers
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`, req.ProviderID, userID).Scan(&provider.ID, &provider.Name, &provider.AccessToken, &provider.APIUrl)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Git provider not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch repository details from Git provider
|
||||
repoDetails, err := fetchGitRepositoryDetails(provider.Name, req.RepoFullName, provider.AccessToken, provider.APIUrl)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch repository details"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if repository is already connected
|
||||
var existingID string
|
||||
err = db.QueryRow(`
|
||||
SELECT id FROM git_repositories
|
||||
WHERE provider_id = $1 AND full_name = $2
|
||||
`, req.ProviderID, req.RepoFullName).Scan(&existingID)
|
||||
|
||||
if err == nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Repository already connected", "repository_id": existingID})
|
||||
return
|
||||
}
|
||||
|
||||
// Create repository record
|
||||
repo := GitRepository{
|
||||
ID: uuid.New().String(),
|
||||
ProviderID: req.ProviderID,
|
||||
Name: repoDetails["name"].(string),
|
||||
FullName: req.RepoFullName,
|
||||
Description: repoDetails["description"].(string),
|
||||
CloneURL: repoDetails["clone_url"].(string),
|
||||
DefaultBranch: repoDetails["default_branch"].(string),
|
||||
IsPrivate: repoDetails["private"].(bool),
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url,
|
||||
default_branch, is_private, user_id, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
|
||||
`, repo.ID, repo.ProviderID, repo.Name, repo.FullName, repo.Description,
|
||||
repo.CloneURL, repo.DefaultBranch, repo.IsPrivate, repo.UserID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect repository"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, repo)
|
||||
}
|
||||
|
||||
func handleCreateWebhook(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
var req CreateWebhookRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate UUIDs
|
||||
if _, err := uuid.Parse(req.RepoID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid repository ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get repository and provider info
|
||||
var repo GitRepository
|
||||
var provider GitProvider
|
||||
err := db.QueryRow(`
|
||||
SELECT r.id, r.provider_id, r.full_name, r.user_id,
|
||||
p.id, p.name, p.access_token, p.webhook_url
|
||||
FROM git_repositories r
|
||||
JOIN git_providers p ON r.provider_id = p.id
|
||||
WHERE r.id = $1 AND r.user_id = $2
|
||||
`, req.RepoID, userID).Scan(&repo.ID, &repo.ProviderID, &repo.FullName, &repo.UserID,
|
||||
&provider.ID, &provider.Name, &provider.AccessToken, &provider.WebhookUrl)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert events to JSON
|
||||
eventsJSON, _ := json.Marshal(req.Events)
|
||||
webhookSecret := generateWebhookSecret()
|
||||
|
||||
// Create webhook on Git provider
|
||||
webhookURL := fmt.Sprintf("%s/api/v1/webhooks/git/%s", "https://your-domain.com", req.RepoID)
|
||||
remoteWebhookID, err := createGitWebhook(provider.Name, repo.FullName, provider.AccessToken,
|
||||
provider.WebhookUrl, webhookURL, req.Events, webhookSecret)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create webhook on Git provider"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create webhook record
|
||||
webhook := GitWebhook{
|
||||
ID: uuid.New().String(),
|
||||
RepoID: req.RepoID,
|
||||
ProviderID: provider.ID,
|
||||
Events: string(eventsJSON),
|
||||
Secret: webhookSecret,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO git_webhooks (id, repo_id, provider_id, events, webhook_secret, active, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
|
||||
`, webhook.ID, webhook.RepoID, webhook.ProviderID, webhook.Events, webhook.Secret, webhook.Active)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create webhook"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"webhook": webhook,
|
||||
"remote_webhook_id": remoteWebhookID,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetConnectedRepositories(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
offset := (page - 1) * limit
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT r.id, r.provider_id, r.name, r.full_name, r.description, r.clone_url,
|
||||
r.default_branch, r.is_private, r.user_id, r.created_at, r.updated_at,
|
||||
p.name as provider_name, p.display_name
|
||||
FROM git_repositories r
|
||||
JOIN git_providers p ON r.provider_id = p.id
|
||||
WHERE r.user_id = $1
|
||||
ORDER BY r.updated_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`, userID, limit, offset)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var repositories []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var repo GitRepository
|
||||
var providerName, providerDisplayName string
|
||||
if err := rows.Scan(&repo.ID, &repo.ProviderID, &repo.Name, &repo.FullName, &repo.Description,
|
||||
&repo.CloneURL, &repo.DefaultBranch, &repo.IsPrivate, &repo.UserID, &repo.CreatedAt, &repo.UpdatedAt,
|
||||
&providerName, &providerDisplayName); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
repositories = append(repositories, map[string]interface{}{
|
||||
"id": repo.ID,
|
||||
"provider_id": repo.ProviderID,
|
||||
"name": repo.Name,
|
||||
"full_name": repo.FullName,
|
||||
"description": repo.Description,
|
||||
"clone_url": repo.CloneURL,
|
||||
"default_branch": repo.DefaultBranch,
|
||||
"is_private": repo.IsPrivate,
|
||||
"created_at": repo.CreatedAt,
|
||||
"updated_at": repo.UpdatedAt,
|
||||
"provider": map[string]string{
|
||||
"name": providerName,
|
||||
"display_name": providerDisplayName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Get total count
|
||||
var total int
|
||||
err = db.QueryRow(`
|
||||
SELECT COUNT(*) FROM git_repositories WHERE user_id = $1
|
||||
`, userID).Scan(&total)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"repositories": repositories,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions (these would need to be implemented with actual Git provider API calls)
|
||||
|
||||
func validateGitToken(provider, token string) bool {
|
||||
// TODO: Implement actual validation with Git provider APIs
|
||||
// For now, just check if token is not empty
|
||||
return token != ""
|
||||
}
|
||||
|
||||
func fetchGitRepositories(provider, token, apiUrl string) ([]map[string]interface{}, error) {
|
||||
// TODO: Implement actual API calls to fetch repositories
|
||||
// For now, return mock data
|
||||
return []map[string]interface{}{
|
||||
{
|
||||
"name": "example-repo",
|
||||
"full_name": "user/example-repo",
|
||||
"description": "An example repository",
|
||||
"clone_url": "https://github.com/user/example-repo.git",
|
||||
"default_branch": "main",
|
||||
"private": false,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fetchGitRepositoryDetails(provider, repoFullName, token, apiUrl string) (map[string]interface{}, error) {
|
||||
// TODO: Implement actual API call to fetch repository details
|
||||
return map[string]interface{}{
|
||||
"name": "example-repo",
|
||||
"description": "An example repository",
|
||||
"clone_url": "https://github.com/user/example-repo.git",
|
||||
"default_branch": "main",
|
||||
"private": false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createGitWebhook(provider, repoFullName, token, webhookUrl, apiUrl string, events []string, secret string) (string, error) {
|
||||
// TODO: Implement actual webhook creation
|
||||
return uuid.New().String(), nil
|
||||
}
|
||||
|
||||
func generateWebhookSecret() string {
|
||||
// TODO: Generate a proper secret
|
||||
return "webhook-secret-" + uuid.New().String()
|
||||
}
|
||||
@@ -1,607 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"containr/internal/ha"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HAManager handles high availability API endpoints
|
||||
type HAManager struct {
|
||||
haManager *ha.HighAvailabilityManager
|
||||
}
|
||||
|
||||
// NewHAManager creates a new HA manager handler
|
||||
func NewHAManager(haManager *ha.HighAvailabilityManager) *HAManager {
|
||||
return &HAManager{
|
||||
haManager: haManager,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers HA routes
|
||||
func (h *HAManager) RegisterRoutes(router *gin.RouterGroup) {
|
||||
ha := router.Group("/ha")
|
||||
{
|
||||
ha.GET("/status", h.GetHAStatus)
|
||||
ha.POST("/enable", h.EnableHA)
|
||||
ha.POST("/disable", h.DisableHA)
|
||||
ha.POST("/failover", h.TriggerFailover)
|
||||
|
||||
// Failover policies
|
||||
ha.GET("/failover/policies", h.GetFailoverPolicies)
|
||||
ha.POST("/failover/policies", h.SetFailoverPolicy)
|
||||
ha.GET("/failover/policies/:serviceId", h.GetFailoverPolicy)
|
||||
ha.PUT("/failover/policies/:serviceId", h.UpdateFailoverPolicy)
|
||||
ha.DELETE("/failover/policies/:serviceId", h.DeleteFailoverPolicy)
|
||||
|
||||
// Health checks
|
||||
ha.GET("/health/checks", h.GetHealthChecks)
|
||||
ha.POST("/health/checks", h.AddHealthCheck)
|
||||
ha.GET("/health/checks/:checkId", h.GetHealthCheck)
|
||||
ha.PUT("/health/checks/:checkId", h.UpdateHealthCheck)
|
||||
ha.DELETE("/health/checks/:checkId", h.DeleteHealthCheck)
|
||||
ha.GET("/health/results", h.GetHealthResults)
|
||||
|
||||
// Alerts
|
||||
ha.GET("/alerts/rules", h.GetAlertRules)
|
||||
ha.POST("/alerts/rules", h.AddAlertRule)
|
||||
ha.GET("/alerts/rules/:ruleId", h.GetAlertRule)
|
||||
ha.PUT("/alerts/rules/:ruleId", h.UpdateAlertRule)
|
||||
ha.DELETE("/alerts/rules/:ruleId", h.DeleteAlertRule)
|
||||
ha.GET("/alerts/active", h.GetActiveAlerts)
|
||||
ha.POST("/alerts/:alertId/resolve", h.ResolveAlert)
|
||||
|
||||
// Notifiers
|
||||
ha.GET("/notifiers", h.GetNotifiers)
|
||||
ha.POST("/notifiers", h.AddNotifier)
|
||||
ha.GET("/notifiers/:notifierId", h.GetNotifier)
|
||||
ha.DELETE("/notifiers/:notifierId", h.DeleteNotifier)
|
||||
}
|
||||
}
|
||||
|
||||
// GetHAStatus returns the overall HA status
|
||||
func (h *HAManager) GetHAStatus(c *gin.Context) {
|
||||
status := h.haManager.GetHealthStatus()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": status,
|
||||
})
|
||||
}
|
||||
|
||||
// EnableHA enables the HA manager
|
||||
func (h *HAManager) EnableHA(c *gin.Context) {
|
||||
h.haManager.Enable()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "High availability manager enabled",
|
||||
"enabled": true,
|
||||
})
|
||||
}
|
||||
|
||||
// DisableHA disables the HA manager
|
||||
func (h *HAManager) DisableHA(c *gin.Context) {
|
||||
h.haManager.Disable()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "High availability manager disabled",
|
||||
"enabled": false,
|
||||
})
|
||||
}
|
||||
|
||||
// TriggerFailover manually triggers a failover
|
||||
func (h *HAManager) TriggerFailover(c *gin.Context) {
|
||||
var request struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
reason := request.Reason
|
||||
if reason == "" {
|
||||
reason = "Manual trigger"
|
||||
}
|
||||
|
||||
if err := h.haManager.TriggerFailover(c.Request.Context(), reason); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Failover triggered successfully",
|
||||
"reason": reason,
|
||||
})
|
||||
}
|
||||
|
||||
// GetFailoverPolicies returns all failover policies
|
||||
func (h *HAManager) GetFailoverPolicies(c *gin.Context) {
|
||||
// TODO: Implement getting all policies
|
||||
// For now, return mock data
|
||||
policies := []map[string]interface{}{
|
||||
{
|
||||
"service_id": "web-service",
|
||||
"enabled": true,
|
||||
"min_healthy_nodes": 2,
|
||||
"max_failures": 3,
|
||||
"failover_timeout": "30s",
|
||||
"recovery_timeout": "5m",
|
||||
"failover_strategy": "active_passive",
|
||||
"backup_nodes": []string{"node-2", "node-3"},
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"policies": policies,
|
||||
"count": len(policies),
|
||||
})
|
||||
}
|
||||
|
||||
// SetFailoverPolicy creates or updates a failover policy
|
||||
func (h *HAManager) SetFailoverPolicy(c *gin.Context) {
|
||||
var policy ha.FailoverPolicy
|
||||
if err := c.ShouldBindJSON(&policy); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.haManager.SetFailoverPolicy(&policy); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Failover policy set successfully",
|
||||
"policy": policy,
|
||||
})
|
||||
}
|
||||
|
||||
// GetFailoverPolicy returns a specific failover policy
|
||||
func (h *HAManager) GetFailoverPolicy(c *gin.Context) {
|
||||
serviceID := c.Param("serviceId")
|
||||
|
||||
policy, err := h.haManager.GetFailoverPolicy(serviceID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"policy": policy,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateFailoverPolicy updates an existing failover policy
|
||||
func (h *HAManager) UpdateFailoverPolicy(c *gin.Context) {
|
||||
serviceID := c.Param("serviceId")
|
||||
|
||||
var policy ha.FailoverPolicy
|
||||
if err := c.ShouldBindJSON(&policy); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the service ID matches
|
||||
policy.ServiceID = serviceID
|
||||
|
||||
if err := h.haManager.SetFailoverPolicy(&policy); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Failover policy updated successfully",
|
||||
"policy": policy,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteFailoverPolicy removes a failover policy
|
||||
func (h *HAManager) DeleteFailoverPolicy(c *gin.Context) {
|
||||
serviceID := c.Param("serviceId")
|
||||
|
||||
// Set policy to disabled instead of deleting
|
||||
policy, err := h.haManager.GetFailoverPolicy(serviceID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
policy.Enabled = false
|
||||
if err := h.haManager.SetFailoverPolicy(policy); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Failover policy disabled successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetHealthChecks returns all health checks
|
||||
func (h *HAManager) GetHealthChecks(c *gin.Context) {
|
||||
// TODO: Implement getting health checks from the health checker
|
||||
// For now, return mock data
|
||||
checks := []map[string]interface{}{
|
||||
{
|
||||
"id": "check-1",
|
||||
"service_id": "web-service",
|
||||
"node_id": "node-1",
|
||||
"type": "http",
|
||||
"config": map[string]interface{}{
|
||||
"interval": "30s",
|
||||
"timeout": "5s",
|
||||
"unhealthy_threshold": 3,
|
||||
"healthy_threshold": 2,
|
||||
"path": "/health",
|
||||
"port": 8080,
|
||||
"protocol": "HTTP",
|
||||
},
|
||||
"last_check": time.Now().Add(-30 * time.Second),
|
||||
"status": "healthy",
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"checks": checks,
|
||||
"count": len(checks),
|
||||
})
|
||||
}
|
||||
|
||||
// AddHealthCheck adds a new health check
|
||||
func (h *HAManager) AddHealthCheck(c *gin.Context) {
|
||||
var check ha.HealthCheck
|
||||
if err := c.ShouldBindJSON(&check); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Add health check to the health checker
|
||||
// For now, just return success
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Health check added successfully",
|
||||
"check": check,
|
||||
})
|
||||
}
|
||||
|
||||
// GetHealthCheck returns a specific health check
|
||||
func (h *HAManager) GetHealthCheck(c *gin.Context) {
|
||||
_ = c.Param("checkId") // Use the parameter to avoid unused variable error
|
||||
|
||||
// TODO: Implement getting specific health check
|
||||
// For now, return mock data
|
||||
check := map[string]interface{}{
|
||||
"id": "check-1",
|
||||
"service_id": "web-service",
|
||||
"node_id": "node-1",
|
||||
"type": "http",
|
||||
"status": "healthy",
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"check": check})
|
||||
}
|
||||
|
||||
// UpdateHealthCheck updates an existing health check
|
||||
func (h *HAManager) UpdateHealthCheck(c *gin.Context) {
|
||||
checkID := c.Param("checkId")
|
||||
|
||||
var check ha.HealthCheck
|
||||
if err := c.ShouldBindJSON(&check); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the check ID matches
|
||||
check.ID = checkID
|
||||
|
||||
// TODO: Update health check in the health checker
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Health check updated successfully",
|
||||
"check": check,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteHealthCheck removes a health check
|
||||
func (h *HAManager) DeleteHealthCheck(c *gin.Context) {
|
||||
_ = c.Param("checkId") // Use the parameter to avoid unused variable error
|
||||
|
||||
// TODO: Remove health check from the health checker
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Health check deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetHealthResults returns all health check results
|
||||
func (h *HAManager) GetHealthResults(c *gin.Context) {
|
||||
// TODO: Implement getting health check results
|
||||
// For now, return mock data
|
||||
results := []map[string]interface{}{
|
||||
{
|
||||
"check_id": "check-1",
|
||||
"status": "healthy",
|
||||
"message": "Service is healthy",
|
||||
"latency": "15ms",
|
||||
"timestamp": time.Now().Add(-30 * time.Second),
|
||||
},
|
||||
{
|
||||
"check_id": "check-2",
|
||||
"status": "unhealthy",
|
||||
"message": "Connection timeout",
|
||||
"latency": "5000ms",
|
||||
"timestamp": time.Now().Add(-25 * time.Second),
|
||||
"error_code": "TIMEOUT",
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"results": results,
|
||||
"count": len(results),
|
||||
})
|
||||
}
|
||||
|
||||
// GetAlertRules returns all alert rules
|
||||
func (h *HAManager) GetAlertRules(c *gin.Context) {
|
||||
// TODO: Implement getting alert rules
|
||||
// For now, return mock data
|
||||
rules := []map[string]interface{}{
|
||||
{
|
||||
"id": "rule-1",
|
||||
"name": "High CPU Usage",
|
||||
"description": "Alert when CPU usage is above 90%",
|
||||
"enabled": true,
|
||||
"condition": map[string]interface{}{
|
||||
"metric": "cpu_usage",
|
||||
"operator": ">",
|
||||
"threshold": 90.0,
|
||||
"duration": "5m",
|
||||
},
|
||||
"severity": "warning",
|
||||
"labels": map[string]string{
|
||||
"service": "web-service",
|
||||
"team": "backend",
|
||||
},
|
||||
"notifiers": []string{"email", "slack"},
|
||||
"cooldown": "10m",
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"rules": rules,
|
||||
"count": len(rules),
|
||||
})
|
||||
}
|
||||
|
||||
// AddAlertRule adds a new alert rule
|
||||
func (h *HAManager) AddAlertRule(c *gin.Context) {
|
||||
var rule ha.AlertRule
|
||||
if err := c.ShouldBindJSON(&rule); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Add alert rule to the alert manager
|
||||
// For now, just return success
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Alert rule added successfully",
|
||||
"rule": rule,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAlertRule returns a specific alert rule
|
||||
func (h *HAManager) GetAlertRule(c *gin.Context) {
|
||||
_ = c.Param("ruleId") // Use the parameter to avoid unused variable error
|
||||
|
||||
// TODO: Implement getting specific alert rule
|
||||
// For now, return mock data
|
||||
rule := map[string]interface{}{
|
||||
"id": "rule-1",
|
||||
"name": "High CPU Usage",
|
||||
"description": "Alert when CPU usage is above 90%",
|
||||
"enabled": true,
|
||||
"severity": "warning",
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"rule": rule,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAlertRule updates an existing alert rule
|
||||
func (h *HAManager) UpdateAlertRule(c *gin.Context) {
|
||||
ruleID := c.Param("ruleId")
|
||||
|
||||
var rule ha.AlertRule
|
||||
if err := c.ShouldBindJSON(&rule); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the rule ID matches
|
||||
rule.ID = ruleID
|
||||
|
||||
// TODO: Update alert rule in the alert manager
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Alert rule updated successfully",
|
||||
"rule": rule,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAlertRule removes an alert rule
|
||||
func (h *HAManager) DeleteAlertRule(c *gin.Context) {
|
||||
// TODO: Remove alert rule from the alert manager
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Alert rule deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetActiveAlerts returns all active alerts
|
||||
func (h *HAManager) GetActiveAlerts(c *gin.Context) {
|
||||
// Parse query parameters
|
||||
limitStr := c.DefaultQuery("limit", "50")
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
// TODO: Implement getting active alerts from the alert manager
|
||||
// For now, return mock data
|
||||
alerts := []map[string]interface{}{
|
||||
{
|
||||
"id": "alert-1",
|
||||
"rule_id": "rule-1",
|
||||
"status": "firing",
|
||||
"severity": "warning",
|
||||
"message": "CPU usage is above 90%",
|
||||
"labels": map[string]string{
|
||||
"service": "web-service",
|
||||
"team": "backend",
|
||||
},
|
||||
"starts_at": time.Now().Add(-10 * time.Minute),
|
||||
"updated_at": time.Now().Add(-2 * time.Minute),
|
||||
},
|
||||
{
|
||||
"id": "alert-2",
|
||||
"rule_id": "rule-2",
|
||||
"status": "firing",
|
||||
"severity": "critical",
|
||||
"message": "Service is down",
|
||||
"labels": map[string]string{
|
||||
"service": "api-service",
|
||||
"team": "backend",
|
||||
},
|
||||
"starts_at": time.Now().Add(-5 * time.Minute),
|
||||
"updated_at": time.Now().Add(-1 * time.Minute),
|
||||
},
|
||||
}
|
||||
|
||||
// Limit results
|
||||
if len(alerts) > limit {
|
||||
alerts = alerts[:limit]
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"alerts": alerts,
|
||||
"count": len(alerts),
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
// ResolveAlert resolves an alert
|
||||
func (h *HAManager) ResolveAlert(c *gin.Context) {
|
||||
alertID := c.Param("alertId")
|
||||
|
||||
// TODO: Resolve alert in the alert manager
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Alert resolved successfully",
|
||||
"alert_id": alertID,
|
||||
})
|
||||
}
|
||||
|
||||
// GetNotifiers returns all notifiers
|
||||
func (h *HAManager) GetNotifiers(c *gin.Context) {
|
||||
// TODO: Implement getting notifiers
|
||||
// For now, return mock data
|
||||
notifiers := []map[string]interface{}{
|
||||
{
|
||||
"id": "email",
|
||||
"type": "email",
|
||||
"config": map[string]interface{}{
|
||||
"smtp_host": "smtp.example.com",
|
||||
"smtp_port": 587,
|
||||
"from": "alerts@containr.com",
|
||||
"to": []string{"admin@example.com"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "slack",
|
||||
"type": "slack",
|
||||
"config": map[string]interface{}{
|
||||
"webhook_url": "https://hooks.slack.com/...",
|
||||
"channel": "#alerts",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"notifiers": notifiers,
|
||||
"count": len(notifiers),
|
||||
})
|
||||
}
|
||||
|
||||
// AddNotifier adds a new notifier
|
||||
func (h *HAManager) AddNotifier(c *gin.Context) {
|
||||
var request struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Config map[string]interface{} `json:"config"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Create notifier based on type
|
||||
switch request.Type {
|
||||
case "email":
|
||||
_ = &ha.EmailNotifier{} // Create but don't use for now
|
||||
case "slack":
|
||||
_ = &ha.SlackNotifier{} // Create but don't use for now
|
||||
case "webhook":
|
||||
_ = &ha.WebhookNotifier{} // Create but don't use for now
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid notifier type"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Add notifier to the alert manager
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Notifier added successfully",
|
||||
"id": request.ID,
|
||||
"type": request.Type,
|
||||
})
|
||||
}
|
||||
|
||||
// GetNotifier returns a specific notifier
|
||||
func (h *HAManager) GetNotifier(c *gin.Context) {
|
||||
_ = c.Param("notifierId") // Use the parameter to avoid unused variable error
|
||||
|
||||
// TODO: Implement getting specific notifier
|
||||
// For now, return mock data
|
||||
notifier := map[string]interface{}{
|
||||
"id": "email",
|
||||
"type": "email",
|
||||
"config": map[string]interface{}{
|
||||
"smtp_host": "smtp.example.com",
|
||||
"smtp_port": 587,
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"notifier": notifier,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteNotifier removes a notifier
|
||||
func (h *HAManager) DeleteNotifier(c *gin.Context) {
|
||||
_ = c.Param("notifierId") // Use the parameter to avoid unused variable error
|
||||
|
||||
// TODO: Remove notifier from the alert manager
|
||||
// For now, just return success
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Notifier deleted successfully",
|
||||
})
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"containr/internal/database"
|
||||
"containr/internal/docker"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LogEntry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Message string `json:"message"`
|
||||
Stream string `json:"stream"`
|
||||
}
|
||||
|
||||
func handleGetLogs(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
serviceIDStr := c.Param("id")
|
||||
serviceID, err := uuid.Parse(serviceIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"})
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var ownerCheck string
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT p.owner_id FROM services s
|
||||
JOIN projects p ON s.project_id = p.id
|
||||
WHERE s.id = $1`,
|
||||
serviceID,
|
||||
).Scan(&ownerCheck)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if ownerCheck != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
follow := c.DefaultQuery("follow", "false") == "true"
|
||||
tail := c.DefaultQuery("tail", "100")
|
||||
|
||||
dockerClient, exists := c.Get("docker_client")
|
||||
if !exists || dockerClient == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"logs": []LogEntry{}, "message": "Docker not available - showing mock logs"})
|
||||
return
|
||||
}
|
||||
|
||||
client := dockerClient.(*docker.Client)
|
||||
containerName := fmt.Sprintf("containr-%s", serviceID)
|
||||
|
||||
logOpts := docker.LogOptions{
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
Follow: follow,
|
||||
Tail: tail,
|
||||
Timestamps: true,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
logsReader, err := client.GetContainerLogs(ctx, containerName, logOpts)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"logs": []LogEntry{
|
||||
{Timestamp: time.Now(), Message: "Service not running or container not found", Stream: "system"},
|
||||
{Timestamp: time.Now(), Message: "Start the service to see logs", Stream: "system"},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
defer logsReader.Close()
|
||||
|
||||
if follow {
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
|
||||
streamWriter := c.Writer
|
||||
flusher, ok := streamWriter.(http.Flusher)
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Streaming not supported"})
|
||||
return
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(logsReader)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
cleanLine := stripDockerLogHeader(line)
|
||||
entry := LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Message: cleanLine,
|
||||
Stream: "stdout",
|
||||
}
|
||||
if strings.Contains(strings.ToLower(cleanLine), "error") || strings.Contains(strings.ToLower(cleanLine), "err") {
|
||||
entry.Stream = "stderr"
|
||||
}
|
||||
|
||||
fmt.Fprintf(streamWriter, "data: {\"timestamp\":\"%s\",\"message\":\"%s\",\"stream\":\"%s\"}\n\n",
|
||||
entry.Timestamp.Format(time.RFC3339),
|
||||
strings.ReplaceAll(entry.Message, `"`, `\"`),
|
||||
entry.Stream,
|
||||
)
|
||||
flusher.Flush()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logBytes, err := io.ReadAll(logsReader)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read logs"})
|
||||
return
|
||||
}
|
||||
|
||||
logContent := string(logBytes)
|
||||
var logEntries []LogEntry
|
||||
scanner := bufio.NewScanner(strings.NewReader(logContent))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
cleanLine := stripDockerLogHeader(line)
|
||||
entry := LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Message: cleanLine,
|
||||
Stream: "stdout",
|
||||
}
|
||||
if strings.Contains(strings.ToLower(cleanLine), "error") || strings.Contains(strings.ToLower(cleanLine), "err") {
|
||||
entry.Stream = "stderr"
|
||||
}
|
||||
logEntries = append(logEntries, entry)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"logs": logEntries})
|
||||
}
|
||||
|
||||
func handleGetDeploymentLogs(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
deploymentIDStr := c.Param("id")
|
||||
deploymentID, err := uuid.Parse(deploymentIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid deployment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var buildLog, runtimeLog string
|
||||
var ownerCheck string
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT d.build_log, d.runtime_log, p.owner_id
|
||||
FROM deployments d
|
||||
JOIN services s ON d.service_id = s.id
|
||||
JOIN projects p ON s.project_id = p.id
|
||||
WHERE d.id = $1`,
|
||||
deploymentID,
|
||||
).Scan(&buildLog, &runtimeLog, &ownerCheck)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Deployment not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if ownerCheck != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
logType := c.DefaultQuery("type", "all")
|
||||
var logs []LogEntry
|
||||
|
||||
parseLogs := func(logContent string, stream string) []LogEntry {
|
||||
var entries []LogEntry
|
||||
scanner := bufio.NewScanner(strings.NewReader(logContent))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Message: line,
|
||||
Stream: stream,
|
||||
})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
if logType == "all" || logType == "build" {
|
||||
logs = append(logs, parseLogs(buildLog, "build")...)
|
||||
}
|
||||
if logType == "all" || logType == "runtime" {
|
||||
logs = append(logs, parseLogs(runtimeLog, "runtime")...)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"logs": logs,
|
||||
"build_log": buildLog,
|
||||
"runtime_log": runtimeLog,
|
||||
})
|
||||
}
|
||||
|
||||
func stripDockerLogHeader(line string) string {
|
||||
if len(line) > 8 && (line[0] == 1 || line[0] == 2) {
|
||||
return line[8:]
|
||||
}
|
||||
return line
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package api
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// firstPathParam returns the first non-empty route param from the provided names.
|
||||
func firstPathParam(c *gin.Context, names ...string) string {
|
||||
for _, name := range names {
|
||||
if value := c.Param(name); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,617 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// PreviewEnvironment represents a preview environment
|
||||
type PreviewEnvironment struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
ProjectID uuid.UUID `json:"project_id" db:"project_id"`
|
||||
ServiceID uuid.UUID `json:"service_id" db:"service_id"`
|
||||
BranchName string `json:"branch_name" db:"branch_name"`
|
||||
PRNumber *int `json:"pr_number" db:"pr_number"`
|
||||
Environment string `json:"environment" db:"environment"` // preview-{branch}-{timestamp}
|
||||
Status string `json:"status" db:"status"` // building, running, failed, stopped, expired
|
||||
URL string `json:"url" db:"url"`
|
||||
ExpiresAt *time.Time `json:"expires_at" db:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
|
||||
// Related data
|
||||
Service *Service `json:"service,omitempty"`
|
||||
DeploymentID *uuid.UUID `json:"deployment_id,omitempty"`
|
||||
}
|
||||
|
||||
// CreatePreviewEnvironmentRequest represents a request to create a preview environment
|
||||
type CreatePreviewEnvironmentRequest struct {
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
ServiceID uuid.UUID `json:"service_id" binding:"required"`
|
||||
BranchName string `json:"branch_name" binding:"required"`
|
||||
PRNumber *int `json:"pr_number"`
|
||||
TTLHours int `json:"ttl_hours" binding:"min=1,max=168"` // 1 hour to 7 days
|
||||
}
|
||||
|
||||
// UpdatePreviewEnvironmentRequest represents a request to update a preview environment
|
||||
type UpdatePreviewEnvironmentRequest struct {
|
||||
Status string `json:"status" binding:"omitempty,oneof=building running failed stopped expired"`
|
||||
URL string `json:"url"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
TTLHours int `json:"ttl_hours" binding:"omitempty,min=1,max=168"`
|
||||
}
|
||||
|
||||
// PromotePreviewEnvironmentRequest represents a request to promote a preview environment
|
||||
type PromotePreviewEnvironmentRequest struct {
|
||||
TargetEnvironment string `json:"target_environment" binding:"required,oneof=production development"`
|
||||
CreateBackup bool `json:"create_backup"`
|
||||
}
|
||||
|
||||
// handleGetPreviewEnvironments retrieves all preview environments for a project
|
||||
func handleGetPreviewEnvironments(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
projectIDStr := firstPathParam(c, "id", "project_id", "projectId")
|
||||
projectID, err := uuid.Parse(projectIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if project exists and user has access
|
||||
var project Project
|
||||
err = db.(*database.DB).QueryRow(
|
||||
"SELECT id, name, owner_id FROM projects WHERE id = $1",
|
||||
projectID,
|
||||
).Scan(&project.ID, &project.Name, &project.OwnerID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token (set by auth middleware)
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns the project
|
||||
if project.OwnerID != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get preview environments for the project with service info
|
||||
rows, err := db.(*database.DB).Query(
|
||||
`SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.pr_number,
|
||||
pe.environment, pe.status, pe.url, pe.expires_at, pe.created_at, pe.updated_at,
|
||||
s.id as service_id, s.name as service_name, s.type as service_type
|
||||
FROM preview_environments pe
|
||||
LEFT JOIN services s ON pe.service_id = s.id
|
||||
WHERE pe.project_id = $1
|
||||
ORDER BY pe.created_at DESC`,
|
||||
projectID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve preview environments"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var environments []PreviewEnvironment
|
||||
for rows.Next() {
|
||||
var env PreviewEnvironment
|
||||
var serviceID sql.NullString
|
||||
var serviceName sql.NullString
|
||||
var serviceType sql.NullString
|
||||
|
||||
err := rows.Scan(
|
||||
&env.ID, &env.ProjectID, &env.ServiceID, &env.BranchName, &env.PRNumber,
|
||||
&env.Environment, &env.Status, &env.URL, &env.ExpiresAt, &env.CreatedAt, &env.UpdatedAt,
|
||||
&serviceID, &serviceName, &serviceType,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan preview environment"})
|
||||
return
|
||||
}
|
||||
|
||||
if serviceID.Valid {
|
||||
parsedServiceID, parseErr := uuid.Parse(serviceID.String)
|
||||
if parseErr == nil {
|
||||
env.Service = &Service{
|
||||
ID: parsedServiceID,
|
||||
Name: serviceName.String,
|
||||
Type: serviceType.String,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
environments = append(environments, env)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"preview_environments": environments})
|
||||
}
|
||||
|
||||
// handleCreatePreviewEnvironment creates a new preview environment
|
||||
func handleCreatePreviewEnvironment(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
projectIDStr := firstPathParam(c, "id", "project_id", "projectId")
|
||||
projectID, err := uuid.Parse(projectIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreatePreviewEnvironmentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.ProjectID == uuid.Nil {
|
||||
req.ProjectID = projectID
|
||||
} else if req.ProjectID != projectID {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Project ID in URL and request body must match"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if project exists and user has access
|
||||
var project Project
|
||||
err = db.(*database.DB).QueryRow(
|
||||
"SELECT id, name, owner_id FROM projects WHERE id = $1",
|
||||
req.ProjectID,
|
||||
).Scan(&project.ID, &project.Name, &project.OwnerID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns the project
|
||||
if project.OwnerID != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if service exists and belongs to the project
|
||||
var service Service
|
||||
err = db.(*database.DB).QueryRow(
|
||||
"SELECT id, name, type FROM services WHERE id = $1 AND project_id = $2",
|
||||
req.ServiceID, req.ProjectID,
|
||||
).Scan(&service.ID, &service.Name, &service.Type)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found or doesn't belong to this project"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if preview environment already exists for this branch and service
|
||||
var count int
|
||||
err = db.(*database.DB).QueryRow(
|
||||
"SELECT COUNT(*) FROM preview_environments WHERE service_id = $1 AND branch_name = $2 AND status NOT IN ('expired', 'stopped')",
|
||||
req.ServiceID, req.BranchName,
|
||||
).Scan(&count)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check existing preview environment"})
|
||||
return
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Preview environment already exists for this branch and service"})
|
||||
return
|
||||
}
|
||||
|
||||
// Set default TTL if not provided
|
||||
ttlHours := req.TTLHours
|
||||
if ttlHours == 0 {
|
||||
ttlHours = 24 // Default 24 hours
|
||||
}
|
||||
|
||||
// Create preview environment
|
||||
env := PreviewEnvironment{
|
||||
ID: uuid.New(),
|
||||
ProjectID: req.ProjectID,
|
||||
ServiceID: req.ServiceID,
|
||||
BranchName: req.BranchName,
|
||||
PRNumber: req.PRNumber,
|
||||
Environment: generatePreviewEnvironmentName(req.BranchName),
|
||||
Status: "building",
|
||||
ExpiresAt: &[]time.Time{time.Now().Add(time.Duration(ttlHours) * time.Hour)}[0],
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Insert preview environment into database
|
||||
_, err = db.(*database.DB).Exec(
|
||||
`INSERT INTO preview_environments
|
||||
(id, project_id, service_id, branch_name, pr_number, environment,
|
||||
status, url, expires_at, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
|
||||
env.ID, env.ProjectID, env.ServiceID, env.BranchName, env.PRNumber,
|
||||
env.Environment, env.Status, env.URL, env.ExpiresAt, env.CreatedAt, env.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create preview environment"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Trigger deployment pipeline for preview environment
|
||||
// This would integrate with the existing deployment engine
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"preview_environment": env})
|
||||
}
|
||||
|
||||
// handleGetPreviewEnvironment retrieves a specific preview environment
|
||||
func handleGetPreviewEnvironment(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
envIDStr := c.Param("id")
|
||||
envID, err := uuid.Parse(envIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid preview environment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get preview environment with project ownership check
|
||||
var env PreviewEnvironment
|
||||
var serviceID sql.NullString
|
||||
var serviceName sql.NullString
|
||||
var serviceType sql.NullString
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.pr_number,
|
||||
pe.environment, pe.status, pe.url, pe.expires_at, pe.created_at, pe.updated_at,
|
||||
s.id as service_id, s.name as service_name, s.type as service_type
|
||||
FROM preview_environments pe
|
||||
LEFT JOIN services s ON pe.service_id = s.id
|
||||
JOIN projects p ON pe.project_id = p.id
|
||||
WHERE pe.id = $1 AND p.owner_id = $2`,
|
||||
envID, userID,
|
||||
).Scan(
|
||||
&env.ID, &env.ProjectID, &env.ServiceID, &env.BranchName, &env.PRNumber,
|
||||
&env.Environment, &env.Status, &env.URL, &env.ExpiresAt, &env.CreatedAt, &env.UpdatedAt,
|
||||
&serviceID, &serviceName, &serviceType,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Populate service info if available
|
||||
if serviceID.Valid {
|
||||
parsedServiceID, parseErr := uuid.Parse(serviceID.String)
|
||||
if parseErr == nil {
|
||||
env.Service = &Service{
|
||||
ID: parsedServiceID,
|
||||
Name: serviceName.String,
|
||||
Type: serviceType.String,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"preview_environment": env})
|
||||
}
|
||||
|
||||
// handleUpdatePreviewEnvironment updates a preview environment
|
||||
func handleUpdatePreviewEnvironment(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
envIDStr := c.Param("id")
|
||||
envID, err := uuid.Parse(envIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid preview environment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdatePreviewEnvironmentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if preview environment exists and user has access
|
||||
var existingEnv PreviewEnvironment
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.pr_number,
|
||||
pe.environment, pe.status, pe.url, pe.expires_at, pe.created_at, pe.updated_at
|
||||
FROM preview_environments pe
|
||||
JOIN projects p ON pe.project_id = p.id
|
||||
WHERE pe.id = $1 AND p.owner_id = $2`,
|
||||
envID, userID,
|
||||
).Scan(
|
||||
&existingEnv.ID, &existingEnv.ProjectID, &existingEnv.ServiceID, &existingEnv.BranchName,
|
||||
&existingEnv.PRNumber, &existingEnv.Environment, &existingEnv.Status, &existingEnv.URL,
|
||||
&existingEnv.ExpiresAt, &existingEnv.CreatedAt, &existingEnv.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update fields if provided
|
||||
if req.Status != "" {
|
||||
existingEnv.Status = req.Status
|
||||
}
|
||||
if req.URL != "" {
|
||||
existingEnv.URL = req.URL
|
||||
}
|
||||
if req.ExpiresAt != nil {
|
||||
existingEnv.ExpiresAt = req.ExpiresAt
|
||||
}
|
||||
if req.TTLHours > 0 {
|
||||
newExpiresAt := time.Now().Add(time.Duration(req.TTLHours) * time.Hour)
|
||||
existingEnv.ExpiresAt = &newExpiresAt
|
||||
}
|
||||
|
||||
existingEnv.UpdatedAt = time.Now()
|
||||
|
||||
// Update preview environment in database
|
||||
_, err = db.(*database.DB).Exec(
|
||||
`UPDATE preview_environments
|
||||
SET status = $1, url = $2, expires_at = $3, updated_at = $4
|
||||
WHERE id = $5`,
|
||||
existingEnv.Status, existingEnv.URL, existingEnv.ExpiresAt, existingEnv.UpdatedAt, existingEnv.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update preview environment"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"preview_environment": existingEnv})
|
||||
}
|
||||
|
||||
// handleDeletePreviewEnvironment deletes a preview environment
|
||||
func handleDeletePreviewEnvironment(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
envIDStr := c.Param("id")
|
||||
envID, err := uuid.Parse(envIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid preview environment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if preview environment exists and user has access
|
||||
var projectOwnerID string
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT p.owner_id
|
||||
FROM preview_environments pe
|
||||
JOIN projects p ON pe.project_id = p.id
|
||||
WHERE pe.id = $1`,
|
||||
envID,
|
||||
).Scan(&projectOwnerID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns the project
|
||||
if projectOwnerID != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Clean up deployment and resources associated with this preview environment
|
||||
// This would integrate with the deployment engine to stop containers, clean up resources, etc.
|
||||
|
||||
// Delete preview environment
|
||||
_, err = db.(*database.DB).Exec(
|
||||
"DELETE FROM preview_environments WHERE id = $1",
|
||||
envID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete preview environment"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Preview environment deleted successfully"})
|
||||
}
|
||||
|
||||
// handlePromotePreviewEnvironment promotes a preview environment to production/development
|
||||
func handlePromotePreviewEnvironment(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
envIDStr := c.Param("id")
|
||||
envID, err := uuid.Parse(envIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid preview environment ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req PromotePreviewEnvironmentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get preview environment details
|
||||
var env PreviewEnvironment
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.environment, pe.status
|
||||
FROM preview_environments pe
|
||||
JOIN projects p ON pe.project_id = p.id
|
||||
WHERE pe.id = $1 AND p.owner_id = $2`,
|
||||
envID, userID,
|
||||
).Scan(
|
||||
&env.ID, &env.ProjectID, &env.ServiceID, &env.BranchName, &env.Environment, &env.Status,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Preview environment not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if preview environment is in a state that can be promoted
|
||||
if env.Status != "running" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Preview environment must be running to promote"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Implement promotion logic
|
||||
// 1. Create backup of target environment if requested
|
||||
// 2. Deploy preview environment code to target environment
|
||||
// 3. Update service configuration
|
||||
// 4. Trigger deployment pipeline
|
||||
|
||||
// For now, just return success with promotion details
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Preview environment promotion initiated",
|
||||
"promotion": map[string]interface{}{
|
||||
"preview_environment_id": env.ID,
|
||||
"target_environment": req.TargetEnvironment,
|
||||
"branch_name": env.BranchName,
|
||||
"create_backup": req.CreateBackup,
|
||||
"status": "initiated",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// generatePreviewEnvironmentName generates a unique environment name for preview
|
||||
func generatePreviewEnvironmentName(branchName string) string {
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
// Sanitize branch name
|
||||
sanitizedBranch := strings.ReplaceAll(branchName, "/", "-")
|
||||
sanitizedBranch = strings.ReplaceAll(sanitizedBranch, "_", "-")
|
||||
return fmt.Sprintf("preview-%s-%s", sanitizedBranch, timestamp)
|
||||
}
|
||||
|
||||
// handleCleanupExpiredPreviewEnvironments cleans up expired preview environments
|
||||
func handleCleanupExpiredPreviewEnvironments(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Find expired preview environments for user's projects
|
||||
rows, err := db.(*database.DB).Query(
|
||||
`SELECT pe.id, pe.project_id, pe.service_id, pe.branch_name, pe.environment
|
||||
FROM preview_environments pe
|
||||
JOIN projects p ON pe.project_id = p.id
|
||||
WHERE p.owner_id = $1 AND pe.expires_at < NOW() AND pe.status != 'expired'`,
|
||||
userID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to find expired preview environments"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var expiredEnvs []PreviewEnvironment
|
||||
for rows.Next() {
|
||||
var env PreviewEnvironment
|
||||
err := rows.Scan(
|
||||
&env.ID, &env.ProjectID, &env.ServiceID, &env.BranchName, &env.Environment,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
expiredEnvs = append(expiredEnvs, env)
|
||||
}
|
||||
|
||||
// Mark expired environments as expired and trigger cleanup
|
||||
cleanupCount := 0
|
||||
for _, env := range expiredEnvs {
|
||||
// Update status to expired
|
||||
_, err := db.(*database.DB).Exec(
|
||||
"UPDATE preview_environments SET status = 'expired', updated_at = NOW() WHERE id = $1",
|
||||
env.ID,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: Trigger cleanup of deployment resources
|
||||
// This would stop containers, clean up resources, etc.
|
||||
|
||||
cleanupCount++
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Cleanup completed",
|
||||
"cleaned_count": cleanupCount,
|
||||
"expired_environments": expiredEnvs,
|
||||
})
|
||||
}
|
||||
@@ -1,396 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ProjectStats struct {
|
||||
ServiceCount int `json:"service_count"`
|
||||
DeploymentCount int `json:"deployment_count"`
|
||||
RunningServices int `json:"running_services"`
|
||||
LastDeployment *string `json:"last_deployment"`
|
||||
}
|
||||
|
||||
type ProjectWithStats struct {
|
||||
Project
|
||||
Stats ProjectStats `json:"stats"`
|
||||
}
|
||||
|
||||
type CreateProjectRequest struct {
|
||||
Name string `json:"name" binding:"required,min=2"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type UpdateProjectRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
func handleGetProjects(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
// Get pagination parameters
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
search := c.DefaultQuery("search", "")
|
||||
|
||||
// Validate and limit pagination
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit > 100 || limit < 1 {
|
||||
limit = 10
|
||||
}
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// Use the optimized view for better performance
|
||||
var query string
|
||||
var args []interface{}
|
||||
|
||||
if search != "" {
|
||||
// Search query with pattern matching
|
||||
query = `
|
||||
SELECT id, name, description, owner_id, created_at, updated_at
|
||||
FROM project_stats
|
||||
WHERE (owner_id = $1 OR id IN (
|
||||
SELECT DISTINCT project_id FROM project_members WHERE user_id = $1
|
||||
)) AND (name ILIKE $2 OR description ILIKE $2)
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
`
|
||||
args = []interface{}{userID, "%" + search + "%", limit, offset}
|
||||
} else {
|
||||
// Optimized query using the view
|
||||
query = `
|
||||
SELECT id, name, description, owner_id, created_at, updated_at
|
||||
FROM project_stats
|
||||
WHERE owner_id = $1 OR id IN (
|
||||
SELECT DISTINCT project_id FROM project_members WHERE user_id = $1
|
||||
)
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
args = []interface{}{userID, limit, offset}
|
||||
}
|
||||
|
||||
// Execute query with timeout context
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database query error"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var projects []ProjectWithStats
|
||||
for rows.Next() {
|
||||
var project ProjectWithStats
|
||||
if err := rows.Scan(&project.ID, &project.Name, &project.Description, &project.OwnerID, &project.CreatedAt, &project.UpdatedAt); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database scan error"})
|
||||
return
|
||||
}
|
||||
projects = append(projects, project)
|
||||
}
|
||||
|
||||
// Get total count with optimized query
|
||||
var totalQuery string
|
||||
var totalArgs []interface{}
|
||||
|
||||
if search != "" {
|
||||
totalQuery = `
|
||||
SELECT COUNT(DISTINCT id)
|
||||
FROM project_stats
|
||||
WHERE (owner_id = $1 OR id IN (
|
||||
SELECT DISTINCT project_id FROM project_members WHERE user_id = $1
|
||||
)) AND (name ILIKE $2 OR description ILIKE $2)
|
||||
`
|
||||
totalArgs = []interface{}{userID, "%" + search + "%"}
|
||||
} else {
|
||||
totalQuery = `
|
||||
SELECT COUNT(DISTINCT id)
|
||||
FROM project_stats
|
||||
WHERE owner_id = $1 OR id IN (
|
||||
SELECT DISTINCT project_id FROM project_members WHERE user_id = $1
|
||||
)
|
||||
`
|
||||
totalArgs = []interface{}{userID}
|
||||
}
|
||||
|
||||
var total int
|
||||
err = db.QueryRowContext(ctx, totalQuery, totalArgs...).Scan(&total)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database count error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Batch fetch stats for all projects
|
||||
if len(projects) > 0 {
|
||||
projectIDs := make([]string, len(projects))
|
||||
for i, p := range projects {
|
||||
projectIDs[i] = p.ID
|
||||
}
|
||||
|
||||
statsMap := getBatchProjectStats(ctx, db, projectIDs)
|
||||
|
||||
for i := range projects {
|
||||
if stats, exists := statsMap[projects[i].ID]; exists {
|
||||
projects[i].Stats = stats
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"projects": projects,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"pages": (total + limit - 1) / limit,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// getBatchProjectStats fetches stats for multiple projects efficiently
|
||||
func getBatchProjectStats(ctx context.Context, db *database.DB, projectIDs []string) map[string]ProjectStats {
|
||||
if len(projectIDs) == 0 {
|
||||
return make(map[string]ProjectStats)
|
||||
}
|
||||
|
||||
// Create placeholders for IN clause
|
||||
placeholders := make([]string, len(projectIDs))
|
||||
args := make([]interface{}, len(projectIDs))
|
||||
for i, id := range projectIDs {
|
||||
placeholders[i] = "$" + strconv.Itoa(i+1)
|
||||
args[i] = id
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
project_id,
|
||||
COUNT(DISTINCT id) as service_count,
|
||||
COUNT(DISTINCT deployment_id) as deployment_count,
|
||||
COUNT(DISTINCT CASE WHEN status = 'running' THEN id END) as running_services,
|
||||
MAX(last_deployment) as last_deployment
|
||||
FROM (
|
||||
SELECT
|
||||
s.project_id,
|
||||
s.id,
|
||||
d.id as deployment_id,
|
||||
s.status,
|
||||
d.created_at as last_deployment
|
||||
FROM services s
|
||||
LEFT JOIN deployments d ON s.id = d.service_id
|
||||
WHERE s.project_id IN (` + strings.Join(placeholders, ",") + `)
|
||||
) sub
|
||||
GROUP BY project_id
|
||||
`
|
||||
|
||||
rows, err := db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return make(map[string]ProjectStats)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
statsMap := make(map[string]ProjectStats)
|
||||
for rows.Next() {
|
||||
var projectID string
|
||||
var stats ProjectStats
|
||||
var lastDeployment sql.NullTime
|
||||
|
||||
err := rows.Scan(&projectID, &stats.ServiceCount, &stats.DeploymentCount, &stats.RunningServices, &lastDeployment)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if lastDeployment.Valid {
|
||||
deploymentStr := lastDeployment.Time.Format(time.RFC3339)
|
||||
stats.LastDeployment = &deploymentStr
|
||||
}
|
||||
|
||||
statsMap[projectID] = stats
|
||||
}
|
||||
|
||||
return statsMap
|
||||
}
|
||||
|
||||
func handleCreateProject(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
var req CreateProjectRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var project Project
|
||||
err := db.QueryRow(`
|
||||
INSERT INTO projects (name, description, owner_id)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, name, description, owner_id, created_at, updated_at
|
||||
`, req.Name, req.Description, userID).Scan(&project.ID, &project.Name, &project.Description, &project.OwnerID, &project.CreatedAt, &project.UpdatedAt)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create project"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create default environments
|
||||
environments := []string{"production", "preview", "development"}
|
||||
for _, env := range environments {
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO environments (name, project_id)
|
||||
VALUES ($1, $2)
|
||||
`, env, project.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create environments"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, project)
|
||||
}
|
||||
|
||||
func handleGetProject(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
projectID := c.Param("id")
|
||||
|
||||
// Validate UUID
|
||||
if _, err := uuid.Parse(projectID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var project Project
|
||||
err := db.QueryRow(`
|
||||
SELECT p.id, p.name, p.description, p.owner_id, p.created_at, p.updated_at
|
||||
FROM projects p
|
||||
WHERE p.id = $1 AND (p.owner_id = $2 OR EXISTS (
|
||||
SELECT 1 FROM project_members pm
|
||||
WHERE pm.project_id = p.id AND pm.user_id = $2
|
||||
))
|
||||
`, projectID, userID).Scan(&project.ID, &project.Name, &project.Description, &project.OwnerID, &project.CreatedAt, &project.UpdatedAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, project)
|
||||
}
|
||||
|
||||
func handleUpdateProject(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
projectID := c.Param("id")
|
||||
|
||||
// Validate UUID
|
||||
if _, err := uuid.Parse(projectID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is owner or admin
|
||||
var role string
|
||||
err := db.QueryRow(`
|
||||
SELECT CASE
|
||||
WHEN p.owner_id = $1 THEN 'owner'
|
||||
ELSE pm.role
|
||||
END as role
|
||||
FROM projects p
|
||||
LEFT JOIN project_members pm ON p.id = pm.project_id AND pm.user_id = $1
|
||||
WHERE p.id = $2
|
||||
`, userID, projectID).Scan(&role)
|
||||
|
||||
if err == sql.ErrNoRows || role == "" || role == "viewer" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
|
||||
return
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateProjectRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
UPDATE projects
|
||||
SET name = COALESCE($1, name), description = COALESCE($2, description)
|
||||
WHERE id = $3
|
||||
`, req.Name, req.Description, projectID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update project"})
|
||||
return
|
||||
}
|
||||
|
||||
// Return updated project
|
||||
handleGetProject(c)
|
||||
}
|
||||
|
||||
func handleDeleteProject(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
projectID := c.Param("id")
|
||||
|
||||
// Validate UUID
|
||||
if _, err := uuid.Parse(projectID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is owner
|
||||
var ownerID string
|
||||
err := db.QueryRow("SELECT owner_id FROM projects WHERE id = $1", projectID).Scan(&ownerID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
if ownerID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Only project owners can delete projects"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete project (cascading deletes will handle related records)
|
||||
_, err = db.Exec("DELETE FROM projects WHERE id = $1", projectID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete project"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Project deleted successfully"})
|
||||
}
|
||||
@@ -1,480 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"containr/internal/proxmox"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ProxmoxHandler handles Proxmox-related API endpoints
|
||||
type ProxmoxHandler struct {
|
||||
service *proxmox.Service
|
||||
}
|
||||
|
||||
// NewProxmoxHandler creates a new Proxmox handler
|
||||
func NewProxmoxHandler(service *proxmox.Service) *ProxmoxHandler {
|
||||
return &ProxmoxHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterProxmoxRoutes registers Proxmox API routes
|
||||
func RegisterProxmoxRoutes(router *gin.Engine, service *proxmox.Service) {
|
||||
handler := NewProxmoxHandler(service)
|
||||
|
||||
proxmox := router.Group("/api/proxmox")
|
||||
{
|
||||
// Cluster and node management
|
||||
proxmox.GET("/cluster/status", handler.getClusterStatus)
|
||||
proxmox.GET("/nodes", handler.getNodes)
|
||||
proxmox.GET("/nodes/:nodeName/stats", handler.getNodeStats)
|
||||
proxmox.GET("/nodes/:nodeName/templates", handler.getTemplates)
|
||||
|
||||
// VM management
|
||||
proxmox.GET("/vms", handler.getAllVMs)
|
||||
proxmox.GET("/vms/:vmid/status", handler.getVMStatus)
|
||||
proxmox.POST("/vms", handler.createVM)
|
||||
proxmox.POST("/vms/:vmid/start", handler.startVM)
|
||||
proxmox.POST("/vms/:vmid/stop", handler.stopVM)
|
||||
proxmox.DELETE("/vms/:vmid", handler.deleteVM)
|
||||
|
||||
// Container management
|
||||
proxmox.GET("/containers", handler.getAllContainers)
|
||||
proxmox.POST("/containers", handler.createContainer)
|
||||
proxmox.POST("/containers/:vmid/start", handler.startContainer)
|
||||
proxmox.POST("/containers/:vmid/stop", handler.stopContainer)
|
||||
proxmox.DELETE("/containers/:vmid", handler.deleteContainer)
|
||||
|
||||
// Resource management
|
||||
proxmox.GET("/resources/usage", handler.getResourceUsage)
|
||||
proxmox.GET("/health", handler.healthCheck)
|
||||
}
|
||||
}
|
||||
|
||||
// getClusterStatus returns the overall cluster status
|
||||
func (h *ProxmoxHandler) getClusterStatus(c *gin.Context) {
|
||||
status, err := h.service.GetClusterStatus()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": status})
|
||||
}
|
||||
|
||||
// getNodes returns all nodes in the cluster
|
||||
func (h *ProxmoxHandler) getNodes(c *gin.Context) {
|
||||
nodes, err := h.service.GetAllNodes()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": nodes})
|
||||
}
|
||||
|
||||
// getNodeStats returns detailed statistics for a specific node
|
||||
func (h *ProxmoxHandler) getNodeStats(c *gin.Context) {
|
||||
nodeName := c.Param("nodeName")
|
||||
|
||||
stats, err := h.service.GetNodeStats(nodeName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": stats})
|
||||
}
|
||||
|
||||
// getTemplates returns available VM and container templates
|
||||
func (h *ProxmoxHandler) getTemplates(c *gin.Context) {
|
||||
nodeName := c.Param("nodeName")
|
||||
|
||||
templates, err := h.service.GetAvailableTemplates(nodeName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": templates})
|
||||
}
|
||||
|
||||
// getAllVMs returns all VMs across all nodes
|
||||
func (h *ProxmoxHandler) getAllVMs(c *gin.Context) {
|
||||
vms, err := h.service.GetAllVMs()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": vms})
|
||||
}
|
||||
|
||||
// getVMStatus returns the status of a specific VM
|
||||
func (h *ProxmoxHandler) getVMStatus(c *gin.Context) {
|
||||
vmidStr := c.Param("vmid")
|
||||
vmid, err := strconv.Atoi(vmidStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid VM ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// For now, we'll need to determine the node - this could be improved
|
||||
// by maintaining a VM-to-node mapping
|
||||
nodes, err := h.service.GetAllNodes()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Try each node until we find the VM
|
||||
for _, node := range nodes {
|
||||
if node.Status == "online" {
|
||||
status, err := h.service.GetInstanceStatus(node.Node, vmid, "qemu")
|
||||
if err == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"data": status})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "VM not found"})
|
||||
}
|
||||
|
||||
// createVM creates a new VM
|
||||
func (h *ProxmoxHandler) createVM(c *gin.Context) {
|
||||
var config proxmox.ServiceVMConfig
|
||||
if err := c.ShouldBindJSON(&config); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// For now, use the first available online node
|
||||
// In a production system, you'd want smarter node selection
|
||||
nodes, err := h.service.GetAllNodes()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var targetNode string
|
||||
for _, node := range nodes {
|
||||
if node.Status == "online" {
|
||||
targetNode = node.Node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetNode == "" {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "No online nodes available"})
|
||||
return
|
||||
}
|
||||
|
||||
vm, err := h.service.CreateServiceVM(targetNode, config)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"data": vm})
|
||||
}
|
||||
|
||||
// startVM starts a VM
|
||||
func (h *ProxmoxHandler) startVM(c *gin.Context) {
|
||||
vmidStr := c.Param("vmid")
|
||||
vmid, err := strconv.Atoi(vmidStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid VM ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Find which node the VM is on
|
||||
vms, err := h.service.GetAllVMs()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var nodeName string
|
||||
for _, vm := range vms {
|
||||
if vm.VMID == vmid {
|
||||
nodeName = vm.Node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nodeName == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "VM not found"})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.service.StartInstance(nodeName, vmid, "qemu")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "VM started successfully"})
|
||||
}
|
||||
|
||||
// stopVM stops a VM
|
||||
func (h *ProxmoxHandler) stopVM(c *gin.Context) {
|
||||
vmidStr := c.Param("vmid")
|
||||
vmid, err := strconv.Atoi(vmidStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid VM ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Find which node the VM is on
|
||||
vms, err := h.service.GetAllVMs()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var nodeName string
|
||||
for _, vm := range vms {
|
||||
if vm.VMID == vmid {
|
||||
nodeName = vm.Node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nodeName == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "VM not found"})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.service.StopInstance(nodeName, vmid, "qemu")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "VM stopped successfully"})
|
||||
}
|
||||
|
||||
// deleteVM deletes a VM
|
||||
func (h *ProxmoxHandler) deleteVM(c *gin.Context) {
|
||||
vmidStr := c.Param("vmid")
|
||||
vmid, err := strconv.Atoi(vmidStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid VM ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Find which node the VM is on
|
||||
vms, err := h.service.GetAllVMs()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var nodeName string
|
||||
for _, vm := range vms {
|
||||
if vm.VMID == vmid {
|
||||
nodeName = vm.Node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nodeName == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "VM not found"})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.service.DeleteInstance(nodeName, vmid, "qemu")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "VM deleted successfully"})
|
||||
}
|
||||
|
||||
// getAllContainers returns all containers across all nodes
|
||||
func (h *ProxmoxHandler) getAllContainers(c *gin.Context) {
|
||||
containers, err := h.service.GetAllContainers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": containers})
|
||||
}
|
||||
|
||||
// createContainer creates a new container
|
||||
func (h *ProxmoxHandler) createContainer(c *gin.Context) {
|
||||
var config proxmox.ServiceContainerConfig
|
||||
if err := c.ShouldBindJSON(&config); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// For now, use the first available online node
|
||||
nodes, err := h.service.GetAllNodes()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var targetNode string
|
||||
for _, node := range nodes {
|
||||
if node.Status == "online" {
|
||||
targetNode = node.Node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetNode == "" {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "No online nodes available"})
|
||||
return
|
||||
}
|
||||
|
||||
container, err := h.service.CreateServiceContainer(targetNode, config)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"data": container})
|
||||
}
|
||||
|
||||
// startContainer starts a container
|
||||
func (h *ProxmoxHandler) startContainer(c *gin.Context) {
|
||||
vmidStr := c.Param("vmid")
|
||||
vmid, err := strconv.Atoi(vmidStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid container ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Find which node the container is on
|
||||
containers, err := h.service.GetAllContainers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var nodeName string
|
||||
for _, container := range containers {
|
||||
if container.VMID == vmid {
|
||||
nodeName = container.Node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nodeName == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Container not found"})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.service.StartInstance(nodeName, vmid, "lxc")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Container started successfully"})
|
||||
}
|
||||
|
||||
// stopContainer stops a container
|
||||
func (h *ProxmoxHandler) stopContainer(c *gin.Context) {
|
||||
vmidStr := c.Param("vmid")
|
||||
vmid, err := strconv.Atoi(vmidStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid container ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Find which node the container is on
|
||||
containers, err := h.service.GetAllContainers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var nodeName string
|
||||
for _, container := range containers {
|
||||
if container.VMID == vmid {
|
||||
nodeName = container.Node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nodeName == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Container not found"})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.service.StopInstance(nodeName, vmid, "lxc")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Container stopped successfully"})
|
||||
}
|
||||
|
||||
// deleteContainer deletes a container
|
||||
func (h *ProxmoxHandler) deleteContainer(c *gin.Context) {
|
||||
vmidStr := c.Param("vmid")
|
||||
vmid, err := strconv.Atoi(vmidStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid container ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Find which node the container is on
|
||||
containers, err := h.service.GetAllContainers()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var nodeName string
|
||||
for _, container := range containers {
|
||||
if container.VMID == vmid {
|
||||
nodeName = container.Node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if nodeName == "" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Container not found"})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.service.DeleteInstance(nodeName, vmid, "lxc")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Container deleted successfully"})
|
||||
}
|
||||
|
||||
// getResourceUsage returns resource usage across the cluster
|
||||
func (h *ProxmoxHandler) getResourceUsage(c *gin.Context) {
|
||||
usage, err := h.service.GetResourceUsage()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": usage})
|
||||
}
|
||||
|
||||
// healthCheck validates the connection to Proxmox
|
||||
func (h *ProxmoxHandler) healthCheck(c *gin.Context) {
|
||||
err := h.service.ValidateConnection()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"status": "unhealthy",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "healthy",
|
||||
"message": "Proxmox connection is working",
|
||||
})
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"containr/internal/build"
|
||||
"containr/internal/config"
|
||||
"containr/internal/database"
|
||||
"containr/internal/deployment"
|
||||
"containr/internal/docker"
|
||||
"containr/internal/metrics"
|
||||
"containr/internal/middleware"
|
||||
"containr/internal/proxmox"
|
||||
"containr/internal/scaling"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SetupRoutes(router *gin.Engine, db *database.DB, redis *database.Redis, cfg *config.Config) {
|
||||
// Initialize Docker client (non-fatal if it fails)
|
||||
var dockerClient *docker.Client
|
||||
var buildManager *build.BuildManager
|
||||
var deploymentEngine *deployment.DeploymentEngine
|
||||
|
||||
if client, err := docker.NewClient(); err != nil {
|
||||
log.Printf("Warning: Failed to initialize Docker client: %v", err)
|
||||
log.Printf("Docker-related features will be disabled")
|
||||
} else {
|
||||
dockerClient = client
|
||||
buildManager = build.NewBuildManager("/tmp/containr-builds", dockerClient)
|
||||
deploymentEngine = deployment.NewDeploymentEngine(buildManager, dockerClient)
|
||||
}
|
||||
|
||||
// Initialize build handler
|
||||
buildHandler := NewBuildHandler(buildManager, dockerClient)
|
||||
|
||||
// Initialize scheduler and metrics systems
|
||||
scheduler := deployment.NewScheduler()
|
||||
metricsStorage := metrics.NewInMemoryMetricsStorage() // Use in-memory for now
|
||||
metricsCollector := metrics.NewMetricsCollector(scheduler, metricsStorage)
|
||||
autoScaler := scaling.NewAutoScaler(scheduler, metricsCollector)
|
||||
|
||||
// Initialize scaling handler
|
||||
scalingHandler := NewScalingHandler(autoScaler)
|
||||
|
||||
// Initialize GORM for agent system
|
||||
gormDB, err := gorm.Open(postgres.Open(cfg.DatabaseURL), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("Failed to initialize GORM: " + err.Error())
|
||||
}
|
||||
|
||||
// Initialize agent handler
|
||||
agentHandler := NewNodeAgentHandler(gormDB)
|
||||
|
||||
// Initialize database handler
|
||||
databaseHandler := NewDatabaseHandler(db.DB)
|
||||
|
||||
// Initialize security handler
|
||||
securityHandler := NewSecurityHandler(db, cfg.JWTSecret)
|
||||
|
||||
// Initialize Proxmox service if configured
|
||||
var proxmoxService *proxmox.Service
|
||||
if cfg.Proxmox.BaseURL != "" {
|
||||
proxmoxConfig := proxmox.Config{
|
||||
BaseURL: cfg.Proxmox.BaseURL,
|
||||
Username: cfg.Proxmox.Username,
|
||||
Password: cfg.Proxmox.Password,
|
||||
TokenID: cfg.Proxmox.TokenID,
|
||||
Token: cfg.Proxmox.Token,
|
||||
}
|
||||
proxmoxService = proxmox.NewService(proxmoxConfig)
|
||||
|
||||
// Register Proxmox routes
|
||||
RegisterProxmoxRoutes(router, proxmoxService)
|
||||
}
|
||||
|
||||
// Add database and JWT secret to gin context for handlers
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set("db", db)
|
||||
c.Set("redis", redis)
|
||||
c.Set("jwt_secret", cfg.JWTSecret)
|
||||
c.Set("docker_client", dockerClient)
|
||||
c.Set("build_manager", buildManager)
|
||||
if deploymentEngine != nil {
|
||||
c.Set("deployment_engine", deploymentEngine)
|
||||
}
|
||||
c.Set("scheduler", scheduler)
|
||||
c.Set("metrics_collector", metricsCollector)
|
||||
c.Set("auto_scaler", autoScaler)
|
||||
c.Set("scaling_handler", scalingHandler)
|
||||
c.Set("gorm_db", gormDB)
|
||||
if proxmoxService != nil {
|
||||
c.Set("proxmox", proxmoxService)
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
|
||||
// Health check endpoint
|
||||
router.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"service": "containr-api",
|
||||
})
|
||||
})
|
||||
|
||||
// API v1 routes
|
||||
v1 := router.Group("/api/v1")
|
||||
{
|
||||
// Public routes (no authentication required)
|
||||
public := v1.Group("/")
|
||||
{
|
||||
public.POST("/auth/login", handleLogin)
|
||||
public.POST("/auth/register", handleRegister)
|
||||
}
|
||||
|
||||
// Protected routes (authentication required)
|
||||
protected := v1.Group("/")
|
||||
protected.Use(middleware.Auth(cfg.JWTSecret))
|
||||
{
|
||||
// User routes
|
||||
protected.GET("/user/profile", handleGetProfile)
|
||||
protected.PUT("/user/profile", handleUpdateProfile)
|
||||
|
||||
// Project routes
|
||||
protected.GET("/projects", handleGetProjects)
|
||||
protected.POST("/projects", handleCreateProject)
|
||||
|
||||
// Service routes (nested under projects)
|
||||
protected.GET("/projects/:id/services", handleGetServices)
|
||||
protected.POST("/projects/:id/services", handleCreateService)
|
||||
|
||||
// Generic project routes
|
||||
protected.GET("/projects/:id", handleGetProject)
|
||||
protected.PUT("/projects/:id", handleUpdateProject)
|
||||
protected.DELETE("/projects/:id", handleDeleteProject)
|
||||
|
||||
// Service routes
|
||||
protected.GET("/services/:id", handleGetService)
|
||||
protected.PUT("/services/:id", handleUpdateService)
|
||||
protected.DELETE("/services/:id", handleDeleteService)
|
||||
|
||||
// Deployment routes
|
||||
protected.GET("/services/:id/deployments", handleGetDeployments)
|
||||
protected.POST("/services/:id/deployments", handleCreateDeployment)
|
||||
protected.GET("/deployments/:id", handleGetDeployment)
|
||||
protected.POST("/deployments/:id/rollback", handleRollbackDeployment)
|
||||
|
||||
// Environment variables routes
|
||||
protected.GET("/services/:id/variables", handleGetVariables)
|
||||
protected.PUT("/services/:id/variables", handleUpdateVariables)
|
||||
|
||||
// Logs routes
|
||||
protected.GET("/services/:id/logs", handleGetLogs)
|
||||
protected.GET("/deployments/:id/logs", handleGetDeploymentLogs)
|
||||
|
||||
// Git integration routes
|
||||
protected.GET("/git/providers", handleGetGitProviders)
|
||||
protected.POST("/git/providers", handleCreateGitProvider)
|
||||
protected.GET("/git/providers/:providerId/repositories", handleGetGitRepositories)
|
||||
protected.POST("/git/repositories/connect", handleConnectGitRepository)
|
||||
protected.GET("/git/repositories", handleGetConnectedRepositories)
|
||||
protected.POST("/git/webhooks", handleCreateWebhook)
|
||||
|
||||
// Build routes
|
||||
protected.POST("/builds", buildHandler.StartBuild)
|
||||
protected.GET("/builds", buildHandler.ListBuilds)
|
||||
protected.GET("/builds/:id", buildHandler.GetBuildStatus)
|
||||
protected.POST("/builds/:id/cancel", buildHandler.CancelBuild)
|
||||
protected.GET("/builds/:id/logs", buildHandler.GetBuildLogs)
|
||||
protected.POST("/builds/plan", buildHandler.GetBuildPlan)
|
||||
protected.GET("/builds/detect", buildHandler.DetectBuildType)
|
||||
|
||||
// Scaling routes
|
||||
scalingHandler.RegisterRoutes(protected)
|
||||
|
||||
// Database routes
|
||||
protected.GET("/databases", databaseHandler.GetDatabases)
|
||||
protected.POST("/databases", databaseHandler.CreateDatabase)
|
||||
protected.GET("/databases/:id", databaseHandler.GetDatabase)
|
||||
protected.PUT("/databases/:id", databaseHandler.UpdateDatabase)
|
||||
protected.DELETE("/databases/:id", databaseHandler.DeleteDatabase)
|
||||
protected.POST("/databases/:id/action", databaseHandler.PerformDatabaseAction)
|
||||
protected.POST("/databases/:id/backup", databaseHandler.CreateBackup)
|
||||
protected.POST("/databases/:id/restore", databaseHandler.RestoreBackup)
|
||||
|
||||
// Node Agent routes
|
||||
api := router.Group("/api")
|
||||
agentHandler.SetupRoutes(api)
|
||||
|
||||
// Preview Environments routes
|
||||
protected.GET("/projects/:id/preview-environments", handleGetPreviewEnvironments)
|
||||
protected.POST("/projects/:id/preview-environments", handleCreatePreviewEnvironment)
|
||||
protected.GET("/preview-environments/:id", handleGetPreviewEnvironment)
|
||||
protected.PUT("/preview-environments/:id", handleUpdatePreviewEnvironment)
|
||||
protected.DELETE("/preview-environments/:id", handleDeletePreviewEnvironment)
|
||||
protected.POST("/preview-environments/:id/promote", handlePromotePreviewEnvironment)
|
||||
protected.POST("/preview-environments/cleanup-expired", handleCleanupExpiredPreviewEnvironments)
|
||||
|
||||
// Security routes
|
||||
protected.POST("/security/scans", securityHandler.StartSecurityScan)
|
||||
protected.GET("/security/scans/:id", securityHandler.GetSecurityScan)
|
||||
protected.GET("/projects/:id/security/history", securityHandler.GetProjectSecurityHistory)
|
||||
protected.GET("/projects/:id/vulnerabilities", securityHandler.GetVulnerabilities)
|
||||
protected.PUT("/vulnerabilities/:id", securityHandler.UpdateVulnerability)
|
||||
protected.POST("/security/compliance/assess", securityHandler.StartComplianceAssessment)
|
||||
protected.GET("/security/compliance/reports/:id", securityHandler.GetComplianceReport)
|
||||
protected.GET("/security/compliance/frameworks", securityHandler.GetComplianceFrameworks)
|
||||
protected.POST("/security/compliance/gdpr/init", securityHandler.InitializeGDPRFramework)
|
||||
protected.GET("/projects/:id/security/metrics", securityHandler.GetSecurityMetrics)
|
||||
protected.GET("/projects/:id/security/audit-logs", securityHandler.GetAuditLogs)
|
||||
|
||||
// WebSocket endpoint
|
||||
protected.GET("/ws", handleWebSocket)
|
||||
|
||||
// Templates routes
|
||||
protected.GET("/templates", handleGetTemplates)
|
||||
protected.GET("/templates/:id", handleGetTemplate)
|
||||
protected.POST("/templates/:id/deploy", handleCreateFromTemplate)
|
||||
|
||||
// Cron Jobs routes
|
||||
protected.GET("/cron-jobs", handleGetCronJobs)
|
||||
protected.POST("/cron-jobs", handleCreateCronJob)
|
||||
protected.GET("/cron-jobs/:id", handleGetCronJob)
|
||||
protected.PUT("/cron-jobs/:id", handleUpdateCronJob)
|
||||
protected.DELETE("/cron-jobs/:id", handleDeleteCronJob)
|
||||
protected.GET("/cron-jobs/:id/executions", handleGetCronExecutions)
|
||||
protected.POST("/cron-jobs/:id/trigger", handleTriggerCronJob)
|
||||
|
||||
// Audit Logs routes
|
||||
protected.GET("/audit-logs", handleGetAuditLogs)
|
||||
protected.GET("/audit-logs/:resource/:id", handleGetResourceAuditLogs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"containr/internal/scaling"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ScalingHandler handles scaling-related API endpoints
|
||||
type ScalingHandler struct {
|
||||
autoScaler *scaling.AutoScaler
|
||||
}
|
||||
|
||||
// NewScalingHandler creates a new scaling handler
|
||||
func NewScalingHandler(autoScaler *scaling.AutoScaler) *ScalingHandler {
|
||||
return &ScalingHandler{
|
||||
autoScaler: autoScaler,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers scaling routes
|
||||
func (h *ScalingHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
scaling := router.Group("/scaling")
|
||||
{
|
||||
scaling.GET("/policies", h.GetScalingPolicies)
|
||||
scaling.POST("/policies", h.SetScalingPolicy)
|
||||
scaling.GET("/policies/:serviceId", h.GetScalingPolicy)
|
||||
scaling.PUT("/policies/:serviceId", h.UpdateScalingPolicy)
|
||||
scaling.DELETE("/policies/:serviceId", h.DeleteScalingPolicy)
|
||||
|
||||
scaling.GET("/services", h.GetServiceStates)
|
||||
scaling.GET("/services/:serviceId", h.GetServiceState)
|
||||
scaling.GET("/services/:serviceId/history", h.GetScalingHistory)
|
||||
|
||||
scaling.POST("/services/:serviceId/scale", h.ManualScale)
|
||||
|
||||
scaling.GET("/status", h.GetScalingStatus)
|
||||
scaling.POST("/enable", h.EnableAutoScaler)
|
||||
scaling.POST("/disable", h.DisableAutoScaler)
|
||||
|
||||
scaling.GET("/metrics", h.GetScalingMetrics)
|
||||
scaling.GET("/events", h.GetScalingEvents)
|
||||
}
|
||||
}
|
||||
|
||||
// GetScalingPolicies returns all scaling policies
|
||||
func (h *ScalingHandler) GetScalingPolicies(c *gin.Context) {
|
||||
states := h.autoScaler.GetAllServiceStates()
|
||||
|
||||
policies := make([]*scaling.ScalingPolicy, 0)
|
||||
for _, state := range states {
|
||||
if state.Policy != nil {
|
||||
policies = append(policies, state.Policy)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"policies": policies,
|
||||
"count": len(policies),
|
||||
})
|
||||
}
|
||||
|
||||
// SetScalingPolicy creates or updates a scaling policy
|
||||
func (h *ScalingHandler) SetScalingPolicy(c *gin.Context) {
|
||||
var policy scaling.ScalingPolicy
|
||||
if err := c.ShouldBindJSON(&policy); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.autoScaler.SetScalingPolicy(&policy); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Scaling policy set successfully",
|
||||
"policy": policy,
|
||||
})
|
||||
}
|
||||
|
||||
// GetScalingPolicy returns a specific scaling policy
|
||||
func (h *ScalingHandler) GetScalingPolicy(c *gin.Context) {
|
||||
serviceID := c.Param("serviceId")
|
||||
|
||||
policy, err := h.autoScaler.GetScalingPolicy(serviceID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"policy": policy,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateScalingPolicy updates an existing scaling policy
|
||||
func (h *ScalingHandler) UpdateScalingPolicy(c *gin.Context) {
|
||||
serviceID := c.Param("serviceId")
|
||||
|
||||
var policy scaling.ScalingPolicy
|
||||
if err := c.ShouldBindJSON(&policy); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the service ID matches
|
||||
policy.ServiceID = serviceID
|
||||
|
||||
if err := h.autoScaler.SetScalingPolicy(&policy); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Scaling policy updated successfully",
|
||||
"policy": policy,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteScalingPolicy removes a scaling policy
|
||||
func (h *ScalingHandler) DeleteScalingPolicy(c *gin.Context) {
|
||||
serviceID := c.Param("serviceId")
|
||||
|
||||
// Set policy to disabled instead of deleting
|
||||
policy, err := h.autoScaler.GetScalingPolicy(serviceID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
policy.Enabled = false
|
||||
if err := h.autoScaler.SetScalingPolicy(policy); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Scaling policy disabled successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetServiceStates returns all service scaling states
|
||||
func (h *ScalingHandler) GetServiceStates(c *gin.Context) {
|
||||
states := h.autoScaler.GetAllServiceStates()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"services": states,
|
||||
"count": len(states),
|
||||
})
|
||||
}
|
||||
|
||||
// GetServiceState returns a specific service's scaling state
|
||||
func (h *ScalingHandler) GetServiceState(c *gin.Context) {
|
||||
serviceID := c.Param("serviceId")
|
||||
|
||||
state, err := h.autoScaler.GetServiceState(serviceID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"state": state,
|
||||
})
|
||||
}
|
||||
|
||||
// GetScalingHistory returns scaling history for a service
|
||||
func (h *ScalingHandler) GetScalingHistory(c *gin.Context) {
|
||||
serviceID := c.Param("serviceId")
|
||||
|
||||
// TODO: Implement scaling history retrieval from database
|
||||
// For now, return mock data
|
||||
history := []map[string]interface{}{
|
||||
{
|
||||
"timestamp": time.Now().Add(-2 * time.Hour),
|
||||
"action": "scale_up",
|
||||
"from": 2,
|
||||
"to": 3,
|
||||
"reason": "CPU usage above target",
|
||||
},
|
||||
{
|
||||
"timestamp": time.Now().Add(-1 * time.Hour),
|
||||
"action": "scale_down",
|
||||
"from": 3,
|
||||
"to": 2,
|
||||
"reason": "CPU usage below target",
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"service_id": serviceID,
|
||||
"history": history,
|
||||
"count": len(history),
|
||||
})
|
||||
}
|
||||
|
||||
// ManualScale performs manual scaling of a service
|
||||
func (h *ScalingHandler) ManualScale(c *gin.Context) {
|
||||
serviceID := c.Param("serviceId")
|
||||
|
||||
var request struct {
|
||||
Replicas int `json:"replicas" binding:"required,min=1,max=20"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Implement manual scaling
|
||||
// This would bypass the auto-scaler and directly scale the service
|
||||
|
||||
c.JSON(http.StatusAccepted, gin.H{
|
||||
"message": "Manual scaling initiated",
|
||||
"service_id": serviceID,
|
||||
"replicas": request.Replicas,
|
||||
"reason": request.Reason,
|
||||
})
|
||||
}
|
||||
|
||||
// GetScalingStatus returns the overall status of the auto-scaler
|
||||
func (h *ScalingHandler) GetScalingStatus(c *gin.Context) {
|
||||
summary := h.autoScaler.GetScalingSummary()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "active",
|
||||
"summary": summary,
|
||||
"enabled": h.autoScaler.IsEnabled(),
|
||||
})
|
||||
}
|
||||
|
||||
// EnableAutoScaler enables the auto-scaler
|
||||
func (h *ScalingHandler) EnableAutoScaler(c *gin.Context) {
|
||||
h.autoScaler.Enable()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Auto-scaler enabled",
|
||||
"enabled": true,
|
||||
})
|
||||
}
|
||||
|
||||
// DisableAutoScaler disables the auto-scaler
|
||||
func (h *ScalingHandler) DisableAutoScaler(c *gin.Context) {
|
||||
h.autoScaler.Disable()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Auto-scaler disabled",
|
||||
"enabled": false,
|
||||
})
|
||||
}
|
||||
|
||||
// GetScalingMetrics returns scaling-related metrics
|
||||
func (h *ScalingHandler) GetScalingMetrics(c *gin.Context) {
|
||||
summary := h.autoScaler.GetScalingSummary()
|
||||
|
||||
// Add additional metrics
|
||||
metrics := map[string]interface{}{
|
||||
"total_services": summary["total_services"],
|
||||
"enabled_services": summary["enabled_services"],
|
||||
"total_replicas": summary["total_replicas"],
|
||||
"services_scaling_up": summary["scaling_up"],
|
||||
"services_scaling_down": summary["scaling_down"],
|
||||
"auto_scaler_enabled": summary["enabled"],
|
||||
"check_interval_seconds": summary["check_interval"],
|
||||
"timestamp": time.Now(),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"metrics": metrics,
|
||||
})
|
||||
}
|
||||
|
||||
// GetScalingEvents returns recent scaling events
|
||||
func (h *ScalingHandler) GetScalingEvents(c *gin.Context) {
|
||||
// Parse query parameters
|
||||
limitStr := c.DefaultQuery("limit", "50")
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
// TODO: Implement scaling events retrieval from database
|
||||
// For now, return mock data
|
||||
events := []map[string]interface{}{
|
||||
{
|
||||
"id": "evt_1",
|
||||
"service_id": "web-service",
|
||||
"action": "scale_up",
|
||||
"from": 2,
|
||||
"to": 3,
|
||||
"reason": "CPU usage (85%) above target (70%)",
|
||||
"timestamp": time.Now().Add(-30 * time.Minute),
|
||||
"cost_impact": 0.01,
|
||||
},
|
||||
{
|
||||
"id": "evt_2",
|
||||
"service_id": "api-service",
|
||||
"action": "scale_down",
|
||||
"from": 5,
|
||||
"to": 3,
|
||||
"reason": "Low request rate (10/s)",
|
||||
"timestamp": time.Now().Add(-1 * time.Hour),
|
||||
"cost_impact": -0.02,
|
||||
},
|
||||
}
|
||||
|
||||
// Limit results
|
||||
if len(events) > limit {
|
||||
events = events[:limit]
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"events": events,
|
||||
"count": len(events),
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
// ScalingPolicyTemplate represents a template for creating scaling policies
|
||||
type ScalingPolicyTemplate struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Template scaling.ScalingPolicy `json:"template"`
|
||||
}
|
||||
|
||||
// GetScalingPolicyTemplates returns predefined scaling policy templates
|
||||
func (h *ScalingHandler) GetScalingPolicyTemplates(c *gin.Context) {
|
||||
templates := []ScalingPolicyTemplate{
|
||||
{
|
||||
Name: "Web Application",
|
||||
Description: "Standard scaling policy for web applications",
|
||||
Template: scaling.ScalingPolicy{
|
||||
MinReplicas: 2,
|
||||
MaxReplicas: 10,
|
||||
TargetCPU: 70.0,
|
||||
TargetMemory: 80.0,
|
||||
ScaleUpCooldown: 3 * time.Minute,
|
||||
ScaleDownCooldown: 5 * time.Minute,
|
||||
ScaleUpStep: 1,
|
||||
ScaleDownStep: 1,
|
||||
Metrics: []string{"cpu", "memory", "requests_per_second"},
|
||||
Enabled: true,
|
||||
CostOptimization: &scaling.CostOptimization{
|
||||
MaxCostPerHour: 1.0,
|
||||
PreferEfficiency: true,
|
||||
IdleTimeout: 10 * time.Minute,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "API Service",
|
||||
Description: "Aggressive scaling for API services",
|
||||
Template: scaling.ScalingPolicy{
|
||||
MinReplicas: 1,
|
||||
MaxReplicas: 20,
|
||||
TargetCPU: 60.0,
|
||||
TargetMemory: 75.0,
|
||||
ScaleUpCooldown: 1 * time.Minute,
|
||||
ScaleDownCooldown: 3 * time.Minute,
|
||||
ScaleUpStep: 2,
|
||||
ScaleDownStep: 1,
|
||||
Metrics: []string{"cpu", "memory", "requests_per_second", "error_rate"},
|
||||
Enabled: true,
|
||||
CostOptimization: &scaling.CostOptimization{
|
||||
MaxCostPerHour: 2.0,
|
||||
PreferEfficiency: false,
|
||||
IdleTimeout: 5 * time.Minute,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Background Worker",
|
||||
Description: "Conservative scaling for background workers",
|
||||
Template: scaling.ScalingPolicy{
|
||||
MinReplicas: 1,
|
||||
MaxReplicas: 5,
|
||||
TargetCPU: 80.0,
|
||||
TargetMemory: 85.0,
|
||||
ScaleUpCooldown: 5 * time.Minute,
|
||||
ScaleDownCooldown: 10 * time.Minute,
|
||||
ScaleUpStep: 1,
|
||||
ScaleDownStep: 1,
|
||||
Metrics: []string{"cpu", "memory"},
|
||||
Enabled: true,
|
||||
CostOptimization: &scaling.CostOptimization{
|
||||
MaxCostPerHour: 0.5,
|
||||
PreferEfficiency: true,
|
||||
IdleTimeout: 15 * time.Minute,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"templates": templates,
|
||||
"count": len(templates),
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateScalingPolicy validates a scaling policy
|
||||
func (h *ScalingHandler) ValidateScalingPolicy(c *gin.Context) {
|
||||
var policy scaling.ScalingPolicy
|
||||
if err := c.ShouldBindJSON(&policy); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
errors := []string{}
|
||||
warnings := []string{}
|
||||
|
||||
// Validate policy
|
||||
if policy.MinReplicas < 1 {
|
||||
errors = append(errors, "min_replicas must be at least 1")
|
||||
}
|
||||
if policy.MaxReplicas < policy.MinReplicas {
|
||||
errors = append(errors, "max_replicas must be greater than or equal to min_replicas")
|
||||
}
|
||||
if policy.TargetCPU <= 0 || policy.TargetCPU > 100 {
|
||||
errors = append(errors, "target_cpu must be between 0 and 100")
|
||||
}
|
||||
if policy.TargetMemory <= 0 || policy.TargetMemory > 100 {
|
||||
errors = append(errors, "target_memory must be between 0 and 100")
|
||||
}
|
||||
if policy.ScaleUpStep < 1 {
|
||||
errors = append(errors, "scale_up_step must be at least 1")
|
||||
}
|
||||
if policy.ScaleDownStep < 1 {
|
||||
errors = append(errors, "scale_down_step must be at least 1")
|
||||
}
|
||||
|
||||
// Warnings
|
||||
if policy.MaxReplicas > 20 {
|
||||
warnings = append(warnings, "max_replicas greater than 20 may be costly")
|
||||
}
|
||||
if policy.ScaleUpCooldown < 1*time.Minute {
|
||||
warnings = append(warnings, "scale_up_cooldown less than 1 minute may cause thrashing")
|
||||
}
|
||||
if policy.ScaleDownCooldown < 2*time.Minute {
|
||||
warnings = append(warnings, "scale_down_cooldown less than 2 minutes may cause thrashing")
|
||||
}
|
||||
|
||||
valid := len(errors) == 0
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"valid": valid,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
})
|
||||
}
|
||||
@@ -1,612 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"containr/internal/security"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// SecurityHandler handles security-related API endpoints
|
||||
type SecurityHandler struct {
|
||||
db *database.DB
|
||||
scanner *security.Scanner
|
||||
complianceManager *security.ComplianceManager
|
||||
encryptionManager *security.EncryptionManager
|
||||
dataRetentionManager *security.DataRetentionManager
|
||||
auditLogger *security.AuditLogger
|
||||
}
|
||||
|
||||
// NewSecurityHandler creates a new security handler
|
||||
func NewSecurityHandler(db *database.DB, encryptionKey string) *SecurityHandler {
|
||||
encryptionManager, _ := security.NewEncryptionManager(encryptionKey)
|
||||
|
||||
return &SecurityHandler{
|
||||
db: db,
|
||||
scanner: security.NewScanner(db),
|
||||
complianceManager: security.NewComplianceManager(db),
|
||||
encryptionManager: encryptionManager,
|
||||
dataRetentionManager: security.NewDataRetentionManager(encryptionManager),
|
||||
auditLogger: security.NewAuditLogger(encryptionManager),
|
||||
}
|
||||
}
|
||||
|
||||
// StartSecurityScan starts a new security scan
|
||||
func (sh *SecurityHandler) StartSecurityScan(c *gin.Context) {
|
||||
var req struct {
|
||||
ProjectID string `json:"project_id" binding:"required"`
|
||||
ServiceID string `json:"service_id,omitempty"`
|
||||
ScanType string `json:"scan_type" binding:"required,oneof=dependency configuration comprehensive"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := sh.requireProjectAccess(c, req.ProjectID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if req.ServiceID != "" {
|
||||
if _, err := uuid.Parse(req.ServiceID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var serviceExists bool
|
||||
err := sh.db.QueryRow(
|
||||
`SELECT EXISTS(
|
||||
SELECT 1 FROM services WHERE id = $1 AND project_id = $2
|
||||
)`,
|
||||
req.ServiceID,
|
||||
req.ProjectID,
|
||||
).Scan(&serviceExists)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate service"})
|
||||
return
|
||||
}
|
||||
|
||||
if !serviceExists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Service not found in project"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Log audit event
|
||||
sh.auditLogger.LogSecurityEvent(userID, "security_scan_started", "project",
|
||||
map[string]interface{}{
|
||||
"project_id": req.ProjectID,
|
||||
"service_id": req.ServiceID,
|
||||
"scan_type": req.ScanType,
|
||||
}, c.ClientIP(), c.GetHeader("User-Agent"), true)
|
||||
|
||||
scan, err := sh.scanner.StartSecurityScan(req.ProjectID, req.ServiceID, req.ScanType)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start security scan"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusAccepted, scan)
|
||||
}
|
||||
|
||||
// GetSecurityScan retrieves a security scan
|
||||
func (sh *SecurityHandler) GetSecurityScan(c *gin.Context) {
|
||||
scanID := firstPathParam(c, "scanId", "id")
|
||||
if !sh.requireSecurityScanAccess(c, scanID) {
|
||||
return
|
||||
}
|
||||
|
||||
scan, err := sh.scanner.GetSecurityScan(scanID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Security scan not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, scan)
|
||||
}
|
||||
|
||||
// GetProjectSecurityHistory retrieves security scan history for a project
|
||||
func (sh *SecurityHandler) GetProjectSecurityHistory(c *gin.Context) {
|
||||
projectID := firstPathParam(c, "projectId", "id", "project_id")
|
||||
if _, ok := sh.requireProjectAccess(c, projectID); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
limit := 10
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 1000 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
scans, err := sh.scanner.GetProjectSecurityHistory(projectID, limit)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get security history"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"scans": scans})
|
||||
}
|
||||
|
||||
// GetVulnerabilities retrieves vulnerabilities for a project
|
||||
func (sh *SecurityHandler) GetVulnerabilities(c *gin.Context) {
|
||||
projectID := firstPathParam(c, "projectId", "id", "project_id")
|
||||
if _, ok := sh.requireProjectAccess(c, projectID); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Query vulnerabilities
|
||||
rows, err := sh.db.Query(`
|
||||
SELECT id, type, severity, title, description, service_id, status, found_at, resolved_at
|
||||
FROM vulnerabilities
|
||||
WHERE project_id = $1
|
||||
ORDER BY
|
||||
CASE severity
|
||||
WHEN 'critical' THEN 1
|
||||
WHEN 'high' THEN 2
|
||||
WHEN 'medium' THEN 3
|
||||
WHEN 'low' THEN 4
|
||||
END,
|
||||
found_at DESC
|
||||
`, projectID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerabilities"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var vulnerabilities []security.Vulnerability
|
||||
for rows.Next() {
|
||||
var vuln security.Vulnerability
|
||||
var resolvedAt *time.Time
|
||||
|
||||
err := rows.Scan(&vuln.ID, &vuln.Type, &vuln.Severity, &vuln.Title, &vuln.Description,
|
||||
&vuln.ServiceID, &vuln.Status, &vuln.FoundAt, &resolvedAt)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
vuln.ResolvedAt = resolvedAt
|
||||
vulnerabilities = append(vulnerabilities, vuln)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"vulnerabilities": vulnerabilities})
|
||||
}
|
||||
|
||||
// UpdateVulnerability updates a vulnerability status
|
||||
func (sh *SecurityHandler) UpdateVulnerability(c *gin.Context) {
|
||||
vulnID := firstPathParam(c, "vulnId", "id")
|
||||
userID, ok := sh.requireVulnerabilityAccess(c, vulnID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status" binding:"required,oneof=open resolved ignored"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var resolvedAt *time.Time
|
||||
if req.Status == "resolved" {
|
||||
now := time.Now()
|
||||
resolvedAt = &now
|
||||
}
|
||||
|
||||
_, err := sh.db.Exec(`
|
||||
UPDATE vulnerabilities
|
||||
SET status = $1, resolved_at = $2
|
||||
WHERE id = $3
|
||||
`, req.Status, resolvedAt, vulnID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update vulnerability"})
|
||||
return
|
||||
}
|
||||
|
||||
// Log audit event
|
||||
sh.auditLogger.LogSecurityEvent(userID, "vulnerability_updated", "vulnerability",
|
||||
map[string]interface{}{
|
||||
"vulnerability_id": vulnID,
|
||||
"new_status": req.Status,
|
||||
"notes": req.Notes,
|
||||
}, c.ClientIP(), c.GetHeader("User-Agent"), true)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "updated"})
|
||||
}
|
||||
|
||||
// StartComplianceAssessment starts a new compliance assessment
|
||||
func (sh *SecurityHandler) StartComplianceAssessment(c *gin.Context) {
|
||||
var req struct {
|
||||
ProjectID string `json:"project_id" binding:"required"`
|
||||
FrameworkID string `json:"framework_id" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := sh.requireProjectAccess(c, req.ProjectID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := uuid.Parse(req.FrameworkID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid framework ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var frameworkExists bool
|
||||
err := sh.db.QueryRow(
|
||||
`SELECT EXISTS(
|
||||
SELECT 1 FROM compliance_frameworks WHERE id = $1
|
||||
)`,
|
||||
req.FrameworkID,
|
||||
).Scan(&frameworkExists)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate framework"})
|
||||
return
|
||||
}
|
||||
if !frameworkExists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Compliance framework not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Log audit event
|
||||
sh.auditLogger.LogSecurityEvent(userID, "compliance_assessment_started", "project",
|
||||
map[string]interface{}{
|
||||
"project_id": req.ProjectID,
|
||||
"framework_id": req.FrameworkID,
|
||||
}, c.ClientIP(), c.GetHeader("User-Agent"), true)
|
||||
|
||||
report, err := sh.complianceManager.AssessCompliance(req.ProjectID, req.FrameworkID, userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start compliance assessment"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusAccepted, report)
|
||||
}
|
||||
|
||||
// GetComplianceReport retrieves a compliance report
|
||||
func (sh *SecurityHandler) GetComplianceReport(c *gin.Context) {
|
||||
reportID := firstPathParam(c, "reportId", "id")
|
||||
if !sh.requireComplianceReportAccess(c, reportID) {
|
||||
return
|
||||
}
|
||||
|
||||
report, err := sh.complianceManager.GetComplianceReport(reportID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Compliance report not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, report)
|
||||
}
|
||||
|
||||
// GetComplianceFrameworks retrieves available compliance frameworks
|
||||
func (sh *SecurityHandler) GetComplianceFrameworks(c *gin.Context) {
|
||||
rows, err := sh.db.Query(`
|
||||
SELECT id, name, description, version, enabled, created_at
|
||||
FROM compliance_frameworks
|
||||
WHERE enabled = true
|
||||
ORDER BY name
|
||||
`)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get compliance frameworks"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var frameworks []security.ComplianceFramework
|
||||
for rows.Next() {
|
||||
var framework security.ComplianceFramework
|
||||
err := rows.Scan(&framework.ID, &framework.Name, &framework.Description,
|
||||
&framework.Version, &framework.Enabled, &framework.CreatedAt)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
frameworks = append(frameworks, framework)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"frameworks": frameworks})
|
||||
}
|
||||
|
||||
// InitializeGDPRFramework initializes the GDPR compliance framework
|
||||
func (sh *SecurityHandler) InitializeGDPRFramework(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
|
||||
err := sh.complianceManager.InitializeGDPRFramework()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to initialize GDPR framework"})
|
||||
return
|
||||
}
|
||||
|
||||
// Log audit event
|
||||
sh.auditLogger.LogSecurityEvent(userID, "gdpr_framework_initialized", "compliance",
|
||||
map[string]interface{}{}, c.ClientIP(), c.GetHeader("User-Agent"), true)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "initialized"})
|
||||
}
|
||||
|
||||
// GetSecurityMetrics retrieves security metrics for a project
|
||||
func (sh *SecurityHandler) GetSecurityMetrics(c *gin.Context) {
|
||||
projectID := firstPathParam(c, "projectId", "id", "project_id")
|
||||
if _, ok := sh.requireProjectAccess(c, projectID); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Get vulnerability counts
|
||||
var vulnMetrics struct {
|
||||
Total int `json:"total"`
|
||||
Critical int `json:"critical"`
|
||||
High int `json:"high"`
|
||||
Medium int `json:"medium"`
|
||||
Low int `json:"low"`
|
||||
Open int `json:"open"`
|
||||
Resolved int `json:"resolved"`
|
||||
}
|
||||
|
||||
err := sh.db.QueryRow(`
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(*) FILTER (WHERE severity = 'critical') as critical,
|
||||
COUNT(*) FILTER (WHERE severity = 'high') as high,
|
||||
COUNT(*) FILTER (WHERE severity = 'medium') as medium,
|
||||
COUNT(*) FILTER (WHERE severity = 'low') as low,
|
||||
COUNT(*) FILTER (WHERE status = 'open') as open,
|
||||
COUNT(*) FILTER (WHERE status = 'resolved') as resolved
|
||||
FROM vulnerabilities
|
||||
WHERE project_id = $1
|
||||
`, projectID).Scan(&vulnMetrics.Total, &vulnMetrics.Critical, &vulnMetrics.High,
|
||||
&vulnMetrics.Medium, &vulnMetrics.Low, &vulnMetrics.Open, &vulnMetrics.Resolved)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get vulnerability metrics"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get latest scan
|
||||
var latestScan struct {
|
||||
ID string `json:"id"`
|
||||
Score int `json:"score"`
|
||||
ScannedAt time.Time `json:"scanned_at"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
err = sh.db.QueryRow(`
|
||||
SELECT id, score, started_at as scanned_at, status
|
||||
FROM security_scans
|
||||
WHERE project_id = $1
|
||||
ORDER BY started_at DESC
|
||||
LIMIT 1
|
||||
`, projectID).Scan(&latestScan.ID, &latestScan.Score, &latestScan.ScannedAt, &latestScan.Status)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
latestScan = struct {
|
||||
ID string `json:"id"`
|
||||
Score int `json:"score"`
|
||||
ScannedAt time.Time `json:"scanned_at"`
|
||||
Status string `json:"status"`
|
||||
}{ID: "", Score: 0, ScannedAt: time.Time{}, Status: "never_scanned"}
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get latest scan"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get compliance status
|
||||
var complianceStatus struct {
|
||||
OverallStatus string `json:"overall_status"`
|
||||
Score int `json:"score"`
|
||||
LastAssessed *time.Time `json:"last_assessed"`
|
||||
}
|
||||
|
||||
err = sh.db.QueryRow(`
|
||||
SELECT overall_status, score, assessment_date
|
||||
FROM compliance_reports
|
||||
WHERE project_id = $1
|
||||
ORDER BY assessment_date DESC
|
||||
LIMIT 1
|
||||
`, projectID).Scan(&complianceStatus.OverallStatus, &complianceStatus.Score, &complianceStatus.LastAssessed)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
complianceStatus = struct {
|
||||
OverallStatus string `json:"overall_status"`
|
||||
Score int `json:"score"`
|
||||
LastAssessed *time.Time `json:"last_assessed"`
|
||||
}{OverallStatus: "not_assessed", Score: 0, LastAssessed: nil}
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get compliance status"})
|
||||
return
|
||||
}
|
||||
|
||||
metrics := gin.H{
|
||||
"vulnerabilities": vulnMetrics,
|
||||
"latest_scan": latestScan,
|
||||
"compliance": complianceStatus,
|
||||
"security_score": sh.calculateOverallSecurityScore(struct{ Total, Critical, High, Medium, Low, Open, Resolved int }{
|
||||
Total: vulnMetrics.Total, Critical: vulnMetrics.Critical, High: vulnMetrics.High,
|
||||
Medium: vulnMetrics.Medium, Low: vulnMetrics.Low, Open: vulnMetrics.Open, Resolved: vulnMetrics.Resolved,
|
||||
}, latestScan.Score, complianceStatus.Score),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, metrics)
|
||||
}
|
||||
|
||||
// calculateOverallSecurityScore calculates an overall security score
|
||||
func (sh *SecurityHandler) calculateOverallSecurityScore(vulnMetrics struct {
|
||||
Total, Critical, High, Medium, Low, Open, Resolved int
|
||||
}, scanScore, complianceScore int) int {
|
||||
// Weight the different components
|
||||
vulnScore := 100
|
||||
if vulnMetrics.Total > 0 {
|
||||
deduction := (vulnMetrics.Critical * 25) + (vulnMetrics.High * 15) + (vulnMetrics.Medium * 8) + (vulnMetrics.Low * 3)
|
||||
vulnScore = max(0, 100-deduction)
|
||||
}
|
||||
|
||||
// Calculate weighted average
|
||||
overallScore := (vulnScore*40 + scanScore*30 + complianceScore*30) / 100
|
||||
return overallScore
|
||||
}
|
||||
|
||||
// GetAuditLogs retrieves audit logs for security events
|
||||
func (sh *SecurityHandler) GetAuditLogs(c *gin.Context) {
|
||||
projectID := firstPathParam(c, "projectId", "id", "project_id")
|
||||
if _, ok := sh.requireProjectAccess(c, projectID); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
limit := 50
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 1000 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
// In a real implementation, this would query the audit database
|
||||
// For now, return a placeholder response
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"audit_logs": []gin.H{
|
||||
{
|
||||
"id": uuid.New().String(),
|
||||
"timestamp": time.Now(),
|
||||
"user_id": c.MustGet("user_id").(string),
|
||||
"action": "security_scan_started",
|
||||
"resource": "project",
|
||||
"ip_address": c.ClientIP(),
|
||||
"success": true,
|
||||
},
|
||||
},
|
||||
"total": 1,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
func (sh *SecurityHandler) requireProjectAccess(c *gin.Context, projectID string) (string, bool) {
|
||||
userIDValue, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return "", false
|
||||
}
|
||||
userID, ok := userIDValue.(string)
|
||||
if !ok || userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user context"})
|
||||
return "", false
|
||||
}
|
||||
|
||||
if _, err := uuid.Parse(projectID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
|
||||
return "", false
|
||||
}
|
||||
|
||||
var hasAccess bool
|
||||
err := sh.db.QueryRow(
|
||||
`SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM projects p
|
||||
WHERE p.id = $1
|
||||
AND (p.owner_id = $2 OR EXISTS (
|
||||
SELECT 1 FROM project_members pm
|
||||
WHERE pm.project_id = p.id AND pm.user_id = $2
|
||||
))
|
||||
)`,
|
||||
projectID, userID,
|
||||
).Scan(&hasAccess)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify project access"})
|
||||
return "", false
|
||||
}
|
||||
|
||||
if !hasAccess {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return "", false
|
||||
}
|
||||
|
||||
return userID, true
|
||||
}
|
||||
|
||||
func (sh *SecurityHandler) requireSecurityScanAccess(c *gin.Context, scanID string) bool {
|
||||
if _, err := uuid.Parse(scanID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid scan ID"})
|
||||
return false
|
||||
}
|
||||
|
||||
var projectID string
|
||||
err := sh.db.QueryRow("SELECT project_id FROM security_scans WHERE id = $1", scanID).Scan(&projectID)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Security scan not found"})
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify scan access"})
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok := sh.requireProjectAccess(c, projectID)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (sh *SecurityHandler) requireComplianceReportAccess(c *gin.Context, reportID string) bool {
|
||||
if _, err := uuid.Parse(reportID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid report ID"})
|
||||
return false
|
||||
}
|
||||
|
||||
var projectID string
|
||||
err := sh.db.QueryRow("SELECT project_id FROM compliance_reports WHERE id = $1", reportID).Scan(&projectID)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Compliance report not found"})
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify report access"})
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok := sh.requireProjectAccess(c, projectID)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (sh *SecurityHandler) requireVulnerabilityAccess(c *gin.Context, vulnID string) (string, bool) {
|
||||
if _, err := uuid.Parse(vulnID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid vulnerability ID"})
|
||||
return "", false
|
||||
}
|
||||
|
||||
var projectID string
|
||||
err := sh.db.QueryRow("SELECT project_id FROM vulnerabilities WHERE id = $1", vulnID).Scan(&projectID)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Vulnerability not found"})
|
||||
return "", false
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify vulnerability access"})
|
||||
return "", false
|
||||
}
|
||||
|
||||
return sh.requireProjectAccess(c, projectID)
|
||||
}
|
||||
|
||||
// max helper function
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -1,458 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Service represents a service in the system
|
||||
type Service struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
ProjectID uuid.UUID `json:"project_id" db:"project_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Type string `json:"type" db:"type"` // web, worker, database, etc.
|
||||
Status string `json:"status" db:"status"` // building, running, failed, stopped
|
||||
Image string `json:"image" db:"image"`
|
||||
Command string `json:"command" db:"command"`
|
||||
Environment string `json:"environment" db:"environment"` // production, preview, development
|
||||
GitRepo string `json:"git_repo" db:"git_repo"`
|
||||
GitBranch string `json:"git_branch" db:"git_branch"`
|
||||
BuildPath string `json:"build_path" db:"build_path"`
|
||||
CPU string `json:"cpu" db:"cpu"`
|
||||
Memory string `json:"memory" db:"memory"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateServiceRequest represents a request to create a service
|
||||
type CreateServiceRequest struct {
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
Name string `json:"name" binding:"required,min=1,max=255"`
|
||||
Type string `json:"type" binding:"required,oneof=web worker database cron"`
|
||||
Image string `json:"image"`
|
||||
Command string `json:"command"`
|
||||
Environment string `json:"environment" binding:"required,oneof=production preview development"`
|
||||
GitRepo string `json:"git_repo"`
|
||||
GitBranch string `json:"git_branch"`
|
||||
BuildPath string `json:"build_path"`
|
||||
CPU string `json:"cpu"`
|
||||
Memory string `json:"memory"`
|
||||
}
|
||||
|
||||
// UpdateServiceRequest represents a request to update a service
|
||||
type UpdateServiceRequest struct {
|
||||
Name string `json:"name" binding:"omitempty,min=1,max=255"`
|
||||
Type string `json:"type" binding:"omitempty,oneof=web worker database cron"`
|
||||
Image string `json:"image"`
|
||||
Command string `json:"command"`
|
||||
Environment string `json:"environment" binding:"omitempty,oneof=production preview development"`
|
||||
GitRepo string `json:"git_repo"`
|
||||
GitBranch string `json:"git_branch"`
|
||||
BuildPath string `json:"build_path"`
|
||||
CPU string `json:"cpu"`
|
||||
Memory string `json:"memory"`
|
||||
}
|
||||
|
||||
// handleGetServices retrieves all services for a project
|
||||
func handleGetServices(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
projectIDStr := firstPathParam(c, "id", "project_id", "projectId")
|
||||
projectID, err := uuid.Parse(projectIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if project exists and user has access
|
||||
var project Project
|
||||
err = db.(*database.DB).QueryRow(
|
||||
"SELECT id, name, owner_id FROM projects WHERE id = $1",
|
||||
projectID,
|
||||
).Scan(&project.ID, &project.Name, &project.OwnerID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token (set by auth middleware)
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns the project
|
||||
if project.OwnerID != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get services for the project
|
||||
rows, err := db.(*database.DB).Query(
|
||||
`SELECT id, project_id, name, type, status, image, command, environment,
|
||||
git_repo, git_branch, build_path, cpu, memory, created_at, updated_at
|
||||
FROM services
|
||||
WHERE project_id = $1
|
||||
ORDER BY created_at DESC`,
|
||||
projectID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve services"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var services []Service
|
||||
for rows.Next() {
|
||||
var service Service
|
||||
err := rows.Scan(
|
||||
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
|
||||
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
|
||||
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
|
||||
&service.CreatedAt, &service.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan service"})
|
||||
return
|
||||
}
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"services": services})
|
||||
}
|
||||
|
||||
// handleCreateService creates a new service
|
||||
func handleCreateService(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
projectIDStr := firstPathParam(c, "id", "project_id", "projectId")
|
||||
projectID, err := uuid.Parse(projectIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateServiceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.ProjectID == uuid.Nil {
|
||||
req.ProjectID = projectID
|
||||
} else if req.ProjectID != projectID {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Project ID in URL and request body must match"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if project exists and user has access
|
||||
var project Project
|
||||
err = db.(*database.DB).QueryRow(
|
||||
"SELECT id, name, owner_id FROM projects WHERE id = $1",
|
||||
req.ProjectID,
|
||||
).Scan(&project.ID, &project.Name, &project.OwnerID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns the project
|
||||
if project.OwnerID != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if service name already exists in the project
|
||||
var count int
|
||||
err = db.(*database.DB).QueryRow(
|
||||
"SELECT COUNT(*) FROM services WHERE project_id = $1 AND name = $2",
|
||||
req.ProjectID, req.Name,
|
||||
).Scan(&count)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check service name"})
|
||||
return
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Service name already exists in this project"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create new service
|
||||
service := Service{
|
||||
ID: uuid.New(),
|
||||
ProjectID: req.ProjectID,
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Status: "stopped", // Initial status
|
||||
Image: req.Image,
|
||||
Command: req.Command,
|
||||
Environment: req.Environment,
|
||||
GitRepo: req.GitRepo,
|
||||
GitBranch: req.GitBranch,
|
||||
BuildPath: req.BuildPath,
|
||||
CPU: req.CPU,
|
||||
Memory: req.Memory,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Set default values if not provided
|
||||
if service.CPU == "" {
|
||||
service.CPU = "0.5"
|
||||
}
|
||||
if service.Memory == "" {
|
||||
service.Memory = "512Mi"
|
||||
}
|
||||
|
||||
// Insert service into database
|
||||
_, err = db.(*database.DB).Exec(
|
||||
`INSERT INTO services
|
||||
(id, project_id, name, type, status, image, command, environment,
|
||||
git_repo, git_branch, build_path, cpu, memory, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`,
|
||||
service.ID, service.ProjectID, service.Name, service.Type, service.Status,
|
||||
service.Image, service.Command, service.Environment, service.GitRepo,
|
||||
service.GitBranch, service.BuildPath, service.CPU, service.Memory,
|
||||
service.CreatedAt, service.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create service"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"service": service})
|
||||
}
|
||||
|
||||
// handleGetService retrieves a specific service
|
||||
func handleGetService(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
serviceIDStr := c.Param("id")
|
||||
serviceID, err := uuid.Parse(serviceIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get service with project ownership check
|
||||
var service Service
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
|
||||
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
|
||||
s.created_at, s.updated_at
|
||||
FROM services s
|
||||
JOIN projects p ON s.project_id = p.id
|
||||
WHERE s.id = $1 AND p.owner_id = $2`,
|
||||
serviceID, userID,
|
||||
).Scan(
|
||||
&service.ID, &service.ProjectID, &service.Name, &service.Type, &service.Status,
|
||||
&service.Image, &service.Command, &service.Environment, &service.GitRepo,
|
||||
&service.GitBranch, &service.BuildPath, &service.CPU, &service.Memory,
|
||||
&service.CreatedAt, &service.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"service": service})
|
||||
}
|
||||
|
||||
// handleUpdateService updates a service
|
||||
func handleUpdateService(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
serviceIDStr := c.Param("id")
|
||||
serviceID, err := uuid.Parse(serviceIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateServiceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if service exists and user has access
|
||||
var existingService Service
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT s.id, s.project_id, s.name, s.type, s.status, s.image, s.command,
|
||||
s.environment, s.git_repo, s.git_branch, s.build_path, s.cpu, s.memory,
|
||||
s.created_at, s.updated_at
|
||||
FROM services s
|
||||
JOIN projects p ON s.project_id = p.id
|
||||
WHERE s.id = $1 AND p.owner_id = $2`,
|
||||
serviceID, userID,
|
||||
).Scan(
|
||||
&existingService.ID, &existingService.ProjectID, &existingService.Name, &existingService.Type,
|
||||
&existingService.Status, &existingService.Image, &existingService.Command,
|
||||
&existingService.Environment, &existingService.GitRepo, &existingService.GitBranch,
|
||||
&existingService.BuildPath, &existingService.CPU, &existingService.Memory,
|
||||
&existingService.CreatedAt, &existingService.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update fields if provided
|
||||
if req.Name != "" {
|
||||
existingService.Name = req.Name
|
||||
}
|
||||
if req.Type != "" {
|
||||
existingService.Type = req.Type
|
||||
}
|
||||
if req.Image != "" {
|
||||
existingService.Image = req.Image
|
||||
}
|
||||
if req.Command != "" {
|
||||
existingService.Command = req.Command
|
||||
}
|
||||
if req.Environment != "" {
|
||||
existingService.Environment = req.Environment
|
||||
}
|
||||
if req.GitRepo != "" {
|
||||
existingService.GitRepo = req.GitRepo
|
||||
}
|
||||
if req.GitBranch != "" {
|
||||
existingService.GitBranch = req.GitBranch
|
||||
}
|
||||
if req.BuildPath != "" {
|
||||
existingService.BuildPath = req.BuildPath
|
||||
}
|
||||
if req.CPU != "" {
|
||||
existingService.CPU = req.CPU
|
||||
}
|
||||
if req.Memory != "" {
|
||||
existingService.Memory = req.Memory
|
||||
}
|
||||
|
||||
existingService.UpdatedAt = time.Now()
|
||||
|
||||
// Update service in database
|
||||
_, err = db.(*database.DB).Exec(
|
||||
`UPDATE services
|
||||
SET name = $1, type = $2, image = $3, command = $4, environment = $5,
|
||||
git_repo = $6, git_branch = $7, build_path = $8, cpu = $9, memory = $10, updated_at = $11
|
||||
WHERE id = $12`,
|
||||
existingService.Name, existingService.Type, existingService.Image, existingService.Command,
|
||||
existingService.Environment, existingService.GitRepo, existingService.GitBranch,
|
||||
existingService.BuildPath, existingService.CPU, existingService.Memory,
|
||||
existingService.UpdatedAt, existingService.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update service"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"service": existingService})
|
||||
}
|
||||
|
||||
// handleDeleteService deletes a service
|
||||
func handleDeleteService(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
serviceIDStr := c.Param("id")
|
||||
serviceID, err := uuid.Parse(serviceIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user ID from JWT token
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if service exists and user has access
|
||||
var projectOwnerID string
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT p.owner_id
|
||||
FROM services s
|
||||
JOIN projects p ON s.project_id = p.id
|
||||
WHERE s.id = $1`,
|
||||
serviceID,
|
||||
).Scan(&projectOwnerID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user owns the project
|
||||
if projectOwnerID != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
// Delete service (cascade will handle related records)
|
||||
_, err = db.(*database.DB).Exec(
|
||||
"DELETE FROM services WHERE id = $1",
|
||||
serviceID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete service"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Service deleted successfully"})
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ServiceTemplate struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Category string `json:"category" db:"category"`
|
||||
Logo string `json:"logo" db:"logo"`
|
||||
Config string `json:"config" db:"config"`
|
||||
Variables string `json:"variables" db:"variables"`
|
||||
IsOfficial bool `json:"is_official" db:"is_official"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
type TemplateConfig struct {
|
||||
Type string `json:"type"`
|
||||
Runtime string `json:"runtime"`
|
||||
BuildCommand string `json:"build_command"`
|
||||
StartCommand string `json:"start_command"`
|
||||
Port int `json:"port"`
|
||||
HealthCheck string `json:"health_check"`
|
||||
Environment map[string]string `json:"environment"`
|
||||
Dockerfile string `json:"dockerfile,omitempty"`
|
||||
NixpacksConfig map[string]string `json:"nixpacks_config,omitempty"`
|
||||
}
|
||||
|
||||
type TemplateVariable struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Default string `json:"default"`
|
||||
Required bool `json:"required"`
|
||||
Secret bool `json:"secret"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func handleGetTemplates(c *gin.Context) {
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
category := c.Query("category")
|
||||
|
||||
query := "SELECT id, name, description, category, logo, config, variables, is_official, created_at, updated_at FROM service_templates"
|
||||
args := []interface{}{}
|
||||
|
||||
if category != "" {
|
||||
query += " WHERE category = $1"
|
||||
args = append(args, category)
|
||||
}
|
||||
query += " ORDER BY is_official DESC, name ASC"
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch templates"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var templates []ServiceTemplate
|
||||
for rows.Next() {
|
||||
var t ServiceTemplate
|
||||
err := rows.Scan(&t.ID, &t.Name, &t.Description, &t.Category, &t.Logo, &t.Config, &t.Variables, &t.IsOfficial, &t.CreatedAt, &t.UpdatedAt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
templates = append(templates, t)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"templates": templates})
|
||||
}
|
||||
|
||||
func handleGetTemplate(c *gin.Context) {
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
templateID := c.Param("id")
|
||||
|
||||
var t ServiceTemplate
|
||||
err := db.QueryRow(
|
||||
"SELECT id, name, description, category, logo, config, variables, is_official, created_at, updated_at FROM service_templates WHERE id = $1",
|
||||
templateID,
|
||||
).Scan(&t.ID, &t.Name, &t.Description, &t.Category, &t.Logo, &t.Config, &t.Variables, &t.IsOfficial, &t.CreatedAt, &t.UpdatedAt)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Template not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var config TemplateConfig
|
||||
if err := json.Unmarshal([]byte(t.Config), &config); err == nil {
|
||||
}
|
||||
|
||||
var variables []TemplateVariable
|
||||
if err := json.Unmarshal([]byte(t.Variables), &variables); err == nil {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"template": t,
|
||||
"config": config,
|
||||
"variables": variables,
|
||||
})
|
||||
}
|
||||
|
||||
func handleCreateFromTemplate(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
templateID := c.Param("id")
|
||||
|
||||
var req struct {
|
||||
ProjectID string `json:"project_id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var template ServiceTemplate
|
||||
err := db.QueryRow(
|
||||
"SELECT id, name, description, category, logo, config, variables, is_official FROM service_templates WHERE id = $1",
|
||||
templateID,
|
||||
).Scan(&template.ID, &template.Name, &template.Description, &template.Category, &template.Logo, &template.Config, &template.Variables, &template.IsOfficial)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Template not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var config TemplateConfig
|
||||
json.Unmarshal([]byte(template.Config), &config)
|
||||
|
||||
var templateVars []TemplateVariable
|
||||
json.Unmarshal([]byte(template.Variables), &templateVars)
|
||||
|
||||
envVars := make(map[string]string)
|
||||
for key, value := range config.Environment {
|
||||
envVars[key] = value
|
||||
}
|
||||
for key, value := range req.Variables {
|
||||
envVars[key] = value
|
||||
}
|
||||
|
||||
envVarsJSON, _ := json.Marshal(envVars)
|
||||
|
||||
serviceID := uuid.New()
|
||||
now := time.Now()
|
||||
|
||||
_, err = db.Exec(
|
||||
`INSERT INTO services (id, project_id, name, type, status, image, command, environment, cpu, memory, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
||||
serviceID, req.ProjectID, req.Name, config.Type, "stopped", config.Runtime, config.StartCommand,
|
||||
string(envVarsJSON), "0.5", "512Mi", now, now,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create service from template"})
|
||||
return
|
||||
}
|
||||
|
||||
LogAudit(userID, "service", serviceID.String(), "create", map[string]interface{}{
|
||||
"template_id": templateID,
|
||||
"name": req.Name,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"service_id": serviceID.String(),
|
||||
"message": "Service created from template",
|
||||
})
|
||||
}
|
||||
|
||||
func SeedTemplates() []ServiceTemplate {
|
||||
templates := []ServiceTemplate{
|
||||
{
|
||||
ID: "tpl-nodejs",
|
||||
Name: "Node.js Application",
|
||||
Description: "Generic Node.js application with automatic dependency detection",
|
||||
Category: "web",
|
||||
Logo: "https://cdn.simpleicons.org/node.js",
|
||||
Config: `{"type":"web","runtime":"node","build_command":"npm install && npm run build","start_command":"npm start","port":3000,"health_check":"/health"}`,
|
||||
Variables: `[{"key":"NODE_ENV","label":"Node Environment","default":"production","required":false,"secret":false},{"key":"NPM_TOKEN","label":"NPM Token","default":"","required":false,"secret":true}]`,
|
||||
IsOfficial: true,
|
||||
},
|
||||
{
|
||||
ID: "tpl-react",
|
||||
Name: "React Application",
|
||||
Description: "React single-page application with Vite",
|
||||
Category: "frontend",
|
||||
Logo: "https://cdn.simpleicons.org/react",
|
||||
Config: `{"type":"web","runtime":"node","build_command":"npm install && npm run build","start_command":"npx serve -s dist","port":3000}`,
|
||||
Variables: `[{"key":"VITE_API_URL","label":"API URL","default":"","required":true,"secret":false}]`,
|
||||
IsOfficial: true,
|
||||
},
|
||||
{
|
||||
ID: "tpl-python",
|
||||
Name: "Python Application",
|
||||
Description: "Python application with FastAPI/Flask support",
|
||||
Category: "web",
|
||||
Logo: "https://cdn.simpleicons.org/python",
|
||||
Config: `{"type":"web","runtime":"python","build_command":"pip install -r requirements.txt","start_command":"python main.py","port":8000}`,
|
||||
Variables: `[{"key":"PYTHON_VERSION","label":"Python Version","default":"3.11","required":false,"secret":false}]`,
|
||||
IsOfficial: true,
|
||||
},
|
||||
{
|
||||
ID: "tpl-go",
|
||||
Name: "Go Application",
|
||||
Description: "Go backend service",
|
||||
Category: "web",
|
||||
Logo: "https://cdn.simpleicons.org/go",
|
||||
Config: `{"type":"web","runtime":"go","build_command":"go build -o app .","start_command":"./app","port":8080}`,
|
||||
Variables: `[{"key":"GO_VERSION","label":"Go Version","default":"1.21","required":false,"secret":false}]`,
|
||||
IsOfficial: true,
|
||||
},
|
||||
{
|
||||
ID: "tpl-postgres",
|
||||
Name: "PostgreSQL Database",
|
||||
Description: "Managed PostgreSQL database",
|
||||
Category: "database",
|
||||
Logo: "https://cdn.simpleicons.org/postgresql",
|
||||
Config: `{"type":"database","runtime":"postgres","port":5432}`,
|
||||
Variables: `[{"key":"POSTGRES_USER","label":"Username","default":"postgres","required":true,"secret":false},{"key":"POSTGRES_PASSWORD","label":"Password","default":"","required":true,"secret":true},{"key":"POSTGRES_DB","label":"Database Name","default":"app","required":true,"secret":false}]`,
|
||||
IsOfficial: true,
|
||||
},
|
||||
{
|
||||
ID: "tpl-redis",
|
||||
Name: "Redis Cache",
|
||||
Description: "In-memory data store",
|
||||
Category: "database",
|
||||
Logo: "https://cdn.simpleicons.org/redis",
|
||||
Config: `{"type":"database","runtime":"redis","port":6379}`,
|
||||
Variables: `[{"key":"REDIS_PASSWORD","label":"Password","default":"","required":false,"secret":true}]`,
|
||||
IsOfficial: true,
|
||||
},
|
||||
{
|
||||
ID: "tpl-mongodb",
|
||||
Name: "MongoDB Database",
|
||||
Description: "NoSQL document database",
|
||||
Category: "database",
|
||||
Logo: "https://cdn.simpleicons.org/mongodb",
|
||||
Config: `{"type":"database","runtime":"mongodb","port":27017}`,
|
||||
Variables: `[{"key":"MONGO_INITDB_ROOT_USERNAME","label":"Root Username","default":"admin","required":true,"secret":false},{"key":"MONGO_INITDB_ROOT_PASSWORD","label":"Root Password","default":"","required":true,"secret":true}]`,
|
||||
IsOfficial: true,
|
||||
},
|
||||
{
|
||||
ID: "tpl-worker",
|
||||
Name: "Background Worker",
|
||||
Description: "Background job processing service",
|
||||
Category: "worker",
|
||||
Logo: "https://cdn.simpleicons.org/terminal",
|
||||
Config: `{"type":"worker","runtime":"node","build_command":"npm install","start_command":"npm run worker"}`,
|
||||
Variables: `[{"key":"WORKER_CONCURRENCY","label":"Concurrency","default":"4","required":false,"secret":false}]`,
|
||||
IsOfficial: true,
|
||||
},
|
||||
{
|
||||
ID: "tpl-cron",
|
||||
Name: "Cron Job",
|
||||
Description: "Scheduled task runner",
|
||||
Category: "cron",
|
||||
Logo: "https://cdn.simpleicons.org/clock",
|
||||
Config: `{"type":"cron","runtime":"node","build_command":"npm install","start_command":"npm run cron"}`,
|
||||
Variables: `[{"key":"CRON_SCHEDULE","label":"Schedule","default":"0 * * * *","required":true,"secret":false}]`,
|
||||
IsOfficial: true,
|
||||
},
|
||||
{
|
||||
ID: "tpl-docker",
|
||||
Name: "Docker Image",
|
||||
Description: "Deploy from any Docker image",
|
||||
Category: "custom",
|
||||
Logo: "https://cdn.simpleicons.org/docker",
|
||||
Config: `{"type":"web","runtime":"docker","port":80}`,
|
||||
Variables: `[{"key":"IMAGE","label":"Docker Image","default":"","required":true,"secret":false},{"key":"TAG","label":"Image Tag","default":"latest","required":false,"secret":false}]`,
|
||||
IsOfficial: true,
|
||||
},
|
||||
}
|
||||
return templates
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type EnvironmentVariable struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
ServiceID uuid.UUID `json:"service_id" db:"service_id"`
|
||||
Key string `json:"key" db:"key"`
|
||||
Value string `json:"value" db:"value"`
|
||||
IsSecret bool `json:"is_secret" db:"is_secret"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
type UpdateVariablesRequest struct {
|
||||
Variables []VariableInput `json:"variables" binding:"required"`
|
||||
}
|
||||
|
||||
type VariableInput struct {
|
||||
Key string `json:"key" binding:"required"`
|
||||
Value string `json:"value"`
|
||||
IsSecret bool `json:"is_secret"`
|
||||
}
|
||||
|
||||
func handleGetVariables(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
serviceIDStr := c.Param("id")
|
||||
serviceID, err := uuid.Parse(serviceIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"})
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var ownerCheck string
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT p.owner_id FROM services s
|
||||
JOIN projects p ON s.project_id = p.id
|
||||
WHERE s.id = $1`,
|
||||
serviceID,
|
||||
).Scan(&ownerCheck)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if ownerCheck != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := db.(*database.DB).Query(
|
||||
`SELECT id, service_id, key, value, is_secret, created_at, updated_at
|
||||
FROM environment_variables
|
||||
WHERE service_id = $1
|
||||
ORDER BY key ASC`,
|
||||
serviceID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve variables"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var variables []EnvironmentVariable
|
||||
for rows.Next() {
|
||||
var v EnvironmentVariable
|
||||
err := rows.Scan(
|
||||
&v.ID, &v.ServiceID, &v.Key, &v.Value, &v.IsSecret, &v.CreatedAt, &v.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan variable"})
|
||||
return
|
||||
}
|
||||
if v.IsSecret {
|
||||
v.Value = "********"
|
||||
}
|
||||
variables = append(variables, v)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"variables": variables})
|
||||
}
|
||||
|
||||
func handleUpdateVariables(c *gin.Context) {
|
||||
db, exists := c.Get("db")
|
||||
if !exists {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection not available"})
|
||||
return
|
||||
}
|
||||
|
||||
serviceIDStr := c.Param("id")
|
||||
serviceID, err := uuid.Parse(serviceIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid service ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateVariablesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var ownerCheck string
|
||||
err = db.(*database.DB).QueryRow(
|
||||
`SELECT p.owner_id FROM services s
|
||||
JOIN projects p ON s.project_id = p.id
|
||||
WHERE s.id = $1`,
|
||||
serviceID,
|
||||
).Scan(&ownerCheck)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Service not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if ownerCheck != userID.(string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := db.(*database.DB).Begin()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to begin transaction"})
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
_, err = tx.Exec("DELETE FROM environment_variables WHERE service_id = $1", serviceID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear existing variables"})
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, v := range req.Variables {
|
||||
varID := uuid.New()
|
||||
_, err = tx.Exec(
|
||||
`INSERT INTO environment_variables (id, service_id, key, value, is_secret, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
varID, serviceID, v.Key, v.Value, v.IsSecret, now, now,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert variable: " + v.Key})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit transaction"})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := db.(*database.DB).Query(
|
||||
`SELECT id, service_id, key, value, is_secret, created_at, updated_at
|
||||
FROM environment_variables
|
||||
WHERE service_id = $1
|
||||
ORDER BY key ASC`,
|
||||
serviceID,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve variables"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var variables []EnvironmentVariable
|
||||
for rows.Next() {
|
||||
var v EnvironmentVariable
|
||||
err := rows.Scan(
|
||||
&v.ID, &v.ServiceID, &v.Key, &v.Value, &v.IsSecret, &v.CreatedAt, &v.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if v.IsSecret {
|
||||
v.Value = "********"
|
||||
}
|
||||
variables = append(variables, v)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"variables": variables, "message": "Environment variables updated successfully"})
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
type WebSocketClient struct {
|
||||
ID string
|
||||
UserID string
|
||||
Conn *websocket.Conn
|
||||
Channels map[string]bool
|
||||
Send chan []byte
|
||||
}
|
||||
|
||||
type WebSocketMessage struct {
|
||||
Type string `json:"type"`
|
||||
Channel string `json:"channel"`
|
||||
Data interface{} `json:"data"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
type WebSocketHub struct {
|
||||
clients map[string]*WebSocketClient
|
||||
broadcast chan *WebSocketMessage
|
||||
register chan *WebSocketClient
|
||||
unregister chan *WebSocketClient
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var wsHub = &WebSocketHub{
|
||||
clients: make(map[string]*WebSocketClient),
|
||||
broadcast: make(chan *WebSocketMessage, 100),
|
||||
register: make(chan *WebSocketClient),
|
||||
unregister: make(chan *WebSocketClient),
|
||||
}
|
||||
|
||||
func init() {
|
||||
go wsHub.run()
|
||||
}
|
||||
|
||||
func (h *WebSocketHub) run() {
|
||||
for {
|
||||
select {
|
||||
case client := <-h.register:
|
||||
h.mu.Lock()
|
||||
h.clients[client.ID] = client
|
||||
h.mu.Unlock()
|
||||
log.Printf("WebSocket client connected: %s", client.ID)
|
||||
|
||||
case client := <-h.unregister:
|
||||
h.mu.Lock()
|
||||
if _, ok := h.clients[client.ID]; ok {
|
||||
delete(h.clients, client.ID)
|
||||
close(client.Send)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
log.Printf("WebSocket client disconnected: %s", client.ID)
|
||||
|
||||
case message := <-h.broadcast:
|
||||
h.mu.RLock()
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
log.Printf("Error marshaling WebSocket message: %v", err)
|
||||
h.mu.RUnlock()
|
||||
continue
|
||||
}
|
||||
|
||||
for _, client := range h.clients {
|
||||
if client.Channels[message.Channel] || message.Channel == "all" {
|
||||
select {
|
||||
case client.Send <- data:
|
||||
default:
|
||||
close(client.Send)
|
||||
delete(h.clients, client.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WebSocketHub) Broadcast(channel string, msgType string, data interface{}) {
|
||||
message := &WebSocketMessage{
|
||||
Type: msgType,
|
||||
Channel: channel,
|
||||
Data: data,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
h.broadcast <- message
|
||||
}
|
||||
|
||||
func (h *WebSocketHub) BroadcastToUser(userID string, msgType string, data interface{}) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
message := &WebSocketMessage{
|
||||
Type: msgType,
|
||||
Channel: "user:" + userID,
|
||||
Data: data,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
messageBytes, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, client := range h.clients {
|
||||
if client.UserID == userID {
|
||||
select {
|
||||
case client.Send <- messageBytes:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleWebSocket(c *gin.Context) {
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Printf("WebSocket upgrade error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
client := &WebSocketClient{
|
||||
ID: generateClientID(),
|
||||
UserID: userID.(string),
|
||||
Conn: conn,
|
||||
Channels: make(map[string]bool),
|
||||
Send: make(chan []byte, 256),
|
||||
}
|
||||
|
||||
wsHub.register <- client
|
||||
|
||||
go client.writePump()
|
||||
go client.readPump()
|
||||
}
|
||||
|
||||
func (c *WebSocketClient) readPump() {
|
||||
defer func() {
|
||||
wsHub.unregister <- c
|
||||
c.Conn.Close()
|
||||
}()
|
||||
|
||||
c.Conn.SetReadLimit(512)
|
||||
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
|
||||
for {
|
||||
_, message, err := c.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
var msg struct {
|
||||
Action string `json:"action"`
|
||||
Channel string `json:"channel"`
|
||||
}
|
||||
if err := json.Unmarshal(message, &msg); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch msg.Action {
|
||||
case "subscribe":
|
||||
c.Channels[msg.Channel] = true
|
||||
case "unsubscribe":
|
||||
delete(c.Channels, msg.Channel)
|
||||
}
|
||||
|
||||
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *WebSocketClient) writePump() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
c.Conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-c.Send:
|
||||
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
if !ok {
|
||||
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
|
||||
w, err := c.Conn.NextWriter(websocket.TextMessage)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
w.Write(message)
|
||||
|
||||
n := len(c.Send)
|
||||
for i := 0; i < n; i++ {
|
||||
w.Write([]byte{'\n'})
|
||||
w.Write(<-c.Send)
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateClientID() string {
|
||||
return time.Now().Format("20060102150405") + "-" + randomString(8)
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letters[time.Now().Nanosecond()%len(letters)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func BroadcastServiceUpdate(serviceID string, data interface{}) {
|
||||
wsHub.Broadcast("service:"+serviceID, "service_update", data)
|
||||
}
|
||||
|
||||
func BroadcastDeploymentUpdate(deploymentID string, data interface{}) {
|
||||
wsHub.Broadcast("deployment:"+deploymentID, "deployment_update", data)
|
||||
}
|
||||
|
||||
func BroadcastBuildUpdate(buildID string, data interface{}) {
|
||||
wsHub.Broadcast("build:"+buildID, "build_update", data)
|
||||
}
|
||||
|
||||
func BroadcastMetricsUpdate(serviceID string, data interface{}) {
|
||||
wsHub.Broadcast("metrics:"+serviceID, "metrics_update", data)
|
||||
}
|
||||
|
||||
func BroadcastScalingEvent(serviceID string, data interface{}) {
|
||||
wsHub.Broadcast("scaling:"+serviceID, "scaling_event", data)
|
||||
}
|
||||
|
||||
func NotifyUser(userID string, notificationType string, data interface{}) {
|
||||
wsHub.BroadcastToUser(userID, notificationType, data)
|
||||
}
|
||||
Reference in New Issue
Block a user