mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-03 20:12:58 +00:00
719 lines
20 KiB
Go
719 lines
20 KiB
Go
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
|
|
}
|