diff --git a/server/.env.example b/server/.env.example index fd3f849..9dbf569 100644 --- a/server/.env.example +++ b/server/.env.example @@ -67,6 +67,7 @@ DELIVERY_MYSQL_INSPECT_TIMEOUT_SECONDS=90 DELIVERY_ROLLBACK_TEMPLATE_ID=0 DELIVERY_SERVICE_TOKEN= CLOUDDM_REGISTER_URL= +CLOUDDM_DELETE_URL= CLOUDDM_API_TOKEN= JWT_SECRET=change-this-secret diff --git a/server/internal/config/config.go b/server/internal/config/config.go index 0028ff8..63b3f60 100644 --- a/server/internal/config/config.go +++ b/server/internal/config/config.go @@ -62,6 +62,7 @@ type Config struct { CloudDMAdminUsername string CloudDMAdminPassword string CloudDMRegisterURL string + CloudDMDeleteURL string CloudDMAPIToken string AWXBaseURL string AWXToken string @@ -147,6 +148,7 @@ func Load() Config { CloudDMAdminUsername: env("CLOUDDM_ADMIN_USERNAME", ""), CloudDMAdminPassword: env("CLOUDDM_ADMIN_PASSWORD", ""), CloudDMRegisterURL: trimURL(env("CLOUDDM_REGISTER_URL", "")), + CloudDMDeleteURL: trimURL(env("CLOUDDM_DELETE_URL", "")), CloudDMAPIToken: env("CLOUDDM_API_TOKEN", ""), AWXBaseURL: trimURL(env("AWX_BASE_URL", "")), AWXToken: env("AWX_TOKEN", ""), diff --git a/server/internal/service/delivery.go b/server/internal/service/delivery.go index b93e206..79d1f5d 100644 --- a/server/internal/service/delivery.go +++ b/server/internal/service/delivery.go @@ -1215,6 +1215,25 @@ func (s *DeliveryService) applyMySQLInspectResults(ctx context.Context, results Updates(map[string]any{"status": "released", "released_at": now, "updated_at": now}).Error; err != nil { return err } + + dataSourceID, deleteStatus := cloudDMDeleteInfo(meta) + if dataSourceID > 0 && deleteStatus != "deleted" { + if err := s.deleteCloudDMDataSource(ctx, current); err != nil { + meta = updateCloudDMDeleteMetadata(meta, "failed", err.Error(), "") + if updateErr := s.db.WithContext(ctx).Model(&model.DeploymentResult{}). + Where("id = ?", result.ID). + Update("metadata", string(mustJSON(meta))).Error; updateErr != nil { + return updateErr + } + return err + } + meta = updateCloudDMDeleteMetadata(meta, "deleted", "", now.Format(time.RFC3339)) + if err := s.db.WithContext(ctx).Model(&model.DeploymentResult{}). + Where("id = ?", result.ID). + Update("metadata", string(mustJSON(meta))).Error; err != nil { + return err + } + } } } return nil @@ -2384,6 +2403,12 @@ type cloudDMRegisterRequest struct { DataSource cloudDMDataSource `json:"dataSource"` } +type cloudDMDeleteRequest struct { + SourceSystem string `json:"sourceSystem"` + ResourceType string `json:"resourceType"` + ExternalResourceID string `json:"externalResourceId"` +} + type cloudDMDataSource struct { InstanceName string `json:"instanceName"` InstanceDesc string `json:"instanceDesc"` @@ -2463,6 +2488,36 @@ func cloudDMMetadata(existing map[string]any, instance model.DeploymentResult, d return meta } +func buildCloudDMDeleteRequest(instance model.DeploymentResult) cloudDMDeleteRequest { + return cloudDMDeleteRequest{ + SourceSystem: "xinfra", + ResourceType: "MYSQL_INSTANCE", + ExternalResourceID: cloudDMExternalResourceID(instance.ID), + } +} + +func cloudDMDeleteInfo(meta map[string]any) (uint64, string) { + clouddm, ok := meta["clouddm"].(map[string]any) + if !ok { + return 0, "" + } + dataSourceID, _ := cloudDMDataSourceIDFromValue(clouddm["data_source_id"]) + deleteStatus := strings.ToLower(strings.TrimSpace(stringValue(clouddm["delete_status"]))) + return dataSourceID, deleteStatus +} + +func updateCloudDMDeleteMetadata(meta map[string]any, deleteStatus, deleteError, deletedAt string) map[string]any { + clouddm, ok := meta["clouddm"].(map[string]any) + if !ok { + clouddm = map[string]any{} + } + clouddm["delete_status"] = deleteStatus + clouddm["delete_error"] = deleteError + clouddm["deleted_at"] = deletedAt + meta["clouddm"] = clouddm + return meta +} + func buildCloudDMRegisterRequest(instance model.DeploymentResult, payload deliveryPayload, password string) cloudDMRegisterRequest { description := strings.TrimSpace(payload.InstanceDesc) if description == "" { @@ -2498,6 +2553,37 @@ func buildCloudDMRegisterRequest(instance model.DeploymentResult, payload delive } } +func (s *DeliveryService) deleteCloudDMDataSource(ctx context.Context, instance model.DeploymentResult) error { + if strings.TrimSpace(s.cfg.CloudDMDeleteURL) == "" { + return fmt.Errorf("CloudDM deletion requires CLOUDDM_DELETE_URL to be configured") + } + if strings.TrimSpace(s.cfg.CloudDMAPIToken) == "" { + return fmt.Errorf("CloudDM deletion requires CLOUDDM_API_TOKEN to be configured") + } + body := buildCloudDMDeleteRequest(instance) + raw, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("encode CloudDM deletion request: %w", err) + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.cfg.CloudDMDeleteURL, bytes.NewReader(raw)) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(s.cfg.CloudDMAPIToken)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Request-ID", randomUUID()) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("CloudDM deletion returned %s: %s", resp.Status, strings.TrimSpace(string(respBody))) + } + return nil +} + func (s *DeliveryService) RegisterCloudDM(ctx context.Context, taskID string) error { if strings.TrimSpace(s.cfg.CloudDMRegisterURL) == "" { return nil