diff --git a/server/internal/database/instance_resource.go b/server/internal/database/instance_resource.go index 1141a5b7..1d6fe631 100644 --- a/server/internal/database/instance_resource.go +++ b/server/internal/database/instance_resource.go @@ -71,6 +71,10 @@ func (r *InstanceResource) Dependencies() []resource.Identifier { return dependencies } +func (r *InstanceResource) TypeDependencies() []resource.Type { + return nil +} + func (r *InstanceResource) Refresh(ctx context.Context, rc *resource.Context) error { if err := r.updateConnectionInfo(ctx, rc); err != nil { return resource.ErrNotFound diff --git a/server/internal/database/lag_tracker_commit_ts_resource.go b/server/internal/database/lag_tracker_commit_ts_resource.go index 93f42897..e6d3fd91 100644 --- a/server/internal/database/lag_tracker_commit_ts_resource.go +++ b/server/internal/database/lag_tracker_commit_ts_resource.go @@ -62,6 +62,10 @@ func (r *LagTrackerCommitTimestampResource) Dependencies() []resource.Identifier return deps } +func (r *LagTrackerCommitTimestampResource) TypeDependencies() []resource.Type { + return nil +} + func (r *LagTrackerCommitTimestampResource) Refresh(ctx context.Context, rc *resource.Context) error { // Connect to receiver node instance, err := GetPrimaryInstance(ctx, rc, r.ReceiverNode) diff --git a/server/internal/database/node_resource.go b/server/internal/database/node_resource.go index 98b83b7a..b53206e8 100644 --- a/server/internal/database/node_resource.go +++ b/server/internal/database/node_resource.go @@ -51,6 +51,10 @@ func (n *NodeResource) Dependencies() []resource.Identifier { return dependencies } +func (n *NodeResource) TypeDependencies() []resource.Type { + return nil +} + func (n *NodeResource) Refresh(ctx context.Context, rc *resource.Context) error { if err := n.Create(ctx, rc); err != nil { return err diff --git a/server/internal/database/operations/helpers_test.go b/server/internal/database/operations/helpers_test.go index fdb465ce..8532e340 100644 --- a/server/internal/database/operations/helpers_test.go +++ b/server/internal/database/operations/helpers_test.go @@ -131,6 +131,10 @@ func (r *orchestratorResource) Dependencies() []resource.Identifier { return nil } +func (r *orchestratorResource) TypeDependencies() []resource.Type { + return nil +} + func (r *orchestratorResource) Refresh(ctx context.Context, rc *resource.Context) error { return nil } diff --git a/server/internal/database/replication_slot_advance_from_cts_resource.go b/server/internal/database/replication_slot_advance_from_cts_resource.go index 37227fe2..ad5e9148 100644 --- a/server/internal/database/replication_slot_advance_from_cts_resource.go +++ b/server/internal/database/replication_slot_advance_from_cts_resource.go @@ -51,6 +51,10 @@ func (r *ReplicationSlotAdvanceFromCTSResource) Dependencies() []resource.Identi } } +func (r *ReplicationSlotAdvanceFromCTSResource) TypeDependencies() []resource.Type { + return nil +} + func (r *ReplicationSlotAdvanceFromCTSResource) Refresh(ctx context.Context, rc *resource.Context) error { return nil } diff --git a/server/internal/database/replication_slot_create_resource.go b/server/internal/database/replication_slot_create_resource.go index 7988cbad..451af000 100644 --- a/server/internal/database/replication_slot_create_resource.go +++ b/server/internal/database/replication_slot_create_resource.go @@ -47,6 +47,10 @@ func (r *ReplicationSlotCreateResource) Dependencies() []resource.Identifier { } } +func (r *ReplicationSlotCreateResource) TypeDependencies() []resource.Type { + return nil +} + func (r *ReplicationSlotCreateResource) Refresh(ctx context.Context, rc *resource.Context) error { instance, err := GetPrimaryInstance(ctx, rc, r.ProviderNode) if err != nil { diff --git a/server/internal/database/replication_slot_resource.go b/server/internal/database/replication_slot_resource.go index 2d493d15..3be0b973 100644 --- a/server/internal/database/replication_slot_resource.go +++ b/server/internal/database/replication_slot_resource.go @@ -52,6 +52,10 @@ func (r *ReplicationSlotResource) Dependencies() []resource.Identifier { } } +func (r *ReplicationSlotResource) TypeDependencies() []resource.Type { + return nil +} + func (r *ReplicationSlotResource) Refresh(ctx context.Context, rc *resource.Context) error { return nil } diff --git a/server/internal/database/subscription_resource.go b/server/internal/database/subscription_resource.go index 267dd451..420ffc20 100644 --- a/server/internal/database/subscription_resource.go +++ b/server/internal/database/subscription_resource.go @@ -61,6 +61,10 @@ func (s *SubscriptionResource) Dependencies() []resource.Identifier { return deps } +func (s *SubscriptionResource) TypeDependencies() []resource.Type { + return nil +} + func (s *SubscriptionResource) Refresh(ctx context.Context, rc *resource.Context) error { subscriber, err := GetPrimaryInstance(ctx, rc, s.SubscriberNode) if err != nil { diff --git a/server/internal/database/switchover_resource.go b/server/internal/database/switchover_resource.go index 81cc0c4a..2d4f99fe 100644 --- a/server/internal/database/switchover_resource.go +++ b/server/internal/database/switchover_resource.go @@ -52,6 +52,10 @@ func (s *SwitchoverResource) Dependencies() []resource.Identifier { } } +func (s *SwitchoverResource) TypeDependencies() []resource.Type { + return nil +} + func (s *SwitchoverResource) Refresh(ctx context.Context, rc *resource.Context) error { if !rc.State.HasResources(s.Dependencies()...) { return resource.ErrNotFound diff --git a/server/internal/database/sync_event_resource.go b/server/internal/database/sync_event_resource.go index 05978ebe..2e8a096c 100644 --- a/server/internal/database/sync_event_resource.go +++ b/server/internal/database/sync_event_resource.go @@ -52,6 +52,10 @@ func (r *SyncEventResource) Dependencies() []resource.Identifier { return deps } +func (r *SyncEventResource) TypeDependencies() []resource.Type { + return nil +} + // Confirm synchronization by sending sync_event from provider and waiting for it on subscriber func (r *SyncEventResource) Refresh(ctx context.Context, rc *resource.Context) error { // Get provider instance diff --git a/server/internal/database/wait_for_sync_event_resource.go b/server/internal/database/wait_for_sync_event_resource.go index c254011d..d9f348d7 100644 --- a/server/internal/database/wait_for_sync_event_resource.go +++ b/server/internal/database/wait_for_sync_event_resource.go @@ -50,6 +50,10 @@ func (r *WaitForSyncEventResource) Dependencies() []resource.Identifier { } } +func (r *WaitForSyncEventResource) TypeDependencies() []resource.Type { + return nil +} + // Confirm synchronization by sending sync_event from provider and waiting for it on subscriber func (r *WaitForSyncEventResource) Refresh(ctx context.Context, rc *resource.Context) error { // Get subscriber instance diff --git a/server/internal/filesystem/dir_resource.go b/server/internal/filesystem/dir_resource.go index 11093678..65982be9 100644 --- a/server/internal/filesystem/dir_resource.go +++ b/server/internal/filesystem/dir_resource.go @@ -64,6 +64,10 @@ func (d *DirResource) Dependencies() []resource.Identifier { } } +func (d *DirResource) TypeDependencies() []resource.Type { + return nil +} + func (d *DirResource) Refresh(ctx context.Context, rc *resource.Context) error { fs, err := do.Invoke[afero.Fs](rc.Injector) if err != nil { diff --git a/server/internal/monitor/instance_monitor_resource.go b/server/internal/monitor/instance_monitor_resource.go index 876aebd8..e3264c5c 100644 --- a/server/internal/monitor/instance_monitor_resource.go +++ b/server/internal/monitor/instance_monitor_resource.go @@ -49,6 +49,10 @@ func (m *InstanceMonitorResource) Dependencies() []resource.Identifier { } } +func (m *InstanceMonitorResource) TypeDependencies() []resource.Type { + return nil +} + func (m *InstanceMonitorResource) Refresh(ctx context.Context, rc *resource.Context) error { service, err := do.Invoke[*Service](rc.Injector) if err != nil { diff --git a/server/internal/monitor/service_instance_monitor_resource.go b/server/internal/monitor/service_instance_monitor_resource.go index 2f8215e4..a68e0b97 100644 --- a/server/internal/monitor/service_instance_monitor_resource.go +++ b/server/internal/monitor/service_instance_monitor_resource.go @@ -50,6 +50,10 @@ func (m *ServiceInstanceMonitorResource) Dependencies() []resource.Identifier { } } +func (m *ServiceInstanceMonitorResource) TypeDependencies() []resource.Type { + return nil +} + func (m *ServiceInstanceMonitorResource) Refresh(ctx context.Context, rc *resource.Context) error { service, err := do.Invoke[*Service](rc.Injector) if err != nil { diff --git a/server/internal/orchestrator/swarm/check_will_restart.go b/server/internal/orchestrator/swarm/check_will_restart.go index d8307f8a..4b400551 100644 --- a/server/internal/orchestrator/swarm/check_will_restart.go +++ b/server/internal/orchestrator/swarm/check_will_restart.go @@ -55,6 +55,10 @@ func (c *CheckWillRestart) Dependencies() []resource.Identifier { } } +func (c *CheckWillRestart) TypeDependencies() []resource.Type { + return nil +} + func (c *CheckWillRestart) Refresh(ctx context.Context, rc *resource.Context) error { if !rc.State.HasResources(c.Dependencies()...) { return resource.ErrNotFound diff --git a/server/internal/orchestrator/swarm/etcd_creds.go b/server/internal/orchestrator/swarm/etcd_creds.go index 19dd20c4..c88f0a33 100644 --- a/server/internal/orchestrator/swarm/etcd_creds.go +++ b/server/internal/orchestrator/swarm/etcd_creds.go @@ -71,6 +71,10 @@ func (c *EtcdCreds) Dependencies() []resource.Identifier { } } +func (c *EtcdCreds) TypeDependencies() []resource.Type { + return nil +} + func (c *EtcdCreds) Refresh(ctx context.Context, rc *resource.Context) error { fs, err := do.Invoke[afero.Fs](rc.Injector) if err != nil { diff --git a/server/internal/orchestrator/swarm/mcp_config_resource.go b/server/internal/orchestrator/swarm/mcp_config_resource.go index e9576db8..94498083 100644 --- a/server/internal/orchestrator/swarm/mcp_config_resource.go +++ b/server/internal/orchestrator/swarm/mcp_config_resource.go @@ -72,6 +72,10 @@ func (r *MCPConfigResource) Dependencies() []resource.Identifier { } } +func (r *MCPConfigResource) TypeDependencies() []resource.Type { + return nil +} + func (r *MCPConfigResource) Refresh(ctx context.Context, rc *resource.Context) error { fs, err := do.Invoke[afero.Fs](rc.Injector) if err != nil { diff --git a/server/internal/orchestrator/swarm/network.go b/server/internal/orchestrator/swarm/network.go index 4f5685a1..d25e6022 100644 --- a/server/internal/orchestrator/swarm/network.go +++ b/server/internal/orchestrator/swarm/network.go @@ -63,6 +63,10 @@ func (n *Network) Dependencies() []resource.Identifier { return nil } +func (n *Network) TypeDependencies() []resource.Type { + return nil +} + func (n *Network) Validate() error { var errs []error if n.Scope == "" { diff --git a/server/internal/orchestrator/swarm/patroni_cluster.go b/server/internal/orchestrator/swarm/patroni_cluster.go index 619d6699..2addfd17 100644 --- a/server/internal/orchestrator/swarm/patroni_cluster.go +++ b/server/internal/orchestrator/swarm/patroni_cluster.go @@ -47,6 +47,10 @@ func (p *PatroniCluster) Dependencies() []resource.Identifier { return nil } +func (p *PatroniCluster) TypeDependencies() []resource.Type { + return nil +} + func (p *PatroniCluster) Refresh(ctx context.Context, rc *resource.Context) error { return nil } diff --git a/server/internal/orchestrator/swarm/patroni_config.go b/server/internal/orchestrator/swarm/patroni_config.go index 9971a5e8..d1f5bb02 100644 --- a/server/internal/orchestrator/swarm/patroni_config.go +++ b/server/internal/orchestrator/swarm/patroni_config.go @@ -80,6 +80,10 @@ func (c *PatroniConfig) Dependencies() []resource.Identifier { return deps } +func (r *PatroniConfig) TypeDependencies() []resource.Type { + return nil +} + func (c *PatroniConfig) Refresh(ctx context.Context, rc *resource.Context) error { fs, err := do.Invoke[afero.Fs](rc.Injector) if err != nil { diff --git a/server/internal/orchestrator/swarm/patroni_member.go b/server/internal/orchestrator/swarm/patroni_member.go index 9bc1f12b..2949df31 100644 --- a/server/internal/orchestrator/swarm/patroni_member.go +++ b/server/internal/orchestrator/swarm/patroni_member.go @@ -51,6 +51,10 @@ func (p *PatroniMember) Dependencies() []resource.Identifier { } } +func (p *PatroniMember) TypeDependencies() []resource.Type { + return nil +} + func (p *PatroniMember) Refresh(ctx context.Context, rc *resource.Context) error { return nil } diff --git a/server/internal/orchestrator/swarm/pgbackrest_config.go b/server/internal/orchestrator/swarm/pgbackrest_config.go index 37803eed..72516b8a 100644 --- a/server/internal/orchestrator/swarm/pgbackrest_config.go +++ b/server/internal/orchestrator/swarm/pgbackrest_config.go @@ -71,6 +71,10 @@ func (c *PgBackRestConfig) Dependencies() []resource.Identifier { } } +func (c *PgBackRestConfig) TypeDependencies() []resource.Type { + return nil +} + func (c *PgBackRestConfig) Refresh(ctx context.Context, rc *resource.Context) error { fs, err := do.Invoke[afero.Fs](rc.Injector) if err != nil { diff --git a/server/internal/orchestrator/swarm/pgbackrest_restore.go b/server/internal/orchestrator/swarm/pgbackrest_restore.go index c7975165..115dbe94 100644 --- a/server/internal/orchestrator/swarm/pgbackrest_restore.go +++ b/server/internal/orchestrator/swarm/pgbackrest_restore.go @@ -68,6 +68,10 @@ func (p *PgBackRestRestore) Dependencies() []resource.Identifier { } } +func (p *PgBackRestRestore) TypeDependencies() []resource.Type { + return nil +} + func (p *PgBackRestRestore) Refresh(ctx context.Context, rc *resource.Context) error { return nil } diff --git a/server/internal/orchestrator/swarm/pgbackrest_stanza.go b/server/internal/orchestrator/swarm/pgbackrest_stanza.go index c94ae583..3c601abc 100644 --- a/server/internal/orchestrator/swarm/pgbackrest_stanza.go +++ b/server/internal/orchestrator/swarm/pgbackrest_stanza.go @@ -49,6 +49,10 @@ func (p *PgBackRestStanza) Dependencies() []resource.Identifier { } } +func (p *PgBackRestStanza) TypeDependencies() []resource.Type { + return nil +} + func (p *PgBackRestStanza) Refresh(ctx context.Context, rc *resource.Context) error { client, err := do.Invoke[*docker.Docker](rc.Injector) if err != nil { diff --git a/server/internal/orchestrator/swarm/postgres_certs.go b/server/internal/orchestrator/swarm/postgres_certs.go index c5840fca..6df2af4c 100644 --- a/server/internal/orchestrator/swarm/postgres_certs.go +++ b/server/internal/orchestrator/swarm/postgres_certs.go @@ -71,6 +71,10 @@ func (c *PostgresCerts) Dependencies() []resource.Identifier { } } +func (c *PostgresCerts) TypeDependencies() []resource.Type { + return nil +} + func (c *PostgresCerts) Refresh(ctx context.Context, rc *resource.Context) error { fs, err := do.Invoke[afero.Fs](rc.Injector) if err != nil { diff --git a/server/internal/orchestrator/swarm/postgres_service.go b/server/internal/orchestrator/swarm/postgres_service.go index 180390f3..13b63c2f 100644 --- a/server/internal/orchestrator/swarm/postgres_service.go +++ b/server/internal/orchestrator/swarm/postgres_service.go @@ -59,6 +59,10 @@ func (s *PostgresService) Dependencies() []resource.Identifier { } } +func (s *PostgresService) TypeDependencies() []resource.Type { + return nil +} + func (s *PostgresService) Refresh(ctx context.Context, rc *resource.Context) error { client, err := do.Invoke[*docker.Docker](rc.Injector) if err != nil { diff --git a/server/internal/orchestrator/swarm/postgres_service_spec.go b/server/internal/orchestrator/swarm/postgres_service_spec.go index 8028a4d2..a6ba8eb3 100644 --- a/server/internal/orchestrator/swarm/postgres_service_spec.go +++ b/server/internal/orchestrator/swarm/postgres_service_spec.go @@ -65,6 +65,10 @@ func (s *PostgresServiceSpecResource) Dependencies() []resource.Identifier { } } +func (s *PostgresServiceSpecResource) TypeDependencies() []resource.Type { + return nil +} + func (s *PostgresServiceSpecResource) Refresh(ctx context.Context, rc *resource.Context) error { network, err := resource.FromContext[*Network](rc, NetworkResourceIdentifier(s.DatabaseNetworkName)) if err != nil { diff --git a/server/internal/orchestrator/swarm/scale_service.go b/server/internal/orchestrator/swarm/scale_service.go index a72692ab..b9de1d7a 100644 --- a/server/internal/orchestrator/swarm/scale_service.go +++ b/server/internal/orchestrator/swarm/scale_service.go @@ -46,6 +46,10 @@ func (s *ScaleService) Dependencies() []resource.Identifier { return append([]resource.Identifier{PostgresServiceResourceIdentifier(s.InstanceID)}, s.Deps...) } +func (s *ScaleService) TypeDependencies() []resource.Type { + return nil +} + func (s *ScaleService) Refresh(ctx context.Context, rc *resource.Context) error { return resource.ErrNotFound } diff --git a/server/internal/orchestrator/swarm/service_instance.go b/server/internal/orchestrator/swarm/service_instance.go index ba75db2a..0e537b0f 100644 --- a/server/internal/orchestrator/swarm/service_instance.go +++ b/server/internal/orchestrator/swarm/service_instance.go @@ -66,6 +66,10 @@ func (s *ServiceInstanceResource) Dependencies() []resource.Identifier { } } +func (s *ServiceInstanceResource) TypeDependencies() []resource.Type { + return nil +} + func (s *ServiceInstanceResource) Refresh(ctx context.Context, rc *resource.Context) error { client, err := do.Invoke[*docker.Docker](rc.Injector) if err != nil { diff --git a/server/internal/orchestrator/swarm/service_instance_spec.go b/server/internal/orchestrator/swarm/service_instance_spec.go index b80942eb..ca103915 100644 --- a/server/internal/orchestrator/swarm/service_instance_spec.go +++ b/server/internal/orchestrator/swarm/service_instance_spec.go @@ -68,6 +68,10 @@ func (s *ServiceInstanceSpecResource) Dependencies() []resource.Identifier { } } +func (s *ServiceInstanceSpecResource) TypeDependencies() []resource.Type { + return nil +} + func (s *ServiceInstanceSpecResource) populateCredentials(rc *resource.Context) error { userRole, err := resource.FromContext[*ServiceUserRole](rc, ServiceUserRoleIdentifier(s.ServiceSpec.ServiceID)) if err != nil { diff --git a/server/internal/orchestrator/swarm/service_user_role.go b/server/internal/orchestrator/swarm/service_user_role.go index 90ff7d76..5dbe4c38 100644 --- a/server/internal/orchestrator/swarm/service_user_role.go +++ b/server/internal/orchestrator/swarm/service_user_role.go @@ -81,6 +81,10 @@ func (r *ServiceUserRole) Dependencies() []resource.Identifier { return nil } +func (r *ServiceUserRole) TypeDependencies() []resource.Type { + return nil +} + func (r *ServiceUserRole) Refresh(ctx context.Context, rc *resource.Context) error { // If username or password is empty, the resource state is from before we // added credential management. Return ErrNotFound to trigger recreation. diff --git a/server/internal/orchestrator/swarm/switchover.go b/server/internal/orchestrator/swarm/switchover.go index 6f9c0b08..9bf8bfba 100644 --- a/server/internal/orchestrator/swarm/switchover.go +++ b/server/internal/orchestrator/swarm/switchover.go @@ -48,6 +48,10 @@ func (s *Switchover) Dependencies() []resource.Identifier { } } +func (s *Switchover) TypeDependencies() []resource.Type { + return nil +} + func (s *Switchover) Refresh(ctx context.Context, rc *resource.Context) error { if !rc.State.HasResources(s.Dependencies()...) { return resource.ErrNotFound diff --git a/server/internal/resource/resource.go b/server/internal/resource/resource.go index d0a18f4b..00a5b85a 100644 --- a/server/internal/resource/resource.go +++ b/server/internal/resource/resource.go @@ -35,15 +35,16 @@ func (r Identifier) String() string { } type ResourceData struct { - NeedsRecreate bool `json:"needs_recreate"` - Executor Executor `json:"executor"` - Identifier Identifier `json:"identifier"` - Attributes json.RawMessage `json:"attributes"` - Dependencies []Identifier `json:"dependencies"` - DiffIgnore []string `json:"diff_ignore"` - ResourceVersion string `json:"resource_version"` - PendingDeletion bool `json:"pending_deletion"` - Error string `json:"error"` + NeedsRecreate bool `json:"needs_recreate"` + Executor Executor `json:"executor"` + Identifier Identifier `json:"identifier"` + Attributes json.RawMessage `json:"attributes"` + Dependencies []Identifier `json:"dependencies"` + TypeDependencies []Type `json:"type_dependencies"` + DiffIgnore []string `json:"diff_ignore"` + ResourceVersion string `json:"resource_version"` + PendingDeletion bool `json:"pending_deletion"` + Error string `json:"error"` } func (r *ResourceData) Diff(other *ResourceData) (jsondiff.Patch, error) { @@ -63,15 +64,16 @@ func (r *ResourceData) Diff(other *ResourceData) (jsondiff.Patch, error) { func (r *ResourceData) Clone() *ResourceData { return &ResourceData{ - NeedsRecreate: r.NeedsRecreate, - Executor: r.Executor, - Identifier: r.Identifier, - Attributes: slices.Clone(r.Attributes), - Dependencies: slices.Clone(r.Dependencies), - DiffIgnore: slices.Clone(r.DiffIgnore), - ResourceVersion: r.ResourceVersion, - PendingDeletion: r.PendingDeletion, - Error: r.Error, + NeedsRecreate: r.NeedsRecreate, + Executor: r.Executor, + Identifier: r.Identifier, + Attributes: slices.Clone(r.Attributes), + Dependencies: slices.Clone(r.Dependencies), + TypeDependencies: slices.Clone(r.TypeDependencies), + DiffIgnore: slices.Clone(r.DiffIgnore), + ResourceVersion: r.ResourceVersion, + PendingDeletion: r.PendingDeletion, + Error: r.Error, } } @@ -134,6 +136,7 @@ type Resource interface { Executor() Executor Identifier() Identifier Dependencies() []Identifier + TypeDependencies() []Type Refresh(ctx context.Context, rc *Context) error Create(ctx context.Context, rc *Context) error Update(ctx context.Context, rc *Context) error @@ -148,12 +151,13 @@ func ToResourceData(resource Resource) (*ResourceData, error) { return nil, fmt.Errorf("failed to marshal resource attributes: %w", err) } return &ResourceData{ - Executor: resource.Executor(), - Identifier: resource.Identifier(), - Attributes: attributes, - Dependencies: resource.Dependencies(), - DiffIgnore: resource.DiffIgnore(), - ResourceVersion: resource.ResourceVersion(), + Executor: resource.Executor(), + Identifier: resource.Identifier(), + Attributes: attributes, + Dependencies: resource.Dependencies(), + TypeDependencies: resource.TypeDependencies(), + DiffIgnore: resource.DiffIgnore(), + ResourceVersion: resource.ResourceVersion(), }, nil } diff --git a/server/internal/resource/state.go b/server/internal/resource/state.go index b7a15749..82f6a971 100644 --- a/server/internal/resource/state.go +++ b/server/internal/resource/state.go @@ -6,6 +6,7 @@ import ( "maps" "slices" + "gonum.org/v1/gonum/graph" "gonum.org/v1/gonum/graph/simple" "github.com/pgEdge/control-plane/server/internal/ds" @@ -177,13 +178,15 @@ func (s *State) topoIter(opts graphOptions) (iter.Seq[[]*ResourceData], error) { func (s *State) graph(opts graphOptions) (*simple.DirectedGraph, error) { nodeIDs := map[Identifier]int64{} - graph := simple.NewDirectedGraph() + nodeIDsByType := map[Type][]int64{} + g := simple.NewDirectedGraph() currID := int64(1) // First pass to add nodes for _, resources := range s.Resources { for _, resource := range resources { nodeIDs[resource.Identifier] = currID - graph.AddNode(&node{ + nodeIDsByType[resource.Identifier.Type] = append(nodeIDsByType[resource.Identifier.Type], currID) + g.AddNode(&node{ id: currID, resource: resource, }) @@ -194,10 +197,13 @@ func (s *State) graph(opts graphOptions) (*simple.DirectedGraph, error) { for _, resources := range s.Resources { for _, resource := range resources { toID := nodeIDs[resource.Identifier] - to := graph.Node(toID) + to := g.Node(toID) for _, dep := range resource.Dependencies { + if dep == resource.Identifier { + return nil, fmt.Errorf("invalid dependency: resource '%s' cannot depend on itself", resource.Identifier) + } fromID, ok := nodeIDs[dep] - from := graph.Node(fromID) + from := g.Node(fromID) if !ok { if opts.ignoreMissingDeps { continue @@ -205,25 +211,38 @@ func (s *State) graph(opts graphOptions) (*simple.DirectedGraph, error) { return nil, fmt.Errorf("dependency of %s not found: %s", resource.Identifier, dep) } } - // Our layered topological sort returns in 'from' to 'to' order. - // So modeling from dependency to dependent gets us the order we - // want for creates and updates. - if opts.creationOrdered { - graph.SetEdge(simple.Edge{ - T: to, - F: from, - }) - } else { - // For deletion order we need to reverse the edge. - graph.SetEdge(simple.Edge{ - T: from, - F: to, - }) + addEdge(opts, g, from, to) + } + for _, ty := range resource.TypeDependencies { + if ty == resource.Identifier.Type { + return nil, fmt.Errorf("invalid type dependency: resource '%s' cannot depend on its own type '%s'", resource.Identifier, ty) + } + for _, fromID := range nodeIDsByType[ty] { + from := g.Node(fromID) + addEdge(opts, g, from, to) } } } } - return graph, nil + return g, nil +} + +func addEdge(opts graphOptions, g *simple.DirectedGraph, from, to graph.Node) { + // Our layered topological sort returns in 'from' to 'to' order. + // So modeling from dependency to dependent gets us the order we + // want for creates and updates. + if opts.creationOrdered { + g.SetEdge(simple.Edge{ + T: to, + F: from, + }) + } else { + // For deletion order we need to reverse the edge. + g.SetEdge(simple.Edge{ + T: from, + F: to, + }) + } } func (s *State) PlanRefresh() (Plan, error) { @@ -274,6 +293,7 @@ func (s *State) planCreates(options PlanOptions, desired *State) (Plan, error) { // Keeps track of all modified resources so that we can update their // dependents. modified := ds.NewSet[Identifier]() + modifiedTypes := ds.NewSet[Type]() for layer := range layers { var phase []*Event @@ -309,7 +329,7 @@ func (s *State) planCreates(options PlanOptions, desired *State) (Plan, error) { Resource: resource, Reason: EventReasonForceUpdate, } - case slices.ContainsFunc(resource.Dependencies, modified.Has): + case slices.ContainsFunc(resource.TypeDependencies, modifiedTypes.Has), slices.ContainsFunc(resource.Dependencies, modified.Has): event = &Event{ Type: EventTypeUpdate, Resource: resource, @@ -333,6 +353,7 @@ func (s *State) planCreates(options PlanOptions, desired *State) (Plan, error) { if event != nil { phase = append(phase, event) modified.Add(resource.Identifier) + modifiedTypes.Add(resource.Identifier.Type) } } @@ -428,6 +449,24 @@ func FromState[T Resource](state *State, identifier Identifier) (T, error) { return ToResource[T](data) } +func AllFromState[T Resource](state *State, resourceType Type) ([]T, error) { + data := state.GetAll(resourceType) + all := make([]T, len(data)) + for i, d := range data { + resource, err := ToResource[T](d) + if err != nil { + return nil, err + } + all[i] = resource + } + + return all, nil +} + func FromContext[T Resource](rc *Context, identifier Identifier) (T, error) { return FromState[T](rc.State, identifier) } + +func AllFromContext[T Resource](rc *Context, resourceType Type) ([]T, error) { + return AllFromState[T](rc.State, resourceType) +} diff --git a/server/internal/resource/state_test.go b/server/internal/resource/state_test.go index 45bb0240..cfd21c18 100644 --- a/server/internal/resource/state_test.go +++ b/server/internal/resource/state_test.go @@ -16,7 +16,7 @@ func TestState(t *testing.T) { resource1 := &testResource{ ID: "test1", TestDependencies: []resource.Identifier{ - testResourceID("test2"), + testResourceID(testResourceType, "test2"), }, } resource1Data, err := resource.ToResourceData(resource1) @@ -25,7 +25,7 @@ func TestState(t *testing.T) { resource2 := &testResource{ ID: "test2", TestDependencies: []resource.Identifier{ - testResourceID("test3"), + testResourceID(testResourceType, "test3"), }, } resource2Data, err := resource.ToResourceData(resource2) @@ -75,7 +75,7 @@ func TestState(t *testing.T) { resource1 := &testResource{ ID: "test1", TestDependencies: []resource.Identifier{ - testResourceID("test2"), + testResourceID(testResourceType, "test2"), }, } resource1Data, err := resource.ToResourceData(resource1) @@ -84,7 +84,7 @@ func TestState(t *testing.T) { resource2 := &testResource{ ID: "test2", TestDependencies: []resource.Identifier{ - testResourceID("test3"), + testResourceID(testResourceType, "test3"), }, } resource2Data, err := resource.ToResourceData(resource2) @@ -136,7 +136,7 @@ func TestState(t *testing.T) { resource1 := &testResource{ ID: "test1", TestDependencies: []resource.Identifier{ - testResourceID("test2"), + testResourceID(testResourceType, "test2"), }, } resource1Data, err := resource.ToResourceData(resource1) @@ -145,7 +145,7 @@ func TestState(t *testing.T) { resource2 := &testResource{ ID: "test2", TestDependencies: []resource.Identifier{ - testResourceID("test3"), + testResourceID(testResourceType, "test3"), }, } resource2Data, err := resource.ToResourceData(resource2) @@ -190,7 +190,7 @@ func TestState(t *testing.T) { resource1 := &testResource{ ID: "test1", TestDependencies: []resource.Identifier{ - testResourceID("test2"), + testResourceID(testResourceType, "test2"), }, } resource1Data, err := resource.ToResourceData(resource1) @@ -199,7 +199,7 @@ func TestState(t *testing.T) { resource2 := &testResource{ ID: "test2", TestDependencies: []resource.Identifier{ - testResourceID("test3"), + testResourceID(testResourceType, "test3"), }, } resource2Data, err := resource.ToResourceData(resource2) @@ -213,7 +213,7 @@ func TestState(t *testing.T) { SomeAttribute: "updated", ID: "test2", TestDependencies: []resource.Identifier{ - testResourceID("test3"), + testResourceID(testResourceType, "test3"), }, } updatedResource2Data, err := resource.ToResourceData(updatedResource2) @@ -258,11 +258,72 @@ func TestState(t *testing.T) { assert.Equal(t, expected, plan) }) + t.Run("updated type dependency", func(t *testing.T) { + resource1 := &testResource{ + TestType: "other_resource", + ID: "test1", + TestTypeDependencies: []resource.Type{ + testResourceType, + }, + } + resource1Data, err := resource.ToResourceData(resource1) + require.NoError(t, err) + + resource2 := &testResource{ + ID: "test2", + } + resource2Data, err := resource.ToResourceData(resource2) + require.NoError(t, err) + + updatedResource2 := &testResource{ + SomeAttribute: "updated", + ID: "test2", + } + updatedResource2Data, err := resource.ToResourceData(updatedResource2) + require.NoError(t, err) + + current := resource.NewState() + desired := resource.NewState() + + current.AddResource(resource1) + current.AddResource(resource2) + + desired.AddResource(resource1) + desired.AddResource(updatedResource2) + + plan, err := current.Plan(resource.PlanOptions{}, desired) + assert.NoError(t, err) + + expectedDiff, err := resource2Data.Diff(updatedResource2Data) + assert.NoError(t, err) + + expected := resource.Plan{ + { + { + Type: resource.EventTypeUpdate, + Resource: updatedResource2Data, + Reason: resource.EventReasonHasDiff, + Diff: expectedDiff, + }, + }, + { + // Resource 1 should be marked for update because it has a + // type dependency on testResourceType. + { + Type: resource.EventTypeUpdate, + Resource: resource1Data, + Reason: resource.EventReasonDependencyUpdated, + }, + }, + } + + assert.Equal(t, expected, plan) + }) t.Run("to empty state", func(t *testing.T) { resource1 := &testResource{ ID: "test1", TestDependencies: []resource.Identifier{ - testResourceID("test2"), + testResourceID(testResourceType, "test2"), }, } resource1Data, err := resource.ToResourceData(resource1) @@ -271,7 +332,7 @@ func TestState(t *testing.T) { resource2 := &testResource{ ID: "test2", TestDependencies: []resource.Identifier{ - testResourceID("test3"), + testResourceID(testResourceType, "test3"), }, } resource2Data, err := resource.ToResourceData(resource2) @@ -320,7 +381,7 @@ func TestState(t *testing.T) { resource1 := &testResource{ ID: "test1", TestDependencies: []resource.Identifier{ - testResourceID("test2"), + testResourceID(testResourceType, "test2"), }, } resource1Data, err := resource.ToResourceData(resource1) @@ -335,7 +396,7 @@ func TestState(t *testing.T) { resource3 := &testResource{ ID: "test3", TestDependencies: []resource.Identifier{ - testResourceID("test4"), + testResourceID(testResourceType, "test4"), }, } resource3Data, err := resource.ToResourceData(resource3) @@ -356,7 +417,7 @@ func TestState(t *testing.T) { resource6 := &testResource{ ID: "test6", TestDependencies: []resource.Identifier{ - testResourceID("test5"), + testResourceID(testResourceType, "test5"), }, } resource6Data, err := resource.ToResourceData(resource6) @@ -426,7 +487,7 @@ func TestState(t *testing.T) { resource1Data, err := resource.ToResourceData(&testResource{ ID: "test1", TestDependencies: []resource.Identifier{ - testResourceID("test2"), + testResourceID(testResourceType, "test2"), }, }) require.NoError(t, err) @@ -472,7 +533,7 @@ func TestState(t *testing.T) { resource1Data, err := resource.ToResourceData(&testResource{ ID: "test1", TestDependencies: []resource.Identifier{ - testResourceID("test2"), + testResourceID(testResourceType, "test2"), }, }) require.NoError(t, err) @@ -517,7 +578,7 @@ func TestState(t *testing.T) { resource1 := &testResource{ ID: "test1", TestDependencies: []resource.Identifier{ - testResourceID("test2"), + testResourceID(testResourceType, "test2"), }, } @@ -531,12 +592,45 @@ func TestState(t *testing.T) { assert.ErrorContains(t, err, "dependency of test_resource::test1 not found: test_resource::test2") assert.Nil(t, plan) }) + t.Run("self-referential dependency", func(t *testing.T) { + resource1 := &testResource{ + ID: "test1", + TestDependencies: []resource.Identifier{ + testResourceID(testResourceType, "test1"), + }, + } + + current := resource.NewState() + desired := resource.NewState() + + // missing dependencies produce an error during creates + desired.AddResource(resource1) + + plan, err := current.Plan(resource.PlanOptions{}, desired) + assert.ErrorContains(t, err, "invalid dependency: resource 'test_resource::test1' cannot depend on itself") + assert.Nil(t, plan) + }) + t.Run("self-referential type dependency", func(t *testing.T) { + resource1 := &testResource{ + ID: "test1", + TestTypeDependencies: []resource.Type{testResourceType}, + } + current := resource.NewState() + desired := resource.NewState() + + // missing dependencies produce an error during creates + desired.AddResource(resource1) + + plan, err := current.Plan(resource.PlanOptions{}, desired) + assert.ErrorContains(t, err, "invalid type dependency: resource 'test_resource::test1' cannot depend on its own type 'test_resource'") + assert.Nil(t, plan) + }) t.Run("missing delete dependency", func(t *testing.T) { resource1 := &testResource{ ID: "test1", TestDependencies: []resource.Identifier{ - testResourceID("test2"), + testResourceID(testResourceType, "test2"), }, } resource1Data, err := resource.ToResourceData(resource1) @@ -587,18 +681,23 @@ func TestState(t *testing.T) { const testResourceType = resource.Type("test_resource") -func testResourceID(id string) resource.Identifier { +func testResourceID(resourceType resource.Type, id string) resource.Identifier { + if resourceType == "" { + resourceType = testResourceType + } return resource.Identifier{ - Type: testResourceType, + Type: resourceType, ID: id, } } type testResource struct { + TestType resource.Type `json:"test_type"` SomeAttribute string `json:"some_attribute"` SomeIgnoredAttribute string `json:"some_ignored_attribute"` ID string `json:"id"` TestDependencies []resource.Identifier `json:"test_dependencies"` + TestTypeDependencies []resource.Type `json:"test_type_dependencies"` NotFound bool `json:"not_found"` Error string `json:"error"` } @@ -618,13 +717,17 @@ func (r *testResource) Executor() resource.Executor { } func (r *testResource) Identifier() resource.Identifier { - return testResourceID(r.ID) + return testResourceID(r.TestType, r.ID) } func (r *testResource) Dependencies() []resource.Identifier { return r.TestDependencies } +func (r *testResource) TypeDependencies() []resource.Type { + return r.TestTypeDependencies +} + func (r *testResource) Refresh(ctx context.Context, rc *resource.Context) error { switch { case r.NotFound: diff --git a/server/internal/scheduler/scheduled_job_resource.go b/server/internal/scheduler/scheduled_job_resource.go index 7321a67e..184db3d0 100644 --- a/server/internal/scheduler/scheduled_job_resource.go +++ b/server/internal/scheduler/scheduled_job_resource.go @@ -8,6 +8,8 @@ import ( "github.com/samber/do" ) +var _ resource.Resource = (*ScheduledJobResource)(nil) + const ResourceTypeScheduledJob resource.Type = "scheduler.job" func ScheduledJobResourceIdentifier(id string) resource.Identifier { @@ -53,9 +55,15 @@ func (r *ScheduledJobResource) Executor() resource.Executor { func (r *ScheduledJobResource) Identifier() resource.Identifier { return ScheduledJobResourceIdentifier(r.ID) } + func (r *ScheduledJobResource) Dependencies() []resource.Identifier { return r.DependsOn } + +func (r *ScheduledJobResource) TypeDependencies() []resource.Type { + return nil +} + func (r *ScheduledJobResource) Refresh(ctx context.Context, rc *resource.Context) error { service, err := do.Invoke[*Service](rc.Injector) if err != nil {