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()) }