diff --git a/api/apiv1/design/task.go b/api/apiv1/design/task.go index 2272116b..5dc0bcc8 100644 --- a/api/apiv1/design/task.go +++ b/api/apiv1/design/task.go @@ -10,6 +10,15 @@ var Task = g.Type("Task", func() { g.Description("The parent task ID of the task.") g.Example("439eb515-e700-4740-b508-4a3f12ec4f83") }) + g.Attribute("scope", g.String, func() { + g.Enum("database", "host") + g.Description("The scope of the task (database or host).") + g.Example("database") + }) + g.Attribute("entity_id", g.String, func() { + g.Description("The entity ID (database_id or host_id) that this task belongs to.") + g.Example("02f1a7db-fca8-4521-b57a-2a375c1ced51") + }) g.Attribute("database_id", g.String, func() { g.Description("The database ID of the task.") g.Example("02f1a7db-fca8-4521-b57a-2a375c1ced51") @@ -55,9 +64,11 @@ var Task = g.Type("Task", func() { g.Example("failed to connect to database") }) - g.Required("database_id", "task_id", "created_at", "type", "status") + g.Required("scope", "entity_id", "task_id", "created_at", "type", "status") g.Example(map[string]any{ + "scope": "database", + "entity_id": "storefront", "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", @@ -88,8 +99,17 @@ var TaskLogEntry = g.Type("TaskLogEntry", func() { }) var TaskLog = g.Type("TaskLog", func() { + g.Attribute("scope", g.String, func() { + g.Enum("database", "host") + g.Description("The scope of the task (database or host).") + g.Example("database") + }) + g.Attribute("entity_id", g.String, func() { + g.Description("The entity ID (database_id or host_id) that this task log belongs to.") + g.Example("02f1a7db-fca8-4521-b57a-2a375c1ced51") + }) g.Attribute("database_id", g.String, func() { - g.Description("The database ID of the task log.") + g.Description("The database ID of the task log. Deprecated: use entity_id instead.") g.Example("02f1a7db-fca8-4521-b57a-2a375c1ced51") }) g.Attribute("task_id", g.String, func() { @@ -109,12 +129,13 @@ var TaskLog = g.Type("TaskLog", func() { g.Description("Entries in the task log.") }) - g.Required("database_id", "task_id", "task_status", "entries") + g.Required("scope", "entity_id", "task_id", "task_status", "entries") g.Example("node_backup task log", func() { g.Description("The task log from a 'node_backup' task. These messages are produced by pgbackrest.") g.Value(map[string]any{ - "database_id": "storefront", + "scope": "database", + "entity_id": "storefront", "entries": []map[string]any{ { "message": "P00 INFO: backup command begin 2.55.1: --config=/opt/pgedge/configs/pgbackrest.backup.conf --exec-id=198-b17fae6e --log-level-console=info --no-log-timestamp --pg1-path=/opt/pgedge/data/pgdata --pg1-user=pgedge --repo1-cipher-type=none --repo1-path=/backups/databases/storefront/n1 --repo1-retention-full=7 --repo1-retention-full-type=time --repo1-type=posix --stanza=db --start-fast --type=full", @@ -178,7 +199,8 @@ var TaskLog = g.Type("TaskLog", func() { g.Example("update task log", func() { g.Description("This is the task log of an update task. This example excludes many entries for brevity.") g.Value(map[string]any{ - "database_id": "storefront", + "scope": "database", + "entity_id": "storefront", "entries": []map[string]any{ { "message": "refreshing current state", @@ -247,6 +269,8 @@ var ListDatabaseTasksResponse = g.Type("ListDatabaseTasksResponse", func() { { "completed_at": "2025-06-18T17:54:36Z", "created_at": "2025-06-18T17:54:28Z", + "scope": "database", + "entity_id": "storefront", "database_id": "storefront", "instance_id": "storefront-n1-689qacsi", "status": "completed", @@ -256,6 +280,8 @@ var ListDatabaseTasksResponse = g.Type("ListDatabaseTasksResponse", func() { { "completed_at": "2025-06-18T17:54:04Z", "created_at": "2025-06-18T17:53:17Z", + "scope": "database", + "entity_id": "storefront", "database_id": "storefront", "status": "completed", "task_id": "0197842c-7c4f-7a8c-829e-7405c2a41c8c", @@ -264,6 +290,8 @@ var ListDatabaseTasksResponse = g.Type("ListDatabaseTasksResponse", func() { { "completed_at": "2025-06-18T17:23:28Z", "created_at": "2025-06-18T17:23:14Z", + "scope": "database", + "entity_id": "storefront", "database_id": "storefront", "status": "completed", "task_id": "01978410-fb5d-7cd2-bbd2-66c0bf929dc0", @@ -272,6 +300,8 @@ var ListDatabaseTasksResponse = g.Type("ListDatabaseTasksResponse", func() { { "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", + "scope": "database", + "entity_id": "storefront", "database_id": "storefront", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", diff --git a/api/apiv1/gen/control_plane/service.go b/api/apiv1/gen/control_plane/service.go index ccdeebe6..618c9612 100644 --- a/api/apiv1/gen/control_plane/service.go +++ b/api/apiv1/gen/control_plane/service.go @@ -870,8 +870,12 @@ type SwitchoverDatabaseNodeResponse struct { type Task struct { // The parent task ID of the task. ParentID *string + // The scope of the task (database or host). + Scope string + // The entity ID (database_id or host_id) that this task belongs to. + EntityID string // The database ID of the task. - DatabaseID string + DatabaseID *string // The name of the node that the task is operating on. NodeName *string // The ID of the instance that the task is operating on. @@ -895,8 +899,12 @@ type Task struct { // TaskLog is the result type of the control-plane service // get-database-task-log method. type TaskLog struct { - // The database ID of the task log. - DatabaseID string + // The scope of the task (database or host). + Scope string + // The entity ID (database_id or host_id) that this task log belongs to. + EntityID string + // The database ID of the task log. Deprecated: use entity_id instead. + DatabaseID *string // The unique ID of the task log. TaskID string // The status of the task. diff --git a/api/apiv1/gen/control_plane/views/view.go b/api/apiv1/gen/control_plane/views/view.go index 2adf81cf..1745f0fe 100644 --- a/api/apiv1/gen/control_plane/views/view.go +++ b/api/apiv1/gen/control_plane/views/view.go @@ -409,6 +409,10 @@ type CreateDatabaseResponseView struct { type TaskView struct { // The parent task ID of the task. ParentID *string + // The scope of the task (database or host). + Scope *string + // The entity ID (database_id or host_id) that this task belongs to. + EntityID *string // The database ID of the task. DatabaseID *string // The name of the node that the task is operating on. @@ -1519,8 +1523,11 @@ func ValidateCreateDatabaseResponseView(result *CreateDatabaseResponseView) (err // ValidateTaskView runs the validations defined on TaskView. func ValidateTaskView(result *TaskView) (err error) { - if result.DatabaseID == nil { - err = goa.MergeErrors(err, goa.MissingFieldError("database_id", "result")) + if result.Scope == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("scope", "result")) + } + if result.EntityID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("entity_id", "result")) } if result.TaskID == nil { err = goa.MergeErrors(err, goa.MissingFieldError("task_id", "result")) @@ -1537,6 +1544,11 @@ func ValidateTaskView(result *TaskView) (err error) { if result.ParentID != nil { err = goa.MergeErrors(err, goa.ValidateFormat("result.parent_id", *result.ParentID, goa.FormatUUID)) } + if result.Scope != nil { + if !(*result.Scope == "database" || *result.Scope == "host") { + err = goa.MergeErrors(err, goa.InvalidEnumValueError("result.scope", *result.Scope, []any{"database", "host"})) + } + } if result.TaskID != nil { err = goa.MergeErrors(err, goa.ValidateFormat("result.task_id", *result.TaskID, goa.FormatUUID)) } diff --git a/api/apiv1/gen/http/control_plane/client/encode_decode.go b/api/apiv1/gen/http/control_plane/client/encode_decode.go index 580c65ec..b7ea96a6 100644 --- a/api/apiv1/gen/http/control_plane/client/encode_decode.go +++ b/api/apiv1/gen/http/control_plane/client/encode_decode.go @@ -3598,7 +3598,9 @@ func unmarshalPgEdgeVersionResponseBodyToControlplanePgEdgeVersion(v *PgEdgeVers func unmarshalTaskResponseBodyToControlplaneTask(v *TaskResponseBody) *controlplane.Task { res := &controlplane.Task{ ParentID: v.ParentID, - DatabaseID: *v.DatabaseID, + Scope: *v.Scope, + EntityID: *v.EntityID, + DatabaseID: v.DatabaseID, NodeName: v.NodeName, InstanceID: v.InstanceID, HostID: v.HostID, diff --git a/api/apiv1/gen/http/control_plane/client/types.go b/api/apiv1/gen/http/control_plane/client/types.go index f8475de8..9d2a4ae4 100644 --- a/api/apiv1/gen/http/control_plane/client/types.go +++ b/api/apiv1/gen/http/control_plane/client/types.go @@ -263,6 +263,10 @@ type ListDatabaseTasksResponseBody struct { type GetDatabaseTaskResponseBody struct { // The parent task ID of the task. ParentID *string `form:"parent_id,omitempty" json:"parent_id,omitempty" xml:"parent_id,omitempty"` + // The scope of the task (database or host). + Scope *string `form:"scope,omitempty" json:"scope,omitempty" xml:"scope,omitempty"` + // The entity ID (database_id or host_id) that this task belongs to. + EntityID *string `form:"entity_id,omitempty" json:"entity_id,omitempty" xml:"entity_id,omitempty"` // The database ID of the task. DatabaseID *string `form:"database_id,omitempty" json:"database_id,omitempty" xml:"database_id,omitempty"` // The name of the node that the task is operating on. @@ -288,7 +292,11 @@ type GetDatabaseTaskResponseBody struct { // GetDatabaseTaskLogResponseBody is the type of the "control-plane" service // "get-database-task-log" endpoint HTTP response body. type GetDatabaseTaskLogResponseBody struct { - // The database ID of the task log. + // The scope of the task (database or host). + Scope *string `form:"scope,omitempty" json:"scope,omitempty" xml:"scope,omitempty"` + // The entity ID (database_id or host_id) that this task log belongs to. + EntityID *string `form:"entity_id,omitempty" json:"entity_id,omitempty" xml:"entity_id,omitempty"` + // The database ID of the task log. Deprecated: use entity_id instead. DatabaseID *string `form:"database_id,omitempty" json:"database_id,omitempty" xml:"database_id,omitempty"` // The unique ID of the task log. TaskID *string `form:"task_id,omitempty" json:"task_id,omitempty" xml:"task_id,omitempty"` @@ -350,6 +358,10 @@ type StartInstanceResponseBody struct { type CancelDatabaseTaskResponseBody struct { // The parent task ID of the task. ParentID *string `form:"parent_id,omitempty" json:"parent_id,omitempty" xml:"parent_id,omitempty"` + // The scope of the task (database or host). + Scope *string `form:"scope,omitempty" json:"scope,omitempty" xml:"scope,omitempty"` + // The entity ID (database_id or host_id) that this task belongs to. + EntityID *string `form:"entity_id,omitempty" json:"entity_id,omitempty" xml:"entity_id,omitempty"` // The database ID of the task. DatabaseID *string `form:"database_id,omitempty" json:"database_id,omitempty" xml:"database_id,omitempty"` // The name of the node that the task is operating on. @@ -1466,6 +1478,10 @@ type PgEdgeVersionResponseBody struct { type TaskResponseBody struct { // The parent task ID of the task. ParentID *string `form:"parent_id,omitempty" json:"parent_id,omitempty" xml:"parent_id,omitempty"` + // The scope of the task (database or host). + Scope *string `form:"scope,omitempty" json:"scope,omitempty" xml:"scope,omitempty"` + // The entity ID (database_id or host_id) that this task belongs to. + EntityID *string `form:"entity_id,omitempty" json:"entity_id,omitempty" xml:"entity_id,omitempty"` // The database ID of the task. DatabaseID *string `form:"database_id,omitempty" json:"database_id,omitempty" xml:"database_id,omitempty"` // The name of the node that the task is operating on. @@ -3547,7 +3563,9 @@ func NewListDatabaseTasksServerError(body *ListDatabaseTasksServerErrorResponseB func NewGetDatabaseTaskTaskOK(body *GetDatabaseTaskResponseBody) *controlplane.Task { v := &controlplane.Task{ ParentID: body.ParentID, - DatabaseID: *body.DatabaseID, + Scope: *body.Scope, + EntityID: *body.EntityID, + DatabaseID: body.DatabaseID, NodeName: body.NodeName, InstanceID: body.InstanceID, HostID: body.HostID, @@ -3610,7 +3628,9 @@ func NewGetDatabaseTaskServerError(body *GetDatabaseTaskServerErrorResponseBody) // "get-database-task-log" endpoint result from a HTTP "OK" response. func NewGetDatabaseTaskLogTaskLogOK(body *GetDatabaseTaskLogResponseBody) *controlplane.TaskLog { v := &controlplane.TaskLog{ - DatabaseID: *body.DatabaseID, + Scope: *body.Scope, + EntityID: *body.EntityID, + DatabaseID: body.DatabaseID, TaskID: *body.TaskID, TaskStatus: *body.TaskStatus, LastEntryID: body.LastEntryID, @@ -3943,7 +3963,9 @@ func NewStartInstanceServerError(body *StartInstanceServerErrorResponseBody) *co func NewCancelDatabaseTaskTaskOK(body *CancelDatabaseTaskResponseBody) *controlplane.Task { v := &controlplane.Task{ ParentID: body.ParentID, - DatabaseID: *body.DatabaseID, + Scope: *body.Scope, + EntityID: *body.EntityID, + DatabaseID: body.DatabaseID, NodeName: body.NodeName, InstanceID: body.InstanceID, HostID: body.HostID, diff --git a/api/apiv1/gen/http/control_plane/server/encode_decode.go b/api/apiv1/gen/http/control_plane/server/encode_decode.go index c61f7c67..f1984f0f 100644 --- a/api/apiv1/gen/http/control_plane/server/encode_decode.go +++ b/api/apiv1/gen/http/control_plane/server/encode_decode.go @@ -3011,6 +3011,8 @@ func marshalControlplanePgEdgeVersionToPgEdgeVersionResponseBody(v *controlplane func marshalControlplaneTaskToTaskResponseBody(v *controlplane.Task) *TaskResponseBody { res := &TaskResponseBody{ ParentID: v.ParentID, + Scope: v.Scope, + EntityID: v.EntityID, DatabaseID: v.DatabaseID, NodeName: v.NodeName, InstanceID: v.InstanceID, diff --git a/api/apiv1/gen/http/control_plane/server/types.go b/api/apiv1/gen/http/control_plane/server/types.go index 2658a98f..9a855ff2 100644 --- a/api/apiv1/gen/http/control_plane/server/types.go +++ b/api/apiv1/gen/http/control_plane/server/types.go @@ -263,8 +263,12 @@ type ListDatabaseTasksResponseBody struct { type GetDatabaseTaskResponseBody struct { // The parent task ID of the task. ParentID *string `form:"parent_id,omitempty" json:"parent_id,omitempty" xml:"parent_id,omitempty"` + // The scope of the task (database or host). + Scope string `form:"scope" json:"scope" xml:"scope"` + // The entity ID (database_id or host_id) that this task belongs to. + EntityID string `form:"entity_id" json:"entity_id" xml:"entity_id"` // The database ID of the task. - DatabaseID string `form:"database_id" json:"database_id" xml:"database_id"` + DatabaseID *string `form:"database_id,omitempty" json:"database_id,omitempty" xml:"database_id,omitempty"` // The name of the node that the task is operating on. NodeName *string `form:"node_name,omitempty" json:"node_name,omitempty" xml:"node_name,omitempty"` // The ID of the instance that the task is operating on. @@ -288,8 +292,12 @@ type GetDatabaseTaskResponseBody struct { // GetDatabaseTaskLogResponseBody is the type of the "control-plane" service // "get-database-task-log" endpoint HTTP response body. type GetDatabaseTaskLogResponseBody struct { - // The database ID of the task log. - DatabaseID string `form:"database_id" json:"database_id" xml:"database_id"` + // The scope of the task (database or host). + Scope string `form:"scope" json:"scope" xml:"scope"` + // The entity ID (database_id or host_id) that this task log belongs to. + EntityID string `form:"entity_id" json:"entity_id" xml:"entity_id"` + // The database ID of the task log. Deprecated: use entity_id instead. + DatabaseID *string `form:"database_id,omitempty" json:"database_id,omitempty" xml:"database_id,omitempty"` // The unique ID of the task log. TaskID string `form:"task_id" json:"task_id" xml:"task_id"` // The status of the task. @@ -350,8 +358,12 @@ type StartInstanceResponseBody struct { type CancelDatabaseTaskResponseBody struct { // The parent task ID of the task. ParentID *string `form:"parent_id,omitempty" json:"parent_id,omitempty" xml:"parent_id,omitempty"` + // The scope of the task (database or host). + Scope string `form:"scope" json:"scope" xml:"scope"` + // The entity ID (database_id or host_id) that this task belongs to. + EntityID string `form:"entity_id" json:"entity_id" xml:"entity_id"` // The database ID of the task. - DatabaseID string `form:"database_id" json:"database_id" xml:"database_id"` + DatabaseID *string `form:"database_id,omitempty" json:"database_id,omitempty" xml:"database_id,omitempty"` // The name of the node that the task is operating on. NodeName *string `form:"node_name,omitempty" json:"node_name,omitempty" xml:"node_name,omitempty"` // The ID of the instance that the task is operating on. @@ -1466,8 +1478,12 @@ type PgEdgeVersionResponseBody struct { type TaskResponseBody struct { // The parent task ID of the task. ParentID *string `form:"parent_id,omitempty" json:"parent_id,omitempty" xml:"parent_id,omitempty"` + // The scope of the task (database or host). + Scope string `form:"scope" json:"scope" xml:"scope"` + // The entity ID (database_id or host_id) that this task belongs to. + EntityID string `form:"entity_id" json:"entity_id" xml:"entity_id"` // The database ID of the task. - DatabaseID string `form:"database_id" json:"database_id" xml:"database_id"` + DatabaseID *string `form:"database_id,omitempty" json:"database_id,omitempty" xml:"database_id,omitempty"` // The name of the node that the task is operating on. NodeName *string `form:"node_name,omitempty" json:"node_name,omitempty" xml:"node_name,omitempty"` // The ID of the instance that the task is operating on. @@ -2705,6 +2721,8 @@ func NewListDatabaseTasksResponseBody(res *controlplane.ListDatabaseTasksRespons func NewGetDatabaseTaskResponseBody(res *controlplane.Task) *GetDatabaseTaskResponseBody { body := &GetDatabaseTaskResponseBody{ ParentID: res.ParentID, + Scope: res.Scope, + EntityID: res.EntityID, DatabaseID: res.DatabaseID, NodeName: res.NodeName, InstanceID: res.InstanceID, @@ -2724,6 +2742,8 @@ func NewGetDatabaseTaskResponseBody(res *controlplane.Task) *GetDatabaseTaskResp // service. func NewGetDatabaseTaskLogResponseBody(res *controlplane.TaskLog) *GetDatabaseTaskLogResponseBody { body := &GetDatabaseTaskLogResponseBody{ + Scope: res.Scope, + EntityID: res.EntityID, DatabaseID: res.DatabaseID, TaskID: res.TaskID, TaskStatus: res.TaskStatus, @@ -2816,6 +2836,8 @@ func NewStartInstanceResponseBody(res *controlplane.StartInstanceResponse) *Star func NewCancelDatabaseTaskResponseBody(res *controlplane.Task) *CancelDatabaseTaskResponseBody { body := &CancelDatabaseTaskResponseBody{ ParentID: res.ParentID, + Scope: res.Scope, + EntityID: res.EntityID, DatabaseID: res.DatabaseID, NodeName: res.NodeName, InstanceID: res.InstanceID, diff --git a/api/apiv1/gen/http/openapi.json b/api/apiv1/gen/http/openapi.json index 98c24e4e..eac5d5b8 100644 --- a/api/apiv1/gen/http/openapi.json +++ b/api/apiv1/gen/http/openapi.json @@ -1357,7 +1357,8 @@ "schema": { "$ref": "#/definitions/Task", "required": [ - "database_id", + "scope", + "entity_id", "task_id", "created_at", "type", @@ -1441,7 +1442,8 @@ "schema": { "$ref": "#/definitions/Task", "required": [ - "database_id", + "scope", + "entity_id", "task_id", "created_at", "type", @@ -1531,7 +1533,8 @@ "schema": { "$ref": "#/definitions/TaskLog", "required": [ - "database_id", + "scope", + "entity_id", "task_id", "task_status", "entries" @@ -5996,6 +5999,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -6004,6 +6009,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -6017,7 +6024,9 @@ "completed_at": "2025-06-18T17:54:36Z", "created_at": "2025-06-18T17:54:28Z", "database_id": "storefront", + "entity_id": "storefront", "instance_id": "storefront-n1-689qacsi", + "scope": "database", "status": "completed", "task_id": "0197842d-9082-7496-b787-77bd2e11809f", "type": "node_backup" @@ -6026,6 +6035,8 @@ "completed_at": "2025-06-18T17:54:04Z", "created_at": "2025-06-18T17:53:17Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "0197842c-7c4f-7a8c-829e-7405c2a41c8c", "type": "update" @@ -6034,6 +6045,8 @@ "completed_at": "2025-06-18T17:23:28Z", "created_at": "2025-06-18T17:23:14Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "01978410-fb5d-7cd2-bbd2-66c0bf929dc0", "type": "update" @@ -6042,6 +6055,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -6479,6 +6494,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -6487,6 +6504,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -6495,6 +6514,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -6508,6 +6529,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -6516,6 +6539,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -6684,6 +6709,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -6692,6 +6719,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -7261,6 +7290,11 @@ "description": "The database ID of the task.", "example": "02f1a7db-fca8-4521-b57a-2a375c1ced51" }, + "entity_id": { + "type": "string", + "description": "The entity ID (database_id or host_id) that this task belongs to.", + "example": "02f1a7db-fca8-4521-b57a-2a375c1ced51" + }, "error": { "type": "string", "description": "The error message if the task failed.", @@ -7287,6 +7321,15 @@ "example": "439eb515-e700-4740-b508-4a3f12ec4f83", "format": "uuid" }, + "scope": { + "type": "string", + "description": "The scope of the task (database or host).", + "example": "database", + "enum": [ + "database", + "host" + ] + }, "status": { "type": "string", "description": "The status of the task.", @@ -7317,12 +7360,15 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" }, "required": [ - "database_id", + "scope", + "entity_id", "task_id", "created_at", "type", @@ -7335,7 +7381,12 @@ "properties": { "database_id": { "type": "string", - "description": "The database ID of the task log.", + "description": "The database ID of the task log. Deprecated: use entity_id instead.", + "example": "02f1a7db-fca8-4521-b57a-2a375c1ced51" + }, + "entity_id": { + "type": "string", + "description": "The entity ID (database_id or host_id) that this task log belongs to.", "example": "02f1a7db-fca8-4521-b57a-2a375c1ced51" }, "entries": { @@ -7368,6 +7419,15 @@ "description": "The ID of the last entry in the task log.", "example": "3c875a27-f6a6-4c1c-ba5f-6972fb1fc348" }, + "scope": { + "type": "string", + "description": "The scope of the task (database or host).", + "example": "database", + "enum": [ + "database", + "host" + ] + }, "task_id": { "type": "string", "description": "The unique ID of the task log.", @@ -7389,7 +7449,7 @@ } }, "example": { - "database_id": "storefront", + "entity_id": "storefront", "entries": [ { "message": "refreshing current state", @@ -7444,11 +7504,13 @@ } ], "last_entry_id": "0197842d-303b-7251-b814-6d12c98e7d25", + "scope": "database", "task_id": "0197842c-7c4f-7a8c-829e-7405c2a41c8c", "task_status": "completed" }, "required": [ - "database_id", + "scope", + "entity_id", "task_id", "task_status", "entries" diff --git a/api/apiv1/gen/http/openapi.yaml b/api/apiv1/gen/http/openapi.yaml index c44b037b..54fafdca 100644 --- a/api/apiv1/gen/http/openapi.yaml +++ b/api/apiv1/gen/http/openapi.yaml @@ -942,7 +942,8 @@ paths: schema: $ref: '#/definitions/Task' required: - - database_id + - scope + - entity_id - task_id - created_at - type @@ -1001,7 +1002,8 @@ paths: schema: $ref: '#/definitions/Task' required: - - database_id + - scope + - entity_id - task_id - created_at - type @@ -1065,7 +1067,8 @@ paths: schema: $ref: '#/definitions/TaskLog' required: - - database_id + - scope + - entity_id - task_id - task_status - entries @@ -4328,12 +4331,16 @@ definitions: - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -4342,25 +4349,33 @@ definitions: - completed_at: "2025-06-18T17:54:36Z" created_at: "2025-06-18T17:54:28Z" database_id: storefront + entity_id: storefront instance_id: storefront-n1-689qacsi + scope: database status: completed task_id: 0197842d-9082-7496-b787-77bd2e11809f type: node_backup - completed_at: "2025-06-18T17:54:04Z" created_at: "2025-06-18T17:53:17Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 0197842c-7c4f-7a8c-829e-7405c2a41c8c type: update - completed_at: "2025-06-18T17:23:28Z" created_at: "2025-06-18T17:23:14Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 01978410-fb5d-7cd2-bbd2-66c0bf929dc0 type: update - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -4651,18 +4666,24 @@ definitions: - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -4671,12 +4692,16 @@ definitions: - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -4806,12 +4831,16 @@ definitions: - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -5222,6 +5251,10 @@ definitions: type: string description: The database ID of the task. example: 02f1a7db-fca8-4521-b57a-2a375c1ced51 + entity_id: + type: string + description: The entity ID (database_id or host_id) that this task belongs to. + example: 02f1a7db-fca8-4521-b57a-2a375c1ced51 error: type: string description: The error message if the task failed. @@ -5243,6 +5276,13 @@ definitions: description: The parent task ID of the task. example: 439eb515-e700-4740-b508-4a3f12ec4f83 format: uuid + scope: + type: string + description: The scope of the task (database or host). + example: database + enum: + - database + - host status: type: string description: The status of the task. @@ -5268,11 +5308,14 @@ definitions: completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create required: - - database_id + - scope + - entity_id - task_id - created_at - type @@ -5283,7 +5326,11 @@ definitions: properties: database_id: type: string - description: The database ID of the task log. + description: 'The database ID of the task log. Deprecated: use entity_id instead.' + example: 02f1a7db-fca8-4521-b57a-2a375c1ced51 + entity_id: + type: string + description: The entity ID (database_id or host_id) that this task log belongs to. example: 02f1a7db-fca8-4521-b57a-2a375c1ced51 entries: type: array @@ -5305,6 +5352,13 @@ definitions: type: string description: The ID of the last entry in the task log. example: 3c875a27-f6a6-4c1c-ba5f-6972fb1fc348 + scope: + type: string + description: The scope of the task (database or host). + example: database + enum: + - database + - host task_id: type: string description: The unique ID of the task log. @@ -5322,7 +5376,7 @@ definitions: - canceled - canceling example: - database_id: storefront + entity_id: storefront entries: - message: refreshing current state timestamp: "2025-06-18T17:53:19Z" @@ -5359,10 +5413,12 @@ definitions: message: finished creating resource swarm.pgbackrest_stanza::n1 (took 1.181454868s) timestamp: "2025-06-18T17:54:03Z" last_entry_id: 0197842d-303b-7251-b814-6d12c98e7d25 + scope: database task_id: 0197842c-7c4f-7a8c-829e-7405c2a41c8c task_status: completed required: - - database_id + - scope + - entity_id - task_id - task_status - entries diff --git a/api/apiv1/gen/http/openapi3.json b/api/apiv1/gen/http/openapi3.json index 2cf537d6..e419efca 100644 --- a/api/apiv1/gen/http/openapi3.json +++ b/api/apiv1/gen/http/openapi3.json @@ -3039,7 +3039,9 @@ "completed_at": "2025-06-18T17:54:36Z", "created_at": "2025-06-18T17:54:28Z", "database_id": "storefront", + "entity_id": "storefront", "instance_id": "storefront-n1-689qacsi", + "scope": "database", "status": "completed", "task_id": "0197842d-9082-7496-b787-77bd2e11809f", "type": "node_backup" @@ -3048,6 +3050,8 @@ "completed_at": "2025-06-18T17:54:04Z", "created_at": "2025-06-18T17:53:17Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "0197842c-7c4f-7a8c-829e-7405c2a41c8c", "type": "update" @@ -3056,6 +3060,8 @@ "completed_at": "2025-06-18T17:23:28Z", "created_at": "2025-06-18T17:23:14Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "01978410-fb5d-7cd2-bbd2-66c0bf929dc0", "type": "update" @@ -3064,6 +3070,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -3181,6 +3189,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -3297,6 +3307,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -3425,7 +3437,7 @@ "summary": "node_backup task log", "description": "The task log from a 'node_backup' task. These messages are produced by pgbackrest.", "value": { - "database_id": "storefront", + "entity_id": "storefront", "entries": [ { "message": "P00 INFO: backup command begin 2.55.1: --config=/opt/pgedge/configs/pgbackrest.backup.conf --exec-id=198-b17fae6e --log-level-console=info --no-log-timestamp --pg1-path=/opt/pgedge/data/pgdata --pg1-user=pgedge --repo1-cipher-type=none --repo1-path=/backups/databases/storefront/n1 --repo1-retention-full=7 --repo1-retention-full-type=time --repo1-type=posix --stanza=db --start-fast --type=full", @@ -3481,6 +3493,7 @@ } ], "last_entry_id": "0197842d-b14d-7c69-86c1-c006a7c65318", + "scope": "database", "task_id": "0197842d-9082-7496-b787-77bd2e11809f", "task_status": "completed" } @@ -3489,7 +3502,7 @@ "summary": "update task log", "description": "This is the task log of an update task. This example excludes many entries for brevity.", "value": { - "database_id": "storefront", + "entity_id": "storefront", "entries": [ { "message": "refreshing current state", @@ -3544,6 +3557,7 @@ } ], "last_entry_id": "0197842d-303b-7251-b814-6d12c98e7d25", + "scope": "database", "task_id": "0197842c-7c4f-7a8c-829e-7405c2a41c8c", "task_status": "completed" } @@ -3838,6 +3852,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -3846,6 +3862,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -3854,6 +3872,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -3862,6 +3882,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -13554,6 +13576,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -13562,6 +13586,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -13575,7 +13601,9 @@ "completed_at": "2025-06-18T17:54:36Z", "created_at": "2025-06-18T17:54:28Z", "database_id": "storefront", + "entity_id": "storefront", "instance_id": "storefront-n1-689qacsi", + "scope": "database", "status": "completed", "task_id": "0197842d-9082-7496-b787-77bd2e11809f", "type": "node_backup" @@ -13584,6 +13612,8 @@ "completed_at": "2025-06-18T17:54:04Z", "created_at": "2025-06-18T17:53:17Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "0197842c-7c4f-7a8c-829e-7405c2a41c8c", "type": "update" @@ -13592,6 +13622,8 @@ "completed_at": "2025-06-18T17:23:28Z", "created_at": "2025-06-18T17:23:14Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "01978410-fb5d-7cd2-bbd2-66c0bf929dc0", "type": "update" @@ -13600,6 +13632,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14078,6 +14112,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14086,6 +14122,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14094,6 +14132,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14107,6 +14147,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14115,6 +14157,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14123,6 +14167,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14146,6 +14192,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14287,6 +14335,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14295,6 +14345,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14303,6 +14355,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14311,6 +14365,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14671,6 +14727,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14693,6 +14751,8 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" @@ -14933,6 +14993,11 @@ "description": "The database ID of the task.", "example": "02f1a7db-fca8-4521-b57a-2a375c1ced51" }, + "entity_id": { + "type": "string", + "description": "The entity ID (database_id or host_id) that this task belongs to.", + "example": "02f1a7db-fca8-4521-b57a-2a375c1ced51" + }, "error": { "type": "string", "description": "The error message if the task failed.", @@ -14959,6 +15024,15 @@ "example": "439eb515-e700-4740-b508-4a3f12ec4f83", "format": "uuid" }, + "scope": { + "type": "string", + "description": "The scope of the task (database or host).", + "example": "database", + "enum": [ + "database", + "host" + ] + }, "status": { "type": "string", "description": "The status of the task.", @@ -14989,12 +15063,15 @@ "completed_at": "2025-06-18T16:52:35Z", "created_at": "2025-06-18T16:52:05Z", "database_id": "storefront", + "entity_id": "storefront", + "scope": "database", "status": "completed", "task_id": "019783f4-75f4-71e7-85a3-c9b96b345d77", "type": "create" }, "required": [ - "database_id", + "scope", + "entity_id", "task_id", "created_at", "type", @@ -15006,7 +15083,12 @@ "properties": { "database_id": { "type": "string", - "description": "The database ID of the task log.", + "description": "The database ID of the task log. Deprecated: use entity_id instead.", + "example": "02f1a7db-fca8-4521-b57a-2a375c1ced51" + }, + "entity_id": { + "type": "string", + "description": "The entity ID (database_id or host_id) that this task log belongs to.", "example": "02f1a7db-fca8-4521-b57a-2a375c1ced51" }, "entries": { @@ -15039,6 +15121,15 @@ "description": "The ID of the last entry in the task log.", "example": "3c875a27-f6a6-4c1c-ba5f-6972fb1fc348" }, + "scope": { + "type": "string", + "description": "The scope of the task (database or host).", + "example": "database", + "enum": [ + "database", + "host" + ] + }, "task_id": { "type": "string", "description": "The unique ID of the task log.", @@ -15060,7 +15151,7 @@ } }, "example": { - "database_id": "storefront", + "entity_id": "storefront", "entries": [ { "message": "refreshing current state", @@ -15115,11 +15206,13 @@ } ], "last_entry_id": "0197842d-303b-7251-b814-6d12c98e7d25", + "scope": "database", "task_id": "0197842c-7c4f-7a8c-829e-7405c2a41c8c", "task_status": "completed" }, "required": [ - "database_id", + "scope", + "entity_id", "task_id", "task_status", "entries" diff --git a/api/apiv1/gen/http/openapi3.yaml b/api/apiv1/gen/http/openapi3.yaml index 8635ee4d..6ead783a 100644 --- a/api/apiv1/gen/http/openapi3.yaml +++ b/api/apiv1/gen/http/openapi3.yaml @@ -2018,25 +2018,33 @@ paths: - completed_at: "2025-06-18T17:54:36Z" created_at: "2025-06-18T17:54:28Z" database_id: storefront + entity_id: storefront instance_id: storefront-n1-689qacsi + scope: database status: completed task_id: 0197842d-9082-7496-b787-77bd2e11809f type: node_backup - completed_at: "2025-06-18T17:54:04Z" created_at: "2025-06-18T17:53:17Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 0197842c-7c4f-7a8c-829e-7405c2a41c8c type: update - completed_at: "2025-06-18T17:23:28Z" created_at: "2025-06-18T17:23:14Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 01978410-fb5d-7cd2-bbd2-66c0bf929dc0 type: update - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -2116,6 +2124,8 @@ paths: completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -2196,6 +2206,8 @@ paths: completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -2287,7 +2299,7 @@ paths: summary: node_backup task log description: The task log from a 'node_backup' task. These messages are produced by pgbackrest. value: - database_id: storefront + entity_id: storefront entries: - message: 'P00 INFO: backup command begin 2.55.1: --config=/opt/pgedge/configs/pgbackrest.backup.conf --exec-id=198-b17fae6e --log-level-console=info --no-log-timestamp --pg1-path=/opt/pgedge/data/pgdata --pg1-user=pgedge --repo1-cipher-type=none --repo1-path=/backups/databases/storefront/n1 --repo1-retention-full=7 --repo1-retention-full-type=time --repo1-type=posix --stanza=db --start-fast --type=full' timestamp: "2025-06-18T17:54:34Z" @@ -2316,13 +2328,14 @@ paths: - message: 'P00 INFO: expire command end: completed successfully' timestamp: "2025-06-18T17:54:36Z" last_entry_id: 0197842d-b14d-7c69-86c1-c006a7c65318 + scope: database task_id: 0197842d-9082-7496-b787-77bd2e11809f task_status: completed update task log: summary: update task log description: This is the task log of an update task. This example excludes many entries for brevity. value: - database_id: storefront + entity_id: storefront entries: - message: refreshing current state timestamp: "2025-06-18T17:53:19Z" @@ -2359,6 +2372,7 @@ paths: message: finished creating resource swarm.pgbackrest_stanza::n1 (took 1.181454868s) timestamp: "2025-06-18T17:54:03Z" last_entry_id: 0197842d-303b-7251-b814-6d12c98e7d25 + scope: database task_id: 0197842c-7c4f-7a8c-829e-7405c2a41c8c task_status: completed "400": @@ -2556,24 +2570,32 @@ paths: - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -9603,12 +9625,16 @@ components: - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -9617,25 +9643,33 @@ components: - completed_at: "2025-06-18T17:54:36Z" created_at: "2025-06-18T17:54:28Z" database_id: storefront + entity_id: storefront instance_id: storefront-n1-689qacsi + scope: database status: completed task_id: 0197842d-9082-7496-b787-77bd2e11809f type: node_backup - completed_at: "2025-06-18T17:54:04Z" created_at: "2025-06-18T17:53:17Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 0197842c-7c4f-7a8c-829e-7405c2a41c8c type: update - completed_at: "2025-06-18T17:23:28Z" created_at: "2025-06-18T17:23:14Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 01978410-fb5d-7cd2-bbd2-66c0bf929dc0 type: update - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -9950,18 +9984,24 @@ components: - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -9970,18 +10010,24 @@ components: - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -9998,6 +10044,8 @@ components: completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -10107,24 +10155,32 @@ components: - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create - completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -10396,6 +10452,8 @@ components: completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -10412,6 +10470,8 @@ components: completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create @@ -10575,6 +10635,10 @@ components: type: string description: The database ID of the task. example: 02f1a7db-fca8-4521-b57a-2a375c1ced51 + entity_id: + type: string + description: The entity ID (database_id or host_id) that this task belongs to. + example: 02f1a7db-fca8-4521-b57a-2a375c1ced51 error: type: string description: The error message if the task failed. @@ -10596,6 +10660,13 @@ components: description: The parent task ID of the task. example: 439eb515-e700-4740-b508-4a3f12ec4f83 format: uuid + scope: + type: string + description: The scope of the task (database or host). + example: database + enum: + - database + - host status: type: string description: The status of the task. @@ -10621,11 +10692,14 @@ components: completed_at: "2025-06-18T16:52:35Z" created_at: "2025-06-18T16:52:05Z" database_id: storefront + entity_id: storefront + scope: database status: completed task_id: 019783f4-75f4-71e7-85a3-c9b96b345d77 type: create required: - - database_id + - scope + - entity_id - task_id - created_at - type @@ -10635,7 +10709,11 @@ components: properties: database_id: type: string - description: The database ID of the task log. + description: 'The database ID of the task log. Deprecated: use entity_id instead.' + example: 02f1a7db-fca8-4521-b57a-2a375c1ced51 + entity_id: + type: string + description: The entity ID (database_id or host_id) that this task log belongs to. example: 02f1a7db-fca8-4521-b57a-2a375c1ced51 entries: type: array @@ -10657,6 +10735,13 @@ components: type: string description: The ID of the last entry in the task log. example: 3c875a27-f6a6-4c1c-ba5f-6972fb1fc348 + scope: + type: string + description: The scope of the task (database or host). + example: database + enum: + - database + - host task_id: type: string description: The unique ID of the task log. @@ -10674,7 +10759,7 @@ components: - canceled - canceling example: - database_id: storefront + entity_id: storefront entries: - message: refreshing current state timestamp: "2025-06-18T17:53:19Z" @@ -10711,10 +10796,12 @@ components: message: finished creating resource swarm.pgbackrest_stanza::n1 (took 1.181454868s) timestamp: "2025-06-18T17:54:03Z" last_entry_id: 0197842d-303b-7251-b814-6d12c98e7d25 + scope: database task_id: 0197842c-7c4f-7a8c-829e-7405c2a41c8c task_status: completed required: - - database_id + - scope + - entity_id - task_id - task_status - entries diff --git a/changes/unreleased/Added-20260114-173755.yaml b/changes/unreleased/Added-20260114-173755.yaml new file mode 100644 index 00000000..f1ee962f --- /dev/null +++ b/changes/unreleased/Added-20260114-173755.yaml @@ -0,0 +1,3 @@ +kind: Added +body: Added support for non-database tasks with new `scope` and `entity_id` fields on tasks and task logs. +time: 2026-01-14T17:37:55.817598-05:00 diff --git a/server/internal/api/apiv1/convert.go b/server/internal/api/apiv1/convert.go index 5e415290..a3158132 100644 --- a/server/internal/api/apiv1/convert.go +++ b/server/internal/api/apiv1/convert.go @@ -537,7 +537,9 @@ func taskToAPI(t *task.Task) *api.Task { } return &api.Task{ ParentID: parentID, - DatabaseID: t.DatabaseID, + Scope: t.Scope.String(), + EntityID: t.EntityID, + DatabaseID: utils.NillablePointerTo(t.DatabaseID), TaskID: t.TaskID.String(), NodeName: utils.NillablePointerTo(t.NodeName), HostID: utils.NillablePointerTo(t.HostID), @@ -564,7 +566,9 @@ func taskLogToAPI(t *task.TaskLog, status task.Status) *api.TaskLog { lastEntryID = utils.PointerTo(t.LastEntryID.String()) } return &api.TaskLog{ - DatabaseID: t.DatabaseID, + Scope: t.Scope.String(), + EntityID: t.EntityID, + DatabaseID: utils.NillablePointerTo(t.DatabaseID), TaskID: t.TaskID.String(), TaskStatus: string(status), LastEntryID: lastEntryID, diff --git a/server/internal/api/apiv1/post_init_handlers.go b/server/internal/api/apiv1/post_init_handlers.go index c7a346a9..a6a40f68 100644 --- a/server/internal/api/apiv1/post_init_handlers.go +++ b/server/internal/api/apiv1/post_init_handlers.go @@ -604,7 +604,7 @@ func (s *PostInitHandlers) SwitchoverDatabaseNode(ctx context.Context, req *api. Limit: 1, } - activeTasks, err := s.taskSvc.GetTasks(ctx, databaseID, opts) + activeTasks, err := s.taskSvc.GetTasks(ctx, task.ScopeDatabase, databaseID, opts) if err != nil { return nil, apiErr(err) } @@ -697,7 +697,7 @@ func (s *PostInitHandlers) FailoverDatabaseNode(ctx context.Context, req *api.Fa Limit: 1, } - activeTasks, err := s.taskSvc.GetTasks(ctx, databaseID, opts) + activeTasks, err := s.taskSvc.GetTasks(ctx, task.ScopeDatabase, databaseID, opts) if err != nil { return nil, apiErr(err) } @@ -728,7 +728,7 @@ func (s *PostInitHandlers) ListDatabaseTasks(ctx context.Context, req *api.ListD return nil, makeInvalidInputErr(err) } - tasks, err := s.taskSvc.GetTasks(ctx, databaseID, options) + tasks, err := s.taskSvc.GetTasks(ctx, task.ScopeDatabase, databaseID, options) if err != nil { return nil, apiErr(err) } @@ -748,7 +748,7 @@ func (s *PostInitHandlers) GetDatabaseTask(ctx context.Context, req *api.GetData return nil, ErrInvalidTaskID } - t, err := s.taskSvc.GetTask(ctx, databaseID, taskID) + t, err := s.taskSvc.GetTask(ctx, task.ScopeDatabase, databaseID, taskID) if err != nil { return nil, apiErr(err) } @@ -766,7 +766,7 @@ func (s *PostInitHandlers) GetDatabaseTaskLog(ctx context.Context, req *api.GetD return nil, ErrInvalidTaskID } - t, err := s.taskSvc.GetTask(ctx, databaseID, taskID) + t, err := s.taskSvc.GetTask(ctx, task.ScopeDatabase, databaseID, taskID) if err != nil { return nil, apiErr(err) } @@ -776,7 +776,7 @@ func (s *PostInitHandlers) GetDatabaseTaskLog(ctx context.Context, req *api.GetD return nil, makeInvalidInputErr(err) } - log, err := s.taskSvc.GetTaskLog(ctx, databaseID, taskID, options) + log, err := s.taskSvc.GetTaskLog(ctx, task.ScopeDatabase, databaseID, taskID, options) if err != nil { return nil, apiErr(err) } @@ -1030,7 +1030,7 @@ func (s *PostInitHandlers) CancelDatabaseTask(ctx context.Context, req *api.Canc return nil, makeInvalidInputErr(fmt.Errorf("invalid task ID: %w", err)) } - t, err := s.taskSvc.GetTask(ctx, databaseID, taskID) + t, err := s.taskSvc.GetTask(ctx, task.ScopeDatabase, databaseID, taskID) if err != nil { return nil, makeInvalidInputErr(fmt.Errorf("task is not associated with database ")) } diff --git a/server/internal/migrate/all_migrations.go b/server/internal/migrate/all_migrations.go index 2a5222be..11c5d701 100644 --- a/server/internal/migrate/all_migrations.go +++ b/server/internal/migrate/all_migrations.go @@ -1,13 +1,12 @@ package migrate +import "github.com/pgEdge/control-plane/server/internal/migrate/migrations" + // allMigrations returns the ordered list of migrations. // Order matters - migrations are executed in slice order. // Add new migrations to this list in chronological order. func allMigrations() []Migration { return []Migration{ - // Add migrations here in chronological order - // Example: - // &AddHostMetadataField{}, - // &RenameDatabaseStatus{}, + &migrations.AddTaskScope{}, } } diff --git a/server/internal/migrate/migrations/add_task_scope.go b/server/internal/migrate/migrations/add_task_scope.go new file mode 100644 index 00000000..e4ff9dd9 --- /dev/null +++ b/server/internal/migrate/migrations/add_task_scope.go @@ -0,0 +1,145 @@ +package migrations + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/pgEdge/control-plane/server/internal/config" + "github.com/pgEdge/control-plane/server/internal/storage" + "github.com/pgEdge/control-plane/server/internal/task" + "github.com/rs/zerolog" + "github.com/samber/do" + clientv3 "go.etcd.io/etcd/client/v3" +) + +type AddTaskScope struct{} + +func (a *AddTaskScope) Identifier() string { + return "add_task_scope" +} + +func (a *AddTaskScope) Run(ctx context.Context, i *do.Injector) error { + cfg, err := do.Invoke[config.Config](i) + if err != nil { + return fmt.Errorf("failed to initialize config: %w", err) + } + logger, err := do.Invoke[zerolog.Logger](i) + if err != nil { + return fmt.Errorf("failed to initialize logger: %w", err) + } + client, err := do.Invoke[*clientv3.Client](i) + if err != nil { + return fmt.Errorf("failed to initialize client: %w", err) + } + taskStore, err := do.Invoke[*task.Store](i) + if err != nil { + return fmt.Errorf("failed to initialize task store: %w", err) + } + + logger = logger.With(). + Str("component", "migration"). + Str("identifier", a.Identifier()). + Logger() + + oldTasksPrefix := storage.Prefix("/", cfg.EtcdKeyRoot, "tasks") + oldTaskRangeOp := storage.NewGetPrefixOp[*oldStoredTask](client, oldTasksPrefix) + oldTasks, err := oldTaskRangeOp.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to query for old tasks: %w", err) + } + + for _, oldTask := range oldTasks { + err := taskStore.Task.Create(oldTask.convert()).Exec(ctx) + switch { + case errors.Is(err, storage.ErrAlreadyExists): + logger.Info(). + Stringer("task_id", oldTask.Task.TaskID). + Msg("task has already been migrated, skipping") + case err != nil: + return fmt.Errorf("failed to migrate task %s: %w", oldTask.Task.TaskID, err) + } + } + + oldTaskLogsPrefix := storage.Prefix("/", cfg.EtcdKeyRoot, "task_log_messages") + oldTaskLogsRangeOp := storage.NewGetPrefixOp[*oldStoredTaskLogEntry](client, oldTaskLogsPrefix) + oldTaskLogs, err := oldTaskLogsRangeOp.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to query for old task logs: %w", err) + } + + for _, oldTaskLog := range oldTaskLogs { + err := taskStore.TaskLogMessage.Put(oldTaskLog.convert()).Exec(ctx) + if err != nil { + return fmt.Errorf("failed to migrate task log entry %s for task %s: %w", oldTaskLog.EntryID, oldTaskLog.TaskID, err) + } + } + + return nil +} + +type oldStoredTask struct { + storage.StoredValue + Task struct { + ParentID uuid.UUID `json:"parent_id"` + DatabaseID string `json:"database_id"` + NodeName string `json:"node_name"` + InstanceID string `json:"instance_id"` + HostID string `json:"host_id"` + TaskID uuid.UUID `json:"task_id"` + CreatedAt time.Time `json:"created_at"` + CompletedAt time.Time `json:"completed_at"` + Type task.Type `json:"type"` + WorkflowInstanceID string `json:"workflow_id"` + WorkflowExecutionID string `json:"workflow_execution_id"` + Status task.Status `json:"status"` + Error string `json:"error"` + } `json:"task"` +} + +func (o *oldStoredTask) convert() *task.StoredTask { + return &task.StoredTask{ + Task: &task.Task{ + Scope: task.ScopeDatabase, + EntityID: o.Task.DatabaseID, + TaskID: o.Task.TaskID, + Type: o.Task.Type, + ParentID: o.Task.ParentID, + Status: o.Task.Status, + Error: o.Task.Error, + HostID: o.Task.HostID, + DatabaseID: o.Task.DatabaseID, + NodeName: o.Task.NodeName, + InstanceID: o.Task.InstanceID, + CreatedAt: o.Task.CreatedAt, + CompletedAt: o.Task.CompletedAt, + WorkflowInstanceID: o.Task.WorkflowInstanceID, + WorkflowExecutionID: o.Task.WorkflowExecutionID, + }, + } +} + +type oldStoredTaskLogEntry struct { + storage.StoredValue + DatabaseID string `json:"database_id"` + TaskID uuid.UUID `json:"task_id"` + EntryID uuid.UUID `json:"entry_id"` + Timestamp time.Time `json:"timestamp"` + Message string `json:"message"` + Fields map[string]any `json:"fields"` +} + +func (o *oldStoredTaskLogEntry) convert() *task.StoredTaskLogEntry { + return &task.StoredTaskLogEntry{ + Scope: task.ScopeDatabase, + EntityID: o.DatabaseID, + DatabaseID: o.DatabaseID, + TaskID: o.TaskID, + EntryID: o.EntryID, + Timestamp: o.Timestamp, + Message: o.Message, + Fields: o.Fields, + } +} diff --git a/server/internal/orchestrator/swarm/pgbackrest_restore.go b/server/internal/orchestrator/swarm/pgbackrest_restore.go index e09fd759..c7975165 100644 --- a/server/internal/orchestrator/swarm/pgbackrest_restore.go +++ b/server/internal/orchestrator/swarm/pgbackrest_restore.go @@ -91,9 +91,13 @@ func (p *PgBackRestRestore) Create(ctx context.Context, rc *resource.Context) er } t, err := p.startTask(ctx, taskSvc) + if err != nil { + return err + } + handleError := func(cause error) error { p.failTask(logger, taskSvc, t, cause) - return err + return cause } svcResource, err := resource.FromContext[*PostgresService](rc, PostgresServiceResourceIdentifier(p.InstanceID)) @@ -135,7 +139,7 @@ func (p *PgBackRestRestore) Create(ctx context.Context, rc *resource.Context) er } func (p *PgBackRestRestore) startTask(ctx context.Context, taskSvc *task.Service) (*task.Task, error) { - t, err := taskSvc.GetTask(ctx, p.DatabaseID, p.TaskID) + t, err := taskSvc.GetTask(ctx, task.ScopeDatabase, p.DatabaseID, p.TaskID) if err != nil { return nil, fmt.Errorf("failed to get task %s: %w", p.TaskID, err) } @@ -269,7 +273,7 @@ func (p *PgBackRestRestore) streamLogsAndWait( Msg("failed to remove pgbackrest restore container") } }() - taskLogger := task.NewTaskLogWriter(ctx, taskSvc, p.DatabaseID, p.TaskID) + taskLogger := task.NewTaskLogWriter(ctx, taskSvc, task.ScopeDatabase, p.DatabaseID, p.TaskID) // The follow: true means that this will block until the container exits. err := dockerClient.ContainerLogs(ctx, taskLogger, containerID, container.LogsOptions{ ShowStdout: true, diff --git a/server/internal/task/service.go b/server/internal/task/service.go index fe41d107..a4766302 100644 --- a/server/internal/task/service.go +++ b/server/internal/task/service.go @@ -40,7 +40,7 @@ func (s *Service) CreateTask(ctx context.Context, opts Options) (*Task, error) { } func (s *Service) UpdateTask(ctx context.Context, task *Task) error { - stored, err := s.Store.Task.GetByKey(task.DatabaseID, task.TaskID).Exec(ctx) + stored, err := s.Store.Task.GetByKey(task.Scope, task.EntityID, task.TaskID).Exec(ctx) if errors.Is(err, storage.ErrNotFound) { return ErrTaskNotFound } else if err != nil { @@ -56,8 +56,8 @@ func (s *Service) UpdateTask(ctx context.Context, task *Task) error { return nil } -func (s *Service) GetTask(ctx context.Context, databaseID string, taskID uuid.UUID) (*Task, error) { - stored, err := s.Store.Task.GetByKey(databaseID, taskID).Exec(ctx) +func (s *Service) GetTask(ctx context.Context, scope Scope, entityID string, taskID uuid.UUID) (*Task, error) { + stored, err := s.Store.Task.GetByKey(scope, entityID, taskID).Exec(ctx) if errors.Is(err, storage.ErrNotFound) { return nil, ErrTaskNotFound } else if err != nil { @@ -67,34 +67,34 @@ func (s *Service) GetTask(ctx context.Context, databaseID string, taskID uuid.UU return stored.Task, nil } -func (s *Service) GetTasks(ctx context.Context, databaseID string, options TaskListOptions) ([]*Task, error) { +func (s *Service) GetTasks(ctx context.Context, scope Scope, entityID string, options TaskListOptions) ([]*Task, error) { if options.Type == "" && options.NodeName == "" && len(options.Statuses) == 0 { - return s.getTasks(ctx, databaseID, options) + return s.getTasks(ctx, scope, entityID, options) } - return s.getTasksFiltered(ctx, databaseID, options) + return s.getTasksFiltered(ctx, scope, entityID, options) } -func (s *Service) DeleteTask(ctx context.Context, databaseID string, taskID uuid.UUID) error { - deleted, err := s.Store.Task.Delete(databaseID, taskID).Exec(ctx) +func (s *Service) DeleteTask(ctx context.Context, scope Scope, entityID string, taskID uuid.UUID) error { + deleted, err := s.Store.Task.Delete(scope, entityID, taskID).Exec(ctx) if err != nil { return fmt.Errorf("failed to delete task: %w", err) } if deleted == 0 { return ErrTaskNotFound } - if err := s.DeleteTaskLogs(ctx, databaseID, taskID); err != nil { + if err := s.DeleteTaskLogs(ctx, scope, entityID, taskID); err != nil { return err } return nil } -func (s *Service) DeleteAllTasks(ctx context.Context, databaseID string) error { - _, err := s.Store.Task.DeleteByDatabaseID(databaseID).Exec(ctx) +func (s *Service) DeleteAllTasks(ctx context.Context, scope Scope, entityID string) error { + _, err := s.Store.Task.DeleteByEntity(scope, entityID).Exec(ctx) if err != nil { return fmt.Errorf("failed to delete tasks: %w", err) } - if err := s.DeleteAllTaskLogs(ctx, databaseID); err != nil { + if err := s.DeleteAllTaskLogs(ctx, scope, entityID); err != nil { return err } return nil @@ -106,7 +106,7 @@ type LogEntry struct { Fields map[string]any } -func (s *Service) AddLogEntry(ctx context.Context, databaseID string, taskID uuid.UUID, entry LogEntry) error { +func (s *Service) AddLogEntry(ctx context.Context, scope Scope, entityID string, taskID uuid.UUID, entry LogEntry) error { entryID, err := uuid.NewV7() if err != nil { return fmt.Errorf("failed to create entry ID: %w", err) @@ -116,12 +116,17 @@ func (s *Service) AddLogEntry(ctx context.Context, databaseID string, taskID uui timestamp = time.Now() } stored := &StoredTaskLogEntry{ - DatabaseID: databaseID, - TaskID: taskID, - EntryID: entryID, - Timestamp: timestamp, - Message: entry.Message, - Fields: entry.Fields, + Scope: scope, + EntityID: entityID, + TaskID: taskID, + EntryID: entryID, + Timestamp: timestamp, + Message: entry.Message, + Fields: entry.Fields, + } + if scope == ScopeDatabase { + // For backward compatibility + stored.DatabaseID = entityID } err = s.Store.TaskLogMessage.Put(stored).Exec(ctx) if err != nil { @@ -131,17 +136,25 @@ func (s *Service) AddLogEntry(ctx context.Context, databaseID string, taskID uui return nil } -func (s *Service) GetTaskLog(ctx context.Context, databaseID string, taskID uuid.UUID, options TaskLogOptions) (*TaskLog, error) { - stored, err := s.Store.TaskLogMessage.GetAllByTaskID(databaseID, taskID, options).Exec(ctx) +func (s *Service) GetTaskLog(ctx context.Context, scope Scope, entityID string, taskID uuid.UUID, options TaskLogOptions) (*TaskLog, error) { + stored, err := s.Store.TaskLogMessage.GetAllByTask(scope, entityID, taskID, options).Exec(ctx) if err != nil { return nil, fmt.Errorf("failed to get task log: %w", err) } log := &TaskLog{ - DatabaseID: databaseID, - TaskID: taskID, - Entries: make([]LogEntry, 0, len(stored)), + Scope: scope, + EntityID: entityID, + TaskID: taskID, + Entries: make([]LogEntry, 0, len(stored)), + } + + // TODO: remove when we remove these fields from the task log type in the + // API. + if scope == ScopeDatabase { + log.DatabaseID = entityID } + for i := len(stored) - 1; i >= 0; i-- { s := stored[i] if s.EntryID == options.AfterEntryID { @@ -164,24 +177,24 @@ func (s *Service) GetTaskLog(ctx context.Context, databaseID string, taskID uuid return log, nil } -func (s *Service) DeleteTaskLogs(ctx context.Context, databaseID string, taskID uuid.UUID) error { - _, err := s.Store.TaskLogMessage.DeleteByTaskID(databaseID, taskID).Exec(ctx) +func (s *Service) DeleteTaskLogs(ctx context.Context, scope Scope, entityID string, taskID uuid.UUID) error { + _, err := s.Store.TaskLogMessage.DeleteByTask(scope, entityID, taskID).Exec(ctx) if err != nil { return fmt.Errorf("failed to delete task logs: %w", err) } return nil } -func (s *Service) DeleteAllTaskLogs(ctx context.Context, databaseID string) error { - _, err := s.Store.TaskLogMessage.DeleteByDatabaseID(databaseID).Exec(ctx) +func (s *Service) DeleteAllTaskLogs(ctx context.Context, scope Scope, entityID string) error { + _, err := s.Store.TaskLogMessage.DeleteByEntity(scope, entityID).Exec(ctx) if err != nil { return fmt.Errorf("failed to delete task logs: %w", err) } return nil } -func (s *Service) getTasks(ctx context.Context, databaseID string, options TaskListOptions) ([]*Task, error) { - stored, err := s.Store.Task.GetAllByDatabaseID(databaseID, options).Exec(ctx) +func (s *Service) getTasks(ctx context.Context, scope Scope, entityID string, options TaskListOptions) ([]*Task, error) { + stored, err := s.Store.Task.GetAllByEntity(scope, entityID, options).Exec(ctx) if errors.Is(err, storage.ErrNotFound) { return []*Task{}, nil } else if err != nil { @@ -196,7 +209,7 @@ func (s *Service) getTasks(ctx context.Context, databaseID string, options TaskL return tasks, nil } -func (s *Service) getTasksFiltered(ctx context.Context, databaseID string, options TaskListOptions) ([]*Task, error) { +func (s *Service) getTasksFiltered(ctx context.Context, scope Scope, entityID string, options TaskListOptions) ([]*Task, error) { perPage := perPageFor(options) tasks := make([]*Task, 0) if options.Limit > 0 { @@ -209,7 +222,7 @@ func (s *Service) getTasksFiltered(ctx context.Context, databaseID string, optio pageOpts.Limit = perPage pageOpts.AfterTaskID = after - stored, err := s.Store.Task.GetAllByDatabaseID(databaseID, pageOpts).Exec(ctx) + stored, err := s.Store.Task.GetAllByEntity(scope, entityID, pageOpts).Exec(ctx) if errors.Is(err, storage.ErrNotFound) { break } else if err != nil { @@ -262,7 +275,7 @@ func matchesFilters(task *Task, opts TaskListOptions) bool { if opts.Type != "" && task.Type != opts.Type { return false } - if !slices.Contains(opts.Statuses, task.Status) { + if len(opts.Statuses) > 0 && !slices.Contains(opts.Statuses, task.Status) { return false } if opts.NodeName != "" && (task == nil || task.NodeName != opts.NodeName) { diff --git a/server/internal/task/service_scoped_test.go b/server/internal/task/service_scoped_test.go new file mode 100644 index 00000000..df3cd132 --- /dev/null +++ b/server/internal/task/service_scoped_test.go @@ -0,0 +1,291 @@ +package task_test + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pgEdge/control-plane/server/internal/storage/storagetest" + "github.com/pgEdge/control-plane/server/internal/task" +) + +func TestServiceScoped(t *testing.T) { + server := storagetest.NewEtcdTestServer(t) + client := server.Client(t) + + t.Run("Create and get database task", func(t *testing.T) { + store := task.NewStore(client, uuid.NewString()) + svc := task.NewService(store) + + // Create database task + tsk, err := svc.CreateTask(t.Context(), task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-1", + Type: task.TypeCreate, + }) + require.NoError(t, err) + assert.Equal(t, task.ScopeDatabase, tsk.Scope) + assert.Equal(t, "database-1", tsk.EntityID) + assert.Equal(t, "database-1", tsk.DatabaseID) + assert.Equal(t, task.TypeCreate, tsk.Type) + assert.Equal(t, task.StatusPending, tsk.Status) + + // Get task by scope and entity ID + retrieved, err := svc.GetTask(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID) + require.NoError(t, err) + assert.Equal(t, tsk.TaskID, retrieved.TaskID) + assert.Equal(t, task.ScopeDatabase, retrieved.Scope) + assert.Equal(t, "database-1", retrieved.EntityID) + }) + + t.Run("Create and get host task", func(t *testing.T) { + store := task.NewStore(client, uuid.NewString()) + svc := task.NewService(store) + + // Create host task + tsk, err := svc.CreateTask(t.Context(), task.Options{ + Scope: task.ScopeHost, + HostID: "host-1", + Type: task.TypeRemoveHost, + }) + require.NoError(t, err) + assert.Equal(t, task.ScopeHost, tsk.Scope) + assert.Equal(t, "host-1", tsk.EntityID) + assert.Equal(t, "host-1", tsk.HostID) + assert.Equal(t, task.TypeRemoveHost, tsk.Type) + + // Get task by scope and entity ID + retrieved, err := svc.GetTask(t.Context(), task.ScopeHost, "host-1", tsk.TaskID) + require.NoError(t, err) + assert.Equal(t, tsk.TaskID, retrieved.TaskID) + assert.Equal(t, task.ScopeHost, retrieved.Scope) + assert.Equal(t, "host-1", retrieved.EntityID) + }) + + t.Run("Get tasks by entity", func(t *testing.T) { + store := task.NewStore(client, uuid.NewString()) + svc := task.NewService(store) + + // Create multiple database tasks + for i := 0; i < 3; i++ { + _, err := svc.CreateTask(t.Context(), task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-1", + Type: task.TypeCreate, + }) + require.NoError(t, err) + } + + // Create task for different database + _, err := svc.CreateTask(t.Context(), task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-2", + Type: task.TypeUpdate, + }) + require.NoError(t, err) + + // Create host task + _, err = svc.CreateTask(t.Context(), task.Options{ + Scope: task.ScopeHost, + HostID: "host-1", + Type: task.TypeRemoveHost, + }) + require.NoError(t, err) + + // Get tasks for database-1 + tasks, err := svc.GetTasks(t.Context(), task.ScopeDatabase, "database-1", task.TaskListOptions{}) + require.NoError(t, err) + assert.Len(t, tasks, 3) + for _, tsk := range tasks { + assert.Equal(t, task.ScopeDatabase, tsk.Scope) + assert.Equal(t, "database-1", tsk.EntityID) + } + + // Get tasks for database-2 + tasks, err = svc.GetTasks(t.Context(), task.ScopeDatabase, "database-2", task.TaskListOptions{}) + require.NoError(t, err) + assert.Len(t, tasks, 1) + assert.Equal(t, "database-2", tasks[0].EntityID) + + // Get tasks for host-1 + tasks, err = svc.GetTasks(t.Context(), task.ScopeHost, "host-1", task.TaskListOptions{}) + require.NoError(t, err) + assert.Len(t, tasks, 1) + assert.Equal(t, task.ScopeHost, tasks[0].Scope) + assert.Equal(t, "host-1", tasks[0].EntityID) + }) + + t.Run("Add and get task log", func(t *testing.T) { + store := task.NewStore(client, uuid.NewString()) + svc := task.NewService(store) + + // Create database task + tsk, err := svc.CreateTask(t.Context(), task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-1", + Type: task.TypeCreate, + }) + require.NoError(t, err) + + // Add log entry + err = svc.AddLogEntry(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID, task.LogEntry{ + Message: "Starting task", + Fields: map[string]any{"step": 1}, + }) + require.NoError(t, err) + + // Get task log + log, err := svc.GetTaskLog(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID, task.TaskLogOptions{}) + require.NoError(t, err) + assert.Equal(t, tsk.TaskID, log.TaskID) + assert.Len(t, log.Entries, 1) + assert.Equal(t, "Starting task", log.Entries[0].Message) + assert.Equal(t, float64(1), log.Entries[0].Fields["step"]) // JSON encoding converts int to float64 + + // Add more log entries + err = svc.AddLogEntry(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID, task.LogEntry{ + Message: "Task completed", + Fields: map[string]any{"step": 2}, + }) + require.NoError(t, err) + + // Get updated log + log, err = svc.GetTaskLog(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID, task.TaskLogOptions{}) + require.NoError(t, err) + assert.Len(t, log.Entries, 2) + }) + + t.Run("Update task", func(t *testing.T) { + store := task.NewStore(client, uuid.NewString()) + svc := task.NewService(store) + + // Create task + tsk, err := svc.CreateTask(t.Context(), task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-1", + Type: task.TypeCreate, + }) + require.NoError(t, err) + assert.Equal(t, task.StatusPending, tsk.Status) + + // Update task + tsk.Start() + err = svc.UpdateTask(t.Context(), tsk) + require.NoError(t, err) + + // Verify update + retrieved, err := svc.GetTask(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID) + require.NoError(t, err) + assert.Equal(t, task.StatusRunning, retrieved.Status) + }) + + t.Run("Delete task", func(t *testing.T) { + store := task.NewStore(client, uuid.NewString()) + svc := task.NewService(store) + + // Create task + tsk, err := svc.CreateTask(t.Context(), task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-1", + Type: task.TypeCreate, + }) + require.NoError(t, err) + + // Delete task + err = svc.DeleteTask(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID) + require.NoError(t, err) + + // Verify deletion + _, err = svc.GetTask(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID) + assert.ErrorIs(t, err, task.ErrTaskNotFound) + }) + + t.Run("Delete all tasks", func(t *testing.T) { + store := task.NewStore(client, uuid.NewString()) + svc := task.NewService(store) + + // Create multiple tasks + for i := 0; i < 3; i++ { + _, err := svc.CreateTask(t.Context(), task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-1", + Type: task.TypeCreate, + }) + require.NoError(t, err) + } + + // Delete all tasks + err := svc.DeleteAllTasks(t.Context(), task.ScopeDatabase, "database-1") + require.NoError(t, err) + + // Verify deletion + tasks, err := svc.GetTasks(t.Context(), task.ScopeDatabase, "database-1", task.TaskListOptions{}) + require.NoError(t, err) + assert.Len(t, tasks, 0) + }) + + t.Run("Delete task logs", func(t *testing.T) { + store := task.NewStore(client, uuid.NewString()) + svc := task.NewService(store) + + // Create task with logs + tsk, err := svc.CreateTask(t.Context(), task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-1", + Type: task.TypeCreate, + }) + require.NoError(t, err) + + // Add log entries + for i := 0; i < 3; i++ { + err = svc.AddLogEntry(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID, task.LogEntry{ + Message: "Log entry", + }) + require.NoError(t, err) + } + + // Delete logs + err = svc.DeleteTaskLogs(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID) + require.NoError(t, err) + + // Verify deletion + log, err := svc.GetTaskLog(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID, task.TaskLogOptions{}) + require.NoError(t, err) + assert.Len(t, log.Entries, 0) + }) + + t.Run("Delete all task logs", func(t *testing.T) { + store := task.NewStore(client, uuid.NewString()) + svc := task.NewService(store) + + // Create multiple tasks with logs + for i := 0; i < 2; i++ { + tsk, err := svc.CreateTask(t.Context(), task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-1", + Type: task.TypeCreate, + }) + require.NoError(t, err) + + err = svc.AddLogEntry(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID, task.LogEntry{ + Message: "Log entry", + }) + require.NoError(t, err) + } + + // Delete all logs + err := svc.DeleteAllTaskLogs(t.Context(), task.ScopeDatabase, "database-1") + require.NoError(t, err) + + // Verify all logs deleted + tasks, err := svc.GetTasks(t.Context(), task.ScopeDatabase, "database-1", task.TaskListOptions{}) + require.NoError(t, err) + for _, tsk := range tasks { + log, err := svc.GetTaskLog(t.Context(), task.ScopeDatabase, "database-1", tsk.TaskID, task.TaskLogOptions{}) + require.NoError(t, err) + assert.Len(t, log.Entries, 0) + } + }) +} diff --git a/server/internal/task/task.go b/server/internal/task/task.go index 7d6496a5..ff5bd90e 100644 --- a/server/internal/task/task.go +++ b/server/internal/task/task.go @@ -10,6 +10,17 @@ import ( "github.com/pgEdge/control-plane/server/internal/utils" ) +type Scope string + +func (s Scope) String() string { + return string(s) +} + +const ( + ScopeDatabase Scope = "database" + ScopeHost Scope = "host" +) + type Type string func (t Type) String() string { @@ -54,6 +65,8 @@ var completedStatuses = ds.NewSet( ) type Task struct { + Scope Scope `json:"scope"` + EntityID string `json:"entity_id"` ParentID uuid.UUID `json:"parent_id"` DatabaseID string `json:"database_id"` NodeName string `json:"node_name"` @@ -74,6 +87,7 @@ func (t *Task) IsComplete() bool { } type Options struct { + Scope Scope `json:"scope"` ParentID uuid.UUID `json:"parent_id"` DatabaseID string `json:"database_id"` NodeName string `json:"node_name"` @@ -84,13 +98,40 @@ type Options struct { WorkflowExecutionID string `json:"workflow_execution_id"` } +func (o Options) EntityID() string { + switch o.Scope { + case ScopeDatabase: + return o.DatabaseID + case ScopeHost: + return o.HostID + default: + return "" + } +} + func (o Options) validate() error { - if o.DatabaseID == "" { - return errors.New("database ID is required when creating a new task") + // Require scope and entity_id + if o.Scope == "" { + return errors.New("scope is required when creating a new task") } if o.Type == "" { return errors.New("type is required when creating a new task") } + + // Enforce scope-specific rules + switch o.Scope { + case ScopeDatabase: + if o.DatabaseID == "" { + return errors.New("database_id is required for database scope") + } + case ScopeHost: + if o.HostID == "" { + return errors.New("host_id is required for host scope") + } + default: + return fmt.Errorf("invalid scope: %s", o.Scope) + } + return nil } @@ -105,6 +146,8 @@ func NewTask(opts Options) (*Task, error) { } return &Task{ + Scope: opts.Scope, + EntityID: opts.EntityID(), ParentID: opts.ParentID, DatabaseID: opts.DatabaseID, NodeName: opts.NodeName, @@ -201,6 +244,8 @@ func (t *Task) SetCompleted() { } type TaskLog struct { + Scope Scope `json:"scope"` + EntityID string `json:"entity_id"` DatabaseID string `json:"database_id"` TaskID uuid.UUID `json:"id"` LastEntryID uuid.UUID `json:"last_entry_id"` diff --git a/server/internal/task/task_log_store.go b/server/internal/task/task_log_store.go index bf0c423e..6cef85b6 100644 --- a/server/internal/task/task_log_store.go +++ b/server/internal/task/task_log_store.go @@ -11,6 +11,8 @@ import ( type StoredTaskLogEntry struct { storage.StoredValue + Scope Scope `json:"scope"` + EntityID string `json:"entity_id"` DatabaseID string `json:"database_id"` TaskID uuid.UUID `json:"task_id"` EntryID uuid.UUID `json:"entry_id"` @@ -32,19 +34,40 @@ func NewTaskLogEntryStore(client *clientv3.Client, root string) *TaskLogEntrySto } func (s *TaskLogEntryStore) Prefix() string { - return storage.Prefix("/", s.root, "task_log_messages") + return storage.Prefix("/", s.root, "task_log_entries") } +// EntityPrefix returns the prefix for all task log entries for a given scope and entity. +func (s *TaskLogEntryStore) EntityPrefix(scope Scope, entityID string) string { + return storage.Prefix(s.Prefix(), scope.String(), entityID) +} + +// DatabasePrefix returns the prefix for all task log entries for a given database. +// Deprecated: Use EntityPrefix(ScopeDatabase, databaseID) instead. func (s *TaskLogEntryStore) DatabasePrefix(databaseID string) string { - return storage.Prefix(s.Prefix(), databaseID) + return s.EntityPrefix(ScopeDatabase, databaseID) +} + +// TaskPrefix returns the prefix for all log entries for a specific task. +func (s *TaskLogEntryStore) TaskPrefix(scope Scope, entityID string, taskID uuid.UUID) string { + return storage.Prefix(s.EntityPrefix(scope, entityID), taskID.String()) } -func (s *TaskLogEntryStore) TaskPrefix(databaseID string, taskID uuid.UUID) string { - return storage.Prefix(s.DatabasePrefix(databaseID), taskID.String()) +// TaskPrefixDeprecated returns the prefix for all log entries for a specific task. +// Deprecated: Use TaskPrefix(ScopeDatabase, databaseID, taskID) instead. +func (s *TaskLogEntryStore) TaskPrefixDeprecated(databaseID string, taskID uuid.UUID) string { + return s.TaskPrefix(ScopeDatabase, databaseID, taskID) } -func (s *TaskLogEntryStore) Key(databaseID string, taskID, entryID uuid.UUID) string { - return storage.Key(s.TaskPrefix(databaseID, taskID), entryID.String()) +// Key returns the storage key for a task log entry. +func (s *TaskLogEntryStore) Key(scope Scope, entityID string, taskID, entryID uuid.UUID) string { + return storage.Key(s.TaskPrefix(scope, entityID, taskID), entryID.String()) +} + +// KeyDeprecated returns the storage key for a task log entry. +// Deprecated: Use Key(ScopeDatabase, databaseID, taskID, entryID) instead. +func (s *TaskLogEntryStore) KeyDeprecated(databaseID string, taskID, entryID uuid.UUID) string { + return s.Key(ScopeDatabase, databaseID, taskID, entryID) } type TaskLogOptions struct { @@ -52,8 +75,8 @@ type TaskLogOptions struct { AfterEntryID uuid.UUID } -func (s *TaskLogEntryStore) GetAllByTaskID(databaseID string, taskID uuid.UUID, options TaskLogOptions) storage.GetMultipleOp[*StoredTaskLogEntry] { - rangeStart := s.TaskPrefix(databaseID, taskID) +func (s *TaskLogEntryStore) GetAllByTask(scope Scope, entityID string, taskID uuid.UUID, options TaskLogOptions) storage.GetMultipleOp[*StoredTaskLogEntry] { + rangeStart := s.TaskPrefix(scope, entityID, taskID) rangeEnd := clientv3.GetPrefixRangeEnd(rangeStart) var opOptions []clientv3.OpOption @@ -64,7 +87,7 @@ func (s *TaskLogEntryStore) GetAllByTaskID(databaseID string, taskID uuid.UUID, // We intentionally treat this as inclusive so that we still return an // entry when AfterEntryID is the last entry. Callers must ignore the // entry with EntryID == AfterEntryID. - rangeStart = s.Key(databaseID, taskID, options.AfterEntryID) + rangeStart = s.Key(scope, entityID, taskID, options.AfterEntryID) } opOptions = append( opOptions, @@ -75,17 +98,35 @@ func (s *TaskLogEntryStore) GetAllByTaskID(databaseID string, taskID uuid.UUID, return storage.NewGetRangeOp[*StoredTaskLogEntry](s.client, rangeStart, rangeEnd, opOptions...) } +// GetAllByTaskID retrieves all log entries for a task. +// Deprecated: Use GetAllByTask(ScopeDatabase, databaseID, taskID, options) instead. +func (s *TaskLogEntryStore) GetAllByTaskID(databaseID string, taskID uuid.UUID, options TaskLogOptions) storage.GetMultipleOp[*StoredTaskLogEntry] { + return s.GetAllByTask(ScopeDatabase, databaseID, taskID, options) +} + func (s *TaskLogEntryStore) Put(item *StoredTaskLogEntry) storage.PutOp[*StoredTaskLogEntry] { - key := s.Key(item.DatabaseID, item.TaskID, item.EntryID) + key := s.Key(item.Scope, item.EntityID, item.TaskID, item.EntryID) return storage.NewPutOp(s.client, key, item) } +func (s *TaskLogEntryStore) DeleteByTask(scope Scope, entityID string, taskID uuid.UUID) storage.DeleteOp { + prefix := s.TaskPrefix(scope, entityID, taskID) + return storage.NewDeletePrefixOp(s.client, prefix) +} + +// DeleteByTaskID deletes all log entries for a task. +// Deprecated: Use DeleteByTask(ScopeDatabase, databaseID, taskID) instead. func (s *TaskLogEntryStore) DeleteByTaskID(databaseID string, taskID uuid.UUID) storage.DeleteOp { - prefix := s.TaskPrefix(databaseID, taskID) + return s.DeleteByTask(ScopeDatabase, databaseID, taskID) +} + +func (s *TaskLogEntryStore) DeleteByEntity(scope Scope, entityID string) storage.DeleteOp { + prefix := s.EntityPrefix(scope, entityID) return storage.NewDeletePrefixOp(s.client, prefix) } +// DeleteByDatabaseID deletes all log entries for a database. +// Deprecated: Use DeleteByEntity(ScopeDatabase, databaseID) instead. func (s *TaskLogEntryStore) DeleteByDatabaseID(databaseID string) storage.DeleteOp { - prefix := s.DatabasePrefix(databaseID) - return storage.NewDeletePrefixOp(s.client, prefix) + return s.DeleteByEntity(ScopeDatabase, databaseID) } diff --git a/server/internal/task/task_log_store_test.go b/server/internal/task/task_log_store_test.go index 158edd3d..29b1a0cd 100644 --- a/server/internal/task/task_log_store_test.go +++ b/server/internal/task/task_log_store_test.go @@ -11,6 +11,160 @@ import ( "github.com/pgEdge/control-plane/server/internal/task" ) +func TestTaskLogEntryStoreKeyGeneration(t *testing.T) { + server := storagetest.NewEtcdTestServer(t) + client := server.Client(t) + + store := task.NewTaskLogEntryStore(client, "test-root") + taskID := uuid.New() + entryID := uuid.New() + + t.Run("EntityPrefix for database scope", func(t *testing.T) { + prefix := store.EntityPrefix(task.ScopeDatabase, "my-database") + expected := "/test-root/task_log_entries/database/my-database/" + assert.Equal(t, expected, prefix) + }) + + t.Run("EntityPrefix for host scope", func(t *testing.T) { + prefix := store.EntityPrefix(task.ScopeHost, "host-1") + expected := "/test-root/task_log_entries/host/host-1/" + assert.Equal(t, expected, prefix) + }) + + t.Run("TaskPrefix for database scope", func(t *testing.T) { + prefix := store.TaskPrefix(task.ScopeDatabase, "my-database", taskID) + expected := "/test-root/task_log_entries/database/my-database/" + taskID.String() + "/" + assert.Equal(t, expected, prefix) + }) + + t.Run("Key for database scope", func(t *testing.T) { + key := store.Key(task.ScopeDatabase, "my-database", taskID, entryID) + expected := "/test-root/task_log_entries/database/my-database/" + taskID.String() + "/" + entryID.String() + assert.Equal(t, expected, key) + }) +} + +func TestTaskLogEntryStoreCRUD(t *testing.T) { + server := storagetest.NewEtcdTestServer(t) + client := server.Client(t) + + t.Run("Put and GetAllByTask with database scope", func(t *testing.T) { + store := task.NewTaskLogEntryStore(client, uuid.NewString()) + taskID := uuid.New() + + // Create log entries + for i := 0; i < 3; i++ { + err := store.Put(&task.StoredTaskLogEntry{ + Scope: task.ScopeDatabase, + EntityID: "my-database", + DatabaseID: "my-database", + TaskID: taskID, + EntryID: uuid.New(), + Message: "test message", + }).Exec(t.Context()) + require.NoError(t, err) + } + + // Get all log entries + entries, err := store.GetAllByTask(task.ScopeDatabase, "my-database", taskID, task.TaskLogOptions{}).Exec(t.Context()) + require.NoError(t, err) + assert.Len(t, entries, 3) + for _, entry := range entries { + assert.Equal(t, task.ScopeDatabase, entry.Scope) + assert.Equal(t, "my-database", entry.EntityID) + assert.Equal(t, taskID, entry.TaskID) + } + }) + + t.Run("Put and GetAllByTask with host scope", func(t *testing.T) { + store := task.NewTaskLogEntryStore(client, uuid.NewString()) + taskID := uuid.New() + + // Create log entries + for i := 0; i < 2; i++ { + err := store.Put(&task.StoredTaskLogEntry{ + Scope: task.ScopeHost, + EntityID: "host-1", + TaskID: taskID, + EntryID: uuid.New(), + Message: "test message", + }).Exec(t.Context()) + require.NoError(t, err) + } + + // Get all log entries + entries, err := store.GetAllByTask(task.ScopeHost, "host-1", taskID, task.TaskLogOptions{}).Exec(t.Context()) + require.NoError(t, err) + assert.Len(t, entries, 2) + for _, entry := range entries { + assert.Equal(t, task.ScopeHost, entry.Scope) + assert.Equal(t, "host-1", entry.EntityID) + } + }) +} + +func TestTaskLogEntryStoreDelete(t *testing.T) { + server := storagetest.NewEtcdTestServer(t) + client := server.Client(t) + + t.Run("DeleteByTask", func(t *testing.T) { + store := task.NewTaskLogEntryStore(client, uuid.NewString()) + taskID := uuid.New() + + // Create log entries + err := store.Put(&task.StoredTaskLogEntry{ + Scope: task.ScopeDatabase, + EntityID: "my-database", + DatabaseID: "my-database", + TaskID: taskID, + EntryID: uuid.New(), + }).Exec(t.Context()) + require.NoError(t, err) + + // Delete by task + _, err = store.DeleteByTask(task.ScopeDatabase, "my-database", taskID).Exec(t.Context()) + require.NoError(t, err) + + // Verify entries are gone + entries, err := store.GetAllByTask(task.ScopeDatabase, "my-database", taskID, task.TaskLogOptions{}).Exec(t.Context()) + require.NoError(t, err) + assert.Len(t, entries, 0) + }) + + t.Run("DeleteByEntity", func(t *testing.T) { + store := task.NewTaskLogEntryStore(client, uuid.NewString()) + + // Create log entries for two entities + err := store.Put(&task.StoredTaskLogEntry{ + Scope: task.ScopeDatabase, + EntityID: "database-1", + DatabaseID: "database-1", + TaskID: uuid.New(), + EntryID: uuid.New(), + }).Exec(t.Context()) + require.NoError(t, err) + + taskID2 := uuid.New() + err = store.Put(&task.StoredTaskLogEntry{ + Scope: task.ScopeDatabase, + EntityID: "database-2", + DatabaseID: "database-2", + TaskID: taskID2, + EntryID: uuid.New(), + }).Exec(t.Context()) + require.NoError(t, err) + + // Delete database-1 + _, err = store.DeleteByEntity(task.ScopeDatabase, "database-1").Exec(t.Context()) + require.NoError(t, err) + + // Verify database-2 entries still exist + entries, err := store.GetAllByTask(task.ScopeDatabase, "database-2", taskID2, task.TaskLogOptions{}).Exec(t.Context()) + require.NoError(t, err) + assert.Len(t, entries, 1) + }) +} + func TestTaskLogEntryStore(t *testing.T) { server := storagetest.NewEtcdTestServer(t) client := server.Client(t) @@ -24,6 +178,8 @@ func TestTaskLogEntryStore(t *testing.T) { task2ID := uuid.New() err := store.Put(&task.StoredTaskLogEntry{ + Scope: task.ScopeDatabase, + EntityID: "database", DatabaseID: "database", TaskID: taskID, EntryID: uuid.New(), @@ -31,6 +187,8 @@ func TestTaskLogEntryStore(t *testing.T) { require.NoError(t, err) err = store.Put(&task.StoredTaskLogEntry{ + Scope: task.ScopeDatabase, + EntityID: "database2", DatabaseID: "database2", TaskID: task2ID, EntryID: uuid.New(), diff --git a/server/internal/task/task_log_writer.go b/server/internal/task/task_log_writer.go index 26d84489..dcfa6b23 100644 --- a/server/internal/task/task_log_writer.go +++ b/server/internal/task/task_log_writer.go @@ -10,17 +10,13 @@ import ( ) type TaskLogWriter struct { - DatabaseID string - TaskID uuid.UUID - writer *utils.LineWriter + writer *utils.LineWriter } -func NewTaskLogWriter(ctx context.Context, service *Service, databaseID string, taskID uuid.UUID) *TaskLogWriter { +func NewTaskLogWriter(ctx context.Context, service *Service, scope Scope, entityID string, taskID uuid.UUID) *TaskLogWriter { return &TaskLogWriter{ - DatabaseID: databaseID, - TaskID: taskID, writer: utils.NewLineWriter(func(b []byte) error { - err := service.AddLogEntry(ctx, databaseID, taskID, LogEntry{ + err := service.AddLogEntry(ctx, scope, entityID, taskID, LogEntry{ Message: utils.Clean(string(b)), // remove all control characters }) if err != nil { diff --git a/server/internal/task/task_scope_test.go b/server/internal/task/task_scope_test.go new file mode 100644 index 00000000..5839289a --- /dev/null +++ b/server/internal/task/task_scope_test.go @@ -0,0 +1,93 @@ +package task_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/pgEdge/control-plane/server/internal/task" +) + +func TestScopeString(t *testing.T) { + assert.Equal(t, "database", task.ScopeDatabase.String()) + assert.Equal(t, "host", task.ScopeHost.String()) +} + +func TestOptionsValidation(t *testing.T) { + tests := []struct { + name string + opts task.Options + expectError string + }{ + { + name: "valid database scope", + opts: task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "my-database", + Type: task.TypeCreate, + }, + expectError: "", + }, + { + name: "valid host scope", + opts: task.Options{ + Scope: task.ScopeHost, + HostID: "host-1", + Type: task.TypeRemoveHost, + }, + expectError: "", + }, + { + name: "missing scope", + opts: task.Options{ + DatabaseID: "my-database", + Type: task.TypeCreate, + }, + expectError: "scope is required", + }, + { + name: "missing type", + opts: task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "my-database", + }, + expectError: "type is required", + }, + { + name: "database scope missing database_id", + opts: task.Options{ + Scope: task.ScopeDatabase, + Type: task.TypeCreate, + }, + expectError: "database_id is required for database scope", + }, + { + name: "host scope missing host_id", + opts: task.Options{ + Scope: task.ScopeHost, + Type: task.TypeRemoveHost, + }, + expectError: "host_id is required for host scope", + }, + { + name: "invalid scope", + opts: task.Options{ + Scope: task.Scope("invalid"), + Type: task.TypeCreate, + }, + expectError: "invalid scope", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := task.NewTask(tt.opts) + if tt.expectError == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectError) + } + }) + } +} diff --git a/server/internal/task/task_store.go b/server/internal/task/task_store.go index 28c9fca1..69b9a768 100644 --- a/server/internal/task/task_store.go +++ b/server/internal/task/task_store.go @@ -25,19 +25,19 @@ func NewTaskStore(client *clientv3.Client, root string) *TaskStore { } func (s *TaskStore) Prefix() string { - return storage.Prefix("/", s.root, "tasks") + return storage.Prefix("/", s.root, "tasks_v2") } -func (s *TaskStore) DatabasePrefix(databaseID string) string { - return storage.Prefix(s.Prefix(), databaseID) +func (s *TaskStore) EntityPrefix(scope Scope, entityID string) string { + return storage.Prefix(s.Prefix(), scope.String(), entityID) } -func (s *TaskStore) Key(databaseID string, taskID uuid.UUID) string { - return storage.Key(s.DatabasePrefix(databaseID), taskID.String()) +func (s *TaskStore) Key(scope Scope, entityID string, taskID uuid.UUID) string { + return storage.Key(s.EntityPrefix(scope, entityID), taskID.String()) } -func (s *TaskStore) GetByKey(databaseID string, taskID uuid.UUID) storage.GetOp[*StoredTask] { - key := s.Key(databaseID, taskID) +func (s *TaskStore) GetByKey(scope Scope, entityID string, taskID uuid.UUID) storage.GetOp[*StoredTask] { + key := s.Key(scope, entityID, taskID) return storage.NewGetOp[*StoredTask](s.client, key) } @@ -63,8 +63,8 @@ type TaskListOptions struct { Statuses []Status } -func (s *TaskStore) GetAllByDatabaseID(databaseID string, options TaskListOptions) storage.GetMultipleOp[*StoredTask] { - rangeStart := s.DatabasePrefix(databaseID) +func (s *TaskStore) GetAllByEntity(scope Scope, entityID string, options TaskListOptions) storage.GetMultipleOp[*StoredTask] { + rangeStart := s.EntityPrefix(scope, entityID) rangeEnd := clientv3.GetPrefixRangeEnd(rangeStart) var opOptions []clientv3.OpOption @@ -78,9 +78,9 @@ func (s *TaskStore) GetAllByDatabaseID(databaseID string, options TaskListOption if options.AfterTaskID != uuid.Nil { switch sortOrder { case clientv3.SortAscend: - rangeStart = s.Key(databaseID, options.AfterTaskID) + "0" + rangeStart = s.Key(scope, entityID, options.AfterTaskID) + "0" case clientv3.SortDescend: - rangeEnd = s.Key(databaseID, options.AfterTaskID) + rangeEnd = s.Key(scope, entityID, options.AfterTaskID) } } @@ -90,21 +90,21 @@ func (s *TaskStore) GetAllByDatabaseID(databaseID string, options TaskListOption } func (s *TaskStore) Create(item *StoredTask) storage.PutOp[*StoredTask] { - key := s.Key(item.Task.DatabaseID, item.Task.TaskID) + key := s.Key(item.Task.Scope, item.Task.EntityID, item.Task.TaskID) return storage.NewCreateOp(s.client, key, item) } func (s *TaskStore) Update(item *StoredTask) storage.PutOp[*StoredTask] { - key := s.Key(item.Task.DatabaseID, item.Task.TaskID) + key := s.Key(item.Task.Scope, item.Task.EntityID, item.Task.TaskID) return storage.NewUpdateOp(s.client, key, item) } -func (s *TaskStore) Delete(databaseID string, taskID uuid.UUID) storage.DeleteOp { - key := s.Key(databaseID, taskID) +func (s *TaskStore) Delete(scope Scope, entityID string, taskID uuid.UUID) storage.DeleteOp { + key := s.Key(scope, entityID, taskID) return storage.NewDeleteKeyOp(s.client, key) } -func (s *TaskStore) DeleteByDatabaseID(databaseID string) storage.DeleteOp { - prefix := s.DatabasePrefix(databaseID) +func (s *TaskStore) DeleteByEntity(scope Scope, entityID string) storage.DeleteOp { + prefix := s.EntityPrefix(scope, entityID) return storage.NewDeletePrefixOp(s.client, prefix) } diff --git a/server/internal/task/task_store_test.go b/server/internal/task/task_store_test.go index 374e5af2..3863a672 100644 --- a/server/internal/task/task_store_test.go +++ b/server/internal/task/task_store_test.go @@ -11,6 +11,249 @@ import ( "github.com/pgEdge/control-plane/server/internal/task" ) +func TestTaskStoreKeyGeneration(t *testing.T) { + server := storagetest.NewEtcdTestServer(t) + client := server.Client(t) + + store := task.NewTaskStore(client, "test-root") + taskID := uuid.New() + + t.Run("EntityPrefix for database scope", func(t *testing.T) { + prefix := store.EntityPrefix(task.ScopeDatabase, "my-database") + expected := "/test-root/tasks_v2/database/my-database/" + assert.Equal(t, expected, prefix) + }) + + t.Run("EntityPrefix for host scope", func(t *testing.T) { + prefix := store.EntityPrefix(task.ScopeHost, "host-1") + expected := "/test-root/tasks_v2/host/host-1/" + assert.Equal(t, expected, prefix) + }) + + t.Run("Key for database scope", func(t *testing.T) { + key := store.Key(task.ScopeDatabase, "my-database", taskID) + expected := "/test-root/tasks_v2/database/my-database/" + taskID.String() + assert.Equal(t, expected, key) + }) + + t.Run("Key for host scope", func(t *testing.T) { + key := store.Key(task.ScopeHost, "host-1", taskID) + expected := "/test-root/tasks_v2/host/host-1/" + taskID.String() + assert.Equal(t, expected, key) + }) +} + +func TestTaskStoreCRUD(t *testing.T) { + server := storagetest.NewEtcdTestServer(t) + client := server.Client(t) + + t.Run("Create and GetByKey with database scope", func(t *testing.T) { + store := task.NewTaskStore(client, uuid.NewString()) + + tsk, err := task.NewTask(task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "my-database", + Type: task.TypeCreate, + }) + require.NoError(t, err) + + err = store.Create(&task.StoredTask{Task: tsk}).Exec(t.Context()) + require.NoError(t, err) + + stored, err := store.GetByKey(task.ScopeDatabase, "my-database", tsk.TaskID).Exec(t.Context()) + require.NoError(t, err) + assert.Equal(t, tsk.TaskID, stored.Task.TaskID) + assert.Equal(t, task.ScopeDatabase, stored.Task.Scope) + assert.Equal(t, "my-database", stored.Task.EntityID) + }) + + t.Run("Create and GetByKey with host scope", func(t *testing.T) { + store := task.NewTaskStore(client, uuid.NewString()) + + tsk, err := task.NewTask(task.Options{ + Scope: task.ScopeHost, + HostID: "host-1", + Type: task.TypeRemoveHost, + }) + require.NoError(t, err) + + err = store.Create(&task.StoredTask{Task: tsk}).Exec(t.Context()) + require.NoError(t, err) + + stored, err := store.GetByKey(task.ScopeHost, "host-1", tsk.TaskID).Exec(t.Context()) + require.NoError(t, err) + assert.Equal(t, tsk.TaskID, stored.Task.TaskID) + assert.Equal(t, task.ScopeHost, stored.Task.Scope) + assert.Equal(t, "host-1", stored.Task.EntityID) + }) + + t.Run("Update task", func(t *testing.T) { + store := task.NewTaskStore(client, uuid.NewString()) + + tsk, err := task.NewTask(task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "my-database", + Type: task.TypeCreate, + }) + require.NoError(t, err) + + err = store.Create(&task.StoredTask{Task: tsk}).Exec(t.Context()) + require.NoError(t, err) + + // Get the stored task to update it + stored, err := store.GetByKey(task.ScopeDatabase, "my-database", tsk.TaskID).Exec(t.Context()) + require.NoError(t, err) + + // Update the task + stored.Task.Start() + err = store.Update(stored).Exec(t.Context()) + require.NoError(t, err) + + // Verify update + updated, err := store.GetByKey(task.ScopeDatabase, "my-database", tsk.TaskID).Exec(t.Context()) + require.NoError(t, err) + assert.Equal(t, task.StatusRunning, updated.Task.Status) + }) +} + +func TestTaskStoreGetAllByEntity(t *testing.T) { + server := storagetest.NewEtcdTestServer(t) + client := server.Client(t) + + t.Run("GetAllByEntity with database scope", func(t *testing.T) { + store := task.NewTaskStore(client, uuid.NewString()) + + // Create tasks for two different databases + for i := 0; i < 3; i++ { + tsk, err := task.NewTask(task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-1", + Type: task.TypeCreate, + }) + require.NoError(t, err) + err = store.Create(&task.StoredTask{Task: tsk}).Exec(t.Context()) + require.NoError(t, err) + } + + for i := 0; i < 2; i++ { + tsk, err := task.NewTask(task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-2", + Type: task.TypeCreate, + }) + require.NoError(t, err) + err = store.Create(&task.StoredTask{Task: tsk}).Exec(t.Context()) + require.NoError(t, err) + } + + // Get all tasks for database-1 + tasks, err := store.GetAllByEntity(task.ScopeDatabase, "database-1", task.TaskListOptions{}).Exec(t.Context()) + require.NoError(t, err) + assert.Len(t, tasks, 3) + for _, stored := range tasks { + assert.Equal(t, "database-1", stored.Task.EntityID) + assert.Equal(t, task.ScopeDatabase, stored.Task.Scope) + } + + // Get all tasks for database-2 + tasks, err = store.GetAllByEntity(task.ScopeDatabase, "database-2", task.TaskListOptions{}).Exec(t.Context()) + require.NoError(t, err) + assert.Len(t, tasks, 2) + for _, stored := range tasks { + assert.Equal(t, "database-2", stored.Task.EntityID) + } + }) + + t.Run("GetAllByEntity with host scope", func(t *testing.T) { + store := task.NewTaskStore(client, uuid.NewString()) + + // Create tasks for a host + for i := 0; i < 2; i++ { + tsk, err := task.NewTask(task.Options{ + Scope: task.ScopeHost, + HostID: "host-1", + Type: task.TypeRemoveHost, + }) + require.NoError(t, err) + err = store.Create(&task.StoredTask{Task: tsk}).Exec(t.Context()) + require.NoError(t, err) + } + + // Get all tasks for host-1 + tasks, err := store.GetAllByEntity(task.ScopeHost, "host-1", task.TaskListOptions{}).Exec(t.Context()) + require.NoError(t, err) + assert.Len(t, tasks, 2) + for _, stored := range tasks { + assert.Equal(t, "host-1", stored.Task.EntityID) + assert.Equal(t, task.ScopeHost, stored.Task.Scope) + } + }) +} + +func TestTaskStoreDelete(t *testing.T) { + server := storagetest.NewEtcdTestServer(t) + client := server.Client(t) + + t.Run("DeleteByEntity with database scope", func(t *testing.T) { + store := task.NewTaskStore(client, uuid.NewString()) + + // Create tasks + tsk1, err := task.NewTask(task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-1", + Type: task.TypeCreate, + }) + require.NoError(t, err) + err = store.Create(&task.StoredTask{Task: tsk1}).Exec(t.Context()) + require.NoError(t, err) + + tsk2, err := task.NewTask(task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database-2", + Type: task.TypeCreate, + }) + require.NoError(t, err) + err = store.Create(&task.StoredTask{Task: tsk2}).Exec(t.Context()) + require.NoError(t, err) + + // Delete database-1 tasks + _, err = store.DeleteByEntity(task.ScopeDatabase, "database-1").Exec(t.Context()) + require.NoError(t, err) + + // Verify database-1 tasks are gone + tasks, err := store.GetAllByEntity(task.ScopeDatabase, "database-1", task.TaskListOptions{}).Exec(t.Context()) + require.NoError(t, err) + assert.Len(t, tasks, 0) + + // Verify database-2 tasks still exist + tasks, err = store.GetAllByEntity(task.ScopeDatabase, "database-2", task.TaskListOptions{}).Exec(t.Context()) + require.NoError(t, err) + assert.Len(t, tasks, 1) + }) + + t.Run("DeleteByEntity with host scope", func(t *testing.T) { + store := task.NewTaskStore(client, uuid.NewString()) + + tsk, err := task.NewTask(task.Options{ + Scope: task.ScopeHost, + HostID: "host-1", + Type: task.TypeRemoveHost, + }) + require.NoError(t, err) + err = store.Create(&task.StoredTask{Task: tsk}).Exec(t.Context()) + require.NoError(t, err) + + // Delete host tasks + _, err = store.DeleteByEntity(task.ScopeHost, "host-1").Exec(t.Context()) + require.NoError(t, err) + + // Verify tasks are gone + tasks, err := store.GetAllByEntity(task.ScopeHost, "host-1", task.TaskListOptions{}).Exec(t.Context()) + require.NoError(t, err) + assert.Len(t, tasks, 0) + }) +} + func TestTaskStore(t *testing.T) { server := storagetest.NewEtcdTestServer(t) client := server.Client(t) @@ -20,24 +263,26 @@ func TestTaskStore(t *testing.T) { // ID A is a prefix of host ID B. store := task.NewTaskStore(client, uuid.NewString()) - err := store.Create(&task.StoredTask{ - Task: &task.Task{ - DatabaseID: "database", - TaskID: uuid.New(), - }, - }).Exec(t.Context()) + tsk, err := task.NewTask(task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database", + Type: task.TypeCreate, + }) + require.NoError(t, err) + err = store.Create(&task.StoredTask{Task: tsk}).Exec(t.Context()) require.NoError(t, err) - err = store.Create(&task.StoredTask{ - Task: &task.Task{ - DatabaseID: "database2", - TaskID: uuid.New(), - }, - }).Exec(t.Context()) + tsk2, err := task.NewTask(task.Options{ + Scope: task.ScopeDatabase, + DatabaseID: "database2", + Type: task.TypeCreate, + }) + require.NoError(t, err) + err = store.Create(&task.StoredTask{Task: tsk2}).Exec(t.Context()) require.NoError(t, err) res, err := store. - GetAllByDatabaseID("database", task.TaskListOptions{}). + GetAllByEntity(task.ScopeDatabase, "database", task.TaskListOptions{}). Exec(t.Context()) require.NoError(t, err) @@ -45,11 +290,11 @@ func TestTaskStore(t *testing.T) { assert.Equal(t, "database", res[0].Task.DatabaseID) // Delete by prefix and ensure only the 'database' tasks are deleted. - _, err = store.DeleteByDatabaseID("database").Exec(t.Context()) + _, err = store.DeleteByEntity(task.ScopeDatabase, "database").Exec(t.Context()) require.NoError(t, err) res, err = store. - GetAllByDatabaseID("database2", task.TaskListOptions{}). + GetAllByEntity(task.ScopeDatabase, "database2", task.TaskListOptions{}). Exec(t.Context()) require.NoError(t, err) diff --git a/server/internal/workflows/activities/apply_event.go b/server/internal/workflows/activities/apply_event.go index e38a7a3c..052b7f3b 100644 --- a/server/internal/workflows/activities/apply_event.go +++ b/server/internal/workflows/activities/apply_event.go @@ -102,21 +102,21 @@ func (a *Activities) ApplyEvent(ctx context.Context, input *ApplyEventInput) (*A resultCh <- fmt.Errorf("failed to refresh resource %s: %w", r.Identifier().String(), err) } case resource.EventTypeCreate: - err := a.logEvent(ctxWithCancel, input.DatabaseID, input.TaskID, "creating", r, func() error { + err := a.logResourceEvent(ctxWithCancel, input.DatabaseID, input.TaskID, "creating", r, func() error { return r.Create(ctxWithCancel, rc) }) if err != nil { resultCh <- fmt.Errorf("failed to create resource %s: %w", r.Identifier().String(), err) } case resource.EventTypeUpdate: - err := a.logEvent(ctxWithCancel, input.DatabaseID, input.TaskID, "updating", r, func() error { + err := a.logResourceEvent(ctxWithCancel, input.DatabaseID, input.TaskID, "updating", r, func() error { return r.Update(ctxWithCancel, rc) }) if err != nil { resultCh <- fmt.Errorf("failed to update resource %s: %w", r.Identifier().String(), err) } case resource.EventTypeDelete: - err := a.logEvent(ctxWithCancel, input.DatabaseID, input.TaskID, "deleting", r, func() error { + err := a.logResourceEvent(ctxWithCancel, input.DatabaseID, input.TaskID, "deleting", r, func() error { return r.Delete(ctxWithCancel, rc) }) if err != nil { @@ -146,7 +146,7 @@ func (a *Activities) ApplyEvent(ctx context.Context, input *ApplyEventInput) (*A }, nil } -func (a *Activities) logEvent( +func (a *Activities) logResourceEvent( ctx context.Context, databaseID string, taskID uuid.UUID, @@ -162,7 +162,7 @@ func (a *Activities) logEvent( } // Currying AddLogEntry log := func(entry task.LogEntry) error { - return a.TaskSvc.AddLogEntry(ctx, databaseID, taskID, entry) + return a.TaskSvc.AddLogEntry(ctx, task.ScopeDatabase, databaseID, taskID, entry) } err := log(task.LogEntry{ Message: fmt.Sprintf("%s resource %s", verb, resourceIdentifier), diff --git a/server/internal/workflows/activities/create_pgbackrest_backup.go b/server/internal/workflows/activities/create_pgbackrest_backup.go index 901594eb..6fcb424c 100644 --- a/server/internal/workflows/activities/create_pgbackrest_backup.go +++ b/server/internal/workflows/activities/create_pgbackrest_backup.go @@ -82,7 +82,7 @@ func (a *Activities) CreatePgBackRestBackup(ctx context.Context, input *CreatePg } }() - taskLogWriter := task.NewTaskLogWriter(ctx, taskSvc, input.DatabaseID, input.TaskID) + taskLogWriter := task.NewTaskLogWriter(ctx, taskSvc, task.ScopeDatabase, input.DatabaseID, input.TaskID) defer taskLogWriter.Close() err = orch.CreatePgBackRestBackup(ctx, taskLogWriter, input.InstanceID, input.BackupOptions) diff --git a/server/internal/workflows/activities/log_task_event.go b/server/internal/workflows/activities/log_task_event.go index 260374df..931077db 100644 --- a/server/internal/workflows/activities/log_task_event.go +++ b/server/internal/workflows/activities/log_task_event.go @@ -13,9 +13,10 @@ import ( ) type LogTaskEventInput struct { - DatabaseID string `json:"database_id"` - TaskID uuid.UUID `json:"task_id"` - Entries []task.LogEntry `json:"messages"` + Scope task.Scope `json:"scope"` + EntityID string `json:"entity_id"` + TaskID uuid.UUID `json:"task_id"` + Entries []task.LogEntry `json:"messages"` } type LogTaskEventOutput struct{} @@ -34,11 +35,14 @@ func (a *Activities) ExecuteLogTaskEvent( } func (a *Activities) LogTaskEvent(ctx context.Context, input *LogTaskEventInput) (*LogTaskEventOutput, error) { - logger := activity.Logger(ctx).With("database_id", input.DatabaseID) + logger := activity.Logger(ctx).With( + "scope", input.Scope, + "entity_id", input.EntityID, + ) logger.Debug("logging task event") for _, entry := range input.Entries { - err := a.TaskSvc.AddLogEntry(ctx, input.DatabaseID, input.TaskID, entry) + err := a.TaskSvc.AddLogEntry(ctx, input.Scope, input.EntityID, input.TaskID, entry) if err != nil { return nil, fmt.Errorf("failed to add task log entry: %w", err) } diff --git a/server/internal/workflows/activities/update_task.go b/server/internal/workflows/activities/update_task.go index 29a0814b..353cb571 100644 --- a/server/internal/workflows/activities/update_task.go +++ b/server/internal/workflows/activities/update_task.go @@ -14,7 +14,8 @@ import ( ) type UpdateTaskInput struct { - DatabaseID string `json:"database_id"` + Scope task.Scope `json:"scope"` + EntityID string `json:"entity_id"` TaskID uuid.UUID `json:"task_id"` UpdateOptions task.UpdateOptions `json:"update_options,omitempty"` } @@ -36,7 +37,8 @@ func (a *Activities) ExecuteUpdateTask( func (a *Activities) UpdateTask(ctx context.Context, input *UpdateTaskInput) (*UpdateTaskOutput, error) { logger := activity.Logger(ctx).With( - "database_id", input.DatabaseID, + "scope", input.Scope, + "entity_id", input.EntityID, "task_id", input.TaskID.String(), ) logger.Info("updating task") @@ -46,7 +48,7 @@ func (a *Activities) UpdateTask(ctx context.Context, input *UpdateTaskInput) (*U return nil, err } - t, err := service.GetTask(ctx, input.DatabaseID, input.TaskID) + t, err := service.GetTask(ctx, input.Scope, input.EntityID, input.TaskID) if err != nil { return nil, fmt.Errorf("failed to get task: %w", err) } diff --git a/server/internal/workflows/common.go b/server/internal/workflows/common.go index 7e5d899d..b098abae 100644 --- a/server/internal/workflows/common.go +++ b/server/internal/workflows/common.go @@ -150,7 +150,8 @@ func (w *Workflows) updateTask( func (w *Workflows) logTaskEvent( ctx workflow.Context, - databaseID string, + scope task.Scope, + entityID string, taskID uuid.UUID, entries ...task.LogEntry, ) error { @@ -160,9 +161,10 @@ func (w *Workflows) logTaskEvent( _, err := w.Activities. ExecuteLogTaskEvent(ctx, &activities.LogTaskEventInput{ - DatabaseID: databaseID, - TaskID: taskID, - Entries: entries, + Scope: scope, + EntityID: entityID, + TaskID: taskID, + Entries: entries, }).Get(ctx) if err != nil { return fmt.Errorf("failed to log task event: %w", err) @@ -173,22 +175,25 @@ func (w *Workflows) logTaskEvent( func (w *Workflows) cancelTask( cleanupCtx workflow.Context, - databaseID string, + scope task.Scope, + entityID string, taskID uuid.UUID, logger *slog.Logger) { updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: databaseID, + Scope: scope, + EntityID: entityID, TaskID: taskID, UpdateOptions: task.UpdateCancel(), } _ = w.updateTask(cleanupCtx, logger, updateTaskInput) - err := w.logTaskEvent(cleanupCtx, databaseID, taskID, task.LogEntry{ + err := w.logTaskEvent(cleanupCtx, scope, entityID, taskID, task.LogEntry{ Message: "task successfully canceled", Fields: map[string]any{"status": "canceled"}, }) - logger.With("error", err).Error("failed to log task event") - + if err != nil { + logger.With("error", err).Error("failed to log task event") + } } func (w *Workflows) getNodeResources( diff --git a/server/internal/workflows/create_pgbackrest_backup.go b/server/internal/workflows/create_pgbackrest_backup.go index c5a7d2fd..c460b23c 100644 --- a/server/internal/workflows/create_pgbackrest_backup.go +++ b/server/internal/workflows/create_pgbackrest_backup.go @@ -41,7 +41,7 @@ func (w *Workflows) CreatePgBackRestBackup(ctx workflow.Context, input *CreatePg if errors.Is(ctx.Err(), workflow.Canceled) { logger.Warn("workflow was canceled") cleanupCtx := workflow.NewDisconnectedContext(ctx) - w.cancelTask(cleanupCtx, input.DatabaseID, input.TaskID, logger) + w.cancelTask(cleanupCtx, task.ScopeDatabase, input.DatabaseID, input.TaskID, logger) } }() @@ -51,7 +51,8 @@ func (w *Workflows) CreatePgBackRestBackup(ctx workflow.Context, input *CreatePg logger.With("error", cause).Error("failed to create pgbackrest backup") updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateFail(cause), } @@ -71,7 +72,8 @@ func (w *Workflows) CreatePgBackRestBackup(ctx workflow.Context, input *CreatePg updateOptions := task.UpdateStart() updateOptions.InstanceID = utils.PointerTo(instance.InstanceID) updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: updateOptions, } @@ -122,7 +124,8 @@ func (w *Workflows) CreatePgBackRestBackup(ctx workflow.Context, input *CreatePg } updateTaskInput = &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateComplete(), } diff --git a/server/internal/workflows/delete_database.go b/server/internal/workflows/delete_database.go index 3341b998..cd80b328 100644 --- a/server/internal/workflows/delete_database.go +++ b/server/internal/workflows/delete_database.go @@ -38,7 +38,7 @@ func (w *Workflows) DeleteDatabase(ctx workflow.Context, input *DeleteDatabaseIn if stateErr != nil { logger.With("error", stateErr).Error("failed to update database state") } - w.cancelTask(cleanupCtx, input.DatabaseID, input.TaskID, logger) + w.cancelTask(cleanupCtx, task.ScopeDatabase, input.DatabaseID, input.TaskID, logger) } }() @@ -58,7 +58,8 @@ func (w *Workflows) DeleteDatabase(ctx workflow.Context, input *DeleteDatabaseIn } updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateFail(cause), } @@ -68,7 +69,8 @@ func (w *Workflows) DeleteDatabase(ctx workflow.Context, input *DeleteDatabaseIn } updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateStart(), } @@ -105,7 +107,8 @@ func (w *Workflows) DeleteDatabase(ctx workflow.Context, input *DeleteDatabaseIn } updateTaskInput = &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateComplete(), } diff --git a/server/internal/workflows/failover.go b/server/internal/workflows/failover.go index 807fe41b..1e1764b2 100644 --- a/server/internal/workflows/failover.go +++ b/server/internal/workflows/failover.go @@ -38,14 +38,15 @@ func (w *Workflows) Failover(ctx workflow.Context, in *FailoverInput) (*Failover if errors.Is(ctx.Err(), workflow.Canceled) { logger.Warn("workflow cancelled; running cleanup") cleanupCtx := workflow.NewDisconnectedContext(ctx) - w.cancelTask(cleanupCtx, in.DatabaseID, in.TaskID, logger) + w.cancelTask(cleanupCtx, task.ScopeDatabase, in.DatabaseID, in.TaskID, logger) } }() handleError := func(cause error) error { logger.With("error", cause).Error("failover failed") updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: in.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: in.DatabaseID, TaskID: in.TaskID, UpdateOptions: task.UpdateFail(cause), } @@ -54,7 +55,8 @@ func (w *Workflows) Failover(ctx workflow.Context, in *FailoverInput) (*Failover } startUpdate := &activities.UpdateTaskInput{ - DatabaseID: in.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: in.DatabaseID, TaskID: in.TaskID, UpdateOptions: task.UpdateStart(), } @@ -138,7 +140,8 @@ func (w *Workflows) Failover(ctx workflow.Context, in *FailoverInput) (*Failover if candidateID == leaderInstanceID { logger.Info("candidate is already the leader; skipping failover", "candidate", candidateID) completeUpdate := &activities.UpdateTaskInput{ - DatabaseID: in.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: in.DatabaseID, TaskID: in.TaskID, UpdateOptions: task.UpdateComplete(), } @@ -160,7 +163,8 @@ func (w *Workflows) Failover(ctx workflow.Context, in *FailoverInput) (*Failover } completeUpdate := &activities.UpdateTaskInput{ - DatabaseID: in.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: in.DatabaseID, TaskID: in.TaskID, UpdateOptions: task.UpdateComplete(), } diff --git a/server/internal/workflows/pgbackrest_restore.go b/server/internal/workflows/pgbackrest_restore.go index 95fc029a..a1b92d5c 100644 --- a/server/internal/workflows/pgbackrest_restore.go +++ b/server/internal/workflows/pgbackrest_restore.go @@ -31,7 +31,7 @@ func (w *Workflows) PgBackRestRestore(ctx workflow.Context, input *PgBackRestRes if errors.Is(ctx.Err(), workflow.Canceled) { logger.Warn("workflow was canceled") cleanupCtx := workflow.NewDisconnectedContext(ctx) - w.cancelTask(cleanupCtx, input.Spec.DatabaseID, input.TaskID, logger) + w.cancelTask(cleanupCtx, task.ScopeDatabase, input.Spec.DatabaseID, input.TaskID, logger) } }() @@ -50,7 +50,8 @@ func (w *Workflows) PgBackRestRestore(ctx workflow.Context, input *PgBackRestRes } updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.Spec.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.Spec.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateStart(), } @@ -103,7 +104,8 @@ func (w *Workflows) PgBackRestRestore(ctx workflow.Context, input *PgBackRestRes } updateTaskInput = &activities.UpdateTaskInput{ - DatabaseID: input.Spec.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.Spec.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateComplete(), } @@ -135,8 +137,9 @@ func (w *Workflows) handlePgBackRestRestoreFailed( } for _, taskID := range input.NodeTaskIDs { updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.Spec.DatabaseID, - TaskID: taskID, + Scope: task.ScopeDatabase, + EntityID: input.Spec.DatabaseID, + TaskID: taskID, UpdateOptions: task.UpdateFail( fmt.Errorf("parent task failed: %w", cause), ), @@ -145,7 +148,8 @@ func (w *Workflows) handlePgBackRestRestoreFailed( } updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.Spec.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.Spec.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateFail(cause), } diff --git a/server/internal/workflows/refresh_current_state.go b/server/internal/workflows/refresh_current_state.go index cdebe3a7..a506b3d5 100644 --- a/server/internal/workflows/refresh_current_state.go +++ b/server/internal/workflows/refresh_current_state.go @@ -63,6 +63,7 @@ func (w *Workflows) RefreshCurrentState(ctx workflow.Context, input *RefreshCurr start := workflow.Now(ctx) err = w.logTaskEvent(ctx, + task.ScopeDatabase, input.DatabaseID, input.TaskID, task.LogEntry{ @@ -77,6 +78,7 @@ func (w *Workflows) RefreshCurrentState(ctx workflow.Context, input *RefreshCurr } duration := workflow.Now(ctx).Sub(start) err = w.logTaskEvent(ctx, + task.ScopeDatabase, input.DatabaseID, input.TaskID, task.LogEntry{ diff --git a/server/internal/workflows/restart_instance.go b/server/internal/workflows/restart_instance.go index 83efc99c..a35c64ec 100644 --- a/server/internal/workflows/restart_instance.go +++ b/server/internal/workflows/restart_instance.go @@ -44,7 +44,7 @@ func (w *Workflows) RestartInstance(ctx workflow.Context, input *RestartInstance if stateErr != nil { logger.With("error", stateErr).Error("failed to update database state") } - w.cancelTask(cleanupCtx, input.DatabaseID, input.TaskID, logger) + w.cancelTask(cleanupCtx, task.ScopeDatabase, input.DatabaseID, input.TaskID, logger) } }() @@ -52,7 +52,8 @@ func (w *Workflows) RestartInstance(ctx workflow.Context, input *RestartInstance logger.With("error", cause).Error("failed to restart instance") updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateFail(cause), } @@ -64,7 +65,8 @@ func (w *Workflows) RestartInstance(ctx workflow.Context, input *RestartInstance } updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateStart(), } @@ -83,7 +85,8 @@ func (w *Workflows) RestartInstance(ctx workflow.Context, input *RestartInstance } updateTaskInput = &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateComplete(), } diff --git a/server/internal/workflows/service.go b/server/internal/workflows/service.go index a58811ee..4964db29 100644 --- a/server/internal/workflows/service.go +++ b/server/internal/workflows/service.go @@ -48,10 +48,11 @@ func (s *Service) CreateDatabase(ctx context.Context, spec *database.Spec) (*tas databaseID := spec.DatabaseID // Clear out any old tasks. This can happen if you were to recreate a // database with the same ID. - if err := s.taskSvc.DeleteAllTasks(ctx, databaseID); err != nil { + if err := s.taskSvc.DeleteAllTasks(ctx, task.ScopeDatabase, databaseID); err != nil { return nil, fmt.Errorf("failed to delete old task logs: %w", err) } t, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, DatabaseID: databaseID, Type: task.TypeCreate, }) @@ -73,6 +74,7 @@ func (s *Service) CreateDatabase(ctx context.Context, spec *database.Spec) (*tas func (s *Service) UpdateDatabase(ctx context.Context, spec *database.Spec, forceUpdate bool, removeHosts ...string) (*task.Task, error) { databaseID := spec.DatabaseID t, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, DatabaseID: databaseID, Type: task.TypeUpdate, }) @@ -95,6 +97,7 @@ func (s *Service) UpdateDatabase(ctx context.Context, spec *database.Spec, force func (s *Service) DeleteDatabase(ctx context.Context, databaseID string) (*task.Task, error) { t, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, DatabaseID: databaseID, Type: task.TypeDelete, }) @@ -122,6 +125,7 @@ func (s *Service) CreatePgBackRestBackup( backupOptions *pgbackrest.BackupOptions, ) (*task.Task, error) { t, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, DatabaseID: databaseID, Type: task.TypeNodeBackup, }) @@ -153,6 +157,7 @@ func (s *Service) PgBackRestRestore( databaseID := spec.DatabaseID t, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, DatabaseID: databaseID, Type: task.TypeRestore, }) @@ -165,6 +170,7 @@ func (s *Service) PgBackRestRestore( nodeTasks := []*task.Task{} for _, node := range targetNodes { nt, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, ParentID: t.TaskID, DatabaseID: databaseID, NodeName: node, @@ -218,7 +224,7 @@ func (s *Service) createWorkflow(ctx context.Context, t *task.Task, wf workflow. func (s *Service) abortTasks(ctx context.Context, tasks ...*task.Task) { for _, t := range tasks { - err := s.taskSvc.DeleteTask(ctx, t.DatabaseID, t.TaskID) + err := s.taskSvc.DeleteTask(ctx, t.Scope, t.EntityID, t.TaskID) if err != nil { s.logger.Err(err). Str("database_id", t.DatabaseID). @@ -263,6 +269,7 @@ func (s *Service) ValidateSpec(ctx context.Context, input *ValidateSpecInput) (* func (s *Service) RestartInstance(ctx context.Context, input *RestartInstanceInput) (*task.Task, error) { t, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, DatabaseID: input.DatabaseID, InstanceID: input.InstanceID, Type: task.TypeRestartInstance, @@ -281,6 +288,7 @@ func (s *Service) RestartInstance(ctx context.Context, input *RestartInstanceInp func (s *Service) StopInstance(ctx context.Context, input *StopInstanceInput) (*task.Task, error) { t, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, DatabaseID: input.DatabaseID, InstanceID: input.InstanceID, HostID: input.HostID, @@ -300,6 +308,7 @@ func (s *Service) StopInstance(ctx context.Context, input *StopInstanceInput) (* func (s *Service) StartInstance(ctx context.Context, input *StartInstanceInput) (*task.Task, error) { t, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, DatabaseID: input.DatabaseID, InstanceID: input.InstanceID, HostID: input.HostID, @@ -317,13 +326,13 @@ func (s *Service) StartInstance(ctx context.Context, input *StartInstanceInput) return t, nil } -func (s *Service) CancelDatabaseTask(ctx context.Context, DatabaseID string, taskID uuid.UUID) (*task.Task, error) { - t, err := s.taskSvc.GetTask(ctx, DatabaseID, taskID) +func (s *Service) CancelDatabaseTask(ctx context.Context, databaseID string, taskID uuid.UUID) (*task.Task, error) { + t, err := s.taskSvc.GetTask(ctx, task.ScopeDatabase, databaseID, taskID) if err != nil { return nil, fmt.Errorf("failed to retrieve task from database : %w", err) } if t.WorkflowInstanceID == "" { - return nil, fmt.Errorf("no worflow instances associated with task") + return nil, fmt.Errorf("no workflow instances associated with task") } t.Status = task.StatusCanceling @@ -331,17 +340,17 @@ func (s *Service) CancelDatabaseTask(ctx context.Context, DatabaseID string, tas if err != nil { return t, fmt.Errorf("failed to update task status to canceling %w", err) } - _ = s.taskSvc.AddLogEntry(ctx, DatabaseID, taskID, task.LogEntry{ + _ = s.taskSvc.AddLogEntry(ctx, task.ScopeDatabase, databaseID, taskID, task.LogEntry{ Message: "task is canceling", Fields: map[string]any{"status": "canceling"}, }) - wrkflw_instance := core.WorkflowInstance{ + workflowInstance := core.WorkflowInstance{ InstanceID: t.WorkflowInstanceID, ExecutionID: t.WorkflowExecutionID, } - if err := s.client.CancelWorkflowInstance(ctx, &wrkflw_instance); err != nil { + if err := s.client.CancelWorkflowInstance(ctx, &workflowInstance); err != nil { return nil, fmt.Errorf("failed to cancel workflow instance %w", err) } @@ -351,6 +360,7 @@ func (s *Service) CancelDatabaseTask(ctx context.Context, DatabaseID string, tas func (s *Service) SwitchoverDatabaseNode(ctx context.Context, input *SwitchoverInput) (*task.Task, error) { t, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, DatabaseID: input.DatabaseID, InstanceID: input.CandidateInstanceID, NodeName: input.NodeName, @@ -371,6 +381,7 @@ func (s *Service) SwitchoverDatabaseNode(ctx context.Context, input *SwitchoverI func (s *Service) FailoverDatabaseNode(ctx context.Context, input *FailoverInput) (*task.Task, error) { t, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, DatabaseID: input.DatabaseID, InstanceID: input.CandidateInstanceID, NodeName: input.NodeName, @@ -397,6 +408,7 @@ func (s *Service) RemoveHost(ctx context.Context, input *RemoveHostInput) ([]*ta for _, dbInput := range input.UpdateDatabaseInputs { dt, err := s.taskSvc.CreateTask(ctx, task.Options{ + Scope: task.ScopeDatabase, DatabaseID: dbInput.Spec.DatabaseID, Type: task.TypeUpdate, }) diff --git a/server/internal/workflows/start_instance.go b/server/internal/workflows/start_instance.go index 34ca2b9b..1ff83c9f 100644 --- a/server/internal/workflows/start_instance.go +++ b/server/internal/workflows/start_instance.go @@ -30,7 +30,8 @@ func (w *Workflows) StartInstance(ctx workflow.Context, input *StartInstanceInpu logger.With("error", cause).Error("failed to start instance") updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateFail(cause), } @@ -42,7 +43,8 @@ func (w *Workflows) StartInstance(ctx workflow.Context, input *StartInstanceInpu } updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateStart(), } @@ -63,7 +65,8 @@ func (w *Workflows) StartInstance(ctx workflow.Context, input *StartInstanceInpu } updateTaskInput = &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateComplete(), } diff --git a/server/internal/workflows/stop_instance.go b/server/internal/workflows/stop_instance.go index d6136444..422499b4 100644 --- a/server/internal/workflows/stop_instance.go +++ b/server/internal/workflows/stop_instance.go @@ -30,7 +30,8 @@ func (w *Workflows) StopInstance(ctx workflow.Context, input *StopInstanceInput) logger.With("error", cause).Error("failed to stop instance") updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateFail(cause), } @@ -42,7 +43,8 @@ func (w *Workflows) StopInstance(ctx workflow.Context, input *StopInstanceInput) } updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateStart(), } @@ -63,7 +65,8 @@ func (w *Workflows) StopInstance(ctx workflow.Context, input *StopInstanceInput) } updateTaskInput = &activities.UpdateTaskInput{ - DatabaseID: input.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateComplete(), } diff --git a/server/internal/workflows/switchover.go b/server/internal/workflows/switchover.go index a6a646f0..d1e1eab8 100644 --- a/server/internal/workflows/switchover.go +++ b/server/internal/workflows/switchover.go @@ -54,14 +54,15 @@ func (w *Workflows) Switchover(ctx workflow.Context, in *SwitchoverInput) (*Swit } } } - w.cancelTask(cleanupCtx, in.DatabaseID, in.TaskID, logger) + w.cancelTask(cleanupCtx, task.ScopeDatabase, in.DatabaseID, in.TaskID, logger) } }() handleError := func(cause error) error { logger.With("error", cause).Error("switchover failed") updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: in.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: in.DatabaseID, TaskID: in.TaskID, UpdateOptions: task.UpdateFail(cause), } @@ -71,7 +72,8 @@ func (w *Workflows) Switchover(ctx workflow.Context, in *SwitchoverInput) (*Swit // mark task as running startUpdate := &activities.UpdateTaskInput{ - DatabaseID: in.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: in.DatabaseID, TaskID: in.TaskID, UpdateOptions: task.UpdateStart(), } @@ -138,7 +140,8 @@ func (w *Workflows) Switchover(ctx workflow.Context, in *SwitchoverInput) (*Swit if candidateID == leaderInstanceID { logger.Info("candidate is already the leader; skipping switchover", "candidate", candidateID) completeUpdate := &activities.UpdateTaskInput{ - DatabaseID: in.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: in.DatabaseID, TaskID: in.TaskID, UpdateOptions: task.UpdateComplete(), } @@ -161,7 +164,8 @@ func (w *Workflows) Switchover(ctx workflow.Context, in *SwitchoverInput) (*Swit } completeUpdate := &activities.UpdateTaskInput{ - DatabaseID: in.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: in.DatabaseID, TaskID: in.TaskID, UpdateOptions: task.UpdateComplete(), } diff --git a/server/internal/workflows/update_database.go b/server/internal/workflows/update_database.go index 25be73ca..e1ae9a72 100644 --- a/server/internal/workflows/update_database.go +++ b/server/internal/workflows/update_database.go @@ -42,7 +42,7 @@ func (w *Workflows) UpdateDatabase(ctx workflow.Context, input *UpdateDatabaseIn logger.With("error", err).Error("failed to update database state ") } - w.cancelTask(cleanupCtx, input.Spec.DatabaseID, input.TaskID, logger) + w.cancelTask(cleanupCtx, task.ScopeDatabase, input.Spec.DatabaseID, input.TaskID, logger) } }() @@ -64,7 +64,8 @@ func (w *Workflows) UpdateDatabase(ctx workflow.Context, input *UpdateDatabaseIn } updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.Spec.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.Spec.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateFail(cause), } @@ -74,7 +75,8 @@ func (w *Workflows) UpdateDatabase(ctx workflow.Context, input *UpdateDatabaseIn } updateTaskInput := &activities.UpdateTaskInput{ - DatabaseID: input.Spec.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.Spec.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateStart(), } @@ -129,7 +131,8 @@ func (w *Workflows) UpdateDatabase(ctx workflow.Context, input *UpdateDatabaseIn } updateTaskInput = &activities.UpdateTaskInput{ - DatabaseID: input.Spec.DatabaseID, + Scope: task.ScopeDatabase, + EntityID: input.Spec.DatabaseID, TaskID: input.TaskID, UpdateOptions: task.UpdateComplete(), }