mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-03 20:12:58 +00:00
feat: initial implementation of container management platform
This commit is contained in:
@@ -0,0 +1,718 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user