deployments.go 16 KB

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