deployments.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. package services
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "time"
  7. "git.linuxforward.com/byop/byop-engine/cloud"
  8. "git.linuxforward.com/byop/byop-engine/dbstore"
  9. "git.linuxforward.com/byop/byop-engine/models"
  10. "github.com/google/uuid"
  11. "github.com/sirupsen/logrus"
  12. )
  13. // DeploymentService handles business logic for deployments
  14. type DeploymentService struct {
  15. store *dbstore.DeploymentStore
  16. componentStore *dbstore.ComponentStore
  17. blueprintStore *dbstore.BlueprintStore
  18. clientStore *dbstore.ClientStore
  19. logger *logrus.Entry
  20. }
  21. // NewDeploymentService creates a new DeploymentService
  22. func NewDeploymentService(
  23. store *dbstore.DeploymentStore,
  24. componentStore *dbstore.ComponentStore,
  25. blueprintStore *dbstore.BlueprintStore,
  26. clientStore *dbstore.ClientStore,
  27. ) *DeploymentService {
  28. return &DeploymentService{
  29. store: store,
  30. componentStore: componentStore,
  31. blueprintStore: blueprintStore,
  32. clientStore: clientStore,
  33. logger: logrus.WithField("component", "deployment_service"),
  34. }
  35. }
  36. // CreateDeployment creates a new deployment
  37. func (s *DeploymentService) CreateDeployment(deployment *models.Deployment) error {
  38. // Generate UUID if not provided
  39. if deployment.ID == "" {
  40. deployment.ID = uuid.New().String()
  41. }
  42. // Validate the deployment
  43. if err := s.validateDeployment(deployment); err != nil {
  44. return fmt.Errorf("invalid deployment: %w", err)
  45. }
  46. // Set appropriate status
  47. deployment.Status = string(models.PENDING_DEPLOYMENT)
  48. // Set timestamps
  49. now := time.Now()
  50. deployment.CreatedAt = now
  51. deployment.UpdatedAt = now
  52. // Get instance from pool
  53. ctx := context.Background()
  54. return s.createDeploymentWithNewInstance(ctx, deployment)
  55. }
  56. // createDeploymentWithNewInstance creates a new instance directly from the provider
  57. func (s *DeploymentService) createDeploymentWithNewInstance(ctx context.Context, deployment *models.Deployment) error {
  58. // Get provider
  59. provider, ok := cloud.GetProvider(deployment.Provider)
  60. if !ok {
  61. return fmt.Errorf("provider %s not found", deployment.Provider)
  62. }
  63. blueprint, err := s.blueprintStore.GetByID(deployment.BlueprintID)
  64. if err != nil {
  65. return fmt.Errorf("failed to retrieve blueprint: %w", err)
  66. }
  67. if blueprint == nil {
  68. return fmt.Errorf("blueprint with ID %s not found", deployment.BlueprintID)
  69. }
  70. var components []models.Component
  71. for _, component := range blueprint.Config.Components {
  72. fmt.Printf("Component ID: %s\n", component.ID)
  73. comp, err := s.componentStore.GetByID(component.ID)
  74. if err != nil {
  75. fmt.Errorf("failed to retrieve component: %w", err)
  76. continue
  77. }
  78. if comp == nil {
  79. fmt.Errorf("component with ID %s not found", component.ID)
  80. continue
  81. }
  82. components = append(components, *comp)
  83. }
  84. // Create instance options
  85. instanceOpts := cloud.InstanceCreateOpts{
  86. Name: fmt.Sprintf("deployment-%s", deployment.ID[:8]),
  87. Components: components,
  88. Tags: map[string]string{
  89. "managed-by": "byop-platform",
  90. "deployment-id": deployment.ID,
  91. "client-id": deployment.ClientID,
  92. // "component-id": deployment.ComponentID,
  93. "blueprint-id": deployment.BlueprintID,
  94. },
  95. }
  96. // Create the instance
  97. s.logger.WithFields(logrus.Fields{
  98. "name": instanceOpts.Name,
  99. "provider": deployment.Provider,
  100. "region": deployment.Region,
  101. "size": deployment.InstanceType,
  102. }).Info("Creating new instance for deployment")
  103. instance, err := provider.GetFirstFreeInstance(ctx)
  104. if err != nil {
  105. return fmt.Errorf("failed to create instance: %w", err)
  106. }
  107. // Update deployment with instance info
  108. deployment.InstanceID = instance.ID
  109. deployment.IPAddress = instance.IPAddress
  110. // Save the deployment
  111. if err := s.store.CreateDeployment(deployment); err != nil {
  112. // If we fail to save deployment, try to delete the instance
  113. _ = provider.ResetInstance(ctx, instance.ID)
  114. return fmt.Errorf("failed to save deployment: %w", err)
  115. }
  116. // Start deployment process asynchronously
  117. go s.processDeployment(ctx, deployment)
  118. return nil
  119. }
  120. // GetDeployment retrieves a deployment by ID
  121. func (s *DeploymentService) GetDeployment(id string) (*models.Deployment, error) {
  122. deployment, err := s.store.GetByID(id)
  123. if err != nil {
  124. return nil, fmt.Errorf("failed to retrieve deployment: %w", err)
  125. }
  126. if deployment != nil {
  127. // Deserialize config fields
  128. if err := s.deserializeConfigFields(deployment); err != nil {
  129. return nil, fmt.Errorf("failed to deserialize config fields: %w", err)
  130. }
  131. }
  132. return deployment, nil
  133. }
  134. // UpdateDeployment updates an existing deployment
  135. func (s *DeploymentService) UpdateDeployment(deployment *models.Deployment) error {
  136. // Validate the deployment ID
  137. if deployment.ID == "" {
  138. return fmt.Errorf("deployment ID is required for update")
  139. }
  140. // Check if deployment exists
  141. existingDeployment, err := s.store.GetByID(deployment.ID)
  142. if err != nil {
  143. return fmt.Errorf("failed to check if deployment exists: %w", err)
  144. }
  145. if existingDeployment == nil {
  146. return fmt.Errorf("deployment with ID %s not found", deployment.ID)
  147. }
  148. // Prevent updates to deployed apps if deployment is not in the right state
  149. if existingDeployment.Status != string(models.PENDING_DEPLOYMENT) &&
  150. existingDeployment.Status != string(models.FAILED_DEPLOYMENT) &&
  151. len(deployment.DeployedComponents) > 0 {
  152. return fmt.Errorf("cannot update deployed apps when deployment is in %s state", existingDeployment.Status)
  153. }
  154. // Validate the deployment
  155. if err := s.validateDeployment(deployment); err != nil {
  156. return fmt.Errorf("invalid deployment: %w", err)
  157. }
  158. // If status was updated to "deploying", update LastDeployedAt
  159. if existingDeployment.Status != string(models.DEPLOYING) &&
  160. deployment.Status == string(models.DEPLOYING) {
  161. deployment.LastDeployedAt = time.Now()
  162. }
  163. // Handle deployed apps setup
  164. if err := s.setupDeployedComponents(deployment); err != nil {
  165. return fmt.Errorf("failed to setup deployed apps: %w", err)
  166. }
  167. // Persist the deployment
  168. if err := s.store.Update(deployment); err != nil {
  169. return fmt.Errorf("failed to update deployment: %w", err)
  170. }
  171. return nil
  172. }
  173. // DeleteDeployment deletes a deployment by ID
  174. func (s *DeploymentService) DeleteDeployment(id string) error {
  175. // Check if deployment exists
  176. deployment, err := s.store.GetByID(id)
  177. if err != nil {
  178. return fmt.Errorf("failed to check if deployment exists: %w", err)
  179. }
  180. if deployment == nil {
  181. return fmt.Errorf("deployment with ID %s not found", id)
  182. }
  183. // Set status to deleting
  184. deployment.Status = string(models.DELETING)
  185. if err := s.store.Update(deployment); err != nil {
  186. return fmt.Errorf("failed to update deployment status: %w", err)
  187. }
  188. // Trigger cleanup process (this would normally be asynchronous)
  189. // This is a placeholder for your actual cleanup logic
  190. go s.processDeploymentCleanup(id)
  191. return nil
  192. }
  193. // ListDeployments retrieves all deployments with optional filtering
  194. func (s *DeploymentService) ListDeployments(filter map[string]interface{}) ([]*models.Deployment, error) {
  195. deployments, err := s.store.List(filter)
  196. if err != nil {
  197. return nil, fmt.Errorf("failed to list deployments: %w", err)
  198. }
  199. // Deserialize config fields for each deployment
  200. for _, deployment := range deployments {
  201. if err := s.deserializeConfigFields(deployment); err != nil {
  202. return nil, fmt.Errorf("failed to deserialize config fields: %w", err)
  203. }
  204. }
  205. return deployments, nil
  206. }
  207. // GetDeploymentsByClientID retrieves deployments for a specific client
  208. func (s *DeploymentService) GetDeploymentsByClientID(clientID string) ([]*models.Deployment, error) {
  209. // Check if client exists
  210. client, err := s.clientStore.GetByID(clientID)
  211. if err != nil {
  212. return nil, fmt.Errorf("failed to check if client exists: %w", err)
  213. }
  214. if client == nil {
  215. return nil, fmt.Errorf("client with ID %s not found", clientID)
  216. }
  217. deployments, err := s.store.GetByClientID(clientID)
  218. if err != nil {
  219. return nil, fmt.Errorf("failed to retrieve deployments for client %s: %w", clientID, err)
  220. }
  221. // Deserialize config fields for each deployment
  222. for _, deployment := range deployments {
  223. if err := s.deserializeConfigFields(deployment); err != nil {
  224. return nil, fmt.Errorf("failed to deserialize config fields: %w", err)
  225. }
  226. }
  227. return deployments, nil
  228. }
  229. // GetDeploymentsByUserID retrieves deployments created by a specific user
  230. func (s *DeploymentService) GetDeploymentsByUserID(userID string) ([]*models.Deployment, error) {
  231. deployments, err := s.store.GetByUserID(userID)
  232. if err != nil {
  233. return nil, fmt.Errorf("failed to retrieve deployments for user %s: %w", userID, err)
  234. }
  235. // Deserialize config fields for each deployment
  236. for _, deployment := range deployments {
  237. if err := s.deserializeConfigFields(deployment); err != nil {
  238. return nil, fmt.Errorf("failed to deserialize config fields: %w", err)
  239. }
  240. }
  241. return deployments, nil
  242. }
  243. // GetDeploymentsByBlueprintID retrieves deployments based on a specific blueprint
  244. func (s *DeploymentService) GetDeploymentsByBlueprintID(blueprintID string) ([]*models.Deployment, error) {
  245. // Check if blueprint exists
  246. blueprint, err := s.blueprintStore.GetByID(blueprintID)
  247. if err != nil {
  248. return nil, fmt.Errorf("failed to check if blueprint exists: %w", err)
  249. }
  250. if blueprint == nil {
  251. return nil, fmt.Errorf("blueprint with ID %s not found", blueprintID)
  252. }
  253. deployments, err := s.store.GetByBlueprintID(blueprintID)
  254. if err != nil {
  255. return nil, fmt.Errorf("failed to retrieve deployments for blueprint %s: %w", blueprintID, err)
  256. }
  257. // Deserialize config fields for each deployment
  258. for _, deployment := range deployments {
  259. if err := s.deserializeConfigFields(deployment); err != nil {
  260. return nil, fmt.Errorf("failed to deserialize config fields: %w", err)
  261. }
  262. }
  263. return deployments, nil
  264. }
  265. // UpdateDeploymentStatus updates the status of a deployment
  266. func (s *DeploymentService) UpdateDeploymentStatus(id string, status string) error {
  267. deployment, err := s.store.GetByID(id)
  268. if err != nil {
  269. return fmt.Errorf("failed to retrieve deployment: %w", err)
  270. }
  271. if deployment == nil {
  272. return fmt.Errorf("deployment with ID %s not found", id)
  273. }
  274. // Update the status
  275. deployment.Status = status
  276. // If status is being set to "deploying", update LastDeployedAt
  277. if status == string(models.DEPLOYING) {
  278. deployment.LastDeployedAt = time.Now()
  279. }
  280. if err := s.store.Update(deployment); err != nil {
  281. return fmt.Errorf("failed to update deployment status: %w", err)
  282. }
  283. return nil
  284. }
  285. // validateDeployment validates a deployment
  286. func (s *DeploymentService) validateDeployment(deployment *models.Deployment) error {
  287. // Validate required fields
  288. if deployment.Name == "" {
  289. return fmt.Errorf("deployment name is required")
  290. }
  291. // Validate relationships
  292. if deployment.ClientID == "" {
  293. return fmt.Errorf("client ID is required")
  294. }
  295. client, err := s.clientStore.GetByID(deployment.ClientID)
  296. if err != nil {
  297. return fmt.Errorf("failed to check client: %w", err)
  298. }
  299. if client == nil {
  300. return fmt.Errorf("client with ID %s not found", deployment.ClientID)
  301. }
  302. if deployment.BlueprintID == "" {
  303. return fmt.Errorf("blueprint ID is required")
  304. }
  305. blueprint, err := s.blueprintStore.GetByID(deployment.BlueprintID)
  306. if err != nil {
  307. return fmt.Errorf("failed to check blueprint: %w", err)
  308. }
  309. if blueprint == nil {
  310. return fmt.Errorf("blueprint with ID %s not found", deployment.BlueprintID)
  311. }
  312. return nil
  313. }
  314. // setupDeployedComponents sets up deployed apps based on the blueprint
  315. func (s *DeploymentService) setupDeployedComponents(deployment *models.Deployment) error {
  316. // If deployment already has deployed apps defined, we assume they're set up correctly
  317. if len(deployment.DeployedComponents) > 0 {
  318. return nil
  319. }
  320. // Get the blueprint
  321. blueprint, err := s.blueprintStore.GetByID(deployment.BlueprintID)
  322. if err != nil {
  323. return fmt.Errorf("failed to retrieve blueprint: %w", err)
  324. }
  325. if blueprint == nil {
  326. return fmt.Errorf("blueprint with ID %s not found", deployment.BlueprintID)
  327. }
  328. // Use the blueprint config to set up deployed apps
  329. var blueprintConfig models.BlueprintConfig
  330. if err := json.Unmarshal([]byte(blueprint.ConfigJSON), &blueprintConfig); err != nil {
  331. return fmt.Errorf("failed to parse blueprint config: %w", err)
  332. }
  333. // Create deployed apps for each component in the blueprint
  334. for _, componentConfig := range blueprintConfig.Components {
  335. // Get the component
  336. component, err := s.componentStore.GetByID(componentConfig.ID)
  337. if err != nil {
  338. return fmt.Errorf("failed to retrieve component: %w", err)
  339. }
  340. if component == nil {
  341. return fmt.Errorf("component with ID %s not found", componentConfig.ID)
  342. }
  343. // Create a deployed app
  344. deployedComponent := models.DeployedComponent{
  345. ID: uuid.New().String(),
  346. DeploymentID: deployment.ID,
  347. ComponentID: component.ID,
  348. Status: string(models.PENDING_APP),
  349. Version: component.Version,
  350. URL: "", // Will be set during deployment
  351. PodCount: componentConfig.Autoscaling.MinReplicas,
  352. HealthStatus: string(models.HEALTHY),
  353. Resources: models.ResourceAllocation{
  354. CPU: componentConfig.Resources.CPU,
  355. Memory: componentConfig.Resources.Memory,
  356. Storage: componentConfig.Resources.Storage,
  357. },
  358. }
  359. // Add to deployment
  360. deployment.DeployedComponents = append(deployment.DeployedComponents, deployedComponent)
  361. }
  362. return nil
  363. }
  364. // processDeployment handles the actual deployment process
  365. func (s *DeploymentService) processDeployment(ctx context.Context, deployment *models.Deployment) {
  366. // Update status to deploying
  367. _ = s.UpdateDeploymentStatus(deployment.ID, string(models.DEPLOYING))
  368. s.logger.WithField("deployment_id", deployment.ID).Info("Starting deployment process")
  369. // Check if we have a valid instance
  370. if deployment.InstanceID == "" {
  371. s.logger.WithField("deployment_id", deployment.ID).Error("No instance ID provided for deployment")
  372. _ = s.UpdateDeploymentStatus(deployment.ID, string(models.FAILED_DEPLOYMENT))
  373. return
  374. }
  375. // Setup the deployed apps (database setup, migrations, etc.)
  376. if err := s.setupDeployedComponents(deployment); err != nil {
  377. s.logger.WithError(err).WithField("deployment_id", deployment.ID).Error("Failed to setup deployed apps")
  378. _ = s.UpdateDeploymentStatus(deployment.ID, string(models.FAILED_DEPLOYMENT))
  379. return
  380. }
  381. // Update the deployment with completed status
  382. deployment.Status = string(models.DEPLOYED)
  383. deployment.LastDeployedAt = time.Now()
  384. if err := s.store.Update(deployment); err != nil {
  385. s.logger.WithError(err).WithField("deployment_id", deployment.ID).Error("Failed to update deployment status")
  386. }
  387. s.logger.WithField("deployment_id", deployment.ID).Info("Deployment completed successfully")
  388. }
  389. // processDeploymentCleanup handles the cleanup process for deleted deployments
  390. func (s *DeploymentService) processDeploymentCleanup(deploymentID string) {
  391. // Get the deployment
  392. deployment, err := s.store.GetByID(deploymentID)
  393. if err != nil {
  394. s.logger.WithError(err).WithField("deployment_id", deploymentID).Error("Failed to get deployment for cleanup")
  395. return
  396. }
  397. if deployment == nil {
  398. s.logger.WithField("deployment_id", deploymentID).Error("Deployment not found for cleanup")
  399. return
  400. }
  401. s.logger.WithField("deployment_id", deploymentID).Info("Starting deployment cleanup process")
  402. ctx := context.Background()
  403. // Otherwise, deprovision the instance
  404. provider, ok := cloud.GetProvider(deployment.Provider)
  405. if !ok {
  406. s.logger.WithField("provider", deployment.Provider).Error("Provider not found for cleanup")
  407. } else {
  408. s.logger.WithField("instance_id", deployment.InstanceID).Info("Terminating instance")
  409. if err := provider.ResetInstance(ctx, deployment.InstanceID); err != nil {
  410. s.logger.WithError(err).WithField("instance_id", deployment.InstanceID).
  411. Error("Failed to terminate instance")
  412. }
  413. }
  414. // Delete the deployment from the database
  415. if err := s.store.Delete(deploymentID); err != nil {
  416. s.logger.WithError(err).WithField("deployment_id", deploymentID).
  417. Error("Failed to delete deployment record")
  418. }
  419. s.logger.WithField("deployment_id", deploymentID).Info("Deployment cleanup completed")
  420. }
  421. // deserializeConfigFields deserializes JSON config fields from strings
  422. func (s *DeploymentService) deserializeConfigFields(deployment *models.Deployment) error {
  423. // Deserialize logs config
  424. if deployment.LogsConfig != "" {
  425. var logsConfig models.LogConfiguration
  426. if err := json.Unmarshal([]byte(deployment.LogsConfig), &logsConfig); err != nil {
  427. return fmt.Errorf("failed to unmarshal logs config: %w", err)
  428. }
  429. // We could set this on the deployment if needed
  430. }
  431. // Deserialize metrics config
  432. if deployment.MetricsConfig != "" {
  433. var metricsConfig models.MetricsConfiguration
  434. if err := json.Unmarshal([]byte(deployment.MetricsConfig), &metricsConfig); err != nil {
  435. return fmt.Errorf("failed to unmarshal metrics config: %w", err)
  436. }
  437. // We could set this on the deployment if needed
  438. }
  439. // Deserialize alerts config
  440. if deployment.AlertsConfig != "" {
  441. var alertsConfig []models.AlertConfiguration
  442. if err := json.Unmarshal([]byte(deployment.AlertsConfig), &alertsConfig); err != nil {
  443. return fmt.Errorf("failed to unmarshal alerts config: %w", err)
  444. }
  445. // We could set this on the deployment if needed
  446. }
  447. return nil
  448. }