diff --git a/proxmox/nodes/tasks/client.go b/proxmox/nodes/tasks/client.go index b48105f4..d7725185 100644 --- a/proxmox/nodes/tasks/client.go +++ b/proxmox/nodes/tasks/client.go @@ -8,6 +8,7 @@ package tasks import ( "fmt" + "net/url" "github.com/bpg/terraform-provider-proxmox/proxmox/api" ) @@ -18,8 +19,17 @@ type Client struct { } // ExpandPath expands a path relative to the client's base path. -func (c *Client) ExpandPath(path string) string { - return c.Client.ExpandPath( - fmt.Sprintf("tasks/%s", path), - ) +func (c *Client) ExpandPath(_ string) string { + panic("ExpandPath of tasks.Client must not be used. Use BuildPath instead.") +} + +// BuildPath builds a path using information from Task ID. +func (c *Client) BuildPath(taskID string, path string) (string, error) { + tid, err := ParseTaskID(taskID) + if err != nil { + return "", err + } + + return fmt.Sprintf("nodes/%s/tasks/%s/%s", + url.PathEscape(tid.NodeName), url.PathEscape(taskID), url.PathEscape(path)), nil } diff --git a/proxmox/nodes/tasks/tasks.go b/proxmox/nodes/tasks/tasks.go index 7bccd86f..d21c683e 100644 --- a/proxmox/nodes/tasks/tasks.go +++ b/proxmox/nodes/tasks/tasks.go @@ -11,7 +11,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "time" "github.com/bpg/terraform-provider-proxmox/proxmox/api" @@ -21,10 +20,15 @@ import ( func (c *Client) GetTaskStatus(ctx context.Context, upid string) (*GetTaskStatusResponseData, error) { resBody := &GetTaskStatusResponseBody{} - err := c.DoRequest( + path, err := c.BuildPath(upid, "status") + if err != nil { + return nil, fmt.Errorf("error building path for task status: %w", err) + } + + err = c.DoRequest( ctx, http.MethodGet, - c.ExpandPath(fmt.Sprintf("%s/status", url.PathEscape(upid))), + path, nil, resBody, ) diff --git a/proxmox/nodes/tasks/tasks_types.go b/proxmox/nodes/tasks/tasks_types.go index 419b76eb..4cf80ee5 100644 --- a/proxmox/nodes/tasks/tasks_types.go +++ b/proxmox/nodes/tasks/tasks_types.go @@ -6,6 +6,13 @@ package tasks +import ( + "fmt" + "strconv" + "strings" + "time" +) + // GetTaskStatusResponseBody contains the body from a node get task status response. type GetTaskStatusResponseBody struct { Data *GetTaskStatusResponseData `json:"data,omitempty"` @@ -17,3 +24,63 @@ type GetTaskStatusResponseData struct { Status string `json:"status,omitempty"` ExitCode string `json:"exitstatus,omitempty"` } + +// TaskID contains the components of a PVE task ID. +type TaskID struct { + NodeName string + PID int + PStart int + StartTime time.Time + Type string + ID string + User string +} + +// ParseTaskID parses a task ID into its component parts. +// The task ID is expected to be in the format of: +// +// UPID:::::::@: +func ParseTaskID(taskID string) (TaskID, error) { + parts := strings.SplitN(taskID, ":", 9) + + if parts[0] != "UPID" || len(parts) < 8 { + return TaskID{}, fmt.Errorf("invalid task ID format: %s", taskID) + } + + if parts[1] == "" { + return TaskID{}, fmt.Errorf("missing node name in task ID: %s", taskID) + } + + pid, err := strconv.ParseInt(parts[2], 16, 32) + if err != nil { + return TaskID{}, fmt.Errorf("error parsing task ID: %w", err) + } + + pstart, err := strconv.ParseInt(parts[3], 16, 32) + if err != nil { + return TaskID{}, fmt.Errorf("error parsing pstart in task ID: %q: %w", taskID, err) + } + + stime, err := strconv.ParseInt(parts[4], 16, 32) + if err != nil { + return TaskID{}, fmt.Errorf("error parsing start time in task ID: %q: %w", taskID, err) + } + + if parts[5] == "" { + return TaskID{}, fmt.Errorf("missing task type in task ID: %q", taskID) + } + + if parts[7] == "" { + return TaskID{}, fmt.Errorf("missing user in task ID: %q", taskID) + } + + return TaskID{ + NodeName: parts[1], + PID: int(pid), + PStart: int(pstart), + StartTime: time.Unix(stime, 0).UTC(), + Type: parts[5], + ID: parts[6], + User: parts[7], + }, nil +} diff --git a/proxmox/nodes/tasks/tasks_types_test.go b/proxmox/nodes/tasks/tasks_types_test.go new file mode 100644 index 00000000..143d8d46 --- /dev/null +++ b/proxmox/nodes/tasks/tasks_types_test.go @@ -0,0 +1,92 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package tasks + +import ( + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestParseTaskID(t *testing.T) { + t.Parallel() + + stime, err := time.Parse(time.RFC3339, "2023-08-30T21:28:16-04:00") + require.NoError(t, err) + + stime = stime.UTC() + + tests := []struct { + name string + taskID string + want TaskID + wantErr bool + }{ + { + name: "imgcopy task", + taskID: "UPID:pve:00061CB3:010BA69C:64EFECB0:imgcopy::root@pam:", + want: TaskID{ + NodeName: "pve", + PID: 400563, + PStart: 17540764, + StartTime: stime, + Type: "imgcopy", + ID: "", + User: "root@pam", + }, + }, + { + name: "qmcreate task", + taskID: "UPID:pve:00061CB3:010BA69C:64EFECB0:qmcreate:101:root@pam:", + want: TaskID{ + NodeName: "pve", + PID: 400563, + PStart: 17540764, + StartTime: stime, + Type: "qmcreate", + ID: "101", + User: "root@pam", + }, + }, + { + name: "missing node", + taskID: "UPID::00061CB3:010BA69C:64EFECB0:qmcreate:101:root@pam:", + wantErr: true, + }, + { + name: "wrong ID format", + taskID: "blah", + wantErr: true, + }, + { + name: "missing pid", + taskID: "UPID:pve::010BA69C:64EFECB0:qmcreate:101:root@pam:", + wantErr: true, + }, + { + name: "missing parts", + taskID: "UPID:pve:00061CB3:010BA69C:64EFECB0::root@pam:", + wantErr: true, + }, + } + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := ParseTaskID(tt.taskID) + if (err != nil) != tt.wantErr { + t.Errorf("ParseTaskID() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseTaskID() got = %v, want %v", got, tt.want) + } + }) + } +}