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)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// AuthCmd represents the auth command
|
||||
var AuthCmd = &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Authenticate with Containr API",
|
||||
Long: `Manage authentication with the Containr API.
|
||||
You can login, logout, and check your current authentication status.`,
|
||||
}
|
||||
|
||||
// loginCmd represents the login command
|
||||
var loginCmd = &cobra.Command{
|
||||
Use: "login [token]",
|
||||
Short: "Login to Containr",
|
||||
Long: `Login to Containr using your API token.
|
||||
You can get your token from the Containr web interface.`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: runLogin,
|
||||
}
|
||||
|
||||
// logoutCmd represents the logout command
|
||||
var logoutCmd = &cobra.Command{
|
||||
Use: "logout",
|
||||
Short: "Logout from Containr",
|
||||
Long: `Remove stored authentication credentials.`,
|
||||
RunE: runLogout,
|
||||
}
|
||||
|
||||
// statusCmd represents the status command
|
||||
var statusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Check authentication status",
|
||||
Long: `Check if you are currently authenticated with Containr.`,
|
||||
RunE: runStatus,
|
||||
}
|
||||
|
||||
func init() {
|
||||
AuthCmd.AddCommand(loginCmd)
|
||||
AuthCmd.AddCommand(logoutCmd)
|
||||
AuthCmd.AddCommand(statusCmd)
|
||||
}
|
||||
|
||||
func runLogin(cmd *cobra.Command, args []string) error {
|
||||
var token string
|
||||
|
||||
if len(args) > 0 {
|
||||
token = args[0]
|
||||
} else {
|
||||
// Prompt for token
|
||||
fmt.Print("Enter your Containr API token: ")
|
||||
fmt.Scanln(&token)
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
return fmt.Errorf("token is required")
|
||||
}
|
||||
|
||||
// Store token in config
|
||||
viper.Set("token", token)
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("failed to save token: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ Successfully logged in to Containr")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runLogout(cmd *cobra.Command, args []string) error {
|
||||
// Remove token from config
|
||||
viper.Set("token", "")
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return fmt.Errorf("failed to remove token: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ Successfully logged out from Containr")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runStatus(cmd *cobra.Command, args []string) error {
|
||||
token := viper.GetString("token")
|
||||
if token == "" {
|
||||
fmt.Println("❌ Not authenticated")
|
||||
fmt.Println("Run 'containr auth login <token>' to authenticate")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("✓ Authenticated with Containr")
|
||||
|
||||
// TODO: Verify token with API
|
||||
apiURL := viper.GetString("api-url")
|
||||
if apiURL != "" {
|
||||
fmt.Printf("API URL: %s\n", apiURL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Project represents a Containr project
|
||||
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"`
|
||||
}
|
||||
|
||||
// ProjectsCmd represents the projects command
|
||||
var ProjectsCmd = &cobra.Command{
|
||||
Use: "projects",
|
||||
Short: "Manage projects",
|
||||
Long: `Manage your Containr projects.
|
||||
You can list, create, update, and delete projects.`,
|
||||
}
|
||||
|
||||
// listProjectsCmd represents the list command
|
||||
var listProjectsCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all projects",
|
||||
Long: `List all your Containr projects.`,
|
||||
RunE: runListProjects,
|
||||
}
|
||||
|
||||
// createProjectCmd represents the create command
|
||||
var createProjectCmd = &cobra.Command{
|
||||
Use: "create [name]",
|
||||
Short: "Create a new project",
|
||||
Long: `Create a new Containr project.
|
||||
Provide a name and optional description.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runCreateProject,
|
||||
}
|
||||
|
||||
// deleteProjectCmd represents the delete command
|
||||
var deleteProjectCmd = &cobra.Command{
|
||||
Use: "delete [project-id]",
|
||||
Short: "Delete a project",
|
||||
Long: `Delete a Containr project by ID.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runDeleteProject,
|
||||
}
|
||||
|
||||
var projectDescription string
|
||||
|
||||
// getAPIURL constructs the full API URL for a given endpoint
|
||||
func getAPIURL(endpoint string) string {
|
||||
baseURL := viper.GetString("api-url")
|
||||
if baseURL == "" {
|
||||
baseURL = "http://localhost:8080/api/v1" // Default for development
|
||||
}
|
||||
|
||||
// Ensure baseURL doesn't end with / and endpoint starts with /
|
||||
baseURL = strings.TrimSuffix(baseURL, "/")
|
||||
if !strings.HasPrefix(endpoint, "/") {
|
||||
endpoint = "/" + endpoint
|
||||
}
|
||||
|
||||
return baseURL + endpoint
|
||||
}
|
||||
|
||||
// formatTime formats a time string for display
|
||||
func formatTime(timeStr string) string {
|
||||
if timeStr == "" {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// Parse the time and format it nicely
|
||||
t, err := time.Parse(time.RFC3339, timeStr)
|
||||
if err != nil {
|
||||
return timeStr // Return original if parsing fails
|
||||
}
|
||||
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func init() {
|
||||
ProjectsCmd.AddCommand(listProjectsCmd)
|
||||
ProjectsCmd.AddCommand(createProjectCmd)
|
||||
ProjectsCmd.AddCommand(deleteProjectCmd)
|
||||
|
||||
// Add flags
|
||||
createProjectCmd.Flags().StringVarP(&projectDescription, "description", "d", "", "Project description")
|
||||
}
|
||||
|
||||
func runListProjects(cmd *cobra.Command, args []string) error {
|
||||
apiURL := getAPIURL("/projects")
|
||||
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+viper.GetString("token"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("API request failed: %s - %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var projects []Project
|
||||
if err := json.Unmarshal(body, &projects); err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if len(projects) == 0 {
|
||||
fmt.Println("No projects found")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Println("Your Projects:")
|
||||
fmt.Println()
|
||||
for _, project := range projects {
|
||||
fmt.Printf("📦 %s (%s)\n", project.Name, project.ID)
|
||||
if project.Description != "" {
|
||||
fmt.Printf(" %s\n", project.Description)
|
||||
}
|
||||
fmt.Printf(" Created: %s\n", formatTime(project.CreatedAt))
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runCreateProject(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
projectData := map[string]interface{}{
|
||||
"name": name,
|
||||
}
|
||||
|
||||
if projectDescription != "" {
|
||||
projectData["description"] = projectDescription
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(projectData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal project data: %w", err)
|
||||
}
|
||||
|
||||
apiURL := getAPIURL("/projects")
|
||||
|
||||
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+viper.GetString("token"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("API request failed: %s - %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var project Project
|
||||
if err := json.Unmarshal(body, &project); err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Project '%s' created successfully!\n", project.Name)
|
||||
fmt.Printf("ID: %s\n", project.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDeleteProject(cmd *cobra.Command, args []string) error {
|
||||
projectID := args[0]
|
||||
|
||||
apiURL := getAPIURL("/projects/" + projectID)
|
||||
|
||||
req, err := http.NewRequest("DELETE", apiURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+viper.GetString("token"))
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("API request failed: %s - %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Project '%s' deleted successfully!\n", projectID)
|
||||
return nil
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"containr/internal/cli/commands"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "containr",
|
||||
Short: "Containr CLI - Manage your self-hosted PaaS",
|
||||
Long: `Containr CLI is a command-line interface for managing your Containr platform.
|
||||
You can manage projects, services, deployments, databases, and more from your terminal.`,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
cobra.CheckErr(rootCmd.Execute())
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
// Global flags
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.containr.yaml)")
|
||||
rootCmd.PersistentFlags().String("api-url", "", "Containr API URL (default is https://api.containr.dev)")
|
||||
rootCmd.PersistentFlags().String("token", "", "Authentication token")
|
||||
|
||||
// Bind flags to viper
|
||||
viper.BindPFlag("api-url", rootCmd.PersistentFlags().Lookup("api-url"))
|
||||
viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token"))
|
||||
|
||||
// Add command groups
|
||||
rootCmd.AddCommand(commands.AuthCmd)
|
||||
rootCmd.AddCommand(commands.ProjectsCmd)
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
// Use config file from the flag.
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
// Find home directory.
|
||||
home, err := os.UserHomeDir()
|
||||
cobra.CheckErr(err)
|
||||
|
||||
// Search config in home directory with name ".containr" (without extension).
|
||||
viper.AddConfigPath(home)
|
||||
viper.SetConfigType("yaml")
|
||||
viper.SetConfigName(".containr")
|
||||
}
|
||||
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
// If a config file is found, read it in.
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the CLI
|
||||
func Run() error {
|
||||
rootCmd.Execute()
|
||||
return nil
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// getAPIURL constructs the full API URL for a given endpoint
|
||||
func getAPIURL(endpoint string) string {
|
||||
baseURL := viper.GetString("api-url")
|
||||
if baseURL == "" {
|
||||
baseURL = "http://localhost:8080/api/v1" // Default for development
|
||||
}
|
||||
|
||||
// Ensure baseURL doesn't end with / and endpoint starts with /
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
if !strings.HasPrefix(endpoint, "/") {
|
||||
endpoint = "/" + endpoint
|
||||
}
|
||||
|
||||
return baseURL + endpoint
|
||||
}
|
||||
|
||||
// formatTime formats a time string for display
|
||||
func formatTime(timeStr string) string {
|
||||
if timeStr == "" {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// Parse the time and format it nicely
|
||||
t, err := time.Parse(time.RFC3339, timeStr)
|
||||
if err != nil {
|
||||
return timeStr // Return original if parsing fails
|
||||
}
|
||||
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Environment string
|
||||
Port string
|
||||
DatabaseURL string
|
||||
RedisURL string
|
||||
JWTSecret string
|
||||
CORS CORSConfig
|
||||
Proxmox ProxmoxConfig
|
||||
}
|
||||
|
||||
type CORSConfig struct {
|
||||
AllowedOrigins []string
|
||||
}
|
||||
|
||||
type ProxmoxConfig struct {
|
||||
BaseURL string
|
||||
Username string
|
||||
Password string
|
||||
TokenID string
|
||||
Token string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
cfg := &Config{
|
||||
Environment: getEnv("ENVIRONMENT", "development"),
|
||||
Port: getEnv("PORT", "8080"),
|
||||
DatabaseURL: getEnv("DATABASE_URL", "postgres://containr:password@localhost:5432/containr?sslmode=disable"),
|
||||
RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"),
|
||||
JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"),
|
||||
CORS: CORSConfig{
|
||||
AllowedOrigins: []string{
|
||||
"http://localhost:3000", // Vite dev server
|
||||
"http://localhost:5173", // Alternative Vite port
|
||||
},
|
||||
},
|
||||
Proxmox: ProxmoxConfig{
|
||||
BaseURL: getEnv("PROXMOX_BASE_URL", ""),
|
||||
Username: getEnv("PROXMOX_USERNAME", ""),
|
||||
Password: getEnv("PROXMOX_PASSWORD", ""),
|
||||
TokenID: getEnv("PROXMOX_TOKEN_ID", ""),
|
||||
Token: getEnv("PROXMOX_TOKEN", ""),
|
||||
},
|
||||
}
|
||||
|
||||
// Add production origins if in production
|
||||
if cfg.Environment == "production" {
|
||||
cfg.CORS.AllowedOrigins = append(cfg.CORS.AllowedOrigins,
|
||||
getEnv("FRONTEND_URL", "https://your-domain.com"))
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getEnvAsBool(key string, defaultValue bool) bool {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if parsed, err := strconv.ParseBool(value); err == nil {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getEnvAsInt(key string, defaultValue int) int {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if parsed, err := strconv.Atoi(value); err == nil {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Migrate runs all migration files in the migrations directory
|
||||
func (db *DB) Migrate(migrationsDir string) error {
|
||||
// Create migrations table if it doesn't exist
|
||||
if err := db.createMigrationsTable(); err != nil {
|
||||
return fmt.Errorf("failed to create migrations table: %w", err)
|
||||
}
|
||||
|
||||
// Get list of migration files
|
||||
files, err := ioutil.ReadDir(migrationsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read migrations directory: %w", err)
|
||||
}
|
||||
|
||||
// Sort files by name to ensure proper order
|
||||
var migrationFiles []string
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && strings.HasSuffix(file.Name(), ".sql") {
|
||||
migrationFiles = append(migrationFiles, file.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(migrationFiles)
|
||||
|
||||
// Run each migration that hasn't been run yet
|
||||
for _, fileName := range migrationFiles {
|
||||
if err := db.runMigration(migrationsDir, fileName); err != nil {
|
||||
return fmt.Errorf("failed to run migration %s: %w", fileName, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("All migrations completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) createMigrationsTable() error {
|
||||
query := `
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename VARCHAR(255) UNIQUE NOT NULL,
|
||||
executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
)
|
||||
`
|
||||
_, err := db.Exec(query)
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *DB) runMigration(migrationsDir, fileName string) error {
|
||||
// Check if migration has already been run
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM migrations WHERE filename = $1", fileName).Scan(&count)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check migration status: %w", err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
log.Printf("Migration %s already executed, skipping", fileName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read migration file
|
||||
filePath := filepath.Join(migrationsDir, fileName)
|
||||
content, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read migration file %s: %w", fileName, err)
|
||||
}
|
||||
|
||||
// Execute migration in a transaction
|
||||
tx, err := db.BeginTx(context.Background(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Execute migration SQL
|
||||
_, err = tx.Exec(string(content))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute migration %s: %w", fileName, err)
|
||||
}
|
||||
|
||||
// Record that migration was executed
|
||||
_, err = tx.Exec("INSERT INTO migrations (filename) VALUES ($1)", fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to record migration %s: %w", fileName, err)
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err = tx.Commit(); err != nil {
|
||||
return fmt.Errorf("failed to commit migration %s: %w", fileName, err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully executed migration: %s", fileName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeedData inserts initial data for development
|
||||
func (db *DB) SeedData() error {
|
||||
// Check if we already have users
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing users: %w", err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
log.Println("Database already has data, skipping seed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Insert demo user
|
||||
hashedPassword := "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi" // "password"
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO users (email, password_hash, name)
|
||||
VALUES ($1, $2, $3)
|
||||
`, "demo@containr.dev", hashedPassword, "Demo User")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create demo user: %w", err)
|
||||
}
|
||||
|
||||
// Insert demo project
|
||||
var projectID string
|
||||
err = db.QueryRow(`
|
||||
INSERT INTO projects (name, description, owner_id)
|
||||
VALUES ($1, $2, (SELECT id FROM users WHERE email = $3))
|
||||
RETURNING id
|
||||
`, "Demo Project", "A sample project to showcase Containr features", "demo@containr.dev").Scan(&projectID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create demo project: %w", err)
|
||||
}
|
||||
|
||||
// Insert environments
|
||||
environments := []string{"production", "preview", "development"}
|
||||
for _, env := range environments {
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO environments (name, project_id)
|
||||
VALUES ($1, $2)
|
||||
`, env, projectID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create environment %s: %w", env, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Database seeded successfully")
|
||||
return nil
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
type DBConfig struct {
|
||||
MaxOpenConns int
|
||||
MaxIdleConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
ConnMaxIdleTime time.Duration
|
||||
}
|
||||
|
||||
func NewConnection(databaseURL string) (*DB, error) {
|
||||
return NewConnectionWithConfig(databaseURL, DBConfig{
|
||||
MaxOpenConns: 25,
|
||||
MaxIdleConns: 25,
|
||||
ConnMaxLifetime: 5 * time.Minute,
|
||||
ConnMaxIdleTime: 5 * time.Minute,
|
||||
})
|
||||
}
|
||||
|
||||
func NewConnectionWithConfig(databaseURL string, config DBConfig) (*DB, error) {
|
||||
db, err := sql.Open("postgres", databaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open database: %w", err)
|
||||
}
|
||||
|
||||
// Configure connection pool
|
||||
db.SetMaxOpenConns(config.MaxOpenConns)
|
||||
db.SetMaxIdleConns(config.MaxIdleConns)
|
||||
db.SetConnMaxLifetime(config.ConnMaxLifetime)
|
||||
db.SetConnMaxIdleTime(config.ConnMaxIdleTime)
|
||||
|
||||
// Test the connection
|
||||
if err := db.PingContext(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf("unable to ping database: %w", err)
|
||||
}
|
||||
|
||||
return &DB{DB: db}, nil
|
||||
}
|
||||
|
||||
func (db *DB) Health(ctx context.Context) error {
|
||||
return db.PingContext(ctx)
|
||||
}
|
||||
|
||||
// Stats returns connection pool statistics for monitoring
|
||||
func (db *DB) Stats() sql.DBStats {
|
||||
return db.Stats()
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
type Redis struct {
|
||||
Client *redis.Client
|
||||
}
|
||||
|
||||
func NewRedis(redisURL string) *Redis {
|
||||
opt, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
// Fallback to default Redis options if URL parsing fails
|
||||
opt = &redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Password: "",
|
||||
DB: 0,
|
||||
}
|
||||
}
|
||||
|
||||
client := redis.NewClient(opt)
|
||||
|
||||
return &Redis{Client: client}
|
||||
}
|
||||
|
||||
func (r *Redis) Close() error {
|
||||
return r.Client.Close()
|
||||
}
|
||||
|
||||
func (r *Redis) Health(ctx context.Context) error {
|
||||
_, err := r.Client.Ping(ctx).Result()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Redis) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
return r.Client.Set(ctx, key, value, expiration).Err()
|
||||
}
|
||||
|
||||
func (r *Redis) Get(ctx context.Context, key string) (string, error) {
|
||||
return r.Client.Get(ctx, key).Result()
|
||||
}
|
||||
|
||||
func (r *Redis) Del(ctx context.Context, keys ...string) error {
|
||||
return r.Client.Del(ctx, keys...).Err()
|
||||
}
|
||||
|
||||
func (r *Redis) Exists(ctx context.Context, key string) (bool, error) {
|
||||
result, err := r.Client.Exists(ctx, key).Result()
|
||||
return result > 0, err
|
||||
}
|
||||
@@ -1,490 +0,0 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"containr/internal/build"
|
||||
"containr/internal/docker"
|
||||
"containr/internal/types"
|
||||
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
type DeploymentEngine struct {
|
||||
buildManager *build.BuildManager
|
||||
dockerClient *docker.Client
|
||||
scheduler *Scheduler
|
||||
deployments map[string]*Deployment
|
||||
deploymentLog chan *DeploymentEvent
|
||||
}
|
||||
|
||||
type Deployment struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ServiceID string `json:"service_id"`
|
||||
Status string `json:"status"`
|
||||
ImageName string `json:"image_name"`
|
||||
ImageTag string `json:"image_tag"`
|
||||
Environment string `json:"environment"`
|
||||
Replicas int `json:"replicas"`
|
||||
Config ServiceConfig `json:"config"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Containers []ContainerInfo `json:"containers"`
|
||||
BuildLog string `json:"build_log"`
|
||||
DeployLog string `json:"deploy_log"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
type ServiceConfig struct {
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
Command []string `json:"command,omitempty"`
|
||||
Environment map[string]string `json:"environment,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
RestartPolicy string `json:"restart_policy"`
|
||||
PortMappings []PortMapping `json:"port_mappings,omitempty"`
|
||||
VolumeMounts []VolumeMount `json:"volume_mounts,omitempty"`
|
||||
Networks []string `json:"networks,omitempty"`
|
||||
Resources ResourceLimits `json:"resources,omitempty"`
|
||||
HealthCheck *HealthCheck `json:"health_check,omitempty"`
|
||||
Replicas int `json:"replicas"`
|
||||
}
|
||||
|
||||
type PortMapping struct {
|
||||
ContainerPort int32 `json:"container_port"`
|
||||
HostPort int32 `json:"host_port,omitempty"`
|
||||
Protocol string `json:"protocol"`
|
||||
HostIP string `json:"host_ip,omitempty"`
|
||||
}
|
||||
|
||||
type VolumeMount struct {
|
||||
Type string `json:"type"`
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
ReadOnly bool `json:"read_only,omitempty"`
|
||||
}
|
||||
|
||||
type ResourceLimits struct {
|
||||
MemoryBytes int64 `json:"memory_bytes,omitempty"`
|
||||
CPUQuota int64 `json:"cpu_quota,omitempty"`
|
||||
CPUPeriod int64 `json:"cpu_period,omitempty"`
|
||||
CPUShares int64 `json:"cpu_shares,omitempty"`
|
||||
}
|
||||
|
||||
type HealthCheck struct {
|
||||
Test []string `json:"test"`
|
||||
Interval time.Duration `json:"interval"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
Retries int `json:"retries"`
|
||||
StartPeriod time.Duration `json:"start_period"`
|
||||
}
|
||||
|
||||
type ContainerInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
Ports []PortInfo `json:"ports,omitempty"`
|
||||
Resources ResourceUsage `json:"resources"`
|
||||
Health *HealthStatus `json:"health,omitempty"`
|
||||
}
|
||||
|
||||
type PortInfo struct {
|
||||
ContainerPort int32 `json:"container_port"`
|
||||
HostPort int32 `json:"host_port,omitempty"`
|
||||
HostIP string `json:"host_ip"`
|
||||
Protocol string `json:"protocol"`
|
||||
}
|
||||
|
||||
type ResourceUsage struct {
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
MemoryUsage int64 `json:"memory_usage"`
|
||||
MemoryLimit int64 `json:"memory_limit"`
|
||||
NetworkRx int64 `json:"network_rx"`
|
||||
NetworkTx int64 `json:"network_tx"`
|
||||
}
|
||||
|
||||
type HealthStatus struct {
|
||||
Status string `json:"status"`
|
||||
FailingStreak int `json:"failing_streak"`
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
}
|
||||
|
||||
type DeploymentEvent struct {
|
||||
Type string `json:"type"`
|
||||
Deployment *Deployment `json:"deployment"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type DeploymentRequest struct {
|
||||
ProjectID string `json:"project_id"`
|
||||
ServiceID string `json:"service_id"`
|
||||
Environment string `json:"environment"`
|
||||
Config ServiceConfig `json:"config"`
|
||||
BuildConfig *BuildConfig `json:"build_config,omitempty"`
|
||||
Trigger TriggerConfig `json:"trigger"`
|
||||
}
|
||||
|
||||
type BuildConfig struct {
|
||||
BuildType string `json:"build_type"`
|
||||
SourcePath string `json:"source_path"`
|
||||
PrebuiltImage string `json:"prebuilt_image"`
|
||||
BuildCommand string `json:"build_command"`
|
||||
StartCommand string `json:"start_command"`
|
||||
Environment map[string]string `json:"environment"`
|
||||
Branch string `json:"branch"`
|
||||
Commit string `json:"commit"`
|
||||
}
|
||||
|
||||
type TriggerConfig struct {
|
||||
Type string `json:"type"` // webhook, manual, api, scheduled
|
||||
Source string `json:"source"` // Source of trigger
|
||||
User string `json:"user"` // User who triggered
|
||||
Data map[string]string `json:"data"` // Trigger-specific data
|
||||
Timestamp time.Time `json:"timestamp"` // When trigger occurred
|
||||
}
|
||||
|
||||
func NewDeploymentEngine(buildManager *build.BuildManager, dockerClient *docker.Client) *DeploymentEngine {
|
||||
return &DeploymentEngine{
|
||||
buildManager: buildManager,
|
||||
dockerClient: dockerClient,
|
||||
scheduler: NewScheduler(),
|
||||
deployments: make(map[string]*Deployment),
|
||||
deploymentLog: make(chan *DeploymentEvent, 1000),
|
||||
}
|
||||
}
|
||||
|
||||
// Deploy starts a new deployment
|
||||
func (de *DeploymentEngine) Deploy(ctx context.Context, req *DeploymentRequest) (*Deployment, error) {
|
||||
deployment := &Deployment{
|
||||
ID: generateDeploymentID(),
|
||||
ProjectID: req.ProjectID,
|
||||
ServiceID: req.ServiceID,
|
||||
Status: "pending",
|
||||
Environment: req.Environment,
|
||||
Config: req.Config,
|
||||
CreatedAt: time.Now(),
|
||||
Metadata: map[string]string{
|
||||
"trigger_type": req.Trigger.Type,
|
||||
"trigger_source": req.Trigger.Source,
|
||||
"branch": req.BuildConfig.Branch,
|
||||
"commit": req.BuildConfig.Commit,
|
||||
},
|
||||
}
|
||||
|
||||
// Store deployment
|
||||
de.deployments[deployment.ID] = deployment
|
||||
|
||||
// Log deployment start
|
||||
de.logEvent(&DeploymentEvent{
|
||||
Type: "deployment_started",
|
||||
Deployment: deployment,
|
||||
Timestamp: time.Now(),
|
||||
Message: fmt.Sprintf("Deployment started for service %s", req.ServiceID),
|
||||
})
|
||||
|
||||
// Start deployment in background
|
||||
go de.executeDeployment(ctx, deployment, req)
|
||||
|
||||
return deployment, nil
|
||||
}
|
||||
|
||||
// executeDeployment executes the deployment process
|
||||
func (de *DeploymentEngine) executeDeployment(ctx context.Context, deployment *Deployment, req *DeploymentRequest) {
|
||||
deployment.Status = "building"
|
||||
deployment.StartedAt = &[]time.Time{time.Now()}[0]
|
||||
|
||||
de.logEvent(&DeploymentEvent{
|
||||
Type: "build_started",
|
||||
Deployment: deployment,
|
||||
Timestamp: time.Now(),
|
||||
Message: "Build process started",
|
||||
})
|
||||
|
||||
// Step 1: Build the image
|
||||
imageName, err := de.buildImage(ctx, deployment, req.BuildConfig)
|
||||
if err != nil {
|
||||
deployment.Status = "failed"
|
||||
deployment.Error = fmt.Sprintf("Build failed: %v", err)
|
||||
deployment.CompletedAt = &[]time.Time{time.Now()}[0]
|
||||
|
||||
de.logEvent(&DeploymentEvent{
|
||||
Type: "build_failed",
|
||||
Deployment: deployment,
|
||||
Timestamp: time.Now(),
|
||||
Message: deployment.Error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
deployment.ImageName = imageName
|
||||
deployment.Status = "deploying"
|
||||
|
||||
de.logEvent(&DeploymentEvent{
|
||||
Type: "build_completed",
|
||||
Deployment: deployment,
|
||||
Timestamp: time.Now(),
|
||||
Message: fmt.Sprintf("Build completed successfully: %s", imageName),
|
||||
})
|
||||
|
||||
// Step 2: Deploy the service
|
||||
err = de.deployService(ctx, deployment)
|
||||
if err != nil {
|
||||
deployment.Status = "failed"
|
||||
deployment.Error = fmt.Sprintf("Deployment failed: %v", err)
|
||||
deployment.CompletedAt = &[]time.Time{time.Now()}[0]
|
||||
|
||||
de.logEvent(&DeploymentEvent{
|
||||
Type: "deployment_failed",
|
||||
Deployment: deployment,
|
||||
Timestamp: time.Now(),
|
||||
Message: deployment.Error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
deployment.Status = "running"
|
||||
deployment.CompletedAt = &[]time.Time{time.Now()}[0]
|
||||
|
||||
de.logEvent(&DeploymentEvent{
|
||||
Type: "deployment_completed",
|
||||
Deployment: deployment,
|
||||
Timestamp: time.Now(),
|
||||
Message: "Deployment completed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// buildImage builds the container image
|
||||
func (de *DeploymentEngine) buildImage(ctx context.Context, deployment *Deployment, buildConfig *BuildConfig) (string, error) {
|
||||
if buildConfig == nil {
|
||||
return "", fmt.Errorf("build config is required")
|
||||
}
|
||||
|
||||
buildReq := &types.BuildRequest{
|
||||
BuildType: buildConfig.BuildType,
|
||||
SourcePath: buildConfig.SourcePath,
|
||||
PrebuiltImage: buildConfig.PrebuiltImage,
|
||||
ImageName: fmt.Sprintf("containr-%s-%s", deployment.ServiceID, deployment.Environment),
|
||||
ImageTag: deployment.ID,
|
||||
BuildCommand: buildConfig.BuildCommand,
|
||||
StartCommand: buildConfig.StartCommand,
|
||||
Environment: buildConfig.Environment,
|
||||
ProjectID: deployment.ProjectID,
|
||||
ServiceID: deployment.ServiceID,
|
||||
DeploymentID: deployment.ID,
|
||||
TriggeredBy: "deployment_engine",
|
||||
Branch: buildConfig.Branch,
|
||||
Commit: buildConfig.Commit,
|
||||
}
|
||||
|
||||
response, err := de.buildManager.Build(ctx, buildReq)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
deployment.BuildLog = response.BuildLog
|
||||
|
||||
return response.ImageName, nil
|
||||
}
|
||||
|
||||
// deployService deploys the service using the built image
|
||||
func (de *DeploymentEngine) deployService(ctx context.Context, deployment *Deployment) error {
|
||||
// Convert service config to Docker container config
|
||||
containerConfig := &docker.ContainerConfig{
|
||||
Name: fmt.Sprintf("containr-%s-%s", deployment.ServiceID, deployment.ID),
|
||||
Image: deployment.ImageName,
|
||||
Cmd: deployment.Config.Command,
|
||||
Labels: deployment.Config.Labels,
|
||||
Networks: make(map[string]*network.EndpointSettings),
|
||||
}
|
||||
|
||||
// Set environment variables
|
||||
for k, v := range deployment.Config.Environment {
|
||||
containerConfig.Env = append(containerConfig.Env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
|
||||
// Set restart policy
|
||||
containerConfig.RestartPolicy = deployment.Config.RestartPolicy
|
||||
|
||||
// Configure port mappings
|
||||
portBindings := make(nat.PortMap)
|
||||
for _, pm := range deployment.Config.PortMappings {
|
||||
port := nat.Port(fmt.Sprintf("%d/%s", pm.ContainerPort, pm.Protocol))
|
||||
if pm.HostPort > 0 {
|
||||
portBindings[port] = []nat.PortBinding{
|
||||
{
|
||||
HostIP: pm.HostIP,
|
||||
HostPort: fmt.Sprintf("%d", pm.HostPort),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
containerConfig.PortBindings = portBindings
|
||||
|
||||
// Configure resource limits
|
||||
if deployment.Config.Resources.MemoryBytes > 0 {
|
||||
containerConfig.Memory = deployment.Config.Resources.MemoryBytes
|
||||
}
|
||||
if deployment.Config.Resources.CPUQuota > 0 {
|
||||
containerConfig.NanoCPUs = deployment.Config.Resources.CPUQuota
|
||||
}
|
||||
|
||||
// Configure volume mounts
|
||||
for _, vm := range deployment.Config.VolumeMounts {
|
||||
mount := mount.Mount{
|
||||
Type: mount.Type(vm.Type),
|
||||
Source: vm.Source,
|
||||
Target: vm.Destination,
|
||||
ReadOnly: vm.ReadOnly,
|
||||
}
|
||||
containerConfig.Mounts = append(containerConfig.Mounts, mount)
|
||||
}
|
||||
|
||||
// Create containers based on replica count
|
||||
deployment.Containers = make([]ContainerInfo, deployment.Config.Replicas)
|
||||
for i := 0; i < deployment.Config.Replicas; i++ {
|
||||
containerName := fmt.Sprintf("%s-%d", containerConfig.Name, i)
|
||||
|
||||
// Create container
|
||||
containerID, err := de.dockerClient.CreateContainer(ctx, *containerConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create container %d: %w", i, err)
|
||||
}
|
||||
|
||||
// Start container
|
||||
err = de.dockerClient.StartContainer(ctx, containerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start container %d: %w", i, err)
|
||||
}
|
||||
|
||||
// Get container info
|
||||
_, err = de.dockerClient.GetContainer(ctx, containerID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get container info for %s: %v", containerID, err)
|
||||
}
|
||||
|
||||
deployment.Containers[i] = ContainerInfo{
|
||||
ID: containerID,
|
||||
Name: containerName,
|
||||
Status: "running",
|
||||
CreatedAt: time.Now(),
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDeployment gets a deployment by ID
|
||||
func (de *DeploymentEngine) GetDeployment(id string) (*Deployment, error) {
|
||||
deployment, exists := de.deployments[id]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("deployment not found: %s", id)
|
||||
}
|
||||
return deployment, nil
|
||||
}
|
||||
|
||||
// ListDeployments lists all deployments
|
||||
func (de *DeploymentEngine) ListDeployments(projectID, serviceID string) ([]*Deployment, error) {
|
||||
var deployments []*Deployment
|
||||
|
||||
for _, deployment := range de.deployments {
|
||||
if projectID != "" && deployment.ProjectID != projectID {
|
||||
continue
|
||||
}
|
||||
if serviceID != "" && deployment.ServiceID != serviceID {
|
||||
continue
|
||||
}
|
||||
deployments = append(deployments, deployment)
|
||||
}
|
||||
|
||||
return deployments, nil
|
||||
}
|
||||
|
||||
// CancelDeployment cancels a running deployment
|
||||
func (de *DeploymentEngine) CancelDeployment(ctx context.Context, id string) error {
|
||||
deployment, exists := de.deployments[id]
|
||||
if !exists {
|
||||
return fmt.Errorf("deployment not found: %s", id)
|
||||
}
|
||||
|
||||
if deployment.Status == "completed" || deployment.Status == "failed" {
|
||||
return fmt.Errorf("cannot cancel completed deployment: %s", id)
|
||||
}
|
||||
|
||||
// Stop all containers
|
||||
for _, container := range deployment.Containers {
|
||||
err := de.dockerClient.StopContainer(ctx, container.ID, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to stop container %s: %v", container.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
deployment.Status = "cancelled"
|
||||
deployment.CompletedAt = &[]time.Time{time.Now()}[0]
|
||||
|
||||
de.logEvent(&DeploymentEvent{
|
||||
Type: "deployment_cancelled",
|
||||
Deployment: deployment,
|
||||
Timestamp: time.Now(),
|
||||
Message: "Deployment was cancelled",
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDeploymentLogs gets the logs for a deployment
|
||||
func (de *DeploymentEngine) GetDeploymentLogs(ctx context.Context, id string) (string, error) {
|
||||
deployment, exists := de.deployments[id]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("deployment not found: %s", id)
|
||||
}
|
||||
|
||||
logs := deployment.BuildLog
|
||||
logs += "\n" + deployment.DeployLog
|
||||
|
||||
// Add container logs
|
||||
for _, container := range deployment.Containers {
|
||||
containerLogs, err := de.dockerClient.GetContainerLogs(ctx, container.ID, docker.LogOptions{
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to get logs for container %s: %v", container.ID, err)
|
||||
continue
|
||||
}
|
||||
logs += fmt.Sprintf("\n=== Container %s Logs ===\n%s", container.Name, containerLogs)
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// WatchDeploymentEvents returns a channel of deployment events
|
||||
func (de *DeploymentEngine) WatchDeploymentEvents() <-chan *DeploymentEvent {
|
||||
return de.deploymentLog
|
||||
}
|
||||
|
||||
// logEvent logs a deployment event
|
||||
func (de *DeploymentEngine) logEvent(event *DeploymentEvent) {
|
||||
select {
|
||||
case de.deploymentLog <- event:
|
||||
default:
|
||||
// Channel is full, drop the event
|
||||
log.Printf("Deployment event channel is full, dropping event: %s", event.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// generateDeploymentID generates a unique deployment ID
|
||||
func generateDeploymentID() string {
|
||||
return fmt.Sprintf("deploy-%d", time.Now().UnixNano())
|
||||
}
|
||||
@@ -1,718 +0,0 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type HistoryManager struct {
|
||||
storagePath string
|
||||
mu sync.RWMutex
|
||||
deployments map[string]*DeploymentRecord
|
||||
}
|
||||
|
||||
type DeploymentRecord struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ServiceID string `json:"service_id"`
|
||||
Environment string `json:"environment"`
|
||||
Status string `json:"status"`
|
||||
ImageName string `json:"image_name"`
|
||||
ImageTag string `json:"image_tag"`
|
||||
Config ServiceConfig `json:"config"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
Containers []ContainerRecord `json:"containers"`
|
||||
BuildLog string `json:"build_log"`
|
||||
DeployLog string `json:"deploy_log"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Trigger TriggerRecord `json:"trigger"`
|
||||
RollbackFrom *string `json:"rollback_from,omitempty"`
|
||||
Rollbacks []string `json:"rollbacks"`
|
||||
Tags []string `json:"tags"`
|
||||
Annotations map[string]interface{} `json:"annotations"`
|
||||
}
|
||||
|
||||
type ContainerRecord struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
StoppedAt *time.Time `json:"stopped_at,omitempty"`
|
||||
Ports []PortRecord `json:"ports,omitempty"`
|
||||
Resources ResourceRecord `json:"resources"`
|
||||
Health *HealthRecord `json:"health,omitempty"`
|
||||
ExitCode *int `json:"exit_code,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
}
|
||||
|
||||
type PortRecord struct {
|
||||
ContainerPort int32 `json:"container_port"`
|
||||
HostPort int32 `json:"host_port,omitempty"`
|
||||
HostIP string `json:"host_ip"`
|
||||
Protocol string `json:"protocol"`
|
||||
}
|
||||
|
||||
type ResourceRecord struct {
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
MemoryUsage int64 `json:"memory_usage"`
|
||||
MemoryLimit int64 `json:"memory_limit"`
|
||||
NetworkRx int64 `json:"network_rx"`
|
||||
NetworkTx int64 `json:"network_tx"`
|
||||
PidsCurrent uint64 `json:"pids_current"`
|
||||
PidsLimit uint64 `json:"pids_limit"`
|
||||
}
|
||||
|
||||
type HealthRecord struct {
|
||||
Status string `json:"status"`
|
||||
FailingStreak int `json:"failing_streak"`
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
Output string `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
type TriggerRecord struct {
|
||||
Type string `json:"type"` // webhook, manual, api, scheduled
|
||||
Source string `json:"source"` // Source of trigger
|
||||
User string `json:"user"` // User who triggered
|
||||
Data map[string]string `json:"data"` // Trigger-specific data
|
||||
Timestamp time.Time `json:"timestamp"` // When trigger occurred
|
||||
}
|
||||
|
||||
type DeploymentFilter struct {
|
||||
ProjectID string `json:"project_id,omitempty"`
|
||||
ServiceID string `json:"service_id,omitempty"`
|
||||
Environment string `json:"environment,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
TriggerType string `json:"trigger_type,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
From time.Time `json:"from,omitempty"`
|
||||
To time.Time `json:"to,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
SortBy string `json:"sort_by,omitempty"` // created_at, started_at, completed_at, duration
|
||||
SortOrder string `json:"sort_order,omitempty"` // asc, desc
|
||||
}
|
||||
|
||||
type DeploymentStats struct {
|
||||
TotalDeployments int `json:"total_deployments"`
|
||||
SuccessfulDeployments int `json:"successful_deployments"`
|
||||
FailedDeployments int `json:"failed_deployments"`
|
||||
AverageDuration time.Duration `json:"average_duration"`
|
||||
DeploymentsByStatus map[string]int `json:"deployments_by_status"`
|
||||
DeploymentsByEnv map[string]int `json:"deployments_by_env"`
|
||||
DeploymentsByDay map[string]int `json:"deployments_by_day"`
|
||||
RecentActivity []DeploymentRecord `json:"recent_activity"`
|
||||
TopServices []ServiceDeploymentStats `json:"top_services"`
|
||||
TopUsers []UserDeploymentStats `json:"top_users"`
|
||||
}
|
||||
|
||||
type ServiceDeploymentStats struct {
|
||||
ServiceID string `json:"service_id"`
|
||||
ServiceName string `json:"service_name"`
|
||||
DeploymentCount int `json:"deployment_count"`
|
||||
SuccessCount int `json:"success_count"`
|
||||
FailureCount int `json:"failure_count"`
|
||||
SuccessRate float64 `json:"success_rate"`
|
||||
AverageDuration time.Duration `json:"average_duration"`
|
||||
LastDeployment time.Time `json:"last_deployment"`
|
||||
}
|
||||
|
||||
type UserDeploymentStats struct {
|
||||
User string `json:"user"`
|
||||
DeploymentCount int `json:"deployment_count"`
|
||||
SuccessCount int `json:"success_count"`
|
||||
FailureCount int `json:"failure_count"`
|
||||
SuccessRate float64 `json:"success_rate"`
|
||||
AverageDuration time.Duration `json:"average_duration"`
|
||||
LastDeployment time.Time `json:"last_deployment"`
|
||||
}
|
||||
|
||||
func NewHistoryManager(storagePath string) *HistoryManager {
|
||||
return &HistoryManager{
|
||||
storagePath: storagePath,
|
||||
deployments: make(map[string]*DeploymentRecord),
|
||||
}
|
||||
}
|
||||
|
||||
// RecordDeployment records a deployment in history
|
||||
func (hm *HistoryManager) RecordDeployment(deployment *Deployment) error {
|
||||
hm.mu.Lock()
|
||||
defer hm.mu.Unlock()
|
||||
|
||||
record := hm.convertToRecord(deployment)
|
||||
hm.deployments[record.ID] = record
|
||||
|
||||
// Save to storage
|
||||
return hm.saveDeployment(record)
|
||||
}
|
||||
|
||||
// GetDeployment gets a deployment record by ID
|
||||
func (hm *HistoryManager) GetDeployment(id string) (*DeploymentRecord, error) {
|
||||
hm.mu.RLock()
|
||||
defer hm.mu.RUnlock()
|
||||
|
||||
record, exists := hm.deployments[id]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("deployment not found: %s", id)
|
||||
}
|
||||
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// ListDeployments lists deployments with filtering
|
||||
func (hm *HistoryManager) ListDeployments(filter DeploymentFilter) ([]*DeploymentRecord, error) {
|
||||
hm.mu.RLock()
|
||||
defer hm.mu.RUnlock()
|
||||
|
||||
var deployments []*DeploymentRecord
|
||||
|
||||
for _, record := range hm.deployments {
|
||||
if hm.matchesFilter(record, filter) {
|
||||
deployments = append(deployments, record)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort deployments
|
||||
hm.sortDeployments(deployments, filter.SortBy, filter.SortOrder)
|
||||
|
||||
// Apply pagination
|
||||
if filter.Limit > 0 {
|
||||
start := filter.Offset
|
||||
if start >= len(deployments) {
|
||||
return []*DeploymentRecord{}, nil
|
||||
}
|
||||
end := start + filter.Limit
|
||||
if end > len(deployments) {
|
||||
end = len(deployments)
|
||||
}
|
||||
deployments = deployments[start:end]
|
||||
}
|
||||
|
||||
return deployments, nil
|
||||
}
|
||||
|
||||
// RollbackDeployment creates a rollback deployment
|
||||
func (hm *HistoryManager) RollbackDeployment(ctx context.Context, deploymentID, reason string, userID string) (*DeploymentRecord, error) {
|
||||
hm.mu.RLock()
|
||||
originalDeployment, exists := hm.deployments[deploymentID]
|
||||
hm.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("deployment not found: %s", deploymentID)
|
||||
}
|
||||
|
||||
// Create rollback deployment record
|
||||
rollbackRecord := &DeploymentRecord{
|
||||
ID: generateDeploymentID(),
|
||||
ProjectID: originalDeployment.ProjectID,
|
||||
ServiceID: originalDeployment.ServiceID,
|
||||
Environment: originalDeployment.Environment,
|
||||
Status: "pending",
|
||||
ImageName: originalDeployment.ImageName,
|
||||
ImageTag: originalDeployment.ImageTag,
|
||||
Config: originalDeployment.Config,
|
||||
CreatedAt: time.Now(),
|
||||
Metadata: map[string]string{
|
||||
"rollback_from": deploymentID,
|
||||
"rollback_reason": reason,
|
||||
},
|
||||
Trigger: TriggerRecord{
|
||||
Type: "rollback",
|
||||
Source: "deployment_history",
|
||||
User: userID,
|
||||
Data: map[string]string{
|
||||
"original_deployment": deploymentID,
|
||||
"reason": reason,
|
||||
},
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
RollbackFrom: &deploymentID,
|
||||
Tags: append(originalDeployment.Tags, "rollback"),
|
||||
}
|
||||
|
||||
// Record the rollback
|
||||
err := hm.RecordDeployment(&Deployment{
|
||||
ID: rollbackRecord.ID,
|
||||
ProjectID: rollbackRecord.ProjectID,
|
||||
ServiceID: rollbackRecord.ServiceID,
|
||||
Environment: rollbackRecord.Environment,
|
||||
Status: rollbackRecord.Status,
|
||||
ImageName: rollbackRecord.ImageName,
|
||||
ImageTag: rollbackRecord.ImageTag,
|
||||
Config: rollbackRecord.Config,
|
||||
CreatedAt: rollbackRecord.CreatedAt,
|
||||
Metadata: rollbackRecord.Metadata,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to record rollback: %w", err)
|
||||
}
|
||||
|
||||
// Update original deployment to track rollbacks
|
||||
hm.mu.Lock()
|
||||
if original, exists := hm.deployments[deploymentID]; exists {
|
||||
original.Rollbacks = append(original.Rollbacks, rollbackRecord.ID)
|
||||
hm.saveDeployment(original)
|
||||
}
|
||||
hm.mu.Unlock()
|
||||
|
||||
return rollbackRecord, nil
|
||||
}
|
||||
|
||||
// GetDeploymentHistory gets the deployment history for a service
|
||||
func (hm *HistoryManager) GetDeploymentHistory(serviceID, environment string, limit int) ([]*DeploymentRecord, error) {
|
||||
filter := DeploymentFilter{
|
||||
ServiceID: serviceID,
|
||||
Environment: environment,
|
||||
Limit: limit,
|
||||
SortBy: "created_at",
|
||||
SortOrder: "desc",
|
||||
}
|
||||
|
||||
return hm.ListDeployments(filter)
|
||||
}
|
||||
|
||||
// GetDeploymentStats gets deployment statistics
|
||||
func (hm *HistoryManager) GetDeploymentStats(projectID string) (*DeploymentStats, error) {
|
||||
hm.mu.RLock()
|
||||
defer hm.mu.RUnlock()
|
||||
|
||||
stats := &DeploymentStats{
|
||||
DeploymentsByStatus: make(map[string]int),
|
||||
DeploymentsByEnv: make(map[string]int),
|
||||
DeploymentsByDay: make(map[string]int),
|
||||
}
|
||||
|
||||
var totalDuration time.Duration
|
||||
var successfulDeployments int
|
||||
|
||||
for _, record := range hm.deployments {
|
||||
if projectID != "" && record.ProjectID != projectID {
|
||||
continue
|
||||
}
|
||||
|
||||
stats.TotalDeployments++
|
||||
|
||||
// Count by status
|
||||
stats.DeploymentsByStatus[record.Status]++
|
||||
|
||||
// Count by environment
|
||||
stats.DeploymentsByEnv[record.Environment]++
|
||||
|
||||
// Count by day
|
||||
day := record.CreatedAt.Format("2006-01-02")
|
||||
stats.DeploymentsByDay[day]++
|
||||
|
||||
// Calculate success metrics
|
||||
if record.Status == "running" || record.Status == "completed" {
|
||||
successfulDeployments++
|
||||
stats.SuccessfulDeployments++
|
||||
} else if record.Status == "failed" {
|
||||
stats.FailedDeployments++
|
||||
}
|
||||
|
||||
// Calculate duration
|
||||
if record.Duration > 0 {
|
||||
totalDuration += record.Duration
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate average duration
|
||||
if stats.TotalDeployments > 0 {
|
||||
stats.AverageDuration = totalDuration / time.Duration(stats.TotalDeployments)
|
||||
}
|
||||
|
||||
// Get recent activity
|
||||
stats.RecentActivity = hm.getRecentActivity(projectID, 10)
|
||||
|
||||
// Get top services and users
|
||||
stats.TopServices = hm.getTopServices(projectID, 5)
|
||||
stats.TopUsers = hm.getTopUsers(projectID, 5)
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// DeleteDeployment removes a deployment from history
|
||||
func (hm *HistoryManager) DeleteDeployment(id string) error {
|
||||
hm.mu.Lock()
|
||||
defer hm.mu.Unlock()
|
||||
|
||||
if _, exists := hm.deployments[id]; !exists {
|
||||
return fmt.Errorf("deployment not found: %s", id)
|
||||
}
|
||||
|
||||
delete(hm.deployments, id)
|
||||
|
||||
// Remove from storage
|
||||
return hm.deleteDeploymentFile(id)
|
||||
}
|
||||
|
||||
// convertToRecord converts a Deployment to DeploymentRecord
|
||||
func (hm *HistoryManager) convertToRecord(deployment *Deployment) *DeploymentRecord {
|
||||
record := &DeploymentRecord{
|
||||
ID: deployment.ID,
|
||||
ProjectID: deployment.ProjectID,
|
||||
ServiceID: deployment.ServiceID,
|
||||
Environment: deployment.Environment,
|
||||
Status: deployment.Status,
|
||||
ImageName: deployment.ImageName,
|
||||
ImageTag: deployment.ImageTag,
|
||||
Config: deployment.Config,
|
||||
CreatedAt: deployment.CreatedAt,
|
||||
StartedAt: deployment.StartedAt,
|
||||
CompletedAt: deployment.CompletedAt,
|
||||
BuildLog: deployment.BuildLog,
|
||||
DeployLog: deployment.DeployLog,
|
||||
Error: deployment.Error,
|
||||
Metadata: deployment.Metadata,
|
||||
Tags: []string{},
|
||||
Annotations: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
// Calculate duration
|
||||
if deployment.StartedAt != nil && deployment.CompletedAt != nil {
|
||||
record.Duration = deployment.CompletedAt.Sub(*deployment.StartedAt)
|
||||
}
|
||||
|
||||
// Convert containers
|
||||
for _, container := range deployment.Containers {
|
||||
containerRecord := ContainerRecord{
|
||||
ID: container.ID,
|
||||
Name: container.Name,
|
||||
Status: container.Status,
|
||||
CreatedAt: container.CreatedAt,
|
||||
StartedAt: container.StartedAt,
|
||||
Resources: ResourceRecord{
|
||||
CPUPercent: container.Resources.CPUPercent,
|
||||
MemoryUsage: container.Resources.MemoryUsage,
|
||||
MemoryLimit: container.Resources.MemoryLimit,
|
||||
NetworkRx: container.Resources.NetworkRx,
|
||||
NetworkTx: container.Resources.NetworkTx,
|
||||
},
|
||||
}
|
||||
|
||||
if container.Health != nil {
|
||||
containerRecord.Health = &HealthRecord{
|
||||
Status: container.Health.Status,
|
||||
FailingStreak: container.Health.FailingStreak,
|
||||
LastCheck: container.Health.LastCheck,
|
||||
}
|
||||
}
|
||||
|
||||
record.Containers = append(record.Containers, containerRecord)
|
||||
}
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
// matchesFilter checks if a deployment record matches the filter
|
||||
func (hm *HistoryManager) matchesFilter(record *DeploymentRecord, filter DeploymentFilter) bool {
|
||||
if filter.ProjectID != "" && record.ProjectID != filter.ProjectID {
|
||||
return false
|
||||
}
|
||||
|
||||
if filter.ServiceID != "" && record.ServiceID != filter.ServiceID {
|
||||
return false
|
||||
}
|
||||
|
||||
if filter.Environment != "" && record.Environment != filter.Environment {
|
||||
return false
|
||||
}
|
||||
|
||||
if filter.Status != "" && record.Status != filter.Status {
|
||||
return false
|
||||
}
|
||||
|
||||
if filter.TriggerType != "" && record.Trigger.Type != filter.TriggerType {
|
||||
return false
|
||||
}
|
||||
|
||||
if filter.User != "" && record.Trigger.User != filter.User {
|
||||
return false
|
||||
}
|
||||
|
||||
if !filter.From.IsZero() && record.CreatedAt.Before(filter.From) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !filter.To.IsZero() && record.CreatedAt.After(filter.To) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(filter.Tags) > 0 {
|
||||
hasTag := false
|
||||
for _, tag := range filter.Tags {
|
||||
for _, recordTag := range record.Tags {
|
||||
if recordTag == tag {
|
||||
hasTag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasTag {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasTag {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// sortDeployments sorts deployments based on the specified criteria
|
||||
func (hm *HistoryManager) sortDeployments(deployments []*DeploymentRecord, sortBy, sortOrder string) {
|
||||
if sortBy == "" {
|
||||
sortBy = "created_at"
|
||||
}
|
||||
if sortOrder == "" {
|
||||
sortOrder = "desc"
|
||||
}
|
||||
|
||||
sort.Slice(deployments, func(i, j int) bool {
|
||||
var less bool
|
||||
|
||||
switch sortBy {
|
||||
case "created_at":
|
||||
less = deployments[i].CreatedAt.Before(deployments[j].CreatedAt)
|
||||
case "started_at":
|
||||
if deployments[i].StartedAt == nil {
|
||||
less = true
|
||||
} else if deployments[j].StartedAt == nil {
|
||||
less = false
|
||||
} else {
|
||||
less = deployments[i].StartedAt.Before(*deployments[j].StartedAt)
|
||||
}
|
||||
case "completed_at":
|
||||
if deployments[i].CompletedAt == nil {
|
||||
less = true
|
||||
} else if deployments[j].CompletedAt == nil {
|
||||
less = false
|
||||
} else {
|
||||
less = deployments[i].CompletedAt.Before(*deployments[j].CompletedAt)
|
||||
}
|
||||
case "duration":
|
||||
less = deployments[i].Duration < deployments[j].Duration
|
||||
default:
|
||||
less = deployments[i].ID < deployments[j].ID
|
||||
}
|
||||
|
||||
if sortOrder == "desc" {
|
||||
return !less
|
||||
}
|
||||
return less
|
||||
})
|
||||
}
|
||||
|
||||
// getRecentActivity gets recent deployment activity
|
||||
func (hm *HistoryManager) getRecentActivity(projectID string, limit int) []DeploymentRecord {
|
||||
var deployments []DeploymentRecord
|
||||
|
||||
for _, record := range hm.deployments {
|
||||
if projectID != "" && record.ProjectID != projectID {
|
||||
continue
|
||||
}
|
||||
deployments = append(deployments, *record)
|
||||
}
|
||||
|
||||
// Sort by created_at desc
|
||||
sort.Slice(deployments, func(i, j int) bool {
|
||||
return deployments[i].CreatedAt.After(deployments[j].CreatedAt)
|
||||
})
|
||||
|
||||
if len(deployments) > limit {
|
||||
deployments = deployments[:limit]
|
||||
}
|
||||
|
||||
return deployments
|
||||
}
|
||||
|
||||
// getTopServices gets top services by deployment count
|
||||
func (hm *HistoryManager) getTopServices(projectID string, limit int) []ServiceDeploymentStats {
|
||||
serviceStats := make(map[string]*ServiceDeploymentStats)
|
||||
|
||||
for _, record := range hm.deployments {
|
||||
if projectID != "" && record.ProjectID != projectID {
|
||||
continue
|
||||
}
|
||||
|
||||
stats, exists := serviceStats[record.ServiceID]
|
||||
if !exists {
|
||||
stats = &ServiceDeploymentStats{
|
||||
ServiceID: record.ServiceID,
|
||||
}
|
||||
serviceStats[record.ServiceID] = stats
|
||||
}
|
||||
|
||||
stats.DeploymentCount++
|
||||
stats.LastDeployment = record.CreatedAt
|
||||
|
||||
if record.Status == "running" || record.Status == "completed" {
|
||||
stats.SuccessCount++
|
||||
} else if record.Status == "failed" {
|
||||
stats.FailureCount++
|
||||
}
|
||||
|
||||
if record.Duration > 0 {
|
||||
// Simple moving average for duration
|
||||
if stats.AverageDuration == 0 {
|
||||
stats.AverageDuration = record.Duration
|
||||
} else {
|
||||
stats.AverageDuration = (stats.AverageDuration + record.Duration) / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate success rates
|
||||
for _, stats := range serviceStats {
|
||||
if stats.DeploymentCount > 0 {
|
||||
stats.SuccessRate = float64(stats.SuccessCount) / float64(stats.DeploymentCount) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to slice and sort
|
||||
var topServices []ServiceDeploymentStats
|
||||
for _, stats := range serviceStats {
|
||||
topServices = append(topServices, *stats)
|
||||
}
|
||||
|
||||
sort.Slice(topServices, func(i, j int) bool {
|
||||
return topServices[i].DeploymentCount > topServices[j].DeploymentCount
|
||||
})
|
||||
|
||||
if len(topServices) > limit {
|
||||
topServices = topServices[:limit]
|
||||
}
|
||||
|
||||
return topServices
|
||||
}
|
||||
|
||||
// getTopUsers gets top users by deployment count
|
||||
func (hm *HistoryManager) getTopUsers(projectID string, limit int) []UserDeploymentStats {
|
||||
userStats := make(map[string]*UserDeploymentStats)
|
||||
|
||||
for _, record := range hm.deployments {
|
||||
if projectID != "" && record.ProjectID != projectID {
|
||||
continue
|
||||
}
|
||||
|
||||
user := record.Trigger.User
|
||||
if user == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
stats, exists := userStats[user]
|
||||
if !exists {
|
||||
stats = &UserDeploymentStats{
|
||||
User: user,
|
||||
}
|
||||
userStats[user] = stats
|
||||
}
|
||||
|
||||
stats.DeploymentCount++
|
||||
stats.LastDeployment = record.CreatedAt
|
||||
|
||||
if record.Status == "running" || record.Status == "completed" {
|
||||
stats.SuccessCount++
|
||||
} else if record.Status == "failed" {
|
||||
stats.FailureCount++
|
||||
}
|
||||
|
||||
if record.Duration > 0 {
|
||||
if stats.AverageDuration == 0 {
|
||||
stats.AverageDuration = record.Duration
|
||||
} else {
|
||||
stats.AverageDuration = (stats.AverageDuration + record.Duration) / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate success rates
|
||||
for _, stats := range userStats {
|
||||
if stats.DeploymentCount > 0 {
|
||||
stats.SuccessRate = float64(stats.SuccessCount) / float64(stats.DeploymentCount) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to slice and sort
|
||||
var topUsers []UserDeploymentStats
|
||||
for _, stats := range userStats {
|
||||
topUsers = append(topUsers, *stats)
|
||||
}
|
||||
|
||||
sort.Slice(topUsers, func(i, j int) bool {
|
||||
return topUsers[i].DeploymentCount > topUsers[j].DeploymentCount
|
||||
})
|
||||
|
||||
if len(topUsers) > limit {
|
||||
topUsers = topUsers[:limit]
|
||||
}
|
||||
|
||||
return topUsers
|
||||
}
|
||||
|
||||
// saveDeployment saves a deployment record to storage
|
||||
func (hm *HistoryManager) saveDeployment(record *DeploymentRecord) error {
|
||||
if err := os.MkdirAll(hm.storagePath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := filepath.Join(hm.storagePath, record.ID+".json")
|
||||
data, err := json.MarshalIndent(record, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, data, 0644)
|
||||
}
|
||||
|
||||
// deleteDeploymentFile removes a deployment file from storage
|
||||
func (hm *HistoryManager) deleteDeploymentFile(id string) error {
|
||||
filename := filepath.Join(hm.storagePath, id+".json")
|
||||
return os.Remove(filename)
|
||||
}
|
||||
|
||||
// loadDeployments loads all deployments from storage
|
||||
func (hm *HistoryManager) loadDeployments() error {
|
||||
if _, err := os.Stat(hm.storagePath); os.IsNotExist(err) {
|
||||
return nil // Storage doesn't exist yet
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(hm.storagePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() || !strings.HasSuffix(file.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
filename := filepath.Join(hm.storagePath, file.Name())
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
continue // Skip files that can't be read
|
||||
}
|
||||
|
||||
var record DeploymentRecord
|
||||
if err := json.Unmarshal(data, &record); err != nil {
|
||||
continue // Skip invalid files
|
||||
}
|
||||
|
||||
hm.deployments[record.ID] = &record
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,379 +0,0 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"containr/internal/docker"
|
||||
)
|
||||
|
||||
type Scheduler struct {
|
||||
nodes map[string]*Node
|
||||
mu sync.RWMutex
|
||||
dockerClient *docker.Client
|
||||
schedulingAlg SchedulingAlgorithm
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Status string `json:"status"`
|
||||
Capacity ResourceCapacity `json:"capacity"`
|
||||
Usage NodeResourceUsage `json:"usage"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
LastHeartbeat time.Time `json:"last_heartbeat"`
|
||||
Containers []string `json:"containers"`
|
||||
}
|
||||
|
||||
type ResourceCapacity struct {
|
||||
CPU int64 `json:"cpu"` // CPU cores in nanoseconds
|
||||
Memory int64 `json:"memory"` // Memory in bytes
|
||||
Storage int64 `json:"storage"` // Storage in bytes
|
||||
Network int64 `json:"network"` // Network bandwidth in bytes per second
|
||||
}
|
||||
|
||||
type NodeResourceUsage struct {
|
||||
CPU float64 `json:"cpu"` // CPU usage percentage
|
||||
Memory int64 `json:"memory"` // Memory usage in bytes
|
||||
Storage int64 `json:"storage"` // Storage usage in bytes
|
||||
Network int64 `json:"network"` // Network usage in bytes per second
|
||||
}
|
||||
|
||||
type SchedulingAlgorithm string
|
||||
|
||||
const (
|
||||
SchedulingAlgorithmRoundRobin SchedulingAlgorithm = "round_robin"
|
||||
SchedulingAlgorithmLeastLoaded SchedulingAlgorithm = "least_loaded"
|
||||
SchedulingAlgorithmBestFit SchedulingAlgorithm = "best_fit"
|
||||
SchedulingAlgorithmRandom SchedulingAlgorithm = "random"
|
||||
)
|
||||
|
||||
type SchedulingDecision struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Reason string `json:"reason"`
|
||||
Score float64 `json:"score"`
|
||||
Alternatives []NodeScore `json:"alternatives"`
|
||||
}
|
||||
|
||||
type NodeScore struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Score float64 `json:"score"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func NewScheduler() *Scheduler {
|
||||
return &Scheduler{
|
||||
nodes: make(map[string]*Node),
|
||||
schedulingAlg: SchedulingAlgorithmLeastLoaded,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterNode registers a new node in the scheduler
|
||||
func (s *Scheduler) RegisterNode(node *Node) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.nodes[node.ID]; exists {
|
||||
return fmt.Errorf("node already registered: %s", node.ID)
|
||||
}
|
||||
|
||||
node.Status = "ready"
|
||||
node.LastHeartbeat = time.Now()
|
||||
s.nodes[node.ID] = node
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnregisterNode removes a node from the scheduler
|
||||
func (s *Scheduler) UnregisterNode(nodeID string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.nodes[nodeID]; !exists {
|
||||
return fmt.Errorf("node not found: %s", nodeID)
|
||||
}
|
||||
|
||||
delete(s.nodes, nodeID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateNode updates node information
|
||||
func (s *Scheduler) UpdateNode(node *Node) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.nodes[node.ID]; !exists {
|
||||
return fmt.Errorf("node not found: %s", node.ID)
|
||||
}
|
||||
|
||||
node.LastHeartbeat = time.Now()
|
||||
s.nodes[node.ID] = node
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNodes returns all registered nodes
|
||||
func (s *Scheduler) GetNodes() []*Node {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
nodes := make([]*Node, 0, len(s.nodes))
|
||||
for _, node := range s.nodes {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// GetReadyNodes returns only nodes that are ready for scheduling
|
||||
func (s *Scheduler) GetReadyNodes() []*Node {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
nodes := make([]*Node, 0, len(s.nodes))
|
||||
for _, node := range s.nodes {
|
||||
if node.Status == "ready" && s.isNodeHealthy(node) {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// ScheduleContainer schedules a container to run on the best available node
|
||||
func (s *Scheduler) ScheduleContainer(ctx context.Context, requirements ResourceCapacity) (*SchedulingDecision, error) {
|
||||
readyNodes := s.GetReadyNodes()
|
||||
if len(readyNodes) == 0 {
|
||||
return nil, fmt.Errorf("no ready nodes available")
|
||||
}
|
||||
|
||||
var decision *SchedulingDecision
|
||||
|
||||
switch s.schedulingAlg {
|
||||
case SchedulingAlgorithmRoundRobin:
|
||||
decision = s.scheduleRoundRobin(readyNodes, requirements)
|
||||
case SchedulingAlgorithmLeastLoaded:
|
||||
decision = s.scheduleLeastLoaded(readyNodes, requirements)
|
||||
case SchedulingAlgorithmBestFit:
|
||||
decision = s.scheduleBestFit(readyNodes, requirements)
|
||||
case SchedulingAlgorithmRandom:
|
||||
decision = s.scheduleRandom(readyNodes, requirements)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown scheduling algorithm: %s", s.schedulingAlg)
|
||||
}
|
||||
|
||||
if decision == nil {
|
||||
return nil, fmt.Errorf("failed to schedule container")
|
||||
}
|
||||
|
||||
return decision, nil
|
||||
}
|
||||
|
||||
// scheduleRoundRobin schedules containers in a round-robin fashion
|
||||
func (s *Scheduler) scheduleRoundRobin(nodes []*Node, requirements ResourceCapacity) *SchedulingDecision {
|
||||
// Find the node with the fewest containers
|
||||
var selectedNode *Node
|
||||
minContainers := int(^uint(0) >> 1) // Max int
|
||||
|
||||
for _, node := range nodes {
|
||||
if len(node.Containers) < minContainers && s.canFitRequirements(node, requirements) {
|
||||
selectedNode = node
|
||||
minContainers = len(node.Containers)
|
||||
}
|
||||
}
|
||||
|
||||
if selectedNode == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &SchedulingDecision{
|
||||
NodeID: selectedNode.ID,
|
||||
Reason: "Round-robin scheduling",
|
||||
Score: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
// scheduleLeastLoaded schedules containers on the least loaded node
|
||||
func (s *Scheduler) scheduleLeastLoaded(nodes []*Node, requirements ResourceCapacity) *SchedulingDecision {
|
||||
var scores []NodeScore
|
||||
|
||||
for _, node := range nodes {
|
||||
if !s.canFitRequirements(node, requirements) {
|
||||
continue
|
||||
}
|
||||
|
||||
score := s.calculateLoadScore(node)
|
||||
scores = append(scores, NodeScore{
|
||||
NodeID: node.ID,
|
||||
Score: score,
|
||||
Reason: "Load-based score",
|
||||
})
|
||||
}
|
||||
|
||||
if len(scores) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort by score (highest first)
|
||||
sort.Slice(scores, func(i, j int) bool {
|
||||
return scores[i].Score > scores[j].Score
|
||||
})
|
||||
|
||||
selected := scores[0]
|
||||
|
||||
return &SchedulingDecision{
|
||||
NodeID: selected.NodeID,
|
||||
Reason: selected.Reason,
|
||||
Score: selected.Score,
|
||||
Alternatives: scores[1:],
|
||||
}
|
||||
}
|
||||
|
||||
// scheduleBestFit schedules containers on the node with the best resource fit
|
||||
func (s *Scheduler) scheduleBestFit(nodes []*Node, requirements ResourceCapacity) *SchedulingDecision {
|
||||
var scores []NodeScore
|
||||
|
||||
for _, node := range nodes {
|
||||
if !s.canFitRequirements(node, requirements) {
|
||||
continue
|
||||
}
|
||||
|
||||
score := s.calculateFitScore(node, requirements)
|
||||
scores = append(scores, NodeScore{
|
||||
NodeID: node.ID,
|
||||
Score: score,
|
||||
Reason: "Best-fit score",
|
||||
})
|
||||
}
|
||||
|
||||
if len(scores) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort by score (highest first)
|
||||
sort.Slice(scores, func(i, j int) bool {
|
||||
return scores[i].Score > scores[j].Score
|
||||
})
|
||||
|
||||
selected := scores[0]
|
||||
|
||||
return &SchedulingDecision{
|
||||
NodeID: selected.NodeID,
|
||||
Reason: selected.Reason,
|
||||
Score: selected.Score,
|
||||
Alternatives: scores[1:],
|
||||
}
|
||||
}
|
||||
|
||||
// scheduleRandom schedules containers on a random available node
|
||||
func (s *Scheduler) scheduleRandom(nodes []*Node, requirements ResourceCapacity) *SchedulingDecision {
|
||||
var availableNodes []*Node
|
||||
|
||||
for _, node := range nodes {
|
||||
if s.canFitRequirements(node, requirements) {
|
||||
availableNodes = append(availableNodes, node)
|
||||
}
|
||||
}
|
||||
|
||||
if len(availableNodes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Simple random selection (in production, use proper random)
|
||||
selectedNode := availableNodes[0] // For simplicity, just pick the first one
|
||||
|
||||
return &SchedulingDecision{
|
||||
NodeID: selectedNode.ID,
|
||||
Reason: "Random selection",
|
||||
Score: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
// canFitRequirements checks if a node can accommodate the resource requirements
|
||||
func (s *Scheduler) canFitRequirements(node *Node, requirements ResourceCapacity) bool {
|
||||
availableCPU := node.Capacity.CPU - int64(node.Usage.CPU*float64(node.Capacity.CPU)/100)
|
||||
availableMemory := node.Capacity.Memory - node.Usage.Memory
|
||||
|
||||
return availableCPU >= requirements.CPU && availableMemory >= requirements.Memory
|
||||
}
|
||||
|
||||
// calculateLoadScore calculates a score based on node load
|
||||
func (s *Scheduler) calculateLoadScore(node *Node) float64 {
|
||||
// Lower load = higher score
|
||||
cpuLoad := node.Usage.CPU / 100.0
|
||||
memoryLoad := float64(node.Usage.Memory) / float64(node.Capacity.Memory)
|
||||
containerLoad := float64(len(node.Containers)) / 10.0 // Assume max 10 containers
|
||||
|
||||
// Combined load score (0-1, where 0 is no load and 1 is full load)
|
||||
combinedLoad := (cpuLoad + memoryLoad + containerLoad) / 3.0
|
||||
|
||||
// Convert to score where higher is better (1 - load)
|
||||
return 1.0 - combinedLoad
|
||||
}
|
||||
|
||||
// calculateFitScore calculates how well the requirements fit the node
|
||||
func (s *Scheduler) calculateFitScore(node *Node, requirements ResourceCapacity) float64 {
|
||||
availableCPU := node.Capacity.CPU - int64(node.Usage.CPU*float64(node.Capacity.CPU)/100)
|
||||
availableMemory := node.Capacity.Memory - node.Usage.Memory
|
||||
|
||||
// Calculate utilization after placing this container
|
||||
newCPUUtilization := float64(node.Capacity.CPU-availableCPU+requirements.CPU) / float64(node.Capacity.CPU)
|
||||
newMemoryUtilization := float64(node.Capacity.Memory-availableMemory+requirements.Memory) / float64(node.Capacity.Memory)
|
||||
|
||||
// Prefer moderate utilization (not too low, not too high)
|
||||
cpuScore := 1.0 - abs(newCPUUtilization-0.7)
|
||||
memoryScore := 1.0 - abs(newMemoryUtilization-0.7)
|
||||
|
||||
return (cpuScore + memoryScore) / 2.0
|
||||
}
|
||||
|
||||
// isNodeHealthy checks if a node is healthy based on heartbeat
|
||||
func (s *Scheduler) isNodeHealthy(node *Node) bool {
|
||||
return time.Since(node.LastHeartbeat) < 30*time.Second
|
||||
}
|
||||
|
||||
// abs returns the absolute value of a float64
|
||||
func abs(x float64) float64 {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// SetSchedulingAlgorithm sets the scheduling algorithm
|
||||
func (s *Scheduler) SetSchedulingAlgorithm(alg SchedulingAlgorithm) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.schedulingAlg = alg
|
||||
}
|
||||
|
||||
// GetNodeStats returns statistics about nodes
|
||||
func (s *Scheduler) GetNodeStats() map[string]interface{} {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
totalNodes := len(s.nodes)
|
||||
readyNodes := 0
|
||||
unhealthyNodes := 0
|
||||
|
||||
for _, node := range s.nodes {
|
||||
if node.Status == "ready" {
|
||||
if s.isNodeHealthy(node) {
|
||||
readyNodes++
|
||||
} else {
|
||||
unhealthyNodes++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_nodes": totalNodes,
|
||||
"ready_nodes": readyNodes,
|
||||
"unhealthy_nodes": unhealthyNodes,
|
||||
"scheduling_alg": string(s.schedulingAlg),
|
||||
}
|
||||
}
|
||||
@@ -1,363 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// Client wraps the Docker client with additional functionality
|
||||
type Client struct {
|
||||
cli *client.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new Docker client
|
||||
func NewClient() (*Client, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Docker client: %w", err)
|
||||
}
|
||||
|
||||
// Test connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err = cli.Ping(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Docker daemon: %w", err)
|
||||
}
|
||||
|
||||
return &Client{cli: cli}, nil
|
||||
}
|
||||
|
||||
// ListContainers returns all containers
|
||||
func (c *Client) ListContainers(ctx context.Context, all bool) ([]types.Container, error) {
|
||||
return c.cli.ContainerList(ctx, container.ListOptions{
|
||||
All: all,
|
||||
})
|
||||
}
|
||||
|
||||
// GetContainer returns detailed information about a specific container
|
||||
func (c *Client) GetContainer(ctx context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
return c.cli.ContainerInspect(ctx, containerID)
|
||||
}
|
||||
|
||||
// CreateContainer creates a new container
|
||||
func (c *Client) CreateContainer(ctx context.Context, config ContainerConfig) (string, error) {
|
||||
containerConfig := &container.Config{
|
||||
Image: config.Image,
|
||||
Cmd: config.Cmd,
|
||||
Env: config.Env,
|
||||
Labels: config.Labels,
|
||||
}
|
||||
|
||||
hostConfig := &container.HostConfig{
|
||||
RestartPolicy: container.RestartPolicy{
|
||||
Name: container.RestartPolicyMode(config.RestartPolicy),
|
||||
},
|
||||
PortBindings: config.PortBindings,
|
||||
Mounts: config.Mounts,
|
||||
Resources: container.Resources{
|
||||
Memory: config.Memory,
|
||||
NanoCPUs: config.NanoCPUs,
|
||||
},
|
||||
NetworkMode: container.NetworkMode(config.NetworkMode),
|
||||
}
|
||||
|
||||
networkingConfig := &network.NetworkingConfig{
|
||||
EndpointsConfig: config.Networks,
|
||||
}
|
||||
|
||||
resp, err := c.cli.ContainerCreate(
|
||||
ctx,
|
||||
containerConfig,
|
||||
hostConfig,
|
||||
networkingConfig,
|
||||
nil,
|
||||
config.Name,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create container: %w", err)
|
||||
}
|
||||
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
// StartContainer starts a container
|
||||
func (c *Client) StartContainer(ctx context.Context, containerID string) error {
|
||||
return c.cli.ContainerStart(ctx, containerID, container.StartOptions{})
|
||||
}
|
||||
|
||||
// StopContainer stops a container
|
||||
func (c *Client) StopContainer(ctx context.Context, containerID string, timeout *time.Duration) error {
|
||||
var timeoutInt *int
|
||||
if timeout != nil {
|
||||
t := int(timeout.Seconds())
|
||||
timeoutInt = &t
|
||||
}
|
||||
return c.cli.ContainerStop(ctx, containerID, container.StopOptions{
|
||||
Timeout: timeoutInt,
|
||||
})
|
||||
}
|
||||
|
||||
// RestartContainer restarts a container
|
||||
func (c *Client) RestartContainer(ctx context.Context, containerID string, timeout *time.Duration) error {
|
||||
var timeoutInt *int
|
||||
if timeout != nil {
|
||||
t := int(timeout.Seconds())
|
||||
timeoutInt = &t
|
||||
}
|
||||
return c.cli.ContainerRestart(ctx, containerID, container.StopOptions{
|
||||
Timeout: timeoutInt,
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveContainer removes a container
|
||||
func (c *Client) RemoveContainer(ctx context.Context, containerID string, force bool) error {
|
||||
return c.cli.ContainerRemove(ctx, containerID, container.RemoveOptions{
|
||||
Force: force,
|
||||
})
|
||||
}
|
||||
|
||||
// GetContainerLogs returns logs for a container
|
||||
func (c *Client) GetContainerLogs(ctx context.Context, containerID string, options LogOptions) (io.ReadCloser, error) {
|
||||
return c.cli.ContainerLogs(ctx, containerID, container.LogsOptions{
|
||||
ShowStdout: options.Stdout,
|
||||
ShowStderr: options.Stderr,
|
||||
Follow: options.Follow,
|
||||
Tail: options.Tail,
|
||||
Timestamps: options.Timestamps,
|
||||
})
|
||||
}
|
||||
|
||||
// GetContainerStats returns real-time resource usage statistics for a container
|
||||
func (c *Client) GetContainerStats(ctx context.Context, containerID string, stream bool) (*container.StatsResponseReader, error) {
|
||||
resp, err := c.cli.ContainerStats(ctx, containerID, stream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// ListImages returns all images
|
||||
func (c *Client) ListImages(ctx context.Context, all bool) ([]image.Summary, error) {
|
||||
return c.cli.ImageList(ctx, image.ListOptions{
|
||||
All: all,
|
||||
})
|
||||
}
|
||||
|
||||
// PullImage pulls an image from a registry
|
||||
func (c *Client) PullImage(ctx context.Context, ref string, auth registry.AuthConfig) (io.ReadCloser, error) {
|
||||
authStr, _ := registry.EncodeAuthConfig(auth)
|
||||
return c.cli.ImagePull(ctx, ref, image.PullOptions{
|
||||
RegistryAuth: authStr,
|
||||
})
|
||||
}
|
||||
|
||||
// BuildImage builds an image from a Dockerfile
|
||||
func (c *Client) BuildImage(ctx context.Context, buildContext io.Reader, options BuildOptions) (types.ImageBuildResponse, error) {
|
||||
return c.cli.ImageBuild(ctx, buildContext, types.ImageBuildOptions{
|
||||
Dockerfile: options.Dockerfile,
|
||||
Tags: options.Tags,
|
||||
BuildArgs: options.BuildArgs,
|
||||
Labels: options.Labels,
|
||||
Remove: options.Remove,
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveImage removes an image
|
||||
func (c *Client) RemoveImage(ctx context.Context, imageID string, force bool) ([]image.DeleteResponse, error) {
|
||||
return c.cli.ImageRemove(ctx, imageID, image.RemoveOptions{
|
||||
Force: force,
|
||||
})
|
||||
}
|
||||
|
||||
// TagImage tags an image
|
||||
func (c *Client) TagImage(ctx context.Context, imageID, ref string) error {
|
||||
return c.cli.ImageTag(ctx, imageID, ref)
|
||||
}
|
||||
|
||||
// ListNetworks returns all networks
|
||||
func (c *Client) ListNetworks(ctx context.Context) ([]network.Summary, error) {
|
||||
return c.cli.NetworkList(ctx, network.ListOptions{})
|
||||
}
|
||||
|
||||
// CreateNetwork creates a new network
|
||||
func (c *Client) CreateNetwork(ctx context.Context, config NetworkConfig) (string, error) {
|
||||
resp, err := c.cli.NetworkCreate(ctx, config.Name, network.CreateOptions{
|
||||
Driver: config.Driver,
|
||||
Internal: config.Internal,
|
||||
Labels: config.Labels,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
// RemoveNetwork removes a network
|
||||
func (c *Client) RemoveNetwork(ctx context.Context, networkID string) error {
|
||||
return c.cli.NetworkRemove(ctx, networkID)
|
||||
}
|
||||
|
||||
// ConnectNetwork connects a container to a network
|
||||
func (c *Client) ConnectNetwork(ctx context.Context, networkID, containerID string, config network.EndpointSettings) error {
|
||||
return c.cli.NetworkConnect(ctx, networkID, containerID, &config)
|
||||
}
|
||||
|
||||
// DisconnectNetwork disconnects a container from a network
|
||||
func (c *Client) DisconnectNetwork(ctx context.Context, networkID, containerID string, force bool) error {
|
||||
return c.cli.NetworkDisconnect(ctx, networkID, containerID, force)
|
||||
}
|
||||
|
||||
// ListVolumes returns all volumes
|
||||
func (c *Client) ListVolumes(ctx context.Context) (volume.ListResponse, error) {
|
||||
return c.cli.VolumeList(ctx, volume.ListOptions{})
|
||||
}
|
||||
|
||||
// CreateVolume creates a new volume
|
||||
func (c *Client) CreateVolume(ctx context.Context, config VolumeConfig) (volume.Volume, error) {
|
||||
return c.cli.VolumeCreate(ctx, volume.CreateOptions{
|
||||
Name: config.Name,
|
||||
Driver: config.Driver,
|
||||
Labels: config.Labels,
|
||||
DriverOpts: config.DriverOpts,
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveVolume removes a volume
|
||||
func (c *Client) RemoveVolume(ctx context.Context, volumeID string, force bool) error {
|
||||
return c.cli.VolumeRemove(ctx, volumeID, force)
|
||||
}
|
||||
|
||||
// GetSystemInfo returns system-wide information
|
||||
func (c *Client) GetSystemInfo(ctx context.Context) (system.Info, error) {
|
||||
return c.cli.Info(ctx)
|
||||
}
|
||||
|
||||
// GetDiskUsage returns Docker disk usage information
|
||||
func (c *Client) GetDiskUsage(ctx context.Context) (types.DiskUsage, error) {
|
||||
return c.cli.DiskUsage(ctx, types.DiskUsageOptions{})
|
||||
}
|
||||
|
||||
// GetEvents returns Docker events
|
||||
func (c *Client) GetEvents(ctx context.Context, options EventOptions) (io.ReadCloser, error) {
|
||||
resp, errChan := c.cli.Events(ctx, events.ListOptions{
|
||||
Since: options.Since,
|
||||
Until: options.Until,
|
||||
Filters: options.Filters,
|
||||
})
|
||||
|
||||
// Convert the channel to a reader
|
||||
r, w := io.Pipe()
|
||||
go func() {
|
||||
defer w.Close()
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-resp:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// Write event data to pipe
|
||||
w.Write([]byte(fmt.Sprintf("%v\n", event)))
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
w.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ExecCreate creates an exec instance in a container
|
||||
func (c *Client) ExecCreate(ctx context.Context, containerID string, config ExecConfig) (types.IDResponse, error) {
|
||||
return c.cli.ContainerExecCreate(ctx, containerID, container.ExecOptions{
|
||||
Cmd: config.Cmd,
|
||||
Env: config.Env,
|
||||
WorkingDir: config.WorkingDir,
|
||||
User: config.User,
|
||||
AttachStdin: config.AttachStdin,
|
||||
AttachStdout: config.AttachStdout,
|
||||
AttachStderr: config.AttachStderr,
|
||||
Tty: config.Tty,
|
||||
})
|
||||
}
|
||||
|
||||
// ExecStart starts an exec instance
|
||||
func (c *Client) ExecStart(ctx context.Context, execID string, config ExecStartConfig) error {
|
||||
return c.cli.ContainerExecStart(ctx, execID, container.ExecStartOptions{
|
||||
Detach: config.Detach,
|
||||
Tty: config.Tty,
|
||||
})
|
||||
}
|
||||
|
||||
// ExecInspect returns information about an exec instance
|
||||
func (c *Client) ExecInspect(ctx context.Context, execID string) (container.ExecInspect, error) {
|
||||
return c.cli.ContainerExecInspect(ctx, execID)
|
||||
}
|
||||
|
||||
// GetImageInfo returns information about a Docker image
|
||||
func (c *Client) GetImageInfo(ctx context.Context, imageName string) (*ImageInfo, error) {
|
||||
images, err := c.cli.ImageList(ctx, image.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, img := range images {
|
||||
for _, tag := range img.RepoTags {
|
||||
if tag == imageName || tag == imageName+":latest" {
|
||||
return &ImageInfo{
|
||||
ID: img.ID,
|
||||
RepoTags: img.RepoTags,
|
||||
Size: img.Size,
|
||||
Created: img.Created,
|
||||
Labels: img.Labels,
|
||||
RepoDigests: img.RepoDigests,
|
||||
Digest: getDigestFromRepoTags(img.RepoDigests),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("image not found: %s", imageName)
|
||||
}
|
||||
|
||||
// PushImage pushes an image to a registry
|
||||
func (c *Client) PushImage(ctx context.Context, imageName, registryURL string) error {
|
||||
auth := registry.AuthConfig{}
|
||||
authStr, _ := registry.EncodeAuthConfig(auth)
|
||||
|
||||
_, err := c.cli.ImagePush(ctx, imageName, image.PushOptions{
|
||||
RegistryAuth: authStr,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Close closes the Docker client connection
|
||||
func (c *Client) Close() error {
|
||||
return c.cli.Close()
|
||||
}
|
||||
|
||||
// Helper function to extract digest from repo digests
|
||||
func getDigestFromRepoTags(digests []string) string {
|
||||
if len(digests) > 0 {
|
||||
return digests[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
// ContainerConfig represents the configuration for creating a container
|
||||
type ContainerConfig struct {
|
||||
Name string
|
||||
Image string
|
||||
Cmd []string
|
||||
Env []string
|
||||
Labels map[string]string
|
||||
RestartPolicy string
|
||||
PortBindings nat.PortMap
|
||||
Mounts []mount.Mount
|
||||
Memory int64
|
||||
NanoCPUs int64
|
||||
NetworkMode string
|
||||
Networks map[string]*network.EndpointSettings
|
||||
}
|
||||
|
||||
// LogOptions represents options for retrieving container logs
|
||||
type LogOptions struct {
|
||||
Stdout bool
|
||||
Stderr bool
|
||||
Follow bool
|
||||
Tail string
|
||||
Timestamps bool
|
||||
}
|
||||
|
||||
// BuildOptions represents options for building an image
|
||||
type BuildOptions struct {
|
||||
Dockerfile string
|
||||
Tags []string
|
||||
BuildArgs map[string]*string
|
||||
Labels map[string]string
|
||||
Remove bool
|
||||
}
|
||||
|
||||
// NetworkConfig represents the configuration for creating a network
|
||||
type NetworkConfig struct {
|
||||
Name string
|
||||
CheckDuplicate bool
|
||||
Driver string
|
||||
Internal bool
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
// VolumeConfig represents the configuration for creating a volume
|
||||
type VolumeConfig struct {
|
||||
Name string
|
||||
Driver string
|
||||
Labels map[string]string
|
||||
DriverOpts map[string]string
|
||||
}
|
||||
|
||||
// EventOptions represents options for filtering Docker events
|
||||
type EventOptions struct {
|
||||
Since string
|
||||
Until string
|
||||
Filters filters.Args
|
||||
}
|
||||
|
||||
// ExecConfig represents the configuration for creating an exec instance
|
||||
type ExecConfig struct {
|
||||
Cmd []string
|
||||
Env []string
|
||||
WorkingDir string
|
||||
User string
|
||||
AttachStdin bool
|
||||
AttachStdout bool
|
||||
AttachStderr bool
|
||||
Tty bool
|
||||
}
|
||||
|
||||
// ExecStartConfig represents the configuration for starting an exec instance
|
||||
type ExecStartConfig struct {
|
||||
Detach bool
|
||||
Tty bool
|
||||
}
|
||||
|
||||
// ServiceConfig represents a service configuration for deployment
|
||||
type ServiceConfig struct {
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
Command []string `json:"command,omitempty"`
|
||||
Environment map[string]string `json:"environment,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
RestartPolicy string `json:"restart_policy"`
|
||||
PortMappings []PortMapping `json:"port_mappings,omitempty"`
|
||||
VolumeMounts []VolumeMount `json:"volume_mounts,omitempty"`
|
||||
Networks []string `json:"networks,omitempty"`
|
||||
Resources ResourceLimits `json:"resources,omitempty"`
|
||||
HealthCheck *HealthCheck `json:"health_check,omitempty"`
|
||||
}
|
||||
|
||||
// PortMapping represents a port mapping configuration
|
||||
type PortMapping struct {
|
||||
ContainerPort int32 `json:"container_port"`
|
||||
HostPort int32 `json:"host_port,omitempty"`
|
||||
Protocol string `json:"protocol"` // tcp or udp
|
||||
HostIP string `json:"host_ip,omitempty"`
|
||||
}
|
||||
|
||||
// VolumeMount represents a volume mount configuration
|
||||
type VolumeMount struct {
|
||||
Type string `json:"type"` // bind, volume, tmpfs
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
ReadOnly bool `json:"read_only,omitempty"`
|
||||
Consistency string `json:"consistency,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceLimits represents resource limits for a container
|
||||
type ResourceLimits struct {
|
||||
MemoryBytes int64 `json:"memory_bytes,omitempty"`
|
||||
CPUQuota int64 `json:"cpu_quota,omitempty"`
|
||||
CPUPeriod int64 `json:"cpu_period,omitempty"`
|
||||
CPUShares int64 `json:"cpu_shares,omitempty"`
|
||||
}
|
||||
|
||||
// HealthCheck represents a health check configuration
|
||||
type HealthCheck struct {
|
||||
Test []string `json:"test"`
|
||||
Interval time.Duration `json:"interval"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
Retries int `json:"retries"`
|
||||
StartPeriod time.Duration `json:"start_period"`
|
||||
}
|
||||
|
||||
// ServiceStatus represents the status of a service
|
||||
type ServiceStatus struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
Ports []PortInfo `json:"ports,omitempty"`
|
||||
Networks []NetworkInfo `json:"networks,omitempty"`
|
||||
Mounts []MountInfo `json:"mounts,omitempty"`
|
||||
Resources ResourceUsage `json:"resources"`
|
||||
Health *HealthStatus `json:"health,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
// PortInfo represents port information for a running container
|
||||
type PortInfo struct {
|
||||
ContainerPort int32 `json:"container_port"`
|
||||
HostPort int32 `json:"host_port,omitempty"`
|
||||
HostIP string `json:"host_ip"`
|
||||
Protocol string `json:"protocol"`
|
||||
}
|
||||
|
||||
// NetworkInfo represents network information for a container
|
||||
type NetworkInfo struct {
|
||||
Name string `json:"name"`
|
||||
NetworkID string `json:"network_id"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
MACAddress string `json:"mac_address,omitempty"`
|
||||
}
|
||||
|
||||
// MountInfo represents mount information for a container
|
||||
type MountInfo struct {
|
||||
Type string `json:"type"`
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}
|
||||
|
||||
// ResourceUsage represents resource usage for a container
|
||||
type ResourceUsage struct {
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
MemoryUsage int64 `json:"memory_usage"`
|
||||
MemoryLimit int64 `json:"memory_limit"`
|
||||
NetworkRx int64 `json:"network_rx"`
|
||||
NetworkTx int64 `json:"network_tx"`
|
||||
BlockRead int64 `json:"block_read"`
|
||||
BlockWrite int64 `json:"block_write"`
|
||||
PidsCurrent uint64 `json:"pids_current"`
|
||||
PidsLimit uint64 `json:"pids_limit"`
|
||||
}
|
||||
|
||||
// HealthStatus represents the health status of a container
|
||||
type HealthStatus struct {
|
||||
Status string `json:"status"`
|
||||
FailingStreak int `json:"failing_streak"`
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
}
|
||||
|
||||
// RegistryConfig represents Docker registry configuration
|
||||
type RegistryConfig struct {
|
||||
URL string `json:"url"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Auth string `json:"auth,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
// ImageInfo represents information about a Docker image
|
||||
type ImageInfo struct {
|
||||
ID string `json:"id"`
|
||||
RepoTags []string `json:"repo_tags"`
|
||||
Size int64 `json:"size"`
|
||||
Created int64 `json:"created"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
RepoDigests []string `json:"repo_digests"`
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
|
||||
// BuildContext represents a build context for Docker images
|
||||
type BuildContext struct {
|
||||
Dockerfile string `json:"dockerfile"`
|
||||
Context string `json:"context"`
|
||||
Tags []string `json:"tags"`
|
||||
BuildArgs map[string]string `json:"build_args,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Target string `json:"target,omitempty"`
|
||||
NoCache bool `json:"no_cache,omitempty"`
|
||||
Remove bool `json:"remove,omitempty"`
|
||||
ForceRm bool `json:"force_rm,omitempty"`
|
||||
Pull bool `json:"pull,omitempty"`
|
||||
}
|
||||
|
||||
// DeploymentConfig represents a deployment configuration
|
||||
type DeploymentConfig struct {
|
||||
Service ServiceConfig `json:"service"`
|
||||
Replicas int `json:"replicas"`
|
||||
Update UpdateConfig `json:"update,omitempty"`
|
||||
Rollback RollbackConfig `json:"rollback,omitempty"`
|
||||
Networks []NetworkConfig `json:"networks,omitempty"`
|
||||
Volumes []VolumeConfig `json:"volumes,omitempty"`
|
||||
Secrets []SecretConfig `json:"secrets,omitempty"`
|
||||
Configs []ConfigFile `json:"configs,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateConfig represents update configuration for deployments
|
||||
type UpdateConfig struct {
|
||||
Parallelism uint `json:"parallelism"`
|
||||
Delay time.Duration `json:"delay"`
|
||||
FailureAction string `json:"failure_action"`
|
||||
Monitor time.Duration `json:"monitor"`
|
||||
MaxFailureRatio float64 `json:"max_failure_ratio"`
|
||||
Order string `json:"order"`
|
||||
}
|
||||
|
||||
// RollbackConfig represents rollback configuration
|
||||
type RollbackConfig struct {
|
||||
Parallelism uint `json:"parallelism"`
|
||||
Delay time.Duration `json:"delay"`
|
||||
FailureAction string `json:"failure_action"`
|
||||
Monitor time.Duration `json:"monitor"`
|
||||
MaxFailureRatio float64 `json:"max_failure_ratio"`
|
||||
Order string `json:"order"`
|
||||
}
|
||||
|
||||
// SecretConfig represents a secret configuration
|
||||
type SecretConfig struct {
|
||||
Name string `json:"name"`
|
||||
Data string `json:"data"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Driver string `json:"driver,omitempty"`
|
||||
Template string `json:"template,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigFile represents a configuration file
|
||||
type ConfigFile struct {
|
||||
Name string `json:"name"`
|
||||
File string `json:"file"`
|
||||
Content string `json:"content"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Template string `json:"template,omitempty"`
|
||||
}
|
||||
@@ -1,736 +0,0 @@
|
||||
package ha
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"containr/internal/deployment"
|
||||
"containr/internal/metrics"
|
||||
)
|
||||
|
||||
// HighAvailabilityManager manages high availability features
|
||||
type HighAvailabilityManager struct {
|
||||
scheduler *deployment.Scheduler
|
||||
metricsCollector *metrics.MetricsCollector
|
||||
failoverManager *FailoverManager
|
||||
healthChecker *HealthChecker
|
||||
alertManager *AlertManager
|
||||
mu sync.RWMutex
|
||||
enabled bool
|
||||
checkInterval time.Duration
|
||||
failoverThreshold int
|
||||
}
|
||||
|
||||
// FailoverManager handles service failover operations
|
||||
type FailoverManager struct {
|
||||
scheduler *deployment.Scheduler
|
||||
failoverPolicies map[string]*FailoverPolicy
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// FailoverPolicy defines failover behavior for a service
|
||||
type FailoverPolicy struct {
|
||||
ServiceID string `json:"service_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
MinHealthyNodes int `json:"min_healthy_nodes"`
|
||||
MaxFailures int `json:"max_failures"`
|
||||
FailoverTimeout time.Duration `json:"failover_timeout"`
|
||||
RecoveryTimeout time.Duration `json:"recovery_timeout"`
|
||||
FailoverStrategy FailoverStrategy `json:"failover_strategy"`
|
||||
BackupNodes []string `json:"backup_nodes"`
|
||||
HealthCheckConfig *HealthCheckConfig `json:"health_check_config"`
|
||||
}
|
||||
|
||||
// FailoverStrategy defines how failover is performed
|
||||
type FailoverStrategy string
|
||||
|
||||
const (
|
||||
FailoverStrategyActivePassive FailoverStrategy = "active_passive"
|
||||
FailoverStrategyActiveActive FailoverStrategy = "active_active"
|
||||
FailoverStrategyGraceful FailoverStrategy = "graceful"
|
||||
)
|
||||
|
||||
// HealthCheckConfig defines health check parameters
|
||||
type HealthCheckConfig struct {
|
||||
Interval time.Duration `json:"interval"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
UnhealthyThreshold int `json:"unhealthy_threshold"`
|
||||
HealthyThreshold int `json:"healthy_threshold"`
|
||||
Path string `json:"path"`
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"protocol"`
|
||||
}
|
||||
|
||||
// HealthChecker performs health checks on services and nodes
|
||||
type HealthChecker struct {
|
||||
scheduler *deployment.Scheduler
|
||||
checks map[string]*HealthCheck
|
||||
results map[string]*HealthCheckResult
|
||||
mu sync.RWMutex
|
||||
checkInterval time.Duration
|
||||
}
|
||||
|
||||
// HealthCheck represents a health check configuration
|
||||
type HealthCheck struct {
|
||||
ID string `json:"id"`
|
||||
ServiceID string `json:"service_id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Type HealthCheckType `json:"type"`
|
||||
Config HealthCheckConfig `json:"config"`
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
Status HealthStatus `json:"status"`
|
||||
}
|
||||
|
||||
// HealthCheckType represents the type of health check
|
||||
type HealthCheckType string
|
||||
|
||||
const (
|
||||
HealthCheckTypeHTTP HealthCheckType = "http"
|
||||
HealthCheckTypeTCP HealthCheckType = "tcp"
|
||||
HealthCheckTypeCommand HealthCheckType = "command"
|
||||
)
|
||||
|
||||
// HealthStatus represents the health status
|
||||
type HealthStatus string
|
||||
|
||||
const (
|
||||
HealthStatusHealthy HealthStatus = "healthy"
|
||||
HealthStatusUnhealthy HealthStatus = "unhealthy"
|
||||
HealthStatusUnknown HealthStatus = "unknown"
|
||||
)
|
||||
|
||||
// HealthCheckResult represents the result of a health check
|
||||
type HealthCheckResult struct {
|
||||
CheckID string `json:"check_id"`
|
||||
Status HealthStatus `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Latency time.Duration `json:"latency"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
ErrorCode string `json:"error_code,omitempty"`
|
||||
}
|
||||
|
||||
// AlertManager handles alerting and notifications
|
||||
type AlertManager struct {
|
||||
rules map[string]*AlertRule
|
||||
activeAlerts map[string]*Alert
|
||||
notifiers map[string]Notifier
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// AlertRule defines when alerts should be triggered
|
||||
type AlertRule struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Condition AlertCondition `json:"condition"`
|
||||
Severity AlertSeverity `json:"severity"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
Notifiers []string `json:"notifiers"`
|
||||
Cooldown time.Duration `json:"cooldown"`
|
||||
}
|
||||
|
||||
// AlertCondition defines the condition for triggering an alert
|
||||
type AlertCondition struct {
|
||||
Metric string `json:"metric"`
|
||||
Operator string `json:"operator"` // >, <, >=, <=, ==, !=
|
||||
Threshold float64 `json:"threshold"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
}
|
||||
|
||||
// AlertSeverity represents the severity level of an alert
|
||||
type AlertSeverity string
|
||||
|
||||
const (
|
||||
AlertSeverityCritical AlertSeverity = "critical"
|
||||
AlertSeverityWarning AlertSeverity = "warning"
|
||||
AlertSeverityInfo AlertSeverity = "info"
|
||||
)
|
||||
|
||||
// Alert represents an active alert
|
||||
type Alert struct {
|
||||
ID string `json:"id"`
|
||||
RuleID string `json:"rule_id"`
|
||||
Status AlertStatus `json:"status"`
|
||||
Severity AlertSeverity `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
StartsAt time.Time `json:"starts_at"`
|
||||
EndsAt *time.Time `json:"ends_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AlertStatus represents the status of an alert
|
||||
type AlertStatus string
|
||||
|
||||
const (
|
||||
AlertStatusFiring AlertStatus = "firing"
|
||||
AlertStatusResolved AlertStatus = "resolved"
|
||||
)
|
||||
|
||||
// Notifier sends alert notifications
|
||||
type Notifier interface {
|
||||
Send(ctx context.Context, alert *Alert) error
|
||||
Type() string
|
||||
}
|
||||
|
||||
// NewHighAvailabilityManager creates a new HA manager
|
||||
func NewHighAvailabilityManager(scheduler *deployment.Scheduler, metricsCollector *metrics.MetricsCollector) *HighAvailabilityManager {
|
||||
failoverManager := &FailoverManager{
|
||||
scheduler: scheduler,
|
||||
failoverPolicies: make(map[string]*FailoverPolicy),
|
||||
}
|
||||
|
||||
healthChecker := &HealthChecker{
|
||||
scheduler: scheduler,
|
||||
checks: make(map[string]*HealthCheck),
|
||||
results: make(map[string]*HealthCheckResult),
|
||||
checkInterval: 30 * time.Second,
|
||||
}
|
||||
|
||||
alertManager := &AlertManager{
|
||||
rules: make(map[string]*AlertRule),
|
||||
activeAlerts: make(map[string]*Alert),
|
||||
notifiers: make(map[string]Notifier),
|
||||
}
|
||||
|
||||
return &HighAvailabilityManager{
|
||||
scheduler: scheduler,
|
||||
metricsCollector: metricsCollector,
|
||||
failoverManager: failoverManager,
|
||||
healthChecker: healthChecker,
|
||||
alertManager: alertManager,
|
||||
enabled: true,
|
||||
checkInterval: 30 * time.Second,
|
||||
failoverThreshold: 3,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the HA management process
|
||||
func (ha *HighAvailabilityManager) Start(ctx context.Context) error {
|
||||
ticker := time.NewTicker(ha.checkInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Printf("HighAvailabilityManager started with check interval: %v", ha.checkInterval)
|
||||
|
||||
// Start health checker
|
||||
go ha.healthChecker.Start(ctx)
|
||||
|
||||
// Start alert manager
|
||||
go ha.alertManager.Start(ctx)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
if ha.enabled {
|
||||
if err := ha.checkHighAvailability(ctx); err != nil {
|
||||
log.Printf("Error during HA check: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkHighAvailability performs HA checks and takes action if needed
|
||||
func (ha *HighAvailabilityManager) checkHighAvailability(ctx context.Context) error {
|
||||
// Check node health
|
||||
nodes := ha.scheduler.GetNodes()
|
||||
unhealthyNodes := 0
|
||||
|
||||
for _, node := range nodes {
|
||||
if !ha.isNodeHealthy(node) {
|
||||
unhealthyNodes++
|
||||
log.Printf("Node %s is unhealthy", node.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger failover if too many nodes are unhealthy
|
||||
if unhealthyNodes >= ha.failoverThreshold {
|
||||
log.Printf("Failover threshold reached: %d unhealthy nodes", unhealthyNodes)
|
||||
if err := ha.failoverManager.TriggerFailover(ctx, "node_failure"); err != nil {
|
||||
return fmt.Errorf("failed to trigger failover: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isNodeHealthy checks if a node is healthy
|
||||
func (ha *HighAvailabilityManager) isNodeHealthy(node *deployment.Node) bool {
|
||||
// Check if node is ready
|
||||
if node.Status != "ready" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check heartbeat
|
||||
if time.Since(node.LastHeartbeat) > 2*time.Minute {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check resource usage
|
||||
if node.Usage.CPU > 95 || node.Usage.Memory > int64(float64(node.Capacity.Memory)*0.95) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// SetFailoverPolicy sets or updates a failover policy
|
||||
func (ha *HighAvailabilityManager) SetFailoverPolicy(policy *FailoverPolicy) error {
|
||||
ha.mu.Lock()
|
||||
defer ha.mu.Unlock()
|
||||
|
||||
ha.failoverManager.SetFailoverPolicy(policy)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFailoverPolicy returns a failover policy
|
||||
func (ha *HighAvailabilityManager) GetFailoverPolicy(serviceID string) (*FailoverPolicy, error) {
|
||||
ha.mu.RLock()
|
||||
defer ha.mu.RUnlock()
|
||||
|
||||
return ha.failoverManager.GetFailoverPolicy(serviceID)
|
||||
}
|
||||
|
||||
// TriggerFailover manually triggers a failover
|
||||
func (ha *HighAvailabilityManager) TriggerFailover(ctx context.Context, reason string) error {
|
||||
return ha.failoverManager.TriggerFailover(ctx, reason)
|
||||
}
|
||||
|
||||
// GetHealthStatus returns the health status of all services and nodes
|
||||
func (ha *HighAvailabilityManager) GetHealthStatus() map[string]interface{} {
|
||||
ha.mu.RLock()
|
||||
defer ha.mu.RUnlock()
|
||||
|
||||
nodes := ha.scheduler.GetNodes()
|
||||
healthyNodes := 0
|
||||
unhealthyNodes := 0
|
||||
|
||||
for _, node := range nodes {
|
||||
if ha.isNodeHealthy(node) {
|
||||
healthyNodes++
|
||||
} else {
|
||||
unhealthyNodes++
|
||||
}
|
||||
}
|
||||
|
||||
healthChecks := ha.healthChecker.GetAllHealthChecks()
|
||||
healthyChecks := 0
|
||||
unhealthyChecks := 0
|
||||
|
||||
for _, result := range ha.healthChecker.GetAllResults() {
|
||||
if result.Status == HealthStatusHealthy {
|
||||
healthyChecks++
|
||||
} else {
|
||||
unhealthyChecks++
|
||||
}
|
||||
}
|
||||
|
||||
activeAlerts := ha.alertManager.GetActiveAlerts()
|
||||
|
||||
return map[string]interface{}{
|
||||
"nodes": map[string]interface{}{
|
||||
"total": len(nodes),
|
||||
"healthy": healthyNodes,
|
||||
"unhealthy": unhealthyNodes,
|
||||
},
|
||||
"health_checks": map[string]interface{}{
|
||||
"total": len(healthChecks),
|
||||
"healthy": healthyChecks,
|
||||
"unhealthy": unhealthyChecks,
|
||||
},
|
||||
"alerts": map[string]interface{}{
|
||||
"active": len(activeAlerts),
|
||||
},
|
||||
"enabled": ha.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// Enable enables the HA manager
|
||||
func (ha *HighAvailabilityManager) Enable() {
|
||||
ha.mu.Lock()
|
||||
defer ha.mu.Unlock()
|
||||
ha.enabled = true
|
||||
}
|
||||
|
||||
// Disable disables the HA manager
|
||||
func (ha *HighAvailabilityManager) Disable() {
|
||||
ha.mu.Lock()
|
||||
defer ha.mu.Unlock()
|
||||
ha.enabled = false
|
||||
}
|
||||
|
||||
// IsEnabled returns whether the HA manager is enabled
|
||||
func (ha *HighAvailabilityManager) IsEnabled() bool {
|
||||
ha.mu.RLock()
|
||||
defer ha.mu.RUnlock()
|
||||
return ha.enabled
|
||||
}
|
||||
|
||||
// FailoverManager methods
|
||||
|
||||
// SetFailoverPolicy sets a failover policy
|
||||
func (fm *FailoverManager) SetFailoverPolicy(policy *FailoverPolicy) {
|
||||
fm.mu.Lock()
|
||||
defer fm.mu.Unlock()
|
||||
fm.failoverPolicies[policy.ServiceID] = policy
|
||||
}
|
||||
|
||||
// GetFailoverPolicy returns a failover policy
|
||||
func (fm *FailoverManager) GetFailoverPolicy(serviceID string) (*FailoverPolicy, error) {
|
||||
fm.mu.RLock()
|
||||
defer fm.mu.RUnlock()
|
||||
|
||||
policy, exists := fm.failoverPolicies[serviceID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no failover policy found for service: %s", serviceID)
|
||||
}
|
||||
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// TriggerFailover triggers a failover for affected services
|
||||
func (fm *FailoverManager) TriggerFailover(ctx context.Context, reason string) error {
|
||||
fm.mu.RLock()
|
||||
policies := make([]*FailoverPolicy, 0, len(fm.failoverPolicies))
|
||||
for _, policy := range fm.failoverPolicies {
|
||||
if policy.Enabled {
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
}
|
||||
fm.mu.RUnlock()
|
||||
|
||||
for _, policy := range policies {
|
||||
if err := fm.performFailover(ctx, policy, reason); err != nil {
|
||||
log.Printf("Failed to perform failover for service %s: %v", policy.ServiceID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// performFailover performs failover for a specific service
|
||||
func (fm *FailoverManager) performFailover(ctx context.Context, policy *FailoverPolicy, reason string) error {
|
||||
log.Printf("Performing failover for service %s: %s", policy.ServiceID, reason)
|
||||
|
||||
// In a real implementation, this would:
|
||||
// 1. Identify healthy backup nodes
|
||||
// 2. Start new instances on backup nodes
|
||||
// 3. Update DNS/load balancer to point to new instances
|
||||
// 4. Wait for health checks to pass
|
||||
// 5. Shut down unhealthy instances
|
||||
|
||||
// For now, we'll just log the action
|
||||
log.Printf("Failover completed for service %s", policy.ServiceID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthChecker methods
|
||||
|
||||
// Start starts the health checker
|
||||
func (hc *HealthChecker) Start(ctx context.Context) error {
|
||||
ticker := time.NewTicker(hc.checkInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Printf("HealthChecker started with check interval: %v", hc.checkInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
if err := hc.performHealthChecks(ctx); err != nil {
|
||||
log.Printf("Error during health checks: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// performHealthChecks performs all configured health checks
|
||||
func (hc *HealthChecker) performHealthChecks(ctx context.Context) error {
|
||||
hc.mu.RLock()
|
||||
checks := make([]*HealthCheck, 0, len(hc.checks))
|
||||
for _, check := range hc.checks {
|
||||
checks = append(checks, check)
|
||||
}
|
||||
hc.mu.RUnlock()
|
||||
|
||||
for _, check := range checks {
|
||||
result := hc.performHealthCheck(ctx, check)
|
||||
hc.mu.Lock()
|
||||
hc.results[check.ID] = result
|
||||
hc.mu.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// performHealthCheck performs a single health check
|
||||
func (hc *HealthChecker) performHealthCheck(ctx context.Context, check *HealthCheck) *HealthCheckResult {
|
||||
start := time.Now()
|
||||
result := &HealthCheckResult{
|
||||
CheckID: check.ID,
|
||||
Timestamp: start,
|
||||
Status: HealthStatusUnknown,
|
||||
}
|
||||
|
||||
// In a real implementation, this would perform actual health checks
|
||||
// For now, we'll simulate the check
|
||||
time.Sleep(10 * time.Millisecond) // Simulate network latency
|
||||
|
||||
// Simulate healthy/unhealthy based on some logic
|
||||
if time.Now().Unix()%10 == 0 { // 10% chance of being unhealthy
|
||||
result.Status = HealthStatusUnhealthy
|
||||
result.Message = "Service not responding"
|
||||
result.ErrorCode = "TIMEOUT"
|
||||
} else {
|
||||
result.Status = HealthStatusHealthy
|
||||
result.Message = "Service is healthy"
|
||||
}
|
||||
|
||||
result.Latency = time.Since(start)
|
||||
return result
|
||||
}
|
||||
|
||||
// AddHealthCheck adds a new health check
|
||||
func (hc *HealthChecker) AddHealthCheck(check *HealthCheck) {
|
||||
hc.mu.Lock()
|
||||
defer hc.mu.Unlock()
|
||||
hc.checks[check.ID] = check
|
||||
}
|
||||
|
||||
// RemoveHealthCheck removes a health check
|
||||
func (hc *HealthChecker) RemoveHealthCheck(checkID string) {
|
||||
hc.mu.Lock()
|
||||
defer hc.mu.Unlock()
|
||||
delete(hc.checks, checkID)
|
||||
delete(hc.results, checkID)
|
||||
}
|
||||
|
||||
// GetAllHealthChecks returns all health checks
|
||||
func (hc *HealthChecker) GetAllHealthChecks() map[string]*HealthCheck {
|
||||
hc.mu.RLock()
|
||||
defer hc.mu.RUnlock()
|
||||
|
||||
result := make(map[string]*HealthCheck)
|
||||
for id, check := range hc.checks {
|
||||
result[id] = check
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAllResults returns all health check results
|
||||
func (hc *HealthChecker) GetAllResults() map[string]*HealthCheckResult {
|
||||
hc.mu.RLock()
|
||||
defer hc.mu.RUnlock()
|
||||
|
||||
result := make(map[string]*HealthCheckResult)
|
||||
for id, checkResult := range hc.results {
|
||||
result[id] = checkResult
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// AlertManager methods
|
||||
|
||||
// Start starts the alert manager
|
||||
func (am *AlertManager) Start(ctx context.Context) error {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Printf("AlertManager started")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
if err := am.evaluateAlertRules(ctx); err != nil {
|
||||
log.Printf("Error evaluating alert rules: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// evaluateAlertRules evaluates all alert rules and triggers alerts if needed
|
||||
func (am *AlertManager) evaluateAlertRules(ctx context.Context) error {
|
||||
am.mu.RLock()
|
||||
rules := make([]*AlertRule, 0, len(am.rules))
|
||||
for _, rule := range am.rules {
|
||||
if rule.Enabled {
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
}
|
||||
am.mu.RUnlock()
|
||||
|
||||
for _, rule := range rules {
|
||||
if am.shouldTriggerAlert(rule) {
|
||||
alert := am.createAlert(rule)
|
||||
if err := am.triggerAlert(ctx, alert); err != nil {
|
||||
log.Printf("Failed to trigger alert: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldTriggerAlert checks if an alert should be triggered
|
||||
func (am *AlertManager) shouldTriggerAlert(rule *AlertRule) bool {
|
||||
// In a real implementation, this would query metrics and evaluate the condition
|
||||
// For now, we'll simulate based on time
|
||||
return time.Now().Unix()%20 == 0 // 5% chance of triggering
|
||||
}
|
||||
|
||||
// createAlert creates an alert from a rule
|
||||
func (am *AlertManager) createAlert(rule *AlertRule) *Alert {
|
||||
return &Alert{
|
||||
ID: fmt.Sprintf("alert_%s_%d", rule.ID, time.Now().Unix()),
|
||||
RuleID: rule.ID,
|
||||
Status: AlertStatusFiring,
|
||||
Severity: rule.Severity,
|
||||
Message: fmt.Sprintf("Alert triggered: %s", rule.Name),
|
||||
Labels: rule.Labels,
|
||||
Annotations: rule.Annotations,
|
||||
StartsAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// triggerAlert triggers an alert
|
||||
func (am *AlertManager) triggerAlert(ctx context.Context, alert *Alert) error {
|
||||
am.mu.Lock()
|
||||
am.activeAlerts[alert.ID] = alert
|
||||
am.mu.Unlock()
|
||||
|
||||
// Send notifications
|
||||
for _, notifierID := range am.getAlertRule(alert.RuleID).Notifiers {
|
||||
if notifier, exists := am.notifiers[notifierID]; exists {
|
||||
if err := notifier.Send(ctx, alert); err != nil {
|
||||
log.Printf("Failed to send notification via %s: %v", notifierID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Alert triggered: %s", alert.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAlertRule returns the rule for an alert
|
||||
func (am *AlertManager) getAlertRule(ruleID string) *AlertRule {
|
||||
am.mu.RLock()
|
||||
defer am.mu.RUnlock()
|
||||
return am.rules[ruleID]
|
||||
}
|
||||
|
||||
// AddAlertRule adds a new alert rule
|
||||
func (am *AlertManager) AddAlertRule(rule *AlertRule) {
|
||||
am.mu.Lock()
|
||||
defer am.mu.Unlock()
|
||||
am.rules[rule.ID] = rule
|
||||
}
|
||||
|
||||
// RemoveAlertRule removes an alert rule
|
||||
func (am *AlertManager) RemoveAlertRule(ruleID string) {
|
||||
am.mu.Lock()
|
||||
defer am.mu.Unlock()
|
||||
delete(am.rules, ruleID)
|
||||
}
|
||||
|
||||
// AddNotifier adds a new notifier
|
||||
func (am *AlertManager) AddNotifier(id string, notifier Notifier) {
|
||||
am.mu.Lock()
|
||||
defer am.mu.Unlock()
|
||||
am.notifiers[id] = notifier
|
||||
}
|
||||
|
||||
// GetActiveAlerts returns all active alerts
|
||||
func (am *AlertManager) GetActiveAlerts() map[string]*Alert {
|
||||
am.mu.RLock()
|
||||
defer am.mu.RUnlock()
|
||||
|
||||
result := make(map[string]*Alert)
|
||||
for id, alert := range am.activeAlerts {
|
||||
result[id] = alert
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ResolveAlert resolves an alert
|
||||
func (am *AlertManager) ResolveAlert(alertID string) {
|
||||
am.mu.Lock()
|
||||
defer am.mu.Unlock()
|
||||
|
||||
if alert, exists := am.activeAlerts[alertID]; exists {
|
||||
now := time.Now()
|
||||
alert.Status = AlertStatusResolved
|
||||
alert.EndsAt = &now
|
||||
alert.UpdatedAt = now
|
||||
}
|
||||
|
||||
delete(am.activeAlerts, alertID)
|
||||
}
|
||||
|
||||
// Mock Notifier implementations
|
||||
|
||||
// EmailNotifier sends alerts via email
|
||||
type EmailNotifier struct {
|
||||
SMTPHost string
|
||||
SMTPPort int
|
||||
Username string
|
||||
Password string
|
||||
From string
|
||||
To []string
|
||||
}
|
||||
|
||||
func (n *EmailNotifier) Send(ctx context.Context, alert *Alert) error {
|
||||
log.Printf("Sending email alert: %s", alert.Message)
|
||||
// In a real implementation, this would send an actual email
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *EmailNotifier) Type() string {
|
||||
return "email"
|
||||
}
|
||||
|
||||
// SlackNotifier sends alerts to Slack
|
||||
type SlackNotifier struct {
|
||||
WebhookURL string
|
||||
Channel string
|
||||
}
|
||||
|
||||
func (n *SlackNotifier) Send(ctx context.Context, alert *Alert) error {
|
||||
log.Printf("Sending Slack alert: %s", alert.Message)
|
||||
// In a real implementation, this would send to Slack webhook
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *SlackNotifier) Type() string {
|
||||
return "slack"
|
||||
}
|
||||
|
||||
// WebhookNotifier sends alerts via webhook
|
||||
type WebhookNotifier struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
func (n *WebhookNotifier) Send(ctx context.Context, alert *Alert) error {
|
||||
log.Printf("Sending webhook alert: %s", alert.Message)
|
||||
// In a real implementation, this would send HTTP request
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *WebhookNotifier) Type() string {
|
||||
return "webhook"
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"containr/internal/deployment"
|
||||
)
|
||||
|
||||
// MetricsCollector collects and aggregates metrics from nodes and services
|
||||
type MetricsCollector struct {
|
||||
nodes map[string]*NodeMetrics
|
||||
services map[string]*ServiceMetrics
|
||||
scheduler *deployment.Scheduler
|
||||
mu sync.RWMutex
|
||||
collectInterval time.Duration
|
||||
storage MetricsStorage
|
||||
}
|
||||
|
||||
// NodeMetrics represents metrics for a node
|
||||
type NodeMetrics struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
CPU CPUMetrics `json:"cpu"`
|
||||
Memory MemoryMetrics `json:"memory"`
|
||||
Storage StorageMetrics `json:"storage"`
|
||||
Network NetworkMetrics `json:"network"`
|
||||
Containers []ContainerMetrics `json:"containers"`
|
||||
System SystemMetrics `json:"system"`
|
||||
}
|
||||
|
||||
// ServiceMetrics represents metrics for a service
|
||||
type ServiceMetrics struct {
|
||||
ServiceID string `json:"service_id"`
|
||||
ServiceName string `json:"service_name"`
|
||||
ProjectID string `json:"project_id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Instances []InstanceMetrics `json:"instances"`
|
||||
Requests RequestMetrics `json:"requests"`
|
||||
Errors ErrorMetrics `json:"errors"`
|
||||
Performance PerformanceMetrics `json:"performance"`
|
||||
Resources ResourceMetrics `json:"resources"`
|
||||
}
|
||||
|
||||
// InstanceMetrics represents metrics for a service instance
|
||||
type InstanceMetrics struct {
|
||||
InstanceID string `json:"instance_id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Status string `json:"status"`
|
||||
CPU float64 `json:"cpu"` // CPU usage percentage
|
||||
Memory int64 `json:"memory"` // Memory usage in bytes
|
||||
Network NetworkMetrics `json:"network"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Health HealthMetrics `json:"health"`
|
||||
}
|
||||
|
||||
// CPUMetrics represents CPU metrics
|
||||
type CPUMetrics struct {
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
UsageCores float64 `json:"usage_cores"`
|
||||
LoadAverage1 float64 `json:"load_average_1"`
|
||||
LoadAverage5 float64 `json:"load_average_5"`
|
||||
LoadAverage15 float64 `json:"load_average_15"`
|
||||
}
|
||||
|
||||
// MemoryMetrics represents memory metrics
|
||||
type MemoryMetrics struct {
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
Available int64 `json:"available"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
SwapTotal int64 `json:"swap_total"`
|
||||
SwapUsed int64 `json:"swap_used"`
|
||||
}
|
||||
|
||||
// StorageMetrics represents storage metrics
|
||||
type StorageMetrics struct {
|
||||
Total int64 `json:"total"`
|
||||
Used int64 `json:"used"`
|
||||
Available int64 `json:"available"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
IOPS int64 `json:"iops"`
|
||||
Throughput int64 `json:"throughput"`
|
||||
}
|
||||
|
||||
// NetworkMetrics represents network metrics
|
||||
type NetworkMetrics struct {
|
||||
BytesIn int64 `json:"bytes_in"`
|
||||
BytesOut int64 `json:"bytes_out"`
|
||||
PacketsIn int64 `json:"packets_in"`
|
||||
PacketsOut int64 `json:"packets_out"`
|
||||
ConnectionsIn int64 `json:"connections_in"`
|
||||
ConnectionsOut int64 `json:"connections_out"`
|
||||
ErrorsIn int64 `json:"errors_in"`
|
||||
ErrorsOut int64 `json:"errors_out"`
|
||||
}
|
||||
|
||||
// ContainerMetrics represents metrics for containers
|
||||
type ContainerMetrics struct {
|
||||
ContainerID string `json:"container_id"`
|
||||
Name string `json:"name"`
|
||||
State string `json:"state"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Memory int64 `json:"memory"`
|
||||
Network NetworkMetrics `json:"network"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
}
|
||||
|
||||
// SystemMetrics represents system-level metrics
|
||||
type SystemMetrics struct {
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
Processes int `json:"processes"`
|
||||
OS string `json:"os"`
|
||||
Kernel string `json:"kernel"`
|
||||
Architecture string `json:"architecture"`
|
||||
}
|
||||
|
||||
// RequestMetrics represents HTTP/request metrics
|
||||
type RequestMetrics struct {
|
||||
Total int64 `json:"total"`
|
||||
Success int64 `json:"success"`
|
||||
Errors int64 `json:"errors"`
|
||||
AvgLatency float64 `json:"avg_latency"`
|
||||
P95Latency float64 `json:"p95_latency"`
|
||||
P99Latency float64 `json:"p99_latency"`
|
||||
Throughput float64 `json:"throughput"`
|
||||
}
|
||||
|
||||
// ErrorMetrics represents error metrics
|
||||
type ErrorMetrics struct {
|
||||
Total int64 `json:"total"`
|
||||
ByType map[string]int64 `json:"by_type"`
|
||||
ByStatusCode map[string]int64 `json:"by_status_code"`
|
||||
Rate float64 `json:"rate"`
|
||||
}
|
||||
|
||||
// PerformanceMetrics represents performance metrics
|
||||
type PerformanceMetrics struct {
|
||||
ResponseTime float64 `json:"response_time"`
|
||||
Throughput float64 `json:"throughput"`
|
||||
Concurrency int64 `json:"concurrency"`
|
||||
Saturation float64 `json:"saturation"`
|
||||
Utilization float64 `json:"utilization"`
|
||||
}
|
||||
|
||||
// ResourceMetrics represents resource utilization metrics
|
||||
type ResourceMetrics struct {
|
||||
CPUUsage float64 `json:"cpu_usage"`
|
||||
MemoryUsage int64 `json:"memory_usage"`
|
||||
StorageUsage int64 `json:"storage_usage"`
|
||||
NetworkUsage int64 `json:"network_usage"`
|
||||
ResourceScore float64 `json:"resource_score"`
|
||||
}
|
||||
|
||||
// HealthMetrics represents health metrics
|
||||
type HealthMetrics struct {
|
||||
Status string `json:"status"`
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
CheckCount int `json:"check_count"`
|
||||
FailureCount int `json:"failure_count"`
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
}
|
||||
|
||||
// MetricsStorage defines the interface for metrics storage
|
||||
type MetricsStorage interface {
|
||||
StoreNodeMetrics(ctx context.Context, metrics *NodeMetrics) error
|
||||
StoreServiceMetrics(ctx context.Context, metrics *ServiceMetrics) error
|
||||
GetNodeMetrics(ctx context.Context, nodeID string, from, to time.Time) ([]*NodeMetrics, error)
|
||||
GetServiceMetrics(ctx context.Context, serviceID string, from, to time.Time) ([]*ServiceMetrics, error)
|
||||
GetAggregatedMetrics(ctx context.Context, query MetricsQuery) (*AggregatedMetrics, error)
|
||||
}
|
||||
|
||||
// MetricsQuery represents a query for aggregated metrics
|
||||
type MetricsQuery struct {
|
||||
Type string `json:"type"` // node, service, project
|
||||
ID string `json:"id"` // node_id, service_id, project_id
|
||||
Metrics []string `json:"metrics"` // cpu, memory, network, etc.
|
||||
From time.Time `json:"from"`
|
||||
To time.Time `json:"to"`
|
||||
Interval time.Duration `json:"interval"`
|
||||
GroupBy []string `json:"group_by"`
|
||||
Filters map[string]string `json:"filters"`
|
||||
}
|
||||
|
||||
// AggregatedMetrics represents aggregated metrics data
|
||||
type AggregatedMetrics struct {
|
||||
Query MetricsQuery `json:"query"`
|
||||
TimeSeries []TimeSeriesPoint `json:"time_series"`
|
||||
Summary map[string]MetricSummary `json:"summary"`
|
||||
}
|
||||
|
||||
// TimeSeriesPoint represents a point in a time series
|
||||
type TimeSeriesPoint struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Values map[string]float64 `json:"values"`
|
||||
}
|
||||
|
||||
// MetricSummary represents summary statistics for a metric
|
||||
type MetricSummary struct {
|
||||
Min float64 `json:"min"`
|
||||
Max float64 `json:"max"`
|
||||
Avg float64 `json:"avg"`
|
||||
P50 float64 `json:"p50"`
|
||||
P95 float64 `json:"p95"`
|
||||
P99 float64 `json:"p99"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// NewMetricsCollector creates a new metrics collector
|
||||
func NewMetricsCollector(scheduler *deployment.Scheduler, storage MetricsStorage) *MetricsCollector {
|
||||
return &MetricsCollector{
|
||||
nodes: make(map[string]*NodeMetrics),
|
||||
services: make(map[string]*ServiceMetrics),
|
||||
scheduler: scheduler,
|
||||
collectInterval: 30 * time.Second,
|
||||
storage: storage,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the metrics collection process
|
||||
func (mc *MetricsCollector) Start(ctx context.Context) error {
|
||||
ticker := time.NewTicker(mc.collectInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
if err := mc.collectMetrics(ctx); err != nil {
|
||||
fmt.Printf("Error collecting metrics: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collectMetrics collects metrics from all nodes and services
|
||||
func (mc *MetricsCollector) collectMetrics(ctx context.Context) error {
|
||||
// Collect node metrics
|
||||
nodes := mc.scheduler.GetNodes()
|
||||
for _, node := range nodes {
|
||||
metrics, err := mc.collectNodeMetrics(ctx, node)
|
||||
if err != nil {
|
||||
fmt.Printf("Error collecting metrics for node %s: %v\n", node.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
mc.mu.Lock()
|
||||
mc.nodes[node.ID] = metrics
|
||||
mc.mu.Unlock()
|
||||
|
||||
// Store metrics
|
||||
if err := mc.storage.StoreNodeMetrics(ctx, metrics); err != nil {
|
||||
fmt.Printf("Error storing node metrics: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Collect service metrics
|
||||
// This would involve querying service instances and collecting their metrics
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectNodeMetrics collects metrics from a specific node
|
||||
func (mc *MetricsCollector) collectNodeMetrics(ctx context.Context, node *deployment.Node) (*NodeMetrics, error) {
|
||||
// In a real implementation, this would collect actual metrics from the node
|
||||
// For now, we'll simulate metrics collection
|
||||
now := time.Now()
|
||||
|
||||
metrics := &NodeMetrics{
|
||||
NodeID: node.ID,
|
||||
Timestamp: now,
|
||||
CPU: CPUMetrics{
|
||||
UsagePercent: node.Usage.CPU,
|
||||
UsageCores: node.Usage.CPU * float64(node.Capacity.CPU) / 100,
|
||||
LoadAverage1: 1.5,
|
||||
LoadAverage5: 1.8,
|
||||
LoadAverage15: 2.1,
|
||||
},
|
||||
Memory: MemoryMetrics{
|
||||
Total: node.Capacity.Memory,
|
||||
Used: node.Usage.Memory,
|
||||
Available: node.Capacity.Memory - node.Usage.Memory,
|
||||
UsagePercent: float64(node.Usage.Memory) / float64(node.Capacity.Memory) * 100,
|
||||
SwapTotal: 1024 * 1024 * 1024, // 1GB
|
||||
SwapUsed: 512 * 1024 * 1024, // 512MB
|
||||
},
|
||||
Storage: StorageMetrics{
|
||||
Total: node.Capacity.Storage,
|
||||
Used: node.Usage.Storage,
|
||||
Available: node.Capacity.Storage - node.Usage.Storage,
|
||||
UsagePercent: float64(node.Usage.Storage) / float64(node.Capacity.Storage) * 100,
|
||||
IOPS: 1000,
|
||||
Throughput: 1024 * 1024 * 100, // 100MB/s
|
||||
},
|
||||
Network: NetworkMetrics{
|
||||
BytesIn: node.Usage.Network,
|
||||
BytesOut: node.Usage.Network,
|
||||
PacketsIn: 10000,
|
||||
PacketsOut: 8000,
|
||||
ConnectionsIn: 50,
|
||||
ConnectionsOut: 30,
|
||||
ErrorsIn: 0,
|
||||
ErrorsOut: 0,
|
||||
},
|
||||
Containers: []ContainerMetrics{},
|
||||
System: SystemMetrics{
|
||||
Uptime: time.Since(node.LastHeartbeat),
|
||||
Processes: 150,
|
||||
OS: "linux",
|
||||
Kernel: "5.15.0",
|
||||
Architecture: "x86_64",
|
||||
},
|
||||
}
|
||||
|
||||
// Collect container metrics for this node
|
||||
for _, containerID := range node.Containers {
|
||||
containerMetrics := mc.collectContainerMetrics(containerID)
|
||||
metrics.Containers = append(metrics.Containers, containerMetrics)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// collectContainerMetrics collects metrics for a specific container
|
||||
func (mc *MetricsCollector) collectContainerMetrics(containerID string) ContainerMetrics {
|
||||
// In a real implementation, this would query Docker/container runtime
|
||||
return ContainerMetrics{
|
||||
ContainerID: containerID,
|
||||
Name: fmt.Sprintf("container-%s", containerID[:8]),
|
||||
State: "running",
|
||||
CPU: 25.5,
|
||||
Memory: 512 * 1024 * 1024, // 512MB
|
||||
Network: NetworkMetrics{
|
||||
BytesIn: 1024 * 1024 * 10, // 10MB
|
||||
BytesOut: 1024 * 1024 * 8, // 8MB
|
||||
PacketsIn: 1000,
|
||||
PacketsOut: 800,
|
||||
},
|
||||
StartTime: time.Now().Add(-1 * time.Hour),
|
||||
}
|
||||
}
|
||||
|
||||
// GetNodeMetrics returns the latest metrics for a node
|
||||
func (mc *MetricsCollector) GetNodeMetrics(nodeID string) (*NodeMetrics, error) {
|
||||
mc.mu.RLock()
|
||||
defer mc.mu.RUnlock()
|
||||
|
||||
metrics, exists := mc.nodes[nodeID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no metrics found for node: %s", nodeID)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// GetAllNodeMetrics returns metrics for all nodes
|
||||
func (mc *MetricsCollector) GetAllNodeMetrics() map[string]*NodeMetrics {
|
||||
mc.mu.RLock()
|
||||
defer mc.mu.RUnlock()
|
||||
|
||||
// Return a copy to avoid race conditions
|
||||
result := make(map[string]*NodeMetrics)
|
||||
for id, metrics := range mc.nodes {
|
||||
result[id] = metrics
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetServiceMetrics returns the latest metrics for a service
|
||||
func (mc *MetricsCollector) GetServiceMetrics(serviceID string) (*ServiceMetrics, error) {
|
||||
mc.mu.RLock()
|
||||
defer mc.mu.RUnlock()
|
||||
|
||||
metrics, exists := mc.services[serviceID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no metrics found for service: %s", serviceID)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// GetAggregatedMetrics returns aggregated metrics based on a query
|
||||
func (mc *MetricsCollector) GetAggregatedMetrics(ctx context.Context, query MetricsQuery) (*AggregatedMetrics, error) {
|
||||
return mc.storage.GetAggregatedMetrics(ctx, query)
|
||||
}
|
||||
|
||||
// GetMetricsSummary returns a summary of all metrics
|
||||
func (mc *MetricsCollector) GetMetricsSummary() map[string]interface{} {
|
||||
mc.mu.RLock()
|
||||
defer mc.mu.RUnlock()
|
||||
|
||||
totalNodes := len(mc.nodes)
|
||||
totalServices := len(mc.services)
|
||||
healthyNodes := 0
|
||||
totalCPU := 0.0
|
||||
totalMemory := int64(0)
|
||||
|
||||
for _, metrics := range mc.nodes {
|
||||
if metrics.CPU.UsagePercent < 80 {
|
||||
healthyNodes++
|
||||
}
|
||||
totalCPU += metrics.CPU.UsagePercent
|
||||
totalMemory += metrics.Memory.Used
|
||||
}
|
||||
|
||||
avgCPU := float64(0)
|
||||
if totalNodes > 0 {
|
||||
avgCPU = totalCPU / float64(totalNodes)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_nodes": totalNodes,
|
||||
"healthy_nodes": healthyNodes,
|
||||
"total_services": totalServices,
|
||||
"avg_cpu_usage": avgCPU,
|
||||
"total_memory": totalMemory,
|
||||
"collect_interval": mc.collectInterval.String(),
|
||||
"last_collection": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
// ExportMetrics exports metrics in various formats
|
||||
func (mc *MetricsCollector) ExportMetrics(format string) ([]byte, error) {
|
||||
mc.mu.RLock()
|
||||
defer mc.mu.RUnlock()
|
||||
|
||||
data := map[string]interface{}{
|
||||
"nodes": mc.nodes,
|
||||
"services": mc.services,
|
||||
"timestamp": time.Now(),
|
||||
}
|
||||
|
||||
switch format {
|
||||
case "json":
|
||||
return json.MarshalIndent(data, "", " ")
|
||||
case "prometheus":
|
||||
return mc.exportPrometheusFormat()
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported export format: %s", format)
|
||||
}
|
||||
}
|
||||
|
||||
// exportPrometheusFormat exports metrics in Prometheus format
|
||||
func (mc *MetricsCollector) exportPrometheusFormat() ([]byte, error) {
|
||||
var output []string
|
||||
|
||||
for nodeID, metrics := range mc.nodes {
|
||||
// Node CPU metrics
|
||||
output = append(output, fmt.Sprintf("# HELP node_cpu_usage_percent CPU usage percentage for node"))
|
||||
output = append(output, fmt.Sprintf("# TYPE node_cpu_usage_percent gauge"))
|
||||
output = append(output, fmt.Sprintf("node_cpu_usage_percent{node=\"%s\"} %f", nodeID, metrics.CPU.UsagePercent))
|
||||
|
||||
// Node memory metrics
|
||||
output = append(output, fmt.Sprintf("# HELP node_memory_usage_bytes Memory usage in bytes for node"))
|
||||
output = append(output, fmt.Sprintf("# TYPE node_memory_usage_bytes gauge"))
|
||||
output = append(output, fmt.Sprintf("node_memory_usage_bytes{node=\"%s\"} %d", nodeID, metrics.Memory.Used))
|
||||
|
||||
// Node network metrics
|
||||
output = append(output, fmt.Sprintf("# HELP node_network_bytes_in Total bytes received for node"))
|
||||
output = append(output, fmt.Sprintf("# TYPE node_network_bytes_in counter"))
|
||||
output = append(output, fmt.Sprintf("node_network_bytes_in{node=\"%s\"} %d", nodeID, metrics.Network.BytesIn))
|
||||
}
|
||||
|
||||
result := []byte(strings.Join(output, "\n"))
|
||||
return result, nil
|
||||
}
|
||||
@@ -1,553 +0,0 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// PostgreSQLMetricsStorage implements MetricsStorage using PostgreSQL
|
||||
type PostgreSQLMetricsStorage struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewPostgreSQLMetricsStorage creates a new PostgreSQL metrics storage
|
||||
func NewPostgreSQLMetricsStorage(db *sql.DB) *PostgreSQLMetricsStorage {
|
||||
return &PostgreSQLMetricsStorage{db: db}
|
||||
}
|
||||
|
||||
// StoreNodeMetrics stores node metrics in the database
|
||||
func (s *PostgreSQLMetricsStorage) StoreNodeMetrics(ctx context.Context, metrics *NodeMetrics) error {
|
||||
query := `
|
||||
INSERT INTO node_metrics (
|
||||
node_id, timestamp, cpu_usage, cpu_cores, load_avg_1, load_avg_5, load_avg_15,
|
||||
memory_total, memory_used, memory_available, memory_usage_percent,
|
||||
storage_total, storage_used, storage_available, storage_usage_percent,
|
||||
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
|
||||
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
|
||||
uptime, processes, os, kernel, architecture
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)
|
||||
ON CONFLICT (node_id, timestamp) DO UPDATE SET
|
||||
cpu_usage = EXCLUDED.cpu_usage,
|
||||
cpu_cores = EXCLUDED.cpu_cores,
|
||||
load_avg_1 = EXCLUDED.load_avg_1,
|
||||
load_avg_5 = EXCLUDED.load_avg_5,
|
||||
load_avg_15 = EXCLUDED.load_avg_15,
|
||||
memory_total = EXCLUDED.memory_total,
|
||||
memory_used = EXCLUDED.memory_used,
|
||||
memory_available = EXCLUDED.memory_available,
|
||||
memory_usage_percent = EXCLUDED.memory_usage_percent,
|
||||
storage_total = EXCLUDED.storage_total,
|
||||
storage_used = EXCLUDED.storage_used,
|
||||
storage_available = EXCLUDED.storage_available,
|
||||
storage_usage_percent = EXCLUDED.storage_usage_percent,
|
||||
network_bytes_in = EXCLUDED.network_bytes_in,
|
||||
network_bytes_out = EXCLUDED.network_bytes_out,
|
||||
network_packets_in = EXCLUDED.network_packets_in,
|
||||
network_packets_out = EXCLUDED.network_packets_out,
|
||||
network_connections_in = EXCLUDED.network_connections_in,
|
||||
network_connections_out = EXCLUDED.network_connections_out,
|
||||
network_errors_in = EXCLUDED.network_errors_in,
|
||||
network_errors_out = EXCLUDED.network_errors_out,
|
||||
uptime = EXCLUDED.uptime,
|
||||
processes = EXCLUDED.processes,
|
||||
os = EXCLUDED.os,
|
||||
kernel = EXCLUDED.kernel,
|
||||
architecture = EXCLUDED.architecture
|
||||
`
|
||||
|
||||
_, err := s.db.ExecContext(ctx, query,
|
||||
metrics.NodeID, metrics.Timestamp, metrics.CPU.UsagePercent, metrics.CPU.UsageCores,
|
||||
metrics.CPU.LoadAverage1, metrics.CPU.LoadAverage5, metrics.CPU.LoadAverage15,
|
||||
metrics.Memory.Total, metrics.Memory.Used, metrics.Memory.Available, metrics.Memory.UsagePercent,
|
||||
metrics.Storage.Total, metrics.Storage.Used, metrics.Storage.Available, metrics.Storage.UsagePercent,
|
||||
metrics.Network.BytesIn, metrics.Network.BytesOut, metrics.Network.PacketsIn, metrics.Network.PacketsOut,
|
||||
metrics.Network.ConnectionsIn, metrics.Network.ConnectionsOut, metrics.Network.ErrorsIn, metrics.Network.ErrorsOut,
|
||||
metrics.System.Uptime, metrics.System.Processes, metrics.System.OS, metrics.System.Kernel, metrics.System.Architecture,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store node metrics: %w", err)
|
||||
}
|
||||
|
||||
// Store container metrics
|
||||
for _, container := range metrics.Containers {
|
||||
if err := s.storeContainerMetrics(ctx, metrics.NodeID, metrics.Timestamp, container); err != nil {
|
||||
return fmt.Errorf("failed to store container metrics: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StoreServiceMetrics stores service metrics in the database
|
||||
func (s *PostgreSQLMetricsStorage) StoreServiceMetrics(ctx context.Context, metrics *ServiceMetrics) error {
|
||||
query := `
|
||||
INSERT INTO service_metrics (
|
||||
service_id, service_name, project_id, timestamp,
|
||||
requests_total, requests_success, requests_errors, requests_avg_latency,
|
||||
requests_p95_latency, requests_p99_latency, requests_throughput,
|
||||
errors_total, errors_rate, performance_response_time, performance_throughput,
|
||||
performance_concurrency, performance_saturation, performance_utilization,
|
||||
resource_cpu_usage, resource_memory_usage, resource_storage_usage,
|
||||
resource_network_usage, resource_score
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
|
||||
ON CONFLICT (service_id, timestamp) DO UPDATE SET
|
||||
requests_total = EXCLUDED.requests_total,
|
||||
requests_success = EXCLUDED.requests_success,
|
||||
requests_errors = EXCLUDED.requests_errors,
|
||||
requests_avg_latency = EXCLUDED.requests_avg_latency,
|
||||
requests_p95_latency = EXCLUDED.requests_p95_latency,
|
||||
requests_p99_latency = EXCLUDED.requests_p99_latency,
|
||||
requests_throughput = EXCLUDED.requests_throughput,
|
||||
errors_total = EXCLUDED.errors_total,
|
||||
errors_rate = EXCLUDED.errors_rate,
|
||||
performance_response_time = EXCLUDED.performance_response_time,
|
||||
performance_throughput = EXCLUDED.performance_throughput,
|
||||
performance_concurrency = EXCLUDED.performance_concurrency,
|
||||
performance_saturation = EXCLUDED.performance_saturation,
|
||||
performance_utilization = EXCLUDED.performance_utilization,
|
||||
resource_cpu_usage = EXCLUDED.resource_cpu_usage,
|
||||
resource_memory_usage = EXCLUDED.resource_memory_usage,
|
||||
resource_storage_usage = EXCLUDED.resource_storage_usage,
|
||||
resource_network_usage = EXCLUDED.resource_network_usage,
|
||||
resource_score = EXCLUDED.resource_score
|
||||
`
|
||||
|
||||
_, err := s.db.ExecContext(ctx, query,
|
||||
metrics.ServiceID, metrics.ServiceName, metrics.ProjectID, metrics.Timestamp,
|
||||
metrics.Requests.Total, metrics.Requests.Success, metrics.Requests.Errors,
|
||||
metrics.Requests.AvgLatency, metrics.Requests.P95Latency, metrics.Requests.P99Latency,
|
||||
metrics.Requests.Throughput, metrics.Errors.Total, metrics.Errors.Rate,
|
||||
metrics.Performance.ResponseTime, metrics.Performance.Throughput,
|
||||
metrics.Performance.Concurrency, metrics.Performance.Saturation, metrics.Performance.Utilization,
|
||||
metrics.Resources.CPUUsage, metrics.Resources.MemoryUsage, metrics.Resources.StorageUsage,
|
||||
metrics.Resources.NetworkUsage, metrics.Resources.ResourceScore,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store service metrics: %w", err)
|
||||
}
|
||||
|
||||
// Store instance metrics
|
||||
for _, instance := range metrics.Instances {
|
||||
if err := s.storeInstanceMetrics(ctx, metrics.ServiceID, metrics.Timestamp, instance); err != nil {
|
||||
return fmt.Errorf("failed to store instance metrics: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNodeMetrics retrieves node metrics from the database
|
||||
func (s *PostgreSQLMetricsStorage) GetNodeMetrics(ctx context.Context, nodeID string, from, to time.Time) ([]*NodeMetrics, error) {
|
||||
query := `
|
||||
SELECT node_id, timestamp, cpu_usage, cpu_cores, load_avg_1, load_avg_5, load_avg_15,
|
||||
memory_total, memory_used, memory_available, memory_usage_percent,
|
||||
storage_total, storage_used, storage_available, storage_usage_percent,
|
||||
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
|
||||
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
|
||||
uptime, processes, os, kernel, architecture
|
||||
FROM node_metrics
|
||||
WHERE node_id = $1 AND timestamp BETWEEN $2 AND $3
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, nodeID, from, to)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query node metrics: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var metrics []*NodeMetrics
|
||||
for rows.Next() {
|
||||
var m NodeMetrics
|
||||
err := rows.Scan(
|
||||
&m.NodeID, &m.Timestamp, &m.CPU.UsagePercent, &m.CPU.UsageCores,
|
||||
&m.CPU.LoadAverage1, &m.CPU.LoadAverage5, &m.CPU.LoadAverage15,
|
||||
&m.Memory.Total, &m.Memory.Used, &m.Memory.Available, &m.Memory.UsagePercent,
|
||||
&m.Storage.Total, &m.Storage.Used, &m.Storage.Available, &m.Storage.UsagePercent,
|
||||
&m.Network.BytesIn, &m.Network.BytesOut, &m.Network.PacketsIn, &m.Network.PacketsOut,
|
||||
&m.Network.ConnectionsIn, &m.Network.ConnectionsOut, &m.Network.ErrorsIn, &m.Network.ErrorsOut,
|
||||
&m.System.Uptime, &m.System.Processes, &m.System.OS, &m.System.Kernel, &m.System.Architecture,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan node metrics: %w", err)
|
||||
}
|
||||
|
||||
// Get container metrics for this timestamp
|
||||
containers, err := s.getContainerMetrics(ctx, nodeID, m.Timestamp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get container metrics: %w", err)
|
||||
}
|
||||
m.Containers = containers
|
||||
|
||||
metrics = append(metrics, &m)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// GetServiceMetrics retrieves service metrics from the database
|
||||
func (s *PostgreSQLMetricsStorage) GetServiceMetrics(ctx context.Context, serviceID string, from, to time.Time) ([]*ServiceMetrics, error) {
|
||||
query := `
|
||||
SELECT service_id, service_name, project_id, timestamp,
|
||||
requests_total, requests_success, requests_errors, requests_avg_latency,
|
||||
requests_p95_latency, requests_p99_latency, requests_throughput,
|
||||
errors_total, errors_rate, performance_response_time, performance_throughput,
|
||||
performance_concurrency, performance_saturation, performance_utilization,
|
||||
resource_cpu_usage, resource_memory_usage, resource_storage_usage,
|
||||
resource_network_usage, resource_score
|
||||
FROM service_metrics
|
||||
WHERE service_id = $1 AND timestamp BETWEEN $2 AND $3
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, serviceID, from, to)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query service metrics: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var metrics []*ServiceMetrics
|
||||
for rows.Next() {
|
||||
var m ServiceMetrics
|
||||
err := rows.Scan(
|
||||
&m.ServiceID, &m.ServiceName, &m.ProjectID, &m.Timestamp,
|
||||
&m.Requests.Total, &m.Requests.Success, &m.Requests.Errors,
|
||||
&m.Requests.AvgLatency, &m.Requests.P95Latency, &m.Requests.P99Latency,
|
||||
&m.Requests.Throughput, &m.Errors.Total, &m.Errors.Rate,
|
||||
&m.Performance.ResponseTime, &m.Performance.Throughput,
|
||||
&m.Performance.Concurrency, &m.Performance.Saturation, &m.Performance.Utilization,
|
||||
&m.Resources.CPUUsage, &m.Resources.MemoryUsage, &m.Resources.StorageUsage,
|
||||
&m.Resources.NetworkUsage, &m.Resources.ResourceScore,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan service metrics: %w", err)
|
||||
}
|
||||
|
||||
// Get instance metrics for this timestamp
|
||||
instances, err := s.getInstanceMetrics(ctx, serviceID, m.Timestamp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get instance metrics: %w", err)
|
||||
}
|
||||
m.Instances = instances
|
||||
|
||||
metrics = append(metrics, &m)
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// GetAggregatedMetrics retrieves aggregated metrics based on a query
|
||||
func (s *PostgreSQLMetricsStorage) GetAggregatedMetrics(ctx context.Context, query MetricsQuery) (*AggregatedMetrics, error) {
|
||||
// This is a simplified implementation
|
||||
// In a real system, you'd build dynamic SQL based on the query
|
||||
|
||||
var timeSeries []TimeSeriesPoint
|
||||
var summary map[string]MetricSummary
|
||||
|
||||
switch query.Type {
|
||||
case "node":
|
||||
// Aggregate node metrics
|
||||
nodeQuery := `
|
||||
SELECT
|
||||
time_bucket($1, timestamp) AS bucket,
|
||||
AVG(cpu_usage) as avg_cpu,
|
||||
AVG(memory_usage_percent) as avg_memory,
|
||||
AVG(storage_usage_percent) as avg_storage
|
||||
FROM node_metrics
|
||||
WHERE node_id = $2 AND timestamp BETWEEN $3 AND $4
|
||||
GROUP BY bucket
|
||||
ORDER BY bucket ASC
|
||||
`
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, nodeQuery, query.Interval, query.ID, query.From, query.To)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query aggregated node metrics: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var bucket time.Time
|
||||
var avgCPU, avgMemory, avgStorage float64
|
||||
if err := rows.Scan(&bucket, &avgCPU, &avgMemory, &avgStorage); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan aggregated metrics: %w", err)
|
||||
}
|
||||
|
||||
point := TimeSeriesPoint{
|
||||
Timestamp: bucket,
|
||||
Values: map[string]float64{
|
||||
"cpu_usage": avgCPU,
|
||||
"memory_usage": avgMemory,
|
||||
"storage_usage": avgStorage,
|
||||
},
|
||||
}
|
||||
timeSeries = append(timeSeries, point)
|
||||
}
|
||||
|
||||
// Calculate summary statistics
|
||||
summary = map[string]MetricSummary{
|
||||
"cpu_usage": calculateSummary(timeSeries, "cpu_usage"),
|
||||
"memory_usage": calculateSummary(timeSeries, "memory_usage"),
|
||||
"storage_usage": calculateSummary(timeSeries, "storage_usage"),
|
||||
}
|
||||
}
|
||||
|
||||
return &AggregatedMetrics{
|
||||
Query: query,
|
||||
TimeSeries: timeSeries,
|
||||
Summary: summary,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
func (s *PostgreSQLMetricsStorage) storeContainerMetrics(ctx context.Context, nodeID string, timestamp time.Time, container ContainerMetrics) error {
|
||||
query := `
|
||||
INSERT INTO container_metrics (
|
||||
node_id, timestamp, container_id, name, state, cpu, memory,
|
||||
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out, start_time
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
ON CONFLICT (node_id, timestamp, container_id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
state = EXCLUDED.state,
|
||||
cpu = EXCLUDED.cpu,
|
||||
memory = EXCLUDED.memory,
|
||||
network_bytes_in = EXCLUDED.network_bytes_in,
|
||||
network_bytes_out = EXCLUDED.network_bytes_out,
|
||||
network_packets_in = EXCLUDED.network_packets_in,
|
||||
network_packets_out = EXCLUDED.network_packets_out,
|
||||
start_time = EXCLUDED.start_time
|
||||
`
|
||||
|
||||
_, err := s.db.ExecContext(ctx, query,
|
||||
nodeID, timestamp, container.ContainerID, container.Name, container.State,
|
||||
container.CPU, container.Memory, container.Network.BytesIn, container.Network.BytesOut,
|
||||
container.Network.PacketsIn, container.Network.PacketsOut, container.StartTime,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *PostgreSQLMetricsStorage) storeInstanceMetrics(ctx context.Context, serviceID string, timestamp time.Time, instance InstanceMetrics) error {
|
||||
query := `
|
||||
INSERT INTO instance_metrics (
|
||||
service_id, timestamp, instance_id, node_id, status, cpu, memory,
|
||||
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
|
||||
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
|
||||
start_time, last_seen, health_status, health_last_check, health_check_count, health_failure_count
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
|
||||
ON CONFLICT (service_id, timestamp, instance_id) DO UPDATE SET
|
||||
node_id = EXCLUDED.node_id,
|
||||
status = EXCLUDED.status,
|
||||
cpu = EXCLUDED.cpu,
|
||||
memory = EXCLUDED.memory,
|
||||
network_bytes_in = EXCLUDED.network_bytes_in,
|
||||
network_bytes_out = EXCLUDED.network_bytes_out,
|
||||
network_packets_in = EXCLUDED.network_packets_in,
|
||||
network_packets_out = EXCLUDED.network_packets_out,
|
||||
network_connections_in = EXCLUDED.network_connections_in,
|
||||
network_connections_out = EXCLUDED.network_connections_out,
|
||||
network_errors_in = EXCLUDED.network_errors_in,
|
||||
network_errors_out = EXCLUDED.network_errors_out,
|
||||
start_time = EXCLUDED.start_time,
|
||||
last_seen = EXCLUDED.last_seen,
|
||||
health_status = EXCLUDED.health_status,
|
||||
health_last_check = EXCLUDED.health_last_check,
|
||||
health_check_count = EXCLUDED.health_check_count,
|
||||
health_failure_count = EXCLUDED.health_failure_count
|
||||
`
|
||||
|
||||
_, err := s.db.ExecContext(ctx, query,
|
||||
serviceID, timestamp, instance.InstanceID, instance.NodeID, instance.Status,
|
||||
instance.CPU, instance.Memory, instance.Network.BytesIn, instance.Network.BytesOut,
|
||||
instance.Network.PacketsIn, instance.Network.PacketsOut, instance.Network.ConnectionsIn,
|
||||
instance.Network.ConnectionsOut, instance.Network.ErrorsIn, instance.Network.ErrorsOut,
|
||||
instance.StartTime, instance.LastSeen, instance.Health.Status, instance.Health.LastCheck,
|
||||
instance.Health.CheckCount, instance.Health.FailureCount,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *PostgreSQLMetricsStorage) getContainerMetrics(ctx context.Context, nodeID string, timestamp time.Time) ([]ContainerMetrics, error) {
|
||||
query := `
|
||||
SELECT container_id, name, state, cpu, memory,
|
||||
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out, start_time
|
||||
FROM container_metrics
|
||||
WHERE node_id = $1 AND timestamp = $2
|
||||
`
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, nodeID, timestamp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var containers []ContainerMetrics
|
||||
for rows.Next() {
|
||||
var c ContainerMetrics
|
||||
err := rows.Scan(
|
||||
&c.ContainerID, &c.Name, &c.State, &c.CPU, &c.Memory,
|
||||
&c.Network.BytesIn, &c.Network.BytesOut, &c.Network.PacketsIn, &c.Network.PacketsOut, &c.StartTime,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containers = append(containers, c)
|
||||
}
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (s *PostgreSQLMetricsStorage) getInstanceMetrics(ctx context.Context, serviceID string, timestamp time.Time) ([]InstanceMetrics, error) {
|
||||
query := `
|
||||
SELECT instance_id, node_id, status, cpu, memory,
|
||||
network_bytes_in, network_bytes_out, network_packets_in, network_packets_out,
|
||||
network_connections_in, network_connections_out, network_errors_in, network_errors_out,
|
||||
start_time, last_seen, health_status, health_last_check, health_check_count, health_failure_count
|
||||
FROM instance_metrics
|
||||
WHERE service_id = $1 AND timestamp = $2
|
||||
`
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query, serviceID, timestamp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var instances []InstanceMetrics
|
||||
for rows.Next() {
|
||||
var i InstanceMetrics
|
||||
err := rows.Scan(
|
||||
&i.InstanceID, &i.NodeID, &i.Status, &i.CPU, &i.Memory,
|
||||
&i.Network.BytesIn, &i.Network.BytesOut, &i.Network.PacketsIn, &i.Network.PacketsOut,
|
||||
&i.Network.ConnectionsIn, &i.Network.ConnectionsOut, &i.Network.ErrorsIn, &i.Network.ErrorsOut,
|
||||
&i.StartTime, &i.LastSeen, &i.Health.Status, &i.Health.LastCheck, &i.Health.CheckCount, &i.Health.FailureCount,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
instances = append(instances, i)
|
||||
}
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
func calculateSummary(timeSeries []TimeSeriesPoint, metricName string) MetricSummary {
|
||||
if len(timeSeries) == 0 {
|
||||
return MetricSummary{}
|
||||
}
|
||||
|
||||
var values []float64
|
||||
for _, point := range timeSeries {
|
||||
if val, exists := point.Values[metricName]; exists {
|
||||
values = append(values, val)
|
||||
}
|
||||
}
|
||||
|
||||
if len(values) == 0 {
|
||||
return MetricSummary{}
|
||||
}
|
||||
|
||||
// Simple calculation - in production, use proper statistics
|
||||
min := values[0]
|
||||
max := values[0]
|
||||
sum := 0.0
|
||||
|
||||
for _, val := range values {
|
||||
if val < min {
|
||||
min = val
|
||||
}
|
||||
if val > max {
|
||||
max = val
|
||||
}
|
||||
sum += val
|
||||
}
|
||||
|
||||
avg := sum / float64(len(values))
|
||||
|
||||
return MetricSummary{
|
||||
Min: min,
|
||||
Max: max,
|
||||
Avg: avg,
|
||||
Count: int64(len(values)),
|
||||
// P50, P95, P99 would require sorting and percentile calculation
|
||||
P50: avg,
|
||||
P95: avg,
|
||||
P99: avg,
|
||||
}
|
||||
}
|
||||
|
||||
// InMemoryMetricsStorage provides an in-memory implementation for testing
|
||||
type InMemoryMetricsStorage struct {
|
||||
nodeMetrics map[string][]*NodeMetrics
|
||||
serviceMetrics map[string][]*ServiceMetrics
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewInMemoryMetricsStorage creates a new in-memory metrics storage
|
||||
func NewInMemoryMetricsStorage() *InMemoryMetricsStorage {
|
||||
return &InMemoryMetricsStorage{
|
||||
nodeMetrics: make(map[string][]*NodeMetrics),
|
||||
serviceMetrics: make(map[string][]*ServiceMetrics),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InMemoryMetricsStorage) StoreNodeMetrics(ctx context.Context, metrics *NodeMetrics) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.nodeMetrics[metrics.NodeID] = append(s.nodeMetrics[metrics.NodeID], metrics)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InMemoryMetricsStorage) StoreServiceMetrics(ctx context.Context, metrics *ServiceMetrics) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.serviceMetrics[metrics.ServiceID] = append(s.serviceMetrics[metrics.ServiceID], metrics)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InMemoryMetricsStorage) GetNodeMetrics(ctx context.Context, nodeID string, from, to time.Time) ([]*NodeMetrics, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
metrics := s.nodeMetrics[nodeID]
|
||||
var result []*NodeMetrics
|
||||
for _, m := range metrics {
|
||||
if m.Timestamp.After(from) && m.Timestamp.Before(to) {
|
||||
result = append(result, m)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *InMemoryMetricsStorage) GetServiceMetrics(ctx context.Context, serviceID string, from, to time.Time) ([]*ServiceMetrics, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
metrics := s.serviceMetrics[serviceID]
|
||||
var result []*ServiceMetrics
|
||||
for _, m := range metrics {
|
||||
if m.Timestamp.After(from) && m.Timestamp.Before(to) {
|
||||
result = append(result, m)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *InMemoryMetricsStorage) GetAggregatedMetrics(ctx context.Context, query MetricsQuery) (*AggregatedMetrics, error) {
|
||||
// Simplified implementation
|
||||
return &AggregatedMetrics{
|
||||
Query: query,
|
||||
TimeSeries: []TimeSeriesPoint{},
|
||||
Summary: map[string]MetricSummary{},
|
||||
}, nil
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Logger middleware
|
||||
func Logger() gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
|
||||
param.ClientIP,
|
||||
param.TimeStamp.Format(time.RFC1123),
|
||||
param.Method,
|
||||
param.Path,
|
||||
param.Request.Proto,
|
||||
param.StatusCode,
|
||||
param.Latency,
|
||||
param.Request.UserAgent(),
|
||||
param.ErrorMessage,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Recovery middleware
|
||||
func Recovery() gin.HandlerFunc {
|
||||
return gin.Recovery()
|
||||
}
|
||||
|
||||
// RequestID middleware adds a unique request ID to each request
|
||||
func RequestID() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
}
|
||||
c.Set("request_id", requestID)
|
||||
c.Header("X-Request-ID", requestID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Auth middleware for JWT authentication
|
||||
func Auth(jwtSecret string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
bearerToken := strings.Split(authHeader, " ")
|
||||
if len(bearerToken) != 2 || bearerToken[0] != "Bearer" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := bearerToken[1]
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||
c.Set("user_id", claims["user_id"])
|
||||
c.Set("email", claims["email"])
|
||||
c.Next()
|
||||
} else {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"})
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorHandler middleware for consistent error handling
|
||||
func ErrorHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
|
||||
// Check if there are any errors
|
||||
if len(c.Errors) > 0 {
|
||||
err := c.Errors.Last()
|
||||
log.Printf("Request error: %v", err)
|
||||
|
||||
// Return JSON error response
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Internal server error",
|
||||
"code": "INTERNAL_ERROR",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CORSMiddleware for CORS handling
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
package networking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// DNSServer provides internal DNS resolution for services
|
||||
type DNSServer struct {
|
||||
server *dns.Server
|
||||
serviceDiscovery *ServiceDiscovery
|
||||
domain string
|
||||
addresses []string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// DNSConfig holds DNS server configuration
|
||||
type DNSConfig struct {
|
||||
Domain string `json:"domain"`
|
||||
Addresses []string `json:"addresses"`
|
||||
Port int `json:"port"`
|
||||
Upstream []string `json:"upstream"`
|
||||
}
|
||||
|
||||
// NewDNSServer creates a new DNS server
|
||||
func NewDNSServer(config DNSConfig, serviceDiscovery *ServiceDiscovery) *DNSServer {
|
||||
return &DNSServer{
|
||||
domain: config.Domain,
|
||||
addresses: config.Addresses,
|
||||
serviceDiscovery: serviceDiscovery,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the DNS server
|
||||
func (d *DNSServer) Start(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
// Create DNS handler
|
||||
handler := dns.NewServeMux()
|
||||
handler.HandleFunc(d.domain, d.handleServiceRequest)
|
||||
handler.HandleFunc("in-addr.arpa.", d.handleReverseRequest)
|
||||
|
||||
// Create server
|
||||
d.server = &dns.Server{
|
||||
Addr: ":53",
|
||||
Net: "udp",
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
// Start server in goroutine
|
||||
go func() {
|
||||
if err := d.server.ListenAndServe(); err != nil {
|
||||
fmt.Printf("DNS server error: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the DNS server
|
||||
func (d *DNSServer) Stop() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.server != nil {
|
||||
return d.server.Shutdown()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleServiceRequest handles DNS requests for services
|
||||
func (d *DNSServer) handleServiceRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
msg := new(dns.Msg)
|
||||
msg.SetReply(r)
|
||||
msg.Authoritative = true
|
||||
|
||||
for _, question := range r.Question {
|
||||
if question.Qtype == dns.TypeA {
|
||||
// Extract service name from query
|
||||
serviceName := d.extractServiceName(question.Name)
|
||||
if serviceName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Resolve service
|
||||
ips, err := d.serviceDiscovery.ResolveService(context.Background(), serviceName, "")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create A records
|
||||
for _, ip := range ips {
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s 30 IN A %s", question.Name, ip))
|
||||
if err == nil {
|
||||
msg.Answer = append(msg.Answer, rr)
|
||||
}
|
||||
}
|
||||
} else if question.Qtype == dns.TypeSRV {
|
||||
// Handle SRV requests for service discovery
|
||||
serviceName := d.extractServiceName(question.Name)
|
||||
if serviceName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get service instances
|
||||
instances, err := d.serviceDiscovery.DiscoverService(context.Background(), serviceName, "")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create SRV records
|
||||
for _, instance := range instances {
|
||||
target := fmt.Sprintf("%s.%s", instance.ServiceName, d.domain)
|
||||
srv := fmt.Sprintf("%s 30 IN SRV 10 5 %d %s", question.Name, instance.Port, target)
|
||||
rr, err := dns.NewRR(srv)
|
||||
if err == nil {
|
||||
msg.Answer = append(msg.Answer, rr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteMsg(msg)
|
||||
}
|
||||
|
||||
// handleReverseRequest handles reverse DNS lookups
|
||||
func (d *DNSServer) handleReverseRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
msg := new(dns.Msg)
|
||||
msg.SetReply(r)
|
||||
msg.Authoritative = true
|
||||
|
||||
for _, question := range r.Question {
|
||||
if question.Qtype == dns.TypePTR {
|
||||
// Extract IP from reverse query
|
||||
ip := d.extractIPFromReverse(question.Name)
|
||||
if ip == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find service by IP
|
||||
var serviceName string
|
||||
for _, instance := range d.serviceDiscovery.services {
|
||||
if instance.IPAddress == ip {
|
||||
serviceName = instance.ServiceName
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if serviceName != "" {
|
||||
ptr := fmt.Sprintf("%s 30 IN PTR %s.%s", question.Name, serviceName, d.domain)
|
||||
rr, err := dns.NewRR(ptr)
|
||||
if err == nil {
|
||||
msg.Answer = append(msg.Answer, rr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteMsg(msg)
|
||||
}
|
||||
|
||||
// extractServiceName extracts service name from DNS query
|
||||
func (d *DNSServer) extractServiceName(query string) string {
|
||||
// Remove domain suffix
|
||||
if strings.HasSuffix(query, d.domain) {
|
||||
name := strings.TrimSuffix(query, d.domain)
|
||||
name = strings.Trim(name, ".")
|
||||
return name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractIPFromReverse extracts IP from reverse DNS query
|
||||
func (d *DNSServer) extractIPFromReverse(reverse string) string {
|
||||
// Handle IPv4 reverse lookup
|
||||
if strings.HasSuffix(reverse, "in-addr.arpa.") {
|
||||
parts := strings.Split(reverse, ".")
|
||||
if len(parts) >= 4 {
|
||||
// Reverse the first 4 parts to get IP
|
||||
ip := fmt.Sprintf("%s.%s.%s.%s", parts[3], parts[2], parts[1], parts[0])
|
||||
return ip
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DNSClient provides DNS resolution utilities
|
||||
type DNSClient struct {
|
||||
servers []string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewDNSClient creates a new DNS client
|
||||
func NewDNSClient(servers []string) *DNSClient {
|
||||
return &DNSClient{
|
||||
servers: servers,
|
||||
timeout: 5 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveService resolves a service name using DNS
|
||||
func (c *DNSClient) ResolveService(serviceName, domain string) ([]string, error) {
|
||||
fqdn := fmt.Sprintf("%s.%s", serviceName, domain)
|
||||
|
||||
// Create DNS message
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(dns.Fqdn(fqdn), dns.TypeA)
|
||||
msg.RecursionDesired = true
|
||||
|
||||
// Try each DNS server
|
||||
for _, server := range c.servers {
|
||||
client := &dns.Client{Timeout: c.timeout}
|
||||
response, _, err := client.Exchange(msg, server+":53")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(response.Answer) > 0 {
|
||||
var ips []string
|
||||
for _, answer := range response.Answer {
|
||||
if a, ok := answer.(*dns.A); ok {
|
||||
ips = append(ips, a.A.String())
|
||||
}
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to resolve service: %s", serviceName)
|
||||
}
|
||||
|
||||
// ResolveSRV resolves SRV records for a service
|
||||
func (c *DNSClient) ResolveSRV(serviceName, domain string) ([]*SRVRecord, error) {
|
||||
fqdn := fmt.Sprintf("_%s._tcp.%s", serviceName, domain)
|
||||
|
||||
// Create DNS message
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(dns.Fqdn(fqdn), dns.TypeSRV)
|
||||
msg.RecursionDesired = true
|
||||
|
||||
// Try each DNS server
|
||||
for _, server := range c.servers {
|
||||
client := &dns.Client{Timeout: c.timeout}
|
||||
response, _, err := client.Exchange(msg, server+":53")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(response.Answer) > 0 {
|
||||
var records []*SRVRecord
|
||||
for _, answer := range response.Answer {
|
||||
if srv, ok := answer.(*dns.SRV); ok {
|
||||
record := &SRVRecord{
|
||||
Priority: srv.Priority,
|
||||
Weight: srv.Weight,
|
||||
Port: srv.Port,
|
||||
Target: srv.Target,
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to resolve SRV record for service: %s", serviceName)
|
||||
}
|
||||
|
||||
// SRVRecord represents an SRV record
|
||||
type SRVRecord struct {
|
||||
Priority uint16
|
||||
Weight uint16
|
||||
Port uint16
|
||||
Target string
|
||||
}
|
||||
|
||||
// NetworkUtils provides network utility functions
|
||||
type NetworkUtils struct{}
|
||||
|
||||
// NewNetworkUtils creates a new network utils instance
|
||||
func NewNetworkUtils() *NetworkUtils {
|
||||
return &NetworkUtils{}
|
||||
}
|
||||
|
||||
// GetLocalIP returns the local IP address
|
||||
func (nu *NetworkUtils) GetLocalIP() (string, error) {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
return ipnet.IP.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no local IP address found")
|
||||
}
|
||||
|
||||
// IsPortOpen checks if a port is open on a host
|
||||
func (nu *NetworkUtils) IsPortOpen(host string, port int, timeout time.Duration) bool {
|
||||
address := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
conn, err := net.DialTimeout("tcp", address, timeout)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// WaitForPort waits for a port to become available
|
||||
func (nu *NetworkUtils) WaitForPort(host string, port int, timeout time.Duration) error {
|
||||
start := time.Now()
|
||||
for time.Since(start) < timeout {
|
||||
if nu.IsPortOpen(host, port, 1*time.Second) {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
return fmt.Errorf("port %d on %s did not become available within %v", port, host, timeout)
|
||||
}
|
||||
|
||||
// GenerateSubnet generates a subnet for a project
|
||||
func (nu *NetworkUtils) GenerateSubnet(projectID string) string {
|
||||
// Simple hash-based subnet generation
|
||||
hash := 0
|
||||
for _, c := range projectID {
|
||||
hash = hash*31 + int(c)
|
||||
}
|
||||
if hash < 0 {
|
||||
hash = -hash
|
||||
}
|
||||
|
||||
// Generate 10.x.y.0/24 subnet
|
||||
octet2 := (hash % 254) + 1
|
||||
octet3 := ((hash / 254) % 254) + 1
|
||||
return fmt.Sprintf("10.%d.%d.0/24", octet2, octet3)
|
||||
}
|
||||
|
||||
// GetAvailablePort finds an available port in a range
|
||||
func (nu *NetworkUtils) GetAvailablePort(start, end int) (int, error) {
|
||||
for port := start; port <= end; port++ {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err == nil {
|
||||
listener.Close()
|
||||
return port, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("no available ports in range %d-%d", start, end)
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
package networking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"containr/internal/deployment"
|
||||
)
|
||||
|
||||
// ServiceDiscovery handles service registration, discovery, and DNS resolution
|
||||
type ServiceDiscovery struct {
|
||||
services map[string]*ServiceInstance
|
||||
instances map[string][]*ServiceInstance
|
||||
mu sync.RWMutex
|
||||
scheduler *deployment.Scheduler
|
||||
dnsDomain string
|
||||
loadBalancer *LoadBalancer
|
||||
}
|
||||
|
||||
// ServiceInstance represents a running instance of a service
|
||||
type ServiceInstance struct {
|
||||
ID string `json:"id"`
|
||||
ServiceID string `json:"service_id"`
|
||||
ServiceName string `json:"service_name"`
|
||||
ProjectID string `json:"project_id"`
|
||||
NodeID string `json:"node_id"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
Port int `json:"port"`
|
||||
Status string `json:"status"`
|
||||
Health HealthStatus `json:"health"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
}
|
||||
|
||||
// HealthStatus represents the health status of a service instance
|
||||
type HealthStatus struct {
|
||||
Status string `json:"status"` // healthy, unhealthy, unknown
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
CheckCount int `json:"check_count"`
|
||||
FailureCount int `json:"failure_count"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ServiceRegistry represents the service registry
|
||||
type ServiceRegistry struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Instances []*ServiceInstance `json:"instances"`
|
||||
Selector map[string]string `json:"selector"`
|
||||
Ports []ServicePort `json:"ports"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ServicePort represents a service port configuration
|
||||
type ServicePort struct {
|
||||
Name string `json:"name"`
|
||||
Port int `json:"port"`
|
||||
TargetPort int `json:"target_port"`
|
||||
Protocol string `json:"protocol"`
|
||||
}
|
||||
|
||||
// LoadBalancer handles load balancing across service instances
|
||||
type LoadBalancer struct {
|
||||
strategy LoadBalancingStrategy
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type LoadBalancingStrategy string
|
||||
|
||||
const (
|
||||
StrategyRoundRobin LoadBalancingStrategy = "round_robin"
|
||||
StrategyLeastConnections LoadBalancingStrategy = "least_connections"
|
||||
StrategyIPHash LoadBalancingStrategy = "ip_hash"
|
||||
StrategyRandom LoadBalancingStrategy = "random"
|
||||
)
|
||||
|
||||
// DNSRecord represents a DNS record for a service
|
||||
type DNSRecord struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // A, SRV, CNAME
|
||||
TTL int `json:"ttl"`
|
||||
Records []string `json:"records"` // IP addresses or hostnames
|
||||
Priority int `json:"priority"` // For SRV records
|
||||
Weight int `json:"weight"` // For SRV records
|
||||
Port int `json:"port"` // For SRV records
|
||||
}
|
||||
|
||||
// NewServiceDiscovery creates a new service discovery instance
|
||||
func NewServiceDiscovery(scheduler *deployment.Scheduler, dnsDomain string) *ServiceDiscovery {
|
||||
return &ServiceDiscovery{
|
||||
services: make(map[string]*ServiceInstance),
|
||||
instances: make(map[string][]*ServiceInstance),
|
||||
scheduler: scheduler,
|
||||
dnsDomain: dnsDomain,
|
||||
loadBalancer: NewLoadBalancer(StrategyRoundRobin),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterService registers a new service instance
|
||||
func (sd *ServiceDiscovery) RegisterService(ctx context.Context, instance *ServiceInstance) error {
|
||||
sd.mu.Lock()
|
||||
defer sd.mu.Unlock()
|
||||
|
||||
// Validate instance
|
||||
if instance.ServiceName == "" || instance.IPAddress == "" {
|
||||
return fmt.Errorf("service name and IP address are required")
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if instance.Status == "" {
|
||||
instance.Status = "starting"
|
||||
}
|
||||
if instance.Health.Status == "" {
|
||||
instance.Health.Status = "unknown"
|
||||
}
|
||||
instance.CreatedAt = time.Now()
|
||||
instance.LastSeen = time.Now()
|
||||
|
||||
// Store instance
|
||||
sd.services[instance.ID] = instance
|
||||
|
||||
// Add to service instances map
|
||||
serviceKey := sd.getServiceKey(instance.ServiceName, instance.ProjectID)
|
||||
sd.instances[serviceKey] = append(sd.instances[serviceKey], instance)
|
||||
|
||||
// Start health checking
|
||||
go sd.startHealthCheck(instance)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnregisterService removes a service instance
|
||||
func (sd *ServiceDiscovery) UnregisterService(ctx context.Context, instanceID string) error {
|
||||
sd.mu.Lock()
|
||||
defer sd.mu.Unlock()
|
||||
|
||||
instance, exists := sd.services[instanceID]
|
||||
if !exists {
|
||||
return fmt.Errorf("service instance not found: %s", instanceID)
|
||||
}
|
||||
|
||||
// Remove from services map
|
||||
delete(sd.services, instanceID)
|
||||
|
||||
// Remove from instances map
|
||||
serviceKey := sd.getServiceKey(instance.ServiceName, instance.ProjectID)
|
||||
instances := sd.instances[serviceKey]
|
||||
for i, inst := range instances {
|
||||
if inst.ID == instanceID {
|
||||
sd.instances[serviceKey] = append(instances[:i], instances[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DiscoverService finds service instances by name and project
|
||||
func (sd *ServiceDiscovery) DiscoverService(ctx context.Context, serviceName, projectID string) ([]*ServiceInstance, error) {
|
||||
sd.mu.RLock()
|
||||
defer sd.mu.RUnlock()
|
||||
|
||||
serviceKey := sd.getServiceKey(serviceName, projectID)
|
||||
instances, exists := sd.instances[serviceKey]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("service not found: %s", serviceName)
|
||||
}
|
||||
|
||||
// Filter healthy instances only
|
||||
var healthyInstances []*ServiceInstance
|
||||
for _, instance := range instances {
|
||||
if instance.Health.Status == "healthy" && instance.Status == "running" {
|
||||
healthyInstances = append(healthyInstances, instance)
|
||||
}
|
||||
}
|
||||
|
||||
if len(healthyInstances) == 0 {
|
||||
return nil, fmt.Errorf("no healthy instances found for service: %s", serviceName)
|
||||
}
|
||||
|
||||
return healthyInstances, nil
|
||||
}
|
||||
|
||||
// GetServiceEndpoints returns all endpoints for a service
|
||||
func (sd *ServiceDiscovery) GetServiceEndpoints(ctx context.Context, serviceName, projectID string) ([]string, error) {
|
||||
instances, err := sd.DiscoverService(ctx, serviceName, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var endpoints []string
|
||||
for _, instance := range instances {
|
||||
endpoint := fmt.Sprintf("%s:%d", instance.IPAddress, instance.Port)
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// ResolveService resolves a service name to IP addresses (DNS-like functionality)
|
||||
func (sd *ServiceDiscovery) ResolveService(ctx context.Context, serviceName, projectID string) ([]string, error) {
|
||||
instances, err := sd.DiscoverService(ctx, serviceName, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ips []string
|
||||
for _, instance := range instances {
|
||||
ips = append(ips, instance.IPAddress)
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
// GetDNSRecords generates DNS records for all services
|
||||
func (sd *ServiceDiscovery) GetDNSRecords(ctx context.Context) ([]*DNSRecord, error) {
|
||||
sd.mu.RLock()
|
||||
defer sd.mu.RUnlock()
|
||||
|
||||
var records []*DNSRecord
|
||||
|
||||
// Group instances by service
|
||||
serviceGroups := make(map[string][]*ServiceInstance)
|
||||
for _, instance := range sd.services {
|
||||
if instance.Health.Status == "healthy" && instance.Status == "running" {
|
||||
serviceGroups[instance.ServiceName] = append(serviceGroups[instance.ServiceName], instance)
|
||||
}
|
||||
}
|
||||
|
||||
// Create A records for each service
|
||||
for serviceName, instances := range serviceGroups {
|
||||
var ips []string
|
||||
for _, instance := range instances {
|
||||
ips = append(ips, instance.IPAddress)
|
||||
}
|
||||
|
||||
if len(ips) > 0 {
|
||||
// Create A record
|
||||
fqdn := fmt.Sprintf("%s.%s", serviceName, sd.dnsDomain)
|
||||
record := &DNSRecord{
|
||||
Name: fqdn,
|
||||
Type: "A",
|
||||
TTL: 30,
|
||||
Records: ips,
|
||||
}
|
||||
records = append(records, record)
|
||||
|
||||
// Create SRV record for services with ports
|
||||
if len(instances) > 0 && instances[0].Port > 0 {
|
||||
srvRecord := &DNSRecord{
|
||||
Name: fmt.Sprintf("_%s._tcp.%s", serviceName, sd.dnsDomain),
|
||||
Type: "SRV",
|
||||
TTL: 30,
|
||||
Port: instances[0].Port,
|
||||
Records: []string{fqdn},
|
||||
Priority: 10,
|
||||
Weight: 5,
|
||||
}
|
||||
records = append(records, srvRecord)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// UpdateServiceHealth updates the health status of a service instance
|
||||
func (sd *ServiceDiscovery) UpdateServiceHealth(ctx context.Context, instanceID string, health HealthStatus) error {
|
||||
sd.mu.Lock()
|
||||
defer sd.mu.Unlock()
|
||||
|
||||
instance, exists := sd.services[instanceID]
|
||||
if !exists {
|
||||
return fmt.Errorf("service instance not found: %s", instanceID)
|
||||
}
|
||||
|
||||
instance.Health = health
|
||||
instance.LastSeen = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServiceStats returns statistics about services
|
||||
func (sd *ServiceDiscovery) GetServiceStats(ctx context.Context) map[string]interface{} {
|
||||
sd.mu.RLock()
|
||||
defer sd.mu.RUnlock()
|
||||
|
||||
totalServices := len(sd.instances)
|
||||
healthyInstances := 0
|
||||
unhealthyInstances := 0
|
||||
|
||||
for _, instance := range sd.services {
|
||||
if instance.Health.Status == "healthy" {
|
||||
healthyInstances++
|
||||
} else if instance.Health.Status == "unhealthy" {
|
||||
unhealthyInstances++
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_services": totalServices,
|
||||
"healthy_instances": healthyInstances,
|
||||
"unhealthy_instances": unhealthyInstances,
|
||||
"dns_domain": sd.dnsDomain,
|
||||
"load_balancer": string(sd.loadBalancer.strategy),
|
||||
}
|
||||
}
|
||||
|
||||
// getServiceKey creates a unique key for a service
|
||||
func (sd *ServiceDiscovery) getServiceKey(serviceName, projectID string) string {
|
||||
return fmt.Sprintf("%s:%s", projectID, serviceName)
|
||||
}
|
||||
|
||||
// startHealthCheck starts periodic health checking for a service instance
|
||||
func (sd *ServiceDiscovery) startHealthCheck(instance *ServiceInstance) {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
healthy := sd.checkInstanceHealth(ctx, instance)
|
||||
|
||||
health := HealthStatus{
|
||||
LastCheck: time.Now(),
|
||||
}
|
||||
|
||||
if healthy {
|
||||
health.Status = "healthy"
|
||||
health.CheckCount++
|
||||
health.FailureCount = 0
|
||||
health.Message = "Health check passed"
|
||||
} else {
|
||||
health.Status = "unhealthy"
|
||||
health.CheckCount++
|
||||
health.FailureCount++
|
||||
health.Message = "Health check failed"
|
||||
}
|
||||
|
||||
sd.UpdateServiceHealth(ctx, instance.ID, health)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkInstanceHealth performs a health check on a service instance
|
||||
func (sd *ServiceDiscovery) checkInstanceHealth(ctx context.Context, instance *ServiceInstance) bool {
|
||||
// Simple TCP connection check
|
||||
if instance.Port > 0 {
|
||||
address := net.JoinHostPort(instance.IPAddress, strconv.Itoa(instance.Port))
|
||||
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// If no port specified, assume healthy
|
||||
return true
|
||||
}
|
||||
|
||||
// NewLoadBalancer creates a new load balancer
|
||||
func NewLoadBalancer(strategy LoadBalancingStrategy) *LoadBalancer {
|
||||
return &LoadBalancer{
|
||||
strategy: strategy,
|
||||
}
|
||||
}
|
||||
|
||||
// SelectInstance selects an instance using the configured load balancing strategy
|
||||
func (lb *LoadBalancer) SelectInstance(instances []*ServiceInstance, clientIP string) *ServiceInstance {
|
||||
lb.mu.RLock()
|
||||
defer lb.mu.RUnlock()
|
||||
|
||||
if len(instances) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch lb.strategy {
|
||||
case StrategyRoundRobin:
|
||||
return lb.roundRobinSelect(instances)
|
||||
case StrategyLeastConnections:
|
||||
return lb.leastConnectionsSelect(instances)
|
||||
case StrategyIPHash:
|
||||
return lb.ipHashSelect(instances, clientIP)
|
||||
case StrategyRandom:
|
||||
return lb.randomSelect(instances)
|
||||
default:
|
||||
return instances[0]
|
||||
}
|
||||
}
|
||||
|
||||
// roundRobinSelect implements round-robin load balancing
|
||||
func (lb *LoadBalancer) roundRobinSelect(instances []*ServiceInstance) *ServiceInstance {
|
||||
// Simple implementation - in production, maintain round-robin state
|
||||
return instances[0]
|
||||
}
|
||||
|
||||
// leastConnectionsSelect selects instance with least connections
|
||||
func (lb *LoadBalancer) leastConnectionsSelect(instances []*ServiceInstance) *ServiceInstance {
|
||||
var selected *ServiceInstance
|
||||
minConnections := int(^uint(0) >> 1) // Max int
|
||||
|
||||
for _, instance := range instances {
|
||||
// In a real implementation, track actual connections
|
||||
connections := 0 // Placeholder
|
||||
if connections < minConnections {
|
||||
selected = instance
|
||||
minConnections = connections
|
||||
}
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
// ipHashSelect selects instance based on client IP hash
|
||||
func (lb *LoadBalancer) ipHashSelect(instances []*ServiceInstance, clientIP string) *ServiceInstance {
|
||||
if clientIP == "" {
|
||||
return instances[0]
|
||||
}
|
||||
|
||||
hash := 0
|
||||
for _, c := range clientIP {
|
||||
hash = hash*31 + int(c)
|
||||
}
|
||||
|
||||
if hash < 0 {
|
||||
hash = -hash
|
||||
}
|
||||
|
||||
index := hash % len(instances)
|
||||
return instances[index]
|
||||
}
|
||||
|
||||
// randomSelect selects a random instance
|
||||
func (lb *LoadBalancer) randomSelect(instances []*ServiceInstance) *ServiceInstance {
|
||||
// Simple implementation - in production, use proper random
|
||||
return instances[0]
|
||||
}
|
||||
@@ -1,441 +0,0 @@
|
||||
package networking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TraefikConfig struct {
|
||||
ConfigDir string
|
||||
AcmeEmail string
|
||||
AcmeCAServer string
|
||||
EntryPoint string
|
||||
CertResolver string
|
||||
DomainSuffix string
|
||||
}
|
||||
|
||||
type TraefikRouter struct {
|
||||
Name string `json:"name"`
|
||||
Rule string `json:"rule"`
|
||||
Service string `json:"service"`
|
||||
EntryPoint string `json:"entryPoints"`
|
||||
Middlewares []string `json:"middlewares,omitempty"`
|
||||
TLS *TLSConfig `json:"tls,omitempty"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
}
|
||||
|
||||
type TraefikService struct {
|
||||
Name string `json:"name"`
|
||||
LoadBalancer *LoadBalancerConfig `json:"loadBalancer"`
|
||||
Weighted *WeightedConfig `json:"weighted,omitempty"`
|
||||
Mirroring *MirroringConfig `json:"mirroring,omitempty"`
|
||||
}
|
||||
|
||||
type LoadBalancerConfig struct {
|
||||
Servers []ServerConfig `json:"servers"`
|
||||
HealthCheck *HealthCheck `json:"healthCheck,omitempty"`
|
||||
Sticky *StickyConfig `json:"sticky,omitempty"`
|
||||
PassHostHeader bool `json:"passHostHeader"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
URL string `json:"url"`
|
||||
Scheme string `json:"scheme,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
}
|
||||
|
||||
type HealthCheck struct {
|
||||
Path string `json:"path"`
|
||||
Interval string `json:"interval"`
|
||||
Timeout string `json:"timeout"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
FollowRedirects bool `json:"followRedirects,omitempty"`
|
||||
}
|
||||
|
||||
type StickyConfig struct {
|
||||
Cookie *CookieConfig `json:"cookie,omitempty"`
|
||||
}
|
||||
|
||||
type CookieConfig struct {
|
||||
Name string `json:"name"`
|
||||
Secure bool `json:"secure"`
|
||||
HTTPOnly bool `json:"httpOnly"`
|
||||
SameSite string `json:"sameSite,omitempty"`
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
CertResolver string `json:"certResolver,omitempty"`
|
||||
Domains []Domain `json:"domains,omitempty"`
|
||||
}
|
||||
|
||||
type Domain struct {
|
||||
Main string `json:"main"`
|
||||
SANS []string `json:"sans,omitempty"`
|
||||
}
|
||||
|
||||
type WeightedConfig struct {
|
||||
Services []WeightedService `json:"services"`
|
||||
}
|
||||
|
||||
type WeightedService struct {
|
||||
Name string `json:"name"`
|
||||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
type MirroringConfig struct {
|
||||
MainService string `json:"mainService"`
|
||||
Mirrors []MirrorService `json:"mirrors"`
|
||||
}
|
||||
|
||||
type MirrorService struct {
|
||||
Name string `json:"name"`
|
||||
Percent int `json:"percent"`
|
||||
}
|
||||
|
||||
type TraefikMiddleware struct {
|
||||
Name string `json:"name"`
|
||||
RateLimit *RateLimitConfig `json:"rateLimit,omitempty"`
|
||||
StripPrefix *StripPrefixConfig `json:"stripPrefix,omitempty"`
|
||||
AddPrefix *AddPrefixConfig `json:"addPrefix,omitempty"`
|
||||
Headers *HeadersConfig `json:"headers,omitempty"`
|
||||
RedirectRegex *RedirectRegexConfig `json:"redirectRegex,omitempty"`
|
||||
RedirectScheme *RedirectSchemeConfig `json:"redirectScheme,omitempty"`
|
||||
Compress *CompressConfig `json:"compress,omitempty"`
|
||||
Auth *AuthConfig `json:"basicAuth,omitempty"`
|
||||
}
|
||||
|
||||
type RateLimitConfig struct {
|
||||
Average int64 `json:"average"`
|
||||
Burst int64 `json:"burst"`
|
||||
Period time.Duration `json:"period"`
|
||||
SourceCriterion *SourceCriterion `json:"sourceCriterion,omitempty"`
|
||||
}
|
||||
|
||||
type SourceCriterion struct {
|
||||
IPStrategy *IPStrategy `json:"ipStrategy,omitempty"`
|
||||
}
|
||||
|
||||
type IPStrategy struct {
|
||||
Depth int `json:"depth"`
|
||||
ExcludedIPs []string `json:"excludedIPs,omitempty"`
|
||||
}
|
||||
|
||||
type StripPrefixConfig struct {
|
||||
Prefixes []string `json:"prefixes"`
|
||||
}
|
||||
|
||||
type AddPrefixConfig struct {
|
||||
Prefix string `json:"prefix"`
|
||||
}
|
||||
|
||||
type HeadersConfig struct {
|
||||
CustomRequestHeaders map[string]string `json:"customRequestHeaders,omitempty"`
|
||||
CustomResponseHeaders map[string]string `json:"customResponseHeaders,omitempty"`
|
||||
AccessControlAllowMethods []string `json:"accessControlAllowMethods,omitempty"`
|
||||
AccessControlAllowHeaders []string `json:"accessControlAllowHeaders,omitempty"`
|
||||
AccessControlAllowOriginList []string `json:"accessControlAllowOriginList,omitempty"`
|
||||
SSLRedirect bool `json:"sslRedirect,omitempty"`
|
||||
SSLProxyHeaders map[string]string `json:"sslProxyHeaders,omitempty"`
|
||||
}
|
||||
|
||||
type RedirectRegexConfig struct {
|
||||
Regex string `json:"regex"`
|
||||
Replacement string `json:"replacement"`
|
||||
Permanent bool `json:"permanent"`
|
||||
}
|
||||
|
||||
type RedirectSchemeConfig struct {
|
||||
Scheme string `json:"scheme"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Permanent bool `json:"permanent"`
|
||||
}
|
||||
|
||||
type CompressConfig struct {
|
||||
MinResponseBodyBytes int `json:"minResponseBodyBytes"`
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
Users []string `json:"users"`
|
||||
UsersFile string `json:"usersFile,omitempty"`
|
||||
}
|
||||
|
||||
type TraefikManager struct {
|
||||
config *TraefikConfig
|
||||
sd *ServiceDiscovery
|
||||
routers map[string]*TraefikRouter
|
||||
services map[string]*TraefikService
|
||||
middlewares map[string]*TraefikMiddleware
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewTraefikManager(config *TraefikConfig, sd *ServiceDiscovery) *TraefikManager {
|
||||
if config.EntryPoint == "" {
|
||||
config.EntryPoint = "websecure"
|
||||
}
|
||||
if config.CertResolver == "" {
|
||||
config.CertResolver = "letsencrypt"
|
||||
}
|
||||
if config.DomainSuffix == "" {
|
||||
config.DomainSuffix = "containr.local"
|
||||
}
|
||||
|
||||
if config.ConfigDir != "" {
|
||||
os.MkdirAll(config.ConfigDir, 0755)
|
||||
}
|
||||
|
||||
return &TraefikManager{
|
||||
config: config,
|
||||
sd: sd,
|
||||
routers: make(map[string]*TraefikRouter),
|
||||
services: make(map[string]*TraefikService),
|
||||
middlewares: make(map[string]*TraefikMiddleware),
|
||||
}
|
||||
}
|
||||
|
||||
type ServiceRouteConfig struct {
|
||||
ServiceName string
|
||||
ProjectID string
|
||||
Port int
|
||||
Domain string
|
||||
PathPrefix string
|
||||
EnableTLS bool
|
||||
EnableAuth bool
|
||||
AuthUsers []string
|
||||
RateLimit *RateLimitConfig
|
||||
HealthPath string
|
||||
StickySession bool
|
||||
Priority int
|
||||
}
|
||||
|
||||
func (tm *TraefikManager) CreateServiceRoute(ctx context.Context, config *ServiceRouteConfig) error {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
serviceName := fmt.Sprintf("%s-%s", config.ProjectID, config.ServiceName)
|
||||
routerName := fmt.Sprintf("%s-router", serviceName)
|
||||
|
||||
if config.Domain == "" {
|
||||
config.Domain = fmt.Sprintf("%s.%s", serviceName, tm.config.DomainSuffix)
|
||||
}
|
||||
|
||||
var servers []ServerConfig
|
||||
if tm.sd != nil {
|
||||
instances, err := tm.sd.DiscoverService(ctx, config.ServiceName, config.ProjectID)
|
||||
if err == nil {
|
||||
for _, instance := range instances {
|
||||
servers = append(servers, ServerConfig{
|
||||
URL: fmt.Sprintf("http://%s:%d", instance.IPAddress, config.Port),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(servers) == 0 {
|
||||
servers = append(servers, ServerConfig{
|
||||
URL: fmt.Sprintf("http://%s:%d", serviceName, config.Port),
|
||||
})
|
||||
}
|
||||
|
||||
lbConfig := &LoadBalancerConfig{
|
||||
Servers: servers,
|
||||
PassHostHeader: true,
|
||||
}
|
||||
|
||||
if config.HealthPath != "" {
|
||||
lbConfig.HealthCheck = &HealthCheck{
|
||||
Path: config.HealthPath,
|
||||
Interval: "30s",
|
||||
Timeout: "5s",
|
||||
}
|
||||
}
|
||||
|
||||
if config.StickySession {
|
||||
lbConfig.Sticky = &StickyConfig{
|
||||
Cookie: &CookieConfig{
|
||||
Name: fmt.Sprintf("%s_sticky", serviceName),
|
||||
Secure: true,
|
||||
HTTPOnly: true,
|
||||
SameSite: "None",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
service := &TraefikService{
|
||||
Name: serviceName,
|
||||
LoadBalancer: lbConfig,
|
||||
}
|
||||
tm.services[serviceName] = service
|
||||
|
||||
rule := fmt.Sprintf("Host(`%s`)", config.Domain)
|
||||
if config.PathPrefix != "" {
|
||||
rule = fmt.Sprintf("%s && PathPrefix(`%s`)", rule, config.PathPrefix)
|
||||
}
|
||||
|
||||
router := &TraefikRouter{
|
||||
Name: routerName,
|
||||
Rule: rule,
|
||||
Service: serviceName,
|
||||
EntryPoint: tm.config.EntryPoint,
|
||||
Priority: config.Priority,
|
||||
}
|
||||
|
||||
var middlewares []string
|
||||
|
||||
if config.RateLimit != nil {
|
||||
mwName := fmt.Sprintf("%s-ratelimit", serviceName)
|
||||
tm.middlewares[mwName] = &TraefikMiddleware{
|
||||
Name: mwName,
|
||||
RateLimit: config.RateLimit,
|
||||
}
|
||||
middlewares = append(middlewares, mwName)
|
||||
}
|
||||
|
||||
if config.EnableAuth && len(config.AuthUsers) > 0 {
|
||||
mwName := fmt.Sprintf("%s-auth", serviceName)
|
||||
tm.middlewares[mwName] = &TraefikMiddleware{
|
||||
Name: "auth",
|
||||
Auth: &AuthConfig{
|
||||
Users: config.AuthUsers,
|
||||
},
|
||||
}
|
||||
middlewares = append(middlewares, mwName)
|
||||
}
|
||||
|
||||
if len(middlewares) > 0 {
|
||||
router.Middlewares = middlewares
|
||||
}
|
||||
|
||||
if config.EnableTLS {
|
||||
router.TLS = &TLSConfig{
|
||||
CertResolver: tm.config.CertResolver,
|
||||
Domains: []Domain{
|
||||
{Main: config.Domain},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tm.routers[routerName] = router
|
||||
|
||||
if tm.config.ConfigDir != "" {
|
||||
if err := tm.writeDynamicConfig(); err != nil {
|
||||
return fmt.Errorf("failed to write traefik config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Created Traefik route for service %s at %s", serviceName, config.Domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tm *TraefikManager) RemoveServiceRoute(ctx context.Context, serviceName, projectID string) error {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
serviceKey := fmt.Sprintf("%s-%s", projectID, serviceName)
|
||||
routerName := fmt.Sprintf("%s-router", serviceKey)
|
||||
|
||||
delete(tm.services, serviceKey)
|
||||
delete(tm.routers, routerName)
|
||||
|
||||
delete(tm.middlewares, fmt.Sprintf("%s-ratelimit", serviceKey))
|
||||
delete(tm.middlewares, fmt.Sprintf("%s-auth", serviceKey))
|
||||
|
||||
if tm.config.ConfigDir != "" {
|
||||
if err := tm.writeDynamicConfig(); err != nil {
|
||||
return fmt.Errorf("failed to write traefik config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Removed Traefik route for service %s", serviceKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tm *TraefikManager) UpdateServiceServers(ctx context.Context, serviceName, projectID string) error {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
serviceKey := fmt.Sprintf("%s-%s", projectID, serviceName)
|
||||
service, exists := tm.services[serviceKey]
|
||||
if !exists {
|
||||
return fmt.Errorf("service not found: %s", serviceKey)
|
||||
}
|
||||
|
||||
if tm.sd == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
instances, err := tm.sd.DiscoverService(ctx, serviceName, projectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var servers []ServerConfig
|
||||
for _, instance := range instances {
|
||||
servers = append(servers, ServerConfig{
|
||||
URL: fmt.Sprintf("http://%s:%d", instance.IPAddress, instance.Port),
|
||||
})
|
||||
}
|
||||
|
||||
if len(servers) > 0 {
|
||||
service.LoadBalancer.Servers = servers
|
||||
}
|
||||
|
||||
if tm.config.ConfigDir != "" {
|
||||
if err := tm.writeDynamicConfig(); err != nil {
|
||||
return fmt.Errorf("failed to write traefik config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tm *TraefikManager) writeDynamicConfig() error {
|
||||
configPath := filepath.Join(tm.config.ConfigDir, "dynamic.yaml")
|
||||
|
||||
config := map[string]interface{}{
|
||||
"http": map[string]interface{}{
|
||||
"routers": tm.routers,
|
||||
"services": tm.services,
|
||||
"middlewares": tm.middlewares,
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(configPath, data, 0644)
|
||||
}
|
||||
|
||||
func (tm *TraefikManager) GetRoutes() []*TraefikRouter {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
routes := make([]*TraefikRouter, 0, len(tm.routers))
|
||||
for _, router := range tm.routers {
|
||||
routes = append(routes, router)
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
func (tm *TraefikManager) GetServices() []*TraefikService {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
services := make([]*TraefikService, 0, len(tm.services))
|
||||
for _, service := range tm.services {
|
||||
services = append(services, service)
|
||||
}
|
||||
return services
|
||||
}
|
||||
|
||||
func (tm *TraefikManager) GenerateDomain(serviceName, projectID string) string {
|
||||
return fmt.Sprintf("%s-%s.%s", projectID, serviceName, tm.config.DomainSuffix)
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client represents a Proxmox API client
|
||||
type Client struct {
|
||||
baseURL string
|
||||
username string
|
||||
password string
|
||||
tokenID string
|
||||
token string
|
||||
httpClient *http.Client
|
||||
ticket string
|
||||
csrfToken string
|
||||
}
|
||||
|
||||
// NewClient creates a new Proxmox API client
|
||||
func NewClient(baseURL, username, password string) *Client {
|
||||
return &Client{
|
||||
baseURL: strings.TrimSuffix(baseURL, "/"),
|
||||
username: username,
|
||||
password: password,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true, // Proxmox typically uses self-signed certs
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientWithToken creates a new Proxmox API client using API token
|
||||
func NewClientWithToken(baseURL, tokenID, token string) *Client {
|
||||
return &Client{
|
||||
baseURL: strings.TrimSuffix(baseURL, "/"),
|
||||
tokenID: tokenID,
|
||||
token: token,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Login authenticates with Proxmox and stores session tokens
|
||||
func (c *Client) Login() error {
|
||||
data := url.Values{}
|
||||
data.Set("username", c.username)
|
||||
data.Set("password", c.password)
|
||||
|
||||
resp, err := c.httpClient.PostForm(c.baseURL+"/api2/json/access/ticket", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to login: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("login failed with status: %s", resp.Status)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Data struct {
|
||||
Ticket string `json:"ticket"`
|
||||
CSRFPreventionToken string `json:"CSRFPreventionToken"`
|
||||
Username string `json:"username"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return fmt.Errorf("failed to decode login response: %w", err)
|
||||
}
|
||||
|
||||
c.ticket = result.Data.Ticket
|
||||
c.csrfToken = result.Data.CSRFPreventionToken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeRequest makes an authenticated request to Proxmox API
|
||||
func (c *Client) makeRequest(method, path string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequest(method, c.baseURL+path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use token authentication if available
|
||||
if c.tokenID != "" && c.token != "" {
|
||||
req.Header.Set("Authorization", "PVEAPIToken="+c.tokenID+"="+c.token)
|
||||
} else {
|
||||
// Use session ticket authentication
|
||||
if c.ticket == "" {
|
||||
if err := c.Login(); err != nil {
|
||||
return nil, fmt.Errorf("failed to authenticate: %w", err)
|
||||
}
|
||||
}
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: "PVEAuthCookie",
|
||||
Value: c.ticket,
|
||||
})
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Add CSRF token for state-changing requests
|
||||
if method != "GET" && method != "HEAD" && c.csrfToken != "" {
|
||||
req.Header.Set("CSRFPreventionToken", c.csrfToken)
|
||||
}
|
||||
|
||||
return c.httpClient.Do(req)
|
||||
}
|
||||
|
||||
// GetNodes retrieves all nodes in the Proxmox cluster
|
||||
func (c *Client) GetNodes() ([]Node, error) {
|
||||
resp, err := c.makeRequest("GET", "/api2/json/nodes", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Data []Node `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode nodes response: %w", err)
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// GetVMs retrieves all VMs/LXCs on a specific node
|
||||
func (c *Client) GetVMs(node string) ([]VM, error) {
|
||||
resp, err := c.makeRequest("GET", fmt.Sprintf("/api2/json/nodes/%s/qemu", node), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Data []VM `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode VMs response: %w", err)
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// GetContainers retrieves all LXC containers on a specific node
|
||||
func (c *Client) GetContainers(node string) ([]Container, error) {
|
||||
resp, err := c.makeRequest("GET", fmt.Sprintf("/api2/json/nodes/%s/lxc", node), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Data []Container `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode containers response: %w", err)
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// CreateVM creates a new VM on the specified node
|
||||
func (c *Client) CreateVM(node string, config VMConfig) (string, error) {
|
||||
data := url.Values{}
|
||||
|
||||
// Basic VM configuration
|
||||
data.Set("vmid", fmt.Sprintf("%d", config.VMID))
|
||||
data.Set("name", config.Name)
|
||||
data.Set("memory", fmt.Sprintf("%d", config.Memory))
|
||||
data.Set("cores", fmt.Sprintf("%d", config.Cores))
|
||||
|
||||
if config.Template != "" {
|
||||
data.Set("template", config.Template)
|
||||
}
|
||||
|
||||
if config.Storage != "" {
|
||||
data.Set("scsi0", fmt.Sprintf("%s:%d", config.Storage, config.DiskSize))
|
||||
}
|
||||
|
||||
if config.NetworkBridge != "" {
|
||||
data.Set("net0", fmt.Sprintf("model=virtio,bridge=%s", config.NetworkBridge))
|
||||
}
|
||||
|
||||
resp, err := c.makeRequest("POST", fmt.Sprintf("/api2/json/nodes/%s/qemu", node), strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("failed to create VM: %s - %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("failed to decode create VM response: %w", err)
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// CreateContainer creates a new LXC container on the specified node
|
||||
func (c *Client) CreateContainer(node string, config ContainerConfig) (string, error) {
|
||||
data := url.Values{}
|
||||
|
||||
// Basic container configuration
|
||||
data.Set("vmid", fmt.Sprintf("%d", config.VMID))
|
||||
data.Set("hostname", config.Hostname)
|
||||
data.Set("memory", fmt.Sprintf("%d", config.Memory))
|
||||
data.Set("cores", fmt.Sprintf("%d", config.Cores))
|
||||
|
||||
if config.Template != "" {
|
||||
data.Set("ostemplate", config.Template)
|
||||
}
|
||||
|
||||
if config.Storage != "" {
|
||||
data.Set("rootfs", fmt.Sprintf("%s:%d", config.Storage, config.DiskSize))
|
||||
}
|
||||
|
||||
if config.NetworkBridge != "" {
|
||||
data.Set("net0", fmt.Sprintf("name=eth0,bridge=%s", config.NetworkBridge))
|
||||
}
|
||||
|
||||
resp, err := c.makeRequest("POST", fmt.Sprintf("/api2/json/nodes/%s/lxc", node), strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return "", fmt.Errorf("failed to create container: %s - %s", resp.Status, string(body))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("failed to decode create container response: %w", err)
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
// StartVM starts a VM
|
||||
func (c *Client) StartVM(node string, vmid int) error {
|
||||
resp, err := c.makeRequest("POST", fmt.Sprintf("/api2/json/nodes/%s/qemu/%d/status/start", node, vmid), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to start VM: %s", resp.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopVM stops a VM
|
||||
func (c *Client) StopVM(node string, vmid int) error {
|
||||
resp, err := c.makeRequest("POST", fmt.Sprintf("/api2/json/nodes/%s/qemu/%d/status/stop", node, vmid), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to stop VM: %s", resp.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteVM deletes a VM
|
||||
func (c *Client) DeleteVM(node string, vmid int) error {
|
||||
resp, err := c.makeRequest("DELETE", fmt.Sprintf("/api2/json/nodes/%s/qemu/%d", node, vmid), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to delete VM: %s", resp.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVMStatus retrieves the status of a VM
|
||||
func (c *Client) GetVMStatus(node string, vmid int) (*VMStatus, error) {
|
||||
resp, err := c.makeRequest("GET", fmt.Sprintf("/api2/json/nodes/%s/qemu/%d/status/current", node, vmid), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Data VMStatus `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode VM status response: %w", err)
|
||||
}
|
||||
|
||||
return &result.Data, nil
|
||||
}
|
||||
@@ -1,456 +0,0 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Service manages Proxmox operations
|
||||
type Service struct {
|
||||
client *Client
|
||||
nodeCache map[string]*NodeStats
|
||||
cacheMu sync.RWMutex
|
||||
config Config
|
||||
}
|
||||
|
||||
// Config holds Proxmox configuration
|
||||
type Config struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
TokenID string `json:"token_id"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// NewService creates a new Proxmox service
|
||||
func NewService(config Config) *Service {
|
||||
var client *Client
|
||||
|
||||
if config.TokenID != "" && config.Token != "" {
|
||||
client = NewClientWithToken(config.BaseURL, config.TokenID, config.Token)
|
||||
} else {
|
||||
client = NewClient(config.BaseURL, config.Username, config.Password)
|
||||
}
|
||||
|
||||
return &Service{
|
||||
client: client,
|
||||
nodeCache: make(map[string]*NodeStats),
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// GetClusterStatus returns the overall cluster status
|
||||
func (s *Service) GetClusterStatus() (*ClusterInfo, error) {
|
||||
// This would require additional API endpoints for cluster info
|
||||
// For now, return basic cluster information
|
||||
nodes, err := s.client.GetNodes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get cluster nodes: %w", err)
|
||||
}
|
||||
|
||||
activeNodes := 0
|
||||
for _, node := range nodes {
|
||||
if node.Status == "online" {
|
||||
activeNodes++
|
||||
}
|
||||
}
|
||||
|
||||
return &ClusterInfo{
|
||||
Name: "containr-cluster",
|
||||
Version: "7.x", // This should be dynamically retrieved
|
||||
Nodes: len(nodes),
|
||||
Quorate: activeNodes > 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAllNodes returns all nodes with their current status
|
||||
func (s *Service) GetAllNodes() ([]Node, error) {
|
||||
return s.client.GetNodes()
|
||||
}
|
||||
|
||||
// GetNodeStats returns detailed statistics for a specific node
|
||||
func (s *Service) GetNodeStats(nodeName string) (*NodeStats, error) {
|
||||
s.cacheMu.RLock()
|
||||
if stats, exists := s.nodeCache[nodeName]; exists {
|
||||
s.cacheMu.RUnlock()
|
||||
return stats, nil
|
||||
}
|
||||
s.cacheMu.RUnlock()
|
||||
|
||||
// Fetch fresh data
|
||||
nodes, err := s.client.GetNodes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get nodes: %w", err)
|
||||
}
|
||||
|
||||
var targetNode *Node
|
||||
for _, node := range nodes {
|
||||
if node.Node == nodeName {
|
||||
targetNode = &node
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetNode == nil {
|
||||
return nil, fmt.Errorf("node %s not found", nodeName)
|
||||
}
|
||||
|
||||
stats := &NodeStats{
|
||||
Node: targetNode.Node,
|
||||
Status: targetNode.Status,
|
||||
CPU: targetNode.CPU,
|
||||
MemoryTotal: targetNode.MaxMemory,
|
||||
MemoryUsed: targetNode.MemoryUsed,
|
||||
MemoryFree: targetNode.MaxMemory - targetNode.MemoryUsed,
|
||||
DiskTotal: targetNode.MaxDisk,
|
||||
DiskUsed: targetNode.DiskUsed,
|
||||
DiskFree: targetNode.MaxDisk - targetNode.DiskUsed,
|
||||
Uptime: targetNode.Uptime,
|
||||
LastUpdate: time.Now(),
|
||||
}
|
||||
|
||||
// Update cache
|
||||
s.cacheMu.Lock()
|
||||
s.nodeCache[nodeName] = stats
|
||||
s.cacheMu.Unlock()
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetAllVMs returns all VMs across all nodes
|
||||
func (s *Service) GetAllVMs() ([]VM, error) {
|
||||
nodes, err := s.client.GetNodes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get nodes: %w", err)
|
||||
}
|
||||
|
||||
var allVMs []VM
|
||||
for _, node := range nodes {
|
||||
if node.Status == "online" {
|
||||
vms, err := s.client.GetVMs(node.Node)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get VMs for node %s: %v", node.Node, err)
|
||||
continue
|
||||
}
|
||||
allVMs = append(allVMs, vms...)
|
||||
}
|
||||
}
|
||||
|
||||
return allVMs, nil
|
||||
}
|
||||
|
||||
// GetAllContainers returns all containers across all nodes
|
||||
func (s *Service) GetAllContainers() ([]Container, error) {
|
||||
nodes, err := s.client.GetNodes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get nodes: %w", err)
|
||||
}
|
||||
|
||||
var allContainers []Container
|
||||
for _, node := range nodes {
|
||||
if node.Status == "online" {
|
||||
containers, err := s.client.GetContainers(node.Node)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get containers for node %s: %v", node.Node, err)
|
||||
continue
|
||||
}
|
||||
allContainers = append(allContainers, containers...)
|
||||
}
|
||||
}
|
||||
|
||||
return allContainers, nil
|
||||
}
|
||||
|
||||
// CreateServiceVM creates a new VM optimized for running services
|
||||
func (s *Service) CreateServiceVM(nodeName string, config ServiceVMConfig) (*VM, error) {
|
||||
// Find the next available VMID
|
||||
vmid, err := s.getNextAvailableVMID(nodeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get next VMID: %w", err)
|
||||
}
|
||||
|
||||
vmConfig := VMConfig{
|
||||
VMID: vmid,
|
||||
Name: config.Name,
|
||||
Memory: config.Memory,
|
||||
Cores: config.Cores,
|
||||
DiskSize: config.DiskSize,
|
||||
Storage: config.Storage,
|
||||
NetworkBridge: config.NetworkBridge,
|
||||
Template: config.Template,
|
||||
}
|
||||
|
||||
taskID, err := s.client.CreateVM(nodeName, vmConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create VM: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("VM creation started with task ID: %s", taskID)
|
||||
|
||||
// Wait for VM to be created and get its status
|
||||
time.Sleep(5 * time.Second) // Give Proxmox time to process
|
||||
|
||||
vms, err := s.client.GetVMs(nodeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get VM status after creation: %w", err)
|
||||
}
|
||||
|
||||
for _, vm := range vms {
|
||||
if vm.VMID == vmid {
|
||||
return &vm, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("VM %d not found after creation", vmid)
|
||||
}
|
||||
|
||||
// CreateServiceContainer creates a new LXC container optimized for running services
|
||||
func (s *Service) CreateServiceContainer(nodeName string, config ServiceContainerConfig) (*Container, error) {
|
||||
// Find the next available VMID
|
||||
vmid, err := s.getNextAvailableVMID(nodeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get next VMID: %w", err)
|
||||
}
|
||||
|
||||
containerConfig := ContainerConfig{
|
||||
VMID: vmid,
|
||||
Hostname: config.Hostname,
|
||||
Memory: config.Memory,
|
||||
Cores: config.Cores,
|
||||
DiskSize: config.DiskSize,
|
||||
Storage: config.Storage,
|
||||
NetworkBridge: config.NetworkBridge,
|
||||
Template: config.Template,
|
||||
}
|
||||
|
||||
taskID, err := s.client.CreateContainer(nodeName, containerConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create container: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Container creation started with task ID: %s", taskID)
|
||||
|
||||
// Wait for container to be created and get its status
|
||||
time.Sleep(5 * time.Second) // Give Proxmox time to process
|
||||
|
||||
containers, err := s.client.GetContainers(nodeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get container status after creation: %w", err)
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
if container.VMID == vmid {
|
||||
return &container, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Container %d not found after creation", vmid)
|
||||
}
|
||||
|
||||
// StartInstance starts a VM or container
|
||||
func (s *Service) StartInstance(nodeName string, vmid int, instanceType string) error {
|
||||
switch instanceType {
|
||||
case "qemu":
|
||||
return s.client.StartVM(nodeName, vmid)
|
||||
case "lxc":
|
||||
// Implement container start
|
||||
return fmt.Errorf("container start not yet implemented")
|
||||
default:
|
||||
return fmt.Errorf("unknown instance type: %s", instanceType)
|
||||
}
|
||||
}
|
||||
|
||||
// StopInstance stops a VM or container
|
||||
func (s *Service) StopInstance(nodeName string, vmid int, instanceType string) error {
|
||||
switch instanceType {
|
||||
case "qemu":
|
||||
return s.client.StopVM(nodeName, vmid)
|
||||
case "lxc":
|
||||
// Implement container stop
|
||||
return fmt.Errorf("container stop not yet implemented")
|
||||
default:
|
||||
return fmt.Errorf("unknown instance type: %s", instanceType)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteInstance deletes a VM or container
|
||||
func (s *Service) DeleteInstance(nodeName string, vmid int, instanceType string) error {
|
||||
switch instanceType {
|
||||
case "qemu":
|
||||
return s.client.DeleteVM(nodeName, vmid)
|
||||
case "lxc":
|
||||
// Implement container delete
|
||||
return fmt.Errorf("container delete not yet implemented")
|
||||
default:
|
||||
return fmt.Errorf("unknown instance type: %s", instanceType)
|
||||
}
|
||||
}
|
||||
|
||||
// GetInstanceStatus returns the status of a VM or container
|
||||
func (s *Service) GetInstanceStatus(nodeName string, vmid int, instanceType string) (interface{}, error) {
|
||||
switch instanceType {
|
||||
case "qemu":
|
||||
return s.client.GetVMStatus(nodeName, vmid)
|
||||
case "lxc":
|
||||
// Implement container status
|
||||
return nil, fmt.Errorf("container status not yet implemented")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown instance type: %s", instanceType)
|
||||
}
|
||||
}
|
||||
|
||||
// getNextAvailableVMID finds the next available VM ID on the specified node
|
||||
func (s *Service) getNextAvailableVMID(nodeName string) (int, error) {
|
||||
vms, err := s.client.GetVMs(nodeName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
containers, err := s.client.GetContainers(nodeName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
usedIDs := make(map[int]bool)
|
||||
for _, vm := range vms {
|
||||
usedIDs[vm.VMID] = true
|
||||
}
|
||||
for _, container := range containers {
|
||||
usedIDs[container.VMID] = true
|
||||
}
|
||||
|
||||
// Start from 1000 and find the first available ID
|
||||
for vmid := 1000; vmid < 9999; vmid++ {
|
||||
if !usedIDs[vmid] {
|
||||
return vmid, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("no available VM IDs found")
|
||||
}
|
||||
|
||||
// ServiceVMConfig represents configuration for creating a service VM
|
||||
type ServiceVMConfig struct {
|
||||
Name string `json:"name"`
|
||||
Memory int `json:"memory"`
|
||||
Cores int `json:"cores"`
|
||||
DiskSize int `json:"disk_size"` // in GB
|
||||
Storage string `json:"storage"`
|
||||
NetworkBridge string `json:"network_bridge"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
// ServiceContainerConfig represents configuration for creating a service container
|
||||
type ServiceContainerConfig struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Memory int `json:"memory"`
|
||||
Cores int `json:"cores"`
|
||||
DiskSize int `json:"disk_size"` // in GB
|
||||
Storage string `json:"storage"`
|
||||
NetworkBridge string `json:"network_bridge"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
// GetResourceUsage returns resource usage across the cluster
|
||||
func (s *Service) GetResourceUsage() (map[string]interface{}, error) {
|
||||
nodes, err := s.client.GetNodes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get nodes: %w", err)
|
||||
}
|
||||
|
||||
var totalCPU, usedCPU float64
|
||||
var totalMemory, usedMemory, totalDisk, usedDisk int64
|
||||
var onlineNodes int
|
||||
|
||||
for _, node := range nodes {
|
||||
if node.Status == "online" {
|
||||
onlineNodes++
|
||||
totalCPU += 1.0 // Assuming 1 CPU per node for simplicity
|
||||
usedCPU += node.CPU
|
||||
totalMemory += int64(node.MaxMemory)
|
||||
usedMemory += int64(node.MemoryUsed)
|
||||
totalDisk += int64(node.MaxDisk)
|
||||
usedDisk += int64(node.DiskUsed)
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_nodes": len(nodes),
|
||||
"online_nodes": onlineNodes,
|
||||
"cpu_usage": map[string]interface{}{
|
||||
"total": totalCPU,
|
||||
"used": usedCPU,
|
||||
"free": totalCPU - usedCPU,
|
||||
},
|
||||
"memory_usage": map[string]interface{}{
|
||||
"total": totalMemory,
|
||||
"used": usedMemory,
|
||||
"free": totalMemory - usedMemory,
|
||||
},
|
||||
"disk_usage": map[string]interface{}{
|
||||
"total": totalDisk,
|
||||
"used": usedDisk,
|
||||
"free": totalDisk - usedDisk,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateConnection tests the connection to Proxmox
|
||||
func (s *Service) ValidateConnection() error {
|
||||
_, err := s.client.GetNodes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to Proxmox: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAvailableTemplates returns a list of available VM and container templates
|
||||
func (s *Service) GetAvailableTemplates(nodeName string) (map[string]interface{}, error) {
|
||||
vms, err := s.client.GetVMs(nodeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get VMs: %w", err)
|
||||
}
|
||||
|
||||
containers, err := s.client.GetContainers(nodeName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get containers: %w", err)
|
||||
}
|
||||
|
||||
var vmTemplates []VMTemplate
|
||||
for _, vm := range vms {
|
||||
if vm.Template {
|
||||
vmTemplates = append(vmTemplates, VMTemplate{
|
||||
VMID: vm.VMID,
|
||||
Name: vm.Name,
|
||||
Node: vm.Node,
|
||||
Storage: "local", // This should be dynamically retrieved
|
||||
CPU: 2, // Default values
|
||||
Memory: 2048,
|
||||
DiskSize: 20,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var containerTemplates []ContainerTemplate
|
||||
for _, container := range containers {
|
||||
if container.Template {
|
||||
containerTemplates = append(containerTemplates, ContainerTemplate{
|
||||
VMID: container.VMID,
|
||||
Name: container.Name,
|
||||
Node: container.Node,
|
||||
Storage: "local", // This should be dynamically retrieved
|
||||
CPU: 1,
|
||||
Memory: 512,
|
||||
DiskSize: 8,
|
||||
OSTemplate: "ubuntu-22.04-standard", // Default template
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"vm_templates": vmTemplates,
|
||||
"container_templates": containerTemplates,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
package proxmox
|
||||
|
||||
import "time"
|
||||
|
||||
// Node represents a Proxmox cluster node
|
||||
type Node struct {
|
||||
Node string `json:"node"`
|
||||
Status string `json:"status"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Memory int `json:"-"`
|
||||
MemoryUsed int `json:"mem"`
|
||||
MaxMemory int `json:"maxmem"`
|
||||
Disk int `json:"disk"`
|
||||
DiskUsed int `json:"diskused"`
|
||||
MaxDisk int `json:"maxdisk"`
|
||||
Uptime int `json:"uptime"`
|
||||
Level string `json:"level"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// VM represents a virtual machine in Proxmox
|
||||
type VM struct {
|
||||
VMID int `json:"vmid"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Memory int `json:"mem"`
|
||||
MemoryUsed int `json:"maxmem"`
|
||||
Disk int `json:"disk"`
|
||||
DiskUsed int `json:"maxdisk"`
|
||||
Uptime int `json:"uptime"`
|
||||
Template bool `json:"template"`
|
||||
Node string `json:"node"`
|
||||
Type string `json:"type"`
|
||||
NetIn int64 `json:"netin"`
|
||||
NetOut int64 `json:"netout"`
|
||||
DiskRead int64 `json:"diskread"`
|
||||
DiskWrite int64 `json:"diskwrite"`
|
||||
CPUUsage float64 `json:"cpuusage"`
|
||||
}
|
||||
|
||||
// Container represents an LXC container in Proxmox
|
||||
type Container struct {
|
||||
VMID int `json:"vmid"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Memory int `json:"mem"`
|
||||
MemoryUsed int `json:"maxmem"`
|
||||
Disk int `json:"disk"`
|
||||
DiskUsed int `json:"maxdisk"`
|
||||
Uptime int `json:"uptime"`
|
||||
Template bool `json:"template"`
|
||||
Node string `json:"node"`
|
||||
Type string `json:"type"`
|
||||
NetIn int64 `json:"netin"`
|
||||
NetOut int64 `json:"netout"`
|
||||
DiskRead int64 `json:"diskread"`
|
||||
DiskWrite int64 `json:"diskwrite"`
|
||||
CPUUsage float64 `json:"cpuusage"`
|
||||
}
|
||||
|
||||
// VMConfig represents the configuration for creating a VM
|
||||
type VMConfig struct {
|
||||
VMID int `json:"vmid"`
|
||||
Name string `json:"name"`
|
||||
Memory int `json:"memory"`
|
||||
Cores int `json:"cores"`
|
||||
DiskSize int `json:"disk_size"` // in GB
|
||||
Storage string `json:"storage"`
|
||||
NetworkBridge string `json:"network_bridge"`
|
||||
Template string `json:"template,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerConfig represents the configuration for creating an LXC container
|
||||
type ContainerConfig struct {
|
||||
VMID int `json:"vmid"`
|
||||
Hostname string `json:"hostname"`
|
||||
Memory int `json:"memory"`
|
||||
Cores int `json:"cores"`
|
||||
DiskSize int `json:"disk_size"` // in GB
|
||||
Storage string `json:"storage"`
|
||||
NetworkBridge string `json:"network_bridge"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
|
||||
// VMStatus represents the current status of a VM
|
||||
type VMStatus struct {
|
||||
VMID int `json:"vmid"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Memory int `json:"mem"`
|
||||
MemoryUsed int `json:"maxmem"`
|
||||
Disk int `json:"disk"`
|
||||
DiskUsed int `json:"maxdisk"`
|
||||
Uptime int `json:"uptime"`
|
||||
Lock string `json:"lock,omitempty"`
|
||||
HA bool `json:"ha"`
|
||||
QMPStatus string `json:"qmpstatus"`
|
||||
Spice bool `json:"spice"`
|
||||
Template bool `json:"template"`
|
||||
Agent bool `json:"agent"`
|
||||
}
|
||||
|
||||
// ContainerStatus represents the current status of a container
|
||||
type ContainerStatus struct {
|
||||
VMID int `json:"vmid"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CPU float64 `json:"cpu"`
|
||||
Memory int `json:"mem"`
|
||||
MemoryUsed int `json:"maxmem"`
|
||||
Disk int `json:"disk"`
|
||||
DiskUsed int `json:"maxdisk"`
|
||||
Uptime int `json:"uptime"`
|
||||
Lock string `json:"lock,omitempty"`
|
||||
HA bool `json:"ha"`
|
||||
Template bool `json:"template"`
|
||||
}
|
||||
|
||||
// NodeStats represents detailed statistics for a node
|
||||
type NodeStats struct {
|
||||
Node string `json:"node"`
|
||||
Status string `json:"status"`
|
||||
CPU float64 `json:"cpu"`
|
||||
MemoryTotal int `json:"memory_total"`
|
||||
MemoryUsed int `json:"memory_used"`
|
||||
MemoryFree int `json:"memory_free"`
|
||||
DiskTotal int `json:"disk_total"`
|
||||
DiskUsed int `json:"disk_used"`
|
||||
DiskFree int `json:"disk_free"`
|
||||
Uptime int `json:"uptime"`
|
||||
LoadAverage []float64 `json:"load_average"`
|
||||
NetworkIn int64 `json:"network_in"`
|
||||
NetworkOut int64 `json:"network_out"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
// VMTemplate represents a VM template that can be cloned
|
||||
type VMTemplate struct {
|
||||
VMID int `json:"vmid"`
|
||||
Name string `json:"name"`
|
||||
Node string `json:"node"`
|
||||
Storage string `json:"storage"`
|
||||
Size int `json:"size"`
|
||||
CPU int `json:"cpu"`
|
||||
Memory int `json:"memory"`
|
||||
DiskSize int `json:"disk_size"`
|
||||
}
|
||||
|
||||
// ContainerTemplate represents an LXC template that can be cloned
|
||||
type ContainerTemplate struct {
|
||||
VMID int `json:"vmid"`
|
||||
Name string `json:"name"`
|
||||
Node string `json:"node"`
|
||||
Storage string `json:"storage"`
|
||||
Size int `json:"size"`
|
||||
CPU int `json:"cpu"`
|
||||
Memory int `json:"memory"`
|
||||
DiskSize int `json:"disk_size"`
|
||||
OSTemplate string `json:"os_template"`
|
||||
}
|
||||
|
||||
// StorageInfo represents storage information on a node
|
||||
type StorageInfo struct {
|
||||
Storage string `json:"storage"`
|
||||
Node string `json:"node"`
|
||||
Type string `json:"type"`
|
||||
Total int `json:"total"`
|
||||
Used int `json:"used"`
|
||||
Available int `json:"avail"`
|
||||
Shared bool `json:"shared"`
|
||||
Content string `json:"content"`
|
||||
Active bool `json:"active"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
}
|
||||
|
||||
// NetworkInfo represents network interface information
|
||||
type NetworkInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Active bool `json:"active"`
|
||||
MACAddress string `json:"mac"`
|
||||
Bridge string `json:"bridge"`
|
||||
IP string `json:"ip"`
|
||||
CIDR string `json:"cidr"`
|
||||
Gateway string `json:"gateway"`
|
||||
DNS string `json:"dns"`
|
||||
}
|
||||
|
||||
// TaskInfo represents a task running on Proxmox
|
||||
type TaskInfo struct {
|
||||
UPID string `json:"upid"`
|
||||
Node string `json:"node"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
User string `json:"user"`
|
||||
StartTime time.Time `json:"starttime"`
|
||||
EndTime time.Time `json:"endtime"`
|
||||
Duration string `json:"duration"`
|
||||
PID int `json:"pid"`
|
||||
}
|
||||
|
||||
// ClusterInfo represents cluster information
|
||||
type ClusterInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Nodes int `json:"nodes"`
|
||||
Quorate bool `json:"quorate"`
|
||||
Links int `json:"links"`
|
||||
Messages string `json:"messages"`
|
||||
}
|
||||
|
||||
// Resource represents a generic resource in Proxmox
|
||||
type Resource struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Node string `json:"node"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
// Pool represents a resource pool
|
||||
type Pool struct {
|
||||
PoolID string `json:"poolid"`
|
||||
Name string `json:"name"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// User represents a Proxmox user
|
||||
type User struct {
|
||||
UserID string `json:"userid"`
|
||||
Realm string `json:"realm"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Email string `json:"email,omitempty"`
|
||||
FirstName string `json:"firstname,omitempty"`
|
||||
LastName string `json:"lastname,omitempty"`
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Expire int `json:"expire,omitempty"`
|
||||
LastLogin int64 `json:"last_login,omitempty"`
|
||||
}
|
||||
|
||||
// Role represents a Proxmox user role
|
||||
type Role struct {
|
||||
RoleID string `json:"roleid"`
|
||||
Privs []string `json:"privs,omitempty"`
|
||||
Special bool `json:"special,omitempty"`
|
||||
}
|
||||
|
||||
// Permission represents a permission in Proxmox
|
||||
type Permission struct {
|
||||
Path string `json:"path"`
|
||||
Role string `json:"role"`
|
||||
User string `json:"user,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
Realm string `json:"realm,omitempty"`
|
||||
}
|
||||
@@ -1,581 +0,0 @@
|
||||
package scaling
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"containr/internal/deployment"
|
||||
"containr/internal/metrics"
|
||||
)
|
||||
|
||||
// AutoScaler manages automatic scaling of services
|
||||
type AutoScaler struct {
|
||||
scheduler *deployment.Scheduler
|
||||
metricsCollector *metrics.MetricsCollector
|
||||
policies map[string]*ScalingPolicy
|
||||
services map[string]*ServiceScalingState
|
||||
mu sync.RWMutex
|
||||
checkInterval time.Duration
|
||||
cooldownPeriod time.Duration
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// ScalingPolicy defines how a service should scale
|
||||
type ScalingPolicy struct {
|
||||
ServiceID string `json:"service_id"`
|
||||
MinReplicas int `json:"min_replicas"`
|
||||
MaxReplicas int `json:"max_replicas"`
|
||||
TargetCPU float64 `json:"target_cpu"` // Target CPU utilization percentage
|
||||
TargetMemory float64 `json:"target_memory"` // Target memory utilization percentage
|
||||
ScaleUpCooldown time.Duration `json:"scale_up_cooldown"`
|
||||
ScaleDownCooldown time.Duration `json:"scale_down_cooldown"`
|
||||
ScaleUpStep int `json:"scale_up_step"` // How many replicas to add when scaling up
|
||||
ScaleDownStep int `json:"scale_down_step"` // How many replicas to remove when scaling down
|
||||
Metrics []string `json:"metrics"` // Which metrics to consider
|
||||
Thresholds map[string]float64 `json:"thresholds"` // Custom thresholds for metrics
|
||||
Enabled bool `json:"enabled"`
|
||||
CostOptimization *CostOptimization `json:"cost_optimization"`
|
||||
}
|
||||
|
||||
// CostOptimization defines cost-related scaling parameters
|
||||
type CostOptimization struct {
|
||||
MaxCostPerHour float64 `json:"max_cost_per_hour"`
|
||||
PreferEfficiency bool `json:"prefer_efficiency"`
|
||||
IdleTimeout time.Duration `json:"idle_timeout"`
|
||||
}
|
||||
|
||||
// ServiceScalingState tracks the current scaling state of a service
|
||||
type ServiceScalingState struct {
|
||||
ServiceID string
|
||||
CurrentReplicas int
|
||||
DesiredReplicas int
|
||||
LastScaleAction time.Time
|
||||
LastScaleDirection string // "up" or "down"
|
||||
ScaleUpCooldown time.Time
|
||||
ScaleDownCooldown time.Time
|
||||
MetricsHistory []MetricsSnapshot
|
||||
Policy *ScalingPolicy
|
||||
}
|
||||
|
||||
// MetricsSnapshot captures metrics at a point in time
|
||||
type MetricsSnapshot struct {
|
||||
Timestamp time.Time
|
||||
CPU float64
|
||||
Memory float64
|
||||
Requests float64
|
||||
Errors float64
|
||||
}
|
||||
|
||||
// ScaleEvent represents a scaling action
|
||||
type ScaleEvent struct {
|
||||
ServiceID string `json:"service_id"`
|
||||
Action string `json:"action"` // "scale_up" or "scale_down"
|
||||
FromReplicas int `json:"from_replicas"`
|
||||
ToReplicas int `json:"to_replicas"`
|
||||
Reason string `json:"reason"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Metrics map[string]float64 `json:"metrics"`
|
||||
CostImpact float64 `json:"cost_impact"`
|
||||
}
|
||||
|
||||
// ScalingDecision contains the decision made by the autoscaler
|
||||
type ScalingDecision struct {
|
||||
ShouldScale bool `json:"should_scale"`
|
||||
Action string `json:"action"`
|
||||
CurrentReplicas int `json:"current_replicas"`
|
||||
DesiredReplicas int `json:"desired_replicas"`
|
||||
Reason string `json:"reason"`
|
||||
Metrics map[string]float64 `json:"metrics"`
|
||||
CostEstimate float64 `json:"cost_estimate"`
|
||||
}
|
||||
|
||||
// NewAutoScaler creates a new auto-scaler
|
||||
func NewAutoScaler(scheduler *deployment.Scheduler, metricsCollector *metrics.MetricsCollector) *AutoScaler {
|
||||
return &AutoScaler{
|
||||
scheduler: scheduler,
|
||||
metricsCollector: metricsCollector,
|
||||
policies: make(map[string]*ScalingPolicy),
|
||||
services: make(map[string]*ServiceScalingState),
|
||||
checkInterval: 30 * time.Second,
|
||||
cooldownPeriod: 5 * time.Minute,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the auto-scaling process
|
||||
func (as *AutoScaler) Start(ctx context.Context) error {
|
||||
ticker := time.NewTicker(as.checkInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Printf("AutoScaler started with check interval: %v", as.checkInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
if as.enabled {
|
||||
if err := as.checkAndScale(ctx); err != nil {
|
||||
log.Printf("Error during auto-scaling check: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndScale evaluates all services and scales if necessary
|
||||
func (as *AutoScaler) checkAndScale(ctx context.Context) error {
|
||||
as.mu.RLock()
|
||||
servicesToCheck := make([]*ServiceScalingState, 0, len(as.services))
|
||||
for _, state := range as.services {
|
||||
if state.Policy != nil && state.Policy.Enabled {
|
||||
servicesToCheck = append(servicesToCheck, state)
|
||||
}
|
||||
}
|
||||
as.mu.RUnlock()
|
||||
|
||||
for _, state := range servicesToCheck {
|
||||
decision, err := as.evaluateScaling(ctx, state)
|
||||
if err != nil {
|
||||
log.Printf("Error evaluating scaling for service %s: %v", state.ServiceID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if decision.ShouldScale {
|
||||
if err := as.executeScaling(ctx, state, decision); err != nil {
|
||||
log.Printf("Error executing scaling for service %s: %v", state.ServiceID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// evaluateScaling determines if a service needs to scale
|
||||
func (as *AutoScaler) evaluateScaling(ctx context.Context, state *ServiceScalingState) (*ScalingDecision, error) {
|
||||
policy := state.Policy
|
||||
now := time.Now()
|
||||
|
||||
// Check cooldowns
|
||||
if now.Before(state.ScaleUpCooldown) && now.Before(state.ScaleDownCooldown) {
|
||||
return &ScalingDecision{
|
||||
ShouldScale: false,
|
||||
CurrentReplicas: state.CurrentReplicas,
|
||||
DesiredReplicas: state.CurrentReplicas,
|
||||
Reason: "In cooldown period",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get current metrics
|
||||
metrics, err := as.getServiceMetrics(ctx, state.ServiceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get service metrics: %w", err)
|
||||
}
|
||||
|
||||
// Calculate desired replicas based on metrics
|
||||
desiredReplicas := as.calculateDesiredReplicas(state, metrics, policy)
|
||||
|
||||
// Ensure within bounds
|
||||
if desiredReplicas < policy.MinReplicas {
|
||||
desiredReplicas = policy.MinReplicas
|
||||
}
|
||||
if desiredReplicas > policy.MaxReplicas {
|
||||
desiredReplicas = policy.MaxReplicas
|
||||
}
|
||||
|
||||
// Check if scaling is needed
|
||||
if desiredReplicas == state.CurrentReplicas {
|
||||
return &ScalingDecision{
|
||||
ShouldScale: false,
|
||||
CurrentReplicas: state.CurrentReplicas,
|
||||
DesiredReplicas: desiredReplicas,
|
||||
Reason: "No scaling needed",
|
||||
Metrics: metrics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Determine action and check cooldowns
|
||||
action := "scale_down"
|
||||
if desiredReplicas > state.CurrentReplicas {
|
||||
action = "scale_up"
|
||||
if now.Before(state.ScaleUpCooldown) {
|
||||
return &ScalingDecision{
|
||||
ShouldScale: false,
|
||||
CurrentReplicas: state.CurrentReplicas,
|
||||
DesiredReplicas: desiredReplicas,
|
||||
Reason: "Scale up cooldown active",
|
||||
Metrics: metrics,
|
||||
}, nil
|
||||
}
|
||||
} else {
|
||||
if now.Before(state.ScaleDownCooldown) {
|
||||
return &ScalingDecision{
|
||||
ShouldScale: false,
|
||||
CurrentReplicas: state.CurrentReplicas,
|
||||
DesiredReplicas: desiredReplicas,
|
||||
Reason: "Scale down cooldown active",
|
||||
Metrics: metrics,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Apply scaling steps
|
||||
if action == "scale_up" {
|
||||
maxStep := policy.ScaleUpStep
|
||||
if maxStep <= 0 {
|
||||
maxStep = 1
|
||||
}
|
||||
if desiredReplicas-state.CurrentReplicas > maxStep {
|
||||
desiredReplicas = state.CurrentReplicas + maxStep
|
||||
}
|
||||
} else {
|
||||
maxStep := policy.ScaleDownStep
|
||||
if maxStep <= 0 {
|
||||
maxStep = 1
|
||||
}
|
||||
if state.CurrentReplicas-desiredReplicas > maxStep {
|
||||
desiredReplicas = state.CurrentReplicas - maxStep
|
||||
}
|
||||
}
|
||||
|
||||
// Cost optimization check
|
||||
if policy.CostOptimization != nil {
|
||||
costEstimate := as.estimateScalingCost(state, desiredReplicas)
|
||||
if costEstimate > policy.CostOptimization.MaxCostPerHour {
|
||||
return &ScalingDecision{
|
||||
ShouldScale: false,
|
||||
CurrentReplicas: state.CurrentReplicas,
|
||||
DesiredReplicas: state.CurrentReplicas,
|
||||
Reason: fmt.Sprintf("Cost estimate %.2f exceeds maximum %.2f", costEstimate, policy.CostOptimization.MaxCostPerHour),
|
||||
Metrics: metrics,
|
||||
CostEstimate: costEstimate,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
reason := as.generateScalingReason(state, metrics, desiredReplicas)
|
||||
|
||||
return &ScalingDecision{
|
||||
ShouldScale: true,
|
||||
Action: action,
|
||||
CurrentReplicas: state.CurrentReplicas,
|
||||
DesiredReplicas: desiredReplicas,
|
||||
Reason: reason,
|
||||
Metrics: metrics,
|
||||
CostEstimate: as.estimateScalingCost(state, desiredReplicas),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// calculateDesiredReplicas calculates the desired number of replicas based on metrics
|
||||
func (as *AutoScaler) calculateDesiredReplicas(state *ServiceScalingState, metrics map[string]float64, policy *ScalingPolicy) int {
|
||||
currentReplicas := state.CurrentReplicas
|
||||
desiredReplicas := currentReplicas
|
||||
|
||||
// CPU-based scaling
|
||||
if cpuUsage, ok := metrics["cpu"]; ok && policy.TargetCPU > 0 {
|
||||
cpuRatio := cpuUsage / policy.TargetCPU
|
||||
if cpuRatio > 1.2 { // Scale up if CPU is 20% above target
|
||||
desiredReplicas = int(math.Ceil(float64(currentReplicas) * cpuRatio))
|
||||
} else if cpuRatio < 0.8 { // Scale down if CPU is 20% below target
|
||||
desiredReplicas = int(math.Floor(float64(currentReplicas) * cpuRatio))
|
||||
}
|
||||
}
|
||||
|
||||
// Memory-based scaling
|
||||
if memoryUsage, ok := metrics["memory"]; ok && policy.TargetMemory > 0 {
|
||||
memoryRatio := memoryUsage / policy.TargetMemory
|
||||
if memoryRatio > 1.2 {
|
||||
memDesired := int(math.Ceil(float64(currentReplicas) * memoryRatio))
|
||||
if memDesired > desiredReplicas {
|
||||
desiredReplicas = memDesired
|
||||
}
|
||||
} else if memoryUsage < 0.8 {
|
||||
memDesired := int(math.Floor(float64(currentReplicas) * memoryRatio))
|
||||
if memDesired < desiredReplicas {
|
||||
desiredReplicas = memDesired
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request rate scaling
|
||||
if requestRate, ok := metrics["requests_per_second"]; ok {
|
||||
// Simple heuristic: scale based on request rate per replica
|
||||
// Assume each replica can handle ~100 requests per second
|
||||
requestsPerReplica := 100.0
|
||||
requestDesired := int(math.Ceil(requestRate / requestsPerReplica))
|
||||
if requestDesired > desiredReplicas {
|
||||
desiredReplicas = requestDesired
|
||||
}
|
||||
}
|
||||
|
||||
// Error rate scaling (scale up if error rate is high)
|
||||
if errorRate, ok := metrics["error_rate"]; ok && errorRate > 0.05 { // 5% error rate
|
||||
errorDesired := currentReplicas + 1
|
||||
if errorDesired > desiredReplicas {
|
||||
desiredReplicas = errorDesired
|
||||
}
|
||||
}
|
||||
|
||||
return desiredReplicas
|
||||
}
|
||||
|
||||
// getServiceMetrics gets current metrics for a service
|
||||
func (as *AutoScaler) getServiceMetrics(ctx context.Context, serviceID string) (map[string]float64, error) {
|
||||
// Get service metrics from the metrics collector
|
||||
serviceMetrics, err := as.metricsCollector.GetServiceMetrics(serviceID)
|
||||
if err != nil {
|
||||
// If no metrics available, return empty map
|
||||
return make(map[string]float64), nil
|
||||
}
|
||||
|
||||
metrics := make(map[string]float64)
|
||||
|
||||
// Calculate average metrics across instances
|
||||
if len(serviceMetrics.Instances) > 0 {
|
||||
var totalCPU, totalMemory, totalRequests float64
|
||||
var totalErrors int64
|
||||
|
||||
for _, instance := range serviceMetrics.Instances {
|
||||
totalCPU += instance.CPU
|
||||
totalMemory += float64(instance.Memory)
|
||||
totalRequests += serviceMetrics.Requests.Throughput
|
||||
totalErrors += serviceMetrics.Errors.Total
|
||||
}
|
||||
|
||||
instanceCount := float64(len(serviceMetrics.Instances))
|
||||
metrics["cpu"] = totalCPU / instanceCount
|
||||
metrics["memory"] = totalMemory / instanceCount / (1024 * 1024 * 1024) // Convert to GB
|
||||
metrics["requests_per_second"] = totalRequests
|
||||
if serviceMetrics.Requests.Total > 0 {
|
||||
metrics["error_rate"] = float64(totalErrors) / float64(serviceMetrics.Requests.Total)
|
||||
} else {
|
||||
metrics["error_rate"] = 0
|
||||
}
|
||||
}
|
||||
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// executeScaling performs the actual scaling action
|
||||
func (as *AutoScaler) executeScaling(ctx context.Context, state *ServiceScalingState, decision *ScalingDecision) error {
|
||||
serviceID := state.ServiceID
|
||||
fromReplicas := state.CurrentReplicas
|
||||
toReplicas := decision.DesiredReplicas
|
||||
|
||||
log.Printf("Executing scaling for service %s: %d -> %d replicas (%s)",
|
||||
serviceID, fromReplicas, toReplicas, decision.Reason)
|
||||
|
||||
// In a real implementation, this would call the deployment engine
|
||||
// to scale the service (add/remove containers)
|
||||
|
||||
// Update state
|
||||
as.mu.Lock()
|
||||
state.CurrentReplicas = toReplicas
|
||||
state.DesiredReplicas = toReplicas
|
||||
state.LastScaleAction = time.Now()
|
||||
state.LastScaleDirection = decision.Action
|
||||
|
||||
// Set cooldowns
|
||||
if decision.Action == "scale_up" {
|
||||
state.ScaleUpCooldown = time.Now().Add(state.Policy.ScaleUpCooldown)
|
||||
} else {
|
||||
state.ScaleDownCooldown = time.Now().Add(state.Policy.ScaleDownCooldown)
|
||||
}
|
||||
as.mu.Unlock()
|
||||
|
||||
// Record the scaling event
|
||||
event := &ScaleEvent{
|
||||
ServiceID: serviceID,
|
||||
Action: decision.Action,
|
||||
FromReplicas: fromReplicas,
|
||||
ToReplicas: toReplicas,
|
||||
Reason: decision.Reason,
|
||||
Timestamp: time.Now(),
|
||||
Metrics: decision.Metrics,
|
||||
CostImpact: decision.CostEstimate,
|
||||
}
|
||||
|
||||
// TODO: Store scaling event in database
|
||||
log.Printf("Scaling event: %+v", event)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateScalingReason creates a human-readable reason for scaling
|
||||
func (as *AutoScaler) generateScalingReason(state *ServiceScalingState, metrics map[string]float64, desiredReplicas int) string {
|
||||
var reasons []string
|
||||
|
||||
if cpuUsage, ok := metrics["cpu"]; ok {
|
||||
if cpuUsage > state.Policy.TargetCPU*1.2 {
|
||||
reasons = append(reasons, fmt.Sprintf("CPU usage %.1f%% above target %.1f%%", cpuUsage, state.Policy.TargetCPU))
|
||||
} else if cpuUsage < state.Policy.TargetCPU*0.8 {
|
||||
reasons = append(reasons, fmt.Sprintf("CPU usage %.1f%% below target %.1f%%", cpuUsage, state.Policy.TargetCPU))
|
||||
}
|
||||
}
|
||||
|
||||
if memoryUsage, ok := metrics["memory"]; ok && state.Policy.TargetMemory > 0 {
|
||||
if memoryUsage > state.Policy.TargetMemory*1.2 {
|
||||
reasons = append(reasons, fmt.Sprintf("Memory usage %.1fGB above target %.1fGB", memoryUsage, state.Policy.TargetMemory))
|
||||
}
|
||||
}
|
||||
|
||||
if requestRate, ok := metrics["requests_per_second"]; ok {
|
||||
reasons = append(reasons, fmt.Sprintf("Request rate %.0f/s requires %d replicas", requestRate, desiredReplicas))
|
||||
}
|
||||
|
||||
if len(reasons) == 0 {
|
||||
return "Automatic scaling based on metrics"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Scale %s: %v", state.LastScaleDirection, reasons)
|
||||
}
|
||||
|
||||
// estimateScalingCost estimates the cost impact of scaling
|
||||
func (as *AutoScaler) estimateScalingCost(state *ServiceScalingState, replicas int) float64 {
|
||||
// Simple cost model: $0.01 per replica per hour
|
||||
// In a real implementation, this would consider actual instance costs
|
||||
baseCost := 0.01
|
||||
return float64(replicas) * baseCost
|
||||
}
|
||||
|
||||
// SetScalingPolicy sets or updates a scaling policy for a service
|
||||
func (as *AutoScaler) SetScalingPolicy(policy *ScalingPolicy) error {
|
||||
as.mu.Lock()
|
||||
defer as.mu.Unlock()
|
||||
|
||||
// Set default values if not specified
|
||||
if policy.ScaleUpCooldown == 0 {
|
||||
policy.ScaleUpCooldown = 3 * time.Minute
|
||||
}
|
||||
if policy.ScaleDownCooldown == 0 {
|
||||
policy.ScaleDownCooldown = 5 * time.Minute
|
||||
}
|
||||
if policy.ScaleUpStep == 0 {
|
||||
policy.ScaleUpStep = 1
|
||||
}
|
||||
if policy.ScaleDownStep == 0 {
|
||||
policy.ScaleDownStep = 1
|
||||
}
|
||||
if policy.MinReplicas == 0 {
|
||||
policy.MinReplicas = 1
|
||||
}
|
||||
if policy.MaxReplicas == 0 {
|
||||
policy.MaxReplicas = 10
|
||||
}
|
||||
|
||||
as.policies[policy.ServiceID] = policy
|
||||
|
||||
// Initialize service state if not exists
|
||||
if _, exists := as.services[policy.ServiceID]; !exists {
|
||||
as.services[policy.ServiceID] = &ServiceScalingState{
|
||||
ServiceID: policy.ServiceID,
|
||||
CurrentReplicas: policy.MinReplicas,
|
||||
DesiredReplicas: policy.MinReplicas,
|
||||
Policy: policy,
|
||||
MetricsHistory: make([]MetricsSnapshot, 0),
|
||||
}
|
||||
} else {
|
||||
as.services[policy.ServiceID].Policy = policy
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetScalingPolicy returns the scaling policy for a service
|
||||
func (as *AutoScaler) GetScalingPolicy(serviceID string) (*ScalingPolicy, error) {
|
||||
as.mu.RLock()
|
||||
defer as.mu.RUnlock()
|
||||
|
||||
policy, exists := as.policies[serviceID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no scaling policy found for service: %s", serviceID)
|
||||
}
|
||||
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// GetServiceState returns the current scaling state of a service
|
||||
func (as *AutoScaler) GetServiceState(serviceID string) (*ServiceScalingState, error) {
|
||||
as.mu.RLock()
|
||||
defer as.mu.RUnlock()
|
||||
|
||||
state, exists := as.services[serviceID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no scaling state found for service: %s", serviceID)
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// GetAllServiceStates returns all service scaling states
|
||||
func (as *AutoScaler) GetAllServiceStates() map[string]*ServiceScalingState {
|
||||
as.mu.RLock()
|
||||
defer as.mu.RUnlock()
|
||||
|
||||
result := make(map[string]*ServiceScalingState)
|
||||
for id, state := range as.services {
|
||||
result[id] = state
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Enable enables the auto-scaler
|
||||
func (as *AutoScaler) Enable() {
|
||||
as.mu.Lock()
|
||||
defer as.mu.Unlock()
|
||||
as.enabled = true
|
||||
}
|
||||
|
||||
// Disable disables the auto-scaler
|
||||
func (as *AutoScaler) Disable() {
|
||||
as.mu.Lock()
|
||||
defer as.mu.Unlock()
|
||||
as.enabled = false
|
||||
}
|
||||
|
||||
// IsEnabled returns whether the auto-scaler is enabled
|
||||
func (as *AutoScaler) IsEnabled() bool {
|
||||
as.mu.RLock()
|
||||
defer as.mu.RUnlock()
|
||||
return as.enabled
|
||||
}
|
||||
|
||||
// GetScalingSummary returns a summary of scaling activities
|
||||
func (as *AutoScaler) GetScalingSummary() map[string]interface{} {
|
||||
as.mu.RLock()
|
||||
defer as.mu.RUnlock()
|
||||
|
||||
totalServices := len(as.services)
|
||||
enabledServices := 0
|
||||
totalReplicas := 0
|
||||
scalingUp := 0
|
||||
scalingDown := 0
|
||||
|
||||
for _, state := range as.services {
|
||||
if state.Policy != nil && state.Policy.Enabled {
|
||||
enabledServices++
|
||||
}
|
||||
totalReplicas += state.CurrentReplicas
|
||||
|
||||
if state.LastScaleDirection == "scale_up" && time.Since(state.LastScaleAction) < time.Hour {
|
||||
scalingUp++
|
||||
} else if state.LastScaleDirection == "scale_down" && time.Since(state.LastScaleAction) < time.Hour {
|
||||
scalingDown++
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_services": totalServices,
|
||||
"enabled_services": enabledServices,
|
||||
"total_replicas": totalReplicas,
|
||||
"scaling_up": scalingUp,
|
||||
"scaling_down": scalingDown,
|
||||
"enabled": as.enabled,
|
||||
"check_interval": as.checkInterval.String(),
|
||||
}
|
||||
}
|
||||
@@ -1,500 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ComplianceFramework represents a compliance framework
|
||||
type ComplianceFramework struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ComplianceControl represents a compliance control
|
||||
type ComplianceControl struct {
|
||||
ID string `json:"id"`
|
||||
FrameworkID string `json:"framework_id"`
|
||||
Code string `json:"code"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
Requirement string `json:"requirement"`
|
||||
TestProcedure string `json:"test_procedure"`
|
||||
Status string `json:"status"` // "compliant", "non_compliant", "not_applicable", "pending"
|
||||
LastAssessed *time.Time `json:"last_assessed,omitempty"`
|
||||
Evidence string `json:"evidence"`
|
||||
Metadata string `json:"metadata"`
|
||||
}
|
||||
|
||||
// ComplianceReport represents a compliance assessment report
|
||||
type ComplianceReport struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
FrameworkID string `json:"framework_id"`
|
||||
AssessmentDate time.Time `json:"assessment_date"`
|
||||
Assessor string `json:"assessor"`
|
||||
OverallStatus string `json:"overall_status"`
|
||||
Score int `json:"score"` // 0-100
|
||||
Controls []ComplianceControl `json:"controls"`
|
||||
Risks []ComplianceRisk `json:"risks"`
|
||||
Recommendations []string `json:"recommendations"`
|
||||
}
|
||||
|
||||
// ComplianceRisk represents a compliance risk
|
||||
type ComplianceRisk struct {
|
||||
ID string `json:"id"`
|
||||
ControlID string `json:"control_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Impact string `json:"impact"` // "high", "medium", "low"
|
||||
Likelihood string `json:"likelihood"` // "high", "medium", "low"
|
||||
Mitigation string `json:"mitigation"`
|
||||
}
|
||||
|
||||
// ComplianceManager handles compliance operations
|
||||
type ComplianceManager struct {
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
// NewComplianceManager creates a new compliance manager
|
||||
func NewComplianceManager(db *database.DB) *ComplianceManager {
|
||||
return &ComplianceManager{db: db}
|
||||
}
|
||||
|
||||
// InitializeGDPRFramework initializes GDPR compliance framework
|
||||
func (cm *ComplianceManager) InitializeGDPRFramework() error {
|
||||
framework := ComplianceFramework{
|
||||
ID: uuid.New().String(),
|
||||
Name: "GDPR",
|
||||
Description: "General Data Protection Regulation compliance framework",
|
||||
Version: "1.0",
|
||||
Enabled: true,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Insert framework
|
||||
_, err := cm.db.Exec(`
|
||||
INSERT INTO compliance_frameworks (id, name, description, version, enabled, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (name) DO UPDATE SET version = $4, enabled = $5
|
||||
`, framework.ID, framework.Name, framework.Description, framework.Version, framework.Enabled, framework.CreatedAt)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create GDPR framework: %w", err)
|
||||
}
|
||||
|
||||
// Add GDPR controls
|
||||
controls := []ComplianceControl{
|
||||
{
|
||||
ID: uuid.New().String(),
|
||||
FrameworkID: framework.ID,
|
||||
Code: "GDPR-Art-32",
|
||||
Title: "Security of Processing",
|
||||
Description: "Technical and organizational measures to ensure data security",
|
||||
Category: "Security",
|
||||
Requirement: "Implement appropriate technical and organizational measures to ensure a level of security appropriate to the risk",
|
||||
TestProcedure: "Review security controls, encryption policies, access controls, and incident response procedures",
|
||||
Status: "pending",
|
||||
Evidence: "",
|
||||
Metadata: `{"risk_level": "high", "review_frequency": "quarterly"}`,
|
||||
},
|
||||
{
|
||||
ID: uuid.New().String(),
|
||||
FrameworkID: framework.ID,
|
||||
Code: "GDPR-Art-25",
|
||||
Title: "Data Protection by Design and by Default",
|
||||
Description: "Implement data protection measures in system design",
|
||||
Category: "Privacy by Design",
|
||||
Requirement: "Implement data protection principles in system design and default settings",
|
||||
TestProcedure: "Review system architecture, privacy settings, and data minimization practices",
|
||||
Status: "pending",
|
||||
Evidence: "",
|
||||
Metadata: `{"risk_level": "medium", "review_frequency": "biannual"}`,
|
||||
},
|
||||
{
|
||||
ID: uuid.New().String(),
|
||||
FrameworkID: framework.ID,
|
||||
Code: "GDPR-Art-24",
|
||||
Title: "Responsibility of the Controller",
|
||||
Description: "Data controller responsibility and compliance demonstration",
|
||||
Category: "Governance",
|
||||
Requirement: "Implement measures to ensure and demonstrate compliance with GDPR",
|
||||
TestProcedure: "Review governance policies, documentation, and compliance monitoring",
|
||||
Status: "pending",
|
||||
Evidence: "",
|
||||
Metadata: `{"risk_level": "medium", "review_frequency": "annual"}`,
|
||||
},
|
||||
{
|
||||
ID: uuid.New().String(),
|
||||
FrameworkID: framework.ID,
|
||||
Code: "GDPR-Art-33",
|
||||
Title: "Notification of Personal Data Breach",
|
||||
Description: "Procedures for notifying data breaches to authorities",
|
||||
Category: "Incident Response",
|
||||
Requirement: "Implement procedures for notifying personal data breaches within 72 hours",
|
||||
TestProcedure: "Review incident response procedures, notification templates, and breach detection mechanisms",
|
||||
Status: "pending",
|
||||
Evidence: "",
|
||||
Metadata: `{"risk_level": "high", "review_frequency": "quarterly"}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, control := range controls {
|
||||
_, err := cm.db.Exec(`
|
||||
INSERT INTO compliance_controls (id, framework_id, code, title, description, category, requirement, test_procedure, status, last_assessed, evidence, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NULL, $10, $11)
|
||||
ON CONFLICT (framework_id, code) DO UPDATE SET title = $4, description = $5, requirement = $7, test_procedure = $8
|
||||
`, control.ID, control.FrameworkID, control.Code, control.Title, control.Description,
|
||||
control.Category, control.Requirement, control.TestProcedure, control.Status, control.Evidence, control.Metadata)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to insert GDPR control %s: %v", control.Code, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssessCompliance performs a compliance assessment
|
||||
func (cm *ComplianceManager) AssessCompliance(projectID, frameworkID, assessor string) (*ComplianceReport, error) {
|
||||
reportID := uuid.New().String()
|
||||
|
||||
report := &ComplianceReport{
|
||||
ID: reportID,
|
||||
ProjectID: projectID,
|
||||
FrameworkID: frameworkID,
|
||||
AssessmentDate: time.Now(),
|
||||
Assessor: assessor,
|
||||
OverallStatus: "in_progress",
|
||||
Score: 0,
|
||||
Controls: []ComplianceControl{},
|
||||
Risks: []ComplianceRisk{},
|
||||
Recommendations: []string{},
|
||||
}
|
||||
|
||||
// Insert report record
|
||||
_, err := cm.db.Exec(`
|
||||
INSERT INTO compliance_reports (id, project_id, framework_id, assessment_date, assessor, overall_status, score)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`, report.ID, report.ProjectID, report.FrameworkID, report.AssessmentDate, report.Assessor, report.OverallStatus, report.Score)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create compliance report: %w", err)
|
||||
}
|
||||
|
||||
// Start assessment in background
|
||||
go cm.performAssessment(report)
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// performAssessment executes the compliance assessment
|
||||
func (cm *ComplianceManager) performAssessment(report *ComplianceReport) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Get framework controls
|
||||
controls, err := cm.getFrameworkControls(report.FrameworkID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get framework controls: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var assessedControls []ComplianceControl
|
||||
var risks []ComplianceRisk
|
||||
var recommendations []string
|
||||
compliantCount := 0
|
||||
|
||||
if len(controls) == 0 {
|
||||
_, updateErr := cm.db.Exec(`
|
||||
UPDATE compliance_reports
|
||||
SET overall_status = $1, score = $2
|
||||
WHERE id = $3
|
||||
`, "non_compliant", 0, report.ID)
|
||||
if updateErr != nil {
|
||||
log.Printf("Failed to update compliance report %s with empty control set: %v", report.ID, updateErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, control := range controls {
|
||||
assessedControl := cm.assessControl(ctx, report.ProjectID, control)
|
||||
assessedControls = append(assessedControls, assessedControl)
|
||||
|
||||
if assessedControl.Status == "compliant" {
|
||||
compliantCount++
|
||||
} else if assessedControl.Status == "non_compliant" {
|
||||
// Generate risk for non-compliant controls
|
||||
risk := ComplianceRisk{
|
||||
ID: uuid.New().String(),
|
||||
ControlID: assessedControl.ID,
|
||||
Title: fmt.Sprintf("Non-compliance: %s", assessedControl.Title),
|
||||
Description: fmt.Sprintf("Control %s is not compliant", assessedControl.Code),
|
||||
Impact: cm.getRiskImpact(assessedControl),
|
||||
Likelihood: cm.getRiskLikelihood(assessedControl),
|
||||
Mitigation: cm.generateMitigation(assessedControl),
|
||||
}
|
||||
risks = append(risks, risk)
|
||||
|
||||
// Generate recommendation
|
||||
rec := fmt.Sprintf("Implement controls to achieve compliance for %s: %s", assessedControl.Code, assessedControl.Title)
|
||||
recommendations = append(recommendations, rec)
|
||||
}
|
||||
|
||||
// Update control status in database
|
||||
_, err := cm.db.Exec(`
|
||||
UPDATE compliance_controls
|
||||
SET status = $1, last_assessed = $2, evidence = $3
|
||||
WHERE id = $4
|
||||
`, assessedControl.Status, assessedControl.LastAssessed, assessedControl.Evidence, assessedControl.ID)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to update control %s: %v", assessedControl.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate overall score
|
||||
score := int((float64(compliantCount) / float64(len(controls))) * 100)
|
||||
|
||||
// Determine overall status
|
||||
overallStatus := "non_compliant"
|
||||
if score >= 90 {
|
||||
overallStatus = "compliant"
|
||||
} else if score >= 70 {
|
||||
overallStatus = "partially_compliant"
|
||||
}
|
||||
|
||||
// Update report with results
|
||||
_, err = cm.db.Exec(`
|
||||
UPDATE compliance_reports
|
||||
SET overall_status = $1, score = $2
|
||||
WHERE id = $3
|
||||
`, overallStatus, score, report.ID)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to update compliance report %s: %v", report.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Store risks and recommendations
|
||||
for _, risk := range risks {
|
||||
_, err := cm.db.Exec(`
|
||||
INSERT INTO compliance_risks (id, report_id, control_id, title, description, impact, likelihood, mitigation)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`, risk.ID, report.ID, risk.ControlID, risk.Title, risk.Description, risk.Impact, risk.Likelihood, risk.Mitigation)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to store risk %s: %v", risk.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getFrameworkControls retrieves all controls for a framework
|
||||
func (cm *ComplianceManager) getFrameworkControls(frameworkID string) ([]ComplianceControl, error) {
|
||||
rows, err := cm.db.Query(`
|
||||
SELECT id, framework_id, code, title, description, category, requirement, test_procedure, status, last_assessed, evidence, metadata
|
||||
FROM compliance_controls WHERE framework_id = $1
|
||||
`, frameworkID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var controls []ComplianceControl
|
||||
for rows.Next() {
|
||||
var control ComplianceControl
|
||||
var lastAssessed sql.NullTime
|
||||
|
||||
err := rows.Scan(&control.ID, &control.FrameworkID, &control.Code, &control.Title, &control.Description,
|
||||
&control.Category, &control.Requirement, &control.TestProcedure, &control.Status, &lastAssessed, &control.Evidence, &control.Metadata)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if lastAssessed.Valid {
|
||||
control.LastAssessed = &lastAssessed.Time
|
||||
}
|
||||
|
||||
controls = append(controls, control)
|
||||
}
|
||||
|
||||
return controls, nil
|
||||
}
|
||||
|
||||
// assessControl assesses a single compliance control
|
||||
func (cm *ComplianceManager) assessControl(ctx context.Context, projectID string, control ComplianceControl) ComplianceControl {
|
||||
assessed := control
|
||||
now := time.Now()
|
||||
assessed.LastAssessed = &now
|
||||
|
||||
// Simulate assessment logic (in real implementation, this would check actual configurations)
|
||||
switch control.Code {
|
||||
case "GDPR-Art-32":
|
||||
// Check security measures
|
||||
hasEncryption := cm.checkDataEncryption(projectID)
|
||||
hasAccessControl := cm.checkAccessControl(projectID)
|
||||
hasIncidentResponse := cm.checkIncidentResponse(projectID)
|
||||
|
||||
if hasEncryption && hasAccessControl && hasIncidentResponse {
|
||||
assessed.Status = "compliant"
|
||||
assessed.Evidence = "Encryption enabled, access controls configured, incident response procedures documented"
|
||||
} else {
|
||||
assessed.Status = "non_compliant"
|
||||
missing := []string{}
|
||||
if !hasEncryption {
|
||||
missing = append(missing, "data encryption")
|
||||
}
|
||||
if !hasAccessControl {
|
||||
missing = append(missing, "access controls")
|
||||
}
|
||||
if !hasIncidentResponse {
|
||||
missing = append(missing, "incident response procedures")
|
||||
}
|
||||
assessed.Evidence = fmt.Sprintf("Missing controls: %s", strings.Join(missing, ", "))
|
||||
}
|
||||
|
||||
case "GDPR-Art-25":
|
||||
// Check privacy by design
|
||||
hasDataMinimization := cm.checkDataMinimization(projectID)
|
||||
hasPrivacySettings := cm.checkPrivacySettings(projectID)
|
||||
|
||||
if hasDataMinimization && hasPrivacySettings {
|
||||
assessed.Status = "compliant"
|
||||
assessed.Evidence = "Privacy by design principles implemented, data minimization configured"
|
||||
} else {
|
||||
assessed.Status = "non_compliant"
|
||||
assessed.Evidence = "Privacy by design principles not fully implemented"
|
||||
}
|
||||
|
||||
default:
|
||||
// Default assessment for other controls
|
||||
assessed.Status = "pending"
|
||||
assessed.Evidence = "Assessment pending manual review"
|
||||
}
|
||||
|
||||
return assessed
|
||||
}
|
||||
|
||||
// Helper functions for assessment checks (simulated)
|
||||
func (cm *ComplianceManager) checkDataEncryption(projectID string) bool {
|
||||
// Simulate checking encryption settings
|
||||
// In real implementation, this would check actual configurations
|
||||
return true
|
||||
}
|
||||
|
||||
func (cm *ComplianceManager) checkAccessControl(projectID string) bool {
|
||||
// Simulate checking access control
|
||||
return true
|
||||
}
|
||||
|
||||
func (cm *ComplianceManager) checkIncidentResponse(projectID string) bool {
|
||||
// Simulate checking incident response procedures
|
||||
return false // Simulate missing for demo
|
||||
}
|
||||
|
||||
func (cm *ComplianceManager) checkDataMinimization(projectID string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (cm *ComplianceManager) checkPrivacySettings(projectID string) bool {
|
||||
return false // Simulate missing for demo
|
||||
}
|
||||
|
||||
func (cm *ComplianceManager) getRiskImpact(control ComplianceControl) string {
|
||||
// Extract impact from metadata or default based on category
|
||||
var metadata map[string]interface{}
|
||||
json.Unmarshal([]byte(control.Metadata), &metadata)
|
||||
|
||||
if impact, ok := metadata["risk_level"].(string); ok {
|
||||
return impact
|
||||
}
|
||||
|
||||
// Default impact based on category
|
||||
switch control.Category {
|
||||
case "Security", "Incident Response":
|
||||
return "high"
|
||||
case "Privacy by Design":
|
||||
return "medium"
|
||||
default:
|
||||
return "low"
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ComplianceManager) getRiskLikelihood(control ComplianceControl) string {
|
||||
// Default likelihood based on control complexity
|
||||
if strings.Contains(control.Requirement, "implement") || strings.Contains(control.Requirement, "procedures") {
|
||||
return "medium"
|
||||
}
|
||||
return "low"
|
||||
}
|
||||
|
||||
func (cm *ComplianceManager) generateMitigation(control ComplianceControl) string {
|
||||
return fmt.Sprintf("Implement and document controls for %s as specified in the requirements", control.Title)
|
||||
}
|
||||
|
||||
// GetComplianceReport retrieves a compliance report by ID
|
||||
func (cm *ComplianceManager) GetComplianceReport(reportID string) (*ComplianceReport, error) {
|
||||
var report ComplianceReport
|
||||
|
||||
err := cm.db.QueryRow(`
|
||||
SELECT id, project_id, framework_id, assessment_date, assessor, overall_status, score
|
||||
FROM compliance_reports WHERE id = $1
|
||||
`, reportID).Scan(&report.ID, &report.ProjectID, &report.FrameworkID, &report.AssessmentDate, &report.Assessor, &report.OverallStatus, &report.Score)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load controls
|
||||
controls, err := cm.getFrameworkControls(report.FrameworkID)
|
||||
if err == nil {
|
||||
report.Controls = controls
|
||||
}
|
||||
|
||||
// Load risks
|
||||
risks, err := cm.getReportRisks(report.ID)
|
||||
if err == nil {
|
||||
report.Risks = risks
|
||||
}
|
||||
|
||||
return &report, nil
|
||||
}
|
||||
|
||||
// getReportRisks retrieves risks for a compliance report
|
||||
func (cm *ComplianceManager) getReportRisks(reportID string) ([]ComplianceRisk, error) {
|
||||
rows, err := cm.db.Query(`
|
||||
SELECT id, control_id, title, description, impact, likelihood, mitigation
|
||||
FROM compliance_risks WHERE report_id = $1
|
||||
`, reportID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var risks []ComplianceRisk
|
||||
for rows.Next() {
|
||||
var risk ComplianceRisk
|
||||
err := rows.Scan(&risk.ID, &risk.ControlID, &risk.Title, &risk.Description, &risk.Impact, &risk.Likelihood, &risk.Mitigation)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
risks = append(risks, risk)
|
||||
}
|
||||
|
||||
return risks, nil
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EncryptionManager handles data encryption and decryption
|
||||
type EncryptionManager struct {
|
||||
gcm cipher.AEAD
|
||||
}
|
||||
|
||||
// NewEncryptionManager creates a new encryption manager
|
||||
func NewEncryptionManager(key string) (*EncryptionManager, error) {
|
||||
// Convert key to 32 bytes for AES-256
|
||||
keyHash := sha256.Sum256([]byte(key))
|
||||
|
||||
block, err := aes.NewCipher(keyHash[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
||||
}
|
||||
|
||||
return &EncryptionManager{gcm: gcm}, nil
|
||||
}
|
||||
|
||||
// Encrypt encrypts data using AES-256 GCM
|
||||
func (em *EncryptionManager) Encrypt(plaintext string) (string, error) {
|
||||
if plaintext == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
nonce := make([]byte, em.gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", fmt.Errorf("failed to generate nonce: %w", err)
|
||||
}
|
||||
|
||||
ciphertext := em.gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts data using AES-256 GCM
|
||||
func (em *EncryptionManager) Decrypt(ciphertext string) (string, error) {
|
||||
if ciphertext == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode base64: %w", err)
|
||||
}
|
||||
|
||||
nonceSize := em.gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext_bytes := data[:nonceSize], data[nonceSize:]
|
||||
plaintext, err := em.gcm.Open(nil, nonce, ciphertext_bytes, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decrypt: %w", err)
|
||||
}
|
||||
|
||||
return string(plaintext), nil
|
||||
}
|
||||
|
||||
// EncryptSensitiveData encrypts sensitive data fields
|
||||
func (em *EncryptionManager) EncryptSensitiveData(data map[string]interface{}) (map[string]interface{}, error) {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for key, value := range data {
|
||||
if em.isSensitiveField(key) {
|
||||
strValue, ok := value.(string)
|
||||
if ok {
|
||||
encrypted, err := em.Encrypt(strValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt field %s: %w", key, err)
|
||||
}
|
||||
result[key] = encrypted
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DecryptSensitiveData decrypts sensitive data fields
|
||||
func (em *EncryptionManager) DecryptSensitiveData(data map[string]interface{}) (map[string]interface{}, error) {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for key, value := range data {
|
||||
if em.isSensitiveField(key) {
|
||||
strValue, ok := value.(string)
|
||||
if ok {
|
||||
decrypted, err := em.Decrypt(strValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt field %s: %w", key, err)
|
||||
}
|
||||
result[key] = decrypted
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// isSensitiveField determines if a field contains sensitive data
|
||||
func (em *EncryptionManager) isSensitiveField(fieldName string) bool {
|
||||
sensitiveFields := []string{
|
||||
"password", "secret", "token", "key", "api_key", "private_key",
|
||||
"database_url", "connection_string", "credit_card", "ssn",
|
||||
"social_security", "bank_account", "auth_token", "jwt_secret",
|
||||
"encryption_key", "webhook_secret", "oauth_secret", "access_token",
|
||||
"refresh_token", "client_secret", "private", "confidential",
|
||||
}
|
||||
|
||||
fieldName = strings.ToLower(fieldName)
|
||||
for _, sensitive := range sensitiveFields {
|
||||
if strings.Contains(fieldName, sensitive) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DataRetentionManager handles data retention policies
|
||||
type DataRetentionManager struct {
|
||||
encryptionManager *EncryptionManager
|
||||
}
|
||||
|
||||
// NewDataRetentionManager creates a new data retention manager
|
||||
func NewDataRetentionManager(encryptionManager *EncryptionManager) *DataRetentionManager {
|
||||
return &DataRetentionManager{
|
||||
encryptionManager: encryptionManager,
|
||||
}
|
||||
}
|
||||
|
||||
// RetentionPolicy defines data retention rules
|
||||
type RetentionPolicy struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DataType string `json:"data_type"`
|
||||
RetentionPeriod time.Duration `json:"retention_period"`
|
||||
Action string `json:"action"` // "delete", "anonymize", "archive"
|
||||
Enabled bool `json:"enabled"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// AnonymizedData represents anonymized user data
|
||||
type AnonymizedData struct {
|
||||
OriginalID string `json:"original_id"`
|
||||
AnonymizedID string `json:"anonymized_id"`
|
||||
DataType string `json:"data_type"`
|
||||
AnonymizedAt time.Time `json:"anonymized_at"`
|
||||
RetainedData string `json:"retained_data"` // Encrypted non-sensitive data
|
||||
}
|
||||
|
||||
// AnonymizeUserData anonymizes user data for GDPR compliance
|
||||
func (drm *DataRetentionManager) AnonymizeUserData(userData map[string]interface{}) (*AnonymizedData, error) {
|
||||
anonymizedID := fmt.Sprintf("anon_%d", time.Now().UnixNano())
|
||||
|
||||
// Separate sensitive and non-sensitive data
|
||||
sensitiveData := make(map[string]interface{})
|
||||
nonSensitiveData := make(map[string]interface{})
|
||||
|
||||
for key, value := range userData {
|
||||
if drm.isPersonalData(key) {
|
||||
sensitiveData[key] = value
|
||||
} else {
|
||||
nonSensitiveData[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt non-sensitive data for retention
|
||||
nonSensitiveJSON, _ := json.Marshal(nonSensitiveData)
|
||||
encryptedRetainedData, err := drm.encryptionManager.Encrypt(string(nonSensitiveJSON))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt retained data: %w", err)
|
||||
}
|
||||
|
||||
// Create anonymized record
|
||||
anonymized := &AnonymizedData{
|
||||
OriginalID: fmt.Sprintf("%v", userData["id"]),
|
||||
AnonymizedID: anonymizedID,
|
||||
DataType: "user",
|
||||
AnonymizedAt: time.Now(),
|
||||
RetainedData: encryptedRetainedData,
|
||||
}
|
||||
|
||||
return anonymized, nil
|
||||
}
|
||||
|
||||
// isPersonalData determines if data is personal information under GDPR
|
||||
func (drm *DataRetentionManager) isPersonalData(fieldName string) bool {
|
||||
personalDataFields := []string{
|
||||
"name", "email", "phone", "address", "birthdate", "gender",
|
||||
"ip_address", "user_agent", "location", "biometric", "health",
|
||||
"political", "religious", "sexual", "criminal", "financial",
|
||||
"education", "employment", "family", "social", "behavioral",
|
||||
"identifier", "cookie", "tracking", "profile", "preferences",
|
||||
}
|
||||
|
||||
fieldName = strings.ToLower(fieldName)
|
||||
for _, personal := range personalDataFields {
|
||||
if strings.Contains(fieldName, personal) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ApplyRetentionPolicy applies retention policies to data
|
||||
func (drm *DataRetentionManager) ApplyRetentionPolicy(dataType string, dataTimestamp time.Time, policy RetentionPolicy) string {
|
||||
if !policy.Enabled {
|
||||
return "retain"
|
||||
}
|
||||
|
||||
expiryDate := dataTimestamp.Add(policy.RetentionPeriod)
|
||||
if time.Now().Before(expiryDate) {
|
||||
return "retain"
|
||||
}
|
||||
|
||||
return policy.Action
|
||||
}
|
||||
|
||||
// GenerateDataSubjectReport generates a report of all data held about a user
|
||||
func (drm *DataRetentionManager) GenerateDataSubjectReport(userID string, userData map[string]interface{}) (map[string]interface{}, error) {
|
||||
report := map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"report_generated": time.Now(),
|
||||
"data_categories": drm.categorizeUserData(userData),
|
||||
"retention_policies": drm.getApplicablePolicies(userData),
|
||||
"data_sources": []string{"database", "logs", "analytics"},
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// categorizeUserData categorizes user data by type
|
||||
func (drm *DataRetentionManager) categorizeUserData(userData map[string]interface{}) map[string][]string {
|
||||
categories := map[string][]string{
|
||||
"identity": {},
|
||||
"contact": {},
|
||||
"technical": {},
|
||||
"behavioral": {},
|
||||
"preferences": {},
|
||||
}
|
||||
|
||||
for key := range userData {
|
||||
lowerKey := strings.ToLower(key)
|
||||
|
||||
switch {
|
||||
case strings.Contains(lowerKey, "name") || strings.Contains(lowerKey, "id"):
|
||||
categories["identity"] = append(categories["identity"], key)
|
||||
case strings.Contains(lowerKey, "email") || strings.Contains(lowerKey, "phone"):
|
||||
categories["contact"] = append(categories["contact"], key)
|
||||
case strings.Contains(lowerKey, "ip") || strings.Contains(lowerKey, "agent"):
|
||||
categories["technical"] = append(categories["technical"], key)
|
||||
case strings.Contains(lowerKey, "activity") || strings.Contains(lowerKey, "behavior"):
|
||||
categories["behavioral"] = append(categories["behavioral"], key)
|
||||
case strings.Contains(lowerKey, "preference") || strings.Contains(lowerKey, "setting"):
|
||||
categories["preferences"] = append(categories["preferences"], key)
|
||||
}
|
||||
}
|
||||
|
||||
return categories
|
||||
}
|
||||
|
||||
// getApplicablePolicies returns applicable retention policies
|
||||
func (drm *DataRetentionManager) getApplicablePolicies(userData map[string]interface{}) []string {
|
||||
policies := []string{
|
||||
"user_data_2_years",
|
||||
"analytics_data_6_months",
|
||||
"logs_data_90_days",
|
||||
"deleted_users_30_days",
|
||||
}
|
||||
|
||||
return policies
|
||||
}
|
||||
|
||||
// AuditLogger handles security audit logging
|
||||
type AuditLogger struct {
|
||||
encryptionManager *EncryptionManager
|
||||
}
|
||||
|
||||
// NewAuditLogger creates a new audit logger
|
||||
func NewAuditLogger(encryptionManager *EncryptionManager) *AuditLogger {
|
||||
return &AuditLogger{
|
||||
encryptionManager: encryptionManager,
|
||||
}
|
||||
}
|
||||
|
||||
// AuditEvent represents a security audit event
|
||||
type AuditEvent struct {
|
||||
ID string `json:"id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Resource string `json:"resource"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// LogAuditEvent logs a security audit event
|
||||
func (al *AuditLogger) LogAuditEvent(event AuditEvent) error {
|
||||
event.ID = fmt.Sprintf("audit_%d", time.Now().UnixNano())
|
||||
event.Timestamp = time.Now()
|
||||
|
||||
// Encrypt sensitive details
|
||||
if event.Details != nil {
|
||||
encryptedDetails, err := al.encryptionManager.EncryptSensitiveData(event.Details)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt audit details: %w", err)
|
||||
}
|
||||
event.Details = encryptedDetails
|
||||
}
|
||||
|
||||
// In a real implementation, this would be stored in a secure audit database
|
||||
// For now, we'll just return success
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogSecurityEvent logs security-related events
|
||||
func (al *AuditLogger) LogSecurityEvent(userID, action, resource string, details map[string]interface{}, ipAddress, userAgent string, success bool) error {
|
||||
event := AuditEvent{
|
||||
UserID: userID,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
Details: details,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
Success: success,
|
||||
}
|
||||
|
||||
return al.LogAuditEvent(event)
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"containr/internal/database"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Vulnerability represents a security vulnerability
|
||||
type Vulnerability struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // "dependency", "configuration", "code"
|
||||
Severity string `json:"severity"` // "critical", "high", "medium", "low"
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
ServiceID string `json:"service_id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
Status string `json:"status"` // "open", "resolved", "ignored"
|
||||
FoundAt time.Time `json:"found_at"`
|
||||
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
||||
Metadata string `json:"metadata"` // JSON string for additional data
|
||||
}
|
||||
|
||||
// SecurityScan represents a security scan result
|
||||
type SecurityScan struct {
|
||||
ID string `json:"id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ServiceID *string `json:"service_id,omitempty"`
|
||||
ScanType string `json:"scan_type"` // "dependency", "configuration", "comprehensive"
|
||||
Status string `json:"status"` // "running", "completed", "failed"
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Vulnerabilities []Vulnerability `json:"vulnerabilities"`
|
||||
Summary ScanSummary `json:"summary"`
|
||||
}
|
||||
|
||||
// ScanSummary provides a summary of scan results
|
||||
type ScanSummary struct {
|
||||
Total int `json:"total"`
|
||||
Critical int `json:"critical"`
|
||||
High int `json:"high"`
|
||||
Medium int `json:"medium"`
|
||||
Low int `json:"low"`
|
||||
Score int `json:"score"` // 0-100 security score
|
||||
}
|
||||
|
||||
// Scanner handles security scanning operations
|
||||
type Scanner struct {
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
// NewScanner creates a new security scanner
|
||||
func NewScanner(db *database.DB) *Scanner {
|
||||
return &Scanner{db: db}
|
||||
}
|
||||
|
||||
// StartSecurityScan initiates a security scan
|
||||
func (s *Scanner) StartSecurityScan(projectID, serviceID, scanType string) (*SecurityScan, error) {
|
||||
scanID := uuid.New().String()
|
||||
|
||||
scan := &SecurityScan{
|
||||
ID: scanID,
|
||||
ProjectID: projectID,
|
||||
ScanType: scanType,
|
||||
Status: "running",
|
||||
StartedAt: time.Now(),
|
||||
Summary: ScanSummary{},
|
||||
}
|
||||
|
||||
if serviceID != "" {
|
||||
scan.ServiceID = &serviceID
|
||||
}
|
||||
|
||||
// Insert scan record
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO security_scans (id, project_id, service_id, scan_type, status, started_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`, scan.ID, scan.ProjectID, scan.ServiceID, scan.ScanType, scan.Status, scan.StartedAt)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create security scan: %w", err)
|
||||
}
|
||||
|
||||
// Start scan in background
|
||||
go s.performScan(scan)
|
||||
|
||||
return scan, nil
|
||||
}
|
||||
|
||||
// performScan executes the actual security scan
|
||||
func (s *Scanner) performScan(scan *SecurityScan) {
|
||||
ctx := context.Background()
|
||||
var vulnerabilities []Vulnerability
|
||||
|
||||
switch scan.ScanType {
|
||||
case "dependency":
|
||||
vulnerabilities = s.scanDependencies(ctx, scan)
|
||||
case "configuration":
|
||||
vulnerabilities = s.scanConfiguration(ctx, scan)
|
||||
case "comprehensive":
|
||||
vulnerabilities = s.scanComprehensive(ctx, scan)
|
||||
default:
|
||||
vulnerabilities = []Vulnerability{}
|
||||
}
|
||||
|
||||
// Calculate summary
|
||||
summary := s.calculateSummary(vulnerabilities)
|
||||
|
||||
// Update scan with results
|
||||
completedAt := time.Now()
|
||||
_, err := s.db.Exec(`
|
||||
UPDATE security_scans
|
||||
SET status = $1, completed_at = $2, summary = $3
|
||||
WHERE id = $4
|
||||
`, "completed", completedAt, summaryToJSON(summary), scan.ID)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to update security scan %s: %v", scan.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Store vulnerabilities
|
||||
for _, vuln := range vulnerabilities {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO vulnerabilities (id, type, severity, title, description, service_id, project_id, status, found_at, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
`, vuln.ID, vuln.Type, vuln.Severity, vuln.Title, vuln.Description, vuln.ServiceID, vuln.ProjectID, vuln.Status, vuln.FoundAt, vuln.Metadata)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to store vulnerability %s: %v", vuln.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// scanDependencies scans for known vulnerable dependencies
|
||||
func (s *Scanner) scanDependencies(ctx context.Context, scan *SecurityScan) []Vulnerability {
|
||||
var vulnerabilities []Vulnerability
|
||||
|
||||
// Get project services
|
||||
query := `SELECT id, name FROM services WHERE project_id = $1`
|
||||
args := []interface{}{scan.ProjectID}
|
||||
if scan.ServiceID != nil {
|
||||
query += ` AND id = $2`
|
||||
args = append(args, *scan.ServiceID)
|
||||
}
|
||||
|
||||
rows, err := s.db.Query(query, args...)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to query services for scan: %v", err)
|
||||
return vulnerabilities
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var serviceID, serviceName string
|
||||
if err := rows.Scan(&serviceID, &serviceName); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Simulate dependency scanning (in real implementation, this would check package.json, go.mod, etc.)
|
||||
serviceVulns := s.simulateDependencyScan(serviceID, serviceName, scan.ProjectID)
|
||||
vulnerabilities = append(vulnerabilities, serviceVulns...)
|
||||
}
|
||||
|
||||
return vulnerabilities
|
||||
}
|
||||
|
||||
// simulateDependencyScan simulates scanning for vulnerable dependencies
|
||||
func (s *Scanner) simulateDependencyScan(serviceID, serviceName, projectID string) []Vulnerability {
|
||||
var vulns []Vulnerability
|
||||
|
||||
// Simulate finding some common vulnerabilities
|
||||
commonVulns := []struct {
|
||||
title string
|
||||
description string
|
||||
severity string
|
||||
}{
|
||||
{"Outdated OpenSSL version", "Service uses OpenSSL version with known vulnerabilities", "high"},
|
||||
{"Vulnerable npm package", "Package 'lodash' version < 4.17.21 has prototype pollution vulnerability", "medium"},
|
||||
{"Outdated Go module", "Go module 'net/http' version has security issues", "low"},
|
||||
}
|
||||
|
||||
for i, vuln := range commonVulns {
|
||||
vulns = append(vulns, Vulnerability{
|
||||
ID: uuid.New().String(),
|
||||
Type: "dependency",
|
||||
Severity: vuln.severity,
|
||||
Title: vuln.title,
|
||||
Description: vuln.description,
|
||||
ServiceID: serviceID,
|
||||
ProjectID: projectID,
|
||||
Status: "open",
|
||||
FoundAt: time.Now(),
|
||||
Metadata: fmt.Sprintf(`{"service": "%s", "package": "example-package-%d"}`, serviceName, i+1),
|
||||
})
|
||||
}
|
||||
|
||||
return vulns
|
||||
}
|
||||
|
||||
// scanConfiguration scans for security configuration issues
|
||||
func (s *Scanner) scanConfiguration(ctx context.Context, scan *SecurityScan) []Vulnerability {
|
||||
var vulnerabilities []Vulnerability
|
||||
|
||||
// Check for common configuration issues
|
||||
configIssues := []struct {
|
||||
title string
|
||||
description string
|
||||
severity string
|
||||
}{
|
||||
{"Debug mode enabled", "Application is running in debug mode in production", "high"},
|
||||
{"No rate limiting", "API endpoints lack rate limiting protection", "medium"},
|
||||
{"CORS too permissive", "CORS configuration allows all origins", "medium"},
|
||||
{"Missing security headers", "Security headers (CSP, HSTS) not configured", "low"},
|
||||
}
|
||||
|
||||
for _, issue := range configIssues {
|
||||
vulnerabilities = append(vulnerabilities, Vulnerability{
|
||||
ID: uuid.New().String(),
|
||||
Type: "configuration",
|
||||
Severity: issue.severity,
|
||||
Title: issue.title,
|
||||
Description: issue.description,
|
||||
ServiceID: "", // Project-level issue
|
||||
ProjectID: scan.ProjectID,
|
||||
Status: "open",
|
||||
FoundAt: time.Now(),
|
||||
Metadata: "{}",
|
||||
})
|
||||
}
|
||||
|
||||
return vulnerabilities
|
||||
}
|
||||
|
||||
// scanComprehensive performs a comprehensive security scan
|
||||
func (s *Scanner) scanComprehensive(ctx context.Context, scan *SecurityScan) []Vulnerability {
|
||||
var allVulnerabilities []Vulnerability
|
||||
|
||||
// Run all scan types
|
||||
allVulnerabilities = append(allVulnerabilities, s.scanDependencies(ctx, scan)...)
|
||||
allVulnerabilities = append(allVulnerabilities, s.scanConfiguration(ctx, scan)...)
|
||||
|
||||
return allVulnerabilities
|
||||
}
|
||||
|
||||
// calculateSummary calculates scan summary from vulnerabilities
|
||||
func (s *Scanner) calculateSummary(vulnerabilities []Vulnerability) ScanSummary {
|
||||
summary := ScanSummary{
|
||||
Total: len(vulnerabilities),
|
||||
}
|
||||
|
||||
for _, vuln := range vulnerabilities {
|
||||
switch vuln.Severity {
|
||||
case "critical":
|
||||
summary.Critical++
|
||||
case "high":
|
||||
summary.High++
|
||||
case "medium":
|
||||
summary.Medium++
|
||||
case "low":
|
||||
summary.Low++
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate security score (0-100, higher is better)
|
||||
if summary.Total == 0 {
|
||||
summary.Score = 100
|
||||
} else {
|
||||
deduction := (summary.Critical * 25) + (summary.High * 15) + (summary.Medium * 8) + (summary.Low * 3)
|
||||
summary.Score = max(0, 100-deduction)
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// GetSecurityScan retrieves a security scan by ID
|
||||
func (s *Scanner) GetSecurityScan(scanID string) (*SecurityScan, error) {
|
||||
var scan SecurityScan
|
||||
var summaryJSON sql.NullString
|
||||
var completedAt sql.NullTime
|
||||
|
||||
err := s.db.QueryRow(`
|
||||
SELECT id, project_id, service_id, scan_type, status, started_at, completed_at, summary
|
||||
FROM security_scans WHERE id = $1
|
||||
`, scanID).Scan(&scan.ID, &scan.ProjectID, &scan.ServiceID, &scan.ScanType, &scan.Status, &scan.StartedAt, &completedAt, &summaryJSON)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if completedAt.Valid {
|
||||
scan.CompletedAt = &completedAt.Time
|
||||
}
|
||||
|
||||
if summaryJSON.Valid {
|
||||
scan.Summary = jsonToSummary(summaryJSON.String)
|
||||
}
|
||||
|
||||
// Load vulnerabilities
|
||||
vulns, err := s.getVulnerabilitiesForScan(scan.ID)
|
||||
if err == nil {
|
||||
scan.Vulnerabilities = vulns
|
||||
}
|
||||
|
||||
return &scan, nil
|
||||
}
|
||||
|
||||
// getVulnerabilitiesForScan retrieves vulnerabilities for a scan
|
||||
func (s *Scanner) getVulnerabilitiesForScan(scanID string) ([]Vulnerability, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, type, severity, title, description, service_id, project_id, status, found_at, resolved_at, metadata
|
||||
FROM vulnerabilities WHERE project_id = (SELECT project_id FROM security_scans WHERE id = $1)
|
||||
ORDER BY severity DESC, found_at DESC
|
||||
`, scanID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var vulnerabilities []Vulnerability
|
||||
for rows.Next() {
|
||||
var vuln Vulnerability
|
||||
var resolvedAt sql.NullTime
|
||||
|
||||
err := rows.Scan(&vuln.ID, &vuln.Type, &vuln.Severity, &vuln.Title, &vuln.Description,
|
||||
&vuln.ServiceID, &vuln.ProjectID, &vuln.Status, &vuln.FoundAt, &resolvedAt, &vuln.Metadata)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if resolvedAt.Valid {
|
||||
vuln.ResolvedAt = &resolvedAt.Time
|
||||
}
|
||||
|
||||
vulnerabilities = append(vulnerabilities, vuln)
|
||||
}
|
||||
|
||||
return vulnerabilities, nil
|
||||
}
|
||||
|
||||
// GetProjectSecurityHistory retrieves security scan history for a project
|
||||
func (s *Scanner) GetProjectSecurityHistory(projectID string, limit int) ([]SecurityScan, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, project_id, service_id, scan_type, status, started_at, completed_at, summary
|
||||
FROM security_scans
|
||||
WHERE project_id = $1
|
||||
ORDER BY started_at DESC
|
||||
LIMIT $2
|
||||
`, projectID, limit)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var scans []SecurityScan
|
||||
for rows.Next() {
|
||||
var scan SecurityScan
|
||||
var summaryJSON sql.NullString
|
||||
var completedAt sql.NullTime
|
||||
|
||||
err := rows.Scan(&scan.ID, &scan.ProjectID, &scan.ServiceID, &scan.ScanType, &scan.Status,
|
||||
&scan.StartedAt, &completedAt, &summaryJSON)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if completedAt.Valid {
|
||||
scan.CompletedAt = &completedAt.Time
|
||||
}
|
||||
|
||||
if summaryJSON.Valid {
|
||||
scan.Summary = jsonToSummary(summaryJSON.String)
|
||||
}
|
||||
|
||||
scans = append(scans, scan)
|
||||
}
|
||||
|
||||
return scans, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func summaryToJSON(summary ScanSummary) string {
|
||||
data, _ := json.Marshal(summary)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func jsonToSummary(jsonStr string) ScanSummary {
|
||||
var summary ScanSummary
|
||||
json.Unmarshal([]byte(jsonStr), &summary)
|
||||
return summary
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// BuildRequest represents a request to build a container image
|
||||
type BuildRequest struct {
|
||||
// Build configuration
|
||||
BuildType string `json:"build_type"` // nixpacks, dockerfile, prebuilt
|
||||
SourcePath string `json:"source_path"` // Path to source code
|
||||
PrebuiltImage string `json:"prebuilt_image"` // Prebuilt image name
|
||||
ImageName string `json:"image_name"` // Output image name
|
||||
ImageTag string `json:"image_tag"` // Output image tag
|
||||
RegistryURL string `json:"registry_url"` // Registry URL for pushing
|
||||
|
||||
// Build commands
|
||||
BuildCommand string `json:"build_command"` // Custom build command
|
||||
StartCommand string `json:"start_command"` // Custom start command
|
||||
|
||||
// Environment and configuration
|
||||
Environment map[string]string `json:"environment"` // Build environment variables
|
||||
BuildArgs map[string]string `json:"build_args"` // Docker build args
|
||||
Labels map[string]string `json:"labels"` // Image labels
|
||||
|
||||
// Context
|
||||
ProjectID string `json:"project_id"` // Project ID
|
||||
ServiceID string `json:"service_id"` // Service ID
|
||||
DeploymentID string `json:"deployment_id"` // Deployment ID
|
||||
TriggeredBy string `json:"triggered_by"` // Who triggered the build
|
||||
Branch string `json:"branch"` // Git branch
|
||||
Commit string `json:"commit"` // Git commit SHA
|
||||
}
|
||||
|
||||
// BuildResponse represents the response from a build operation
|
||||
type BuildResponse struct {
|
||||
ImageName string `json:"image_name"` // Built image name
|
||||
ImageTag string `json:"image_tag"` // Image tag
|
||||
Size int64 `json:"size"` // Image size in bytes
|
||||
Digest string `json:"digest"` // Image digest
|
||||
BuildTime time.Time `json:"build_time"` // When build completed
|
||||
BuildLog string `json:"build_log"` // Build logs
|
||||
Success bool `json:"success"` // Whether build succeeded
|
||||
Error string `json:"error"` // Error message if failed
|
||||
}
|
||||
|
||||
// BuildStatus represents the status of a build
|
||||
type BuildStatus struct {
|
||||
ID string `json:"id"` // Build ID
|
||||
ProjectID string `json:"project_id"` // Project ID
|
||||
ServiceID string `json:"service_id"` // Service ID
|
||||
DeploymentID string `json:"deployment_id"` // Deployment ID
|
||||
Status string `json:"status"` // pending, building, success, failed
|
||||
Progress int `json:"progress"` // Build progress 0-100
|
||||
StartedAt time.Time `json:"started_at"` // When build started
|
||||
CompletedAt *time.Time `json:"completed_at"` // When build completed
|
||||
ImageName string `json:"image_name"` // Built image name
|
||||
ImageTag string `json:"image_tag"` // Image tag
|
||||
Size int64 `json:"size"` // Image size in bytes
|
||||
Error string `json:"error"` // Error message if failed
|
||||
Log string `json:"log"` // Build logs
|
||||
Metadata map[string]string `json:"metadata"` // Additional metadata
|
||||
}
|
||||
|
||||
// BuildPlan represents a build plan for inspection
|
||||
type BuildPlan struct {
|
||||
BuildType string `json:"build_type"` // Type of build
|
||||
Runtime string `json:"runtime"` // Detected runtime
|
||||
Builder string `json:"builder"` // Builder to use
|
||||
Steps []BuildStep `json:"steps"` // Build steps
|
||||
Environment map[string]string `json:"environment"` // Environment variables
|
||||
Dependencies []string `json:"dependencies"` // Dependencies
|
||||
Estimate BuildEstimate `json:"estimate"` // Build time/size estimate
|
||||
}
|
||||
|
||||
// BuildStep represents a single step in the build process
|
||||
type BuildStep struct {
|
||||
Name string `json:"name"` // Step name
|
||||
Command string `json:"command"` // Command to run
|
||||
Args []string `json:"args"` // Command arguments
|
||||
Environment map[string]string `json:"environment"` // Step-specific environment
|
||||
Timeout time.Duration `json:"timeout"` // Step timeout
|
||||
Critical bool `json:"critical"` // Whether step is critical
|
||||
}
|
||||
|
||||
// BuildEstimate provides estimates for build time and size
|
||||
type BuildEstimate struct {
|
||||
Duration time.Duration `json:"duration"` // Estimated build duration
|
||||
ImageSize int64 `json:"image_size"` // Estimated image size
|
||||
Confidence float64 `json:"confidence"` // Confidence in estimate (0-1)
|
||||
}
|
||||
|
||||
// BuildCache represents build cache information
|
||||
type BuildCache struct {
|
||||
Key string `json:"key"` // Cache key
|
||||
Path string `json:"path"` // Cache path
|
||||
Size int64 `json:"size"` // Cache size in bytes
|
||||
CreatedAt time.Time `json:"created_at"` // When cache was created
|
||||
AccessedAt time.Time `json:"accessed_at"` // When cache was last accessed
|
||||
Metadata map[string]string `json:"metadata"` // Cache metadata
|
||||
}
|
||||
|
||||
// BuildTrigger represents what triggered a build
|
||||
type BuildTrigger struct {
|
||||
Type string `json:"type"` // webhook, manual, api, scheduled
|
||||
Source string `json:"source"` // Source of trigger
|
||||
User string `json:"user"` // User who triggered (if applicable)
|
||||
Data map[string]string `json:"data"` // Trigger-specific data
|
||||
Timestamp time.Time `json:"timestamp"` // When trigger occurred
|
||||
}
|
||||
|
||||
// BuildConfig represents global build configuration
|
||||
type BuildConfig struct {
|
||||
DefaultBuilder string `json:"default_builder"` // Default builder type
|
||||
CacheEnabled bool `json:"cache_enabled"` // Whether build caching is enabled
|
||||
CacheSizeLimit int64 `json:"cache_size_limit"` // Cache size limit in bytes
|
||||
ParallelBuilds int `json:"parallel_builds"` // Max parallel builds
|
||||
Timeout time.Duration `json:"timeout"` // Default build timeout
|
||||
Registry string `json:"registry"` // Default registry
|
||||
Environment map[string]string `json:"environment"` // Default environment variables
|
||||
AllowedRegistries []string `json:"allowed_registries"` // Allowed container registries
|
||||
}
|
||||
|
||||
// BuildMetric represents build metrics for monitoring
|
||||
type BuildMetric struct {
|
||||
Timestamp time.Time `json:"timestamp"` // When metric was recorded
|
||||
BuildCount int `json:"build_count"` // Number of builds
|
||||
SuccessCount int `json:"success_count"` // Number of successful builds
|
||||
FailureCount int `json:"failure_count"` // Number of failed builds
|
||||
AvgDuration time.Duration `json:"avg_duration"` // Average build duration
|
||||
AvgSize int64 `json:"avg_size"` // Average image size
|
||||
}
|
||||
|
||||
// BuildQueue represents the build queue status
|
||||
type BuildQueue struct {
|
||||
Running int `json:"running"` // Currently running builds
|
||||
Pending int `json:"pending"` // Pending builds in queue
|
||||
Completed int `json:"completed"` // Completed builds
|
||||
Failed int `json:"failed"` // Failed builds
|
||||
Capacity int `json:"capacity"` // Queue capacity
|
||||
WaitTime time.Duration `json:"wait_time"` // Average wait time
|
||||
}
|
||||
Reference in New Issue
Block a user