@@ -8,7 +8,7 @@ defmodule SentinelCp.Services do
88
99 import Ecto.Query , warn: false
1010 alias SentinelCp.Repo
11- alias SentinelCp.Services . { Service , ServiceTemplate , ProjectConfig , UpstreamGroup , UpstreamTarget , Certificate , AuthPolicy , OpenApiSpec }
11+ alias SentinelCp.Services . { Service , ServiceTemplate , ProjectConfig , UpstreamGroup , UpstreamTarget , Certificate , AuthPolicy , OpenApiSpec , DiscoverySource , DiscoverySync }
1212
1313 ## Services
1414
@@ -483,6 +483,140 @@ defmodule SentinelCp.Services do
483483 end )
484484 end
485485
486+ ## Discovery Sources
487+
488+ @ doc """
489+ Lists discovery sources for a project, preloading upstream_group.
490+ """
491+ def list_discovery_sources ( project_id ) do
492+ from ( d in DiscoverySource ,
493+ where: d . project_id == ^ project_id ,
494+ preload: [ :upstream_group ]
495+ )
496+ |> Repo . all ( )
497+ end
498+
499+ @ doc """
500+ Gets a single discovery source by ID.
501+ """
502+ def get_discovery_source ( id ) , do: Repo . get ( DiscoverySource , id )
503+
504+ @ doc """
505+ Gets a single discovery source by ID, raises if not found.
506+ """
507+ def get_discovery_source! ( id ) , do: Repo . get! ( DiscoverySource , id )
508+
509+ @ doc """
510+ Gets the discovery source for an upstream group, if any.
511+ """
512+ def get_discovery_source_for_group ( upstream_group_id ) do
513+ from ( d in DiscoverySource , where: d . upstream_group_id == ^ upstream_group_id )
514+ |> Repo . one ( )
515+ end
516+
517+ @ doc """
518+ Creates a discovery source.
519+ """
520+ def create_discovery_source ( attrs ) do
521+ % DiscoverySource { }
522+ |> DiscoverySource . changeset ( attrs )
523+ |> Repo . insert ( )
524+ end
525+
526+ @ doc """
527+ Updates a discovery source (hostname, interval, auto_sync).
528+ """
529+ def update_discovery_source ( % DiscoverySource { } = source , attrs ) do
530+ source
531+ |> DiscoverySource . update_changeset ( attrs )
532+ |> Repo . update ( )
533+ end
534+
535+ @ doc """
536+ Deletes a discovery source.
537+ """
538+ def delete_discovery_source ( % DiscoverySource { } = source ) do
539+ Repo . delete ( source )
540+ end
541+
542+ @ doc """
543+ Lists all discovery sources with auto_sync enabled (across all projects).
544+ """
545+ def list_auto_sync_sources do
546+ from ( d in DiscoverySource , where: d . auto_sync == true )
547+ |> Repo . all ( )
548+ end
549+
550+ @ doc """
551+ Synchronizes a discovery source by resolving DNS SRV records and reconciling targets.
552+
553+ Returns `{:ok, %{added: N, removed: N, kept: N}}` on success or `{:error, reason}` on failure.
554+ """
555+ def sync_discovery_source ( % DiscoverySource { } = source ) do
556+ # Mark as syncing
557+ { :ok , source } =
558+ source
559+ |> DiscoverySource . sync_changeset ( % { last_sync_status: "syncing" } )
560+ |> Repo . update ( )
561+
562+ case dns_resolver ( ) . resolve_srv ( source . hostname ) do
563+ { :ok , records } ->
564+ group = get_upstream_group! ( source . upstream_group_id )
565+ current_targets = group . targets || [ ]
566+
567+ result = DiscoverySync . reconcile ( current_targets , records )
568+
569+ # Apply additions
570+ for target_attrs <- result . add do
571+ add_upstream_target (
572+ Map . merge ( target_attrs , % { upstream_group_id: source . upstream_group_id } )
573+ )
574+ end
575+
576+ # Apply removals
577+ for target <- result . remove do
578+ remove_upstream_target ( target )
579+ end
580+
581+ total_count = length ( result . add ) + length ( result . keep )
582+
583+ { :ok , source } =
584+ source
585+ |> DiscoverySource . sync_changeset ( % {
586+ last_synced_at: DateTime . utc_now ( ) |> DateTime . truncate ( :second ) ,
587+ last_sync_status: "synced" ,
588+ last_sync_error: nil ,
589+ last_sync_targets_count: total_count
590+ } )
591+ |> Repo . update ( )
592+
593+ Phoenix.PubSub . broadcast (
594+ SentinelCp.PubSub ,
595+ "discovery:#{ source . id } " ,
596+ { :discovery_synced , source . id }
597+ )
598+
599+ { :ok , % { added: length ( result . add ) , removed: length ( result . remove ) , kept: length ( result . keep ) } }
600+
601+ { :error , reason } ->
602+ error_msg = if is_binary ( reason ) , do: reason , else: inspect ( reason )
603+
604+ source
605+ |> DiscoverySource . sync_changeset ( % {
606+ last_synced_at: DateTime . utc_now ( ) |> DateTime . truncate ( :second ) ,
607+ last_sync_status: "error" ,
608+ last_sync_error: error_msg
609+ } )
610+ |> Repo . update ( )
611+
612+ { :error , error_msg }
613+ end
614+ end
615+
616+ defp dns_resolver do
617+ Application . get_env ( :sentinel_cp , :dns_resolver , SentinelCp.Services.DnsResolver.Inet )
618+ end
619+
486620 defp resolve_auth_policy_id ( % { security_refs: refs } , policy_map ) when is_list ( refs ) do
487621 Enum . find_value ( refs , fn ref -> Map . get ( policy_map , ref ) end )
488622 end
0 commit comments