2026-01-22 14:39:50 +02:00

266 lines
8.9 KiB
Go

package deployments
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/DeBrosOfficial/network/pkg/deployments"
"github.com/DeBrosOfficial/network/pkg/rqlite"
"github.com/google/uuid"
"go.uber.org/zap"
)
// DeploymentService manages deployment operations
type DeploymentService struct {
db rqlite.Client
homeNodeManager *deployments.HomeNodeManager
portAllocator *deployments.PortAllocator
logger *zap.Logger
}
// NewDeploymentService creates a new deployment service
func NewDeploymentService(
db rqlite.Client,
homeNodeManager *deployments.HomeNodeManager,
portAllocator *deployments.PortAllocator,
logger *zap.Logger,
) *DeploymentService {
return &DeploymentService{
db: db,
homeNodeManager: homeNodeManager,
portAllocator: portAllocator,
logger: logger,
}
}
// CreateDeployment creates a new deployment
func (s *DeploymentService) CreateDeployment(ctx context.Context, deployment *deployments.Deployment) error {
// Assign home node if not already assigned
if deployment.HomeNodeID == "" {
homeNodeID, err := s.homeNodeManager.AssignHomeNode(ctx, deployment.Namespace)
if err != nil {
return fmt.Errorf("failed to assign home node: %w", err)
}
deployment.HomeNodeID = homeNodeID
}
// Allocate port for dynamic deployments
if deployment.Type != deployments.DeploymentTypeStatic && deployment.Type != deployments.DeploymentTypeNextJSStatic {
port, err := s.portAllocator.AllocatePort(ctx, deployment.HomeNodeID, deployment.ID)
if err != nil {
return fmt.Errorf("failed to allocate port: %w", err)
}
deployment.Port = port
}
// Serialize environment variables
envJSON, err := json.Marshal(deployment.Environment)
if err != nil {
return fmt.Errorf("failed to marshal environment: %w", err)
}
// Insert deployment
query := `
INSERT INTO deployments (
id, namespace, name, type, version, status,
content_cid, build_cid, home_node_id, port, subdomain, environment,
memory_limit_mb, cpu_limit_percent, disk_limit_mb,
health_check_path, health_check_interval, restart_policy, max_restart_count,
created_at, updated_at, deployed_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
_, err = s.db.Exec(ctx, query,
deployment.ID, deployment.Namespace, deployment.Name, deployment.Type, deployment.Version, deployment.Status,
deployment.ContentCID, deployment.BuildCID, deployment.HomeNodeID, deployment.Port, deployment.Subdomain, string(envJSON),
deployment.MemoryLimitMB, deployment.CPULimitPercent, deployment.DiskLimitMB,
deployment.HealthCheckPath, deployment.HealthCheckInterval, deployment.RestartPolicy, deployment.MaxRestartCount,
deployment.CreatedAt, deployment.UpdatedAt, deployment.DeployedBy,
)
if err != nil {
return fmt.Errorf("failed to insert deployment: %w", err)
}
// Record in history
s.recordHistory(ctx, deployment, "deployed")
s.logger.Info("Deployment created",
zap.String("id", deployment.ID),
zap.String("namespace", deployment.Namespace),
zap.String("name", deployment.Name),
zap.String("type", string(deployment.Type)),
zap.String("home_node", deployment.HomeNodeID),
zap.Int("port", deployment.Port),
)
return nil
}
// GetDeployment retrieves a deployment by namespace and name
func (s *DeploymentService) GetDeployment(ctx context.Context, namespace, name string) (*deployments.Deployment, error) {
type deploymentRow struct {
ID string `db:"id"`
Namespace string `db:"namespace"`
Name string `db:"name"`
Type string `db:"type"`
Version int `db:"version"`
Status string `db:"status"`
ContentCID string `db:"content_cid"`
BuildCID string `db:"build_cid"`
HomeNodeID string `db:"home_node_id"`
Port int `db:"port"`
Subdomain string `db:"subdomain"`
Environment string `db:"environment"`
MemoryLimitMB int `db:"memory_limit_mb"`
CPULimitPercent int `db:"cpu_limit_percent"`
DiskLimitMB int `db:"disk_limit_mb"`
HealthCheckPath string `db:"health_check_path"`
HealthCheckInterval int `db:"health_check_interval"`
RestartPolicy string `db:"restart_policy"`
MaxRestartCount int `db:"max_restart_count"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeployedBy string `db:"deployed_by"`
}
var rows []deploymentRow
query := `SELECT * FROM deployments WHERE namespace = ? AND name = ? LIMIT 1`
err := s.db.Query(ctx, &rows, query, namespace, name)
if err != nil {
return nil, fmt.Errorf("failed to query deployment: %w", err)
}
if len(rows) == 0 {
return nil, deployments.ErrDeploymentNotFound
}
row := rows[0]
var env map[string]string
if err := json.Unmarshal([]byte(row.Environment), &env); err != nil {
env = make(map[string]string)
}
return &deployments.Deployment{
ID: row.ID,
Namespace: row.Namespace,
Name: row.Name,
Type: deployments.DeploymentType(row.Type),
Version: row.Version,
Status: deployments.DeploymentStatus(row.Status),
ContentCID: row.ContentCID,
BuildCID: row.BuildCID,
HomeNodeID: row.HomeNodeID,
Port: row.Port,
Subdomain: row.Subdomain,
Environment: env,
MemoryLimitMB: row.MemoryLimitMB,
CPULimitPercent: row.CPULimitPercent,
DiskLimitMB: row.DiskLimitMB,
HealthCheckPath: row.HealthCheckPath,
HealthCheckInterval: row.HealthCheckInterval,
RestartPolicy: deployments.RestartPolicy(row.RestartPolicy),
MaxRestartCount: row.MaxRestartCount,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
DeployedBy: row.DeployedBy,
}, nil
}
// CreateDNSRecords creates DNS records for a deployment
func (s *DeploymentService) CreateDNSRecords(ctx context.Context, deployment *deployments.Deployment) error {
// Get node IP
nodeIP, err := s.getNodeIP(ctx, deployment.HomeNodeID)
if err != nil {
s.logger.Error("Failed to get node IP", zap.Error(err))
return err
}
// Create node-specific record
nodeFQDN := fmt.Sprintf("%s.%s.orama.network.", deployment.Name, deployment.HomeNodeID)
if err := s.createDNSRecord(ctx, nodeFQDN, "A", nodeIP, deployment.Namespace, deployment.ID); err != nil {
s.logger.Error("Failed to create node-specific DNS record", zap.Error(err))
}
// Create load-balanced record if subdomain is set
if deployment.Subdomain != "" {
lbFQDN := fmt.Sprintf("%s.orama.network.", deployment.Subdomain)
if err := s.createDNSRecord(ctx, lbFQDN, "A", nodeIP, deployment.Namespace, deployment.ID); err != nil {
s.logger.Error("Failed to create load-balanced DNS record", zap.Error(err))
}
}
return nil
}
// createDNSRecord creates a single DNS record
func (s *DeploymentService) createDNSRecord(ctx context.Context, fqdn, recordType, value, namespace, deploymentID string) error {
query := `
INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, deployment_id, is_active, created_at, updated_at, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(fqdn) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
`
now := time.Now()
_, err := s.db.Exec(ctx, query, fqdn, recordType, value, 300, namespace, deploymentID, true, now, now, "system")
return err
}
// getNodeIP retrieves the IP address for a node
func (s *DeploymentService) getNodeIP(ctx context.Context, nodeID string) (string, error) {
type nodeRow struct {
IPAddress string `db:"ip_address"`
}
var rows []nodeRow
query := `SELECT ip_address FROM dns_nodes WHERE id = ? LIMIT 1`
err := s.db.Query(ctx, &rows, query, nodeID)
if err != nil {
return "", err
}
if len(rows) == 0 {
return "", fmt.Errorf("node not found: %s", nodeID)
}
return rows[0].IPAddress, nil
}
// BuildDeploymentURLs builds all URLs for a deployment
func (s *DeploymentService) BuildDeploymentURLs(deployment *deployments.Deployment) []string {
urls := []string{
fmt.Sprintf("https://%s.%s.orama.network", deployment.Name, deployment.HomeNodeID),
}
if deployment.Subdomain != "" {
urls = append(urls, fmt.Sprintf("https://%s.orama.network", deployment.Subdomain))
}
return urls
}
// recordHistory records deployment history
func (s *DeploymentService) recordHistory(ctx context.Context, deployment *deployments.Deployment, status string) {
query := `
INSERT INTO deployment_history (id, deployment_id, version, content_cid, build_cid, deployed_at, deployed_by, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`
_, err := s.db.Exec(ctx, query,
uuid.New().String(),
deployment.ID,
deployment.Version,
deployment.ContentCID,
deployment.BuildCID,
time.Now(),
deployment.DeployedBy,
status,
)
if err != nil {
s.logger.Error("Failed to record history", zap.Error(err))
}
}