small fix, don't worry about it

This commit is contained in:
Tomas Dvorak
2026-04-10 12:02:36 +02:00
parent 08bd0c6e5c
commit 08cb5754f3
638 changed files with 57332 additions and 34706 deletions
-679
View File
@@ -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)
}
}
-177
View File
@@ -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
}
-220
View File
@@ -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
}
-368
View File
@@ -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),
})
}
-416
View File
@@ -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
}
-543
View File
@@ -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)
}
-569
View File
@@ -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",
})
}
-458
View File
@@ -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()
}
-607
View File
@@ -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",
})
}
-244
View File
@@ -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
}
-13
View File
@@ -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 ""
}
-617
View File
@@ -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,
})
}
-396
View File
@@ -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"})
}
-480
View File
@@ -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",
})
}
-236
View File
@@ -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)
}
}
}
-455
View File
@@ -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,
})
}
-612
View File
@@ -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
}
-458
View File
@@ -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"})
}
-284
View File
@@ -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
}
-207
View File
@@ -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"})
}
-270
View File
@@ -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)
}
-103
View File
@@ -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
}
-234
View File
@@ -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
}
-75
View File
@@ -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
}
-41
View File
@@ -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")
}
-84
View File
@@ -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
}
-155
View File
@@ -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
}
-59
View File
@@ -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()
}
-54
View File
@@ -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
}
-490
View File
@@ -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())
}
-718
View File
@@ -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
}
-379
View File
@@ -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),
}
}
-363
View File
@@ -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 ""
}
-281
View File
@@ -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"`
}
-736
View File
@@ -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"
}
-473
View File
@@ -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
}
-553
View File
@@ -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
}
-126
View File
@@ -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()
}
}
-361
View File
@@ -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)
}
-447
View File
@@ -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]
}
-441
View File
@@ -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)
}
-334
View File
@@ -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
}
-456
View File
@@ -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
}
-262
View File
@@ -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"`
}
-581
View File
@@ -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(),
}
}
-500
View File
@@ -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
}
-358
View File
@@ -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)
}
-409
View File
@@ -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
}
-140
View File
@@ -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
}