Skip to main content

pipedash_core/application/services/
provider_service.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use pipedash_plugin_api::{
5    Plugin as PluginTrait,
6    PluginRegistry,
7};
8use tokio::sync::{
9    Mutex,
10    RwLock,
11};
12
13use crate::domain::{
14    DomainError,
15    DomainResult,
16    FetchStatus,
17    Provider,
18    ProviderConfig,
19    ProviderSummary,
20};
21use crate::event::{
22    CoreEvent,
23    EventBus,
24};
25use crate::infrastructure::config::token_ref::TokenReference;
26use crate::infrastructure::database::Repository;
27use crate::infrastructure::providers::PluginAdapter;
28use crate::plugins;
29
30pub struct ProviderService {
31    repository: Arc<Repository>,
32    http_client_manager: Arc<crate::infrastructure::HttpClientManager>,
33    providers: Arc<RwLock<HashMap<i64, Arc<dyn Provider>>>>,
34    plugin_registry: Arc<PluginRegistry>,
35    parameter_fetches: Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>,
36    event_bus: Arc<dyn EventBus>,
37}
38
39impl ProviderService {
40    pub fn new(
41        repository: Arc<Repository>,
42        http_client_manager: Arc<crate::infrastructure::HttpClientManager>,
43        event_bus: Arc<dyn EventBus>,
44    ) -> Self {
45        let plugin_registry = plugins::create_plugin_registry();
46
47        Self {
48            repository,
49            http_client_manager,
50            providers: Arc::new(RwLock::new(HashMap::new())),
51            plugin_registry: Arc::new(plugin_registry),
52            parameter_fetches: Arc::new(Mutex::new(HashMap::new())),
53            event_bus,
54        }
55    }
56
57    pub fn repository(&self) -> &Arc<Repository> {
58        &self.repository
59    }
60
61    pub fn list_available_plugins(&self) -> Vec<pipedash_plugin_api::PluginMetadata> {
62        let mut metadata_list = Vec::new();
63        for provider_type in self.plugin_registry.provider_types() {
64            if let Some(plugin) = self.plugin_registry.get(&provider_type) {
65                metadata_list.push(plugin.metadata().clone());
66            }
67        }
68        metadata_list
69    }
70
71    pub fn create_uninitialized_plugin(
72        &self, provider_type: &str,
73    ) -> DomainResult<Box<dyn PluginTrait>> {
74        plugins::create_plugin(provider_type).ok_or_else(|| {
75            DomainError::InvalidProviderType(format!("Unknown provider type: {}", provider_type))
76        })
77    }
78
79    pub async fn add_provider(&self, config: ProviderConfig) -> DomainResult<i64> {
80        let mut plugin = self.create_uninitialized_plugin(&config.provider_type)?;
81
82        let mut plugin_config = config.config.clone();
83        plugin_config.insert("token".to_string(), config.token.clone());
84
85        let http_client = if let Some(base_url) = plugin_config.get("base_url") {
86            self.http_client_manager.client_for_url(base_url)?
87        } else {
88            self.http_client_manager.default_client()
89        };
90
91        plugin
92            .initialize(0, plugin_config.clone(), Some(http_client))
93            .map_err(|e| DomainError::InvalidConfig(format!("Failed to initialize plugin: {e}")))?;
94
95        let (validation_result, permission_status) =
96            tokio::join!(plugin.validate_credentials(), plugin.check_permissions());
97
98        validation_result.map_err(|e| {
99            DomainError::InvalidConfig(format!("Credential validation failed: {e}"))
100        })?;
101
102        let provider_type = plugin.provider_type();
103        if provider_type != config.provider_type {
104            return Err(DomainError::InvalidConfig(format!(
105                "Provider type mismatch: expected '{}', got '{}'",
106                config.provider_type, provider_type
107            )));
108        }
109
110        let permission_status = permission_status.ok();
111
112        let id = self.repository.add_provider(&config).await?;
113
114        if let Some(ref status) = permission_status {
115            if let Err(e) = self.repository.store_provider_permissions(id, status).await {
116                tracing::warn!(provider_id = id, error = %e, "Failed to store permissions");
117            }
118        }
119
120        let mut config_with_id = config.clone();
121        config_with_id.id = Some(id);
122        let provider = self.create_provider(&config_with_id)?;
123
124        let mut providers = self.providers.write().await;
125        providers.insert(id, provider);
126        drop(providers);
127
128        let fresh_config = self.repository.get_provider(id).await?;
129        let cached_pipelines = self.repository.get_cached_pipelines(Some(id)).await?;
130        let pipeline_count = cached_pipelines.len();
131        let last_updated = cached_pipelines.iter().map(|p| p.last_updated).max();
132
133        let icon = self
134            .plugin_registry
135            .get(&fresh_config.provider_type)
136            .and_then(|plugin| plugin.metadata().icon.clone());
137
138        let configured_repositories = fresh_config
139            .config
140            .get("selected_items")
141            .map(|items| {
142                if fresh_config.provider_type == "argocd" {
143                    return Vec::new();
144                }
145
146                items
147                    .split(',')
148                    .map(|s| s.trim().to_string())
149                    .filter(|s| !s.is_empty())
150                    .map(|item| {
151                        if fresh_config.provider_type == "tekton" && item.contains("__") {
152                            item.replace("__", "/")
153                        } else if fresh_config.provider_type == "jenkins" && !item.contains('/') {
154                            format!("(root)/{}", item)
155                        } else {
156                            item
157                        }
158                    })
159                    .collect()
160            })
161            .unwrap_or_default();
162
163        const FETCH_STATUS_SUCCESS: &str = "success";
164        const FETCH_STATUS_ERROR: &str = "error";
165        const FETCH_STATUS_NEVER: &str = "never";
166
167        let (fetch_status_str, last_fetch_error, last_fetch_at) = self
168            .repository
169            .get_provider_fetch_status(id)
170            .await
171            .unwrap_or_else(|_| (FETCH_STATUS_NEVER.to_string(), None, None));
172
173        let fetch_status_enum = match fetch_status_str.as_str() {
174            FETCH_STATUS_SUCCESS => FetchStatus::Success,
175            FETCH_STATUS_ERROR => FetchStatus::Error,
176            _ => FetchStatus::Never,
177        };
178
179        let last_fetch_at_parsed = last_fetch_at.and_then(|s| {
180            chrono::DateTime::parse_from_rfc3339(&s)
181                .ok()
182                .map(|dt| dt.with_timezone(&chrono::Utc))
183        });
184
185        let provider_summary = ProviderSummary {
186            id,
187            name: fresh_config.display_name().to_string(),
188            provider_type: fresh_config.provider_type,
189            icon,
190            pipeline_count,
191            last_updated,
192            refresh_interval: fresh_config.refresh_interval,
193            configured_repositories,
194            last_fetch_status: fetch_status_enum,
195            last_fetch_error,
196            last_fetch_at: last_fetch_at_parsed,
197            version: fresh_config.version.unwrap_or(1),
198        };
199
200        let timestamp = std::time::SystemTime::now()
201            .duration_since(std::time::UNIX_EPOCH)
202            .unwrap()
203            .as_millis() as i64;
204
205        self.event_bus
206            .emit(CoreEvent::ProviderAdded {
207                provider: provider_summary,
208                timestamp,
209            })
210            .await;
211
212        Ok(id)
213    }
214
215    pub async fn get_provider_config(&self, id: i64) -> DomainResult<ProviderConfig> {
216        self.repository.get_provider(id).await
217    }
218
219    pub async fn list_providers(&self) -> DomainResult<Vec<ProviderSummary>> {
220        let configs = self.repository.list_providers().await?;
221        let mut summaries = Vec::new();
222
223        for config in configs {
224            let cached_pipelines = self.repository.get_cached_pipelines(config.id).await?;
225            let pipeline_count = cached_pipelines.len();
226            let last_updated = cached_pipelines.iter().map(|p| p.last_updated).max();
227
228            let icon = self
229                .plugin_registry
230                .get(&config.provider_type)
231                .and_then(|plugin| plugin.metadata().icon.clone());
232
233            let configured_repositories = config
234                .config
235                .get("selected_items")
236                .map(|items| {
237                    if config.provider_type == "argocd" {
238                        return Vec::new();
239                    }
240
241                    items
242                        .split(',')
243                        .map(|s| s.trim().to_string())
244                        .filter(|s| !s.is_empty())
245                        .map(|item| {
246                            if config.provider_type == "tekton" && item.contains("__") {
247                                item.replace("__", "/")
248                            } else if config.provider_type == "jenkins" && !item.contains('/') {
249                                format!("(root)/{}", item)
250                            } else {
251                                item
252                            }
253                        })
254                        .collect()
255                })
256                .unwrap_or_default();
257
258            let provider_id = config.id.unwrap();
259            const FETCH_STATUS_SUCCESS: &str = "success";
260            const FETCH_STATUS_ERROR: &str = "error";
261            const FETCH_STATUS_NEVER: &str = "never";
262
263            let (fetch_status, last_fetch_error, last_fetch_at) = self
264                .repository
265                .get_provider_fetch_status(provider_id)
266                .await
267                .unwrap_or_else(|_| (FETCH_STATUS_NEVER.to_string(), None, None));
268
269            let fetch_status_enum = match fetch_status.as_str() {
270                FETCH_STATUS_SUCCESS => FetchStatus::Success,
271                FETCH_STATUS_ERROR => FetchStatus::Error,
272                _ => FetchStatus::Never,
273            };
274
275            let last_fetch_at_parsed = last_fetch_at.and_then(|s| {
276                chrono::DateTime::parse_from_rfc3339(&s)
277                    .ok()
278                    .map(|dt| dt.with_timezone(&chrono::Utc))
279            });
280
281            summaries.push(ProviderSummary {
282                id: provider_id,
283                name: config.display_name().to_string(),
284                provider_type: config.provider_type,
285                icon,
286                pipeline_count,
287                last_updated,
288                refresh_interval: config.refresh_interval,
289                configured_repositories,
290                last_fetch_status: fetch_status_enum,
291                last_fetch_error,
292                last_fetch_at: last_fetch_at_parsed,
293                version: config.version.unwrap_or(1),
294            });
295        }
296
297        Ok(summaries)
298    }
299
300    pub async fn update_provider(&self, id: i64, config: ProviderConfig) -> DomainResult<()> {
301        let mut plugin = self.create_uninitialized_plugin(&config.provider_type)?;
302
303        let mut plugin_config = config.config.clone();
304        plugin_config.insert("token".to_string(), config.token.clone());
305
306        let http_client = if let Some(base_url) = plugin_config.get("base_url") {
307            self.http_client_manager.client_for_url(base_url)?
308        } else {
309            self.http_client_manager.default_client()
310        };
311
312        plugin
313            .initialize(id, plugin_config.clone(), Some(http_client))
314            .map_err(|e| DomainError::InvalidConfig(format!("Failed to initialize plugin: {e}")))?;
315
316        plugin.validate_credentials().await.map_err(|e| {
317            DomainError::InvalidConfig(format!("Credential validation failed: {e}"))
318        })?;
319
320        let permission_status = plugin.check_permissions().await.ok();
321
322        let current = self.repository.get_provider(id).await?;
323        let current_version = current.version.unwrap_or(1);
324
325        let success = self
326            .repository
327            .update_provider_with_version(id, &config, current_version)
328            .await?;
329
330        if !success {
331            return Err(DomainError::ConcurrentModification(format!(
332                "Provider {} was modified by another client",
333                id
334            )));
335        }
336
337        if let Some(ref status) = permission_status {
338            if let Err(e) = self.repository.store_provider_permissions(id, status).await {
339                tracing::warn!(provider_id = id, error = %e, "Failed to store permissions");
340            }
341        }
342
343        let mut config_with_id = config.clone();
344        config_with_id.id = Some(id);
345        let new_provider = self.create_provider(&config_with_id)?;
346
347        let providers = Arc::clone(&self.providers);
348        let old_provider = {
349            let mut map = providers.write().await;
350            map.insert(id, new_provider)
351        };
352
353        if old_provider.is_some() {
354            tokio::spawn(async move {
355                drop(old_provider);
356                tracing::debug!(
357                    provider_id = id,
358                    "Old provider cleanup completed after update"
359                );
360            });
361        }
362
363        let fresh_config = self.repository.get_provider(id).await?;
364        let cached_pipelines = self.repository.get_cached_pipelines(Some(id)).await?;
365        let pipeline_count = cached_pipelines.len();
366        let last_updated = cached_pipelines.iter().map(|p| p.last_updated).max();
367
368        let icon = self
369            .plugin_registry
370            .get(&fresh_config.provider_type)
371            .and_then(|plugin| plugin.metadata().icon.clone());
372
373        let configured_repositories = fresh_config
374            .config
375            .get("selected_items")
376            .map(|items| {
377                if fresh_config.provider_type == "argocd" {
378                    return Vec::new();
379                }
380
381                items
382                    .split(',')
383                    .map(|s| s.trim().to_string())
384                    .filter(|s| !s.is_empty())
385                    .map(|item| {
386                        if fresh_config.provider_type == "tekton" && item.contains("__") {
387                            item.replace("__", "/")
388                        } else if fresh_config.provider_type == "jenkins" && !item.contains('/') {
389                            format!("(root)/{}", item)
390                        } else {
391                            item
392                        }
393                    })
394                    .collect()
395            })
396            .unwrap_or_default();
397
398        const FETCH_STATUS_SUCCESS: &str = "success";
399        const FETCH_STATUS_ERROR: &str = "error";
400        const FETCH_STATUS_NEVER: &str = "never";
401
402        let (fetch_status_str, last_fetch_error, last_fetch_at) = self
403            .repository
404            .get_provider_fetch_status(id)
405            .await
406            .unwrap_or_else(|_| (FETCH_STATUS_NEVER.to_string(), None, None));
407
408        let fetch_status_enum = match fetch_status_str.as_str() {
409            FETCH_STATUS_SUCCESS => FetchStatus::Success,
410            FETCH_STATUS_ERROR => FetchStatus::Error,
411            _ => FetchStatus::Never,
412        };
413
414        let last_fetch_at_parsed = last_fetch_at.and_then(|s| {
415            chrono::DateTime::parse_from_rfc3339(&s)
416                .ok()
417                .map(|dt| dt.with_timezone(&chrono::Utc))
418        });
419
420        let provider_summary = ProviderSummary {
421            id,
422            name: fresh_config.display_name().to_string(),
423            provider_type: fresh_config.provider_type,
424            icon,
425            pipeline_count,
426            last_updated,
427            refresh_interval: fresh_config.refresh_interval,
428            configured_repositories,
429            last_fetch_status: fetch_status_enum,
430            last_fetch_error,
431            last_fetch_at: last_fetch_at_parsed,
432            version: fresh_config.version.unwrap_or(1),
433        };
434
435        let timestamp = std::time::SystemTime::now()
436            .duration_since(std::time::UNIX_EPOCH)
437            .unwrap()
438            .as_millis() as i64;
439
440        self.event_bus
441            .emit(CoreEvent::ProviderUpdated {
442                provider: provider_summary,
443                timestamp,
444            })
445            .await;
446
447        Ok(())
448    }
449
450    pub async fn update_provider_refresh_interval(
451        &self, id: i64, refresh_interval: i64,
452    ) -> DomainResult<()> {
453        let mut config = self.repository.get_provider(id).await?;
454        config.refresh_interval = refresh_interval;
455        self.repository.update_provider(id, &config).await?;
456        Ok(())
457    }
458
459    pub async fn remove_provider(&self, id: i64) -> DomainResult<()> {
460        let fresh_config = self.repository.get_provider(id).await?;
461        let cached_pipelines = self.repository.get_cached_pipelines(Some(id)).await?;
462        let pipeline_count = cached_pipelines.len();
463        let last_updated = cached_pipelines.iter().map(|p| p.last_updated).max();
464
465        let icon = self
466            .plugin_registry
467            .get(&fresh_config.provider_type)
468            .and_then(|plugin| plugin.metadata().icon.clone());
469
470        let configured_repositories = fresh_config
471            .config
472            .get("selected_items")
473            .map(|items| {
474                if fresh_config.provider_type == "argocd" {
475                    return Vec::new();
476                }
477
478                items
479                    .split(',')
480                    .map(|s| s.trim().to_string())
481                    .filter(|s| !s.is_empty())
482                    .map(|item| {
483                        if fresh_config.provider_type == "tekton" && item.contains("__") {
484                            item.replace("__", "/")
485                        } else if fresh_config.provider_type == "jenkins" && !item.contains('/') {
486                            format!("(root)/{}", item)
487                        } else {
488                            item
489                        }
490                    })
491                    .collect()
492            })
493            .unwrap_or_default();
494
495        const FETCH_STATUS_SUCCESS: &str = "success";
496        const FETCH_STATUS_ERROR: &str = "error";
497        const FETCH_STATUS_NEVER: &str = "never";
498
499        let (fetch_status_str, last_fetch_error, last_fetch_at) = self
500            .repository
501            .get_provider_fetch_status(id)
502            .await
503            .unwrap_or_else(|_| (FETCH_STATUS_NEVER.to_string(), None, None));
504
505        let fetch_status_enum = match fetch_status_str.as_str() {
506            FETCH_STATUS_SUCCESS => FetchStatus::Success,
507            FETCH_STATUS_ERROR => FetchStatus::Error,
508            _ => FetchStatus::Never,
509        };
510
511        let last_fetch_at_parsed = last_fetch_at.and_then(|s| {
512            chrono::DateTime::parse_from_rfc3339(&s)
513                .ok()
514                .map(|dt| dt.with_timezone(&chrono::Utc))
515        });
516
517        let provider_summary = ProviderSummary {
518            id,
519            name: fresh_config.display_name().to_string(),
520            provider_type: fresh_config.provider_type,
521            icon,
522            pipeline_count,
523            last_updated,
524            refresh_interval: fresh_config.refresh_interval,
525            configured_repositories,
526            last_fetch_status: fetch_status_enum,
527            last_fetch_error,
528            last_fetch_at: last_fetch_at_parsed,
529            version: fresh_config.version.unwrap_or(1),
530        };
531
532        let pipelines = cached_pipelines;
533
534        self.repository.remove_provider(id).await?;
535
536        let providers = Arc::clone(&self.providers);
537        tokio::spawn(async move {
538            let mut map = providers.write().await;
539            map.remove(&id);
540            tracing::debug!(provider_id = id, "Provider cleanup completed");
541        });
542
543        let mut fetches = self.parameter_fetches.lock().await;
544        for pipeline in pipelines {
545            fetches.remove(&pipeline.id);
546        }
547
548        let timestamp = std::time::SystemTime::now()
549            .duration_since(std::time::UNIX_EPOCH)
550            .unwrap()
551            .as_millis() as i64;
552
553        self.event_bus
554            .emit(CoreEvent::ProviderRemoved {
555                provider: provider_summary,
556                timestamp,
557            })
558            .await;
559
560        Ok(())
561    }
562
563    pub async fn get_provider(&self, id: i64) -> DomainResult<Arc<dyn Provider>> {
564        let providers = self.providers.read().await;
565
566        if let Some(provider) = providers.get(&id) {
567            return Ok(Arc::clone(provider));
568        }
569
570        drop(providers);
571
572        let config = self.repository.get_provider(id).await?;
573        let provider = self.create_provider(&config)?;
574
575        let mut providers = self.providers.write().await;
576        providers.insert(id, Arc::clone(&provider));
577
578        Ok(provider)
579    }
580
581    pub async fn get_provider_instance(&self, id: i64) -> DomainResult<Arc<dyn Provider>> {
582        self.get_provider(id).await
583    }
584
585    pub async fn load_all_providers(&self) -> DomainResult<()> {
586        let configs = self.repository.list_providers().await?;
587        let mut old_providers_to_cleanup = Vec::new();
588
589        for config in configs {
590            if let Some(id) = config.id {
591                if config.token.is_empty() {
592                    tracing::debug!(
593                        provider_id = id,
594                        provider_type = %config.provider_type,
595                        "Skipping provider load - token not available yet (vault initializing in background)"
596                    );
597                    continue;
598                }
599
600                match self.create_provider(&config) {
601                    Ok(provider) => {
602                        let mut providers = self.providers.write().await;
603                        if let Some(old) = providers.insert(id, provider) {
604                            old_providers_to_cleanup.push((id, old));
605                        }
606                    }
607                    Err(e) => {
608                        tracing::warn!(provider_id = id, error = %e, "Failed to load provider");
609                    }
610                }
611            }
612        }
613
614        if !old_providers_to_cleanup.is_empty() {
615            tokio::spawn(async move {
616                for (id, provider) in old_providers_to_cleanup {
617                    drop(provider);
618                    tracing::debug!(
619                        provider_id = id,
620                        "Old provider cleanup completed after reload"
621                    );
622                }
623            });
624        }
625
626        Ok(())
627    }
628
629    fn create_provider(&self, config: &ProviderConfig) -> DomainResult<Arc<dyn Provider>> {
630        if !self.plugin_registry.is_registered(&config.provider_type) {
631            return Err(DomainError::InvalidProviderType(format!(
632                "Plugin not found for provider type: {}",
633                config.provider_type
634            )));
635        }
636
637        let mut plugin = self.create_uninitialized_plugin(&config.provider_type)?;
638
639        let provider_id = config.id.unwrap_or(0);
640        let mut plugin_config = config.config.clone();
641
642        let resolved_token = if config.token.is_empty() {
643            String::new()
644        } else {
645            match TokenReference::parse(&config.token) {
646                Ok(TokenReference::EnvVar(var_name)) => {
647                    std::env::var(&var_name).unwrap_or_else(|_| {
648                        tracing::warn!(
649                            provider_id = provider_id,
650                            var_name = %var_name,
651                            "Environment variable not found, using empty token"
652                        );
653                        String::new()
654                    })
655                }
656                Ok(TokenReference::None) => String::new(),
657                Ok(_) => config.token.clone(),
658                Err(_) => config.token.clone(),
659            }
660        };
661        plugin_config.insert("token".to_string(), resolved_token);
662
663        let http_client = if let Some(base_url) = plugin_config.get("base_url") {
664            self.http_client_manager.client_for_url(base_url)?
665        } else {
666            self.http_client_manager.default_client()
667        };
668
669        plugin
670            .initialize(provider_id, plugin_config, Some(http_client))
671            .map_err(|e| DomainError::InvalidConfig(format!("Failed to initialize plugin: {e}")))?;
672
673        let adapter = PluginAdapter::new(plugin, config.provider_type.clone(), provider_id);
674        Ok(Arc::new(adapter))
675    }
676
677    pub async fn get_workflow_parameters(
678        &self, provider_id: i64, workflow_id: &str,
679    ) -> DomainResult<Vec<pipedash_plugin_api::WorkflowParameter>> {
680        if let Ok(Some(cached)) = self
681            .repository
682            .get_cached_workflow_parameters(workflow_id)
683            .await
684        {
685            return Ok(cached);
686        }
687
688        let fetch_lock = {
689            let mut fetches = self.parameter_fetches.lock().await;
690            fetches
691                .entry(workflow_id.to_string())
692                .or_insert_with(|| Arc::new(Mutex::new(())))
693                .clone()
694        };
695
696        let _guard = fetch_lock.lock().await;
697
698        if let Ok(Some(cached)) = self
699            .repository
700            .get_cached_workflow_parameters(workflow_id)
701            .await
702        {
703            return Ok(cached);
704        }
705
706        let providers = self.providers.read().await;
707        let provider = providers.get(&provider_id).ok_or_else(|| {
708            DomainError::ProviderNotFound(format!("Provider {provider_id} not found"))
709        })?;
710
711        let parameters = provider
712            .get_workflow_parameters(workflow_id)
713            .await
714            .map_err(|e| {
715                DomainError::InvalidConfig(format!("Failed to get workflow parameters: {e}"))
716            })?;
717
718        let _ = self
719            .repository
720            .cache_workflow_parameters(workflow_id, &parameters)
721            .await;
722
723        {
724            let mut fetches = self.parameter_fetches.lock().await;
725            fetches.remove(workflow_id);
726        }
727
728        Ok(parameters)
729    }
730
731    pub async fn get_provider_permissions(
732        &self, provider_id: i64,
733    ) -> DomainResult<Option<pipedash_plugin_api::PermissionStatus>> {
734        self.repository.get_provider_permissions(provider_id).await
735    }
736
737    pub async fn recheck_provider_permissions(
738        &self, provider_id: i64,
739    ) -> DomainResult<pipedash_plugin_api::PermissionStatus> {
740        let config = self.repository.get_provider(provider_id).await?;
741        let mut plugin = self.create_uninitialized_plugin(&config.provider_type)?;
742
743        let mut plugin_config = config.config.clone();
744        plugin_config.insert("token".to_string(), config.token.clone());
745
746        let http_client = if let Some(base_url) = plugin_config.get("base_url") {
747            self.http_client_manager.client_for_url(base_url)?
748        } else {
749            self.http_client_manager.default_client()
750        };
751
752        plugin
753            .initialize(provider_id, plugin_config, Some(http_client))
754            .map_err(|e| DomainError::InvalidConfig(format!("Failed to initialize plugin: {e}")))?;
755
756        let status = plugin
757            .check_permissions()
758            .await
759            .map_err(|e| DomainError::InvalidConfig(format!("Failed to check permissions: {e}")))?;
760
761        self.repository
762            .store_provider_permissions(provider_id, &status)
763            .await?;
764
765        Ok(status)
766    }
767}