diff --git a/server/internal/database/operations/golden_test/TestUpdateDatabase/add_service_to_existing_database.json b/server/internal/database/operations/golden_test/TestUpdateDatabase/add_service_to_existing_database.json new file mode 100644 index 00000000..e89c89bd --- /dev/null +++ b/server/internal/database/operations/golden_test/TestUpdateDatabase/add_service_to_existing_database.json @@ -0,0 +1,42 @@ +[ + [ + [ + { + "type": "create", + "resource_id": "swarm.network::database-id", + "reason": "does_not_exist", + "diff": null + }, + { + "type": "create", + "resource_id": "swarm.service_user_role::database-id-test-svc-host-1-id", + "reason": "does_not_exist", + "diff": null + } + ], + [ + { + "type": "create", + "resource_id": "swarm.service_instance_spec::database-id-test-svc-host-1-id", + "reason": "does_not_exist", + "diff": null + } + ], + [ + { + "type": "create", + "resource_id": "swarm.service_instance::database-id-test-svc-host-1-id", + "reason": "does_not_exist", + "diff": null + } + ], + [ + { + "type": "create", + "resource_id": "monitor.service_instance::database-id-test-svc-host-1-id", + "reason": "does_not_exist", + "diff": null + } + ] + ] +] \ No newline at end of file diff --git a/server/internal/database/operations/golden_test/TestUpdateDatabase/remove_service_from_existing_database.json b/server/internal/database/operations/golden_test/TestUpdateDatabase/remove_service_from_existing_database.json new file mode 100644 index 00000000..de7e4f9c --- /dev/null +++ b/server/internal/database/operations/golden_test/TestUpdateDatabase/remove_service_from_existing_database.json @@ -0,0 +1,37 @@ +[ + [ + [ + { + "type": "delete", + "resource_id": "monitor.service_instance::database-id-test-svc-host-1-id", + "diff": null + } + ], + [ + { + "type": "delete", + "resource_id": "swarm.service_instance::database-id-test-svc-host-1-id", + "diff": null + } + ], + [ + { + "type": "delete", + "resource_id": "swarm.service_instance_spec::database-id-test-svc-host-1-id", + "diff": null + } + ], + [ + { + "type": "delete", + "resource_id": "swarm.network::database-id", + "diff": null + }, + { + "type": "delete", + "resource_id": "swarm.service_user_role::database-id-test-svc-host-1-id", + "diff": null + } + ] + ] +] \ No newline at end of file diff --git a/server/internal/database/operations/golden_test/TestUpdateDatabase/single_node_with_service_from_empty.json b/server/internal/database/operations/golden_test/TestUpdateDatabase/single_node_with_service_from_empty.json new file mode 100644 index 00000000..50cc4fd8 --- /dev/null +++ b/server/internal/database/operations/golden_test/TestUpdateDatabase/single_node_with_service_from_empty.json @@ -0,0 +1,74 @@ +[ + [ + [ + { + "type": "create", + "resource_id": "orchestrator.resource::n1-instance-1-dep-1-id", + "reason": "does_not_exist", + "diff": null + } + ], + [ + { + "type": "create", + "resource_id": "database.instance::n1-instance-1-id", + "reason": "does_not_exist", + "diff": null + } + ], + [ + { + "type": "create", + "resource_id": "database.node::n1", + "reason": "does_not_exist", + "diff": null + }, + { + "type": "create", + "resource_id": "monitor.instance::n1-instance-1-id", + "reason": "does_not_exist", + "diff": null + } + ] + ], + [ + [ + { + "type": "create", + "resource_id": "swarm.network::database-id", + "reason": "does_not_exist", + "diff": null + }, + { + "type": "create", + "resource_id": "swarm.service_user_role::database-id-test-svc-host-1-id", + "reason": "does_not_exist", + "diff": null + } + ], + [ + { + "type": "create", + "resource_id": "swarm.service_instance_spec::database-id-test-svc-host-1-id", + "reason": "does_not_exist", + "diff": null + } + ], + [ + { + "type": "create", + "resource_id": "swarm.service_instance::database-id-test-svc-host-1-id", + "reason": "does_not_exist", + "diff": null + } + ], + [ + { + "type": "create", + "resource_id": "monitor.service_instance::database-id-test-svc-host-1-id", + "reason": "does_not_exist", + "diff": null + } + ] + ] +] \ No newline at end of file diff --git a/server/internal/database/operations/golden_test/TestUpdateDatabase/single_node_with_service_no-op.json b/server/internal/database/operations/golden_test/TestUpdateDatabase/single_node_with_service_no-op.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/server/internal/database/operations/golden_test/TestUpdateDatabase/single_node_with_service_no-op.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/server/internal/database/operations/golden_test/TestUpdateDatabase/update_database_node_with_unchanged_service.json b/server/internal/database/operations/golden_test/TestUpdateDatabase/update_database_node_with_unchanged_service.json new file mode 100644 index 00000000..e57acc61 --- /dev/null +++ b/server/internal/database/operations/golden_test/TestUpdateDatabase/update_database_node_with_unchanged_service.json @@ -0,0 +1,34 @@ +[ + [ + [ + { + "type": "create", + "resource_id": "orchestrator.resource::n1-instance-1-dep-2-id", + "reason": "does_not_exist", + "diff": null + } + ], + [ + { + "type": "update", + "resource_id": "database.instance::n1-instance-1-id", + "reason": "dependency_updated", + "diff": null + } + ], + [ + { + "type": "update", + "resource_id": "database.node::n1", + "reason": "dependency_updated", + "diff": null + }, + { + "type": "update", + "resource_id": "monitor.instance::n1-instance-1-id", + "reason": "dependency_updated", + "diff": null + } + ] + ] +] \ No newline at end of file diff --git a/server/internal/database/operations/helpers_test.go b/server/internal/database/operations/helpers_test.go index d759e9a5..fdb465ce 100644 --- a/server/internal/database/operations/helpers_test.go +++ b/server/internal/database/operations/helpers_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/pgEdge/control-plane/server/internal/database" + "github.com/pgEdge/control-plane/server/internal/database/operations" "github.com/pgEdge/control-plane/server/internal/monitor" "github.com/pgEdge/control-plane/server/internal/resource" "github.com/stretchr/testify/assert" @@ -176,3 +177,152 @@ func (r *restoreResource) Identifier() resource.Identifier { Type: "orchestrator.restore_resource", } } + +// Service resource stubs using the orchestratorResource embedding pattern. +// These mirror the real resource types' Identifier/Dependencies/DiffIgnore +// without importing the swarm package. + +type serviceNetworkResource struct { + orchestratorResource + nodeNames []string +} + +func (r *serviceNetworkResource) Identifier() resource.Identifier { + return resource.Identifier{ID: r.ID, Type: "swarm.network"} +} + +func (r *serviceNetworkResource) DiffIgnore() []string { + return []string{"/network_id", "/subnet", "/gateway"} +} + +func (r *serviceNetworkResource) Executor() resource.Executor { + return resource.ManagerExecutor() +} + +func (r *serviceNetworkResource) Dependencies() []resource.Identifier { + var deps []resource.Identifier + for _, name := range r.nodeNames { + deps = append(deps, database.NodeResourceIdentifier(name)) + } + return deps +} + +type serviceUserRoleResource struct { + orchestratorResource + nodeNames []string +} + +func (r *serviceUserRoleResource) Identifier() resource.Identifier { + return resource.Identifier{ID: r.ID, Type: "swarm.service_user_role"} +} + +func (r *serviceUserRoleResource) DiffIgnore() []string { + return []string{"/postgres_host_id", "/username", "/password"} +} + +func (r *serviceUserRoleResource) Dependencies() []resource.Identifier { + var deps []resource.Identifier + for _, name := range r.nodeNames { + deps = append(deps, database.NodeResourceIdentifier(name)) + } + return deps +} + +type serviceInstanceSpecResource struct { + orchestratorResource + networkID string + serviceInstanceID string + hostID string +} + +func (r *serviceInstanceSpecResource) Executor() resource.Executor { + return resource.HostExecutor(r.hostID) +} + +func (r *serviceInstanceSpecResource) Identifier() resource.Identifier { + return resource.Identifier{ID: r.ID, Type: "swarm.service_instance_spec"} +} + +func (r *serviceInstanceSpecResource) DiffIgnore() []string { + return []string{"/spec"} +} + +func (r *serviceInstanceSpecResource) Dependencies() []resource.Identifier { + return []resource.Identifier{ + {ID: r.networkID, Type: "swarm.network"}, + {ID: r.serviceInstanceID, Type: "swarm.service_user_role"}, + } +} + +type serviceInstanceResource struct { + orchestratorResource + serviceInstanceID string +} + +func (r *serviceInstanceResource) Identifier() resource.Identifier { + return resource.Identifier{ID: r.ID, Type: "swarm.service_instance"} +} + +func (r *serviceInstanceResource) DiffIgnore() []string { + return []string{"/database_id", "/service_id", "/host_id"} +} + +func (r *serviceInstanceResource) Executor() resource.Executor { + return resource.ManagerExecutor() +} + +func (r *serviceInstanceResource) Dependencies() []resource.Identifier { + return []resource.Identifier{ + {ID: r.serviceInstanceID, Type: "swarm.service_user_role"}, + {ID: r.serviceInstanceID, Type: "swarm.service_instance_spec"}, + } +} + +func makeServiceResources(t testing.TB, databaseID, serviceID, hostID string, nodeNames []string) *operations.ServiceResources { + t.Helper() + + serviceInstanceID := database.GenerateServiceInstanceID(databaseID, serviceID, hostID) + databaseNetworkID := database.GenerateDatabaseNetworkID(databaseID) + + resources := []resource.Resource{ + &serviceNetworkResource{ + orchestratorResource: orchestratorResource{ID: databaseNetworkID}, + nodeNames: nodeNames, + }, + &serviceUserRoleResource{ + orchestratorResource: orchestratorResource{ID: serviceInstanceID}, + nodeNames: nodeNames, + }, + &serviceInstanceSpecResource{ + orchestratorResource: orchestratorResource{ID: serviceInstanceID}, + networkID: databaseNetworkID, + serviceInstanceID: serviceInstanceID, + hostID: hostID, + }, + &serviceInstanceResource{ + orchestratorResource: orchestratorResource{ID: serviceInstanceID}, + serviceInstanceID: serviceInstanceID, + }, + } + + resourceData := make([]*resource.ResourceData, len(resources)) + for i, res := range resources { + rd, err := resource.ToResourceData(res) + if err != nil { + t.Fatal(err) + } + resourceData[i] = rd + } + + monitorResource := &monitor.ServiceInstanceMonitorResource{ + DatabaseID: databaseID, + ServiceInstanceID: serviceInstanceID, + HostID: hostID, + } + + return &operations.ServiceResources{ + ServiceInstanceID: serviceInstanceID, + Resources: resourceData, + MonitorResource: monitorResource, + } +} diff --git a/server/internal/database/operations/update_database_test.go b/server/internal/database/operations/update_database_test.go index 1c931778..9651f271 100644 --- a/server/internal/database/operations/update_database_test.go +++ b/server/internal/database/operations/update_database_test.go @@ -153,11 +153,31 @@ func TestUpdateDatabase(t *testing.T) { ), ) + svcRes := makeServiceResources(t, "database-id", "test-svc", "host-1-id", nil) + + singleNodeWithServiceState := makeState(t, + []resource.Resource{ + n1Instance1.Instance, + makeMonitorResource(n1Instance1), + &database.NodeResource{ + Name: "n1", + PrimaryInstanceID: n1Instance1.InstanceID(), + InstanceIDs: []string{n1Instance1.InstanceID()}, + }, + svcRes.MonitorResource, + }, + slices.Concat( + n1Instance1.Resources, + svcRes.Resources, + ), + ) + for _, tc := range []struct { name string options operations.UpdateDatabaseOptions start *resource.State nodes []*operations.NodeResources + services []*operations.ServiceResources expectedErr string }{ { @@ -444,13 +464,77 @@ func TestUpdateDatabase(t *testing.T) { options: operations.UpdateDatabaseOptions{}, start: twoNodeState, }, + { + name: "single node with service from empty", + options: operations.UpdateDatabaseOptions{}, + start: resource.NewState(), + nodes: []*operations.NodeResources{ + { + NodeName: "n1", + InstanceResources: []*database.InstanceResources{n1Instance1}, + }, + }, + services: []*operations.ServiceResources{ + makeServiceResources(t, "database-id", "test-svc", "host-1-id", nil), + }, + }, + { + name: "single node with service no-op", + options: operations.UpdateDatabaseOptions{}, + start: singleNodeWithServiceState, + nodes: []*operations.NodeResources{ + { + NodeName: "n1", + InstanceResources: []*database.InstanceResources{n1Instance1}, + }, + }, + services: []*operations.ServiceResources{svcRes}, + }, + { + name: "add service to existing database", + options: operations.UpdateDatabaseOptions{}, + start: singleNodeState, + nodes: []*operations.NodeResources{ + { + NodeName: "n1", + InstanceResources: []*database.InstanceResources{n1Instance1}, + }, + }, + services: []*operations.ServiceResources{ + makeServiceResources(t, "database-id", "test-svc", "host-1-id", nil), + }, + }, + { + name: "remove service from existing database", + options: operations.UpdateDatabaseOptions{}, + start: singleNodeWithServiceState, + nodes: []*operations.NodeResources{ + { + NodeName: "n1", + InstanceResources: []*database.InstanceResources{n1Instance1}, + }, + }, + services: nil, + }, + { + name: "update database node with unchanged service", + options: operations.UpdateDatabaseOptions{}, + start: singleNodeWithServiceState, + nodes: []*operations.NodeResources{ + { + NodeName: "n1", + InstanceResources: []*database.InstanceResources{n1Instance1WithNewDependency}, + }, + }, + services: []*operations.ServiceResources{svcRes}, + }, } { t.Run(tc.name, func(t *testing.T) { plans, err := operations.UpdateDatabase( tc.options, tc.start, tc.nodes, - nil, + tc.services, ) if tc.expectedErr != "" { assert.Nil(t, plans)