From 150612bd1174e39d9366a8e03b5fafdd13970091 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Tue, 7 Dec 2021 17:23:29 -0500 Subject: [PATCH 01/34] Added the show Task Stage endpoint This endpoint will be used to retrieve run task results for the Run Tasks CLI feature. --- errors.go | 5 ++++ task_stages.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++ tfe.go | 2 ++ 3 files changed, 86 insertions(+) create mode 100644 task_stages.go diff --git a/errors.go b/errors.go index 91f4b92f9..0ffb4ac71 100644 --- a/errors.go +++ b/errors.go @@ -52,6 +52,11 @@ var ( // ErrInvalidRunID is returned when the run ID is invalid. ErrInvalidRunID = errors.New("invalid value for run ID") + // Task Stage errors + + //ErrInvalidTaskStageID is returned when the task stage ID is invalid. + ErrInvalidTaskStageID = errors.New("invalid value for task stage ID") + // ErrInvalidApplyID is returned when the apply ID is invalid. ErrInvalidApplyID = errors.New("invalid value for apply ID") diff --git a/task_stages.go b/task_stages.go new file mode 100644 index 000000000..112409428 --- /dev/null +++ b/task_stages.go @@ -0,0 +1,79 @@ +package tfe + +import ( + "context" + "fmt" + "time" +) + +type TaskStages interface { + Read(ctx context.Context, taskStageID string, options *TaskStageReadOptions) (*TaskStage, error) +} + +type taskStages struct { + client *Client +} + +type TaskStage struct { + ID string `jsonapi:"primary,task-stages"` + Stage string `jsonapi:"attr,stage"` + StatusTimestamps RunTaskStatusTimestamp `jsonapi:"attr,status-timestamps"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + + Run *Run `jsonapi:"relation,run"` + TaskResults []*TaskResult `jsonapi:"relation,task-results"` +} + +type RunTaskStatusTimestamp struct { + ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"` + RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` +} + +type TaskStageReadOptions struct { + Include string `url:"include"` +} + +func (s *taskStages) Read(ctx context.Context, taskStageID string, options *TaskStageReadOptions) (*TaskStage, error) { + if !validStringID(&taskStageID) { + return nil, ErrInvalidTaskStageID + } + + u := fmt.Sprintf("task-stages/%s", taskStageID) + req, err := s.client.newRequest("GET", u, &options) + if err != nil { + return nil, err + } + + t := &TaskStage{} + err = s.client.do(ctx, req, t) + if err != nil { + return nil, err + } + + return t, nil +} + +type TaskResultStatus string + +const ( + Passed TaskResultStatus = "passed" + Failed TaskResultStatus = "failed" +) + +type TaskResult struct { + ID string `jsonapi:"primary,task-results"` + Status TaskResultStatus `jsonapi:"attr,status"` + Message string `jsonapi:"attr,message"` + StatusTimestamps RunTaskStatusTimestamp `jsonapi:"attr,status-timestamps"` + Url string `jsonapi:"attr,url"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + TaskID string `jsonapi:"attr,task-id"` + TaskName string `jsonapi:"attr,task-name"` + TaskUrl string `jsonapi:"attr,task-url"` + WorkspaceTaskID string `jsonapi:"attr,workspace-task-id"` + WorkspaceTaskEnforcementLevel string `jsonapi:"attr,workspace-task-enforcement-level"` + + TaskStage *TaskStage `jsonapi:"relation,task_stage"` +} diff --git a/tfe.go b/tfe.go index 51bdc0722..217913596 100644 --- a/tfe.go +++ b/tfe.go @@ -124,6 +124,7 @@ type Client struct { SSHKeys SSHKeys StateVersionOutputs StateVersionOutputs StateVersions StateVersions + TaskStages TaskStages Teams Teams TeamAccess TeamAccesses TeamMembers TeamMembers @@ -261,6 +262,7 @@ func NewClient(cfg *Config) (*Client, error) { client.SSHKeys = &sshKeys{client: client} client.StateVersionOutputs = &stateVersionOutputs{client: client} client.StateVersions = &stateVersions{client: client} + client.TaskStages = &taskStages{client: client} client.Teams = &teams{client: client} client.TeamAccess = &teamAccesses{client: client} client.TeamMembers = &teamMembers{client: client} From 0cc90ac251f8417376029efec04d46a8fca77299 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 8 Dec 2021 17:00:10 -0500 Subject: [PATCH 02/34] test: an automated smoke test to ensure proper deserialization --- task_stages_integration_test.go | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 task_stages_integration_test.go diff --git a/task_stages_integration_test.go b/task_stages_integration_test.go new file mode 100644 index 000000000..2ca496f5b --- /dev/null +++ b/task_stages_integration_test.go @@ -0,0 +1,49 @@ +package tfe + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTaskStagesRead(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + // hardcoded currently + taskStageID := "ts-xzskdpGj36B4ZJGn" + + t.Run("without include param", func(t *testing.T) { + taskStage, err := client.TaskStages.Read(ctx, taskStageID, nil) + require.NoError(t, err) + + assert.NotEmpty(t, taskStage.ID) + assert.NotEmpty(t, taskStage.Stage) + assert.NotNil(t, taskStage.StatusTimestamps.ErroredAt) + assert.NotNil(t, taskStage.StatusTimestamps.RunningAt) + assert.NotNil(t, taskStage.CreatedAt) + assert.NotNil(t, taskStage.UpdatedAt) + assert.NotNil(t, taskStage.Run) + assert.NotNil(t, taskStage.TaskResults) + + // so this bit is interesting, if the relation is not specified in the include + // param, the fields of the struct will be zeroed out, minus the ID + assert.NotEmpty(t, taskStage.TaskResults[0].ID) + assert.Empty(t, taskStage.TaskResults[0].Status) + assert.Empty(t, taskStage.TaskResults[0].Message) + }) + + t.Run("with include param task_results", func(t *testing.T) { + taskStage, err := client.TaskStages.Read(ctx, taskStageID, &TaskStageReadOptions{ + Include: "task_results", + }) + require.NoError(t, err) + + t.Run("task results are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, taskStage.TaskResults[0].Status) + assert.NotEmpty(t, taskStage.TaskResults[0].Message) + }) + }) +} From 3887c32400a6c55ff25fab55db3192ca0997b700 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Thu, 9 Dec 2021 15:21:51 -0500 Subject: [PATCH 03/34] Added List Task Stages Endpoint Enable fetching all task stages associated with a run. This endpoint will be critical to fetching the task results for a task stage as you'll need to a task stage ID to do so. --- task_stages.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/task_stages.go b/task_stages.go index 112409428..be1fbd4ee 100644 --- a/task_stages.go +++ b/task_stages.go @@ -8,6 +8,8 @@ import ( type TaskStages interface { Read(ctx context.Context, taskStageID string, options *TaskStageReadOptions) (*TaskStage, error) + + List(ctx context.Context, runID string, options *TaskStageListOptions) (*TaskStageList, error) } type taskStages struct { @@ -25,6 +27,11 @@ type TaskStage struct { TaskResults []*TaskResult `jsonapi:"relation,task-results"` } +type TaskStageList struct { + *Pagination + Items []*TaskStage +} + type RunTaskStatusTimestamp struct { ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"` RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` @@ -54,6 +61,31 @@ func (s *taskStages) Read(ctx context.Context, taskStageID string, options *Task return t, nil } +type TaskStageListOptions struct { + ListOptions +} + +func (s *taskStages) List(ctx context.Context, runID string, options *TaskStageListOptions) (*TaskStageList, error) { + if !validStringID(&runID) { + return nil, ErrInvalidRunID + } + + u := fmt.Sprintf("runs/%s/task-stages", runID) + req, err := s.client.newRequest("GET", u, &options) + if err != nil { + return nil, err + } + + tlist := &TaskStageList{} + + err = s.client.do(ctx, req, tlist) + if err != nil { + return nil, err + } + + return tlist, nil +} + type TaskResultStatus string const ( From 7f1fb08be85f39eb1d2e3d0734f4341a4c24aac5 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Thu, 9 Dec 2021 15:25:15 -0500 Subject: [PATCH 04/34] Automated smoke test for listing task stages from a run --- task_stages_integration_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/task_stages_integration_test.go b/task_stages_integration_test.go index 2ca496f5b..c6094c816 100644 --- a/task_stages_integration_test.go +++ b/task_stages_integration_test.go @@ -47,3 +47,18 @@ func TestTaskStagesRead(t *testing.T) { }) }) } + +func TestTaskStagesList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + runID := "run-TRdorPvcxENJ5t52" + + t.Run("with no params", func(t *testing.T) { + taskStageList, err := client.TaskStages.List(ctx, runID, nil) + require.NoError(t, err) + + assert.NotNil(t, taskStageList.Items) + assert.NotEmpty(t, taskStageList.Items[0].ID) + }) +} From 744ee4e49e8c9a340968b1088c57a462f25f5a90 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Fri, 10 Dec 2021 10:34:26 -0500 Subject: [PATCH 05/34] Added the TaskStages relation to Run struct --- run.go | 1 + 1 file changed, 1 insertion(+) diff --git a/run.go b/run.go index c7a21760e..b3c2b107a 100644 --- a/run.go +++ b/run.go @@ -115,6 +115,7 @@ type Run struct { CreatedBy *User `jsonapi:"relation,created-by"` Plan *Plan `jsonapi:"relation,plan"` PolicyChecks []*PolicyCheck `jsonapi:"relation,policy-checks"` + TaskStage []*TaskStage `jsonapi:"relation,task-stages,omitempty"` Workspace *Workspace `jsonapi:"relation,workspace"` } From 615758107ee2c0d7d94dbdfb525bea83eaff87f2 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 15 Dec 2021 13:19:35 -0500 Subject: [PATCH 06/34] fix: task result status enums --- task_stages.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/task_stages.go b/task_stages.go index be1fbd4ee..f759ef2f7 100644 --- a/task_stages.go +++ b/task_stages.go @@ -87,10 +87,15 @@ func (s *taskStages) List(ctx context.Context, runID string, options *TaskStageL } type TaskResultStatus string +type TaskEnforcementLevel string const ( - Passed TaskResultStatus = "passed" - Failed TaskResultStatus = "failed" + TaskPassed TaskResultStatus = "passed" + TaskFailed TaskResultStatus = "failed" + TaskPending TaskResultStatus = "pending" + TaskUnreachable TaskResultStatus = "unreachable" + Advisory TaskEnforcementLevel = "advisory" + Mandatory TaskEnforcementLevel = "mandatory" ) type TaskResult struct { @@ -105,7 +110,7 @@ type TaskResult struct { TaskName string `jsonapi:"attr,task-name"` TaskUrl string `jsonapi:"attr,task-url"` WorkspaceTaskID string `jsonapi:"attr,workspace-task-id"` - WorkspaceTaskEnforcementLevel string `jsonapi:"attr,workspace-task-enforcement-level"` + WorkspaceTaskEnforcementLevel TaskEnforcementLevel `jsonapi:"attr,workspace-task-enforcement-level"` TaskStage *TaskStage `jsonapi:"relation,task_stage"` } From ae7f95b107fbb002095590a7afcef0608297a562 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 15 Dec 2021 13:56:09 -0500 Subject: [PATCH 07/34] fix: added pre apply stage statuses to run --- run.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/run.go b/run.go index b3c2b107a..b0d6a0552 100644 --- a/run.go +++ b/run.go @@ -69,6 +69,8 @@ const ( RunPolicyChecking RunStatus = "policy_checking" RunPolicyOverride RunStatus = "policy_override" RunPolicySoftFailed RunStatus = "policy_soft_failed" + RunPreApplyRunning RunStatus = "pre_apply_running" + RunPreApplyCompleted RunStatus = "pre_apply_completed" ) // RunSource represents a source type of a run. @@ -115,7 +117,7 @@ type Run struct { CreatedBy *User `jsonapi:"relation,created-by"` Plan *Plan `jsonapi:"relation,plan"` PolicyChecks []*PolicyCheck `jsonapi:"relation,policy-checks"` - TaskStage []*TaskStage `jsonapi:"relation,task-stages,omitempty"` + TaskStages []*TaskStage `jsonapi:"relation,task-stages,omitempty"` Workspace *Workspace `jsonapi:"relation,workspace"` } From 481afccea193615a5d4f669a5a9848751a1f78a0 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Thu, 6 Jan 2022 09:23:44 -0700 Subject: [PATCH 08/34] style: fix Url casing --- task_stages.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/task_stages.go b/task_stages.go index f759ef2f7..d09add2d5 100644 --- a/task_stages.go +++ b/task_stages.go @@ -103,12 +103,12 @@ type TaskResult struct { Status TaskResultStatus `jsonapi:"attr,status"` Message string `jsonapi:"attr,message"` StatusTimestamps RunTaskStatusTimestamp `jsonapi:"attr,status-timestamps"` - Url string `jsonapi:"attr,url"` + URL string `jsonapi:"attr,url"` CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` TaskID string `jsonapi:"attr,task-id"` TaskName string `jsonapi:"attr,task-name"` - TaskUrl string `jsonapi:"attr,task-url"` + TaskURL string `jsonapi:"attr,task-url"` WorkspaceTaskID string `jsonapi:"attr,workspace-task-id"` WorkspaceTaskEnforcementLevel TaskEnforcementLevel `jsonapi:"attr,workspace-task-enforcement-level"` From 8587f8fff2d1f40a4cd890016cb3e1d4195945e6 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Thu, 6 Jan 2022 14:38:35 -0500 Subject: [PATCH 09/34] Minor fixes --- task_stages.go | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/task_stages.go b/task_stages.go index d09add2d5..f1991c217 100644 --- a/task_stages.go +++ b/task_stages.go @@ -17,11 +17,11 @@ type taskStages struct { } type TaskStage struct { - ID string `jsonapi:"primary,task-stages"` - Stage string `jsonapi:"attr,stage"` - StatusTimestamps RunTaskStatusTimestamp `jsonapi:"attr,status-timestamps"` - CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` - UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + ID string `jsonapi:"primary,task-stages"` + Stage string `jsonapi:"attr,stage"` + StatusTimestamps RunTaskStatusTimestamps `jsonapi:"attr,status-timestamps"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` Run *Run `jsonapi:"relation,run"` TaskResults []*TaskResult `jsonapi:"relation,task-results"` @@ -32,7 +32,7 @@ type TaskStageList struct { Items []*TaskStage } -type RunTaskStatusTimestamp struct { +type RunTaskStatusTimestamps struct { ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"` RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` } @@ -92,6 +92,7 @@ type TaskEnforcementLevel string const ( TaskPassed TaskResultStatus = "passed" TaskFailed TaskResultStatus = "failed" + TaskRunning TaskResultStatus = "running" TaskPending TaskResultStatus = "pending" TaskUnreachable TaskResultStatus = "unreachable" Advisory TaskEnforcementLevel = "advisory" @@ -99,18 +100,18 @@ const ( ) type TaskResult struct { - ID string `jsonapi:"primary,task-results"` - Status TaskResultStatus `jsonapi:"attr,status"` - Message string `jsonapi:"attr,message"` - StatusTimestamps RunTaskStatusTimestamp `jsonapi:"attr,status-timestamps"` - URL string `jsonapi:"attr,url"` - CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` - UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` - TaskID string `jsonapi:"attr,task-id"` - TaskName string `jsonapi:"attr,task-name"` - TaskURL string `jsonapi:"attr,task-url"` - WorkspaceTaskID string `jsonapi:"attr,workspace-task-id"` - WorkspaceTaskEnforcementLevel TaskEnforcementLevel `jsonapi:"attr,workspace-task-enforcement-level"` + ID string `jsonapi:"primary,task-results"` + Status TaskResultStatus `jsonapi:"attr,status"` + Message string `jsonapi:"attr,message"` + StatusTimestamps RunTaskStatusTimestamps `jsonapi:"attr,status-timestamps"` + URL string `jsonapi:"attr,url"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + TaskID string `jsonapi:"attr,task-id"` + TaskName string `jsonapi:"attr,task-name"` + TaskURL string `jsonapi:"attr,task-url"` + WorkspaceTaskID string `jsonapi:"attr,workspace-task-id"` + WorkspaceTaskEnforcementLevel TaskEnforcementLevel `jsonapi:"attr,workspace-task-enforcement-level"` TaskStage *TaskStage `jsonapi:"relation,task_stage"` } From 3a77d0cf602b51105f3ea809cb9f11fc22bea801 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Fri, 7 Jan 2022 11:14:17 -0500 Subject: [PATCH 10/34] Refactored task results from task stages file, endpoint completion In order to keep things organized and clean, I've moved the task result API definitions from the task_stages.go file. I've also added the Read endpoint for task results, although I'm hesistant to include this in the release as individual task results are rarely fetched since they are included when you poll a task stage (more discovery would be needed). --- errors.go | 5 ++++ task_result.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ task_stages.go | 30 ---------------------- tfe.go | 1 + 4 files changed, 73 insertions(+), 30 deletions(-) create mode 100644 task_result.go diff --git a/errors.go b/errors.go index 0ffb4ac71..581eccdf0 100644 --- a/errors.go +++ b/errors.go @@ -52,6 +52,11 @@ var ( // ErrInvalidRunID is returned when the run ID is invalid. ErrInvalidRunID = errors.New("invalid value for run ID") + // Task Result errrors + + //ErrInvalidTaskResultID is returned when the task result ID is invalid + ErrInvalidTaskResultID = errors.New("invalid value for task result ID") + // Task Stage errors //ErrInvalidTaskStageID is returned when the task stage ID is invalid. diff --git a/task_result.go b/task_result.go new file mode 100644 index 000000000..064f013f2 --- /dev/null +++ b/task_result.go @@ -0,0 +1,67 @@ +package tfe + +import ( + "context" + "fmt" + "time" +) + +var _ TaskResults = (*taskResults)(nil) + +type TaskResults interface { + Read(ctx context.Context, taskResultID string) (*TaskResult, error) +} + +type taskResults struct { + client *Client +} + +type TaskResultStatus string +type TaskEnforcementLevel string + +const ( + TaskPassed TaskResultStatus = "passed" + TaskFailed TaskResultStatus = "failed" + TaskRunning TaskResultStatus = "running" + TaskPending TaskResultStatus = "pending" + TaskUnreachable TaskResultStatus = "unreachable" + Advisory TaskEnforcementLevel = "advisory" + Mandatory TaskEnforcementLevel = "mandatory" +) + +type TaskResult struct { + ID string `jsonapi:"primary,task-results"` + Status TaskResultStatus `jsonapi:"attr,status"` + Message string `jsonapi:"attr,message"` + StatusTimestamps RunTaskStatusTimestamps `jsonapi:"attr,status-timestamps"` + URL string `jsonapi:"attr,url"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + TaskID string `jsonapi:"attr,task-id"` + TaskName string `jsonapi:"attr,task-name"` + TaskURL string `jsonapi:"attr,task-url"` + WorkspaceTaskID string `jsonapi:"attr,workspace-task-id"` + WorkspaceTaskEnforcementLevel TaskEnforcementLevel `jsonapi:"attr,workspace-task-enforcement-level"` + + TaskStage *TaskStage `jsonapi:"relation,task_stage"` +} + +func (t *taskResults) Read(ctx context.Context, taskResultID string) (*TaskResult, error) { + if !validStringID(&taskResultID) { + return nil, ErrInvalidTaskResultID + } + + u := fmt.Sprintf("task-results/%s", taskResultID) + req, err := t.client.newRequest("GET", u, nil) + if err != nil { + return nil, err + } + + r := &TaskResult{} + err = t.client.do(ctx, req, r) + if err != nil { + return nil, err + } + + return r, nil +} diff --git a/task_stages.go b/task_stages.go index f1991c217..0fbd9220c 100644 --- a/task_stages.go +++ b/task_stages.go @@ -85,33 +85,3 @@ func (s *taskStages) List(ctx context.Context, runID string, options *TaskStageL return tlist, nil } - -type TaskResultStatus string -type TaskEnforcementLevel string - -const ( - TaskPassed TaskResultStatus = "passed" - TaskFailed TaskResultStatus = "failed" - TaskRunning TaskResultStatus = "running" - TaskPending TaskResultStatus = "pending" - TaskUnreachable TaskResultStatus = "unreachable" - Advisory TaskEnforcementLevel = "advisory" - Mandatory TaskEnforcementLevel = "mandatory" -) - -type TaskResult struct { - ID string `jsonapi:"primary,task-results"` - Status TaskResultStatus `jsonapi:"attr,status"` - Message string `jsonapi:"attr,message"` - StatusTimestamps RunTaskStatusTimestamps `jsonapi:"attr,status-timestamps"` - URL string `jsonapi:"attr,url"` - CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` - UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` - TaskID string `jsonapi:"attr,task-id"` - TaskName string `jsonapi:"attr,task-name"` - TaskURL string `jsonapi:"attr,task-url"` - WorkspaceTaskID string `jsonapi:"attr,workspace-task-id"` - WorkspaceTaskEnforcementLevel TaskEnforcementLevel `jsonapi:"attr,workspace-task-enforcement-level"` - - TaskStage *TaskStage `jsonapi:"relation,task_stage"` -} diff --git a/tfe.go b/tfe.go index 217913596..18beaed62 100644 --- a/tfe.go +++ b/tfe.go @@ -124,6 +124,7 @@ type Client struct { SSHKeys SSHKeys StateVersionOutputs StateVersionOutputs StateVersions StateVersions + TaskResults TaskResults TaskStages TaskStages Teams Teams TeamAccess TeamAccesses From 938e7058bcfa21de74d81fed83ae71e0d5e2f8c1 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Fri, 7 Jan 2022 13:49:44 -0500 Subject: [PATCH 11/34] Added Run Task endpoints Currently Run Tasks are abstracted into two forms even though they share the same underlying model: Organization run tasks and Workspace run tasks. An organization run task represents a run task that is created at the organization level, defining the metadata of where TFC will send webhook payloads. These tasks however will not be executed during a run until they are attached to a workspace. The same organization run task can be attached to multiple workspaces. --- errors.go | 11 +++ run_tasks.go | 230 +++++++++++++++++++++++++++++++++++++++++++++++++++ tfe.go | 1 + 3 files changed, 242 insertions(+) create mode 100644 run_tasks.go diff --git a/errors.go b/errors.go index 581eccdf0..1e537bf5e 100644 --- a/errors.go +++ b/errors.go @@ -52,6 +52,17 @@ var ( // ErrInvalidRunID is returned when the run ID is invalid. ErrInvalidRunID = errors.New("invalid value for run ID") + // Run Task errors + + //ErrInvalidRunTaskCategory is returned when a run task has a category other than "task" + ErrInvalidRunTaskCategory = errors.New(`category must be "tasks"`) + + //ErrInvalidRunTaskID is returned when the run task ID is invalid + ErrInvalidRunTaskID = errors.New("invalid value for run task ID") + + //ErrInvalidRunTaskURL is returned when the run task URL is invalid + ErrInvalidRunTaskURL = errors.New("invalid url for run task URL") + // Task Result errrors //ErrInvalidTaskResultID is returned when the task result ID is invalid diff --git a/run_tasks.go b/run_tasks.go new file mode 100644 index 000000000..4cc109543 --- /dev/null +++ b/run_tasks.go @@ -0,0 +1,230 @@ +package tfe + +import ( + "context" + "fmt" + "net/url" +) + +var _ RunTasks = (*runTasks)(nil) + +type RunTasks interface { + Create(ctx context.Context, organization string, options RunTaskCreateOptions) (*RunTask, error) + + List(ctx context.Context, organization string, options RunTaskListOptions) (*RunTaskList, error) + + Read(ctx context.Context, runTaskID string) (*RunTask, error) + + ReadWithOptions(ctx context.Context, runTaskID string, options *RunTaskReadOptions) (*RunTask, error) + + Update(ctx context.Context, runTaskID string, options RunTaskUpdateOptions) (*RunTask, error) + + Delete(ctx context.Context, runTaskID string) error +} + +type runTasks struct { + client *Client +} + +type WorkspaceTasks interface { + Create() + + List() + + Read() + + Update() + + Delete() +} + +type RunTask struct { + ID string `jsonapi:"primary,tasks"` + Name string `jsonapi:"attr,name"` + URL string `jsonapi:"attr,url"` + Category string `jsonapi:"attr,category"` + HmacKey string `jsonapi:"attr,hmac-key,omitempty"` + + Organization *Organization `jsonapi:"relation,organization"` +} + +type RunTaskList struct { + *Pagination + Items []*RunTask +} + +type RunTaskCreateOptions struct { + Type string `jsonapi:"primary,tasks"` + Name string `jsonapi:"attr,name"` + URL string `jsonapi:"attr,url"` + Category string `jsonapi:"attr,category"` + HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` +} + +func (o *RunTaskCreateOptions) valid() error { + if !validString(&o.Name) { + return ErrRequiredName + } + + if !validString(&o.URL) { + return ErrInvalidRunTaskURL + } + + if o.Category != "tasks" { + return ErrInvalidRunTaskCategory + } + + return nil +} + +func (s *runTasks) Create(ctx context.Context, organization string, options RunTaskCreateOptions) (*RunTask, error) { + if !validStringID(&organization) { + return nil, ErrInvalidOrg + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) + req, err := s.client.newRequest("POST", u, &options) + if err != nil { + return nil, err + } + + r := &RunTask{} + err = s.client.do(ctx, req, r) + if err != nil { + return nil, err + } + + return r, nil +} + +type RunTaskListOptions struct { + Include string `url:"include"` + ListOptions +} + +func (s *runTasks) List(ctx context.Context, organization string, options RunTaskListOptions) (*RunTaskList, error) { + if !validStringID(&organization) { + return nil, ErrInvalidOrg + } + + u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) + req, err := s.client.newRequest("GET", u, &options) + if err != nil { + return nil, err + } + + rl := &RunTaskList{} + err = s.client.do(ctx, req, rl) + if err != nil { + return nil, err + } + + return rl, nil +} + +func (s *runTasks) Read(ctx context.Context, runTaskID string) (*RunTask, error) { + return s.ReadWithOptions(ctx, runTaskID, nil) +} + +type RunTaskReadOptions struct { + Include string `url:"include"` +} + +func (s *runTasks) ReadWithOptions(ctx context.Context, runTaskID string, options *RunTaskReadOptions) (*RunTask, error) { + if !validStringID(&runTaskID) { + return nil, ErrInvalidRunTaskID + } + + u := fmt.Sprintf("tasks/%s", url.QueryEscape(runTaskID)) + req, err := s.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + r := &RunTask{} + err = s.client.do(ctx, req, r) + if err != nil { + return nil, err + } + + return r, nil +} + +type RunTaskUpdateOptions struct { + Type string `jsonapi:"primary,tasks"` + Name *string `jsonapi:"attr,name,omitempty"` + URL *string `jsonapi:"attr,url,omitempty"` + Category *string `jsonapi:"attr,category,omitempty"` + HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` +} + +func (o *RunTaskUpdateOptions) valid() error { + if !validString(o.Name) { + return ErrRequiredName + } + + if !validString(o.URL) { + return ErrInvalidRunTaskURL + } + + if *o.Category != "tasks" { + return ErrInvalidRunTaskCategory + } + + return nil +} + +func (s *runTasks) Update(ctx context.Context, runTaskID string, options *RunTaskUpdateOptions) (*RunTask, error) { + if !validStringID(&runTaskID) { + return nil, ErrInvalidRunTaskID + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("tasks/%s", url.QueryEscape(runTaskID)) + req, err := s.client.newRequest("PATCH", u, options) + if err != nil { + return nil, err + } + + r := &RunTask{} + err = s.client.do(ctx, req, r) + if err != nil { + return nil, err + } + + return r, nil +} + +func (s *runTasks) Delete(ctx context.Context, runTaskID string) error { + if !validStringID(&runTaskID) { + return ErrInvalidRunTaskID + } + + u := fmt.Sprintf("tasks/%s", runTaskID) + req, err := s.client.newRequest("DELETE", u, nil) + if err != nil { + return err + } + + return s.client.do(ctx, req, nil) +} + +type WorkspaceTask struct { + Type string `jsonapi:"primary,workspace-tasks"` + EnforcementLevel string `jsonapi:"attr,enforcement-level"` + + RunTask *RunTask `jsonapi:"relation,task"` + Workspace *Workspace `jsonapi:"relation,workspace"` +} + +type WorkspaceTaskList struct { + *Pagination + Items []*WorkspaceTask +} diff --git a/tfe.go b/tfe.go index 18beaed62..76b182eb1 100644 --- a/tfe.go +++ b/tfe.go @@ -120,6 +120,7 @@ type Client struct { PolicySets PolicySets RegistryModules RegistryModules Runs Runs + RunTasks RunTasks RunTriggers RunTriggers SSHKeys SSHKeys StateVersionOutputs StateVersionOutputs From 3524f1dd21e4c9ed6d18b09b115da758bedb9bea Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Fri, 7 Jan 2022 17:16:26 -0500 Subject: [PATCH 12/34] Added Workspace Run Tasks endpoints These endpoints represent when a run task is attached to a workspace. A workspace run task is given an enforcement level that is either advisory or mandatory. As of this commit, you don't specify which task stage to add the run task to since the only supported stage is "pre-apply" --- errors.go | 18 +++- run_task.go | 206 ++++++++++++++++++++++++++++++++++++++++++ run_tasks.go | 25 ----- tfe.go | 3 + workspace_run_task.go | 189 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 411 insertions(+), 30 deletions(-) create mode 100644 run_task.go create mode 100644 workspace_run_task.go diff --git a/errors.go b/errors.go index 1e537bf5e..d65ba9f16 100644 --- a/errors.go +++ b/errors.go @@ -54,23 +54,31 @@ var ( // Run Task errors - //ErrInvalidRunTaskCategory is returned when a run task has a category other than "task" + // ErrInvalidRunTaskCategory is returned when a run task has a category other than "task" ErrInvalidRunTaskCategory = errors.New(`category must be "tasks"`) - //ErrInvalidRunTaskID is returned when the run task ID is invalid + // ErrInvalidRunTaskID is returned when the run task ID is invalid ErrInvalidRunTaskID = errors.New("invalid value for run task ID") - //ErrInvalidRunTaskURL is returned when the run task URL is invalid + // ErrInvalidRunTaskURL is returned when the run task URL is invalid ErrInvalidRunTaskURL = errors.New("invalid url for run task URL") + // Workspace Run Task errors + + //ErrInvalidWorkspaceRunTaskID is returned when the workspace run task ID is invalid + ErrInvalidWorkspaceRunTaskID = errors.New("invalid value for workspace run task ID") + + //ErrInvalidWorkspaceRunTaskType is returned when Type is not "workspace-tasks" + ErrInvalidWorkspaceRunTaskType = errors.New(`invalid value for type, please use "workspace-tasks"`) + // Task Result errrors - //ErrInvalidTaskResultID is returned when the task result ID is invalid + // ErrInvalidTaskResultID is returned when the task result ID is invalid ErrInvalidTaskResultID = errors.New("invalid value for task result ID") // Task Stage errors - //ErrInvalidTaskStageID is returned when the task stage ID is invalid. + // ErrInvalidTaskStageID is returned when the task stage ID is invalid. ErrInvalidTaskStageID = errors.New("invalid value for task stage ID") // ErrInvalidApplyID is returned when the apply ID is invalid. diff --git a/run_task.go b/run_task.go new file mode 100644 index 000000000..6ca99129f --- /dev/null +++ b/run_task.go @@ -0,0 +1,206 @@ +package tfe + +import ( + "context" + "fmt" + "net/url" +) + +var _ RunTasks = (*runTasks)(nil) + +type RunTasks interface { + Create(ctx context.Context, organization string, options RunTaskCreateOptions) (*RunTask, error) + + List(ctx context.Context, organization string, options *RunTaskListOptions) (*RunTaskList, error) + + Read(ctx context.Context, runTaskID string) (*RunTask, error) + + ReadWithOptions(ctx context.Context, runTaskID string, options *RunTaskReadOptions) (*RunTask, error) + + Update(ctx context.Context, runTaskID string, options RunTaskUpdateOptions) (*RunTask, error) + + Delete(ctx context.Context, runTaskID string) error +} + +type runTasks struct { + client *Client +} + +type RunTask struct { + ID string `jsonapi:"primary,tasks"` + Name string `jsonapi:"attr,name"` + URL string `jsonapi:"attr,url"` + Category string `jsonapi:"attr,category"` + HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` + + Organization *Organization `jsonapi:"relation,organization"` + WorkspaceRunTasks []*WorkspaceRunTask `jsonapi:"relation,tasks"` +} + +type RunTaskList struct { + *Pagination + Items []*RunTask +} + +type RunTaskCreateOptions struct { + Type string `jsonapi:"primary,tasks"` + Name string `jsonapi:"attr,name"` + URL string `jsonapi:"attr,url"` + Category string `jsonapi:"attr,category"` + HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` +} + +func (o *RunTaskCreateOptions) valid() error { + if !validString(&o.Name) { + return ErrRequiredName + } + + if !validString(&o.URL) { + return ErrInvalidRunTaskURL + } + + if o.Category != "tasks" { + return ErrInvalidRunTaskCategory + } + + return nil +} + +func (s *runTasks) Create(ctx context.Context, organization string, options RunTaskCreateOptions) (*RunTask, error) { + if !validStringID(&organization) { + return nil, ErrInvalidOrg + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) + req, err := s.client.newRequest("POST", u, &options) + if err != nil { + return nil, err + } + + r := &RunTask{} + err = s.client.do(ctx, req, r) + if err != nil { + return nil, err + } + + return r, nil +} + +type RunTaskListOptions struct { + Include string `url:"include"` + ListOptions +} + +func (s *runTasks) List(ctx context.Context, organization string, options *RunTaskListOptions) (*RunTaskList, error) { + if !validStringID(&organization) { + return nil, ErrInvalidOrg + } + + u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) + req, err := s.client.newRequest("GET", u, &options) + if err != nil { + return nil, err + } + + rl := &RunTaskList{} + err = s.client.do(ctx, req, rl) + if err != nil { + return nil, err + } + + return rl, nil +} + +func (s *runTasks) Read(ctx context.Context, runTaskID string) (*RunTask, error) { + return s.ReadWithOptions(ctx, runTaskID, nil) +} + +type RunTaskReadOptions struct { + Include string `url:"include"` +} + +func (s *runTasks) ReadWithOptions(ctx context.Context, runTaskID string, options *RunTaskReadOptions) (*RunTask, error) { + if !validStringID(&runTaskID) { + return nil, ErrInvalidRunTaskID + } + + u := fmt.Sprintf("tasks/%s", url.QueryEscape(runTaskID)) + req, err := s.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + r := &RunTask{} + err = s.client.do(ctx, req, r) + if err != nil { + return nil, err + } + + return r, nil +} + +type RunTaskUpdateOptions struct { + Type string `jsonapi:"primary,tasks"` + Name *string `jsonapi:"attr,name,omitempty"` + URL *string `jsonapi:"attr,url,omitempty"` + Category *string `jsonapi:"attr,category,omitempty"` + HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` +} + +func (o *RunTaskUpdateOptions) valid() error { + if !validString(o.Name) { + return ErrRequiredName + } + + if !validString(o.URL) { + return ErrInvalidRunTaskURL + } + + if *o.Category != "tasks" { + return ErrInvalidRunTaskCategory + } + + return nil +} + +func (s *runTasks) Update(ctx context.Context, runTaskID string, options RunTaskUpdateOptions) (*RunTask, error) { + if !validStringID(&runTaskID) { + return nil, ErrInvalidRunTaskID + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("tasks/%s", url.QueryEscape(runTaskID)) + req, err := s.client.newRequest("PATCH", u, options) + if err != nil { + return nil, err + } + + r := &RunTask{} + err = s.client.do(ctx, req, r) + if err != nil { + return nil, err + } + + return r, nil +} + +func (s *runTasks) Delete(ctx context.Context, runTaskID string) error { + if !validStringID(&runTaskID) { + return ErrInvalidRunTaskID + } + + u := fmt.Sprintf("tasks/%s", runTaskID) + req, err := s.client.newRequest("DELETE", u, nil) + if err != nil { + return err + } + + return s.client.do(ctx, req, nil) +} diff --git a/run_tasks.go b/run_tasks.go index 4cc109543..d1707754d 100644 --- a/run_tasks.go +++ b/run_tasks.go @@ -26,18 +26,6 @@ type runTasks struct { client *Client } -type WorkspaceTasks interface { - Create() - - List() - - Read() - - Update() - - Delete() -} - type RunTask struct { ID string `jsonapi:"primary,tasks"` Name string `jsonapi:"attr,name"` @@ -215,16 +203,3 @@ func (s *runTasks) Delete(ctx context.Context, runTaskID string) error { return s.client.do(ctx, req, nil) } - -type WorkspaceTask struct { - Type string `jsonapi:"primary,workspace-tasks"` - EnforcementLevel string `jsonapi:"attr,enforcement-level"` - - RunTask *RunTask `jsonapi:"relation,task"` - Workspace *Workspace `jsonapi:"relation,workspace"` -} - -type WorkspaceTaskList struct { - *Pagination - Items []*WorkspaceTask -} diff --git a/tfe.go b/tfe.go index 76b182eb1..94d0592e0 100644 --- a/tfe.go +++ b/tfe.go @@ -135,6 +135,7 @@ type Client struct { UserTokens UserTokens Variables Variables Workspaces Workspaces + WorkspaceRunTasks WorkspaceRunTasks Meta Meta } @@ -260,6 +261,7 @@ func NewClient(cfg *Config) (*Client, error) { client.PolicySets = &policySets{client: client} client.RegistryModules = ®istryModules{client: client} client.Runs = &runs{client: client} + client.RunTasks = &runTasks{client: client} client.RunTriggers = &runTriggers{client: client} client.SSHKeys = &sshKeys{client: client} client.StateVersionOutputs = &stateVersionOutputs{client: client} @@ -273,6 +275,7 @@ func NewClient(cfg *Config) (*Client, error) { client.UserTokens = &userTokens{client: client} client.Variables = &variables{client: client} client.Workspaces = &workspaces{client: client} + client.WorkspaceRunTasks = &workspaceRunTasks{client: client} client.Meta = Meta{ IPRanges: &ipRanges{client: client}, diff --git a/workspace_run_task.go b/workspace_run_task.go new file mode 100644 index 000000000..b17125d4c --- /dev/null +++ b/workspace_run_task.go @@ -0,0 +1,189 @@ +package tfe + +import ( + "context" + "fmt" + "net/url" +) + +var _ WorkspaceRunTasks = (*workspaceRunTasks)(nil) + +type WorkspaceRunTasks interface { + Create(ctx context.Context, workspaceID string, options WorkspaceRunTaskCreateOptions) (*WorkspaceRunTask, error) + + List(ctx context.Context, workspaceID string, options *WorkspaceRunTaskListOptions) (*WorkspaceRunTaskList, error) + + Read(ctx context.Context, workspaceID string, workspaceTaskID string) (*WorkspaceRunTask, error) + + Update(ctx context.Context, workspaceID string, workspaceTaskID string, options WorkspaceRunTaskUpdateOptions) (*WorkspaceRunTask, error) + + Delete(ctx context.Context, workspaceID string, workspaceTaskID string) error +} + +type workspaceRunTasks struct { + client *Client +} + +type WorkspaceRunTask struct { + ID string `jsonapi:"primary,workspace-tasks"` + EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"` + + RunTask *RunTask `jsonapi:"relation,task"` + Workspace *Workspace `jsonapi:"relation,workspace"` +} + +type WorkspaceRunTaskList struct { + *Pagination + Items []*WorkspaceRunTask +} + +type WorkspaceRunTaskListOptions struct { + ListOptions + + Include *string `url:"include,omitempty"` +} + +func (s *workspaceRunTasks) List(ctx context.Context, workspaceID string, options *WorkspaceRunTaskListOptions) (*WorkspaceRunTaskList, error) { + if !validStringID(&workspaceID) { + return nil, ErrInvalidWorkspaceID + } + + u := fmt.Sprintf("workspaces/%s/tasks", url.QueryEscape(workspaceID)) + req, err := s.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + rl := &WorkspaceRunTaskList{} + err = s.client.do(ctx, req, rl) + if err != nil { + return nil, err + } + + return rl, nil +} + +func (s *workspaceRunTasks) Read(ctx context.Context, workspaceID string, workspaceTaskID string) (*WorkspaceRunTask, error) { + if !validStringID(&workspaceID) { + return nil, ErrInvalidWorkspaceID + } + + if !validStringID(&workspaceTaskID) { + return nil, ErrInvalidWorkspaceRunTaskID + } + + u := fmt.Sprintf( + "workspaces/%s/tasks/%s", + url.QueryEscape(workspaceID), + url.QueryEscape(workspaceTaskID), + ) + req, err := s.client.newRequest("GET", u, nil) + if err != nil { + return nil, err + } + + wr := &WorkspaceRunTask{} + err = s.client.do(ctx, req, wr) + if err != nil { + return nil, err + } + + return wr, nil +} + +type WorkspaceRunTaskCreateOptions struct { + Type string `jsonapi:"primary,workspace-tasks"` + EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"` + RunTask RunTask `jsoniapi:"relation,tasks"` +} + +func (o *WorkspaceRunTaskCreateOptions) valid() error { + if o.RunTask.ID == "" { + return ErrInvalidRunTaskID + } + + if o.Type != "workspace-tasks" { + return ErrInvalidWorkspaceRunTaskType + } + + return nil +} + +func (s *workspaceRunTasks) Create(ctx context.Context, workspaceID string, options WorkspaceRunTaskCreateOptions) (*WorkspaceRunTask, error) { + if !validStringID(&workspaceID) { + return nil, ErrInvalidWorkspaceID + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("workspaces/%s/tasks", workspaceID) + req, err := s.client.newRequest("POST", u, options) + if err != nil { + return nil, err + } + + wr := &WorkspaceRunTask{} + err = s.client.do(ctx, req, &wr) + if err != nil { + return nil, err + } + + return wr, nil +} + +type WorkspaceRunTaskUpdateOptions struct { + Type string `jsonapi:"primary,workspace-tasks"` + EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"` +} + +func (s *workspaceRunTasks) Update(ctx context.Context, workspaceID string, workspaceTaskID string, options WorkspaceRunTaskUpdateOptions) (*WorkspaceRunTask, error) { + if !validStringID(&workspaceID) { + return nil, ErrInvalidWorkspaceID + } + + if !validStringID(&workspaceTaskID) { + return nil, ErrInvalidWorkspaceRunTaskID + } + + u := fmt.Sprintf( + "workspaces/%s/tasks/%s", + url.QueryEscape(workspaceID), + url.QueryEscape(workspaceTaskID), + ) + req, err := s.client.newRequest("PATCH", u, &options) + if err != nil { + return nil, err + } + + wr := &WorkspaceRunTask{} + err = s.client.do(ctx, req, wr) + if err != nil { + return nil, err + } + + return wr, nil +} + +func (s *workspaceRunTasks) Delete(ctx context.Context, workspaceID string, workspaceTaskID string) error { + if !validStringID(&workspaceID) { + return ErrInvalidWorkspaceID + } + + if !validStringID(&workspaceTaskID) { + return ErrInvalidWorkspaceRunTaskType + } + + u := fmt.Sprintf( + "workspaces/%s/tasks/%s", + url.QueryEscape(workspaceID), + url.QueryEscape(workspaceTaskID), + ) + req, err := s.client.newRequest("DELETE", u, nil) + if err != nil { + return err + } + + return s.client.do(ctx, req, nil) +} From 533c57f98e3b6c95fdae98cb94aa1d00dac68d13 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Mon, 10 Jan 2022 18:01:53 -0500 Subject: [PATCH 13/34] Added Organization Run Task API integration tests --- helper_test.go | 76 +++++++++++++ run_task_integration_test.go | 161 +++++++++++++++++++++++++++ run_tasks.go | 205 ----------------------------------- 3 files changed, 237 insertions(+), 205 deletions(-) create mode 100644 run_task_integration_test.go delete mode 100644 run_tasks.go diff --git a/helper_test.go b/helper_test.go index 0530f76b3..fe55103f1 100644 --- a/helper_test.go +++ b/helper_test.go @@ -702,6 +702,36 @@ func createRegistryModuleWithVersion(t *testing.T, client *Client, org *Organiza } } +func createRunTask(t *testing.T, client *Client, org *Organization) (*RunTask, func()) { + var orgCleanup func() + + if org == nil { + org, orgCleanup = createOrganization(t, client) + } + + ctx := context.Background() + r, err := client.RunTasks.Create(ctx, org.Name, RunTaskCreateOptions{ + Name: "tst-" + randomString(t), + URL: "http://54.167.177.151/success", + Category: "tasks", + }) + if err != nil { + t.Fatal(err) + } + + return r, func() { + if err := client.RunTasks.Delete(ctx, r.ID); err != nil { + t.Errorf("Error removing Run Task! WARNING: Run task limit\n"+ + "may be reached if not deleted! The full error is shown below.\n\n"+ + "Run Task: %s\nError: %s", r.Name, err) + } + + if orgCleanup != nil { + orgCleanup() + } + } +} + func createSSHKey(t *testing.T, client *Client, org *Organization) (*SSHKey, func()) { var orgCleanup func() @@ -989,6 +1019,52 @@ func createWorkspaceWithVCS(t *testing.T, client *Client, org *Organization, opt } } +func createWorkspaceRunTask(t *testing.T, client *Client, workspace *Workspace, runTask *RunTask) (*WorkspaceRunTask, func()) { + var organization *Organization + var runTaskCleanup func() + var workspaceCleanup func() + var orgCleanup func() + + if workspace == nil { + organization, orgCleanup = createOrganization(t, client) + workspace, workspaceCleanup = createWorkspace(t, client, organization) + } + + if runTask == nil { + runTask, runTaskCleanup = createRunTask(t, client, organization) + } + + ctx := context.Background() + wr, err := client.WorkspaceRunTasks.Create(ctx, workspace.ID, WorkspaceRunTaskCreateOptions{ + EnforcementLevel: Advisory, + RunTask: *runTask, + }) + if err != nil { + t.Fatal(err) + } + + return wr, func() { + if err := client.WorkspaceRunTasks.Delete(ctx, workspace.ID, wr.ID); err != nil { + t.Errorf("Error destroying workspace run task!\n"+ + "Workspace: %s\n"+ + "Workspace Run Task: %s\n"+ + "Error: %s", workspace.ID, wr.ID, err) + } + + if runTaskCleanup != nil { + runTaskCleanup() + } + + if workspaceCleanup != nil { + workspaceCleanup() + } + + if orgCleanup != nil { + orgCleanup() + } + } +} + func genSha(t *testing.T, secret, data string) string { h := hmac.New(sha256.New, []byte(secret)) _, err := h.Write([]byte(data)) diff --git a/run_task_integration_test.go b/run_task_integration_test.go new file mode 100644 index 000000000..063da4de5 --- /dev/null +++ b/run_task_integration_test.go @@ -0,0 +1,161 @@ +package tfe + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunTasksCreate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + runTaskServerURI := "http://54.167.177.151/success" + runTaskName := "tst-runtask-" + randomString(t) + + t.Run("add run task to organization", func(t *testing.T) { + r, err := client.RunTasks.Create(ctx, orgTest.Name, RunTaskCreateOptions{ + Name: runTaskName, + URL: runTaskServerURI, + Category: "task", + }) + require.NoError(t, err) + + assert.NotEmpty(t, r.ID) + assert.Equal(t, r.Name, runTaskName) + assert.Equal(t, r.URL, runTaskServerURI) + assert.Equal(t, r.Category, "task") + + t.Run("ensure org is deserialized properly", func(t *testing.T) { + assert.Equal(t, r.Organization.Name, orgTest.Name) + }) + }) +} + +func TestRunTasksList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + _, runTaskTest1Cleanup := createRunTask(t, client, orgTest) + defer runTaskTest1Cleanup() + + _, runTaskTest2Cleanup := createRunTask(t, client, orgTest) + defer runTaskTest2Cleanup() + + t.Run("with no params", func(t *testing.T) { + runTaskList, err := client.RunTasks.List(ctx, orgTest.Name, nil) + require.NoError(t, err) + assert.NotNil(t, runTaskList.Items) + assert.NotEmpty(t, runTaskList.Items[0].ID) + assert.NotEmpty(t, runTaskList.Items[0].URL) + assert.NotEmpty(t, runTaskList.Items[1].ID) + assert.NotEmpty(t, runTaskList.Items[1].URL) + }) +} + +func TestRunTasksRead(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) + defer runTaskTestCleanup() + + t.Run("by ID", func(t *testing.T) { + r, err := client.RunTasks.Read(ctx, runTaskTest.ID) + require.NoError(t, err) + + assert.Equal(t, runTaskTest.ID, r.ID) + assert.Equal(t, runTaskTest.URL, r.URL) + assert.Equal(t, runTaskTest.Category, r.Category) + assert.Equal(t, runTaskTest.HmacKey, r.HmacKey) + }) + + t.Run("with options", func(t *testing.T) { + wkTest1, wkTest1Cleanup := createWorkspace(t, client, orgTest) + defer wkTest1Cleanup() + + wkTest2, wkTest2Cleanup := createWorkspace(t, client, orgTest) + defer wkTest2Cleanup() + + _, wrTest1Cleanup := createWorkspaceRunTask(t, client, wkTest1, runTaskTest) + defer wrTest1Cleanup() + + _, wrTest2Cleanup := createWorkspaceRunTask(t, client, wkTest2, runTaskTest) + defer wrTest2Cleanup() + + r, err := client.RunTasks.ReadWithOptions(ctx, runTaskTest.ID, &RunTaskReadOptions{ + Include: "workspace_tasks", + }) + + require.NoError(t, err) + + assert.NotEmpty(t, r.WorkspaceRunTasks) + assert.NotEmpty(t, r.WorkspaceRunTasks[0].ID) + assert.NotEmpty(t, r.WorkspaceRunTasks[0].EnforcementLevel) + assert.NotEmpty(t, r.WorkspaceRunTasks[1].ID) + assert.NotEmpty(t, r.WorkspaceRunTasks[1].EnforcementLevel) + }) +} + +func TestRunTasksUpdate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) + defer runTaskTestCleanup() + + t.Run("rename task", func(t *testing.T) { + rename := runTaskTest.Name + "-UPDATED" + r, err := client.RunTasks.Update(ctx, runTaskTest.ID, RunTaskUpdateOptions{ + Name: &rename, + }) + require.NoError(t, err) + + r, err = client.RunTasks.Read(ctx, r.ID) + require.NoError(t, err) + + assert.Equal(t, rename, r.Name) + }) +} + +func TestRunTasksDelete(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + runTaskTest, _ := createRunTask(t, client, orgTest) + + t.Run("with valid options", func(t *testing.T) { + err := client.RunTasks.Delete(ctx, runTaskTest.ID) + require.NoError(t, err) + + _, err = client.RunTasks.Read(ctx, runTaskTest.ID) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the run task does not exist", func(t *testing.T) { + err := client.RunTasks.Delete(ctx, runTaskTest.ID) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the run task ID is invalid", func(t *testing.T) { + err := client.RunTasks.Delete(ctx, badIdentifier) + assert.EqualError(t, err, ErrInvalidRunTaskID.Error()) + }) +} diff --git a/run_tasks.go b/run_tasks.go deleted file mode 100644 index d1707754d..000000000 --- a/run_tasks.go +++ /dev/null @@ -1,205 +0,0 @@ -package tfe - -import ( - "context" - "fmt" - "net/url" -) - -var _ RunTasks = (*runTasks)(nil) - -type RunTasks interface { - Create(ctx context.Context, organization string, options RunTaskCreateOptions) (*RunTask, error) - - List(ctx context.Context, organization string, options RunTaskListOptions) (*RunTaskList, error) - - Read(ctx context.Context, runTaskID string) (*RunTask, error) - - ReadWithOptions(ctx context.Context, runTaskID string, options *RunTaskReadOptions) (*RunTask, error) - - Update(ctx context.Context, runTaskID string, options RunTaskUpdateOptions) (*RunTask, error) - - Delete(ctx context.Context, runTaskID string) error -} - -type runTasks struct { - client *Client -} - -type RunTask struct { - ID string `jsonapi:"primary,tasks"` - Name string `jsonapi:"attr,name"` - URL string `jsonapi:"attr,url"` - Category string `jsonapi:"attr,category"` - HmacKey string `jsonapi:"attr,hmac-key,omitempty"` - - Organization *Organization `jsonapi:"relation,organization"` -} - -type RunTaskList struct { - *Pagination - Items []*RunTask -} - -type RunTaskCreateOptions struct { - Type string `jsonapi:"primary,tasks"` - Name string `jsonapi:"attr,name"` - URL string `jsonapi:"attr,url"` - Category string `jsonapi:"attr,category"` - HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` -} - -func (o *RunTaskCreateOptions) valid() error { - if !validString(&o.Name) { - return ErrRequiredName - } - - if !validString(&o.URL) { - return ErrInvalidRunTaskURL - } - - if o.Category != "tasks" { - return ErrInvalidRunTaskCategory - } - - return nil -} - -func (s *runTasks) Create(ctx context.Context, organization string, options RunTaskCreateOptions) (*RunTask, error) { - if !validStringID(&organization) { - return nil, ErrInvalidOrg - } - - if err := options.valid(); err != nil { - return nil, err - } - - u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) - req, err := s.client.newRequest("POST", u, &options) - if err != nil { - return nil, err - } - - r := &RunTask{} - err = s.client.do(ctx, req, r) - if err != nil { - return nil, err - } - - return r, nil -} - -type RunTaskListOptions struct { - Include string `url:"include"` - ListOptions -} - -func (s *runTasks) List(ctx context.Context, organization string, options RunTaskListOptions) (*RunTaskList, error) { - if !validStringID(&organization) { - return nil, ErrInvalidOrg - } - - u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, &options) - if err != nil { - return nil, err - } - - rl := &RunTaskList{} - err = s.client.do(ctx, req, rl) - if err != nil { - return nil, err - } - - return rl, nil -} - -func (s *runTasks) Read(ctx context.Context, runTaskID string) (*RunTask, error) { - return s.ReadWithOptions(ctx, runTaskID, nil) -} - -type RunTaskReadOptions struct { - Include string `url:"include"` -} - -func (s *runTasks) ReadWithOptions(ctx context.Context, runTaskID string, options *RunTaskReadOptions) (*RunTask, error) { - if !validStringID(&runTaskID) { - return nil, ErrInvalidRunTaskID - } - - u := fmt.Sprintf("tasks/%s", url.QueryEscape(runTaskID)) - req, err := s.client.newRequest("GET", u, options) - if err != nil { - return nil, err - } - - r := &RunTask{} - err = s.client.do(ctx, req, r) - if err != nil { - return nil, err - } - - return r, nil -} - -type RunTaskUpdateOptions struct { - Type string `jsonapi:"primary,tasks"` - Name *string `jsonapi:"attr,name,omitempty"` - URL *string `jsonapi:"attr,url,omitempty"` - Category *string `jsonapi:"attr,category,omitempty"` - HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` -} - -func (o *RunTaskUpdateOptions) valid() error { - if !validString(o.Name) { - return ErrRequiredName - } - - if !validString(o.URL) { - return ErrInvalidRunTaskURL - } - - if *o.Category != "tasks" { - return ErrInvalidRunTaskCategory - } - - return nil -} - -func (s *runTasks) Update(ctx context.Context, runTaskID string, options *RunTaskUpdateOptions) (*RunTask, error) { - if !validStringID(&runTaskID) { - return nil, ErrInvalidRunTaskID - } - - if err := options.valid(); err != nil { - return nil, err - } - - u := fmt.Sprintf("tasks/%s", url.QueryEscape(runTaskID)) - req, err := s.client.newRequest("PATCH", u, options) - if err != nil { - return nil, err - } - - r := &RunTask{} - err = s.client.do(ctx, req, r) - if err != nil { - return nil, err - } - - return r, nil -} - -func (s *runTasks) Delete(ctx context.Context, runTaskID string) error { - if !validStringID(&runTaskID) { - return ErrInvalidRunTaskID - } - - u := fmt.Sprintf("tasks/%s", runTaskID) - req, err := s.client.newRequest("DELETE", u, nil) - if err != nil { - return err - } - - return s.client.do(ctx, req, nil) -} From 1914cd0ffed6710999ed260e7d06e54115eda483 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Mon, 10 Jan 2022 18:20:57 -0500 Subject: [PATCH 14/34] Refactored stages to enumerator, added other timestamps to Status enum The Stage enum will hold all possible stages in a run lifecycle that a TaskStage can occur. --- task_stages.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/task_stages.go b/task_stages.go index 0fbd9220c..b360e805e 100644 --- a/task_stages.go +++ b/task_stages.go @@ -16,9 +16,16 @@ type taskStages struct { client *Client } +type Stage string + +const ( + PreApply Stage = "pre-apply" + PostPlan Stage = "post-plan" +) + type TaskStage struct { ID string `jsonapi:"primary,task-stages"` - Stage string `jsonapi:"attr,stage"` + Stage Stage `jsonapi:"attr,stage"` StatusTimestamps RunTaskStatusTimestamps `jsonapi:"attr,status-timestamps"` CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` @@ -33,8 +40,11 @@ type TaskStageList struct { } type RunTaskStatusTimestamps struct { - ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"` - RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` + ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"` + RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` + CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"` + FailedAt time.Time `jsonapi:"attr,failed-at,rfc3339"` + PassedAt time.Time `jsonapi:"attr,passed-at,rfc3339"` } type TaskStageReadOptions struct { From 01933e7caae9cbb671d0c255a0b50c6d4dcb0037 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Tue, 11 Jan 2022 13:54:44 -0500 Subject: [PATCH 15/34] Added workspace run task integration tests --- workspace_run_task_integration_test.go | 170 +++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 workspace_run_task_integration_test.go diff --git a/workspace_run_task_integration_test.go b/workspace_run_task_integration_test.go new file mode 100644 index 000000000..0c5214b35 --- /dev/null +++ b/workspace_run_task_integration_test.go @@ -0,0 +1,170 @@ +package tfe + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWorkspaceRunTasksCreate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) + defer runTaskTestCleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + t.Run("attach run task to workspace", func(t *testing.T) { + fmt.Println(wkspaceTest.ID) + fmt.Println(runTaskTest.ID) + wr, err := client.WorkspaceRunTasks.Create(ctx, wkspaceTest.ID, WorkspaceRunTaskCreateOptions{ + EnforcementLevel: Mandatory, + RunTask: runTaskTest, + }) + + require.NoError(t, err) + assert.NotEmpty(t, wr.ID) + assert.Equal(t, wr.EnforcementLevel, Mandatory) + + t.Run("ensure run task is deserialized properly", func(t *testing.T) { + assert.NotEmpty(t, wr.RunTask.ID) + }) + }) +} + +func TestWorkspaceRunTasksList(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + runTaskTest1, runTaskTest1Cleanup := createRunTask(t, client, orgTest) + defer runTaskTest1Cleanup() + + runTaskTest2, runTaskTest2Cleanup := createRunTask(t, client, orgTest) + defer runTaskTest2Cleanup() + + _, wrTaskTest1Cleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest1) + defer wrTaskTest1Cleanup() + + _, wrTaskTest2Cleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest2) + defer wrTaskTest2Cleanup() + + t.Run("with no params", func(t *testing.T) { + wrTaskList, err := client.WorkspaceRunTasks.List(ctx, wkspaceTest.ID, nil) + require.NoError(t, err) + assert.NotNil(t, wrTaskList.Items) + assert.Equal(t, len(wrTaskList.Items), 2) + assert.NotEmpty(t, wrTaskList.Items[0].ID) + assert.NotEmpty(t, wrTaskList.Items[0].EnforcementLevel) + }) +} + +func TestWorkspaceRunTasksRead(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) + defer runTaskTestCleanup() + + wrTaskTest, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest) + defer wrTaskTestCleanup() + + t.Run("by ID", func(t *testing.T) { + wr, err := client.WorkspaceRunTasks.Read(ctx, wkspaceTest.ID, wrTaskTest.ID) + require.NoError(t, err) + + assert.Equal(t, wrTaskTest.ID, wr.ID) + assert.Equal(t, wrTaskTest.EnforcementLevel, wr.EnforcementLevel) + + t.Run("ensure run task is deserialized", func(t *testing.T) { + assert.Equal(t, wr.RunTask.ID, runTaskTest.ID) + }) + + t.Run("ensure workspace is deserialized", func(t *testing.T) { + assert.Equal(t, wr.Workspace.ID, wkspaceTest.ID) + }) + }) +} + +func TestWorkspaceRunTasksUpdate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) + defer runTaskTestCleanup() + + wrTaskTest, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest) + defer wrTaskTestCleanup() + + t.Run("rename task", func(t *testing.T) { + wr, err := client.WorkspaceRunTasks.Update(ctx, wkspaceTest.ID, wrTaskTest.ID, WorkspaceRunTaskUpdateOptions{ + EnforcementLevel: Mandatory, + }) + require.NoError(t, err) + + wr, err = client.WorkspaceRunTasks.Read(ctx, wkspaceTest.ID, wr.ID) + require.NoError(t, err) + + assert.Equal(t, wr.EnforcementLevel, Mandatory) + }) +} + +func TestWorkspaceRunTasksDelete(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + // defer wkspaceTestCleanup() + + runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) + defer runTaskTestCleanup() + + wrTaskTest, _ := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest) + + t.Run("with valid options", func(t *testing.T) { + err := client.WorkspaceRunTasks.Delete(ctx, wkspaceTest.ID, wrTaskTest.ID) + require.NoError(t, err) + + _, err = client.WorkspaceRunTasks.Read(ctx, wkspaceTest.ID, wrTaskTest.ID) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the workspace run task does not exist", func(t *testing.T) { + err := client.WorkspaceRunTasks.Delete(ctx, wkspaceTest.ID, wrTaskTest.ID) + assert.Equal(t, err, ErrResourceNotFound) + }) + + t.Run("when the workspace does not exist", func(t *testing.T) { + wkspaceTestCleanup() + err := client.WorkspaceRunTasks.Delete(ctx, wkspaceTest.ID, wrTaskTest.ID) + assert.EqualError(t, err, ErrResourceNotFound.Error()) + }) +} From 4b91ad9eb3365fd09851338a2c0cfe9c2073af4c Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Tue, 11 Jan 2022 18:10:58 -0500 Subject: [PATCH 16/34] Fixed run task deserialization errors from tests, request body as pointer --- helper_test.go | 4 ++-- run_task.go | 12 ++++++------ workspace_run_task.go | 14 ++++---------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/helper_test.go b/helper_test.go index fe55103f1..df87c4d9d 100644 --- a/helper_test.go +++ b/helper_test.go @@ -713,7 +713,7 @@ func createRunTask(t *testing.T, client *Client, org *Organization) (*RunTask, f r, err := client.RunTasks.Create(ctx, org.Name, RunTaskCreateOptions{ Name: "tst-" + randomString(t), URL: "http://54.167.177.151/success", - Category: "tasks", + Category: "task", }) if err != nil { t.Fatal(err) @@ -1037,7 +1037,7 @@ func createWorkspaceRunTask(t *testing.T, client *Client, workspace *Workspace, ctx := context.Background() wr, err := client.WorkspaceRunTasks.Create(ctx, workspace.ID, WorkspaceRunTaskCreateOptions{ EnforcementLevel: Advisory, - RunTask: *runTask, + RunTask: runTask, }) if err != nil { t.Fatal(err) diff --git a/run_task.go b/run_task.go index 6ca99129f..528b40017 100644 --- a/run_task.go +++ b/run_task.go @@ -34,7 +34,7 @@ type RunTask struct { HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` Organization *Organization `jsonapi:"relation,organization"` - WorkspaceRunTasks []*WorkspaceRunTask `jsonapi:"relation,tasks"` + WorkspaceRunTasks []*WorkspaceRunTask `jsonapi:"relation,workspace-tasks"` } type RunTaskList struct { @@ -59,7 +59,7 @@ func (o *RunTaskCreateOptions) valid() error { return ErrInvalidRunTaskURL } - if o.Category != "tasks" { + if o.Category != "task" { return ErrInvalidRunTaskCategory } @@ -152,15 +152,15 @@ type RunTaskUpdateOptions struct { } func (o *RunTaskUpdateOptions) valid() error { - if !validString(o.Name) { + if o.Name != nil && !validString(o.Name) { return ErrRequiredName } - if !validString(o.URL) { + if o.URL != nil && !validString(o.URL) { return ErrInvalidRunTaskURL } - if *o.Category != "tasks" { + if o.Category != nil && *o.Category != "task" { return ErrInvalidRunTaskCategory } @@ -177,7 +177,7 @@ func (s *runTasks) Update(ctx context.Context, runTaskID string, options RunTask } u := fmt.Sprintf("tasks/%s", url.QueryEscape(runTaskID)) - req, err := s.client.newRequest("PATCH", u, options) + req, err := s.client.newRequest("PATCH", u, &options) if err != nil { return nil, err } diff --git a/workspace_run_task.go b/workspace_run_task.go index b17125d4c..7ee479b77 100644 --- a/workspace_run_task.go +++ b/workspace_run_task.go @@ -39,8 +39,6 @@ type WorkspaceRunTaskList struct { type WorkspaceRunTaskListOptions struct { ListOptions - - Include *string `url:"include,omitempty"` } func (s *workspaceRunTasks) List(ctx context.Context, workspaceID string, options *WorkspaceRunTaskListOptions) (*WorkspaceRunTaskList, error) { @@ -94,7 +92,7 @@ func (s *workspaceRunTasks) Read(ctx context.Context, workspaceID string, worksp type WorkspaceRunTaskCreateOptions struct { Type string `jsonapi:"primary,workspace-tasks"` EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"` - RunTask RunTask `jsoniapi:"relation,tasks"` + RunTask *RunTask `jsonapi:"relation,task"` } func (o *WorkspaceRunTaskCreateOptions) valid() error { @@ -102,10 +100,6 @@ func (o *WorkspaceRunTaskCreateOptions) valid() error { return ErrInvalidRunTaskID } - if o.Type != "workspace-tasks" { - return ErrInvalidWorkspaceRunTaskType - } - return nil } @@ -119,13 +113,13 @@ func (s *workspaceRunTasks) Create(ctx context.Context, workspaceID string, opti } u := fmt.Sprintf("workspaces/%s/tasks", workspaceID) - req, err := s.client.newRequest("POST", u, options) + req, err := s.client.newRequest("POST", u, &options) if err != nil { return nil, err } wr := &WorkspaceRunTask{} - err = s.client.do(ctx, req, &wr) + err = s.client.do(ctx, req, wr) if err != nil { return nil, err } @@ -135,7 +129,7 @@ func (s *workspaceRunTasks) Create(ctx context.Context, workspaceID string, opti type WorkspaceRunTaskUpdateOptions struct { Type string `jsonapi:"primary,workspace-tasks"` - EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"` + EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level,omitempty"` } func (s *workspaceRunTasks) Update(ctx context.Context, workspaceID string, workspaceTaskID string, options WorkspaceRunTaskUpdateOptions) (*WorkspaceRunTask, error) { From 2b6ccf239cbda6d87296fdd221afb07c34ca923c Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Tue, 11 Jan 2022 18:23:18 -0500 Subject: [PATCH 17/34] Refactored the timestamps for TaskResult and TaskStage Even though the struct definitions are identical, it may be the case that a TaskResult (or TaskStage) might support different or more timestamps in the future. This is effectively pre-emptively planning for future changes. --- task_result.go | 32 ++++++++++++++++++++------------ task_stages.go | 12 ++++++------ 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/task_result.go b/task_result.go index 064f013f2..6d112c69d 100644 --- a/task_result.go +++ b/task_result.go @@ -29,19 +29,27 @@ const ( Mandatory TaskEnforcementLevel = "mandatory" ) +type TaskResultStatusTimestamps struct { + ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"` + RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` + CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"` + FailedAt time.Time `jsonapi:"attr,failed-at,rfc3339"` + PassedAt time.Time `jsonapi:"attr,passed-at,rfc3339"` +} + type TaskResult struct { - ID string `jsonapi:"primary,task-results"` - Status TaskResultStatus `jsonapi:"attr,status"` - Message string `jsonapi:"attr,message"` - StatusTimestamps RunTaskStatusTimestamps `jsonapi:"attr,status-timestamps"` - URL string `jsonapi:"attr,url"` - CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` - UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` - TaskID string `jsonapi:"attr,task-id"` - TaskName string `jsonapi:"attr,task-name"` - TaskURL string `jsonapi:"attr,task-url"` - WorkspaceTaskID string `jsonapi:"attr,workspace-task-id"` - WorkspaceTaskEnforcementLevel TaskEnforcementLevel `jsonapi:"attr,workspace-task-enforcement-level"` + ID string `jsonapi:"primary,task-results"` + Status TaskResultStatus `jsonapi:"attr,status"` + Message string `jsonapi:"attr,message"` + StatusTimestamps TaskResultStatusTimestamps `jsonapi:"attr,status-timestamps"` + URL string `jsonapi:"attr,url"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + TaskID string `jsonapi:"attr,task-id"` + TaskName string `jsonapi:"attr,task-name"` + TaskURL string `jsonapi:"attr,task-url"` + WorkspaceTaskID string `jsonapi:"attr,workspace-task-id"` + WorkspaceTaskEnforcementLevel TaskEnforcementLevel `jsonapi:"attr,workspace-task-enforcement-level"` TaskStage *TaskStage `jsonapi:"relation,task_stage"` } diff --git a/task_stages.go b/task_stages.go index b360e805e..ab392b483 100644 --- a/task_stages.go +++ b/task_stages.go @@ -24,11 +24,11 @@ const ( ) type TaskStage struct { - ID string `jsonapi:"primary,task-stages"` - Stage Stage `jsonapi:"attr,stage"` - StatusTimestamps RunTaskStatusTimestamps `jsonapi:"attr,status-timestamps"` - CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` - UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` + ID string `jsonapi:"primary,task-stages"` + Stage Stage `jsonapi:"attr,stage"` + StatusTimestamps TaskStageStatusTimestamps `jsonapi:"attr,status-timestamps"` + CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"` + UpdatedAt time.Time `jsonapi:"attr,updated-at,iso8601"` Run *Run `jsonapi:"relation,run"` TaskResults []*TaskResult `jsonapi:"relation,task-results"` @@ -39,7 +39,7 @@ type TaskStageList struct { Items []*TaskStage } -type RunTaskStatusTimestamps struct { +type TaskStageStatusTimestamps struct { ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"` RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` CanceledAt time.Time `jsonapi:"attr,canceled-at,rfc3339"` From 14decd2f04da19ef1996d0c16d51a5e46e220a45 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 12 Jan 2022 13:18:18 -0500 Subject: [PATCH 18/34] Removed hardcoded values, using test helpers to test e2e --- task_stages_integration_test.go | 59 +++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/task_stages_integration_test.go b/task_stages_integration_test.go index c6094c816..abac2e363 100644 --- a/task_stages_integration_test.go +++ b/task_stages_integration_test.go @@ -12,11 +12,28 @@ func TestTaskStagesRead(t *testing.T) { client := testClient(t) ctx := context.Background() - // hardcoded currently - taskStageID := "ts-xzskdpGj36B4ZJGn" + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) + defer runTaskTestCleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + wrTaskTest, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest) + defer wrTaskTestCleanup() + + rTest, rTestCleanup := createRun(t, client, wkspaceTest) + defer rTestCleanup() t.Run("without include param", func(t *testing.T) { - taskStage, err := client.TaskStages.Read(ctx, taskStageID, nil) + r, err := client.Runs.ReadWithOptions(ctx, rTest.ID, &RunReadOptions{ + Include: "task_stages", + }) + require.NoError(t, err) + + taskStage, err := client.TaskStages.Read(ctx, r.TaskStages[0].ID, nil) require.NoError(t, err) assert.NotEmpty(t, taskStage.ID) @@ -36,14 +53,22 @@ func TestTaskStagesRead(t *testing.T) { }) t.Run("with include param task_results", func(t *testing.T) { - taskStage, err := client.TaskStages.Read(ctx, taskStageID, &TaskStageReadOptions{ + r, err := client.Runs.ReadWithOptions(ctx, rTest.ID, &RunReadOptions{ + Include: "task_stages", + }) + require.NoError(t, err) + + taskStage, err := client.TaskStages.Read(ctx, r.TaskStages[0].ID, &TaskStageReadOptions{ Include: "task_results", }) require.NoError(t, err) t.Run("task results are properly decoded", func(t *testing.T) { + assert.NotEmpty(t, taskStage.TaskResults[0].ID) assert.NotEmpty(t, taskStage.TaskResults[0].Status) - assert.NotEmpty(t, taskStage.TaskResults[0].Message) + assert.NotEmpty(t, taskStage.TaskResults[0].CreatedAt) + assert.Equal(t, wrTaskTest.ID, taskStage.TaskResults[0].WorkspaceTaskID) + assert.Equal(t, runTaskTest.Name, taskStage.TaskResults[0].TaskName) }) }) } @@ -52,13 +77,33 @@ func TestTaskStagesList(t *testing.T) { client := testClient(t) ctx := context.Background() - runID := "run-TRdorPvcxENJ5t52" + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) + defer runTaskTestCleanup() + + runTaskTest2, runTaskTest2Cleanup := createRunTask(t, client, orgTest) + defer runTaskTest2Cleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + _, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest) + defer wrTaskTestCleanup() + + _, wrTaskTest2Cleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest2) + defer wrTaskTest2Cleanup() + + rTest, rTestCleanup := createRun(t, client, wkspaceTest) + defer rTestCleanup() t.Run("with no params", func(t *testing.T) { - taskStageList, err := client.TaskStages.List(ctx, runID, nil) + taskStageList, err := client.TaskStages.List(ctx, rTest.ID, nil) require.NoError(t, err) assert.NotNil(t, taskStageList.Items) assert.NotEmpty(t, taskStageList.Items[0].ID) + assert.Equal(t, 2, len(taskStageList.Items[0].TaskResults)) }) } From f3f1b54c60704231730093b90603187de170e774 Mon Sep 17 00:00:00 2001 From: uturunku1 Date: Wed, 12 Jan 2022 13:37:09 -0500 Subject: [PATCH 19/34] change values to string enum Stage --- task_stages.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/task_stages.go b/task_stages.go index ab392b483..8e77c1e69 100644 --- a/task_stages.go +++ b/task_stages.go @@ -19,8 +19,8 @@ type taskStages struct { type Stage string const ( - PreApply Stage = "pre-apply" - PostPlan Stage = "post-plan" + PreApply Stage = "pre_apply" + PostPlan Stage = "post_plan" ) type TaskStage struct { From b3149a95199e9bbdf85caeb54f74f6819232aad2 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 12 Jan 2022 13:36:35 -0500 Subject: [PATCH 20/34] Added a convenience function for attaching run tasks to a workspace WorkspaceRunTasks.Create() is not as intuitive to the user and doesn't clearly express the action that's happening. When a workspace run task is created the action is "attaching a run task to a workspace". The API docs also state this action instead of "create workspace run task". Therefore this method adds a more expressive wrapper over Create() keeping the user within the "Run Tasks" context. --- run_task.go | 9 +++++++++ run_task_integration_test.go | 20 ++++++++++++++++++++ task_stages.go | 1 - 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/run_task.go b/run_task.go index 528b40017..d4b000868 100644 --- a/run_task.go +++ b/run_task.go @@ -20,6 +20,8 @@ type RunTasks interface { Update(ctx context.Context, runTaskID string, options RunTaskUpdateOptions) (*RunTask, error) Delete(ctx context.Context, runTaskID string) error + + AttachToWorkspace(ctx context.Context, workspaceID string, runTaskID string, enforcementLevel TaskEnforcementLevel) (*WorkspaceRunTask, error) } type runTasks struct { @@ -204,3 +206,10 @@ func (s *runTasks) Delete(ctx context.Context, runTaskID string) error { return s.client.do(ctx, req, nil) } + +func (s *runTasks) AttachToWorkspace(ctx context.Context, workspaceID string, runTaskID string, enforcement TaskEnforcementLevel) (*WorkspaceRunTask, error) { + return s.client.WorkspaceRunTasks.Create(ctx, workspaceID, WorkspaceRunTaskCreateOptions{ + EnforcementLevel: enforcement, + RunTask: &RunTask{ID: runTaskID}, + }) +} diff --git a/run_task_integration_test.go b/run_task_integration_test.go index 063da4de5..39b240576 100644 --- a/run_task_integration_test.go +++ b/run_task_integration_test.go @@ -159,3 +159,23 @@ func TestRunTasksDelete(t *testing.T) { assert.EqualError(t, err, ErrInvalidRunTaskID.Error()) }) } + +func TestRunTasksAttachToWorkspace(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) + defer runTaskTestCleanup() + + wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) + defer wkspaceTestCleanup() + + t.Run("to a valid workspace", func(t *testing.T) { + wr, err := client.RunTasks.AttachToWorkspace(ctx, wkspaceTest.ID, runTaskTest.ID, Advisory) + require.NoError(t, err) + require.NotNil(t, wr.ID) + }) +} diff --git a/task_stages.go b/task_stages.go index 8e77c1e69..5ee858c25 100644 --- a/task_stages.go +++ b/task_stages.go @@ -20,7 +20,6 @@ type Stage string const ( PreApply Stage = "pre_apply" - PostPlan Stage = "post_plan" ) type TaskStage struct { From 5e2f288ce90c95d666b7303d99869b730a7c19cc Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Mon, 7 Feb 2022 12:25:59 -0500 Subject: [PATCH 21/34] Code documentation for run tasks --- errors.go | 2 +- run_task.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/errors.go b/errors.go index d65ba9f16..7183376b7 100644 --- a/errors.go +++ b/errors.go @@ -55,7 +55,7 @@ var ( // Run Task errors // ErrInvalidRunTaskCategory is returned when a run task has a category other than "task" - ErrInvalidRunTaskCategory = errors.New(`category must be "tasks"`) + ErrInvalidRunTaskCategory = errors.New(`category must be "task"`) // ErrInvalidRunTaskID is returned when the run task ID is invalid ErrInvalidRunTaskID = errors.New("invalid value for run task ID") diff --git a/run_task.go b/run_task.go index d4b000868..399098fe1 100644 --- a/run_task.go +++ b/run_task.go @@ -6,28 +6,41 @@ import ( "net/url" ) +// Compile-time proof of interface implementation var _ RunTasks = (*runTasks)(nil) +// RunTasks represents all the run task related methods in the context of an organization +// that the Terraform Cloud/Enterprise API supports. +// **Note: This API is still in BETA and subject to change.** type RunTasks interface { + // Create a run task for an organization Create(ctx context.Context, organization string, options RunTaskCreateOptions) (*RunTask, error) + // List all run tasks for an organization List(ctx context.Context, organization string, options *RunTaskListOptions) (*RunTaskList, error) + // Read an organization's run task by ID Read(ctx context.Context, runTaskID string) (*RunTask, error) + // Read an organization's run task by ID with given options ReadWithOptions(ctx context.Context, runTaskID string, options *RunTaskReadOptions) (*RunTask, error) + // Update a run task for an organization Update(ctx context.Context, runTaskID string, options RunTaskUpdateOptions) (*RunTask, error) + // Delete an organization's run task Delete(ctx context.Context, runTaskID string) error + // Attach a run task to an organization's workspace AttachToWorkspace(ctx context.Context, workspaceID string, runTaskID string, enforcementLevel TaskEnforcementLevel) (*WorkspaceRunTask, error) } +// runTasks implements RunTasks type runTasks struct { client *Client } +// RunTask represents a TFC/E run task type RunTask struct { ID string `jsonapi:"primary,tasks"` Name string `jsonapi:"attr,name"` @@ -39,17 +52,31 @@ type RunTask struct { WorkspaceRunTasks []*WorkspaceRunTask `jsonapi:"relation,workspace-tasks"` } +// RunTaskList represents a list of run tasks type RunTaskList struct { *Pagination Items []*RunTask } +// RunTaskCreateOptions represents the set of options for creating a run task type RunTaskCreateOptions struct { - Type string `jsonapi:"primary,tasks"` - Name string `jsonapi:"attr,name"` - URL string `jsonapi:"attr,url"` - Category string `jsonapi:"attr,category"` - HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,tasks"` + + // Required: The name of the run task + Name string `jsonapi:"attr,name"` + + // Required: The URL to send a run task payload + URL string `jsonapi:"attr,url"` + + // Required: Must be "task" + Category string `jsonapi:"attr,category"` + + // Optional: An HMAC key to verify the run task + HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` } func (o *RunTaskCreateOptions) valid() error { @@ -68,6 +95,7 @@ func (o *RunTaskCreateOptions) valid() error { return nil } +// Create is used to create a new run task for an organization func (s *runTasks) Create(ctx context.Context, organization string, options RunTaskCreateOptions) (*RunTask, error) { if !validStringID(&organization) { return nil, ErrInvalidOrg @@ -92,11 +120,14 @@ func (s *runTasks) Create(ctx context.Context, organization string, options RunT return r, nil } +// RunTaskListOptions represents the set of options for listing run tasks type RunTaskListOptions struct { + // A list of relations to include Include string `url:"include"` ListOptions } +// List all the run tasks for an organization func (s *runTasks) List(ctx context.Context, organization string, options *RunTaskListOptions) (*RunTaskList, error) { if !validStringID(&organization) { return nil, ErrInvalidOrg @@ -117,14 +148,17 @@ func (s *runTasks) List(ctx context.Context, organization string, options *RunTa return rl, nil } +// Read is used to read an organization's run task by ID func (s *runTasks) Read(ctx context.Context, runTaskID string) (*RunTask, error) { return s.ReadWithOptions(ctx, runTaskID, nil) } +// RunTaskReadOptions represents the set of options for reading a run task type RunTaskReadOptions struct { Include string `url:"include"` } +// Read is used to read an organization's run task by ID with options func (s *runTasks) ReadWithOptions(ctx context.Context, runTaskID string, options *RunTaskReadOptions) (*RunTask, error) { if !validStringID(&runTaskID) { return nil, ErrInvalidRunTaskID @@ -145,12 +179,25 @@ func (s *runTasks) ReadWithOptions(ctx context.Context, runTaskID string, option return r, nil } +// RunTaskUpdateOptions represents the set of options for updating an organization's run task type RunTaskUpdateOptions struct { - Type string `jsonapi:"primary,tasks"` - Name *string `jsonapi:"attr,name,omitempty"` - URL *string `jsonapi:"attr,url,omitempty"` + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,tasks"` + + // Optional: The name of the run task, defaults to previous value + Name *string `jsonapi:"attr,name,omitempty"` + + // Optional: The URL to send a run task payload, defaults to previous value + URL *string `jsonapi:"attr,url,omitempty"` + + // Optional: Must be "task", defaults to "task" Category *string `jsonapi:"attr,category,omitempty"` - HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` + + // Optional: An HMAC key to verify the run task + HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` } func (o *RunTaskUpdateOptions) valid() error { @@ -169,6 +216,7 @@ func (o *RunTaskUpdateOptions) valid() error { return nil } +// Update an existing run task for an organization by ID func (s *runTasks) Update(ctx context.Context, runTaskID string, options RunTaskUpdateOptions) (*RunTask, error) { if !validStringID(&runTaskID) { return nil, ErrInvalidRunTaskID @@ -193,6 +241,7 @@ func (s *runTasks) Update(ctx context.Context, runTaskID string, options RunTask return r, nil } +// Delete an existing run task for an organization by ID func (s *runTasks) Delete(ctx context.Context, runTaskID string) error { if !validStringID(&runTaskID) { return ErrInvalidRunTaskID @@ -207,6 +256,7 @@ func (s *runTasks) Delete(ctx context.Context, runTaskID string) error { return s.client.do(ctx, req, nil) } +// Convenient method to attach a run task to a workspace. See: WorkspaceRunTasks.Create() func (s *runTasks) AttachToWorkspace(ctx context.Context, workspaceID string, runTaskID string, enforcement TaskEnforcementLevel) (*WorkspaceRunTask, error) { return s.client.WorkspaceRunTasks.Create(ctx, workspaceID, WorkspaceRunTaskCreateOptions{ EnforcementLevel: enforcement, From 947487fbb39d5d8ca9b42a33d7077d585daa97a0 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Tue, 8 Feb 2022 14:43:02 -0500 Subject: [PATCH 22/34] Code documentation for task stage and task result endpoints (beta) --- task_result.go | 27 ++++++++++++++++++++------- task_stages.go | 18 ++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/task_result.go b/task_result.go index 6d112c69d..e67e6f38a 100644 --- a/task_result.go +++ b/task_result.go @@ -6,29 +6,39 @@ import ( "time" ) +// Compile-time proof of interface implementation var _ TaskResults = (*taskResults)(nil) +// TaskResults describes all the task result related methods that the TFC/E API supports. +// **Note**: This API is still in BETA and is subject to change type TaskResults interface { + // Read a task result by ID Read(ctx context.Context, taskResultID string) (*TaskResult, error) } +// taskResults implements TaskResults type taskResults struct { client *Client } +//TaskResultStatus is an enum that represents all possible statuses for a task result type TaskResultStatus string + +//TaskEnforcementLevel is an enum that describes the enforcement levels for a run task type TaskEnforcementLevel string const ( - TaskPassed TaskResultStatus = "passed" - TaskFailed TaskResultStatus = "failed" - TaskRunning TaskResultStatus = "running" - TaskPending TaskResultStatus = "pending" - TaskUnreachable TaskResultStatus = "unreachable" - Advisory TaskEnforcementLevel = "advisory" - Mandatory TaskEnforcementLevel = "mandatory" + TaskPassed TaskResultStatus = "passed" + TaskFailed TaskResultStatus = "failed" + TaskRunning TaskResultStatus = "running" + TaskPending TaskResultStatus = "pending" + TaskUnreachable TaskResultStatus = "unreachable" + + Advisory TaskEnforcementLevel = "advisory" + Mandatory TaskEnforcementLevel = "mandatory" ) +// TaskResultStatusTimestamps represents the set of timestamps recorded for a task result type TaskResultStatusTimestamps struct { ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"` RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` @@ -37,6 +47,7 @@ type TaskResultStatusTimestamps struct { PassedAt time.Time `jsonapi:"attr,passed-at,rfc3339"` } +// TaskResult represents the result of a TFC/E run task type TaskResult struct { ID string `jsonapi:"primary,task-results"` Status TaskResultStatus `jsonapi:"attr,status"` @@ -51,9 +62,11 @@ type TaskResult struct { WorkspaceTaskID string `jsonapi:"attr,workspace-task-id"` WorkspaceTaskEnforcementLevel TaskEnforcementLevel `jsonapi:"attr,workspace-task-enforcement-level"` + // The task stage this result belongs to TaskStage *TaskStage `jsonapi:"relation,task_stage"` } +// Read a task result by ID func (t *taskResults) Read(ctx context.Context, taskResultID string) (*TaskResult, error) { if !validStringID(&taskResultID) { return nil, ErrInvalidTaskResultID diff --git a/task_stages.go b/task_stages.go index 5ee858c25..1f1a74360 100644 --- a/task_stages.go +++ b/task_stages.go @@ -6,22 +6,34 @@ import ( "time" ) +// Compile-time proof of interface implementation +var _ TaskStages = (*taskStages)(nil) + +// TaskStages describes all the task stage related methods that the TFC/E API +// supports. +// **Note: This API is still in BETA and is subject to change.** type TaskStages interface { + // Read a task stage by ID Read(ctx context.Context, taskStageID string, options *TaskStageReadOptions) (*TaskStage, error) + // List all task stages for a given rrun List(ctx context.Context, runID string, options *TaskStageListOptions) (*TaskStageList, error) } +// taskStages implements TaskStages type taskStages struct { client *Client } +// Stage is an enum that represents the possible run stages for run tasks type Stage string const ( + PostPlan Stage = "post_plan" PreApply Stage = "pre_apply" ) +// TaskStage represents a TFC/E run's stage where run tasks can occur type TaskStage struct { ID string `jsonapi:"primary,task-stages"` Stage Stage `jsonapi:"attr,stage"` @@ -33,11 +45,13 @@ type TaskStage struct { TaskResults []*TaskResult `jsonapi:"relation,task-results"` } +// TaskStageList represents a list of task stages type TaskStageList struct { *Pagination Items []*TaskStage } +// TaskStageStatusTimestamps represents the set of timestamps recorded for a task stage type TaskStageStatusTimestamps struct { ErroredAt time.Time `jsonapi:"attr,errored-at,rfc3339"` RunningAt time.Time `jsonapi:"attr,running-at,rfc3339"` @@ -46,10 +60,12 @@ type TaskStageStatusTimestamps struct { PassedAt time.Time `jsonapi:"attr,passed-at,rfc3339"` } +// TaskStageReadOptions represents the set of options when reading a task stage type TaskStageReadOptions struct { Include string `url:"include"` } +// Read a task stage by ID func (s *taskStages) Read(ctx context.Context, taskStageID string, options *TaskStageReadOptions) (*TaskStage, error) { if !validStringID(&taskStageID) { return nil, ErrInvalidTaskStageID @@ -70,10 +86,12 @@ func (s *taskStages) Read(ctx context.Context, taskStageID string, options *Task return t, nil } +// TaskStageListOptions represents the options for listing task stages for a run type TaskStageListOptions struct { ListOptions } +// List task stages for a run func (s *taskStages) List(ctx context.Context, runID string, options *TaskStageListOptions) (*TaskStageList, error) { if !validStringID(&runID) { return nil, ErrInvalidRunID From 29a3c510a3f03ea56bc0323d800b89bb32de2f70 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Tue, 8 Feb 2022 14:43:28 -0500 Subject: [PATCH 23/34] Code documentation for workspace run tasks --- workspace_run_task.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/workspace_run_task.go b/workspace_run_task.go index 7ee479b77..40f64875b 100644 --- a/workspace_run_task.go +++ b/workspace_run_task.go @@ -6,24 +6,34 @@ import ( "net/url" ) +// Compile-time proof of interface implementation var _ WorkspaceRunTasks = (*workspaceRunTasks)(nil) +// WorkspaceRunTasks represent all the run task related methods in the context of a workspace that the Terraform Cloud/Enterprise API supports. +// **Note: This API is still in BETA and subject to change.** type WorkspaceRunTasks interface { + // Add a run task to a workspace Create(ctx context.Context, workspaceID string, options WorkspaceRunTaskCreateOptions) (*WorkspaceRunTask, error) + // List all run tasks for a workspace List(ctx context.Context, workspaceID string, options *WorkspaceRunTaskListOptions) (*WorkspaceRunTaskList, error) + // Read a workspace run task by ID Read(ctx context.Context, workspaceID string, workspaceTaskID string) (*WorkspaceRunTask, error) + // Update a workspace run task by ID Update(ctx context.Context, workspaceID string, workspaceTaskID string, options WorkspaceRunTaskUpdateOptions) (*WorkspaceRunTask, error) + // Delete a workspace's run task by ID Delete(ctx context.Context, workspaceID string, workspaceTaskID string) error } +// workspaceRunTasks implements WorkspaceRunTasks type workspaceRunTasks struct { client *Client } +// WorkspaceRunTask represents a TFC/E run task that belongs to a workspace type WorkspaceRunTask struct { ID string `jsonapi:"primary,workspace-tasks"` EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"` @@ -32,15 +42,18 @@ type WorkspaceRunTask struct { Workspace *Workspace `jsonapi:"relation,workspace"` } +// WorkspaceRunTaskList represents a list of workspace run tasks type WorkspaceRunTaskList struct { *Pagination Items []*WorkspaceRunTask } +// WorkspaceRunTaskListOptions represents the set of options for listing workspace run tasks type WorkspaceRunTaskListOptions struct { ListOptions } +// List all run tasks attached to a workspace func (s *workspaceRunTasks) List(ctx context.Context, workspaceID string, options *WorkspaceRunTaskListOptions) (*WorkspaceRunTaskList, error) { if !validStringID(&workspaceID) { return nil, ErrInvalidWorkspaceID @@ -61,6 +74,7 @@ func (s *workspaceRunTasks) List(ctx context.Context, workspaceID string, option return rl, nil } +// Read a workspace run task by ID func (s *workspaceRunTasks) Read(ctx context.Context, workspaceID string, workspaceTaskID string) (*WorkspaceRunTask, error) { if !validStringID(&workspaceID) { return nil, ErrInvalidWorkspaceID @@ -89,10 +103,13 @@ func (s *workspaceRunTasks) Read(ctx context.Context, workspaceID string, worksp return wr, nil } +// WorkspaceRunTaskCreateOptions represents the set of options for creating a workspace run task type WorkspaceRunTaskCreateOptions struct { - Type string `jsonapi:"primary,workspace-tasks"` + Type string `jsonapi:"primary,workspace-tasks"` + // Required: The enforcement level for a run task EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"` - RunTask *RunTask `jsonapi:"relation,task"` + // Required: The run task to attach to the workspace + RunTask *RunTask `jsonapi:"relation,task"` } func (o *WorkspaceRunTaskCreateOptions) valid() error { @@ -103,6 +120,7 @@ func (o *WorkspaceRunTaskCreateOptions) valid() error { return nil } +// Create is used to attach a run task to a workspace, or in other words: create a workspace run task. The run task must exist in the workspace's organization. func (s *workspaceRunTasks) Create(ctx context.Context, workspaceID string, options WorkspaceRunTaskCreateOptions) (*WorkspaceRunTask, error) { if !validStringID(&workspaceID) { return nil, ErrInvalidWorkspaceID @@ -127,11 +145,13 @@ func (s *workspaceRunTasks) Create(ctx context.Context, workspaceID string, opti return wr, nil } +// WorkspaceRunTaskUpdateOptions represent the set of options for updating a workspace run task. type WorkspaceRunTaskUpdateOptions struct { Type string `jsonapi:"primary,workspace-tasks"` EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level,omitempty"` } +// Update an existing workspace run task by ID func (s *workspaceRunTasks) Update(ctx context.Context, workspaceID string, workspaceTaskID string, options WorkspaceRunTaskUpdateOptions) (*WorkspaceRunTask, error) { if !validStringID(&workspaceID) { return nil, ErrInvalidWorkspaceID @@ -160,6 +180,7 @@ func (s *workspaceRunTasks) Update(ctx context.Context, workspaceID string, work return wr, nil } +// Delete a workspace run task by ID func (s *workspaceRunTasks) Delete(ctx context.Context, workspaceID string, workspaceTaskID string) error { if !validStringID(&workspaceID) { return ErrInvalidWorkspaceID From edf67e732f0170561c2c7a541be0f75a0db74932 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Mon, 14 Feb 2022 12:57:56 -0500 Subject: [PATCH 24/34] Add skipIfBeta test helper This test helper is built to handle tests that require access to beta features. CI testing currently does not support beta features. Until there is a clearer strategy on how to handle testing beta features, this will serve as the intermediary solution. --- helper_test.go | 12 ++++++++++++ run_task_integration_test.go | 12 ++++++++++++ task_stages_integration_test.go | 4 ++++ workspace_run_task_integration_test.go | 10 ++++++++++ 4 files changed, 38 insertions(+) diff --git a/helper_test.go b/helper_test.go index df87c4d9d..6fc123c4a 100644 --- a/helper_test.go +++ b/helper_test.go @@ -1105,6 +1105,13 @@ func skipIfFreeOnly(t *testing.T) { } } +// skips a test if the test requires a beta feature +func skipIfBeta(t *testing.T) { + if !betaFeaturesEnabled() { + t.Skip("Skipping test related to a Terraform Cloud beta feature. Set ENABLE_BETA=1 to run.") + } +} + // Checks to see if ENABLE_TFE is set to 1, thereby enabling enterprise tests. func enterpriseEnabled() bool { return os.Getenv("ENABLE_TFE") == "1" @@ -1113,3 +1120,8 @@ func enterpriseEnabled() bool { func paidFeaturesDisabled() bool { return os.Getenv("SKIP_PAID") == "1" } + +// Checks to see if ENABLE_BETA is set to 1, thereby enabling tests for beta features. +func betaFeaturesEnabled() bool { + return os.Getenv("ENABLE_BETA") == "1" +} diff --git a/run_task_integration_test.go b/run_task_integration_test.go index 39b240576..2f2bb78c4 100644 --- a/run_task_integration_test.go +++ b/run_task_integration_test.go @@ -9,6 +9,8 @@ import ( ) func TestRunTasksCreate(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() @@ -38,6 +40,8 @@ func TestRunTasksCreate(t *testing.T) { } func TestRunTasksList(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() @@ -62,6 +66,8 @@ func TestRunTasksList(t *testing.T) { } func TestRunTasksRead(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() @@ -109,6 +115,8 @@ func TestRunTasksRead(t *testing.T) { } func TestRunTasksUpdate(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() @@ -133,6 +141,8 @@ func TestRunTasksUpdate(t *testing.T) { } func TestRunTasksDelete(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() @@ -161,6 +171,8 @@ func TestRunTasksDelete(t *testing.T) { } func TestRunTasksAttachToWorkspace(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() diff --git a/task_stages_integration_test.go b/task_stages_integration_test.go index abac2e363..2fbe34bda 100644 --- a/task_stages_integration_test.go +++ b/task_stages_integration_test.go @@ -9,6 +9,8 @@ import ( ) func TestTaskStagesRead(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() @@ -74,6 +76,8 @@ func TestTaskStagesRead(t *testing.T) { } func TestTaskStagesList(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() diff --git a/workspace_run_task_integration_test.go b/workspace_run_task_integration_test.go index 0c5214b35..ee0f040f5 100644 --- a/workspace_run_task_integration_test.go +++ b/workspace_run_task_integration_test.go @@ -10,6 +10,8 @@ import ( ) func TestWorkspaceRunTasksCreate(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() @@ -41,6 +43,8 @@ func TestWorkspaceRunTasksCreate(t *testing.T) { } func TestWorkspaceRunTasksList(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() @@ -73,6 +77,8 @@ func TestWorkspaceRunTasksList(t *testing.T) { } func TestWorkspaceRunTasksRead(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() @@ -106,6 +112,8 @@ func TestWorkspaceRunTasksRead(t *testing.T) { } func TestWorkspaceRunTasksUpdate(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() @@ -135,6 +143,8 @@ func TestWorkspaceRunTasksUpdate(t *testing.T) { } func TestWorkspaceRunTasksDelete(t *testing.T) { + skipIfBeta(t) + client := testClient(t) ctx := context.Background() From 87033a9065415590783e07c3e9902e2265c9b7ed Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Mon, 14 Feb 2022 13:06:08 -0500 Subject: [PATCH 25/34] Update optional env vars to include ENABLE_BETA --- TESTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TESTS.md b/TESTS.md index 8f171d6df..9ccb8f85e 100644 --- a/TESTS.md +++ b/TESTS.md @@ -27,6 +27,7 @@ Tests are run against an actual backend so they require a valid backend address 1. `GITHUB_REGISTRY_MODULE_IDENTIFIER` - GitHub registry module repository identifier in the format `username/repository`. Required for running registry module tests. 1. `ENABLE_TFE` - Some tests are only applicable to Terraform Enterprise or Terraform Cloud. By setting `ENABLE_TFE=1` you will enable enterprise only tests and disable cloud only tests. In CI `ENABLE_TFE` is not set so if you are writing enterprise only features you should manually test with `ENABLE_TFE=1` against a Terraform Enterprise instance. 1. `SKIP_PAID` - Some tests depend on paid only features. By setting `SKIP_PAID=1`, you will skip tests that access paid features. +1. `ENABLE_BETA` - Some tests require access to beta features. By setting `ENABLE_BETA=1` you will enable tests that require access to beta features. IN CI `ENABLE_BETA` is not set so if you are writing beta only features you should manually test with `ENABLE_BETA=1` against a Terraform Enterprise instance with those features enabled. You can set your environment variables up however you prefer. The following are instructions for setting up environment variables using [envchain](https://github.com/sorah/envchain). 1. Make sure you have envchain installed. [Instructions for this can be found in the envchain README](https://github.com/sorah/envchain#installation). From 307fac061fcab9fd7075a9e2301ece9662508ec4 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Tue, 15 Feb 2022 14:19:26 -0500 Subject: [PATCH 26/34] Read URL from env var when creating run tasks to test --- TESTS.md | 1 + helper_test.go | 7 ++++++- run_task_integration_test.go | 11 ++++++++--- workspace_run_task_integration_test.go | 5 +---- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/TESTS.md b/TESTS.md index 9ccb8f85e..6e2a327d8 100644 --- a/TESTS.md +++ b/TESTS.md @@ -28,6 +28,7 @@ Tests are run against an actual backend so they require a valid backend address 1. `ENABLE_TFE` - Some tests are only applicable to Terraform Enterprise or Terraform Cloud. By setting `ENABLE_TFE=1` you will enable enterprise only tests and disable cloud only tests. In CI `ENABLE_TFE` is not set so if you are writing enterprise only features you should manually test with `ENABLE_TFE=1` against a Terraform Enterprise instance. 1. `SKIP_PAID` - Some tests depend on paid only features. By setting `SKIP_PAID=1`, you will skip tests that access paid features. 1. `ENABLE_BETA` - Some tests require access to beta features. By setting `ENABLE_BETA=1` you will enable tests that require access to beta features. IN CI `ENABLE_BETA` is not set so if you are writing beta only features you should manually test with `ENABLE_BETA=1` against a Terraform Enterprise instance with those features enabled. +1. `TFC_RUN_TASK_URL` - Run task integration tests require a URL to use when creating run tasks. To learn more about the Run Task API, [read here](https://www.terraform.io/cloud-docs/api-docs/run-tasks) You can set your environment variables up however you prefer. The following are instructions for setting up environment variables using [envchain](https://github.com/sorah/envchain). 1. Make sure you have envchain installed. [Instructions for this can be found in the envchain README](https://github.com/sorah/envchain#installation). diff --git a/helper_test.go b/helper_test.go index 6fc123c4a..55995cd95 100644 --- a/helper_test.go +++ b/helper_test.go @@ -709,10 +709,15 @@ func createRunTask(t *testing.T, client *Client, org *Organization) (*RunTask, f org, orgCleanup = createOrganization(t, client) } + runTaskURL := os.Getenv("TFC_RUN_TASK_URL") + if runTaskURL == "" { + t.Error("Cannot create a run task with an empty URL. You must set TFC_RUN_TASK_URL for run task related tests.") + } + ctx := context.Background() r, err := client.RunTasks.Create(ctx, org.Name, RunTaskCreateOptions{ Name: "tst-" + randomString(t), - URL: "http://54.167.177.151/success", + URL: runTaskURL, Category: "task", }) if err != nil { diff --git a/run_task_integration_test.go b/run_task_integration_test.go index 2f2bb78c4..e901215ad 100644 --- a/run_task_integration_test.go +++ b/run_task_integration_test.go @@ -2,6 +2,7 @@ package tfe import ( "context" + "os" "testing" "github.com/stretchr/testify/assert" @@ -17,20 +18,24 @@ func TestRunTasksCreate(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) defer orgTestCleanup() - runTaskServerURI := "http://54.167.177.151/success" + runTaskServerURL := os.Getenv("TFC_RUN_TASK_URL") + if runTaskServerURL == "" { + t.Error("Cannot create a run task with an empty URL. You must set TFC_RUN_TASK_URL for run task related tests.") + } + runTaskName := "tst-runtask-" + randomString(t) t.Run("add run task to organization", func(t *testing.T) { r, err := client.RunTasks.Create(ctx, orgTest.Name, RunTaskCreateOptions{ Name: runTaskName, - URL: runTaskServerURI, + URL: runTaskServerURL, Category: "task", }) require.NoError(t, err) assert.NotEmpty(t, r.ID) assert.Equal(t, r.Name, runTaskName) - assert.Equal(t, r.URL, runTaskServerURI) + assert.Equal(t, r.URL, runTaskServerURL) assert.Equal(t, r.Category, "task") t.Run("ensure org is deserialized properly", func(t *testing.T) { diff --git a/workspace_run_task_integration_test.go b/workspace_run_task_integration_test.go index ee0f040f5..f54981a81 100644 --- a/workspace_run_task_integration_test.go +++ b/workspace_run_task_integration_test.go @@ -2,7 +2,6 @@ package tfe import ( "context" - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -25,8 +24,6 @@ func TestWorkspaceRunTasksCreate(t *testing.T) { defer wkspaceTestCleanup() t.Run("attach run task to workspace", func(t *testing.T) { - fmt.Println(wkspaceTest.ID) - fmt.Println(runTaskTest.ID) wr, err := client.WorkspaceRunTasks.Create(ctx, wkspaceTest.ID, WorkspaceRunTaskCreateOptions{ EnforcementLevel: Mandatory, RunTask: runTaskTest, @@ -152,7 +149,7 @@ func TestWorkspaceRunTasksDelete(t *testing.T) { defer orgTestCleanup() wkspaceTest, wkspaceTestCleanup := createWorkspace(t, client, orgTest) - // defer wkspaceTestCleanup() + defer wkspaceTestCleanup() runTaskTest, runTaskTestCleanup := createRunTask(t, client, orgTest) defer runTaskTestCleanup() From 79b4ab5231642d63caa68e59d864cc9fffd2bfbc Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Tue, 15 Feb 2022 14:46:07 -0500 Subject: [PATCH 27/34] Using string enums for include property --- run_task.go | 16 +++++++++++++--- run_task_integration_test.go | 2 +- task_stages.go | 10 ++++++++-- task_stages_integration_test.go | 2 +- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/run_task.go b/run_task.go index 399098fe1..38b446034 100644 --- a/run_task.go +++ b/run_task.go @@ -120,11 +120,21 @@ func (s *runTasks) Create(ctx context.Context, organization string, options RunT return r, nil } +// A list of relations to include with a run task. See available resources: +// https://www.terraform.io/cloud-docs/api-docs/run-tasks#list-run-tasks +type RunTaskIncludeOps string + +const ( + RunTaskWorkspaceTasks RunTaskIncludeOps = "workspace_tasks" + RunTaskWorkspace RunTaskIncludeOps = "workspace_tasks.workspace" +) + // RunTaskListOptions represents the set of options for listing run tasks type RunTaskListOptions struct { - // A list of relations to include - Include string `url:"include"` ListOptions + + // A list of relations to include + Include []RunTaskIncludeOps `url:"include"` } // List all the run tasks for an organization @@ -155,7 +165,7 @@ func (s *runTasks) Read(ctx context.Context, runTaskID string) (*RunTask, error) // RunTaskReadOptions represents the set of options for reading a run task type RunTaskReadOptions struct { - Include string `url:"include"` + Include []RunTaskIncludeOps `url:"include"` } // Read is used to read an organization's run task by ID with options diff --git a/run_task_integration_test.go b/run_task_integration_test.go index e901215ad..d775c04a8 100644 --- a/run_task_integration_test.go +++ b/run_task_integration_test.go @@ -106,7 +106,7 @@ func TestRunTasksRead(t *testing.T) { defer wrTest2Cleanup() r, err := client.RunTasks.ReadWithOptions(ctx, runTaskTest.ID, &RunTaskReadOptions{ - Include: "workspace_tasks", + Include: []RunTaskIncludeOps{RunTaskWorkspaceTasks}, }) require.NoError(t, err) diff --git a/task_stages.go b/task_stages.go index 1f1a74360..4c2c124d5 100644 --- a/task_stages.go +++ b/task_stages.go @@ -30,7 +30,6 @@ type Stage string const ( PostPlan Stage = "post_plan" - PreApply Stage = "pre_apply" ) // TaskStage represents a TFC/E run's stage where run tasks can occur @@ -60,9 +59,16 @@ type TaskStageStatusTimestamps struct { PassedAt time.Time `jsonapi:"attr,passed-at,rfc3339"` } +// A list of relations to include. +type TaskStageIncludeOps string + +const ( + TaskStageTaskResults TaskStageIncludeOps = "task_results" +) + // TaskStageReadOptions represents the set of options when reading a task stage type TaskStageReadOptions struct { - Include string `url:"include"` + Include []TaskStageIncludeOps `url:"include"` } // Read a task stage by ID diff --git a/task_stages_integration_test.go b/task_stages_integration_test.go index 2fbe34bda..259fbd6de 100644 --- a/task_stages_integration_test.go +++ b/task_stages_integration_test.go @@ -61,7 +61,7 @@ func TestTaskStagesRead(t *testing.T) { require.NoError(t, err) taskStage, err := client.TaskStages.Read(ctx, r.TaskStages[0].ID, &TaskStageReadOptions{ - Include: "task_results", + Include: []TaskStageIncludeOps{TaskStageTaskResults}, }) require.NoError(t, err) From c28500209eb73824c7d86cf1114ebd08e45589f7 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 16 Feb 2022 10:58:24 -0500 Subject: [PATCH 28/34] Change pre apply run statuses to post plan --- run.go | 4 ++-- run_task.go | 6 +++--- run_task_integration_test.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/run.go b/run.go index b0d6a0552..ff4954aa3 100644 --- a/run.go +++ b/run.go @@ -69,8 +69,8 @@ const ( RunPolicyChecking RunStatus = "policy_checking" RunPolicyOverride RunStatus = "policy_override" RunPolicySoftFailed RunStatus = "policy_soft_failed" - RunPreApplyRunning RunStatus = "pre_apply_running" - RunPreApplyCompleted RunStatus = "pre_apply_completed" + RunPostPlanRunning RunStatus = "post_plan_running" + RunPostPlanCompleted RunStatus = "post_plan_completed" ) // RunSource represents a source type of a run. diff --git a/run_task.go b/run_task.go index 38b446034..820d05bd5 100644 --- a/run_task.go +++ b/run_task.go @@ -46,7 +46,7 @@ type RunTask struct { Name string `jsonapi:"attr,name"` URL string `jsonapi:"attr,url"` Category string `jsonapi:"attr,category"` - HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` + HMACKey *string `jsonapi:"attr,hmac-key,omitempty"` Organization *Organization `jsonapi:"relation,organization"` WorkspaceRunTasks []*WorkspaceRunTask `jsonapi:"relation,workspace-tasks"` @@ -76,7 +76,7 @@ type RunTaskCreateOptions struct { Category string `jsonapi:"attr,category"` // Optional: An HMAC key to verify the run task - HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` + HMACKey *string `jsonapi:"attr,hmac-key,omitempty"` } func (o *RunTaskCreateOptions) valid() error { @@ -207,7 +207,7 @@ type RunTaskUpdateOptions struct { Category *string `jsonapi:"attr,category,omitempty"` // Optional: An HMAC key to verify the run task - HmacKey *string `jsonapi:"attr,hmac-key,omitempty"` + HMACKey *string `jsonapi:"attr,hmac-key,omitempty"` } func (o *RunTaskUpdateOptions) valid() error { diff --git a/run_task_integration_test.go b/run_task_integration_test.go index d775c04a8..1abd9fd19 100644 --- a/run_task_integration_test.go +++ b/run_task_integration_test.go @@ -89,7 +89,7 @@ func TestRunTasksRead(t *testing.T) { assert.Equal(t, runTaskTest.ID, r.ID) assert.Equal(t, runTaskTest.URL, r.URL) assert.Equal(t, runTaskTest.Category, r.Category) - assert.Equal(t, runTaskTest.HmacKey, r.HmacKey) + assert.Equal(t, runTaskTest.HMACKey, r.HMACKey) }) t.Run("with options", func(t *testing.T) { From 4522417e7f2fb8cf137860be42b206d6fe9828b9 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Wed, 16 Feb 2022 18:18:06 -0500 Subject: [PATCH 29/34] Added build tags for tests --- run_task_integration_test.go | 3 +++ task_stages_integration_test.go | 3 +++ workspace_run_task_integration_test.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/run_task_integration_test.go b/run_task_integration_test.go index 1abd9fd19..11b1b86b6 100644 --- a/run_task_integration_test.go +++ b/run_task_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + package tfe import ( diff --git a/task_stages_integration_test.go b/task_stages_integration_test.go index 259fbd6de..b59d750df 100644 --- a/task_stages_integration_test.go +++ b/task_stages_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + package tfe import ( diff --git a/workspace_run_task_integration_test.go b/workspace_run_task_integration_test.go index f54981a81..234033588 100644 --- a/workspace_run_task_integration_test.go +++ b/workspace_run_task_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + package tfe import ( From 76c5aa4604e6816d397b6829920b028cc3e7fd02 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Wed, 16 Feb 2022 15:23:32 -0700 Subject: [PATCH 30/34] fix: vscode go settings for test files This should fix the issue of the go language server not being able to give editor feedback in *_test.go files. All our test files use a separate build flag, presumably to differentiate from unit tests. --- .vscode/settings.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..404211a73 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "go.buildFlags": [ + "-tags=integration" + ], + "go.testTags": "integration", +} From b009229392c6576c67d834c2237ab7a7d59d160f Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Thu, 17 Feb 2022 11:19:38 -0700 Subject: [PATCH 31/34] add CONTRIBUTING.md with language server hints --- CONTRIBUTING.md | 11 +++++++++++ README.md | 9 ++++----- 2 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..3f68bdd76 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,11 @@ +# Contributing to go-tfe + +If you find an issue with this package, please create an issue in GitHub. If you'd like, we welcome any contributions. Fork this repository and submit a pull request. + +### Writing Tests + +The test suite contains many acceptance tests that are run against the latest version of Terraform Enterprise. You can read more about running the tests against your own Terraform Enterprise environment in [TESTS.md](TESTS.md). Our CI system (Circle) will not test your fork unless you are an authorized employee, so a HashiCorp maintainer will initiate the tests or you and report any missing tests or simple problems. In order to speed up this process, it's not uncommon for your commits to be incorportated into another PR that we can commit test changes to. + +### Editor Settings + +We've included VSCode settings to assist with configuring the go extension. For other editors that integrate with the [Go Language Server](https://github.com/golang/tools/tree/master/gopls), the main thing to do is to add the `integration` build tags so that the test files are found by the language server. See `.vscode/settings.json` for more details. diff --git a/README.md b/README.md index 0becaa832..1c4a28dc1 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,11 @@ See the [examples directory](https://github.com/hashicorp/go-tfe/tree/main/examp ## Running tests -See [TESTS.md](https://github.com/hashicorp/go-tfe/tree/main/TESTS.md). +See [TESTS.md](TESTS.md). ## Issues and Contributing -If you find an issue with this package, please report an issue. If you'd like, -we welcome any contributions. Fork this repository and submit a pull request. +See [CONTRIBUTING.md](CONTRIBUTING.md) ## Releases @@ -80,12 +79,12 @@ Documentation updates and test fixes that only touch test files don't require a ### Creating a release 1. [Create a new release in GitHub](https://help.github.com/en/github/administering-a-repository/creating-releases) by clicking on "Releases" and then "Draft a new release" -1. Set the `Tag version` to a new tag, using [Semantic Versioning](https://semver.org/) as a guideline. +1. Set the `Tag version` to a new tag, using [Semantic Versioning](https://semver.org/) as a guideline. 1. Set the `Target` as `main`. 1. Set the `Release title` to the tag you created, `vX.Y.Z` 1. Use the description section to describe why you're releasing and what changes you've made. You should include links to merged PRs. Use the following headers in the description of your release: - BREAKING CHANGES: Use this for any changes that aren't backwards compatible. Include details on how to handle these changes. - - FEATURES: Use this for any large new features added, + - FEATURES: Use this for any large new features added, - ENHANCEMENTS: Use this for smaller new features added - BUG FIXES: Use this for any bugs that were fixed. - NOTES: Use this section if you need to include any additional notes on things like upgrading, upcoming deprecations, or any other information you might want to highlight. From 9406f9abbc8656a5483f398c1c5d913509a88324 Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Fri, 18 Feb 2022 09:31:13 -0700 Subject: [PATCH 32/34] initial scaffolding code example --- CONTRIBUTING.md | 263 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 261 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f68bdd76..596166ae6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,10 +2,269 @@ If you find an issue with this package, please create an issue in GitHub. If you'd like, we welcome any contributions. Fork this repository and submit a pull request. -### Writing Tests +## Writing Tests The test suite contains many acceptance tests that are run against the latest version of Terraform Enterprise. You can read more about running the tests against your own Terraform Enterprise environment in [TESTS.md](TESTS.md). Our CI system (Circle) will not test your fork unless you are an authorized employee, so a HashiCorp maintainer will initiate the tests or you and report any missing tests or simple problems. In order to speed up this process, it's not uncommon for your commits to be incorportated into another PR that we can commit test changes to. -### Editor Settings +## Editor Settings We've included VSCode settings to assist with configuring the go extension. For other editors that integrate with the [Go Language Server](https://github.com/golang/tools/tree/master/gopls), the main thing to do is to add the `integration` build tags so that the test files are found by the language server. See `.vscode/settings.json` for more details. + +## Adding a new Endpoint + +Here you will find a scaffold to get you started when building a json:api RESTful endpoint. The comments are meant to guide you but should be replaced with endpoint-specific and type-specific documentation. Additionally, you'll need to add an integration test that covers each method of the main interface. + +In general, an interface should cover one RESTful resource, which sometimes involves two or more endpoints. Add all new modules to the tfe package. + +```go +package tfe + +import ( + "context" + "errors" + "fmt" + "net/url" +) + +var ErrInvalidExampleID = errors.New("invalid value for example ID") // move this line to errors.go + +// Compile-time proof of interface implementation +var _ ExampleResource = (*example)(nil) + +// Example represents all the example methods in the context of an organization +// that the Terraform Cloud/Enterprise API supports. +// If this API is in beta or pre-release state, include that warning here. +type ExampleResource interface { + // Create an example for an organization + Create(ctx context.Context, organization string, options ExampleCreateOptions) (*Example, error) + + // List all examples for an organization + List(ctx context.Context, organization string, options *ExampleListOptions) (*ExampleList, error) + + // Read an organization's example by ID + Read(ctx context.Context, exampleID string) (*Example, error) + + // Read an organization's example by ID with given options + ReadWithOptions(ctx context.Context, exampleID string, options *ExampleReadOptions) (*Example, error) + + // Update an example for an organization + Update(ctx context.Context, exampleID string, options ExampleUpdateOptions) (*Example, error) + + // Delete an organization's example + Delete(ctx context.Context, exampleID string) error +} + +// example implements Example +type example struct { + client *Client +} + +// Example represents a TFC/E example resource +type Example struct { + ID string `jsonapi:"primary,tasks"` + Name string `jsonapi:"attr,name"` + URL string `jsonapi:"attr,url"` + OptionalValue *string `jsonapi:"attr,optional-value,omitempty"` + + Organization *Organization `jsonapi:"relation,organization"` +} + +// ExampleList represents a list of examples +type ExampleList struct { + *Pagination + Items []*Example +} + +// ExampleCreateOptions represents the set of options for creating an example +type ExampleCreateOptions struct { + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,tasks"` + + // Required: The name of the example + Name string `jsonapi:"attr,name"` + + // Required: The URL to send in the example + URL string `jsonapi:"attr,url"` + + // Optional: An optional value that is omitted if empty + OptionalValue *string `jsonapi:"attr,optional-value,omitempty"` +} + +func (o *ExampleCreateOptions) valid() error { + if !validString(&o.Name) { + return ErrRequiredName + } + + if !validString(&o.URL) { + return ErrInvalidRunTaskURL + } + + return nil +} + +// Create is used to create a new example for an organization +func (s *example) Create(ctx context.Context, organization string, options ExampleCreateOptions) (*Example, error) { + if !validStringID(&organization) { + return nil, ErrInvalidOrg + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) + req, err := s.client.newRequest("POST", u, &options) + if err != nil { + return nil, err + } + + r := &Example{} + err = s.client.do(ctx, req, r) + if err != nil { + return nil, err + } + + return r, nil +} + +// A list of relations to include with an example. See available resources: +// https://www.terraform.io/cloud-docs/api-docs/examples#list-examples (replace this URL with the actual documentation URL) +type ExampleIncludeOps string + +const ( + ExampleOrganization ExampleIncludeOps = "organization" +) + +// ExampleListOptions represents the set of options for listing examples +type ExampleListOptions struct { + ListOptions + + // A list of relations to include + Include []ExampleIncludeOps `url:"include,omitempty"` +} + +// List all the examples for an organization +func (s *example) List(ctx context.Context, organization string, options *ExampleListOptions) (*ExampleList, error) { + if !validStringID(&organization) { + return nil, ErrInvalidOrg + } + + u := fmt.Sprintf("organizations/%s/examples", url.QueryEscape(organization)) + req, err := s.client.newRequest("GET", u, &options) + if err != nil { + return nil, err + } + + el := &ExampleList{} + err = s.client.do(ctx, req, el) + if err != nil { + return nil, err + } + + return el, nil +} + +// Read is used to read an organization's example by ID +func (s *example) Read(ctx context.Context, exampleID string) (*Example, error) { + return s.ReadWithOptions(ctx, exampleID, nil) +} + +// ExampleReadOptions represents the set of options for reading an example +type ExampleReadOptions struct { + Include []RunTaskIncludeOps `url:"include,omitempty"` +} + +// Read is used to read an organization's example by ID with options +func (s *example) ReadWithOptions(ctx context.Context, exampleID string, options *ExampleReadOptions) (*Example, error) { + if !validStringID(&exampleID) { + return nil, ErrInvalidExampleID + } + + u := fmt.Sprintf("examples/%s", url.QueryEscape(exampleID)) + req, err := s.client.newRequest("GET", u, options) + if err != nil { + return nil, err + } + + e := &Example{} + err = s.client.do(ctx, req, e) + if err != nil { + return nil, err + } + + return e, nil +} + +// ExampleUpdateOptions represents the set of options for updating an organization's examples +type ExampleUpdateOptions struct { + // Type is a public field utilized by JSON:API to + // set the resource type via the field tag. + // It is not a user-defined value and does not need to be set. + // https://jsonapi.org/format/#crud-creating + Type string `jsonapi:"primary,tasks"` + + // Optional: The name of the example, defaults to previous value + Name *string `jsonapi:"attr,name,omitempty"` + + // Optional: The URL to send a example payload, defaults to previous value + URL *string `jsonapi:"attr,url,omitempty"` + + // Optional: An optional value + OptionalValue *string `jsonapi:"attr,optional-value,omitempty"` +} + +func (o *ExampleUpdateOptions) valid() error { + if o.Name != nil && !validString(o.Name) { + return ErrRequiredName + } + + if o.URL != nil && !validString(o.URL) { + return ErrInvalidRunTaskURL + } + + return nil +} + +// Update an existing example for an organization by ID +func (s *example) Update(ctx context.Context, exampleID string, options ExampleUpdateOptions) (*Example, error) { + if !validStringID(&exampleID) { + return nil, ErrInvalidExampleID + } + + if err := options.valid(); err != nil { + return nil, err + } + + u := fmt.Sprintf("examples/%s", url.QueryEscape(exampleID)) + req, err := s.client.newRequest("PATCH", u, &options) + if err != nil { + return nil, err + } + + r := &Example{} + err = s.client.do(ctx, req, r) + if err != nil { + return nil, err + } + + return r, nil +} + +// Delete an existing example for an organization by ID +func (s *example) Delete(ctx context.Context, exampleID string) error { + if !validStringID(&exampleID) { + return ErrInvalidExampleID + } + + u := fmt.Sprintf("examples/%s", exampleID) + req, err := s.client.newRequest("DELETE", u, nil) + if err != nil { + return err + } + + return s.client.do(ctx, req, nil) +} +``` From 7e9d5365a96e63d16acf483723462ae3299841aa Mon Sep 17 00:00:00 2001 From: Brandon Croft Date: Fri, 18 Feb 2022 11:16:11 -0700 Subject: [PATCH 33/34] don't need address of options in List methods --- CONTRIBUTING.md | 2 +- run_task.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 596166ae6..910bc3fa7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -153,7 +153,7 @@ func (s *example) List(ctx context.Context, organization string, options *Exampl } u := fmt.Sprintf("organizations/%s/examples", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, &options) + req, err := s.client.newRequest("GET", u, options) if err != nil { return nil, err } diff --git a/run_task.go b/run_task.go index 820d05bd5..0f764aace 100644 --- a/run_task.go +++ b/run_task.go @@ -144,7 +144,7 @@ func (s *runTasks) List(ctx context.Context, organization string, options *RunTa } u := fmt.Sprintf("organizations/%s/tasks", url.QueryEscape(organization)) - req, err := s.client.newRequest("GET", u, &options) + req, err := s.client.newRequest("GET", u, options) if err != nil { return nil, err } From f0881ce9816d2e229edd148c3b7a8311a54543ff Mon Sep 17 00:00:00 2001 From: Lauren Date: Fri, 18 Feb 2022 12:19:12 -0600 Subject: [PATCH 34/34] prefix constants to prevent export --- tfe.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tfe.go b/tfe.go index 19b209275..517769ada 100644 --- a/tfe.go +++ b/tfe.go @@ -28,10 +28,10 @@ import ( ) const ( - userAgent = "go-tfe" - headerRateLimit = "X-RateLimit-Limit" - headerRateReset = "X-RateLimit-Reset" - headerAPIVersion = "TFP-API-Version" + _userAgent = "go-tfe" + _headerRateLimit = "X-RateLimit-Limit" + _headerRateReset = "X-RateLimit-Reset" + _headerAPIVersion = "TFP-API-Version" // DefaultAddress of Terraform Enterprise. DefaultAddress = "https://app.terraform.io" @@ -81,7 +81,7 @@ func DefaultConfig() *Config { } // Set the default user agent. - config.Headers.Set("User-Agent", userAgent) + config.Headers.Set("User-Agent", _userAgent) return config } @@ -367,8 +367,8 @@ func rateLimitBackoff(min, max time.Duration, attemptNum int, resp *http.Respons // First create some jitter bounded by the min and max durations. jitter := time.Duration(rnd.Float64() * float64(max-min)) - if resp != nil && resp.Header.Get(headerRateReset) != "" { - v := resp.Header.Get(headerRateReset) + if resp != nil && resp.Header.Get(_headerRateReset) != "" { + v := resp.Header.Get(_headerRateReset) reset, err := strconv.ParseFloat(v, 64) if err != nil { log.Fatal(err) @@ -421,8 +421,8 @@ func (c *Client) getRawAPIMetadata() (rawAPIMetadata, error) { } resp.Body.Close() - meta.APIVersion = resp.Header.Get(headerAPIVersion) - meta.RateLimit = resp.Header.Get(headerRateLimit) + meta.APIVersion = resp.Header.Get(_headerAPIVersion) + meta.RateLimit = resp.Header.Get(_headerRateLimit) return meta, nil }